Jerry
Jerry

Reputation: 3586

Nested Promises

I have a function that does a series of asynchronous actions that in turn execute loops of other asynchronous actions. I'd like to know when everything is complete. It seemed like a great time to get my head wrapped around promises.

My code in the before-promise state boils down to something like this (hopefully in the simplification process I haven't rendered the example useless):

myClass.prototype.doMaintenance = function() {
    var types = ['choreType1', 'choreType2'];

    types.forEach(function(choreType) {
        // find all chores of the type with score 0 (need to be done) 
        redisClient.zrangebyscore('chores:'+choreType, 0, 0, function(err, chores) {
            if (!err) {
                chores.foreach(function(chore) {
                    doChore(chore, function(err, result){
                        // chore complete!
                    });
                })
            }
        });
    });
}

I go through a loop, and for each item in the loop I make an asynchronous database call, and loop through the results returned, making another asynchronous call for each result. Using callbacks to pass notification that all chores are done seems like it would be ugly at best. Therefore my goal: construct a promise that will resolve when all the chores are done.

I'm facing two difficulties. One is simply getting the promise syntax right. I'll show you what I've tried below. First through, an issue that may render this insolvable: say that the first database query comes back with a single chore. I (somehow) put that as part of the overall "all chores done" promise. Now I go back to get a list of the next type of chore. What if in the meantime the first chore is completed? The all-chores-done promise will be satisfied, and will resolve, before the rest of the chores are added.

I'm using the Q library in a node.js environment. I use Redis but it could be any asynch data source.

myClass.prototype.doMaintenance = function() {
    var types = ['choreType1', 'choreType2'];
    var typePromises = [];

    types.forEach(function(choreType) {
        // find all chores of the type with score 0 (need to be done)
        Q.npost(redisClient, 'zrangebyscore', ['chores:'+choreType, 0, 0]).done(chores) {
            var chorePromises = [];
            chores.foreach(function(chore) {
                chorePromises.push(doChore(chore)); // doChore returns a promise
            });
            typePromises.push(Q.all(chorePromises));
        });
    });

    return Q.all(typePromises); // at this point, typePromises is empty. Bummer!
}

What I've been trying to build (not quite there yet) is a promise that is a collection of typePromises, which in turn are collections of chorePromises.

I think what I need is a structure that says "I promise to get you the all-chores-done promise as soon as it's available." This is starting to make my head explode. Any guidance (including using a different pattern entirely) would be greatly appreciated.

Upvotes: 3

Views: 1998

Answers (1)

Bergi
Bergi

Reputation: 664195

You are constructing the list of typePromises asynchronically - and when you call Q.all(typePromises), it is still empty. Instead, you immediately need to return a promise for the database result which you can immediately collect into the list. If you don't know yet what the return value of those promises will be - no worries, use then to compose the tasks like getting Q.all(chorePromises) after a redis result has arrived.

I also would propose using map instead of pushing to an array in an each loop - this also helps to make sure that the promises are constructed immediately.

myClass.prototype.doMaintenance = function() {
    var types = ['choreType1', 'choreType2'];
    var typePromises = types.map(function(choreType) {
        // find all chores of the type with score 0 (need to be done)
        return Q.npost(redisClient, 'zrangebyscore', ['chores:'+choreType, 0, 0]).then(function(chores) {
            var chorePromises = chores.map(doChore); // doChore returns a promise
            return Q.all(chorePromises);
        }); // then returns a promise
    });
    return Q.all(typePromises);
}

Upvotes: 4

Related Questions