アーキテクチャ
04. リクエストのサービスとその寿命
リクエスト処理でアプリケーションサービスを取得・共有・解放する仕組みを読み解きます。
本文のストリーミングが続く場合、リクエストのサービスはレスポンス生成後も生存する必要があります。 EffrontはアプリケーションのLayerをリクエストごとに取得し、応答本文の寿命の管理をホストのHTTP境界に委ねます。 ホスト所有のサービスの参照を捕捉する処理は別であり、所有権を引き取ることも寿命を延ばすこともありません。
アプリケーション定義 がLayerと コンパイル済みルート を供給します。packages/core/src/http.ts と server/application.ts が、これらを各リクエストに接続します。
1. 現在のリクエスト用にサービスを取得する
toHttpEffect(application) は現在の HttpServerRequest を処理し、HttpServerResponse を返します。 呼び出し側はリクエストのScopeと、アプリケーションLayerが要求する外部サービスを供給します。 サービスを取得するのは、このEffectを作るときではなく実行するときです。
packages/core/src/http.ts の抜粋export const toHttpEffect = <Services, ApplicationError, Requirements>(
application: ApplicationDefinition<Services, ApplicationError, Requirements>,
): HttpApplicationEffect<ApplicationError, Requirements> =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest;
const length = request.headers["content-length"];
if (length !== undefined) {
const size = Number(length);
if (!Number.isSafeInteger(size) || size < 0 || size > maxRequestBodySize) {
return HttpServerResponse.text("Request body exceeds the 10 MiB limit.", { status: 413 });
}
}
// Request layers must not reuse instances from a host's construction memo map.
const memoMap = yield* Layer.makeMemoMap;
const handler = yield* HttpRouter.toHttpEffect(ServerApplication.httpLayer(application)).pipe(
Effect.provideService(Layer.CurrentMemoMap, memoMap),
);
const response = yield* handler;新しい Layer.CurrentMemoMap によって、ホスト構築時にメモ化したアプリケーションのインスタンスの再利用を防ぎます。HttpRouter.toHttpEffect はアプリケーションのHTTP Layerを構築し、そのhandlerを現在のContextで直ちに実行します。 Effectを再利用しても、Layerは評価ごとに取得します。
取得前に、指定されたContent-Lengthが10 MiB以下の安全な非負整数に変換できなければ413を返します。 これはヘッダーの検査であり、ヘッダーがない場合の本文サイズの実測ではありません。Server Functionのデコード は、読み取るバイト数を別途制限します。
workers.ts の createFetchHandler は、このEffectを HttpEffect.toWebHandler に渡します。 呼び出しごとに新しい WorkersRequestContext を追加しますが、リクエスト単位の取得は変えません。
2. 取得したサービスをルートで使えるようにする
ServerApplication.httpLayer は、アプリケーションのLayer、FlightRenderer.layer、HtmlRenderer.layer を RequestLayer にまとめます。Layer.build(RequestLayer) が applicationServices のContextを生成します。 ルートの実行前に、RequestContextMiddleware がこれを実行中のHTTP Contextへ合流させます。
Services はアプリケーションLayerの出力を表します。Requirements は構築や実行に必要な外部サービスを表し、HTTP Effectの型に残ります。 ルーターを組み立てても、これらの外部要求は満たされません。
GETルートはEffect HTTPのネイティブなdescriptorでPageのmiddlewareを合成します。 POSTはReactの関数参照をデコードしてから、そのmiddlewareと描画先のrefreshに追加で必要なmiddlewareを適用します。 どちらもリクエストで取得したサービスを使いますが、関数固有のmiddlewareはデコードで関数を特定するまで選べません。
3. レスポンス本文が終わるまでリソースを維持する
ストリーミングする HttpServerResponse は、ヘッダーの生成後もリクエストのScopeを必要とします。 Effect HTTPのWebハンドラーは、完了・失敗・キャンセルまでScopeを本文へ引き継ぎます。 生成済みテキストなどの非ストリーム応答は、読み手を待たずに処理完了時にリソースを解放します。
packages/core/src/http.ts の抜粋/**
* Handles the current HTTP request in its host-owned scope.
*
* Application layers are built once per evaluation, using the current services.
* The host must retain the request scope until the body ends, fails, or is cancelled.
* Effect HTTP's Web handler performs this transfer automatically. Do not wrap
* response production alone in Effect.scoped: producing headers does not consume a body.
*/HEADの本文は消費されません。Effect HTTPのWebハンドラーとネイティブなNode/Bunホストは、HEAD でもヘッダーを維持したままストリームを破棄し、リクエストのScopeを解放します。coreは応答をそのまま返します。
HTTPに直接接続するホストも、同じ境界を保つ必要があります。 応答生成だけに Effect.scoped を適用すると、遅延した本文処理に必要なリソースを早く閉じすぎます。 後からサービスを読む独自の本文は、必要なContextも捕捉しなければなりません。サービスを生かしておくだけでは、後のEffectに自動で提供されません。
Flightの描画は、ストリーム完了に結び付いた子Scopeと解放処理を追加します。描画 は、この子Scopeと応答のライフサイクルの関係を説明します。
4. リクエストの状態を持ち越さずにホストのサービスを再利用する
makeHttpEffect(application) は外部サービスの参照を捕捉し、再利用可能なHTTP Effectを返します。 構築時にアプリケーションのリクエストLayerを作ることも、参照の所有権を引き取ることもありません。 所有者は、そのサービスを使うすべての応答本文が終わるまで維持する必要があります。
packages/core/src/http.ts の抜粋 const handler = toHttpEffect(application);
return Effect.map(Effect.context<CapturedRequirements<Requirements>>(), (context) => {
const captured: Context.Context<CapturedRequirements<Requirements>> =
captureExternalContext<HttpRequirements<Requirements>>(context);
return Effect.contextWith(
(requestContext: Context.Context<RemainingRequirements<Requirements>>) => {
const provided: Context.Context<CapturedRequirements<Requirements>> = Context.merge(
captured,
requestContext,
);
return Effect.provideContext(handler, provided);
},
);
});実行中のリクエストの値は、捕捉した値より優先されます。 合流前に captureExternalContext が構築時のScope、HTTPリクエスト、解析済み検索パラメーター、ルートContext、ルーター、Layerのmemo mapを除きます。Effect.context<R>() は型引数で実行時のキーを絞り込まないため、明示的に除く必要があります。
WorkersRequestContext はreadonlyの request、env、executionContext を保持します。createWorkersContextAccessors が作るのは、その参照の型付き読み取り関数であり、新たなServiceやLayerではありません。 型はホストの値を検証せず、参照がない状態で読むと TypeError になります。
@effront/cloudflare/workers は読み取り関数を、waitUntil(Promise<unknown>) を含む CloudflareExecutionContext に具体化します。 ホストの値はEffect Contextにとどまり、FlightやHTMLへ暗黙にシリアライズされません。アーキテクチャの全体図 は、このアダプター境界と描画・ブラウザーの入口の位置関係を示します。