node.jsで使える非同期コントロールフローライブラリ nue その10 - node-seqのサンプルと比較

GREEではnode-seqを使っているんですね。


気になったのでnode-seq(https://github.com/substack/node-seq/tree/master/examples)のサンプルをnueで実装したらどうなるか試してみました。

stat_all.js
var flow = require('nue').flow;
var fs = require('fs');

flow('stat_all')(
  function readdir() {
    fs.readdir(__dirname, this.async());
  },
  function stat(files) {
    process.nextTick(this.async(files));
    files.forEach(function (file) {
      fs.stat(__dirname + '/' + file, this.async())
    }.bind(this));
  },
  function end(files) {
    if (this.err) throw this.err;
    var statsArray = this.args.slice(1);
    var sizes = files.reduce(function (obj, key, i) {
      obj[key] = statsArray[i].size;
      return obj;
    }, {});
    console.log(sizes);
    this.next();
  }
)();
parseq.js
var flow = require('nue').flow;
var fs = require('fs');
var exec = require('child_process').exec;

flow('parseq')(
  function whoami() {
    exec('whoami', this.async());
  },
  function parallel(who) {
    exec('groups ' + who, this.async());
    fs.readFile(__filename, 'ascii', this.async());
  },
  function end(groups, stderr, src) {
    if (this.err) throw this.err;
    console.log('Groups: ' + groups.trim());
    console.log('This file has ' + src.length + ' bytes');
    this.next();
  }
)();
join.js
var flow = require('nue').flow;

flow('join')(
  function parallel() {
    setTimeout(this.async('a'), 300);
    setTimeout(this.async('b'), 200);
    setTimeout(this.async('c'), 100);
  },
  function end(a, b, c) {
    if (this.err) throw this.err;
    console.dir([ a, b, c ]);
    this.next();
  }
)();


感触としては、stat_all.jsについてははnode-seqのほうが簡潔に書けた、parseq.jsとjoin.jsについてはnueのほうが簡潔に書けた、という感じです。


nueは配列の扱いにもう一工夫あってもいいかもしれないなあ。配列のforEachがあれば事足りるとか思ったものの。
イメージとしてはこういう感じで書けるといいかも。

上記stat_all.js改良イメージ
var flow = require('nue').flow;
var each = require('nue').each;
var fs = require('fs');

flow('stat_all')(
  function readdir() {
    fs.readdir(__dirname, this.async());
  },
  each(function stat(file) {
    fs.stat(__dirname + '/' + file, this.async(file))
  }),
  function end() {
    if (this.err) throw this.err;
    var sizes = this.args.reduce(function (obj, result) {
      obj[result[0]] = result[1].size;
      return obj;
    }, {});
    console.log(sizes);
    this.next();
  }
)();

node.jsで使える非同期コントロールフローライブラリ nue その9 - 0.2.0リリース

0.2.0をリリースしました。npm install nue でインストールできます。

CHANGELOG
  • 非同期コールバックに渡されるエラーはNueAsyncErrorでラップして通知しデバッグしやすくしました。
  • ハンドルされなかったエラーはNueUnhandledErrorでラップして通知しデバッグしやすくしました。
  • flowに名前を付けられるようにしました。
  • step関数(flowに渡される関数)内で、this.flowNameを使用できるようにしました。
  • step関数内で、this.stepNameを使用できるようにしました。
  • step関数内で、this.end関数が最初の引数でエラーオブジェクトを受け取らなくなりました。エラーでflowを終了したい場合は単にそのエラーをthrowしてください。


以下、代表的なものについて簡単に説明します。

非同期コールバックに渡されるエラーのハンドリング


非同期コールバックに渡されるエラーの問題点は、発生個所がわかりにくいことです。nueでは、flowやflowの内側の関数(stepと呼びます)の名前をエラーに含めることで、エラーの発生個所を特定しやすいようにしました。

例として次のコードを考えます。flow関数の呼び出しに'readFileFlow'という文字列を渡していますが、これがflowの名前です。名前は必須ではないのですが、これがあるとデバッグしやすくなります。エラー情報にこの名前を使うからです。(名前がない場合は単に空文字が使われる)。それから、同じ理由でstepにも名前をつけています。

var flow = require('nue').flow;
var fs = require('fs');

var myFlow = flow('readFilesFlow')(
  function readFiles(file1, file2) {
    fs.readFile(file1, 'utf8', this.async());
    fs.readFile(file2, 'utf8', this.async());
  },
  function concat(data1, data2) {
    this.next(data1 + data2);
  },
  function end(data) {
    if (this.err) throw this.err;
    console.log(data);
    console.log('done');
    this.next();
  }
);
myFlow('file1', '存在しないファイル名');


このflowの定義をmyFlowという変数で受けて、2番目のパラメータに存在しないファイル名を渡して実行します。存在しないファイルを読もうとするので実行結果はエラーになりますが、そのときの出力内容はこうなります。

NueAsyncError: cause = [Error: ENOENT, no such file or directory '存在しないファイル名'], 
flowName = 'readFilesFlow', stepName = 'readFiles', stepIndex = 0, asyncIndex = 1
    at /Users/toshihiro/WebstormProjects/nue/lib/nue.js:194:39
    at [object Object].<anonymous> (fs.js:88:5)
    at [object Object].emit (events.js:67:17)
    at Object.oncomplete (fs.js:1058:12)


まずは、最初のNueAsyncErrorという記述で非同期コールバックにエラーが渡されたことを示します。それから各情報は次のとおりです。

  • cause: 非同期コールバックに渡されたエラー
  • flowName: flowの名前
  • stepName: stepの名前
  • stepIndex: stepがflowの中において何番目に定義されているか
  • asynIndex: stepの中において何回目のthis.asyncが返すコールバックで発生したか

this.flowNameとthis.stepName

こんな使い方ができます。ちょっとした挙動確認(ちゃんとstepが呼ばれているか?とか)に便利です。

var flow = require('nue').flow;

function log() {
  console.log(this.flowName + '.' + this.stepName);
}

var myFlow = flow('myFlow')(
  function step1() {
    log.call(this);
    this.next();
  },
  function step2() {
    log.call(this);
    this.next();
  },
  function step3() {
    if (this.err) throw this.err;
    log.call(this);
    this.next();
  }
);
myFlow();


出力はこうなります。

myFlow.step1
myFlow.step2
myFlow.step3

this.end関数の仕様変更


これまでは、次のような書き方ができました。

var flow = require('nue').flow;

var myFlow = flow(
  function step1() {
    this.end(new Error('hoge')); // step3へジャンプ。 step3のthis.errはここで渡したエラーオブジェクト。
  },
  function step2() {
    this.next();
  },
  function step3() {
    if (this.err) throw this.err;
    this.next();
  }
);
myFlow();


しかし、上記のサポートはやめて、エラーでflowを終了するときは単にthrow new Error()をしてもらうことになりました。this.endは正常系のflowの終了のみに使えます。

var flow = require('nue').flow;

var myFlow = flow(
  function step1() {
    throw new Error('hoge'); // step3へジャンプ。step3のthis.errはここでthrowしたエラーオブジェクト。
  },
  function step2() {
    this.next();
  },
  function step3() {
    if (this.err) throw this.err;
    this.next();
  }
);
myFlow();