Monday, December 30, 2013

Nodejs: Async to Sync. Using javascript generators to untangle callback mess.

Javascript is a single threaded language where some benefits of multi-threading are achieved by running functions of one callback chain (nest?) in gaps between running functions of another. All within the same thread.

Nodejs is the king of this approach and thus is it's blessing (speed) and it's curse (see list below).

This approach forces a declarative manner of nested callbacks. There are quite a few problems with it:

  1. Mess. Callback nesting declarations add so much noise into program's logic that it becomes borderline-unreadable.
  2. Scoping. Normally, when you run loops, you expect each iteration's code share the same scope which is not the case with looping through async calls.
  3. Timing. Normally, when you run loops, you expect each iteration end before the next one starts. Again, not the case with looping through async calls.
  4. Exceptions. Normally, when you enclose code in try-catch clause, you expect all exceptions to be caught. Not the case with async. 
Fortunately, since Javascript 1.7 (Node.version >= 0.11) you can use generators and yield operator to achieve manageable suspension of code.

Smart javascript developers attracted by elegance of idea behind Nodejs, but annoyed by the callback problems quickly realized that they can misuse generators to achieve more or less structured code without losing speed benefits which got them into this mess in the first place. 

I am not going to cover generators here, but will provide my version of balance between elegance and speed to that end: 


1.  Install the following code by running npm install synchronode

  
  
npm install synchronode
  

Or download it from github: https://github.com/apodgorny/synchronode
 
  
  
 
module.exports = function(fGenerator) {
    var oG   = fGenerator(),
        next = function(oError, oData) {
            // To distinguish between two different callback signatures -optional
            if (typeof oData == 'undefined') {
                oData = oError;
                oError = null;
            }

            if (oError) {
                return oG.throw(oError);            
            } else {
                var oResult = oG.send(oData);
                if (!oResult.done) {
                    oResult.value(next);
                }
            }
        }
 next();                                         
}

Function.prototype.sync = function() {
    var o = this,
        a = arguments;
  
    return function(fCallback) {
        a[a.length ++] = fCallback;
        o.apply(o, a);
    }
}
 
This code creates async loop between generator code in fGenerator and main function of the module, it's goal is to iterate through all yield statements within fGenerator function.

On each iteration it makes yield supply value which is normally sent by a callback of async call.

2. Use module:

  
  
  
// Require module
var Sync = require('sync');
var Fs   = require('fs');
    
// Sync is a controller that runs generator code
Sync(function*() {
    // Call async function sync-ly, omitting last callback param
    // You do have to specify all optional params though
    // Value that you'd normally receive in callback, is yield'ed to your
    // regular program flow. Enjoy!
    var sContent = yield Fs.readFile.sync('myfile.txt', 'utf8');
    console.log(sContent);
});
 
Loop example:
  
  
   
// Include sync module
var Sync = require('sync');


// Declare your async routine as usual
var authParther = function(sPartner, sToken, fCallback) {
    oDatabase.get('SELECT SOME AUTH RECORD', [sPartner, sToken], fCallback);
}


// Call async routine in a godly manner
var authenticate = function(sToken, fCallback) {
    var bAuthenticated = false;
    Sync(function*() {
        var oUser;
        for (sPartner in ['facebook', 'google', 'linkedin']) {
            // sync method added to Function.prototype avoids having to
            // write a wrapper for every method on Earth
            // Nice and clean!
            if (oUser = yield authPartner.sync(sPartner, sToken)) {
               bAuthenticated = true;
               fCallback(oUser);
            }
        }
    }
    if (!bAuthenticated) { fCallback(null); }
}
 

No comments:

Post a Comment