callback-hell

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

Usage no npm install needed!

<script type="module">
  import callbackHell from 'https://cdn.skypack.dev/callback-hell';
</script>

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 : ? ... }
});