async-chainable
Flow control for NodeJS applications.
This builds on the foundations of the Async library while adding better handling of mixed Series / Parallel tasks via object chaining.
var asyncChainable = require('async-chainable');
asyncChainable()
.parallel([fooFunc, barFunc, bazFunc])
.series([fooFunc, barFunc, bazFunc])
.end(console.log)
asyncChainable()
.limit(2)
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await('foo', 'bar')
.then(function() { console.log(this.foo, this.bar) })
.await()
.end(function() { console.log(this) })
asyncChainable()
.parallel('foo', fooFunc)
.prereq('foo', 'bar', barFunc)
.prereq(['foo', 'bar'], 'baz', bazFunc)
.end(console.log)
asyncChainable()
.forEach([
'What do we want?',
'Race conditions!',
'When do we want them?',
'Whenever!',
], function(next, item) {
console.log(item);
next();
})
.end();
asyncChainable()
.parallel({
foo: fooFunc,
bar: barFunc,
baz: bazFunc
})
.then(function(next) {
console.log(this);
console.log('Foo =', this.foo);
next();
})
.end();
var tasks = asyncChainable();
tasks.defer('foo', fooFunc);
tasks.defer('bar', barFunc);
if () {
tasks.defer('bar', 'baz', bazFunc);
}
tasks.end();
asyncChainable()
.defer('quz', 'foo', function(next) { next(null, 'fooValue') })
.defer(['baz', 'foo'], 'bar', function(next) { next(null, 'barValue') })
.defer('baz', function(next) { next(null, 'bazValue') })
.defer('baz', 'quz', function(next) { next(null, 'quzValue') })
.await()
.end();
app.get('/order/:id', function(req, res) {
asyncChainable()
.then(function(next) {
if (!req.params.id) return next('No ID specified');
next();
})
.then('order', function(next) {
Orders.findOne({_id: req.params.id}, next);
})
.then(function(next) {
setTimeout(next, 1000);
})
.end(function(err) {
if (err) return res.status(400).send(err);
res.send(this.order);
});
});
Project Goals
This project has the following goals:
- Be semi-compatible with the Async library so existing applications are portable over time
- Provide a readable and dependable model for asynchronous tasks
- Have a 'sane' (YMMV) syntax that will fit most use cases
- Have an extendible plugin system to allow additional components to be easily brought into the project
- Work with any async paradigm - callbacks, async functions, promises etc.
Plugins
There are a number of async-chainable plugins available which can extend the default functionality of the module:
FAQ
Some frequently asked questions:
-
Why not just use Async? - Async is an excellent library and suitable for 90% of tasks out there but it quickly becomes unmanageable when dealing with complex nests such as a mix of series and parallel tasks.
-
Why was this developed? - Some research I was doing involved the nesting of ridiculously complex parallel and series based tasks and Async was becoming more of a hindrance than a helper.
-
What alternatives are there to this library? - The only ones I've found that come close are node-seq and queue-async but both of them do not provide the functionality listed here
-
Is this the module I should use for Async JavaScript? - If you're doing simple parallel or series based tasks use Async, if you're doing complex nested operations you might want to take a look at this one
-
Whats license do you use? - We use the MIT license, please credit the original library and authors if you wish to fork or share
-
Who wrote this / who do I blame? - Matt Carter and David Porter
More complex examples
var asyncChainable = require('async-chainable');
asyncChainable()
.series([
function(next) { setTimeout(function() { console.log('Series 1'); next(); }, 100); },
function(next) { setTimeout(function() { console.log('Series 2'); next(); }, 200); },
function(next) { setTimeout(function() { console.log('Series 3'); next(); }, 300); },
])
.then(function(next) {
console.log('Finished step 1');
})
.parallel([
function(next) { setTimeout(function() { console.log('Parallel 1'); next(); }, 300); },
function(next) { setTimeout(function() { console.log('Parallel 2'); next(); }, 200); },
function(next) { setTimeout(function() { console.log('Parallel 3'); next(); }, 100); },
])
.end(function(next) {
console.log('Finished simple example');
});
asyncChainable()
.series({
foo: function(next) {
setTimeout(function() { console.log('Series 2-1'); next(null, 'foo result'); }, 100);
},
bar: function(next, results) {
setTimeout(function() { console.log('Series 2-2'); next(null, 'bar result'); }, 100);
},
baz: function(next) {
setTimeout(function() { console.log('Series 2-3'); next(null, 'baz result'); }, 100);
},
})
.parallel({
fooParallel: function(next) {
setTimeout(function() { console.log('Series 2-1'); next(null, 'foo parallel result'); }, 100);
},
barParallel: function(next) {
setTimeout(function() { console.log('Series 2-2'); next(null, 'bar parallel result'); }, 100);
},
bazParallel: function(next) {
setTimeout(function() { console.log('Series 2-3'); next(null, 'baz parallel result'); }, 100);
},
})
.then(function(next, results) {
console.log("Results", results);
})
.reset()
.end(function(next, results) {
console.log('Results should be blank', results);
};
fooFunc = barFunc = bazFunc = quzFunc = function(next) {
setTimeout(function() {
next(null, arguments.callee.toString().substr(0, 3) + ' value');
}, Math.random() * 1000);
};
asyncChainable()
.series({foo: fooFunc, bar: barFunc, baz: bazFunc})
.end(console.log);
asyncChainable()
.then('foo', fooFunc)
.then('bar', barFunc)
.then('baz', bazFunc)
.end(console.log);
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await()
.end(console.log);
asyncChainable()
.parallel('foo', fooFunc)
.parallel('bar', barFunc)
.parallel('baz', bazFunc)
.await()
.end(console.log);
asyncChainable()
.limit(2)
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await('foo', 'bar')
.then(console.log)
.await()
.end(console.log)
asyncChainable()
.waterfall([
function(next) { next(null, 'foo') };
function(next, fooResult) { console.log(fooResult); next(); }
]);
API
.await()
Wait for one or more fired defer functions to complete before containing down the asyncChainable chain.
await() // Wait for all defered functions to finish
await(string) // Wait for at least the named defer to finish
await(string,...) // Wait for the specified named defers to finish
await(array) // Wait for the specified named defers to finish
Some examples:
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await()
.end(console.log)
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await('foo', 'bar')
.end(console.log)
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await(['foo', 'bar'])
.end(console.log)
.context()
Set the context used by async-chainable during subsequent function calls.
In effect this sets what this
is for each call.
Omitting an argument or supplying a 'falsy' value will instruct async-chainable to use its own default context object.
asyncChainable()
.then({foo: fooFunc})
.context({hello: 'world'})
.then({bar: barFunc})
.context()
.then({baz: bazFunc})
.end(this)
Note that even if the context is switched async-chainable still stores any named values in its own context for later retrieval (in the above example this is barFunc()
returning a value even though the context has been changed to a custom object).
See the Context Section for further details on what the async-chainable context object contains.
.defer()
Execute a function and continue down the asyncChainable chain.
defer(function)
defer(string, function) // Named function (`this.name` gets set to whatever gets passed to `next()`)
defer(string, string, function) // Named function (name is second arg) with prereq (first arg)
defer(array, function) // Run an anonymous function with the specified pre-reqs
defer(array, string, function) // Named function (name is second arg) with prereq array (first arg)
defer(string, array, function) // Name function (name is first, prereqs second) this is a varient of the above which matches the `gulp.task(id, prereq)` syntax
defer(array)
defer(object) // Named function object (each object key gets assigned to this with the value passed to `next()`)
defer(collection) // See 'object' definition
defer(null) // Gets skipped automatically
Use await()
to gather the parallel functions.
This can be considered the parallel process twin to series()
/ then()
.
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await('foo', 'bar')
.then(console.log)
.await()
.end(console.log)
NOTE: All defers take their 'next' handler as the first argument. All subsequent arguments are the resolved value of the prerequisites. In the above example barFunc
would be called as barFunc(next, resultOfFoo)
and bazFunc
would be called as bazFunc(next, resultOfBaz)
.
.end()
The final stage in the chain, .end()
must be called to execute the queue of actions.
end() // Run the chain but don't do anything when completed
end(function) // Final function to execute as `function(err)`
end(string, function) // Extract the context value specified by string and provide it to the callback
NOTE: The form end(string, function)
is used is the internal equivalent to end(err => callback(err, this[string]))
- i.e. the context value is extracted for you.
While similar to series()
/ then()
this function will always be executed last and be given the error if any occurred in the form function(err)
.
asyncChainable()
.then('foo', fooFunc)
.then('bar', barFunc)
.then('baz', bazFunc)
.end(console.log)
In the above if fooFunc, barFunc or bazFunc call next with a first parameter that is true execution will stop and continue on passing the error to end()
:
asyncChainable()
.then('foo', fooFunc)
.then('bar', barFunc)
.then('baz', bazFunc)
.end(console.log)
If an error is caused in the middle of execution the result object is still available:
asyncChainable()
.then('foo', fooFunc)
.then('bar', barFunc)
.then('baz', bazFunc)
.end(console.log)
.fire()
Trigger a hook. This function will run a callback on completion whether or not any hooks executed.
fire(string, function) // Fire a hook and run the callback on completion
this.fire(...) // Same as above invocations but accessible within a chain
asyncChainable()
.forEach(['foo', 'bar', 'baz'], function(next, item, key) { console.log(item) })
.hook('hello', function(next) { console.log('Hello world!'); next() })
.then(function(next) {
this.fire('hello, next);
})
.end();
.forEach()
The forEach()
function is a slight variation on the parallel()
function but with some additional behaviour.
forEach(fromNumber, toNumber, function) // Call function toNumber-fromNumber times
forEach(toNumber, function) // Call function toNumber times (same as `forEach(1, NUMBER, function)`)
forEach(array, function) // Run each item in the array though `function(next, value)`
forEach(object, function) // Run each item in the object though `function(next, value, key)`
forEach(collection,function) // see 'array, function' definition (collections are just treated like an array of objects with 'forEach')
forEach(string, function) // Lookup `this[string]` then process according to its type (see above type styles) - This is used for late binding
forEach(string, array, function) // Perform a map operation on the array setting the `this[string]` to the ordered result return
forEach(string, object, function) // Perform a map operation on an object array setting the `this[string]` to the result return
forEach(string, string, function) // Lookup the first string and process it as a map operation according to its type (see above 2 examples) - This is used for late binding
forEach(null) // Gets skipped automatically (also empty arrays, objects)
It can be given an array, object or collection as the first argument and a function as the second. All items in the array will be iterated over in parallel and passed to the function which is expected to execute a next condition returning an error if the forEach iteration should stop.
asyncChainable()
.forEach(['foo', 'bar', 'baz'], function(next, item, key) { console.log(item) })
.end();
In the above example the simple array is passed to the function with each payload item as a parameter and the iteration key (an offset if its an array or collection, a key if its an object).
forEach()
has one additional piece of behaviour where if the first argument is a string the context will be examined for a value to iterate over. The string can be a simple key to use within the passed object or a deeply nested path using dotted notation (e.g. key1.key2.key3
).
asyncChainable()
.set({
items: ['foo', 'bar', 'baz'],
})
.forEach('items', function(next, item, key) { console.log(item); next() })
.end();
This allows late binding of variables who's content will only be examined when the chain item is executed.
.getPath()
GetPath is the utility function used by forEach()
to lookup deeply nested objects or arrays to iterate over.
It is functionally similar to the Lodash get()
function.
.hook()
Attach a callback hook to a named trigger. These callbacks can all fire errors themselves and can fire out of sequence, unlike normal chains.
Hooks can be defined multiple times - if multiple callbacks are registered they are fired in allocation order in series. If any hook raises an error the chain is terminated as though a callback raised an error.
Defined hooks can be start
, end
, timeout
as well as any user-defined hooks.
Hooks can also be registered within a callback via this.hook(hook, callback)
unless context is reset.
Hooks can also have an optional id and an array of prerequisites
hook(string, function) // Register a callback against a hook
hook(array, function) // Register a callback against a number of hooks, if any fire the callback is called
hook(string, string, function) // Register a named hook
hook(string, array, function) // Register a hook with prerequisites
hook(string, string, array, function) // Register a named hook with an array of prerequisite hooks
this.hook(...) // Same as above invocations but accessible within a chain
asyncChainable()
.forEach(['foo', 'bar', 'baz'], function(next, item, key) { console.log(item) })
.hook('start', function(next) { console.log('Start!'); next() })
.hook('end', function(next) { console.log('End!'); next() })
.hook(['start', 'end'], function(next) { console.log('Start OR End!'); next() })
.end();
.limit()
Restrict the number of defer operations that can run at any one time.
limit() // Allow unlimited parallel / defer functions to execute at once after this chain item
limit(Number) // Restrict the number of parallel / defer functions after this chain item
This function can be used in the pipeline as many times as needed to change the limit as we work down the execution chain.
asyncChainable()
.limit(2)
.defer(fooFunc)
.defer(barFunc)
.defer(bazFunc)
.defer(quzFunc)
.await()
.limit(3)
.defer(fooFunc)
.defer(barFunc)
.defer(bazFunc)
.defer(quzFunc)
.await()
.limit()
.defer(fooFunc)
.defer(barFunc)
.defer(bazFunc)
.defer(quzFunc)
.await()
.end(console.log)
.map()
The map()
function is really just an alias for forEach()
aimed specifically at the following functions:
map(string, array, function) // Perform a map operation on the array setting the `this[string]` to the ordered result return
map(string, object, function) // Perform a map operation on an object array setting the `this[string]` to the result return
map(string, string, function) // Lookup the first string and process it as a map operation according to its type (see above 2 examples) - This is used for late binding
NOTES:
- Each function is expected to return a result which is used to compose the new object / array.
- The object return type can also specify an alternate key as the third parameter to the callback (i.e.
callback(error, valueReturn, keyReturn)
). If unspecified the original key is used, if specified it is used instead of the original. - Late binding maps can accept the same input and output key name in order to overwrite the original.
See the test files for more examples.
.promise()
Alternative to end()
which returns a JS standard promise instead of using the .end(callback)
system.
promise() // Return a promise which will resolve with no value
promise(string) // Return a promise which will return with the extracted context value
promise(function) // Return a promise but also run a callback
promise(string, function) // Extract the context value specified by string and provide it to the callback
asyncChainable()
.then(doSomethingOne)
.then(doSomethingTwo)
.then(doSomethingThree)
.promise()
.then(function() {
.catch(function(err) {
.race()
Run multiple functions setting the named key to the first function to return with a non-null, non-undefined value.
If an error is thrown before or after the result is achived it will be returned instead.
asyncChainable()
.race('myKey', [
fooFunc,
barFunc,
bazFunc,
])
.end(function(err) {
console.log('myKey =', this.myKey);
});
.reset()
Clear the result buffer, releasing all results held in memory.
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await('foo', 'bar')
.then(console.log)
.reset()
.defer('quz', quzFunc)
.end(console.log)
.run()
Internal function to resolve a function, async function, promise etc. etc. and then run a callback.
run([context], fn, cb, [args])
.runArray()
Internal callback resolver. Run is used to execute an array of callbacks then run a final callback. This function is NOT chainable, will execute immediately and is documented here as it is useful when writing plugins.
runArray(array, limit, callback)
.runWhile()
Internal callback resolver until a function returns falsy. This function is NOT chainable, will execute immediately and is documented here as it is useful when writing plugins.
Unlike runArray()
this function does not require a precomputed array of items to iterate over which makes it a kind of generator function useful for potencially large data set iterations.
runWhile(function(next, index) {}, limit, callback)
.series() / .parallel()
Execute an array or object of functions either in series or parallel.
series(function)
series(string, function) // Named function (`this.name` gets set to whatever gets passed to `next()`)
series(array)
series(object) // Named function object (each object key gets assigned to this with the value passed to `next()`)
series(collection) // See 'object' definition
series(array, function) // Backwards compatibility with `async.series`
series(object, function) // Backwards compatibility with `async.series`
parallel(function)
parallel(string, function) // Named function (`this.name` gets set to whatever gets passed to `next()`)
parallel(array)
parallel(object) // Named function object (each object key gets assigned to this with the value passed to `next()`)
parallel(collection) // See 'object' definition
parallel(array, function) // Backwards compatibility with `parallel.series`
parallel(object, function) // Backwards compatibility with `parallel.series`
Some examples:
asyncChainable()
.parallel(Array)
.parallel(Object)
.parallel(Collection)
.parallel(String, function)
.parallel(function)
.end()
asyncChainable()
.series(Array)
.series(Object)
.series(Collection)
.series(String, function)
.series(function)
.end()
.set()
Set is a helper function to quickly allocate the value of a context item as we move down the chain.
set(string, mixed) // Set the single item in `this` specified the first string to the value of the second arg
set(object) // Merge the object into `this` to quickly set a number of values
set(function) // Alias for `series(function)`
set(string, function) // Alias for `series(string, function)`
It can be used as a named single item key/value or as a setter object.
asyncChainable()
.set('foo', 'foo value')
.then(function(next) { console.log(this.foo); next() })
.set({bar: 'bar value'})
.then(function(next) { console.log(this.foo); next() })
.set(baz, function(next) { next(null, 'baz value') })
.then(function(next) { console.log(this.foo); next() })
.end()
.timeout()
Set a delay and/or a callback to run if the Async chain goes over a specified timeout.
timeout(number) // Set the timeout delay
timeout(number, function) // Set the timeout delay + a callback
timeout(function) // Set the timeout callback
timeout(false) // Disable timeouts
asyncChainable()
.timeout(100, function() {
console.log('Timer went over 100ms!');
})
.then(function(next) {
setTimeout(next, 2000);
})
.end()
NOTE: Timeouts will also fire the timeout
hook if the timeout function is the default (i.e. the user hasn't changed the function to their own).
.then()
Execute a function, wait for it to complete and continue down the asyncChainable chain.
This function is an alias for series()
.
This can be considered the series process twin to then()
.
asyncChainable()
.then('foo', fooFunc)
.then('bar', barFunc)
.then('baz', bazFunc)
.end(console.log)
Context
Unless overridden by a call to .context()
, async-chainable will use its own context object which can be accessed via this
inside any callback function.
The context contains the results of any named functions as well as some meta data.
asyncChainable()
.series('foo', fooFunc)
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await('foo', 'bar')
.then(function(next) {
console.log('Context is', this);
next();
})
.await('baz')
.end(function(next) {
console.log('Context is', this);
next();
});
In addition to storing all named values the context object also provides the following meta object values.
Key | Type | Description |
---|
this._struct | Collection | The structure of the async chain constructed by the developer |
this._structPointer | Int | Offset in the this._struct collection as to the current executing function. Change this if you wish to move up and down |
this._options | Object | Various options used by async-chainable including things like the defer limit |
this._deferredRunning | Int | The number of running deferred tasks (limit this using .limit()) |
this._item | Mixed | During a forEach loop _item gets set to the currently iterating item value |
this._key | Mixed | During a forEach loop _key gets set to the currently iterating array offset or object key |
this._id | Mixed | During a defer call _id gets set to the currently defered task id |
this.fire | Function | Utility function used to manually fire hooks |
this.hook | Function | Utility function used to manually register a hook |
Each item in the this._struct
object is composed of the following keys:
Key | Type | Description |
---|
completed | Boolean | An indicator as to whether this item has been executed yet |
payload | Mixed | The options for the item, in parallel or series modes this is an array or object of the tasks to execute |
type | String | A supported internal execution type |
waitingOn | Int | When the type is a defer operation this integer tracks the number of defers that have yet to resolve |
Gotchas
A list of some common errors when using async-chainable.
Forgetting a final await()
when using end()
By default async-chainable will not imply an .await()
call before each .end()
call. For example:
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.end(console.log);
In the above no .await()
call is made before .end()
so this chain will immediately complete - async-chainable will not wait for the deferred tasks to complete.
asyncChainable()
.defer('foo', fooFunc)
.defer('bar', barFunc)
.defer('baz', bazFunc)
.await()
.end(console.log);
In the above async-chainable will wait for the deferred tasks to complete before firing the end condition.
Forgetting the ()
when initializing
Async-chainable needs to store state as it processes the task stack, to do this it instantiates itself as an object. This means you must declare it with an additional ()
after the require()
statement if you wish to use it straight away. For example:
var asyncChainable = require('async-chainable')();
asyncChainable()
.parallel([fooFunc, barFunc, bazFunc])
.series([fooFunc, barFunc, bazFunc])
.end(console.log)
If you want to use multiple instances you can use either:
var asyncChainable = require('async-chainable');
asyncChainable()
.parallel([fooFunc, barFunc, bazFunc])
.series([fooFunc, barFunc, bazFunc])
.end(console.log)
asyncChainable()
.parallel([fooFunc, barFunc, bazFunc])
.series([fooFunc, barFunc, bazFunc])
.end(console.log)
Its annoying we have to do this but without hacking around how Nodes module system works its not possible to return a singleton object like the async library does and also work with nested instances (i.e. having one .js file require() another that uses async-chainable and the whole thing not end up in a messy stack trace as the second instance inherits the firsts return state).
Useful techniques
Debugging
If you find that async-chainable is hanging try setting a .timeout()
on the object to be notified when something is taking a while.
For one off operations async-chainable will also respond to the DEBUG=async-chainable
environment variable. For example running your script as:
DEBUG=async-chainable node myscript.js
... will automatically set a .timeout(5000)
call on all async-chainable objects with the default timeout handler (which should give some useful information on anything that is hanging).
Make a variable number of tasks then execute them
Since JavaScript passes everything via pointers you can pass in an array or object to a .parallel() or .series() call which will get evaluated only when that chain item gets executed. This means that preceding items can rewrite the actual tasks conducted during that call.
For example in the below otherTasks
is an array which is passed into the .parallel() call (the second chain item). However the initial .then() callback actually writes the items that that parallel call should make.
var otherTasks = [];
asyncChainable()
.then(function(next) {
for (var i = 0; i < 20; i++) {
(function(i) {
otherTasks.push(function(next) {
console.log('Hello World', i);
next();
});
})(i);
}
next();
})
.parallel(otherTasks)
.end();
Compose an array of items then run each though a handler function
Like the above example async-chainable can be used to prepare items for execution then thread them into a subsequent chain for processing.
This is a neater version of the above that uses a fixed processing function to process an array of data.
var asyncChainable = require('./index');
var items = [];
asyncChainable()
.then(function(next) {
for (var i = 0; i < 20; i++) {
items.push({text: 'Hello World ' + i});
}
next();
})
.forEach(items, function(next, item) {
console.log(item);
next();
})
.end();