4

After recently discovering JS promises, I have been studying them so that I might build a certain functionality that allows me to execute 4 async queries, use the result of each to build an object that I can finally send as a response to a request directed at my node app.

  • The final object is made up of 3 array properties containing the resulting rows of each query.

It seems that I've done something wrong handling the promises, though, because ultimately, game is not being built. It is sent as an empty object. Here's a JSFiddle.

What is my mistake?


Here's what I have so far:

function sendGame(req, res, sales, settings, categories) {

    var game = new Object();

    game.sales = sales;

    game.settings = settings;

    game.categories = categories;



    JSONgame = JSON.stringify(game);

    res.writeHead(200, {
        'Access-Control-Allow-Origin': 'http://localhost',
            'Content-Length': JSONgame.length,
            'Content-Type': 'application/json'
    });

    res.write(JSONgame);
    res.end();

    console.log('Game: ' + JSON.stringify(game, null, 4));

    console.log('--------------------------------------');

    console.log('User ' + req.body.username + ' successfully retrieved game!');
}

function retrieveSales(req, connection, timeFrame) {

    console.log('User ' + req.body.username + ' retrieving sales...');

    connection.query('select * from sales_entries where date BETWEEN ? AND ?', timeFrame,

    function (err, rows, fields) {
        if (err) {
            callback(new Error('Failed to connect'), null);
        } else {
            sales = [];
            for (x = 0; x < rows.length; x++) {
                sales.push(rows[x]);
            }
            //console.log('Sales: ' + JSON.stringify(sales, null, 4));
            return sales;
        }
    });
}

retrieveCategories() and retrieveSettings() omitted for readability; they are the same as retrieveSales() mostly.

function gameSucceed(req, res) {

    console.log('User ' + req.body.username + ' retrieving game...');

    var timeFrame = [moment().days(0).format("YYYY-MM-DD HH:mm:ss"), moment().days(6).format("YYYY-MM-DD HH:mm:ss")];

    var connection = createConnection();

    connection.connect(function (err) {

        if (err) return callback(new Error('Failed to connect'), null);
        console.log('Connection with the Officeball MySQL database openned for game retrieval...');

        var sales = retrieveSales(req, connection, timeFrame);
        var settings = retrieveSettings(req, connection);
        var categories = retrieveCategories(req, connection);

        var all = q.all([sales, settings, categories]);

        all.done(function () {
            sendGame(req, res, sales, settings, categories);
        });

    });

}

1 Answers1

4

Your problem is that you're not using promises. All your APIs use callbacks.

A promise is like a closed box:

enter image description here

A promise also has a method that opens the box, works on the value and returns another box on the value (also opening any additional boxes along the way). That method is .then:

In boxes, it does:

enter image description here =>( open. => e) => e

That is, it adds a handler that gets an open box and returns a box. Everything else just combines stuff. All .all does is wait for a list of promises to resolve, it is exactly like .then in the fact it waits for a result. Because promises are boxes, you can pass them around and return them which is very cool.

Generally:

  • Whenever you return from a promise handler (not a rejection), you are fullfilling it indicating normal flow continuation.
  • Whenever you throw at a promise handler, you are rejecting indication exceptional flow.

So basically in node speak:

  • Whenever you returned a null error and a response, you resolve the promise.
  • Whenever you returned an error and no response, you reject the promise.

So:

function myFunc(callback){
    nodeBack(function(err,data){
         if(err!== null){
             callback(new Error(err),null);
         }
         callback(data+"some processing");
     })
 });

Becomes:

 function myFunc(){
     return nodeBack().then(function(data){ return data+"some processing"; });
 }

Which I think is a lot clearer. Errors are propagated across the promise chain just like in synchronous code - it's very common to find synchronous analogs to promise code.

Q.all takes a list of promises and waits for them to complete, instead you want Q.nfcall to transform a callback based API to a promise one and then use Q.all on that.

That is:

    var sales = Q.nfcall(retrieveSales,req, connection, timeFrame);
    var settings = Q.nfcall(retrieveSettings,req, connection);
    var categories = Q.nfcall(retrieveCategories, req, connection);

