かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

JavaScript(node.js)で非同期処理を扱いたい(coモジュール使ってみたよ)

1つ前のエントリで async モジュール使ってみました。

blog.okazuki.jp

関数の配列を渡してやれば、よしなに実行してくれるいいやつでした。次は co です!

www.npmjs.com

async が古き良き仕組みの上に非同期処理をいい感じに扱うという観点のライブラリという雰囲気を感じましたが co は async/await は無いけど Promise とジェネレーターを使って非同期処理をいい感じに書こうという雰囲気です。

ジェネレーターについては MDN がいい感じにまとまってます。

developer.mozilla.org

co

以下のコマンドで導入でいます。

npm i co

co は非常にシンプルで co の引数にジェネレーター関数を渡すだけです。そして戻り値に対してthenを呼び出して正常に完了したときの処理とエラーが起きたときの処理を渡します。 なので、基本的にこうなります。

co(function*() { ... })
.then(function(result) {}, function(error) {});

ということで試しにやってみましょう。

var co = require('co');

function main() {
    co(function*() {
        var process1 = yield Promise.resolve(1);
        var process2 = yield Promise.resolve(2);
        var process3 = yield Promise.resolve(3);
        return [process1, process2, process3];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行すると以下のように表示されます。

[1,2,3]

試しに例外を発生させてみます。

var co = require('co');

function main() {
    co(function*() {
        var process1 = yield Promise.resolve(1);
        var process2 = yield Promise.resolve(2);
        var process3 = yield Promise.resolve(3);
        throw new Error('oops!!');
        return [process1, process2, process3];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行するとエラーのほうのフローにわたってることが確認できます。

error:  oops!!

実際に非同期処理っぽいことをしてみましょう。setTimeoutをラップした関数を用意して重たい処理をエミュレートします。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

function main() {
    co(function*() {
        var process1 = yield asyncProcess('process1');
        var process2 = yield asyncProcess('process2');
        var process3 = yield asyncProcess('process3')
        return [process1, process2, process3];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行するとこうなります。

["process1 completed at 2017-08-23T06:36:50.560Z","process2 completed at 2017-08-23T06:36:51.560Z","process3 completed at 2017-08-23T06:36:52.561Z"]

Promise の配列に対しても co の中で yield 出来たりします。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

function main() {
    co(function*() {
        return yield [
            asyncProcess('process1'),
            asyncProcess('process2'),
            asyncProcess('process3'),
        ];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

この場合は当然ですがprocess1 - 3 はパラレルに走りますね。

["process1 completed at 2017-08-23T06:39:23.346Z","process2 completed at 2017-08-23T06:39:23.347Z","process3 completed at 2017-08-23T06:39:23.347Z"]

さらにプロパティが Promise を含むオブジェクトに対しても yield 出来ます。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

function main() {
    co(function*() {
        return yield {
            process1: asyncProcess('process1'),
            process2: asyncProcess('process2'),
            process3: asyncProcess('process3'),
            other: 'other value',
        };
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行するとこんな感じです。

{"process1":"process1 completed at 2017-08-23T06:41:23.638Z","process2":"process2 completed at 2017-08-23T06:41:23.638Z","process3":"process3 completed at 2017-08-23T06:41:23.638Z","other":"other value"}

この他にもジェネレータ関数を Promise を返す関数にしてくれる co.wrap(function*(arg) {}) というのがあります。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

// (number, number): Promise<number> みたいな
var asyncDiv = co.wrap(function* (x, y) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            if (y == 0) {
                return reject('divide by zero');
            }
            resolve(x / y);
        }, 1000);
    });
});

function main() {
    asyncDiv(10, 2).then(function(answer) { 
        console.log('answer: ', answer); 
    }, function(error) { 
        console.log('error: ', error); 
    });
    asyncDiv(10, 0).then(function(answer) { 
        console.log('answer: ', answer); 
    }, function(error) { 
        console.log('error: ', error); 
    });
}

main();

実行するとこうなります。

answer:  5
error:  divide by zero

実際に使ってみよう

ということで async のほうでもやった azure-storage を使った例を co でもやってみます。 こんな雰囲気ですかね?

var co = require('co');
var uuid = require('uuid/v4');
var azure = require('azure-storage');

function main() {
    co(function*() {
        var tableService = azure.createTableService('<accont name>', '<key>');
        var entGen = azure.TableUtilities.entityGenerator;
        yield new Promise(function(resolve, reject) {
            tableService.createTableIfNotExists('cosample', function(error, result) {
                if (error) {
                    return reject(error);
                }
                resolve(result);
            });
        });
        console.log('table created');
        var entity = {
            PartitionKey: entGen.String('sample'),
            RowKey: entGen.String(uuid()),
            Message: entGen.String('Hello world at ' + new Date().toISOString())
        };
        return yield new Promise(function(resolve, reject) {
            tableService.insertEntity('cosample', entity, function(error, result, response) {
                if (error) {
                    return reject(error);
                }
                resolve([result, response]);
            });
        });
    }).then(function(result) {
        console.log('result is ', JSON.stringify(result[0]));
        console.log('response is ', JSON.stringify(result[1]));
    }, function(error) {
        console.log('error is ', JSON.stringify(error));
    });
}

main();

実行すると、こんな感じになりました。

table created
result is  {".metadata":{"etag":"W/\"datetime'2017-08-23T07%3A03%3A01.8221368Z'\""}}
response is  {"isSuccessful":true,"statusCode":204,"body":"","headers":{"cache-control":"no-cache","content-length":"0","etag":"W/\"datetime'2017-08-23T07%3A03%3A01.8221368Z'\"","location":"https://funcrelationacf6.table.core.windows.net/cosample(PartitionKey='sample',RowKey='bd89259e-442c-4f20-b2cb-6180d15fcfbd')","server":"Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0","x-ms-request-id":"07a43392-0002-0002-70dd-1b9917000000","x-ms-version":"2017-04-17","x-content-type-options":"nosniff","preference-applied":"return-no-content","dataserviceid":"https://funcrelationacf6.table.core.windows.net/cosample(PartitionKey='sample',RowKey='bd89259e-442c-4f20-b2cb-6180d15fcfbd')","date":"Wed, 23 Aug 2017 07:03:01 GMT","connection":"close"}}

azure-storage モジュールの API が Promise ベースじゃないので幸福度が上がりませんね。 なので、自前で Promise が戻り値になるような API にラップしてみました。 ついでに、ここにきてアロー関数も使ってみました。

var co = require('co');
var uuid = require('uuid/v4');
var azure = require('azure-storage');

class MyTableService {
    get tableService() {
        return this._tableService ||
            (this._tableService = azure.createTableService('funcrelationacf6', 'Fg3DQ0pfmJGMKrpdd1IcIOQ8brUsz2/BBTHlctx/RClAlkwJYZkW+6c5m1GnC7+cKYIXhkbxQ1CMJSNwRE2fbg=='));
    }

    createTableIfNotExists() {
        return new Promise((resolve, reject) => {
            this.tableService.createTableIfNotExists('cosample', (error, result, response) => {
                if (error) {
                    return reject(error);
                }
                resolve({
                    result: result,
                    response: response
                });
            });
        });
    }

    insertEntity(entity) {
        var entGen = azure.TableUtilities.entityGenerator;
        return new Promise((resolve, reject) => {
            var azureStorageEntity = {
                PartitionKey: entGen.String(entity.PartitionKey),
                RowKey: entGen.String(entity.RowKey),
                Message: entGen.String(entity.Message)
            };
            this.tableService.insertEntity('cosample', azureStorageEntity, (error, result, response) => {
                if (error) {
                    return reject(error);
                }
                resolve({
                    result: result,
                    response: response
                });
            });
        });
    }
}

function main() {
    co(function* () {
        try {
            var tableService = new MyTableService();
            yield tableService.createTableIfNotExists();
            console.log('table created');
            var entity = {
                PartitionKey: 'sample',
                RowKey: uuid(),
                Message: `Hello world at ${new Date().toISOString()}`
            };
            var r = yield tableService.insertEntity(entity);
            console.log(`result is ${JSON.stringify(r.result)}`);
            console.log(`response is ${JSON.stringify(r.response)}`);
        } catch (error) {
            console.log(`error: ${error.message}`);
        }
    });
}

main();

ラップするのは苦行だけど悪くないね。

ラップの自動化

世の中には怠け者(誉め言葉)がいるみたいで、このラッピングを自動でしてくれるようなライブラリ作者がいるみたいです。 bluebird モジュールがそれになるみたいです。早速入れてみましょう。

npm i bluebird

これを使うと promisifyAll でさくっと Promise 化してくれます。

var co = require('co');
var uuid = require('uuid/v4');
var azure = require('azure-storage');
var Promise = require('bluebird');

function main() {
    co(function* () {
        try {
            var entGen = azure.TableUtilities.entityGenerator;
            var tableService = Promise.promisifyAll(azure.createTableService('<storage account>', '<key>'));
            yield tableService.createTableIfNotExistsAsync('bluebirdsample');
            console.log('table created');
            var entity = {
                PartitionKey: entGen.String('sample'),
                RowKey: entGen.String(uuid()),
                Message: entGen.String(`Hello world at ${new Date().toISOString()}`)
            };
            var r = yield tableService.insertEntityAsync('bluebirdsample', entity);
            console.log(`result is ${JSON.stringify(r)}`);
        } catch (error) {
            console.log(`error: ${error.message}`);
        }
    });
}

main();

ただ、azure-storage のコールバックは(error, result, response)という3つの引数を受け取るものが多い印象なのですが、こういうインターフェースだと response がロストしてるように感じます。う~む。

preview 版を使う??

まぁ、こういう不満は皆持ってるみたいで azure-storage 2.0.0-preview で Promise 対応が入ってます。

github.com

github.com

ただ、4月に preview が出てから4か月たってるけど正式リリースはまだみたいですね? そして、これは azure という超巨大モジュールの preview には適用されてるけど azure-storage モジュールには適用されてなさそう?

次は、これを調べてみたいと思います。今回は co がメインだしね!