To-Series is a zero-dependency control-flow module for running functions in order in a promise-like fashion. Provides some nice semantics for readability and a result object that describes the operations.
Why another control-flow library, especially in the world of async/await? There are some reasons:
- Lots of code still uses callbacks
- This one is really easy to read
- Aren't promises+async/await just another abstraction that remove callback hell, but add try/catch hell? And then you need to remember what dependencies/libraries use promises, and which don't? Just saying...
- You can still put promise code in each of these steps.
- You need
.each()
(that runs in a series) and a.while()
when controlling flow.
Because reading code is the easiest way to understand.
var series = require('to-series');
series()
.first((done) => {
setTimeout(function() {
console.log(1);
return done();
}, 1000);
})
.next(() => { console.log(2); }) // not async
.next((done) => {
setTimeout(function() {
console.log(3);
return done();
}, 1000);
})
.finally((done) => {
setTimeout(function() {
console.log(4);
return done();
}, 1000);
})
.end((err, info) => { // where everything is actually executed
console.log('Done');
console.log(info); // { count: 4, early: false }
});
Since this is a control flow library, and not necessarily only an async library, it also supports synchronous functions. Asynchronous functions need 1 callback, and synchronous functions need 0. If you pass a function with more than 1 argument defined, to-series
will throw an error.
Synchronous (or zero-argument) functions will be executed in a try {} catch() {}
block, and if an error occurs, it will be passed to the .end
function.
When calling the done()
callback in async functions, you can pass an error and futher steps in the flow will not execute. The .end
will receive the error.
You'll notice that the API includes .first
, .next
and .finally
. These are exactly the same as each other under-the-hood in to-series
. If you choose to use them, they can provide a nice readability to the code.
If you want a promise-like experience, you can chain the functions together.
//var series = require('to-series');
var num = 0;
var async = function(done) {
++num;
setTimeout(function() {
console.log('function', num);
return done();
}, 250);
};
var iterator = function(item, done) {
num+=item;
setTimeout(function() {
console.log('function', num);
return done();
}, 250);
};
var condition = function() {
return num < 5;
};
series()
.first(async)
.while(condition, async)
.each([1, 2, 3], iterator)
.next(async)
.finally(async)
.end((err) => { console.log('Done.') });
/* Console output:
$ node test/basic-example.js
function 1
function 2
function 3
function 4
function 5
function 6
function 8
function 11
function 12
function 13
Done.
*/
Chaining can be convenient, and look nice too, but sometimes it's just as nice to conditionally add functions to the flow. Consider the following (using setTimeout
to simulate database/IO):
var series = require('to-series');
var env = 'development',
passwordValidated = false;
var userAuthentication = series(); // establish a variable for this flow
userAuthentication.first((done) => {
console.log('Look up user');
setTimeout(function() { return done(); }, 250);
});
if (env === 'development') {
userAuthentication.next((done) => {
console.log('Send welcome email example.');
setTimeout(function() { return done(); }, 250);
});
}
userAuthentication.next((done) => {
console.log('Validate password');
passwordValidated = true;
setTimeout(function() { return done(); }, 250);
});
userAuthentication.next((done) => {
console.log('Check password');
if (!passwordValidated) userAuthentication.finish(); // not validated? Jump straight to `.end` and skip the rest
setTimeout(function() { return done(); }, 250);
});
userAuthentication.next((done) => {
if (passwordValidated) {
console.log('Log successful login');
}
setTimeout(function() { return done(); }, 250);
});
userAuthentication.end((err, info) => {
console.log('Done');
console.log(info); // { count: 5, early: false }
});
/* =>
Look up user
Send welcome email example.
Validate password
Check password
Log successful login
Done
*/
Had userAuthentication.finish();
run, early: true
would be present in the info
object, and count: 4
.
(And aliases .first()
and .finally()
)
Add a function to the control-flow. The function can accept 0 or 1 argument. 1 argument functions are assumed to be passed a callback that signifies that it's done.
Run a condition function and another function until the condition funciton no longer returns true. (New in 0.0.3)
let counter = 1;
series()
.first(cb => {
return cb();
})
.while(() => counter < 3, cb => {
counter++;
return cb();
})
.end(err => {});
Run a function on each of the elements of the supplied array. The array of items should be the first agrument, and the iterator function should be the second. The function does not need to have a callback, but can in case it's async. Array iterators process one after the other, and not all at once. (New in 0.0.3)
// with callback
let counter = 1;
series()
.first(cb => {
return cb();
})
.each([1, 2, 3], (item, cb) => {
counter+=item;
return cb();
})
.end(err => {});
// without callback
let counter = 1;
series()
.first(cb => {
return cb();
})
.each([1, 2, 3], (item) => {
counter+=item;
})
.end(err => {});
Abort the rest of the control flow steps and go straight to .end()
after the current function runs.
Clear the current setup so that you can reuse the named flow. If you are done with the whole process, and want to re-use it for something else, you can clear it out.
This is the final callback/function for when the process is complete. And, it actually triggers the whole process to start. If you never call .end()
, your flow never starts. It accepts a callback with 2 arguments: err
and info
. You don't need to specify either of them if you're not interested in them.
.end(function(err, info) {
if(err) {
// do something!
}
console.log('I am done with this control flow.');
});