はじめに
「Promiseはもうネイティブにあるから、外部ライブラリはいらない」——そう思っている方も多いのではないでしょうか。確かにasync/awaitが使える今、Promiseを作るだけならBluebirdは不要です。
ですが、複数のリクエストを「同時に3件まで」に制限したい、時間がかかりすぎた処理を打ち切りたい、コールバック形式の古いAPIをPromise化したい——こうした場面になると、ネイティブのPromiseだけでは手が足りなくなります。Bluebirdは、こうした「Promiseの一歩先」を長年にわたり提供し続けているライブラリです。
先にどんなことができるのか触ってみたい方は、こちらからどうぞ。
Bluebirdとは
Bluebirdは、Petka Antonov氏が開発したJavaScript向けのPromiseライブラリです。Promises/A+仕様に完全準拠しながら、標準のPromiseにはない同時実行数の制御・タイムアウト・キャンセル・デバッグ支援機能などを追加で提供します。2013年の登場以来、パフォーマンスの高さで広く知られ、Node.jsの主要フレームワークやライブラリの内部実装にも採用されてきました。
主な特徴
- 豊富なコレクションAPI -
Promise.map()やPromise.each()など、配列に対する非同期処理を同時実行数付きで扱えます - キャンセル可能なPromise -
.cancel()で不要になった処理を途中で打ち切れます(ネイティブPromiseにはない機能です) - promisifyによる変換 - コールバック形式の関数を
Promise.promisify()でPromiseベースに変換できます - デバッグ支援 - long stack tracesや、返し忘れたPromiseへの警告など、非同期処理特有のバグを見つけやすくします
インストール
npm install bluebird
yarn add bluebird
Bluebirdのサンプルを動かす
Bluebirdの代表的な機能がPromise.map()による同時実行数の制御です。配列の各要素に非同期処理を適用しつつ、concurrencyオプションで同時に走らせる件数を絞れます。下のサンプルは6件の疑似タスクを、指定した同時実行数で処理する様子をログ表示します。
import Bluebird from 'bluebird'
const userIds = [1, 2, 3, 4, 5]
// concurrency: 2 なら常に最大2件までしか同時実行されない
Bluebird.map(userIds, (id) => fetchUser(id), { concurrency: 2 })
.then((users) => console.log(users))
同時実行数を1にすると1件ずつ順番に、6にすると全件が一斉に走ります。数値を変えて「開始」と「完了」のタイミングがどう変わるか確認してみてください。
同時実行数を減らすほど、次のtaskの「開始」ログが前のtaskの「完了」を待ってから出るようになります。APIのレート制限を守りながら大量のリクエストを捌きたいときに、Promise.map()のこのオプションがそのまま使えます。
基本的な使い方
BluebirdのPromiseは、ネイティブのPromiseと同じようにnew Bluebird((resolve, reject) => {...})で生成し、.then()や.catch()で繋げます。加えて、値を変更せずに副作用だけを実行する.tap()のような、Bluebird独自のメソッドも使えます。
import Bluebird from 'bluebird'
function delay(ms, value) {
return new Bluebird((resolve) => setTimeout(() => resolve(value), ms))
}
delay(300, 'done')
.tap((v) => console.log('tap:', v)) // 値をそのまま次に渡しつつログだけ出す
.then((v) => console.log('then:', v))
.catch((err) => console.error(err))
実践的なユースケース
promisifyでコールバックAPIをPromise化
古いライブラリや自作のユーティリティには、(err, result)を受け取るコールバック形式の関数がまだ残っています。Bluebird.promisify()を使えば、こうした関数を1行でPromiseベースの関数に変換できます。
import Bluebird from 'bluebird'
function legacyGetUser(name, callback) {
setTimeout(() => {
if (!name) return callback(new Error('nameは必須です'))
callback(null, { name, id: Math.floor(Math.random() * 1000) })
}, 300)
}
const getUserAsync = Bluebird.promisify(legacyGetUser)
getUserAsync('taro').then((user) => console.log(user))
下のサンプルでは、ユーザー名を空にして実行するとcallback(new Error(...))側の分岐に入り、エラーメッセージが表示されます。値を入れ直せば成功のレスポンスに切り替わります。
timeoutで処理に制限時間を設ける
外部通信を伴う処理は、いつまでも終わらないリスクを常に抱えています。.timeout(ms)を繋げるだけで、指定時間を超えた場合にBluebird.TimeoutErrorで失敗させられます。
import Bluebird from 'bluebird'
function slowTask(ms) {
return new Bluebird((resolve) => setTimeout(resolve, ms))
}
slowTask(3000)
.timeout(1000, '1秒以内に終わりませんでした')
.then(() => console.log('成功'))
.catch(Bluebird.TimeoutError, (err) => console.error(err.message))
処理時間とタイムアウト時間をそれぞれ入力して実行してみてください。処理時間がタイムアウト時間を上回ると、.catch(Bluebird.TimeoutError, ...)の分岐に入ります。
cancelで不要になった処理を中断する
ユーザーが画面を離れたのに裏でリクエストが動き続けている、という状況を避けたいことがあります。BluebirdではBluebird.config({ cancellation: true })を有効にすると、生成したPromiseに.cancel()を呼べるようになります。
import Bluebird from 'bluebird'
Bluebird.config({ cancellation: true })
const promise = new Bluebird((resolve, reject, onCancel) => {
const timer = setTimeout(resolve, 5000)
onCancel(() => clearTimeout(timer)) // キャンセル時の後始末
})
cancelButton.onclick = () => promise.cancel()
「処理を開始」を押した3秒後に完了する処理に対して、「キャンセル」を押すとonCancel内のclearTimeout()が呼ばれ、完了メッセージが表示される前に処理が打ち切られます。
.timeout()が「時間切れで自動的に失敗させる」機能だったのに対し、.cancel()は「呼び出し側の都合で任意のタイミングで中断する」機能です。検索のオートコンプリートのように、新しい入力が来たら前のリクエストを打ち切りたい場面で役立ちます。
まとめ
Bluebirdは、ネイティブPromiseだけでは足りない「同時実行数の制御」「タイムアウト」「キャンセル」「コールバックAPIの変換」を、Promise.map()や.timeout()、.cancel()、Promise.promisify()といった具体的なAPIとして提供してくれるライブラリです。すべての場面でネイティブPromiseの代わりになる必要はありませんが、これらの機能が必要になったときの選択肢として覚えておくと、いざという時に役立つはずです。
まずは自分のプロジェクトで、大量のリクエストをPromise.map()のconcurrencyオプションで制御するところから試してみてはいかがでしょうか。
