UNPKG

callback-hell

Version:

my abstraction for dealing with async functions that must run sequentially or can run in parallel

214 lines (146 loc) 7.53 kB
# 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: ```javascript 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: ```javascript 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: ```javascript 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: ```javascript 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: ```javascript 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: ```javascript // 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: ```javascript var err = h.mkError('error wrapped result'); h.isError(err); //evaluates to true ``` ## Code Examples Toy database library: ```javascript 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 ```javascript 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 ```javascript 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 : ? ... } });