How to delay a promise until async.each completes? - javascript

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);
});
};

Related

Meteor async function in meteor-method with database update

for the last few hours, I have been trying to get an async method to play nice with meteor methods and its database.
While using wrapAsync works great for simple method calls, I struggling with getting it to work in this case.
Any help would be much appreciated.
https://docs.meteor.com/api/core.html#Meteor-wrapAsync
The async method in question:
chargebee.subscription.list({
limit : 5,
"plan_id[is]" : "basic",
"status[is]" : "active",
"sort_by[asc]" : "created_at"
}).request(function(error,result){
if(error){
//handle error
console.log(error);
}else{
for(var i = 0; i < result.list.length;i++){
var entry=result.list[i]
console.log(entry);
var subscription = entry.subscription;
var customer = entry.customer;
var card = entry.card;
}
}
});
What I tried and didn't work:
try {
var result = Meteor.wrapAsync(chargebee.subscription.list, chargebee.subscription)({
limit: 5,
"customer_id[is]": Meteor.userId(),
"status[is]": "active",
"sort_by[asc]": "created_at"
}).request();
if (result.list[0]) {
const subscription = result.list[0].subscription;
console.log("quantity", subscription.plan_quantity);
Subs.update(
{
owner_id: this.userId
}, {
$set: {
quantity: subscription.plan_quantity
}
}
);
}
} catch(error) {
console.log(error);
}
You should wrap in Meteor.wrapAsync the async method itself. In your code you're wrapping the chargebee.subscription.list only, and it isn't async (based on your example).
You should wrap .request() method instead (not its call):
// Call to `subscription.list()` extracted
// for better understanding
try {
var list = chargebee.subscription.list({
limit: 5,
"customer_id[is]": Meteor.userId(),
"status[is]": "active",
"sort_by[asc]": "created_at"
});
var result = Meteor.wrapAsync(list.request)();
// process result
} catch(error) {
// process error
}

For loop returning null in node js

