Understanding promises in node.js for recursive function

Problem

I'm trying to use recursive calls to get data out of redis, stopping and returning when the members return null.

So my data is added like this:

SADD parents.<name> <parent1> <parent2>
SADD parents.<parent1> <grandparent1> <grandparent2>
...

And the final data should look like:

[
 {
     label: <name>,
     parents: [
         { label: <parent1>,
           parents: [ {label: <grandparent1>}, {label: <grandparent2> }] },
         { label: <parent2> }
     ]
 }
]

Here's the code I'm messing with (sort of cobbled together from different sources), but I have no idea what I'm doing. Not sure if this code is even useful, I could be way off track.

var redis = require('node-redis');
var r_client = redis.createClient();
var Q = require('q');


function getFromRedis(nodeName){
        var ret = Q.defer();
        r_client.smembers('parents.' + nodeName,function(err,val){
                if (err) ret.reject(err);
                else {
                        var constructedObject={};  //this is our returned object
                        var dependents=[];
                        if (val)
                        {
                                for (var k in val){  //iterate the keys in val
                                        constructedObject.name = val[k];

                                        dependents.push(getFromRedis(val[k])
                                        .then(function(subVal){
                                                constructedObject[k]=subVal;
                                                return ret.promise;
                                        })
                                        );
                                }
                        }
                        else { return [] }

                }
                Q.all(dependents)
                .then(function(){ret.resolve(constructedObject);},ret.reject.bind(ret));

        });
                return ret;
}

getFromRedis( 'greg', function(out) {console.log('Final output: ' + JSON.stringify( out ))} );

I can look at the examples and see theoretically how it's supposed to work, but I can't get my mind around how it should work with the q implementation. Any help would be greatly appreciated.

Problem courtesy of: coding_hero

Solution

  • Try to be as pure as you can when working with promises. Avoid functions that have side effects, i.e. do manipulate any variables outside of their own scope.
  • Avoid passing callbacks to functions. Do only pass them to promise methods. You are doing this both with r_client.smembers() and when invoking your getFromRedis method

I can see only one specific mistake that would keep your script from working:

return [];

does not have any effect from the callback. So, ret is never going to be resolved in this case. You would do ret.resolve([]); return; if at all. However, there are better solutions that let you use return again.

To restructure your script, there are two points:

  • Use the Q.nfcall helper function (and the like) to avoid dealing with callback-style APIs directly. Use then to transform its result then - synchronously returning the tree leaves or a promise for the descendant-getting computations.
  • Use Q.all first, and then transform its result. Don't add a handler to each dependent, but get the whole result and build the construct in one single step.

function getFromRedis(nodeName){
    return Q.ninvoke(r_client, "smembers", 'parents.' + nodeName).then(function(val) {
        // this is our returned object
        var constructedObject = {label: nodeName};
        if (val) {
            var dependents = val.map(function(par) {
                // get a promise for the next level
                return getFromRedis(nodeName+"."+par.toString());
            });
            return Q.all(dependents).then(function(dependentResults) {
                 constructedObject.parents = dependentResults;
                 return constructedObject;
            });
        } else { 
            return constructedObject; // without parents
        }
    });
}

getFromRedis( 'greg' ).done(function(out) {
    console.log('Final output: ' + JSON.stringify( out ));
});
Solution courtesy of: Bergi

Discussion

There is currently no discussion for this recipe.

This recipe can be found in it's original form on Stack Over Flow.