I don't find async's waterfall very useful because of it's requirement to pass on state to the next function. That makes the signature of each step unique, difficult to refactor, and error-prone.
async.waterfall uses eachOfSeries under-the-hood and so does this.
No external dependencies but it depends on (and bundles) async-es/eachOfSeries
under the hood.
npm install steppin --save
A task/step signature is constant and always (state, next)
. The signature of next is always (err, result)
and the contents of result become associated with the key passed to steppin.
var steppin = require('steppin');
function calculate(color, size, callback) {
// fake lookup
setTimeout(() => callback(null, {
cost: 1299,
tax: 200,
total: 1499,
}));
}
steppin({
color: (state, next) => next(null, 'red'),
size: (state, next) => next(null, 'small'),
price: (state, next) => calculate(state.color, state.size, next),
}, (err, state) => {
console.log('state', JSON.stringify(state, null, 2));
});
Output:
{
"color": "red",
"size": "small",
"price": {
"cost": 1299,
"tax": 200,
"total": 1499
}
}
let initialState = { currency: 'usd' }; // Warning: value is not copied and steppin will modify the contents of this object
steppin({}, initialState, callback)
State is just a regular object and there is nothing stopping you from doing state.color = "red"
in a task instead of returning it to next.
However, if it's easily possible to avoid that you should. Avoiding that makes it easily understood what's happening, where everything is coming from, and how it can be reordered or refactored.
Because consistency is good and allows for other things. For example, with the consistency, you could easily implement a timeout wrapper around your tasks or any other wrapper at any point in the future:
var wrapInTimeout = (maxWait, step) => {
return (state, next) => {
var timeout = setTimeout(() => next(new Error('timeout')), maxWait);
var done = (err, result) => {
clearTimeout(timeout);
next(err, result);
};
step(state, done);
};
};
steppin({
one: wrapInTimeout(1000, (state, next) => {
thirdPartyServiceRequest(userId, next);
}),
})
In theory, the hash passed to steppin does not have the execution order guaranteed. However, in practice, I'm unaware of any engines or environments that would not execute in the order defined.
If you're still concerned, you can refactor the above example into this and it'll work:
steppin([
(state, next) => next(null, 'red'),
(state, next) => next(null, 'small'),
(state, next) => calculate(state.color, state.size, next),
], (err, state) => {
console.log('state', JSON.stringify(state, null, 2));
});
The keys will be a string that is the index of the step in the array:
{
"0": "red",
"1": "small",
"2": {
"cost": 1299,
"tax": 200,
"total": 1499
}
}