commit 67028344b6abd05d378eb8f06c67bdb8d9eb389d
Author: James Halliday
Date: Thu Dec 2 05:25:54 2010 +0000

Seq is a node.js library for chainable, asynchronous flow control. With Seq, you can turn complicated nested callback logic into a cleaner and more straightforward pipeline-style. Even the error handling is chainable! Plus, you can do fancy things like set limits on the number of parallel tasks executing at once.

Here's a simple example of Seq that executes some shell commands and reads files.

// parseq.js
var fs = require('fs');
var exec = require('child_process').exec;

var Seq = require('seq');
Seq()
    .seq(function () {
        exec('whoami', this)
    })
    .par(function (who) {
        exec('groups ' + who, this);
    })
    .par(function (who) {
        fs.readFile(__filename, 'ascii', this);
    })
    .seq(function (groups, src) {
        console.log('Groups: ' + groups.trim());
        console.log('This file has ' + src.length + ' bytes');
    })
;

In each .seq() and .par(), use this in place of a callback and the next action down the chain waiting on the result will fire when the result becomes available.

.seq() waits for all running actions to stop before itself firing and the next action in the chain doesn't fire until .seq() has yielded its result.

.par() executes immediately and moves along to the next action in the chain. .par()'s result is pushed onto the argument stack in the order it appeared in the chain, so you can chain together parallel actions and .seq() to join the results:

var Seq = require('seq');
Seq()
    .par(function () {
        var that = this;
        setTimeout(function () { that(null, 'a') }, 300);
    })
    .par(function () {
        var that = this;
        setTimeout(function () { that(null, 'b') }, 200);
    })
    .par(function () {
        var that = this;
        setTimeout(function () { that(null, 'c') }, 100);
    })
    .seq(function (a, b, c) {
        console.dir([ a, b, c ])
    })
;

$ node join.js 
[ 'a', 'b', 'c' ]

Here, despite 'c' finishing first, its result shows up last in the final call to .seq() because it was last in the chain of .par()s.

Errors are propagated through the first argument to this(). When an error occurs (a non-falsy value is given), it travels down the chain to the first .catch() it sees. If there hasn't been an error, .catch() just gets skipped. By default there is a .catch() at the end of all chains for uncaught errors that looks like:

.catch(function (err) {
    console.error(err.stack ? err.stack : err)
})

This approach gets rid of a lot of the duplication and hassle of error handling since most of the time when an error occurs you just want to print it and abort the transaction. You can throw in custom logic if you need it at the same time, so I figure this is a pretty reasonable default.

Seq borrows heavily from other flow control libraries like Step and Async.js but provides a chainable interface instead of array-based actions. This chainable approach means you don't need to chain multiple functions together in the same call, but more importantly gives fancier parallel flow control a more consistent interface. For instance:

// stat_all.js

var fs = require('fs');
var Hash = require('traverse/hash');
var Seq = require('seq');

Seq()
    .seq(function () {
        fs.readdir(__dirname, this);
    })
    .flatten()
    .parEach(function (file) {
        fs.stat(__dirname + '/' + file, this.into(file));
    })
    .seq(function () {
        var sizes = Hash.map(this.vars, function (s) { return s.size })
        console.dir(sizes);
    })
;

$ node stat_all.js 
{ 'join.js': 443
, 'parseq.js': 464
, 'stat_all.js': 404
}

In the above example, the first .seq() pulls down the directory list asynchronously from __dirname, flattens the results so they fill out the argument stack instead of the first argument, and computes the size of each of the files in parallel with .parEach().

.parEach() takes an optional concurrency limit as its first argument, so to make that previous example only have at most 2 asynchronous fs.stat() operations waiting at a time, just change:

.parEach(function (file) {
    fs.stat(__dirname + '/' + file, this.into(file));
})

to read:

.parEach(2, function (file) {
    fs.stat(__dirname + '/' + file, this.into(file));
})

Super easy! To read more about Seq and to check out the code, go to http://github.com/substack/node-seq. For a more complicated example of Seq in action, check out this DNode-based IRC bot I've been hacking on.

To install with npm, just do:

npm install seq

more
git clone http://substack.net/blog.git