Web Worker と TypeScript FSA

Web Worker を使ってみると数種類のメッセージをやり取りするときのペイロードの型安全性が気になったため、 Redux を使うときにお世話になっている TypeScript FSA を Web Worker に対して使ってみました。
いくつかの注意点はあれど、簡単に使ってみた感じでは問題なく動きました。

サンプルプログラムはこちらから

Web Worker

Web Worker とは、メインスレッド(UI) でなくバックグラウンドでもスクリプト処理する仕組みの一つです。重い処理を UI スレッドから逃すことで UI 描画の遅延を防ぐことができます。

Web Worker と UI スレッドとの間は postMessage と event listener によりオブジェクトを互いに受送信してやりとりします。
複数種類のメッセージをやりとりする場合は Web Worker を使用する - Web API | MDN § 高度な JSON データの引き渡しと振り分けシステムを作成する のように振り分け情報を付与して対応するよう勧められています。

TypeScript FSA

TypeScript FSA は Flux の Action 実装を Type-safe に実現するためのヘルパーライブラリです。
レポジトリにある使い方を短縮するとこんな感じです。

interface Action<Payload> {
type: string;
payload: Payload;
error?: boolean;
meta?: Object;
}

const actionCreator = actionCreatorFactory();
const somethingHappened = actionCreator<{foo: string}>('SOMETHING_HAPPENED');
const action = somethingHappened({foo: 'bar'});
if (isType(action, somethingHappened)) {
// action.payload is inferred as {foo: string};
return;
}

actionCreator により Action ジェネレータを作成して、それにパラメタを与えて呼び出すと Action オブジェクトが生成されます。 Action オブジェクトは Action 識別用の type とペイロードの payload などを持ちます。
isType は TypeScript の “User-Defined Type Guards” を使いつつも type フィールド同士を比べているだけの簡単な関数です。

組み合わせる

パラメタがシリアライズ可能なオブジェクトな限り、 Action オブジェクトも同様にシリアライズ可能なオブジェクトになります(上の {foo: 'bar'}action はシリアライズ可能)。また、 isType の実装は Flux を含めて何らかの実装に依存するものでもありません。
これはタイトルにあるよう Web Worker に活用できる可能性を示唆しています。

送信側では、 postMessage で Action オブジェクトを渡すようにすることで、( Redux でいうところの) Dispatch は簡単に行えます。 postMessagedispatch の代わりをする感覚です。

const paid = actionCreator<{ amount: number }>("PAID");
worker.postMessage(paid({ amount }));

受信側では、 isTypeevent.data と Action ジェネレータを比較することで型情報を推論できます。ちょっとトリッキーですが、これで( nullundefined を post しないルールにする限り)タイプセーフに実装できました。

ctx.addEventListener("message", event => {
if (isType(event.data, paid)) {
// Do something with event.data.payload
}
}

注意点

バンドルが冗長
UI スレッド用スクリプトと Worker 用スクリプトが完全に分かれる関係上、 Action 定義が両方のバンドルに含まれることになります。大量の Action を定義する場合には問題になるかもしれません。
普段の使い方との違い
Redux で TypeScript FSA の `isType` を使うのは Reducer の中だけのはずです。この例では受信処理がある場所なら役割にかかわらず書くことになり混乱するかもしれません。
RPC としては微妙
[Comlink](https://github.com/GoogleChromeLabs/comlink) と違い、関数を呼んで結果を受け取るように Worker とやり取りする処理を書きたいときの手間はかわりません。
シリアライズ可能という制限
当然ながら用途が異なるため、 TypeScript FSA のペイロードには[シリアライズ可能](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)という制限はかかっていません。うっかり `Error` を渡してしまうと実行時 DOM Exception を喰らってしまいます。

まとめ

Web Worker とのメッセージ受送信に TypeScript FSA を適用し、動作レベルで問題なく使えることを確認しました。

Web Worker を大々的に使う事例を見たことがないため、ラベルベースの振り分けが必要なほど Web Worker 開発を Type-safe にスケールさせることは少ないかもしれません。
個人としては Comlink より好きです。