How to use jQuery Deferred functionality instead of async.waterfall? - javascript

I have a chain of function calls and use async.waterfall. It works like a charm. But I'd like to do it with jQuery Deferred. How to transform my code?
The example from jQuery site is like this. Both results are passed to done function:
$.when( $.ajax( "/page1.php" ), $.ajax( "/page2.php" ) ).done(function( a1, a2 ) {
// a1 and a2 are arguments resolved for the page1 and page2 ajax requests, respectively.
// Each argument is an array with the following structure: [ data, statusText, jqXHR ]
var data = a1[ 0 ] + a2[ 0 ]; // a1[ 0 ] = "Whip", a2[ 0 ] = " It"
if ( /Whip It/.test( data ) ) {
alert( "We got what we came for!" );
}
});
But my code is different. I need to pass a callback to every step of a waterfall and I have ifs in callbacks. How to implement it with jQuery? Is it possible?
async.waterfall([
function(cb) {
VK.Api.call('users.get', {user_ids: res.session.mid, fields: fields}, function(userDataRes) {
cb(null, userDataRes);
});
},
function(userDataRes, cb) {
if(userDataRes.response[0].city) {
VK.Api.call('database.getCitiesById', {city_ids: userDataRes.response[0].city}, function(cityDataRes) {
cb(null, userDataRes, {city: cityDataRes.response[0].name});
});
}
else {
cb(null, userDataRes, {});
}
},
function(userDataRes, cityDataRes, cb) {
if(userDataRes.response[0].country) {
VK.Api.call("database.getCountriesById", {country_ids: userDataRes.response[0].country}, function(countryDataRes) {
cb(null, userDataRes, cityDataRes, {country: countryDataRes.response[0].name});
});
}
else {
cb(null, userDataRes, {}, {});
}
},
function(userDataRes, cityDataRes, countryDataRes, cb) {
var resObj = $.extend(true, {}, userDataRes.response[0], cityDataRes, countryDataRes);
cb(null, resObj);
},
],
function(err, res) {
console.log("res::: ", res);
}
);
UPD 1:
So, I've implemented a solution, but it doesn't work as expected. There is an asynchronous API function call in .then() and jQuery deferred flow is broken there. I don't know how to make a .then() function as an API callback.
var dfr = $.Deferred();
dfr.then(function(val) {
// THIS is an asynchronous API function call. And its callback returns result that is passed to the next .then()
// But jQuery deferred flow doesn't follow this API call.
// It goes along to the next .then ignoring this API call.
// How to make it enter this API call and be returned from a API's callback.
VK.Api.call('users.get', {user_ids: res.session.mid, fields: fields}, function(userDataRes) {
// cb(null, userDataRes);
console.log("countryDataRes: ", userDataRes);
return userDataRes;
});
}).
then(function(userDataRes) {
console.log("countryDataRes: ", userDataRes);
if(userDataRes.response[0].city) {
VK.Api.call('database.getCitiesById', {city_ids: userDataRes.response[0].city}, function(cityDataRes) {
// cb(null, userDataRes, {city: cityDataRes.response[0].name});
return [userDataRes, {city: cityDataRes.response[0].name}];
});
}
else {
// cb(null, userDataRes, {});
return [userDataRes, {}];
}
}).
then(function(aRes) {
if(aRes[0].response[0].country) {
VK.Api.call("database.getCountriesById", {country_ids: aRes[0].response[0].country}, function(countryDataRes) {
// cb(null, userDataRes, cityDataRes, {country: countryDataRes.response[0].name});
return [aRes[0], aRes[1], {country: countryDataRes.response[0].name}];
});
}
else {
cb(null, aRes[0], {}, {});
}
}).
then(function(aRes) {
var resObj = $.extend(true, {}, aRes[0].response[0], aRes[1], aRes[2]);
console.log("cityDataRes: ", aRes[1]);
console.log("countryDataRes: ", aRes[2]);
cb(null, resObj);
return resObj;
}).
done(function(res) {
console.log("res::: ", res);
});
dfr.resolve();

