coming from a php background, I'm trying to get my head around this callback stuff.
Basically I wanna get some rows, then I would like to loop through these rows and check them against an other model (different db). I want the call back to wait until they all have been looped through and checked.
The callback gets called before sequelize has looped through all the results.
Basically I want the function to be 'blocking'. What do I have to change?
toexport.getlasttransactions = function(lower,upper,callback){
var deferred = Q.defer();
var transactionsToUpdate = [];
///////////////////////////
// set import conditions //
///////////////////////////
var lowerbound = (lower) ? lower.format() : moment.utc().subtract(10, 'minutes').format();
var upperbound = (upper) ? upper.format() : moment.utc().format();
///////////////////////////////
// get IDs From Failed syncs //
///////////////////////////////
FailedSync.find({ limit: 100 })
.then(function(res){
var FailedIDs = [];
_.each(res, function(value,index){
FailedIDs.push(value.transaction_id);
});
// build condition
var queryCondition = { where: { updated_at: { between: [lowerbound,upperbound] } }, limit: 3 };
if(FailedIDs.length > 0){
queryCondition = {
where: Sequelize.and({ updated_at: { between: [lowerbound,upperbound] } },
Sequelize.or(
{ id: FailedIDs }
))
}
}
//////////////////////////////
// get Phoenix Transactions //
//////////////////////////////
PhoenixTransaction
.findAll(queryCondition)
.then(function(poenixTrx){
_.each(poenixTrx, function(value, index){
Transaction.findOne({ where: { id: value.id }})
.then(function(result){
if(!result || result.length === 0){
transactionsToUpdate.push(value);
console.log('!result || result.length === 0')
}
else if(result && result.length === 1){
if(result.hash != value.hash){
transactionsToUpdate.push(value);
console.log('result.hash != poenixTrx[i].hash')
}
}
})
.catch(function(err) {
console.log(err)
})
})
deferred.resolve(transactionsToUpdate);
})
.catch(function(err){
throw new Error("Something went wrong getting PhoenixTransaction")
})
})
deferred.promise.nodeify(callback);
return deferred.promise;
}
You have a lot of patterns new promise users have in your code:
You're using a deferred when you don't need to.
You're not using promise aggregation methods
You're not waiting for things in appropriate places but nesting instead.
Promises represent a value over time. You can use promises and access their result via then at a later point and not just right away - Sequelize's promises are based on bluebird and offer a rich API that does aggregation for you.
Here is an annotated version of cleaned up code - note it is not nesting:
toexport.getlasttransactions = function(lower,upper){ // no need for callback
var lowerbound = (lower || moment.utc().subtract(10, 'minutes')).format();
var upperbound = (upper || moment.utc()).format();
// use `map` over a `each` with a push.
var failedIds = FailedSync.find({ limit: 100 }).map(function(value){
return value.transaction_id;
});
// build condition.
var queryCondition = {
where: { updated_at: { between: [lowerbound,upperbound] } }, limit: 3
};
var query = failedIds.then(function(ids){ // use promise as proxy
if(ids.length === 0) return queryCondition;
return { // You can return a value or a promise from `then`
where: Sequelize.and({ updated_at: { between: [lowerbound,upperbound] } },
Sequelize.or({ id: ids});
};
});
var pheonixTransactions = query.then(function(condition){
return PhoenixTransaction.findAll(queryCondition); // filter based on result
});
return pheonixTransactions.map(function(value){ // again, map over each
return Transaction.findOne({ where: { id: value.id }}); // get the relevant one
}).filter(function(result){ // filter over if chain and push
return (!result || result.length === 0) ||
((result && result.length === 1) && result.hash != value.hash);
});
};
Ideally you'll want to either use something like Bluebird's reduce with an array of promises, but I'll provide an async.series implementation as its easier to understand.
Install async
npm install async
Require it in your file
var async = require('async')
Then implement it as such:
//////////////////////////////
// get Phoenix Transactions //
//////////////////////////////
PhoenixTransaction
.findAll(queryCondition)
.then(function(poenixTrx){
var queryArray = poenixTrx.map(function(value){
return function(callback){
Transaction.findOne({ where: { id: value.id }})
.then(function(result){
if(!result || result.length === 0){
transactionsToUpdate.push(value);
console.log('!result || result.length === 0')
}
else if(result && result.length === 1){
if(result.hash != value.hash){
transactionsToUpdate.push(value);
console.log('result.hash != poenixTrx[i].hash')
}
}
// trigger callback with any result you want
callback(null, result)
})
.catch(function(err) {
console.log(err)
// trigger error callback
callback(err)
})
}
})
// async.series will loop through he queryArray, and execute each function one by one until they are all completed or an error is thrown.
// for additional information see https://github.com/caolan/async#seriestasks-callback
async.series(queryArray, function(err, callback){
// after all your queries are done, execution will be here
// resolve the promise with the transactionToUpdate array
deferred.resolve(transactionsToUpdate);
})
})
.catch(function(err){
throw new Error("Something went wrong getting PhoenixTransaction")
})
The whole thing is a little messy to be honest. Especially the promise/callback mix up will probably cause you problems at some point. Anyway you use the deferred.resolve on the transactionsToUpdate which is just an array so it calls the callback right away.
If you keep that script as it is use instead of _.each something like async (https://github.com/caolan/async) to run your transactions in paralell and use that as callback.
It could look like this:
toexport.getlasttransactions = function(lower,upper,callback){
var transactionsToUpdate = [];
///////////////////////////
// set import conditions //
///////////////////////////
var lowerbound = (lower) ? lower.format() : moment.utc().subtract(10, 'minutes').format();
var upperbound = (upper) ? upper.format() : moment.utc().format();
///////////////////////////////
// get IDs From Failed syncs //
///////////////////////////////
FailedSync.find({ limit: 100 })
.then(function(res){
var FailedIDs = [];
_.each(res, function(value,index){
FailedIDs.push(value.transaction_id);
});
// build condition
var queryCondition = { where: { updated_at: { between: [lowerbound,upperbound] } }, limit: 3 };
if(FailedIDs.length > 0){
queryCondition = {
where: Sequelize.and({ updated_at: { between: [lowerbound,upperbound] } },
Sequelize.or(
{ id: FailedIDs }
))
}
}
//////////////////////////////
// get Phoenix Transactions //
//////////////////////////////
PhoenixTransaction
.findAll(queryCondition)
.then(function(poenixTrx){
async.each(poenixTrx, function(value, next){
Transaction.findOne({ where: { id: value.id }})
.then(function(result){
if(!result || result.length === 0){
transactionsToUpdate.push(value);
console.log('!result || result.length === 0')
}
else if(result && result.length === 1){
if(result.hash != value.hash){
transactionsToUpdate.push(value);
console.log('result.hash != poenixTrx[i].hash')
}
}
next();
})
.catch(function(err) {
console.log(err)
})
}, function(err) {
//Return the array transactionsToUpdate in your callback for further use
return callback(err, transactionsToUpdate);
});
})
.catch(function(err){
throw new Error("Something went wrong getting PhoenixTransaction")
})
})
}
Which would be the way with a callback.
But you need make your mind up what you want to use: callback OR promises. Don't use both together (as in: If your method expects a callback it shouldn't return a promise or if it returns a promise it shouldn't expect a callback).
Additional if you use callback you don't want to throw errors, you just call the callback and give the error in the callback - whoever uses your method can check the error from the callback and handle it.
Hope that kinda makes sense to you, I know the whole callback and promises thing is a little strange if you come from something like php and it needs some getting used to :)
thanks for explaining the differences. I think working with promises is the way forward, because it makes the code look nicer and avoids this "callback hell".
For example:
PhoenixSyncTransactions.getlasttransactions(lastTimeSynced,null)
.then(function(res){
return PersistTransaction.prepareTransactions(res).then(function(preparedTrx){
return preparedTrx;
})
}).then(function(preparedTrx){
return PersistTransaction.persistToDB(preparedTrx).then(function(Processes){
return Processes;
})
})
.then(function(Processes){
return PersistTransaction.checkIfMultiProcess(Processes).then(function(result){
return result;
})
})
.then(function(result){
console.log('All jobs done');
})
The whole code is easier to read.
Related
EDIT: currently i think the problem with this is that forEach is not promise aware. https://zellwk.com/blog/async-await-in-loops/
I am trying to apply a node javascript translation function (ive put it at the end of the post because it is quite long) to loop over an array of values. However when i loop for some reason i'm only getting certain parts of my looped function to appear after the loop has completed: Allow me to make this more clear:
array = [["hello", "hello" ],
["my", "my", ],
["name", "name" ],
["is", "my" ],
["joe", "joe"]]
function process (item,index){
const translate = require('#vitalets/google-translate-api');
console.log('loopaction'); //this shows that the loop is executing
translate(item[0], {to: 'sp'}).then(result => {
console.log(result.text);
}).catch(err => {
console.error(err);
})
array.forEach(process); // Applying the function process to the array in a ForEach loop
from this i am getting
loopaction
loopaction
loopaction
loopaction
loopaction
hola
mi
nombre
es
joe
So it seems that the forEach loop is completing before the values are being allowed to be displayed. Which is something i really don't understand since the array values are being translated correctly and then logged out in the correct order. As if they had been stored in the memory for later. And then called at the end of the forEach loop order.
The translate function looks like this:
function translate(text, opts, gotopts) {
opts = opts || {};
gotopts = gotopts || {};
var e;
[opts.from, opts.to].forEach(function (lang) {
if (lang && !languages.isSupported(lang)) {
e = new Error();
e.code = 400;
e.message = 'The language \'' + lang + '\' is not supported';
}
});
if (e) {
return new Promise(function (resolve, reject) {
reject(e);
});
}
opts.from = opts.from || 'auto';
opts.to = opts.to || 'en';
opts.tld = opts.tld || 'com';
opts.from = languages.getCode(opts.from);
opts.to = languages.getCode(opts.to);
var url = 'https://translate.google.' + opts.tld;
return got(url, gotopts).then(function (res) {
var data = {
'rpcids': 'MkEWBc',
'f.sid': extract('FdrFJe', res),
'bl': extract('cfb2h', res),
'hl': 'en-US',
'soc-app': 1,
'soc-platform': 1,
'soc-device': 1,
'_reqid': Math.floor(1000 + (Math.random() * 9000)),
'rt': 'c'
};
return data;
}).then(function (data) {
url = url + '/_/TranslateWebserverUi/data/batchexecute?' + querystring.stringify(data);
gotopts.body = 'f.req=' + encodeURIComponent(JSON.stringify([[['MkEWBc', JSON.stringify([[text, opts.from, opts.to, true], [null]]), null, 'generic']]])) + '&';
gotopts.headers['content-type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
return got.post(url, gotopts).then(function (res) {
var json = res.body.slice(6);
var length = '';
var result = {
text: '',
pronunciation: '',
from: {
language: {
didYouMean: false,
iso: ''
},
text: {
autoCorrected: false,
value: '',
didYouMean: false
}
},
raw: ''
};
try {
length = /^\d+/.exec(json)[0];
json = JSON.parse(json.slice(length.length, parseInt(length, 10) + length.length));
json = JSON.parse(json[0][2]);
result.raw = json;
} catch (e) {
return result;
}
if (json[1][0][0][5] === undefined) {
// translation not found, could be a hyperlink?
result.text = json[1][0][0][0];
} else {
json[1][0][0][5].forEach(function (obj) {
if (obj[0]) {
result.text += obj[0];
}
});
}
result.pronunciation = json[1][0][0][1];
// From language
if (json[0] && json[0][1] && json[0][1][1]) {
result.from.language.didYouMean = true;
result.from.language.iso = json[0][1][1][0];
} else if (json[1][3] === 'auto') {
result.from.language.iso = json[2];
} else {
result.from.language.iso = json[1][3];
}
// Did you mean & autocorrect
if (json[0] && json[0][1] && json[0][1][0]) {
var str = json[0][1][0][0][1];
str = str.replace(/<b>(<i>)?/g, '[');
str = str.replace(/(<\/i>)?<\/b>/g, ']');
result.from.text.value = str;
if (json[0][1][0][2] === 1) {
result.from.text.autoCorrected = true;
} else {
result.from.text.didYouMean = true;
}
}
return result;
}).catch(function (err) {
err.message += `\nUrl: ${url}`;
if (err.statusCode !== undefined && err.statusCode !== 200) {
err.code = 'BAD_REQUEST';
} else {
err.code = 'BAD_NETWORK';
}
throw err;
});
});
}
I realise there is a promise format and the problem im having may have to do with the asychronisity of the function and how the long the promise is taking to get resolved. I cant seem to figure out why the promise is not resolving or displaying after my forEach function is completely looped yet it seems to be saved correctly and in the correct order. Very odd.
Any ideas about what is it about the function translate() that is making this happen? Is there anyway i can rewrite my function process () to make sure that the translate functions resolved promise and the .then() in function process () is fully executed before moving on?
You are correct, you are using promises, so translate() will run asynchronously (in the background) while the rest of your code is executing. That is why you go through all the foreach() before the translate function returns, and therefore you get that output.
However, there is also a problem using a forEach loop in an async function or a promise block. The callback function is not being awaited. Therefore, the promise chain is broken, resulting in the unexpected behavior.
Don't use forEach loop in a promise or async function. Instead, use a for loop to iterate through the items of the array:
To avoid these problems, change the forEach loop to a For loop and use async and await like this:
async function process (item,index){
const translate = require('#vitalets/google-translate-api');
console.log('loopaction'); //this shows that the loop is executing
await translate(item[0], {to: 'sp'})
.then(result => {
console.log(result.text);
})
.catch(err => {
console.error(err);
})
}
async function main() {
array = [["hello", "hello" ],
["my", "my" ],
["name", "name" ],
["is", "my" ],
["joe", "joe"]]
for (let i = 0; i < array.length; i++) {
await process(array[i], i);
}
}
main()
await makes the function wait until the promise is resolved.
NOTE: You tried to create a timeout with object.sleep(), this doesn't exist in javascript, use setTimeout() instead, refer to: Sleep() in Javascript
I'm working with Nodejs and i want to use promises in order to make a full response after a for loop.
exports.getAlerts = function(req,res,next){
var detected_beacons = [];
if (!req.body || Object.keys(req.body).length == 0) {
res.status(401);
res.json({message:'No data sent'});
return
}
var nets = req.body.networks;
db.collection("beaconConfig", function(err, beaconConfigCollection){
if (!err){
var promises = [];
for(var i=0;i<nets.length;i++){
var defer = q.defer();
beaconConfigCollection.find({$or: [{"data.major" : nets[i].toString()},{"data.major" : nets[i]}], batteryLevel : {$lt : 70}}).toArray(function(errFind, saver){
if (!errFind && saver && saver.length > 0){
promises.push(defer.promise);
console.log("--------------------savers -------------------");
console.log(saver);
for(var j=0; j<saver.length;j++){
console.log("--------------------saver[j]-------------------");
console.log(saver[j]);
var detected = {}
var major = saver[j].data.major;
detected.major = major;
console.log("--------------------detected -------------------");
console.log(detected);
detected_beacons.push(detected);
defer.resolve(detected);
}
}
});
}
q.all(promises).then(function(results){
console.log("--------------------detected_beacons -------------------");
console.log(detected_beacons);
res.json(detected_beacons);
});
} else {
console.error(err);
res.status(500);
res.json({message:"Couldn't connect to database"});
}
});};
All the consoles.log works fine unless the last one, the ---detected_beacons--- one, which is THE FIRST ONE to be shown and it is empty.
That is the reason why i'm thinking that the promises are not working well. I have var q = require('q'); at the top and the mongo connection does not return any problem.
Thanks for the help.
First of all, an awesome guide about how to get along with Promises.
Well, haters gonna hate but there is nothing wrong with Promises at all (at least, I hope so).
According to 'MongoDb for Node' documentation, .toArray() returns a Promise, like most of the methods of this library. I felt free to make some appointments along your code:
exports.getAlerts = function(req, res, next) {
if (!req.body || Object.keys(req.body).length == 0) {
res.status(401);
res.json({message: 'No data sent'});
return;
}
// db.collection returns a promise :)
return db.collection("beaconConfig").then(function(beaconConfigCollection) {
// you can use .map() function to put together all promise from .find()
var promises = req.body.networks.map(function(net) {
// .find() also returns a promise. this promise will be concat with all
// promises from each net element.
return beaconConfigCollection.find({
$or: [{
"data.major": net.toString()
}, {
"data.major": net
}],
batteryLevel: {
$lt: 70
}
}).toArray().then(function(saver) {
// you can use the .find() response to create an array
// with only the data that you want.
return saver.map(function(saverElement) {
// your result array will be composed using saverElement.data.major
return saverElement.data.major;
});
}).catch(function(err) {});
});
// use q.all to create a promise that will be resolved when all promises
// from the array `promises` were resolved.
return q.all(promises);
}).then(function(results) {
console.log("results", results);
}).catch(function(err) {});
};
I hope it helps you!
In a Parse server function, it's getting Matches and profiles.
From a query to get matches another function is called to get Profiles by id but the result is :
{"_resolved":false,"_rejected":false,"_reso resolvedCallbacks":[],"_rejectedCallbacks":[]}
Main Query :
mainQuery.find().then(function(matches) {
_.each(matches, function(match) {
// Clear the current users profile, no need to return that over the network, and clean the Profile
if(match.get('uid1') === user.id) {
match.set('profile2', _processProfile(match.get('profile2')))
match.unset('profile1')
}
else if (match.get('uid2') === user.id) {
var profileMatch = _getProfile(match.get('profile1').id);
alert(">>>"+JSON.stringify(profileMatch));
match.set('profile1', _processProfile(match.get('profile1')))
match.unset('profile2')
}
})
the function to get Profile info:
function _getProfile(id){
var promise = new Parse.Promise();
Parse.Cloud.useMasterKey();
var queryProfile = new Parse.Query(Profile);
return queryProfile.equalTo("objectId",id).find()
.then(function(result){
if(result){
promise.resolve(result);
alert("!!!!"+result);
}
else {
console.log("Profile ID: " + id + " was not found");
promise.resolve(null);
}
},
function(error){
promise.reject(error)
});
return promise;
}
Just found this a little late. You've probably moved on, but for future readers: the key to solving something like this is to use promises as returns from small, logical asynch (or sometimes asynch, as in your case) operations.
The whole _getProfile function can be restated as:
function _getProfile(id){
Parse.Cloud.useMasterKey();
var queryProfile = new Parse.Query(Profile);
return queryProfile.get(id);
}
Since it returns a promise, though, you cannot call it like this:
var myProfileObject = _getProfile("abc123");
// use result here
Instead, call it like this:
_getProfile("abc123").then(function(myProfileObject) { // use result here });
Knowing that, we need to rework the loop that calls this function. The key idea is that, since the loop sometimes produces promises, we'll need to let those promises resolve at the end.
// return a promise to change all of the passed matches' profile attributes
function updateMatchesProfiles(matches) {
// setup mainQuery
mainQuery.find().then(function(matches) {
var promises = _.map(matches, function(match) {
// Clear the current users profile, no need to return that over the network, and clean the Profile
if(match.get('uid1') === user.id) {
match.set('profile2', _processProfile(match.get('profile2'))); // assuming _processProfile is a synchronous function!!
match.unset('profile1');
return match;
} else if (match.get('uid2') === user.id) {
var profileId = match.get('profile1').id;
return _getProfile(profileId).then(function(profileMatch) {
alert(">>>"+JSON.stringify(profileMatch));
match.set('profile1', _processProfile(match.get('profile1')))
match.unset('profile2');
return match;
});
}
});
// return a promise that is fulfilled when all of the loop promises have been
return Parse.Promise.when(promises);
}
I am developing an app in Parse and I'm trying to understand promises. I'm not finding very many working examples other than the very simple ones here: https://parse.com/docs/js/guide.
I'm querying the _User table. Then I loop through the users in an _.each loop. I'm running 2 cloud functions inside the loop for each iteration. At what point do I create the promise? Do I create one for each cloud function success within the loop? Or do I push each success return value onto an array and make that the promise value outside of the loop? I've tried both but I can't figure out the correct syntax to do either, it seems.
I'll break it down in pseudo-code because that may be easier than actual code:
var query = new Parse.Query(Parse.User);
query.find().then(function(users){
loop through each user in an _.each loop and run a cloud function for each that returns a number.
If the number > 0, then I push their username onto array1.
Then I run a 2nd cloud function on the user (still within the _.each loop) that returns a number.
If the number > 0, then I push their username onto array2.
}).then(function(promisesArray){
// I would like "promisesArray" to either be the 2 arrays created in the preceding section, or a concatenation of them.
// Ultimately, I need a list of usernames here. Specifically, the users who had positive number values from the cloud functions in the preceding section
concatenate the 2 arrays, if they're not already concatenated
remove duplicates
send push notifications to the users in the array
});
Questions:
- At what point do I create & return promises & what syntax should I use for that?
- Should .then(function(promisesArray){ be .when(function(promisesArray){ (when instead of then)?
Thank you both for your ideas! This is what ultimately worked:
var query = new Parse.Query(Parse.User);
query.find().then(function(users){
var allPromises = [];
var promise1, promise2;
_.each(users, function(user){
if(user.get("myvalue") != "undefined" && user.get("myvalue") != ""){
promise1 = Parse.Cloud.run("getBatch1", {param1: param1value, param2: param2value})
.then(function(numResult){
if(Number(numResult) > 0){
return Parse.Promise.as(user.getUsername());
}
});
}
allPromises.push(promise1);
if(user.get("anothervalue")==true){
promise2 = Parse.Cloud.run("getBatch2", {param1: param1value, param2: param2value})
.then(function(numResult2){
if(Number(numResult2) > 0){
return Parse.Promise.as(user.getUsername());
}
});
}
allPromises.push(promise2);
});
// Return when all promises have succeeded.
return Parse.Promise.when(allPromises);
}).then(function(){
var allPushes = [];
_.each(arguments, function(pushUser){
// Only add the user to the push array if it's a valid user & not already there.
if(pushUser != null && allPushes.indexOf(pushUser) === -1){
allPushes.push(pushUser);
}
});
// Send pushes to users who got new leads.
if(allPushes.length > 0){
Parse.Push.send({
channels: allPushes,
data: {
alert: "You have new leads."
}
}, {
success: function () {
response.success("Leads updated and push notifications sent.");
},
error: function (error) {
console.log(error);
console.error(error);
response.error(error.message);
}
});
}
response.success(JSON.stringify(allPushes));
}, // If the query was not successful, log the error
function(error){
console.log(error);
console.error(error);
response.error(error.message);
});
I'm not familiar with Parse API but I'd do it this way. Of course, I can't test my code so tell me if it works or not:
var query = new Parse.Query(Parse.User);
query.find()
.then(function(users) {
var promises = [];
users.forEach(function(user) {
// the first API call return a promise so let's store it
var promise = cloudFn1(user)
.then(function(result) {
if (result > 0) {
// just a way to say 'ok, the promise is resolved, here's the user name'
return Parse.Promise.as(user.name);
} else {
// return another promise for that second API call
return cloudFn2(user).then(function(res) {
if (result > 0) {
return Parse.Promise.as(user.name);
}
});
}
});
// store this promise for this user
promises.push(promise);
});
// return a promise that will be resolved when all promises for all users are resolved
return Parse.Promise.when(promises);
}).then(function(myUsers) {
// remove duplicates is easy with _
myUsers = _.uniq(myUsers);
// do your push
myUsers.forEach( function(user) {
});
});
First, you need to understand what Promises are. From what I understand of what you're trying to do it should look something like this:
//constructs the Parse Object
var query = new Parse.Query(Parse.User);
//find method returns a Promise
var res = query.find()
//good names will be a Promise of an array of usernames
//whose value is above 0
var goodNames = res
.then(function(data) {
//assumes the find method returns an array of
//objects, one of the properties is username
//we will map over it to create an Array of promises
//with the eventual results of calling the AJAX fn
var numberPromises = data.map(function(obj) {
//wrap the call to the cloud function in a new
//promise
return new Promise(resolve, reject) {
someCloudFn(obj.username, function(err) {
if (err) {
reject(err);
} else {
resolve(num);
}
});
}
};
//Promise.all will take the array of promises of numbers
//and return a promise of an array of results
return [data, Promise.all(numberPromises)];
})
.then(function(arr) {
//we only get here when all of the Promises from the
//cloud function resolve
var data = arr[0];
var numbers = arr[1];
return data
.filter(function(obj, i) {
//filter out the objects whose username number
//is zero or less
return numbers[i] > 0;
})
.map(function(obj) {
//get the username out of the query result obj
return obj.username;
});
})
.catch(function(err) {
console.log(JSON.stringify(err));
});
Now whenever you need to use the list of usernames whose number isn't zero you can call the then method of goodNames and get the result:
goodNames.then(function(listOfNames) {
//do something with the names
});
I want to send a list of new books to a user. So far the below code works fine. The problem is that I don't want to send a book multiple times, so I want to filter them.
Current code works fine:
function checkActiveBooks(books) {
var queue = _(books).map(function(book) {
var deferred = Q.defer();
// Get all alerts on given keywords
request('http://localhost:5000/books?l=0&q=' + book.name, function(error, response, body) {
if (error) {
deferred.reject(error);
}
var books = JSON.parse(body);
if (!_.isEmpty(books)) {
// Loop through users of current book.
var userBooks = _(book.users).map(function(user) {
// Save object for this user with name and deals.
return {
user: user,
book: book.name,
books: books
}
});
if (_.isEmpty(userBooks)) {
deferred.resolve(null);
} else {
deferred.resolve(userBooks);
}
} else {
deferred.resolve(null);
}
});
return deferred.promise;
});
return Q.all(queue);
}
But now I want to filter already sent books:
function checkActiveBooks(books) {
var queue = _(books).map(function(book) {
var deferred = Q.defer();
// Get all alerts on given keywords
request('http://localhost:5000/books?l=0&q=' + book.name, function(error, response, body) {
if (error) {
deferred.reject(error);
}
var books = JSON.parse(body);
if (!_.isEmpty(books)) {
// Loop through users of current book.
var userBooks = _(book.users).map(function(user) {
var defer = Q.defer();
var userBook = user.userBook.dataValues;
// Check per given UserBook which books are already sent to the user by mail
checkSentBooks(userBook).then(function(sentBooks) {
// Filter books which are already sent.
var leftBooks = _.reject(books, function(obj) {
return sentBooks.indexOf(obj.id) > -1;
});
// Save object for this user with name and deals.
var result = {
user: user,
book: book.name,
books: leftBooks
}
return deferred.resolve(result);
});
return Q.all(userBooks);
} else {
deferred.resolve(null);
}
});
return deferred.promise;
});
return Q.all(queue);
}
But above code doesn't work. It doesn't stop looping. I thought it made sense to use q.all twice, because it contains two loops. But I guess I'm doing it wrong...
First of all you should always promisify at the lowest level. You're complicating things here and have multiple deferreds. Generally you should only have deferreds when converting an API to promises. Promises chain and compose so let's do that :)
var request = Q.nfbind(require("request")); // a promised version.
This can make your code in the top section become:
function checkActiveBooks(books) {
return Q.all(books.map(function(book){
return request('http://.../books?l=0&q=' + book.name)
.get(1) // body
.then(JSON.parse) // parse body as json
.then(function(book){
if(_.isEmpty(book.users)) return null;
return book.users.map(function(user){
return {user: user, book: book.name, books: books };
});
});
});
}
Which is a lot more elegant in my opinion.
Now, if we want to filter them by a predicate we can do:
function checkActiveBooksThatWereNotSent(books) {
return checkActiveBooks(books).then(function(books){
return books.filter(function(book){
return checkSentBooks(book.book);
});
});
}
It's worth mentioning that the Bluebird library has utility methods for all this like Promise#filter and Promise#map that'd make this code shorter.
Note that if checkSentBook is asynchronous you'd need to modify the code slightly:
function checkActiveBooksThatWereNotSent(books) {
return checkActiveBooks(books).then(function(books){
return Q.all(books.map(function(book){ // note the Q.all
return Q.all([book, checkSentBooks(book.book)]);
})).then(function(results){
return results.filter(function(x){ return x[1]; })
.map(function(x){ return x[0]; });
});
});
}
Like I said, with different libraries this would look a lot nicer. Here is how the code would look like in Bluebird which is also two orders of magnitude faster and has good stack traces and detection of unhandled rejections. For fun and glory I threw in ES6 arrows and shorthand properties:
var request = Promise.promisify(require("request"));
var checkActiveBooks = (books) =>
Promise.
map(books, book => request("...&q=" + book.name).get(1)).
map(JSON.parse).
map(book => book.users.length ?
book.users.map(user => {user, books, book: book.name) : null))
var checkActiveBooksThatWereNotSent = (books) =>
checkActiveBooks(books).filter(checkBookSent)
Which I find a lot nicer.
Acting on #Benjamins's suggestion, here is what the code would look like when checkSentBooks returns a promise:
var request = Q.nfbind(require("request")); // a promised version.
function checkActiveBooks(books) {
return Q.all(_(books).map(function(book) {
// a callback with multiple arguments will resolve the promise with
// an array, so we use `spread` here
return request('http://localhost:5000/books?l=0&q=' + book.name).spread(function(response, body) {
var books = JSON.parse(body);
if (_.isEmpty(books)) return null;
return Q.all(_(book.users).map(function(user) {
return checkSentBooks(user.userBook.dataValues).then(function(sentBooks) {
// ^^^^^^ return a promise to the array for `Q.all`
return {
user: user,
book: book.name,
books: _.reject(books, function(obj) {
return sentBooks.indexOf(obj.id) > -1;
})
};
});
}));
});
}));
}