はじめに
前回は、Node.jsのモジュール管理を拡張するパッケージ「teaos」の概要を紹介しました。
今回は、teaosの基本APIをもう少し詳しく解説します。
teaosは、Node.jsのCommonJSモジュールシステムを利用しながら、モジュールの読み込み、動的な初期化、非同期コンテキスト管理などの機能を提供します。
さらに、2026年11月2日に公開したバージョン0.1.2では、次の機能を追加しました。
-
Mailbox:実行コンテキスト間のメッセージ通信 -
teaos.hooks:モジュールの読み込み方法の差し替え -
teaos.extensionHooks:Extensionの検出方法の差し替え
これらの機能は、Node.jsの単一プロセス内で複数の論理的な実行単位を管理する仕組みや、Cloudflare Workersのような異なる実行環境への対応にも関係します。
今回は、これらの機能を実際のコードとともに紹介します。
対象バージョン:teaos 0.1.2
1. teaosの基本API
teaosの主要なAPIを整理すると、次のようになります。
| API | 役割 |
|---|---|
teaos.require() |
モジュールの読み込み |
teaos.use() |
モジュールの初期化とExtensionの読み込み |
teaos.run() |
指定したコンテキストで関数を実行 |
teaos.getContext() |
現在の実行コンテキストを取得 |
teaos.on() |
イベントリスナーの登録 |
teaos.declare() |
コンポーネントの宣言 |
teaos.Mailbox |
メッセージキューによる通信 |
teaos.hooks |
モジュールローダーの差し替え |
teaos.extensionHooks |
Extension一覧の取得方法の差し替え |
これらのAPIは、大きく3つの領域に分類できます。
teaos
│
├── Module Management
│ ├── require()
│ ├── use()
│ ├── hooks
│ └── extensionHooks
│
├── Component Management
│ ├── declare()
│ └── on()
│
└── Execution Context
├── run()
├── getContext()
└── Mailbox
この3つを組み合わせることで、モジュールの読み込みから実行状態の管理までを統一的に扱えます。
2. require() ― モジュールを読み込む
Node.jsでは、通常require()を使ってCommonJSモジュールを読み込みます。
const fs = require('fs');
teaosでは、次のように記述します。
const teaos = require('teaos');
const fs = teaos.require('fs');
teaosのrequire()は、Node.jsのモジュール読み込み機構を利用しながら、独自のローカルライブラリにも対応します。
例えば、次のような構成です。
project/
├── index.js
├── node_modules/
│ └── teaos/
└── lib/
└── math/
└── index.js
lib/math/index.jsに次のコードを記述します。
exports.add = (a, b) => a + b;
exports.multiply = (a, b) => a * b;
アプリケーション側では、次のように読み込みます。
const teaos = require('teaos');
const math = teaos.require('@math');
console.log(math.add(10, 20));
console.log(math.multiply(10, 20));
出力:
30
200
teaosでは、@で始まる名前をローカルライブラリとして扱います。
これにより、モジュールの物理的な配置と、アプリケーションから参照する名前を分離できます。
また、読み込み前にイベントを受け取ることもできます。
teaos.on('require', (name) => {
console.log('Loading:', name);
});
teaos.require('@math');
この仕組みは、モジュールの読み込み状況の追跡などに利用できます。
3. use() ― モジュールの初期化と動的な利用
require()がモジュールを読み込むためのAPIであるのに対して、use()はモジュールの初期化を含むAPIです。
例えば、次のようなライブラリを考えます。
// lib/logger/index.js
exports.use = (options = {}) => {
const prefix = options.prefix || 'LOG';
return {
log(message) {
console.log(`[${prefix}] ${message}`);
}
};
};
アプリケーションから利用します。
const teaos = require('teaos');
const logger = teaos.use('@logger', {
prefix: 'APP'
});
logger.log('Hello teaos');
出力:
[APP] Hello teaos
teaosのuse()は、読み込んだモジュールがuse()メソッドを持つ場合、そのメソッドにオプションを渡して呼び出します。
use()メソッドが存在しない場合は、モジュール自体を返します。
さらに、resourcesオプションを指定することで、ライブラリのExtensionを追加で読み込むこともできます。
teaos.use('@example', {
resources: '*'
});
この場合、対象ライブラリで検出されたExtensionの種類に応じて、Extensionの読み込みが行われます。
Extensionの仕組みについては、次回以降の記事で詳しく紹介します。
4. run()とgetContext() ― 実行コンテキストの管理
Node.jsは、通常、単一のJavaScriptスレッド上でイベントループを実行します。
非同期処理を利用すると、複数の処理が時間的に重なって進行します。
ここで問題になるのが、処理ごとの状態管理です。
例えば、Webアプリケーションでは、リクエストごとに次のような情報を管理する場合があります。
{
requestId: 'request-001',
userId: 123,
permissions: ['read', 'write']
}
これらを単純なグローバル変数に格納すると、非同期処理が重なった際に、異なるリクエストの情報が混在する可能性があります。
teaosでは、この問題に対応するために、Node.jsのAsyncLocalStorageを利用しています。
run()でコンテキストを設定する
const teaos = require('teaos');
teaos.run(
{ requestId: 'A' },
() => {
console.log(teaos.getContext());
}
);
出力:
{ requestId: 'A' }
teaos.run(context, callback)は、指定したコンテキストの下でコールバックを実行します。
teaos.getContext()は、現在の非同期実行コンテキストを返します。
非同期処理でもコンテキストを維持する
const teaos = require('teaos');
async function processRequest() {
await Promise.resolve();
console.log(teaos.getContext());
}
teaos.run(
{ requestId: 'A' },
processRequest
);
awaitを挟んだ後でも、対応するコンテキストを取得できます。
複数のコンテキストを並行して実行する
const teaos = require('teaos');
async function task() {
await Promise.resolve();
console.log(
teaos.getContext().requestId
);
}
Promise.all([
teaos.run(
{ requestId: 'A' },
task
),
teaos.run(
{ requestId: 'B' },
task
)
]);
出力例:
A
B
2つのタスクは同じNode.jsプロセス内で動作しますが、それぞれ独立したコンテキストを参照できます。
この仕組みを利用すると、リクエスト、セッション、ジョブなどを論理的な実行単位として扱いやすくなります。
なお、run()はコールバックの戻り値を返します。非同期コールバックの場合はPromiseが返されます。
また、run()の外側でgetContext()を呼び出すと、共通のフォールバックオブジェクトが返されます。そのため、リクエスト固有の情報は必ず個別のコンテキスト内に格納する必要があります。
5. AsyncLocalStorageと疑似プロセス
ここからは、少し設計上の話になります。
通常、OSのプロセスは、それぞれ独立したアドレス空間などの実行資源を持ちます。
一方、Node.jsの非同期処理では、同じJavaScript実行環境を共有しながら、複数のタスクを進行させることができます。
teaosでは、run()とgetContext()を利用して、これらのタスクに独立した論理コンテキストを割り当てられます。
例えば、次のようなイメージです。
Node.js Process
│
├── Context A
│ ├── pid: 1
│ └── Task A
│
├── Context B
│ ├── pid: 2
│ └── Task B
│
└── Context C
├── pid: 3
└── Task C
ここでは、このような論理的な実行単位を「疑似プロセス」と呼ぶことにします。
ただし、疑似プロセスはOSのプロセスと同等の隔離機構ではありません。
同じJavaScript実行環境にあるため、モジュールのグローバル変数や共有オブジェクトにはアクセスできる場合があります。
AsyncLocalStorageが提供するのは、あくまで非同期処理ごとのコンテキスト管理です。
この点を踏まえたうえで、疑似プロセス間の通信を考えます。
6. Mailbox ― 疑似プロセス間のメッセージ通信
teaos 0.1.2では、新たにMailboxを追加しました。
Mailboxは、メッセージを送信する側と受信する側を分離するための軽量なキューです。
基本的なAPIは次のとおりです。
const mailbox = new teaos.Mailbox();
mailbox.send(message);
const message = await mailbox.recv();
mailbox.close();
-
send():メッセージを送信 -
recv():メッセージを非同期に受信 -
close():Mailboxを閉じる
メッセージはFIFO(先入れ先出し)の順序で管理されます。
受信待ちの処理がある場合、送信されたメッセージは待機中の受信者に渡されます。
受信待ちの処理がない場合、メッセージはキューに保存されます。
疑似プロセス間で通信する
次の例では、2つの疑似プロセスを作成し、一方から他方へメッセージを送ります。
const teaos = require('teaos');
const mailbox = new teaos.Mailbox();
async function main() {
// 受信側
const processB = teaos.run(
{ pid: 2 },
async () => {
try {
while (true) {
const message = await mailbox.recv();
console.log(
'Process B:',
teaos.getContext().pid,
message
);
}
} catch (error) {
if (error.code !== 'ERR_MAILBOX_CLOSED') {
throw error;
}
}
}
);
// 送信側
teaos.run(
{ pid: 1 },
() => {
mailbox.send({
type: 'MESSAGE',
data: 'Hello from A'
});
}
);
mailbox.close();
await processB;
}
main().catch(console.error);
出力例:
Process B: 2 { type: 'MESSAGE', data: 'Hello from A' }
ここで注目したいのは、受信側のコンテキストが維持されていることです。
送信側のコンテキストがpid: 1であっても、受信側はpid: 2のコンテキストで処理を継続します。
これにより、疑似プロセスごとに異なる状態を管理しながら、メッセージを交換できます。
Mailboxの終了処理
close()が呼び出されると、新しいメッセージの送信はできなくなります。
受信待ちの処理には、MailboxClosedErrorが通知されます。
try {
await mailbox.recv();
} catch (error) {
if (error.code === 'ERR_MAILBOX_CLOSED') {
console.log('Mailbox closed');
}
}
ただし、終了前にキューへ保存されたメッセージは、終了後も受信できます。
また、Mailboxはメッセージオブジェクトを自動的に複製するわけではありません。送信側と受信側で同じオブジェクトを参照する場合があるため、信頼境界をまたぐ通信ではデータの検証やコピーが必要です。
7. hooks ― モジュール読み込みの抽象化
teaos 0.1.2では、Cloudflare Workersなどの環境への対応を進めるため、teaos.hooksを追加しました。
Node.jsでは、通常、ファイルシステム上のモジュールを動的に探索して読み込みます。
しかし、Cloudflare Workersのような環境では、Node.jsと同じ方法で実行時にファイルシステムを探索できるとは限りません。
また、バンドラーによってJavaScriptを単一のファイルにまとめる場合、実行時の動的なrequire()が問題になることがあります。
そこで、teaosではモジュールの読み込み方法を外部から登録できるようにしました。
const teaos = require('teaos');
teaos.hooks['@example'] = () => {
return require('./lib/example');
};
この状態で、次のように呼び出します。
const example = teaos.require('@example');
teaosは、通常のモジュール解決よりも先に、登録されたHookを確認します。
対応するHookが存在する場合は、その関数を呼び出してモジュールを取得します。
これにより、モジュールの論理名と実際の読み込み方法を分離できます。
重要なのは、Hookに登録する関数が、ビルド時にバンドラーから静的に認識できるようになっていることです。
Hookを登録するだけで、未収録のモジュールが自動的にバンドルされるわけではありません。
8. extensionHooks ― Extensionの検出方法を抽象化する
teaosでは、Extensionを次のようなディレクトリ構造で管理できます。
lib/
└── example/
└── lib/
└── extensions/
└── command/
└── hello.js
通常のNode.js環境では、ディレクトリを走査することでExtensionを検出できます。
一方、ファイルシステムの探索ができない実行環境では、別の方法が必要になります。
そこで追加されたのが、teaos.extensionHooksです。
teaos.extensionHooks['@example'] = () => ({
command: {
'hello.js': true
}
});
これにより、Extensionの一覧を静的に提供できます。
モジュールの読み込みHookと組み合わせると、次のようになります。
const teaos = require('teaos');
teaos.hooks['@example'] = () =>
require('./lib/example');
teaos.hooks[
'@example/lib/extensions/command/hello.js'
] = () =>
require('./lib/example/lib/extensions/command/hello.js');
teaos.extensionHooks['@example'] = () => ({
command: {
'hello.js': true
}
});
teaos.use('@example', {
resources: {
command: {
'hello.js': true
}
}
});
この例では、extensionHooksがExtensionの存在を示し、hooksが実際のモジュールを読み込みます。
つまり、次の2つを分離できます。
- どのExtensionが存在するか。
- そのExtensionをどのように読み込むか。
これは、クロスプラットフォーム対応において重要な設計です。
なお、現在のHookは実行環境全体で共有されるマッピングです。コンテキストごとのアクセス権を制御するものではありません。
9. Node.jsからCloudflare Workersへ
ここまで紹介した機能を組み合わせると、teaosの今後の方向性が見えてきます。
Application
│
▼
teaos Runtime
│
├── Module Loader
│ ├── require()
│ ├── use()
│ └── hooks
│
├── Extension Loader
│ └── extensionHooks
│
└── Execution Context
├── run()
├── getContext()
└── Mailbox
│
▼
Execution Environment
├── Node.js
└── Cloudflare Workers(対応を拡張中)
Node.jsでは、ファイルシステムとCommonJSを利用してモジュールを読み込めます。
一方、Cloudflare Workersでは、静的にバンドルされたモジュールを利用する構成が中心になります。
hooksとextensionHooksは、この違いを吸収するための仕組みです。
また、run()とgetContext()は、同一JavaScript実行環境内で複数の論理的な処理を管理するためのインターフェースになります。
Mailboxは、そのような処理単位の間でメッセージを受け渡すための仕組みです。
ただし、現在のteaosが提供するコンテキスト管理とMailboxだけで、安全なサンドボックスが完成するわけではありません。
特に、ユーザーが作成したスクリプトなどを実行する場合には、モジュールのアクセス制限、共有オブジェクトの分離、実行時間やリソースの制限などを別途設計する必要があります。
また、現行のrun()はNode.jsのAsyncLocalStorageを使用するため、Cloudflare Workersでの動作には、利用するランタイムの互換性確認が必要です。
10. まとめ
今回は、teaosの基本APIと、バージョン0.1.2で追加された機能を紹介しました。
| 機能 | 役割 |
|---|---|
require() |
モジュールを読み込む |
use() |
モジュールを初期化し、Extensionを読み込む |
run() |
非同期コンテキストを設定して実行する |
getContext() |
現在のコンテキストを取得する |
Mailbox |
論理的な実行単位の間でメッセージを交換する |
hooks |
モジュールの読み込み方法を差し替える |
extensionHooks |
Extensionの検出方法を差し替える |
teaosは、単なるモジュールローダーにとどまらず、モジュールの実行方法とコンテキスト管理を抽象化するための基盤へと発展しています。
今後は、これらの仕組みを利用して、Extensionの型やインターフェースを整理し、Node.jsやCloudflare Workersなどの異なる環境でも共通のアプリケーション構成を扱える仕組みを整備していく予定です。
次回は、teaosのExtension機構 ― declare / onと拡張インターフェースをテーマに、Extensionの定義方法、読み込み、イベント通知、型の設計について解説します。
関連リンク
teaos 技術シリーズ 第2回
対象バージョン:teaos 0.1.2