Let's start with the general rule for using promises:
Every function that does something asynchronous must return a promise
Which functions are these in your case? Basically, the complete waterfall, each of the waterfall functions that took a cb and VK.Api.call.
Hm, VK.Api.call doesn't return a promise, and it's a library function so we cannot modify it. Rule 2 comes into play:
Create an immediate wrapper for every function that doesn't
In our case, it will look like this:
function callApi(method, data) {
var dfr = $.Deferred();
VK.Api.call(method, data, function(result) {
dfr.resolve(result);
});
// No error callbacks? That's scary!
// If it does offer one, call `dfr.reject(err)` from it
return dfr.promise();
}
Now we have only promises around, and do no more need any deferreds. Third rule comes into play:
Everything that does something with an async result goes into a .then callback
…and returns its result.
That result might as well be a promise not a plain value, .then can handle these - and will give us back a new promise for the eventual result of executing the "something". So, let's chain some then()s:
apiCall('users.get', {user_ids: res.session.mid, fields: fields})
.then(function(userDataRes) {
console.log("countryDataRes: ", userDataRes);
if (userDataRes.response[0].city) {
return apiCall('database.getCitiesById', {city_ids: userDataRes.response[0].city})
.then(function(cityDataRes) {
return [userDataRes, {city: cityDataRes.response[0].name}];
});
} else {
return [userDataRes, {}];
}
})
.then(function(aRes) {
if (aRes[0].response[0].country) {
return apiCall("database.getCountriesById", {country_ids: aRes[0].response[0].country})
.then(function(countryDataRes) {
return [aRes[0], aRes[1], {country: countryDataRes.response[0].name}];
});
} else {
return [aRes[0], aRes[1], {}];
}
})
.then(function(aRes) {
var resObj = $.extend(true, {}, aRes[0].response[0], aRes[1], aRes[2]);
console.log("cityDataRes: ", aRes[1]);
console.log("countryDataRes: ", aRes[2]);
return resObj;
})
.done(function(res) {
console.log("res::: ", res);
});
At least, that's what your original waterfall did. Let's polish it up a bit by executing getCitiesById and getCountriesById in parallel, and removing all the boilerplate of explicitly creating these aRes arrays.
function callApi(method, data) {
var dfr = $.Deferred();
VK.Api.call(method, data, function(result) {
dfr.resolve(result.response[0]);
// changed: ^^^^^^^^^^^^
});
// No error callbacks? That's scary!
// If it does offer one, call `dfr.reject(err)` from it
return dfr.promise();
}
apiCall('users.get', {user_ids: res.session.mid, fields: fields})
.then(function(userData) {
if (userData.city)
var cityProm = apiCall('database.getCitiesById', {city_ids: userData.city});
if (userData.country)
var countryProm = apiCall("database.getCountriesById", {country_ids: userData.country});
return $.when(cityProm, countrProm).then(function(city, country) {
var resObj = $.extend(true, {}, userData);
if (city)
resObj.city = city.name;
if (country)
resObj.country = country.name;
return resObj;
});
})
.done(function(res) {
console.log("res::: ", res);
});

Related

Follow on actions after asynch function completion

I'm going to do my best to explain this one since this is a reduced version of the real code and some parts might seem redundant or excessive (and they might be). The model can be found at https://jsfiddle.net/ux5t7ykm/1/. I am using a function to build an array of values that are passed to another function to do a REST call for the data that corresponds to the values. The array lists the name of the list to get the data from, the field name, the filter field name, the filter value, and the target array to pass the autocomplete values back to. This part is working fine and is essentially what Step1() does. setAutocomplete() then uses getData() to make the multiple REST calls. I'm using sprLib for that part so it's already a promise (this is getListItems() normally). getData() is where the magic is happening. It recursively cycles through the list and performs getListItems() until all the data is retrieved. I have this set as an async function with await. This also seems to be running fine.
The problem I have is that once this whole sequence is complete, I then have another function that I need to run. It manipulates the data from the previous function. I've tried to put step1() in a promise, make it async, etc. Nothing seems to work. CheatStep() seems to be where a response or some other method signaling to Step1() should go to then allow the next function to run. I've been banging my head against this all week and I feel like my understanding of promises and asnyc is regressing.
var dict = [{
'id': '1',
'title': 'test1'
}, {
'id': '2',
'title': 'test2'
}, {
'id': '3',
'title': 'test3'
}, {
'id': '4',
'title': 'test4'
}, ]
step1()
nextStep()
function nextStep() {
console.log('I run after Step1')
}
function cheatStep() {
console.log('I cheated')
}
function step1() {
setAutoComplete(4);
}
function setAutoComplete(listIndex) {
getData(listIndex,
function(result) {
for (var i = 0; i < listIndex; i++) {
var autoDict = result[i];
console.log(autoDict)
}
cheatStep()
},
function(e) {
console.error(e);
alert('An error occurred wile setting autocomplete data for item');
})
}
async function getData(listIndex, success, error, curIndex, result) {
var curIndex = curIndex || 0;
var result = result || {};
await getListItems(curIndex,
function(listItems) {
result[curIndex] = listItems;
curIndex++;
if (listIndex > curIndex) {
getData(listIndex, success, error, curIndex, result);
console.log('1');
} else {
console.log('2');
success(result);
}
},
error)
}
function getListItems(curIndex, success, error) {
new Promise(
function(resolve, reject) {
var x = dict[curIndex];
resolve(x);
})
.then(function(x) {
success(x);
});
}
I have adopted your code to a more async/await view coz you still use heavily callback approach:
var dict = [{
'id': '1',
'title': 'test1'
}, {
'id': '2',
'title': 'test2'
}, {
'id': '3',
'title': 'test3'
}, {
'id': '4',
'title': 'test4'
}, ]
function nextStep(result) {
console.log('I run after Step1',result)
}
function cheatStep() {
console.log('I cheated')
}
step1().then((result)=>{nextStep(result)})
async function step1() {
return await setAutoComplete(4);
}
async function setAutoComplete(listIndex) {
const result = await getData(listIndex);
for (var i = 0; i < listIndex; i++) {
var autoDict = result[i];
console.log(autoDict)
}
cheatStep()
return result
}
async function getData(listIndex, curIndex=0, result={}) {
const listItems = await getListItems(curIndex)
result[curIndex] = listItems;
curIndex++;
if (listIndex > curIndex) {
return await getData(listIndex, curIndex, result);
} else {
console.log('2');
return (result);
}
}
function getListItems(curIndex) {
return new Promise(
(resolve, reject) =>{
resolve(dict[curIndex]);
})
}
I also kept recursion here, but personally I would prefer solution with a loop instead.
Also a minor advise. Try to avoid write declaration after usage:
nextStep()
function nextStep() {
console.log('I run after Step1')
}
JS hoisting does that a valid syntax but it is really hard to read=)