I am using googles api to get 3 fields,
name rating and place review. Since place review uses another api call I created it as a function to get the data inside my place_review.
response.results.forEach((entry)=>{
var restaurantName = {
"name" : entry.name,
"rating" : entry.rating,
"place_review" : placeReview(entry.place_id)
}
arr.push(restaurantName);
console.log(restaurantName);// shows only name and rating
});
The extra function
function placeReview(place_id){
console.log(place_id) // i see all place id via forloop.
googlePlaces.placeDetailsRequest({placeid: place_id},function(error,response){
if (error) throw error;
return response.result.reviews[0].text;
});
}
When I run the code , i get name : somename, rating : 4.5 and place_review : null
I thought the forloop will call the function and the return of the function will append the string to "place_review" not sure what I am doing wrong.
The placeDetailsRequest api is asynchronous, meaning it is initiated but doesn't complete right away. Consequently, the line:
return response.result.reviews[0].text;
is meaningless because there is nothing to return to.
Your console.log(restaurantName); will be run before the request completes so you will only get an 'undefined'.
To fix this, you will need to do something a fair bit more complicated ( will assume, for now that Promises are something you are not familiar with but that is typically the way people go once they are comfortable with the way asynchronous programming works):
response.results.forEach((entry)=>{
var restaurantName = {
"name" : entry.name,
"rating" : entry.rating,
"place_review" : undefined // will be filled in later
}
placeReview(entry.place_id, restaurantName);
});
function placeReview(place_id, obj){
console.log(place_id) // i see all place id via forloop.
googlePlaces.placeDetailsRequest({placeid: place_id},function(error,response){
if (error) throw error;
obj.place_review = response.result.reviews[0].text;
console.log(obj);
});
}
Of course, you will only see the console results as each request finishes.
This uses a technique called 'callbacks' to manage the information that is only available at a later time. There is lots of information about this online and on SO.
Here's a version that you can run to see the technique in action - very similar to above but hard-wired some data.
var list = [
{ name: 'Aaron', rating: '14', place_id: 21 },
{ name: 'Brad', rating: '33', place_id: 33 }
];
list.forEach(function (entry) {
var restaurantName = {
"name" : entry.name,
"rating" : entry.rating,
"place_review" : undefined // will be filled in later
};
placeReview(entry.place_id, restaurantName);
});
function placeDetailsRequest(idObj, cb) {
setTimeout(function() {
var result = (idObj.placeid === 21) ? 'def' : 'abc';
cb(null, { result: { reviews: [{ text: result}]}});
}, 2000);
}
function placeReview(place_id, obj) {
console.log(place_id); // i see all place id via for loop.
placeDetailsRequest( { placeid: place_id }, function(error,response){
if (error) throw error;
obj.place_review = response.result.reviews[0].text;
console.log(obj);
});
}
Further Edit to Show Promise Solution:
var list = [
{ name: 'Aaron', rating: '14', place_id: 21 },
{ name: 'Brad', rating: '33', place_id: 33 }
];
var allPromises = list.map(function (entry) {
var restaurantName = {
"name" : entry.name,
"rating" : entry.rating,
"place_review" : undefined // will be filled in later
};
return placeReview(entry.place_id, restaurantName);
});
Promise.all(allPromises)
.then(results => console.log(results))
.catch(error => console.log(error));
function placeReview(place_id, obj) {
return new Promise((resolve, reject) => {
// Here's where you would do your google api call to placeDetailsRequest
// if error then reject the promise, otherwise resolve it with your results
//googlePlaces.placeDetailsRequest({placeid: place_id},function(error,response){
// if (error) {
// reject(error);
// } else {
// obj.place_review = response.result.reviews[0].text;
// resolve(obj);
// }
//});
// simulating with delayed response
setTimeout(() => {
var result = (place_id === 21) ? 'def' : 'abc';
obj.place_review = result;
resolve(obj);
}, 2000);
});
}
There are other ways to approach this, but in this solution I am resolving the promise with the completed object. The Promise.all() function then either shows the successful results or the catch will display an error.
Promises
You could use Q in order to implement promises in your place review function:
function placeReview (place_id) {
var deferred = Q.defer();
googlePlaces.placeDetailsRequest({placeid: place_id},function(error,response){
if (error) deferred.reject(err);
else deferred.resolve(response.result.reviews[0].text);
});
return deferred.promise // the promise is returned
}
then you can invoke that function in your processing logic like this:
response.results.forEach((entry)=>{
placeReview(entity.place_id).then(function(text) {
var restaurantName = {
"name" : entry.name,
"rating" : entry.rating,
"place_review" : text
}
arr.push(restaurantName);
console.log(restaurantName);// shows only name and rating
});
});
you will not push the restaurant details till your call to the API is complete.
Async option
You could use async to coordinate your calls:
This function will be executed once per each entry:
function placeReview(entry, callback){
console.log(entry.place_id)
googlePlaces.placeDetailsRequest({entry.placeid: place_id},function(error,response){
if (error) throw error;
//this call back retrives the restaurant entity that async stack in a list
callback(null,{
"name" : entry.name,
"rating" : entry.rating,
"place_review" : response.result.reviews[0].text
});
});
}
Then you ask async to iterate your result array and execute placeReview for each one. Async will gather results in an array:
async.map(response.results, placeReview, function(err, results) {
// results will have the results of all 2
console.log(results); //this is your array of restaurants
});

Javascript Promises: Iterate over all object keys arrays and then resolve