Q.nfcall takes a nodeback in the err,data convention and converts it to a promise API.

Also, when you do

return sales;

You are not really returning anything, since it returns synchronously. You need to use callback like in your error case or promisify it altogether. If you don't mind, I'll do it with Bluebird since it comes with much better facilities for dealing with these interop cases and does so much much faster, if you'd like you can switch promisifyAll for a bunch of Q.nfcall calls.

// somewhere, on top of file
connection = Promise.promisifyAll(connection); 

// note I'm passing just the username - passing the request breaks separation of concerns.
var retrieveSales = Promise.method(username, connection, timeFrame) {
    console.log('User ' + username + ' retrieving sales...');
    var q = 'select * from sales_entries where date BETWEEN ? AND ?';
    return connection.queryAsync(q, timeFrame).then(function(rows, fields){
       return rows;
    });
}

Note that suddenly you don't need a lot of boilerplate for making a query, you can use queryAsync directly instead if you'd like.

Now the code that wraps it becomes:

var gameSucceed = Promise.method(function gameSucceed(req, res) {

    console.log('User ' + req.body.username + ' retrieving game...');
    var timeFrame = [moment()....];
    var connection = Promise.promisifyAll(createConnection());

    return conn.connectAsync().then(function () {
        console.log('Connection with the ...');
        //sending req, but should really be what they use.
        return Promise.all([retrieveSales(req,conn,timeFrame),
                     retrieveSettings(req,conn),
                     retrieveCategories(req,conn)]);
    });

});

Now you can call sendGame(req, res, sales, settings, categories); outside of gameSucceed which doesn't hide what it does as much -

gameSucceed(req,res).spread(function(sales,settings,cats){
    return sendGame(req,res,sales,settings,cats);
});
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Have I ventured into the world of advanced JS, or am I still in the realm of newbie scripting? Because this sh*t is above my level right now lol. Excuse me while I re-read this probably-perfect answer for the third time. –  Mar 25 '14 at 23:43
  • @jt0dd word of advice, nothing is above your level, if you don't understand something it is crucial that you ask it or otherwise neither of us learn anything. I recommend you read http://stackoverflow.com/questions/14220321/how-to-return-the-response-from-an-ajax-call if you're unsure about concurrency in JS in general. If something is unclear please let me know what parts and I can try and make them clearer. – Benjamin Gruenbaum Mar 25 '14 at 23:45
  • I added more background on promises, let me know if that helped – Benjamin Gruenbaum Mar 25 '14 at 23:52
  • You're absolutely right. What I meant was at this point, I have preparation to do. Now, I noticed in your provided snippet you replace `categories = []; for (x = 0; x < rows.length; x++) { categories.push(rows[x]);} return categories;` with a simple return of `rows` - is that intentional, and a shortcut that I've missed? –  Mar 25 '14 at 23:58
  • You're pushing the entirety of the rows array into a new array and then returning it. You can simply return `rows` as it is identical. to be fair you can probably drop the last two lines altogether. – Benjamin Gruenbaum Mar 26 '14 at 00:04
  • Yes, your additional explanation on Promises is helpful. In addition to my confusion with the rows return, I also couldn't see where `conn` was coming from, but I think you may have been shortening the variable `connection` for readability. –  Mar 26 '14 at 00:05
  • Thank you for this fantastic, detailed answer. I appreciate your time. I got an error by the way, maybe it involves bluebird. I installed bluebird and required it to the variable `Promise` - but - I'm getting an error: https://gist.github.com/anonymous/9774467. –  Mar 26 '14 at 00:26
  • @jt0dd should be `Promise.method(function(...` – Benjamin Gruenbaum Mar 26 '14 at 08:58
  • I properly implememnted this with no error. But your query lead to a problem with retrieving some weird meta data http://stackoverflow.com/questions/22667975/processing-unwanted-meta-data-from-this-node-mysql-query-result –  Mar 27 '14 at 00:51
  • queryAsync seems to be doing something weird. Is it necessary for your answer to work properly? –  Mar 27 '14 at 01:33