Callback not waiting for function to finish executing

I have a callback function that returns an object from the database. However, in my async.waterfall the function 'external' does not wait for the object to be fully loaded meaning it is undefined when passed in. This means my final error is TypeError: Cannot read property 'replace' of undefined. What am I doing wrong?
function loadModelInstance (name, callback) {
Model.findOne({ name: name }, function (_err, result) {
if (result) {
return callback(_err, result.content)
}
})
}
function generatedNow (modelInstance) {
generatedKeys = generatedKeys.concat(getAllMatches(generatedRegexp, modelInstance.replace(/(\n|\r)/g, '')));
}
async.waterfall(
[
function loadTemplate (wfaCallback) {
loadModelInstance(name, function (_err, modelInstance) {
wfaCallback(_err, modelInstance)
})
},
function external (modelInstance, wfaCallback) {
generatedNow(tracking, message, modelInstance, placeholders, function (err, updatedPlaceholders) {
})
},
],
function (err) {
// Node.js and JavaScript Rock!
}
);
Could you please provide more details. where are you calling "generateNow" function. i don't see function call for "generateNow".
Looks like you haven't used the parameter order properly. Below code should work.
async.waterfall(
[
function loadTemplate(wfaCallback) {
loadModelInstance(name, function(_err, modelInstance) {
wfaCallback(_err, modelInstance);
});
},
function external(err, modelInstance, wfaCallback) {
generatedNow(modelInstance, tracking, message, placeholders, function(
err,
updatedPlaceholders
) {});
}
],
function(err) {
// Node.js and JavaScript Rock!
}
);

Return promises in order