I have this JS object:
let setOfWords = {
"nouns": [
"work",
"construction",
"industry"
],
"verbs": [
"work"
],
}
I'm using the google translate API which calls a REST resource, so I need to wait for the response of each translation and then resolve the same object structure but with the translated words.
function translateByCategory(){
let translatedObj = {};
return new Promise(function(resolve, reject){
Object.keys(obj).forEach(function(category){
if (translatedObj[category] == undefined) {
translatedObj[category] = [];
}
setOfWords.forEach(function(word){
google.translate(word, 'es', 'en').then(function(translation){
translatedObj[category].push(translation.translatedText);
});
});
// return the translatedObj until all of the categories are translated
resolve(translatedObj);
});
});
}
You can use Promise.all() to wait for all promise fulfillments (or first rejection):
var translateRequests = [];
Object.keys(setOfWords).forEach(function(category){
setOfWords[category].forEach(function(word){
translateRequests.push(google.translate(word, 'es', 'en'));
});
});
});
Promise.all(translateRequests).then(function(translateResults){
//do something with all the results
});
See the docs: Promise.all()
Individual promises need to be aggregated with Promise.all() and fundamentally, that's what's missing.
But you can do better by reducing the number of calls to the Google service.
The Google translate API allows multiple text strings to be translated in one hit by passing an array of words instead of one word per call, giving you a performance advantage though probably not price advantage - google currently charges for its translation service "per character", not "per call".
I can't find any documentation for google.translate() but, with a few assumptions, you may be able to write :
function translateByCategory(obj, sourceCode, targetCode) {
let translatedObj = {};
var promises = Object.keys(obj).map(function(key) {
return google.translate(obj[key], sourceCode, targetCode).then(function(translations) {
translatedObj[key] = translations.map(function(t) {
return t.translatedText || '-';
});
}, function(error) {
translatedObj[key] = [];
});
});
return Promise.all(promises).then(function() {
return translatedObj;
});
}
If that doesn't work, then this documentation explains how to call google's RESTful translation service directly.
You should be able to write :
function translateTexts(baseParams, arrayOfStrings) {
let queryString = baseParams.concat(arrayOfStrings.map(function(str) {
return 'q=' + encodeURIComponent(str);
})).join('&');
return http.ajax({ // some arbitrary HTTP lib that GETs by default.
url: 'https://translation.googleapis.com/language/translate/v2?' + queryString,
}).then(function(response) {
return response.data.translations.map(function(t) {
return t.translatedText || '-';
});
}, function(error) {
translatedObj[key] = []; // on error, default to empty array
});
}
function translateByCategory(obj, sourceCode, targetCode) {
let baseParams = [
'key=' + MY_API_KEY, // from some outer scope
'source=' + sourceCode, // eg 'en'
'target=' + targetCode // eg 'es'
];
let translatedObj = {};
let promises = Object.keys(obj).map(function(key) {
return translateTexts(baseParams, obj[key]).then(function(translations) {
translatedObj[key] = translations;
}, function(error) {
translatedObj[key] = []; // on error, default to empty array
});
});
return Promise.all(promises).then(function() {
return translatedObj;
});
}
In either case, call as follows :
let setOfWords = {
"nouns": [
"work",
"construction",
"industry"
],
"verbs": [
"work"
],
};
translateByCategory(setOfWords, 'en', 'es').then(function(setOfTranslatedWords) {
console.log(setOfTranslatedWords);
});
The method suggested by #hackerrdave can be modified to make it more compatible with async await feature of JavaScript, do this like the following:
function translateByCategory(){
return new Promise(function(resolve, reject){
var translateRequests = [];
Object.keys(setOfWords).forEach(function(category){
setOfWords[category].forEach(function(word){
translateRequests.push(google.translate(word, 'es', 'en'));
});
});
Promise.all(translateRequests).resolve(resolve(translateRequests));
});
}
So now you can do something like:
let translatedObj = await translateByCategory();
And you will get what you want in "translatedObj".

JS build object recursively

