UNPKG

genji

Version:

Writing reusable, modular and flexible node.js applications made easy.

352 lines (332 loc) 10.3 kB
/** * Utilities for controlling javascript execution flow */ /** * Module dependencies */ var toArray = require('./util').toArray; /** * Add functions in named chain and generate a function to call them in the adding order serially * * @constructor */ var Chain = function () { this._chain = {}; }; /** * Chain prototype object */ Chain.prototype = { /** * Add a function to a named chain * * @param name {String} Name of the chain * @param fn {Function} Function to be added * @public */ add: function (name, fn) { if (!this._chain[name]) { this._chain[name] = []; } this._chain[name].push(fn); }, /** * Get the chained function * * @param name {String} Name of the chain * @returns {Function} * @public */ get: function (name) { if (this._chain[name]) { var fnChain = chain(this._chain[name], function (fn, idx, fnChain, next, args) { args = toArray(args); args.push(next); if (fn.apply(this, args)) { next(); } }); return function () { fnChain(0, toArray(arguments), this); }; } } }; /** * Generate a function which calls each function in array in serial order * * @param array {Array} Array of items need to be iterated and called against "callback" * @param callback {Function} Callback function for each iteration * @param [doneCallback] Optional finishing callback which will be called at the end of iteration * @returns {Function} * @public */ function chain(array, callback, doneCallback) { var length; if (!Array.isArray(array)) { if (isFinite(array)) { // can be parsed as a number, // this is useful if you want to call one function many times in serial order length = parseInt(array, 10); } else { throw new Error('First argument should be either `Array` or `Number`, `' + typeof array + '` got.'); } } else { length = array.length; } return function next(step, args, ctx) { step = step || 0; if (step >= length) { if (doneCallback) { doneCallback.call(ctx); } return; } callback.call(ctx, array[step], step++, array, function () { next(step, arguments.length > 0 ? toArray(arguments) : args, ctx); }, args); }; } /** * Executes async function `callback` for data sets `array` in parallel * * @param array {Array} * @param callback {Function} * @param doneCallback {Function} * @param context {Object} * @returns {Object} * @public */ function parallel(array, callback, doneCallback, context) { var count = 0, len = array.length, result = []; var failCallback; var timer, timeoutSeconds, timeoutCallback; process.nextTick(function () { array.forEach(function (item, idx, arr) { callback.call(context, item, idx, arr, function (err, res) { ++count; if (timer && count === len) { clearTimeout(timer); } if (err) { if (failCallback) { failCallback.call(context, err, item, idx); } else { console.trace('Please set error handling function with `.fail`'); throw err; } } else { result[idx] = res; } if (count === len) { if (doneCallback) { doneCallback.call(context, result); } } }); }); if (timeoutSeconds) { timer = setTimeout(function () { timeoutCallback.call(context, result); }, timeoutSeconds * 1000); } }); var ret = { done: function (callback) { doneCallback = callback; return ret; }, fail: function (callback) { failCallback = callback; return ret; }, timeout: function (callback, timeoutInSeconds) { timeoutSeconds = timeoutInSeconds; timeoutCallback = callback; } }; return ret; } /** * Simple function which can convert your async function to promise style and call as many times as you wish * * @param {Function} asyncFn Your original async function like `fs.readFile` * @param {Object} context Context you want to keep for the asyncFn (`this` inside your function scope) * @returns {Function} Promise version of your function, * which you can register the callback functions by calling `then(callback)` of the returned object * @public */ function promise(asyncFn, context) { return function () { // the callback registered by calling `when(callback)` var callback; // we assume last argument of the original function is its callback // and you should omit this argument for the promised function generated var args = toArray(arguments); args.push(function () { if (callback) { callback.apply(context, arguments); } }); // call the original async function with given context in next tick // to make sure the `when` function set the callback first process.nextTick(function () { asyncFn.apply(context, args); }); return { when: function (handleFunc) { // register the callback function callback = handleFunc; } }; }; } /** * This an extended version of previous `promise` function with error handling and flow control facilities * @param {Function} asyncFn Your original async function like `fs.readFile` * @param {Object} context Context you want to keep for the asyncFn (`this` inside your function scope) * @param {Object} emitter Event emitter (delegate) use to emit and listen events * @returns {Function} Deferred version of your function, * which you can register the callback functions by calling `then(callback)` of the returned object * and set the error handling function by calling `fail(errback)` respectively */ function defer(asyncFn, context, emitter) { return function () { // the callback handler(s) registered by calling `then(callback)` var thenCallbacks = [], failCallbacks = [], andCallbacks = [], doneCallback, callback, nextDeferred; // we assume last argument of the original function is its callback // and you should omit this argument for the deferred function generated var args = toArray(arguments); args.push(function (err) { if (nextDeferred || callback) { // `callback` and `nextDeferred` should accept `err` as argument andCallbacks.push(function (defer) { var _args = toArray(arguments); _args[0] = err; // call the `callback` first if any if (callback) { callback.apply(this, _args); } if (nextDeferred) { if (err) { nextDeferred.error(err); } else { nextDeferred.next.apply(this, _args.slice(1)); } } }); } // this assume that, the first argument of the callback is an error object if error occurs, otherwise should be `null` if (!err) { var theArgs = toArray(arguments); // since there is no `err`, remove it from the arguments // this can help you separate your error handling logic from business logic // and make the error handling part reusable theArgs.shift(); if (thenCallbacks.length > 0) { // call the `then` stack thenCallbacks.forEach(function (fn) { fn.apply(context, theArgs); }); } if (andCallbacks.length > 0) { // call the `and` stack chain(andCallbacks, function (fn, idx, fnChains, next, chainArgs) { chainArgs.unshift({next: next, error: errback_}); if (fn.apply(this, chainArgs)) { chainArgs.shift(); next.apply(null, chainArgs); } }, doneCallback || (emitter && function () { emitter.emit('done'); }))(0, theArgs, context); } } else { errback_(err); } // define an error callback so that we can call it in the `and` chain function errback_(err) { if (failCallbacks.length > 0 || emitter) { if (emitter) { emitter.emit('fail', err); } // if we have error handler then use it failCallbacks.forEach(function (fn) { fn.call(context, err); }); } else { // otherwise throw the exception console.trace('Please set error handling function with `.fail` or supply an `emitter` and listen to the `fail` event.'); throw err; } } }); // call the original async function with given context in next tick // to make sure the `then`,`and`,`done`,`fail` function set the callback first process.nextTick(function () { asyncFn.apply(context, args); }); var ret = { then: function () { var fns = toArray(arguments); if (andCallbacks.length > 0) { // put at the tail of `and` callbacks var fn = function () { var args = toArray(arguments); args.shift(); // shift the `defer` object fns.forEach(function (f) { f.apply(this, args); }, this); return true; }; andCallbacks.push(fn); } else { // register the callback function in parallel thenCallbacks = thenCallbacks.concat(fns); } return ret; }, and: function () { andCallbacks = andCallbacks.concat(toArray(arguments)); return ret; }, callback: function (cb) { callback = cb; return ret; }, defer: function (deferred) { nextDeferred = deferred; return ret; }, done: function (fn) { doneCallback = emitter ? function () { emitter.emit('done'); fn.call(this); } : fn; return ret; }, fail: function (errorFunc, replace) { if (replace) { failCallbacks = [errorFunc]; } else { failCallbacks.push(errorFunc); } return ret; } }; if (emitter) { ret.emitter = emitter; } return ret; }; } /** * Module exports */ module.exports = { chain: chain, parallel: parallel, Chain: Chain, promise: promise, defer: defer };