I'm facing an issue to return promises using $q#all method.
I want to make promises dependent on each other, i.e.:
If I set obj1, obj2 and obj3 I want to get them in the same order.
How can I achieve this?
Factory:
mainFactory.$inject = ['$http', '$q'];
function mainFactory($http, $q) {
var mainFactory = {
getPromises: getPromises
};
return mainFactory;
function getPromises(id) {
promises = {
'obj1': $http.get('http1'),
'obj2': $http.get('http2'),
'obj3': $http.get('http3'),
'obj4': $http.get('http4', { params: { 'id': id } }),
'obj5': $http.get('http5'),
'obj6': $http.get('http6', { params: { 'id': id } })
};
return $q.all(promises);
}
}
Controller:
MainCtrl.$inject = ['mainFactory'];
function MainCtrl(mainFactory) {
var vm = this;
mainFactory.getPromises(id)
.then(getResponse)
.catch(getError);
function getResponse(response) {
var keys = Object.keys(response), i = keys.length;
while (i--) {
var key = keys[i];
console.log(key); // I want all the keys in order, i.e. => obj1, obj2.. and so on
var value = response[key].data;
switch(key) {
...
}
}
}
function getError(error) {
console.log(error);
}
}
EDIT:
I tried this way also:
var promises = {};
return $http.get('/admin/http1.json').then(function (value) {
promises['obj1'] = value;
})
.then(function (result) {
return $http.get('/admin/http2.json').then(function (value) {
promises['obj2'] = value;
});
}).then(function (result) {
return $http.get('/admin/http3.json').then(function (value) {
promises['obj3'] = value;
});
});
return $q.all(promises);
Using $q.all will resolve each promise in no particular order. If you want them to execute after each promise has been resolve, use promise chaining.
function getPromises(id) {
var getObjA = function () {
return $http.get('http1');
};
var getObjB = function () {
return $http.get('http2');
};
var getObjC = function () {
return $http.get('http3');
};
var getObjD = function () {
return $http.get('http4', { params: { 'id': id } });
};
var getObjE = function () {
return $http.get('http5');
};
var getObjF = function () {
return $http.get('http5', { params: { 'id': id } });
};
return getObjA()
.then(getObjB)
.then(getObjC)
.then(getObjD)
.then(getObjE)
.then(getObjF);
}
Edit: as an additional info, you can catch any error in any of those promise by placing a catch statement here
getPromises("id")
.then(<success callback here>)
.catch(<error callback that will catch error on any of the promises>);
Meaning, once a promise fails, all the succeeding promises below wouldn't be executed and will be caught by your catch statement
Edit 2
Mistake, I just copied you code above without realizing it was an object. LOL.
promises = [
$http.get('http1'),
$http.get('http2'),
$http.get('http3'),
$http.get('http4', { params: { 'id': id } }),
$http.get('http5'),
$http.get('http6', { params: { 'id': id } })
]
Edit 1
Sorry I didn't notice the comments Jared Smith is correct.
Object keys are inherently unordered. Use an array instead.
Edit 0
Object keys wont be ordered. Use array on declaring your promises.
promises = [
$http.get('http1'),
$http.get('http2'),
$http.get('http3'),
$http.get('http4', { params: { 'id': id } }),
$http.get('http5'),
$http.get('http6', { params: { 'id': id } })
]
$q.all(promises)
.then(functions(resolves){
// resolves here is an array
}).catch(function(err){
// throw err
});

Control when a `.then()` callback fires

