scope-chain

Another asynchronous function chain wrapper offering both middleware and non-middleware uses


Keywords
scope, chain, middleware, trycatch
License
MIT
Install
npm install scope-chain@1.2.0

Documentation

scope-chain

Another asynchronous function chain wrapper offering both middleware and non-middleware uses

var app = require('express')();
var chain = require('scope-chain');
var fs = require('fs');

app.get('/', function (req, res, next) {
    var scopeVariable = Date() + '\n';
    chain(next, function step() {
        res.write(scopeVariable, this);
        
    }, function step() {
        setTimeout(this, 2000);
        
    }, function step() {
        res.write(Date() + '\n', this);
        
    });

}, function (req, res, next) {
    res.end();

}).listen(3000);

Installation

$ npm install scope-chain

Features

  • Encourages middleware code-layouts presenting a clear birds-eye view
  • Flexible next-step callback functionality
  • Uncluttered usage lets the code tell it's own story
  • Invisible testing of err arguments
  • invisible try/catch wrapping of each step function
  • string final arg used as informative error prefix
  • optional additional this attributes as 1st arg

Philosophy

Programming a multi-step activity in the asynchronous NodeJS architecture can lead to a staircase of in-line functions as in this contrived example.

function middleware(req, res, next) {
    res.setHeader('Content-Type', 'text/plain');
    fs.readdir('/var/run', function (err, files) {
        for (var file in files) {
            fs.readyFile('/var/run/' + file, function (err, data) {
            });
            ... etc - async gets messy here
        }
    });
}

This scope-chain is for those who prefer to see code as a more linear progression.

function middleware(req, res, next) {
    res.setHeader('Content-Type', 'text/plain');
    chain({ path: '/var/run' }, next, function () {
        fs.readdir(this.path, this); // cb(err1, files)

    }, function callee(files, file,  err2, data) {
        if (file && !err2) // 
            res.write(file + ': ' + data.trim() + '\n');
        if (!files.length)
            return this();

        var file = files.shift();
        fs.readFile(this.path + '/' + file, callee.bind(this, files, file)); // cb(err2, data)
        
   });
}

Detail

The chain function has the signature chain(?ctx, final, step, step, step, ...)

The optional ctx argument chain is described after final is described.

The 1st mandatory argument to chain is the final, and is either a string or a function having a signature of function (err, args, ...). As a convenience, when final is provided as a string, it is transformed into a function as shown below. This convenience is intended to help identify the code location where an Error originates.

function (err) {
    err && console.error(this + (err.stack || err));
}.bind(final + ' ')

The provided OR auto-generated final is is always called last. In a middleware usage, the middleware-next argument is usually supplied here.

The optional ctx argument provides a means to add attrubutes to the this function-object passed to each step. Any enumerable attributes of the optional ctx argument are added to the final function, which are then subject to Attribute Copying as described below.

The remaining step arguments are the sequential activities of the chain. When called, each function is wrapped in its own try/catch where all exceptions are caught and sent to the final function, aborting all subsequent steps. For each of the step functions this is a function-object to be used as the inner-next for each step function.

The this object provides four variants of the inner-next functionality having the following signatures:

this(err, arg1, arg2, arg3, ...)

When this() is called with a falsy err value, the subsequent step function is called with just the available argN arguments.

When called with a truthy err value, the final function is called with that truthy err value, thereby aborting the step sequence.

this.silent(err, arg1, arg2, arg3, ...)

When this.silent() is called with a falsy err value, the subsequent step function is called with just the available argN arguments.

When called with a truthy err value, the final function is called with no arguments so discarding the error but also aborting the step sequence.

this.ignore(err, arg1, arg2, arg3, ...)

When this.ignore() is called with a falsy err value, the subsequent step function is called with just the available argN arguments.

When called with a truthy err value, the error is discarded and the subsequent step function is called with just the available argN arguments.

this.noerror(arg1, arg2, arg3, ...)

As this.noerror() has no concept of an err argument, the subsequent step function is called with all the available argN arguments. Arg1 may be an err value, but the chain functionality is ignorant of this.

Attribute Copying

Any attributes found on the provided OR autogenerated final function are copied to each of the four variant inner-next functions, but NOT copied back on completion. This can useful is passing context to private implementations of asynchronous logic.

The author used attribute-copying in an ExpressJS application to attach a unique index to each req object so that log-lines from interleaved requests could be attributed to the original ExpressJS request.

app.use(function (req, res, next) {
    req.index = ++global.index; next();
});
app.get('/', function (req, res, next) {
    chain({ index: req.index }, next, function () {
        mysql(sql, this);
        
    }, function (rows, meta) {
        ...
        
    });
});

function mysql(sql, callback) { // cb(err, rows, cols)
    debug(callback.index, sql, ...);
    ...
}

On occassions, a step yields an array result where a subsequent step needs to be invoked for each array member. For example, a first step may use a sql select to return an array or rows, and the next step must update each row individually.

app.get('/', function (req, res, next) {
    chain(next, function step() { // chain-A
        mysql('select * from database.table where uuid is null', this);
        
    }, function callee(rows, cols) {
        if (!rows.length) // all-done
            return this();
            
        var row = rows.shift();
        chain(this, function () { // chain-B
            mysql('update database.table set uuid=? where id=?', [uuid(), row.id], this);
            
        }, function inner() {
            callee.call(this.this, rows, cols); // repeat for next row
            // `this` is the this-object of `inner`, the step of chain-B
            // `this.this` is the this-object of `callee`, the step of chain-A
            
        });
        
    }, function alldone() {
        this();

    });
});

License

MIT