UNPKG

jam

Version:

JAM your async calls together *faster*

334 lines (259 loc) 11 kB
// lib/jam.js - Main JAM entrypoint module.exports = (function() { var assert = require('assert'), tick = null; // Find out if we have setImmediate and fallback to setTimeout if necessary. tick = typeof setImmediate === 'function' ? setImmediate : function(func) { setTimeout(func, 0); }; // # INTERNAL HELPERS // Function and arguments helpers. var toArgs = function(args) { return Array.prototype.slice.call(args); }; function replaceHead(args, newHead) { args = toArgs(args); if (!args.length) return [newHead]; args[0] = newHead; return args; } function bind(func, context) { return function() { return func.apply(context, arguments); }; } // Common assertions function ensureFunc(func, argName) { assert(typeof func === 'function', argName + ' argument missing or not a function'); }; function ensureNum(num, argName) { assert(typeof num === 'number', argName + ' argument missing or not a number'); }; function ensureArray(arr, argName) { assert(arr && typeof arr === 'object' && typeof arr.length === 'number', argName + ' argument missing or does not looks like an array'); }; // --- // # HELPERS // Additional functions that adds to the original jam function. function includeHelpers(func) { // ## jam.identity() // Simple function that passes the values it receives to the next function. // Useful if you need a `process.nextTick` inserted in-between your call chain. func.identity = function(next) { function _identity(next) { var args = arguments; tick(function() { next.apply(this, replaceHead(args, null)); }); } // This function can also be passed to jam verbatim. return (typeof next === 'function') ? _identity.apply(this, arguments) : _identity; }; // ## jam.nextTick() // Alias for `.identity`. Use when you need a `process.nextTick` inserted in-between // your call chain. func.nextTick = func.identity // ## jam.return( [args...] ) // Returns a set of values to the next function in the chain. Useful when you want to // pass in the next function verbatim without wrapping it in a `function() { }` just // to pass values into it. func.return = function() { var args = toArgs(arguments); return function(next) { args.unshift(null); next.apply(this, args); }; }; // ## jam.null() // Similar to `.identity` but absorbs all arguments that has been passed to it and // forward nothing to the next function. Effectively nullifying any arguments passed // from previous jam call. // // Like `jam.identity`, this function can be passed to the jam chain verbatim. func.null = function(next) { function _null(next) { next(); } return (typeof next === 'function') ? _null.call(this, next) : _null; }; // ## jam.call( function, [args...] ) // Convenience for calling functions that accepts arguments in standard node.js // convention. Since jam insert `next` as first argument, most functions cannot be // passed directly into the jam chain, thus this helper function. // // If no `args` is given, this function passes arguments given to `next()` call from // previous function directly to the function (with proper callback) placement). // // Use this in combination with `jam.return` or `jam.null` if you want to control the // arguments that are passed to the function. func.call = function(func) { ensureFunc(func, 'function'); var args = toArgs(arguments); args.shift(); // func if (args.length) { // use provided arguments return function(next) { args.push(next); func.apply(this, args); }; } else { // use passed-in arguments during chain resolution return function(next) { args = toArgs(arguments); args.shift(); // move next to last position args.push(next); func.apply(this, args); }; } }; // ## jam.each( array, iterator( next, element, index ) ) // Execute the given `iterator` function for each element given in the `array`. The // iterator is given a `next` function and the element to act on. The next step in the // chain will receive the original array passed verbatim so you can chain multiple // `.each` calls to act on the same array. // // You can also pass `arguments` and `"strings"` as an array or you can omit the array // entirely, in which case this method will assume that the previous chain step // returns something that looks like an array as its first result. // // Under the hood, a JAM step is added for each element. So the iterator will be // called serially, one after another finish. A parallel version maybe added in the // future. func.each = function(array, iterator) { if (typeof array === 'function') { iterator = array; array = null } else { ensureArray(array, 'array'); } ensureFunc(iterator, 'iterator'); return function(next, array_) { var arr = array || array_; // Builds another JAM chain internally var chain = jam(jam.identity) , count = arr.length; for (var i = 0; i < count; i++) (function(element, i) { chain = chain(function(next) { iterator(next, element, i); }); })(arr[i], i); chain = chain(function(next) { next(null, arr); }); return chain(next); }; }; // ## jam.map( array, iterator( next, element, index ) ) // Works exactly like the `each` helper but if a value is passed to the iterator's // `next` function, it is collected into a new array which will be passed to the next // function in the JAM chain after `map`. // // Like with `each`, you can omit the `array` input, in which case this method will // assume that the previous chain step returns something that looks like an array as // its first result. func.map = function(array, iterator) { if (typeof array === 'function') { iterator = array; array = null; } else { ensureArray(array, 'array'); } ensureFunc(iterator, 'iterator'); return function(next, array_) { var arr = array || array_; // Builds another JAM chain internally and collect results. // TODO: Dry with .each? var chain = jam(jam.identity) , count = arr.length , result = []; for (var i = 0; i < count; i++) (function(element, i) { chain = chain(function(next, previous) { result.push(previous); iterator(next, element, i); }); })(arr[i], i); chain = chain(function(next, last) { result.push(last); result.shift(); // discard first undefined element next(null, result); }); return chain(next); }; }; // ## jam.timeout( timeout ) // Pauses the chain for the specified `timeout` using `setTimeout`. Useful for // inserting a delay in-between a long jam chain. func.timeout = function(timeout) { ensureNum(timeout, 'timeout'); return function(next) { var args = replaceHead(arguments, null); setTimeout(function() { next.apply(this, args); }, timeout); }; }; // ## jam.promise( [chain] ) // Returns a JAM promise, useful when you are starting an asynchronous call outside of // the JAM chain itself but wants the callback to call into the chain. In other words, // this allow you to put a 'waiting point' (aka promise?) into existing JAM chain that // waits for the initial call to finish and also pass any arguments passed to the // callback to the next step in the JAM chain as well. // // This function will returns a callback that automatically bridges into the JAM // chain. You can pass the returned callback to any asynchronous function and the JAM // chain (at the point of calling .promise()) will wait for that asynchronous function // to finish effectively creating a 'waiting point'. // // Additionally, any arguments passed to the callback are forwarded to the next call // in the JAM chain as well. If errors are passed, then it is fast-forwarded to the // last handler normally like normal JAM steps. func.promise = function(chain) { chain = typeof chain === 'function' ? chain : // chain is supplied typeof this === 'function' ? this : // called from the chain variable ensureFunc(chain, 'chain'); // fails if (typeof chain === 'undefined' && typeof this === 'function') { chain = this; } var args = null, next = null; chain(function(next_) { if (args) return next_.apply(this, args); // callback already called next = next_; // wait for callback }); return function() { if (next) return next.apply(this, arguments); // chain promise already called args = arguments; // wait for chain to call the promise }; }; // TODO: noError() ? or absorbError() return func; }; // --- // # JAM function // Exported function starts the asynchronous call chain. function jam(func, context) { ensureFunc(func, 'function'); var steps = []; // ##### Chain resolver. // The resolver will execute all functions passed to the chain as soon as `nextTick`. // Thus jam will not works across async context where the chain is not built all at // once in a single event loop, which is not really a problem from my personal // experience. tick(function resolve(e) { var args = Array.prototype.slice.call(arguments); // Any errors passed to next() are (fast-)forwarded to the last function in the // chain skipping any functions that's left to be executed. if (e) return steps[steps.length - 1].apply(this, args); // Any parameters given to next() are passed as arguments to the next function in // the chain (except for errors, of course.) var next = steps.shift() , args = Array.prototype.slice.call(arguments) if (steps.length) { args.shift(); // error arg args.unshift(resolve); // next() function } return next.apply(this, args); }); // ##### Chain context continuation. // Subsequent invocation of the function returned from the `jam` function simply adds // the given function to the chain. function continuable(func, context) { ensureFunc(func, 'function'); if (context) { // TODO: Handle falsy things? func = bind(func, context); } steps.push(func); return continuable; }; return includeHelpers(continuable(func, context)); }; // --- return includeHelpers(jam); })();