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();