メインコンテンツまでスキップ

実行コンテキスト

ZeltはAsyncLocalStorage(ALS)なしでも実行できます。リクエストアクセサとmiddleware値は、どちらの方式でも同じAPIを使います。

HTTP入口での操作syncasync
デフォルト引数でrequest()・response()・middlewareValue(M)を取得可能可能
取得済みのアクセサや値をawait後に利用可能可能
middlewareがawait後にnext(value)を呼ぶ可能可能
await後にコンテキスト取得関数を直接呼ぶエラー可能

syncでは、フレームワークがメソッドを呼んだときの同期実行区間だけ、暗黙のコンテキスト取得ができます。タイマーや.then()のコールバックも区間外です。上流middlewareがawaitしてからnextを呼んでも、下流middlewareやhandlerの呼び出し時には新しく注入区間を開きます。

方式の選択​

coreの初期状態はsyncで、Node組み込みモジュールをimportしません。非同期の暗黙伝播を使う場合は、別エントリからALSを有効にしてからruntimeを作成します。

import { createApp, http } from '@zeltjs/core';
import { enableAsyncContext } from '@zeltjs/core/async-context';

enableAsyncContext();

const app = createApp([http({ controllers: [] })]);
const runtime = await app.createRuntime({ contextMode: 'async' });

enableAsyncContext()は複数回呼んでも同じ状態です。有効化後に作成するruntimeの既定値はasyncになります。既存runtimeの方式は変わりません。ALSが有効な環境でも、contextMode: 'sync'を明示したruntimeの暗黙取得は非同期区間へ延長されません。

Node・Bun・Lambda・ElectronアダプタはALSを有効にし、既定でasyncを選びます。各アダプタのcontextModeでsyncも選べます。Cloudflare WorkersアダプタはALSをimportせず、Zeltのcoreコンテキスト処理のためのnodejs_compat・nodejs_alsは不要です。アプリケーションの別の依存がこれらのフラグを必要とする場合はあります。

ALSを有効にせずasyncを指定すると、runtime作成時にエラーになります。自動で同期方式に切り替えて再実行はしません。

移植できるhandlerの書き方​

フレームワークの入力は、呼び出し時に受け取ります。

import { Controller, Get, request, response } from '@zeltjs/core';

@Controller('/users')
class UsersController {
  @Get('/:id')
  async find(req = request(), res = response()) {
    await Promise.resolve();
    return res.json({ id: req.pathParam('id') });
  }
}

value = middlewareValue(MyMiddleware)・opts = middlewareOptions(MyMiddleware)も同様です。reqはbodyキャッシュ・pathパラメータ・Hono Contextを保持します。middlewareは共有シングルトンのまま、引数とnextが要求ごとの状態を保持します。保存したnextを、そのmiddlewareのuse()完了後に呼ぶことはできません。

後からコンテキストを読み書きする必要がある場合は、同期区間で実行ハンドルを捕捉します。

import { captureContext, Controller, Get, request } from '@zeltjs/core';

@Controller('/example')
class ExampleController {
  @Get('/')
  async handle(req = request()) {
    const context = captureContext();
    await Promise.resolve();
    return context.run(() => ({ path: request().path(), method: req.method() }));
  }
}

syncでcontext.run(async () => ...)を使っても、暗黙取得ができるのはそのコールバックの最初の中断までです。必要な入力はawait前に捕捉します。setUser・Sessionの更新関数・プラットフォームbindingの取得も同じ契約です。

その他の機能​

  • ログ:logger.child(bindings)のbindingsは、両方式でawait後も保持します。withLogContextの暗黙伝播がawaitを越えるのはasyncのみです。
  • Task:run()・runAndWait()は両方式で使えます。syncでは、実行中Taskがあるとshutdown()がTaskRunnerSynchronousShutdownErrorになります。外部からの終了と、Task自身の終了待ちを区別できないためです。runAndWait()の完了後に終了するか、終了時のTask待機にはasyncを使います。
  • afterResponse()のTaskは、完了したリクエストのflush側スコープで実行します。登録時の一時的なstorageスコープ(DBトランザクションやログbindingsなど)は引き継ぎません。Taskに必要なデータやchild loggerは、handlerから戻る前に受け取っておきます。
  • DB:自動トランザクション伝播にはasyncが必要です。この変更でDBのトランザクション設計は変更していません。
  • CLI・GraphQL:args(...)をメソッドのデフォルト引数で受け取ります。非同期のresolver解決後にも、呼び出し時の実行コンテキストを復元します。

移植性を確認するテストでは、onTestに{ contextMode: 'sync' }を渡します。await後の暗黙取得が必要なテストは、setupでenableAsyncContext()を呼べます。