sync-actionsという、非同期処理を同期的に実行できるJavaScript/TypeScript用ライブラリを公開しました。
特にTypeScriptでは、定義した関数を型安全に呼ぶことができます。
asyncを付けたくない(付けられない)関数内で、非同期処理を実行したい場合の利用を想定しています。
特徴
- Node.jsのworker_threadsを利用
- 非同期処理をサブスレッドで実行し、メインスレッドではその完了を同期的に待つ実装です。
- 型安全な関数呼び出し
- TypeScriptでは定義した関数の型情報を利用できます。
- Native ESMとして公開
- CommonJSのサポートをしないことでシンプルに作っています
リポジトリ
使い方
インストール
npmパッケージとして公開しているので、npm installなどでインストールしてください。
npm install sync-actions
基本的な使い方
Promiseオブジェクトを返す非同期関数を defineSyncWorker()
に渡すことでインターフェースを定義し、launch()
でワーカースレッドを開始します。ワーカーを定義するファイルを他の処理ファイルと分けて作る想定です。
// worker.js
import { defineSyncWorker } from "sync-actions";
export const { actions, worker } = defineSyncWorker(import.meta.filename, {
ping: async () => {
// 非同期処理を実行し、
await new Promise((resolve) => setTimeout(resolve, 1000));
// 結果を返り値として返します
return "pong";
}
}).launch();
// main.js
import { actions, worker } from "./worker.js";
// 非同期関数を同期的に実行できます
console.log(actions.ping()); // => 1秒後に "pong" が出力されます
worker.terminate();
型安全な関数呼び出し
TypeScriptでは、defineSyncWorker
で定義した関数を型安全に呼び出すことができます。
// worker.ts
import { defineSyncWorker } from "sync-actions";
export const { actions, worker } = defineSyncWorker(import.meta.filename, {
// 引数と返り値の型を指定することで、型安全な呼び出しが可能です
add: async (a: number, b: number): Promise<number> => {
return a + b;
}
}).launch();
// main.ts
import { actions, worker } from "./worker.js";
// 型安全な呼び出し
actions.add(1, 2); // => 3 (number)
// @ts-expect-error
actions.add("1", 2);
// => Argument of type 'string' is not assignable to parameter of type 'number'
worker.terminate();
作成背景
ここまでの内容だとREADMEと同じなので、作成背景を記載します。
私は Accel Record というORMを開発しています1。Accel Record は一般的なORMとは異なり、DBアクセスを同期的なインターフェースで行う設計になっています2。DBアクセスを同期的に実行する部分は、child_processモジュールを利用して起動したサブプロセスで非同期処理を実行することで実現していました3。そこをchild_processの代わりにworker_threadsを利用することで、実行時のオーバーヘッドを減らすことができるだろうと考えていました。
また Accel Record は Ruby on Rails の Active Record に使い勝手を寄せていますが、今後実現したいことの一つとして、CarrierWaveのようなライブラリを作成することがあります。CarrierWaveでは、レコードの保存時に画像を外部のストレージサービス(AWSのS3など)に保存することができますが、これを Accel Record で実現するためには、画像アップロードという非同期処理を同期的に実行する必要があります。こちらの処理も、サブプロセスよりもworker_threadsを利用することで、より高速に実行できるという期待があります。
そこで一度、worker_threadsを利用した非同期処理の同期実行ライブラリを探してみました。synckit, deasyncなどいくつかのライブラリが見つかったのですが、いずれも手元で期待した動作ができなかったため、自分で作ることにしました。せっかくなので、TypeScriptで型安全に使えるようなインターフェースにしようと思いました。
より内部的なこと
- worker_threadsで起動したサブスレッドの非同期処理が完了するまでは、Atomic.wait()を利用してメインスレッドをブロックしています。
- スレッド間の通信にはMessageChannelを利用しています。このあたりの実装にはsynckitのソースコードがとても参考になりました。
- worker_threadsのWorkerを起動する際には、.tsファイルを.jsにトランスパイルする必要があります。その部分にはesbuildを利用しています。
- Workerを起動する際は、トランスパイル済みのソースコードを文字列としてWorkerに渡して実行したかったのですが、手元の環境ではそれが正常に動作しませんでした。ここが一番苦労した部分になります。最終的にはnode_modules以下にファイルとして書き出して、そのパスをWorkerに渡すようにしています。結局この方法が一番安定して動きました。