I am attempting to build a file-structure index using nodeJS. I'm using the fs.readir function to iterate the files, which works fine. My problem is descending into the directory structure and returning a full object with the correct structure.
I have a simple function named identify which, when given file name "myfile.txt" will return an object {name: "myfile", type: "txt"}, which will explain that part of the function below...
My problem is that nothing is being returned when I run the indexer into the "me" variable. The console.log(results) line does return, however. This leaves me quite confused.
Any help would be greatly appreciated!
indexer =
function(directory){
Self.indexleft++;
var results = {};
Self.client.readdir(directory, function(err,fileLst){
if(err){ return; }
for(var count=0; count < fileLst.length; count++){
var ident = identify(fileLst[count]);
if(ident.type = 'dir'){
var descendant = (directory !== '') ?
directory + '\\' + ident.name : ident.name;
ident.children = indexer(descendant);
}
//directory = (directory.split('\\').pop());
results[ident.name] = ident;
}
console.log(results);
return results;
});
}
var me = indexer(''); console.log(me);
EDIT::
I've actually got something working now, though it's not quite as elegant as I'd like. Below is what I did. If anyone has a suggestion on optimizing I'd be happy to hear it!!
Newest (working) Code:
var events = require('events'),
event = new events.EventEmitter(),
setToValue = function(obj, value, path) {
path = path.split('\\');
for (i = 0; i < path.length - 1; i++)
obj = obj[path[i]];
obj[path[i]] = value;
},
identify = function(file){
var split = file.split('.'),
type = (split.length > 1) ? split.pop() : 'dir',
filename = split.join('.');
return { name: filename, type: type };
};
Indexer = function(cli,dir,callback){
this.client = cli; // File reading client
this.startDir = dir; // Starting directory
this.results = {}; // Result object
this.running = 0; // How many itterations of start() are running
this.start(dir); // Start indexing
this.monit(); // Start never returns anything, monit() checks ever 5 seconds and will fire callback if 0 itterations are running.
this.callbackDone = false; // Checks whether the callback has already been fired. Important in case of interval staggering
this.cb = callback;
}
Indexer.prototype = {
start: function(directory){
var Self = this;
Self.running++;
Self.client.readdir(directory, function(err,fileLst){
if(err){ Self.running--; return; }
for(var count=0; count < fileLst.length; count++){
var ident = identify(fileLst[count]);
var descendant = (directory !== '') ? directory + '\\' + ident.name : ident.name;
if(ident.type === 'dir'){
Self.start(descendant);
}
setToValue(Self.results, ident, descendant);
}
Self.running--;
console.log('running' + Self.running);
});
},
monit: function(){
var Self = this;
Self.intervalA = setInterval(function(){
if(Self.running < 1){
if(!Self.callbackDone){
this.callbackDone=true;
Self.cb(Self.results);
}
clearInterval(Self.intervalA);
}
}, 5000)
}
}
var ix = new Indexer(Self.client,'',function(res){
console.log("Index Complete!");
fs.writeFile(path.join(Self.localLibBase,'/index.json'), JSON.stringify(res), (err)=> {
console.log("FileWrite Complete!");
});
});
Example of returned object structure :
{
"Applications" : {
"name" : "Applications",
"type" : "dir",
"Microsoft Exchange Server 2007" : {
"name" : "Microsoft Exchange Server 2007",
"type" : "dir",
"Microsoft Exchange Server 2007 SP1" : {
"name" : "Microsoft Exchange Server 2007 SP1",
"type" : "iso"
}
}
}
}
The result is only available asynchronously, so you are trying to output the result too soon. The inner code is only executed later.
You can solve this in many ways. A very nice solution to working with asynchronous code is using promises.
As you have a recursive call, you'll have to resolve that with promises too.
NB: Note you had a bug in the comparison with "dir": you assigned instead of comparing.
Here is how your code would look:
var indexer = function(directory) {
// return a promise object
return new Promise(function (resolve, reject) {
Self.indexleft++;
var results = {};
Self.client.readdir(directory, function(err,fileLst){
if(err) {
reject(); // promise is rejected
return;
}
// "Iterate" over file list asyonchronously
(function nextFile(fileList) {
if (!fileList.length) {
resolve(results); // promise is resolved
return;
}
var file = fileLst.shift(); // shop off first file
var ident = identify(file);
results[ident.name] = ident;
if(ident.type === 'dir'){ // There was a bug here: equal sign!
var descendant = directory !== ''
? directory + '\\' + ident.name : ident.name;
// recursively call indexer: it is again a promise!
indexer(descendant).then(function (result) {
ident.children = result;
// recursively continue with next file from list
nextFile(fileList);
});
} else {
nextFile(fileLst);
}
})(fileLst); // start first iteration with full list
});
});
};
// Call as a promise. Result is passed async to callback.
indexer('').then(function(me) {
console.log(me);
});
I made some dummy functions for your external references to make this snippet work:
// Below code added to mimic the external references -- can be ignored
var filesystem = [
"",
"images",
"images\\photo.png",
"images\\backup",
"images\\backup\\old_photo.png",
"images\\backup\\removed_pic.jpg",
"images\\panorama.jpg",
"docs",
"docs\\essay.doc",
"readme.txt",
];
var Self = {
indexLeft: 0,
client: {
readdir: function (directory, callback) {
var list = filesystem.filter( path =>
path.indexOf(directory) == 0
&& path.split('\\').length == directory.split('\\').length + (directory!=='')
&& path !== directory
).map ( path => path.split('\\').pop() );
setTimeout(callback.bind(null, 0, list), 100);
}
}
}
function identify(item) {
return {
name: item,
type: item.indexOf('.') > -1 ? 'file' : 'dir'
};
}
// Above code added to mimic the external references -- can be ignored
var indexer = function(directory) {
// return a promise object
return new Promise(function (resolve, reject) {
Self.indexleft++;
var results = {};
Self.client.readdir(directory, function(err,fileLst){
if(err) {
reject(); // promise is rejected
return;
}
// "Iterate" over file list asyonchronously
(function nextFile(fileList) {
if (!fileList.length) {
resolve(results); // promise is resolved
return;
}
var file = fileLst.shift(); // shop off first file
var ident = identify(file);
results[ident.name] = ident;
if(ident.type === 'dir'){ // There was a bug here: equal sign!
var descendant = directory !== ''
? directory + '\\' + ident.name : ident.name;
// recursively call indexer: it is again a promise!
indexer(descendant).then(function (result) {
ident.children = result;
// recursively continue with next file from list
nextFile(fileList);
});
} else {
nextFile(fileLst);
}
})(fileLst); // start first iteration with full list
});
});
};
// Call as a promise. Result is passed async to callback.
indexer('').then(function(me) {
console.log(me);
});
It's not really obvious how you're expecting to that returned object from the code you have, but I can help you get the object nonetheless.
The shape of the object is bad because you're using filenames as keys on the object but that's wrong. Keys should be identifiers known to your program, and since filenames can be almost anything, using a filename as a key is terrible.
For example, consider if a file was named name in your structure
{ "Applications" : {
"name" : "Applications",
"type" : "dir",
"name" : {
"name" : "name"
... } } }
Yep, it just broke. Don't worry tho, our solution won't run into such troubles.
const co = require('co')
const {stat,readdir} = require('fs')
const {extname,join} = require('path')
// "promisified" fs functions
const readdirp = path =>
new Promise ((t,f) => readdir (path, (err, res) => err ? f (err) : t (res)))
const statp = fd =>
new Promise ((t,f) => stat (fd, (err,stats) => err ? f (err) : t (stats)))
// tree data constructors
const Dir = (path, children) =>
({type: 'd', path, children})
const File = (path, ext) =>
({type: 'f', path, ext})
// your function
const indexer = function* (path) {
const stats = yield statp (path)
if (stats.isDirectory ())
return Dir (path, yield (yield readdirp (path)) .map (p => indexer (join (path,p))))
else
return File (path, extname (path))
}
This is good design because we didn't tangle directory tree building in with whatever Self.client is. Parsing a directory and building a tree is its own thing, and if you need an Object to inherit that behaviour there are other ways to do it.
Ok let's setup a sample tree of files and then run it
$ mkdir test
$ cd test
$ mkdir foo
$ touch foo/disk.iso foo/image.jpg foo/readme.txt
$ mkdir foo/bar
$ touch foo/bar/build foo/bar/code.js foo/bar/migrate.sql
Using indexer is easy
// co returns a Promise
// once indexer is done, you will have a fully built tree
co (indexer ('./test')) .then (
tree => console.log (JSON.stringify (tree, null, ' ')),
err => console.error (err.message)
)
Output (some \n removed for brevity)
{
"type": "d",
"path": "./foo",
"children": [
{
"type": "d",
"path": "foo/bar",
"children": [
{ "type": "f", "path": "foo/bar/build", "ext": "" },
{ "type": "f", "path": "foo/bar/code.js", "ext": ".js" },
{ "type": "f", "path": "foo/bar/migrate.sql", "ext": ".sql" }
]
},
{ "type": "f", "path": "foo/disk.iso", "ext": ".iso" },
{ "type": "f", "path": "foo/image.jpg", "ext": ".jpg" },
{ "type": "f", "path": "foo/readme.txt", "ext": ".txt" }
]
}
If you try indexer on a path to a file, it will not fail
co (indexer ('./test/foo/disk.iso')) .then (
tree => console.log (JSON.stringify (tree, null, ' ')),
err => console.error (err.message)
)
Output
{ "type": "f", "path": "./foo/disk.iso", "ext": ".iso" }

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

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);
});

Categories

Resources