chainbuilder
v2.2.0
Published
Construct a chainable library from a set of async functions.
Downloads
38
Maintainers
Readme
chainBuilder.js
Create chains out of your async functions.
To install: npm install chainbuilder --save
Write chains like this
var users = require('./users');
users
.find('user-bob')
.getGames()
.pluck('highScore')
.end(cb);
With functions you provide like this
// users.js
var chainBuilder = require('chainbuilder');
module.exports = chainBuilder({
methods: {
find: function (userId, cb) { ... },
getGames: function (cb) {
var user = this.previousResult();
...
},
pluck: function (key, cb) {
var object = this.previousResult();
cb(null, _.pluck(object, key));
}
}
});
So you can abandon code that looks like this
var findUser = function (userId, cb) { ... };
var getGames = function (gameIds, cb) { ... };
findUser('user-bob', function (err, user) {
if (err) return cb(err);
getGames(user.gameIds, function (err, games) {
if (err) return cb(err);
var highScores = _.pluck(games, 'highScore');
cb(null, highScores);
});
});
Alternatively, use one file per function
You can keep things tidy with one file per function and requireDir (you'll need to npm install require-dir --save
), like...
lib/
|- getHighScores.js
|- users/
|- index.js
|- find.js
|- getGames.js
|- pluck.js
// lib/users/index.js
var chainBuilder = require('chainbuilder');
var requireDir = require('require-dir');
module.exports = chainBuilder({ methods: requireDir('.') });
Mixins
Some common libraries are available as mixins. They're added to your chain via the mixins
option:
module.exports = chainBuilder({
methods: {/* ... your methods ... */},
mixins: [
require('chainbuilder-lodash')(), // Adds lodash methods like map, forEach, flatten etc...
require('chainbuilder-request')(), // Methods for making HTTP requests
require('chainbuilder-retry')({ // Methods for retrying erroring calls
retries: 3, maxTimeout: 100
}),
require('chainbuilder-flow')(), // Flow methods like if, while, each and map
require('chainbuilder-save')() // Methods for saving and re-injecting values in the chain
]
});
Available mixins:
Blocks
Some mixins (like flow and retry) contain "block" functions that conditionally run, or re-run parts of a chain. Block methods come in pairs with $begin
and $end
prefixes. They're called like:
myChain(3)
.$beginWhile(function (value) { return value < 15 })
.plus(1)
.times(3)
.$endWhile()
.plus(1)
.end(function (err, result) { console.log(result); /* > 40 */ });
Troubleshooting / Logging
Detailed logs can be generated with the log-console mixin.
API
chainBuilder(options)
Build a Chain
class. The methods you provide will be present, along with some helpers detailed below. Returns a Chain factory function.
e.g.
var request = chainBuilder({
methods: {
get: function (url, cb) {
http.get(url, function (response) {
if (response.statusCode === 200) cb(null, response.body) else cb(response.statusMessage);
});
},
getFromPreviousResult: function (cb) {
this.getMethod('get')(this.previousResult(), cb);
},
asJson: function (cb) {
cb(null, JSON.stringify(this.previousResult().body));
},
...
}
});
@param options.methods Object<string, function(..., function(\*,\*))>
a dictionary of functions that take a callback as their final parameter. The callback takes an error as the first parameter, and a result as the second. Each function is run with the currently running Chain
as this
, so will have access to methods like previousResult()
.
@return function(options):Chain
Chain
chaining methods
Methods you can use when constructing chains.
chain(initialValue) constructor
Create an instance of the chain. If initialValue is passed, the chain will start executing immediately. If not, it will wait for #run()
to be called.
@param initialValue *
(optional)
#yourMethod(...)
All methods you pass to chainBuilder(...)
are available on the constructed chain, with the same signature except for the callback. Each method has access to the context methods described below.
e.g.
request().get('http://jsonip.com').asJson()...
#tap(fn)
Peek at the current value in the chain. The passed function has access to context methods.
e.g.
request()
.get('http://jsonip.com')
.tap(function (err, result) { console.log('' + result); /* > {"ip":"123.123.101","about":"/about","Pro!":"http://getjsonip.com"} */ })
.asJson()
.tap(function (err, result) { console.log('' + result); /* > [object Object] */ })
.run()
@param fn function(\*,\*)
a callback that receives an error as the first parameter or the last call's result as the second.
#inject(value)
Inject the value into the chain (so it's available as .previousResult()
to the next call).
e.g.
request()
.inject('foobar')
.tap(function (err, result) { console.log(result); /* > 'foobar' */ })
.run()
@param value *
the value to inject.
#transform(fn)
Alter the current value in the chain. Called when the previous call returned successfully, or if one of the previous calls errors. The passed function has acces to context methods.
request()
.get('http://jsonip.com')
.asJson()
.transform(function (err, result, cb) { cb(null, result.ip); })
.tap(function (err, result) { console.log(result); /* > 123.123.101 */ })
.run()
@param fn function(\*,\*, function(\*,\*))
a function that receives an error as the first parameter or the last call's result as the second, and a callback as the final parameter that takes the transformed error or result.
#transformResult(fn)
Alter the current value in the chain. The transform function is passed the previousResult, and expected to return a new result. The passed function has acces to context methods.
request()
.get('http://jsonip.com')
.asJson()
.transformResult(function (result) { return result.ip; })
.tap(function (err, result) { console.log(result); /* > 123.123.101 */ })
.run()
@param fn function(\*,\*, function(\*,\*))
a function that receives an error as the first parameter or the last call's result as the second, and a callback as the final parameter that takes the transformed error or result.
#recover(fn)
Recover from an error thrown by one of the previous calls in the chain. Similar to transform, but only called if one of the previous calls errored and is only passed the error and cb.
request()
.get('INVALID')
.asJson() // will not be called, as the above call threw an error
.recover(function (err, cb) { cb(null, '0.0.0.0'); })
.tap(function (err, result) { console.log(result); /* > 0.0.0.0 */ })
.run()
@param fn function(\*,\*, function(\*,\*))
a function that receives an error as the first parameter or the last call's result as the second, and a callback as the final parameter that takes the transformed error or result.
#run(initialValue, cb)
Run the chain from the beginning, with initialValue available to the first method via previousResult()
.
e.g.
var jsonParser = request()
.getFromPreviousResult()
.asJson();
jsonParser.run('http://jsonip.com', function (err, result) { console.log('' + result); /* > [object Object] */ });
@param initialValue *
(optional) initial value to start the chain with.
@param cb function(\*,\*, function(\*,\*))
(optional) execute a chain from the beginning.
#clone()
Create a clone of the chain.
#end(fn)
Get the final result in the chain (really just a more final sounding alias of #tap
).
context methods
Methods you can use from within your functions.
this.previousResult()
The result provided by the previous call in the chain.
@return String
this.getMethod(methodName)
Gets a method passed via the methods options.
@param String
methodName the name of the method
@return Function
this.newChain(initialValue)
Create a new chain object (will show in logs as a sub-chain).
@param String
initialValue the name of the method
@return Chain
Creating mixins
A mixin is merely a map of functions like methods
. Each function just needs to take a callback as its final parameter, and has access to all the context methods.
Block mixins
Are created by defining a begin and end method with the $beginSubchain
/$endSubchain
set like so:
var beginEach = function (done) {
// Pass the previous result on to the end method
done(err, this.previousResult());
};
var endEach = function (chain, done) {
// Pass the previous result on to the end method
var array = this.previousResult();
var next = function (err) {
if (err) return done(err);
if (array.length === 0) return done();
chain.run(array.pop(), next);
};
};
beginEach.$beginSubchain = 'each';
endEach.$endSubchain = 'each';
module.exports = {
$beginEach: beginEach,
$endEach: endEach
};
The end method will be passed the subchain as its first parameter.
By convention, begin methods always start $begin
and end methods with $end
. They also need to have the .$beginSubchain
and .$endSubchain
values set to the same value (for identifying them as block methods and detection of unclosed / mismatched blocks). There are lots of examples of block mixins in the chainbuilder-flow mixin.
Behavior
Execution
if a parameter is provided to the initial chain call, it will start executing immediately with that as the initial value. Otherwise, it will wait for
#run()
to be called.each call merely returns the original instance, not a clone, so breaking a chain won't create a new one (like it does for lodash). This can result in some confusing behavior such as:
var a = mathChain(); var b = a.add(2); var c = a.add(3); b.run(1, function (err, result) { /* result === 6 */ }); c.run(1, function (err, result) { /* result === 6 */ });
to create a clone at a certain point, call the clone() method. e.g:
var a = mathChain().initialNumber(1); var b = a.clone().add(2); var c = a.clone().add(3); b.run(1, function (err, result) { /* result === 3 */ }); c.run(1, function (err, result) { /* result === 4 */ });
Errors
- errors can be provided as the first argument of the callback or thrown
- if an error occurs, subsequent calls will be skipped until
end(...)
,transform(...)
orrecover(...)
are encountered.
Version History
2016-02-08 v2.2.0
- Provide stack traces to logging (when
enableStack: true
is provided as a chainbuilder option) - Add #cleanStack context method
2016-02-08 v2.1.1
- Log calls to 'end' as 'chainEnd'
2016-02-08 v2.1.0
- Enable logging mixins via fn.$loggingHandler
- Remove logging in favor of log-console mixin
2016-01-22 v2.0.15
- Add instanceOf validation to args
2016-01-22 v2.0.14
- Add argument validation via fn.$previousResult and fn.$args
2016-01-06 v2.0.13
- Fix logging of undefined/null
2016-01-06 v2.0.12
- Fix logging of errors + functions
2016-01-06 v2.0.11
- refactor, moving subchain tracking from Chain to CallQueue
- add
.parent
to CallContext
2015-12-31 v2.0.10
- support mixin-provided context methods
2015-12-30 v2.0.9
- fix logging of objects with circular reference
2015-12-30 v2.0.8
- fix logging output for subchains within aggregate functions.
2015-12-30 v2.0.7
- add
#newChain()
context method. - tweak subchain logging output.
2015-12-30 v2.0.6
- improve logging.
2015-12-29 v2.0.5
- improve logging.
2015-12-29 v2.0.4
- add logging with optional dependency debug.
2015-12-29 v2.0.3
- add
#transformResult()
.
2015-12-29 v2.0.1
- add
#inject()
- make all
#run(initialValue, cb)
params optional.
2015-12-29 v2.0.0
- introduction of
#run(initialValue, cb)
, and deferred running of chain unless an initial value is provided. - introduction of
#clone()
. - support for subchains.
- function context separated from chain object.
- removal of
#eachResult()
and#save()
/#restore()
. They'll be readded later as mixins.