I have a long chain of promises that wind through a module of my code. I don't know beforehand how many promises I'll wind through, nor do I have scope from any one promise to any other other promise (meaning I can't do a Promise.join()).
My problem is that I have then() callbacks attached to multiple promises on this chain. How can I control which one is fired last?
UPDATE: Here's a simplified example:
var aFn = function () {
return bFn().then(someFn);
};
var bFn = function () {
return new Promise(function (resolve, reject) {
if (a) return resolve();
else return reject();
});
};
aFn().then(anotherFn);
My problem is that the .then() in aFn().then(anotherFn) gets called before bFn().then(someFn).
Here are a few code snippets that helps illustrate my issue:
strategy.js
execute: function (model, options) {
options.url = this.policy.getUrl(model, this.method, options);
options.collection = this.policy.getCollection(model, options);
options.model = model;
// This is a Promise that eventually calls get()
return this.sync(model, options);
},
get: function (key, options) {
var updateCollection = this._getUpdateCollection(options);
var getFromCache = _.bind(this.store.get, this.store, key, options);
if (updateCollection) {
// updateCollection received a promise from store-helpers.js:proxyGetItem()
return updateCollection().then(
function (collection) {
var modelResponse = collection.policy.findSameModel(collection.raw, options.model);
return modelResponse ? modelResponse : getFromCache();
},
getFromCache
);
} else {
return getFromCache();
}
},
_getUpdateCollection: function (options) {
var collection = options && options.collection;
var collectionControl = collection && collection.sync && collection.sync.hoardControl;
if (collection && collectionControl) {
var collectionKey = collectionControl.policy.getKey(collection, options);
return _.bind(function () {
// store.get() passes the returned promise of store-helpers.js:proxyGetItem()
return this.store.get(collectionKey, options).then(function (rawCollection) {
return {
control: collectionControl,
policy: collectionControl.policy,
key: collectionKey,
raw: rawCollection
};
});
}, this);
}
},
store.js
// Just a proxy function that wraps a synchronous get
get: function (key, options) {
return this.getItem.apply(this, arguments);
},
getItem: StoreHelpers.proxyGetItem
store-helpers.js
proxyGetItem: function (key, options) {
return Hoard.Promise.resolve()
.then(_.bind(function () {
return this.backend.getItem(key);
}, this))
.then(function (raw) {
var storedValue = JSON.parse(raw);
if (storedValue !== null) {
return storedValue;
} else {
return Hoard.Promise.reject();
}
});
},
In a very different part of the app I also have:
var originalExecute = Hoard.Strategy.prototype.execute;
Hoard.Strategy.prototype.execute = function (model, options) {
options.originalOptions = _.clone(options);
if (options.saveToCacheFirst)
return originalExecute.call(this, model, options)
.then(_.result(options.originalOptions, 'success'), _.result(options.originalOptions, 'error'));
return originalExecute.call(this, model, options);
}
I would like for the .then() above to fire last, however when .resolve in store-helpers.js is fired, this last .then() callback is invoked.
My problem is that the .then() in aFn().then(anotherFn) gets called before bFn().then(someFn)
No it doesn't. As you've written the example, that expression is equivalent to
bFn().then(someFn).then(anotherFn)
- and while the .then() method does get called before someFn, the anotherFn callback does not.

How to delay a promise until async.each completes?

How do I delay a promise until an asynchronous operation has completed? I am using async and bluebird libraries. As soon as I boot up my program, the done() function returns an error that is an empty or nearly empty 'masterlist' object. Why isn't async waiting until the iterator has finished its operation?
// bundler.js
var masterlist = {
"children": []
, "keywords": []
, "mentions": 0
, "name" : "newsfeed"
, "size" : 0
}
// initialize() returns a promise with the populated masterlist
exports.initialize = function() {
return new Promise(function(resolve, reject) {
// pullBreakingNews() returns a promise with the breaking news articles
nytimes.pullBreakingNews().then(function(abstracts) {
async.map(abstracts, iterator, done);
function iterator(item, callback) {
alchemyapi.entities('text', item, {}, function(response) {
// initialize each entity with masterlist
response.entities.forEach(function(entity) {
masterlist.children[masterlist.children.length] =
{
"abstract": item
, "children": []
, "name": entity.text
, "size": 0
};
masterlist.size += 1;
masterlist.keywords.push(entity.text);
});
callback(masterlist);
});
};
function done(err, results) {
if (err) {
console.log("ERROR: ", err);
} else {
resolve(results);
}
};
});
});
};
Firehose.js is the module that calls initializer(). I believe that firehose gets run first, and the promise is called in the process.
server.js => firehose.js => bundler.js => nytimes api
// firehose.js
// Compares entities to Twitter stream, counts every match
exports.aggregator = function(callback) {
bundler.initialize().then(function(masterlist) {
t.stream('statuses/filter', { track: masterlist.keywords }, function(stream) {
// read twitter firehose for incoming tweets.
stream.on('data', function(tweet) {
var tweetText = tweet.text.toLowerCase();
// sift through each tweet for presence of entities
masterlist.children.forEach(function(parentObject) {
// if the entity exists in the tweet, update counters
if (tweetText.indexOf(parentObject.name.toLowerCase()) !== -1) {
parentObject.size += 1;
masterlist.mentions += 1;
callback(masterlist);
}
});
});
});
});
};
Thanks very much for any help.
Please don't mix callbacks and promises, only use either one.
// Do this somewhere else, it is only needed once
// it adds promise returning versions of all alchemy methods for you
Promise.promisifyAll(require('alchemy-api').prototype);
exports.initialize = function() {
return nytimes.pullBreakingNews().map(function(abstract) {
// Note that it is entitiesAsync that is a promise returning function
return alchemyapi.entitiesAsync('text', abstract, {}).then(function(response){
response.entities.forEach(function(entity) {
masterlist.children[masterlist.children.length] =
{
"abstract": abstract
, "children": []
, "name": entity.text
, "size": 0
};
masterlist.size += 1;
masterlist.keywords.push(entity.text);
});
});
}).return(masterlist);
};
Also your initialize function isn't checking if it is initialized already
The iterator's callback accepts an error as the first argument. You should pass a falsy value (like a null) there instead of masterlist if there's no error.
function iterator(item, callback) {
alchemyapi.entities('text', item, {}, function(response) {
// initialize each entity with masterlist
response.entities.forEach(function(entity) {
masterlist.children[masterlist.children.length] =
{
"abstract": item
, "children": []
, "name": entity.text
, "size": 0
};
masterlist.size += 1;
masterlist.keywords.push(entity.text);
});
callback(null, masterlist);
});
};

Categories

Resources