callback-hell
v1.1.13
Published
my abstraction for dealing with async functions that must run sequentially or can run in parallel
Downloads
74
Readme
Callback-Hell
Installation
npm install callback-hell --save
Prerequisite Knowledge
the callback-hell library allows for async functions to be easily chained together.
The library expects all functions to expect callbacks which themselves expect input in a specific format:
Wrapped Result
a wrapped result is a simple object, that can contain:
- a result (a non-null value, mapped from key:
result
) - an error (a non-null value, mapped from key:
error
)
If a non-null error is present, the wrapped result is always treated as an error value - despite also potentially having a non-null result value.
Fixing non-compliant functions
Common callback signatures are:
var errorCallBack = function(err) { };
var resultCallBack = function(res) { };
var errorResultCallBack = function(err,res) { };
Using wrapper methods provided by the library, we can fix functions that expect the above callbacks as follows:
var h = require('callback-hell');
var errorCallBackFn = function(cb) { cb('error!'); };
var errorCallBackFnFixed = function(cb) { errorCallBackFn( h.ew(cb)); }; //ew - stands for ErrorWrap
var resultCallBackFn = function(cb) { cb('result!'); };
var resultCallBackFnFixed = function(cb) { resultCallBackFn( h.rw(cb)); }; //rw - stands for ResultWrap
var result = null;
var errorResultCallBackFn = function(cb) { cb('error!',null); };
var errorResultCallBackFnFixed = function(cb) { errorResultCallBackFn( h.bw(cb)); }; //bw - stands for Both (Error and Result) Wrap
Write Orders
a write order is a simple object. It contains:
- a value ( mapped from key
value
) - a key ( mapped from key
key
)
Using wrapper methods similar to above, we can extend a callback cb
, such that any wrapped result value being passed in is first itself wrapped in a write order:
var result = null;
var cbFn = function(cb) { cb('error!',null); };
// in two steps of wrapping: first turning it compliant -> then decorating with a write order:
var wrappedCbFn = function(cb) { cbFn( h.bw(cb)); }; //bw - stands for Both (Error and Result) Wrap
var writeOrderCbFn = function(cb) { wrappedCbFn( h.ww( cb, 'write_order_key' )); };
// or all at once (notice the ordering seems to be reversed):
var writeOrderCbFn2 = function(cb) { cbFn( h.bw( h.ww( cb, 'write_order_key' ) ) ); };
These write orders allow us to cleanly access previous computations when chaining our async functions.
Async Chaining Functions
AsyncSerial
This function calls a list of functions in sequence, one after another. It expects two arguments:
- a list of functions
- a final callback to be run
The functions in the list must be of type:
var fn = function( reader, callback ) { };
The reader
argument lets us examine the results of previously run async functions in the list that have used write orders to 'save' their results.
This is done by calling the get
method on the reader object, which takes a key as an argument.
The final async is passed an object containing all of the write orders that have been executed in the list.
If any of the functions return an error value (in a wrapped result of course), no further async functions in the list are run. The error value is immediately passed to the final callback instead of a dump of the write orders.
AsyncParallel
This function calls a list of functions all at once. It expects two arguments:
- a list of functions
- a final callback to be run
The functions in the list must be of type:
var fn = function(callback) { };
Unlike the serial function, we don't expect a reader - as it doesn't make sense in a parallel context.
The final callback is called immediately if any of the functions error - it is passed the error value.
Alternatively, upon successful completeion of all functions, a dump of the write orders is instead passed to the final callback (like in AsyncSerial
Utils
Also provided are a collection of utilities:
Additional wrapper functions exist which aid in modifying results before they are passed to the wrapped callback:
// h.mw (or MapWrap), allows us to modify the wrapped result (if present, i.e. if not an error), by using
// a specified 'mapping' function:
var wrappedCbFn = function(cb) { cbFn( h.rw (h.mw( cb, function(x) { return x['foo']; } ) ) ); };
// h.ix (or IndexWrap), is a common case of map wrap. It is used to extract a value from an object using a specified key
var wrappedCbFn2 = function(cb) { cbFn( h.rw ( h.ix( cb, 'foo' ) ) ); };
Also, functions exist which let you examine the status of a wrapped result:
var err = h.mkError('error wrapped result');
h.isError(err); //evaluates to true
Code Examples
Toy database library:
var db.get( sql, params, callBack ) // function(err,res) { ... };
var db.insert( sql, params, callBack ) // function(err) { ... };
Account Exists -> Parallel Insert
Code that checks the existence of an account - and based on its existence, adds some entries to the db
var h = require('callback-hell');
var _ = require('underscore');
var accountId = 'foo';
var valsToAdd = [1,2,3,4,5,6];
// create parallel async funcs for the insert - we don't care what order they're run.
var insertFns = _.map( valsToAdd, function(x) {
return function(cb) { db.insert( 'insert into vals value ( ?, ? )', [accountId, x ], h.ew( cb )); };
});
var mainFns = [
// first lift the value into a wrapped result -> then -> index the value using the key 'num' -> then -> wrap the value in a write order with the key 'num'
function(_r,cb) { db.get( 'select count(*) as num from accounts where account_id = ?', [accountId], h.rw( h.iw( h.ww(cb, 'num'), 'num' ))); },
function(re,cb) {
if( re.get("num") === 0)
cb(h.mkError( "account doesn't exist!" );
else
cb(h.mkNull());
},
function(_r,cb) { h.asyncParallel( insertFns, cb ); }
];
h.asyncSerial(mainFns,function(w) {
if(h.isError(w))
console.log(w.error);
});
Account Exists -> Parallel Get
Code that checks the existence of an account - and based on its existence retrieves some entries from the db
var h = require('callback-hell');
var _ = require('underscore');
var accountId = 'foo';
var keys = [1,2,3,4,5,6];
var mapFn = function(x) { return x[0].val; };
// create parallel async funcs for the insert - we don't care what order they're run.
var getFns = _.map( keys, function(x) {
// first lift the value into a wrapped result -> then -> map the value using the mapping fn (get the val from the first row) -> then -> wrap the value in a write order with the key equal to the search key
return function(cb) { db.get( 'select val from key_vals where account_id = ? and key = ?', [accountId, x ], h.rw( h.mw( h.ww(cb, x ), mapFn ))); };
});
var mainFns = [
function(_r,cb) { db.get( 'select count(*) as num from accounts where account_id = ?', [accountId], h.rw( h.iw( h.ww(cb, 'num'), 'num' ))); },
function(re,cb) {
if( re.get("num") === 0)
cb(h.mkError( "account doesn't exist!" );
else
cb(h.mkNull());
},
function(_r,cb) { h.asyncParallel( insertFns, h.ww(cb,'vals') ); }
];
h.asyncSerial(mainFns,function(w) {
if(h.isError(w))
console.log(w.error);
else
console.log(w.result.vals); //an object mapping keys to values: { 1 : ?, 2 : ? ... }
});