Node.jsのWorker Threadsは、本物のスレッドプログラミングができます。ワーカーの処理を記述したJavaScriptを与えて、ワーカーを起動するわけですが、TypeScriptのファイルを指定するにはどしたらいいのでしょうか?
本稿でわかること
- ts-nodeとWorker Threadsを組み合わせて、TypeScriptのワーカーを起動する方法
- ts-node-devでTypeScriptのワーカーを起動する方法
前提知識
本稿を理解するにあたっては、下記の技術についての基礎的な知識が必要です。
- Worker Threads
- 本物のスレッドプログラミングができるNodeモジュール。
- 概要と基礎的な使い方は、次の投稿をご覧ください。
- ts-node
- TypeScriptのコンパイルとJavaScriptの実行をコマンド一つでできるツール。
-
tsc && node dist/main.js
を一発でできるようにしたツール。
-
- nodeコマンドの感覚でTypeScriptを実行できる。 例:
ts-node src/main.ts
- TypeScriptのコンパイルとJavaScriptの実行をコマンド一つでできるツール。
- ts-node-dev
- ts-nodeとnode-devを組み合わせた開発ツール。
- TypeScriptのコードに変更が加わると自動的にコンパイルし、プログラムを再起動してくれる。
解決したい課題: WorkerにTypeScriptを指定することはできない
JavaScriptでWorker Threadsを実行する方法は以下のような手順になります。
まず、ワーカー側の処理を実装したJavaScriptファイルを作ります:
console.log('Hello from worker')
次に、ワーカーを起動する処理を書くのですが、Worker
コンストラクタでワーカーのファイル名を指定する必要があります:
const {Worker} = require('worker_threads')
const worker = new Worker('./worker.js')
このmain.jsを実行すると、ワーカーが起動することが確認できます:
$ node main.js
Hello from worker
このコードをTypeScriptで書き直し、ts-nodeで同じように実行するとどうなるでしょうか? やってみましょう。
まず、ワーカー側の実装をTSに移植します。内容はworker.jsと全く同じです:
console.log('Hello from worker')
次に、main.jsをTSに移植します。大きな変更は、worker.js
ではなくworker.ts
を起動するように変える点です:
import {Worker} from 'worker_threads'
new Worker(__dirname + '/worker.ts') // tsファイルを指定
このコードを、ts-nodeで起動してみます。すると、次のようなエラーが発生し、ワーカーが起動できないことが分かります。エラー内容は、「ワーカースクリプトの拡張子はjs, mjs, cjsじゃないとダメだよ」というものです。
$ ts-node src/main.ts
The worker script extension must be ".js", ".mjs", or ".cjs". Received ".ts"
このことから、Worker Threadsでは直接TypeScriptファイルが指定できないことが分かったと思います。
解決策: いったんJavaScriptファイルを経由するようにする
Worker Threadsで起動できるコードはJavaScriptのみという制約があるので、直接TypeScriptのワーカーを起動するのはあきらめます。迂回手段として、まずJavaScriptのワーカーを起動し、その中でTypeScriptコードをrequire
するようにします。
先述した失敗作TypeScriptコードを手直ししていきましょう。
まず、main.tsはworker.tsではなく、worker.jsを起動するように直します:
import {Worker} from 'worker_threads'
new Worker(__dirname + '/worker.js') // jsを起動するように直す
次に、最終的な目的地である、TypeScriptワーカーのファイルを作ります。名前はworker.jsと区別できるようtsWorker.tsにしておきます:
console.log('tsWorker.ts started')
最後に、main.tsとtsWorker.tsを橋渡しする、worker.jsを実装します。worker.jsの重要な役割は、ts-nodeをregisterすることです。これにより、以降のコードではtsファイルをrequireして実行できるようになります:
console.log('worker.js started')
require('ts-node').register() // 重要
require(__dirname + '/tsWorker.ts')
このコードを実行してみましょう。
$ ts-node src/main.ts
worker.js started
tsWorker.ts started
出力結果から、まずworker.jsが実行され、次にtsWoekr.tsが読み込まれ実行されたことがわかると思います。
ts-node-devでは、execArgvを空っぽにしてWorkerを起動する
これまでts-nodeでTypeScriptワーカーを起動する方法を説明してきましたが、類似のツールであるts-node-devでも同じ方法で対応できるのでしょうか? 結論を言うと、そのままでは対応できません。
上のmain.tsをts-node-devで実行してみると分かりますが、worker.jsは起動するものの、worker.js内のrequireが動作せず、スレッドが終了してしまいます:
$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
worker.jsでexecArgv
を確認すると、ワーカー側では不要なts-node-devのフックが渡ってきているのがわかります:
console.log('worker.js started')
console.log(process.execArgv)
// require('ts-node').register()
// require(__dirname + '/tsWorker.ts')
$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
[
'-r',
'/var/folders/4l/mrmcxh3x40lbcpwxyz29ppcw0000gn/T/ts-node-dev-hook-1629690677650566.js'
]
解決策としては、main.tsのnew Worker
のオプションでexecArgv
をカラにすることです:
import {Worker} from 'worker_threads'
new Worker(__dirname + '/worker.js', {execArgv: []})
こうしておくと、まずはts-node-devでもTypeScriptのワーカーが起動できるようになります。
$ ts-node-dev src/main.ts
Using ts-node version 8.8.1, typescript version 3.8.3
worker.js started
tsWorker.ts started
しかし、この方法に問題がないわけではありません。ts-node-devの醍醐味としては、TypeScriptのコードを書き直したら、自動的に再コンパイルして、プロセスを起動しなおしてくれることです。しかし、この対処法では、スレッド側でrequireされたファイルをいくら修正しても、自動再コンパイル&再起動はされません。
この課題の解決策については、また時間を見つけて調べてみたいと思います。