アーキテクチャ

07. Server Functionの呼び出しから画面更新まで

Server Function の呼び出しからサーバー側の処理、画面更新までの実装を読み解きます。

ブラウザーからの Server Function 呼び出しに対する正しい Flight レスポンスは、呼び出し結果と更新されたルートツリーを含みます。 そのため、関数の失敗がHTTP 200で届くことがあります。一方、成功しても古くなった応答で、ユーザーが離れたページを復元してはいけません。 アプリケーションでの利用方法は 実行と画面更新のガイド を参照してください。

1. 届いた呼び出しを識別して検証する

client/call-server.ts は現在の履歴entryまたはURLと、増加する呼び出し順序を記録します。 Reactの encodeReply で引数をエンコードし、FlightClient でそのURLへPOSTします。x-effront-server-fn を送り、Flightを要求します。 描画先のPOST handlerが prepareServerFnRequest を呼びます。

デコード前に validateOrigin が、解析したOrigin URLのhostと、小文字化したHostヘッダーを比べます。 ヘッダーの欠落、不正なOrigin URL、不一致は403になります。 比較するのはhostであり、schemeやorigin全体ではありません。処理の認証や認可も行いません。 リクエストは、リクエストScopeのAbortSignalを使ってWeb Requestになります。 action IDのヘッダーがあればクライアント呼び出し、なければJavaScriptなしでも使えるprogressive formの経路を選びます。

どちらの経路も実際に読んだバイト数を数え、10 MiBを超える本文を400で拒否します。HTTP入口のContent-Length検査 は別であり、読み取り前に413を返すことがあります。 バッファーに読み込んだmultipart本文はFormData、それ以外はtextになります。 読み取りとmultipart解析の失敗は400です。

packages/core/src/server/server-fn-request.ts の抜粋
  const temporaryReferences = createTemporaryReferenceSet();
  const body = yield* readBody(request);
  const decoded = yield* Effect.tryPromise({
    try: () =>
      decodeReply(body, {
        arraySizeLimit: ServerFnArraySizeLimit,
        temporaryReferences,
      }),
    catch: (cause) => requestError("Failed to decode Server Function arguments.", 400, cause),
  });
  const args = yield* decodeArguments(decoded).pipe(
    Effect.mapError((cause) =>
      requestError("Expected a Server Function argument array.", 400, cause),
    ),
  );
  const action = yield* Effect.tryPromise({
    try: () => loadServerAction(actionId),
    catch: (cause) => requestError("The requested Server Function does not exist.", 400, cause),
  });

decodeReply は配列サイズ上限10,000と一時参照を使って引数を復元します。 Effrontは結果が配列であることを確認し、loadServerAction が関数を解決します。 デコード、配列形式、参照解決の失敗は、アプリケーション入力の検証前に400になります。 サーバーは一時参照をFlightへ引き継ぎ、ブラウザーは encodeReply に渡した集合でデコードします。

2. Reactの参照から型付きの実行処理を取り出す

makeServerFnFactory で作ったServer Functionを呼ぶと、handlerを直ちに実行せず、Effectを記述するbrand付きPromiseを返します。 これにより、HTTP処理は実行前にアプリケーションidentityとmiddlewareを取り出せます。

packages/core/src/application/server-fn.ts の抜粋
    const schemas = Array.ensure<Schema.ConstraintDecoder<unknown, AvailableServices>>(input);
    const decode = Schema.decodeUnknownEffect(Schema.Tuple(schemas));

呼び出し側はSchemaの Encoded 値を渡し、handlerは Schema.Tuple 成功後にデコード済みの Type 値を受け取ります。 単一Schemaは最初の引数をデコードし、余分なネイティブ引数は無視し、省略時はundefinedをデコードします。 Schema配列は位置付きの引数列を検証し、入力を省略した関数は空の引数列を要求します。 デコードとhandlerは AvailableServices を要求でき、その型付き失敗は ServerFnOperationError になります。

packages/core/src/application/server-fn.ts の抜粋
      const unavailable =
        Promise.reject<ServerFnWireValue<Effect.Success<typeof effect>>>(directInvocationError());
      void unavailable.catch(() => undefined);

      return Object.assign(unavailable, {
        [ServerFnInvocationTypeId]: Object.freeze({ effect, identity, middleware }),
      });

返されるPromiseには、Effect、identity、middlewareを持つbrandが付きます。 サーバーグラフで直接awaitすると TypeError でrejectします。 HTTPでは代わりに matchServerFnInvocation が処理を取り出し、アプリケーションidentity を検査します。 別のEFFRONT identityは拒否します。 brandのないネイティブReact Server Functionは別経路を通り、Effrontの関数middlewareを使わずEffect内で結果を待ちます。

3. Middleware内で実行し結果を描画に渡す

PreparedServerFnRequest は execute と関数のmiddlewareを含みます。server/application.ts の executeServerFnAndRefresh が、実行と描画をそのmiddlewareで包みます。 描画先だけが必要とするmiddlewareは、関数に適用済みのものを除いて描画を包みます。 レンダラーは実行時スコープの検査用に、両方をまとめた一覧を受け取ります。

クライアント呼び出しでは、serverFnOutcome が処理のExitを serverFnResult のSuccessまたはFailureへ変換します。 どちらもstatus 200で描画するため、HTTPの成功だけでは関数の成功を示しません。 割り込みはFailureのデータにせず、割り込みのまま保ちます。Flightペイロード が結果とルートツリーの両方を運びます。

progressive formはmultipartのFormDataとReactの decodeAction を必要とします。 actionの欠落やデコード失敗は400です。 実行後、decodeFormState がSSRとhydrationに状態を供給します。 成功時は formState と serverFnResult: null をstatus 200で描画し、通常のドキュメントフォーム要求にはHTMLを返します。 型付きの実行失敗とフォーム状態のデコード失敗は500です。 POSTルートは ServerFnRequestError を、関数結果のペイロードではなく、指定statusとテキスト応答へ変換します。

4. 呼び出し結果を確定してから画面の更新方法を選ぶ

非2xx、Flight以外、結果の欠落は、結果に基づくrefreshを行わずにブラウザーの呼び出しをrejectします。 有効なSuccessは呼び出し元のPromiseをresolveし、有効なFailureは ServerFnCallError でrejectします。 Effrontは確定後に継続処理を登録し、Reactの既存Actionの処理がrefresh Transitionより先に実行されるようにします。 両方の結果が同じrefresh判定を使うため、関数の失敗はUIを変更しないことを意味しません。

返されたツリーを再利用できるのは、次の条件を満たす場合だけです。

  • 最後に開始した呼び出しの応答である。
  • ナビゲーションのTransitionが進行中でない。
  • 現在の履歴entryが記録したIDと一致する。または、どちらにもentryがなくURLが変わっていない。

古いrefreshの割り込み後も、Effrontは再検査します。後始末の間に別の呼び出しや遷移が先へ進む可能性があるためです。 適さない応答は解放し、RouteRefresher.refreshCurrentRoute("server-function") が代わりに現在の描画先を更新します。 この経路は遷移が落ち着くのを待ち、refreshを新しいルート遷移と競合させます。

再利用できる応答では、RouteLoader.prepareRefresh がキャッシュを無効化し、startTransition がtype server-function でツリーを公開します。 Transition Actionはcommit Promiseを返しません。返すと、待機対象のcommitを妨げるためです。 別のスコープ付きFiberが応答完了とReactのcommitを待ってキャッシュし、先に公開が退役したら待機をやめます。 どちらの場合も後始末で応答を解放します。 これは リクエストからブラウザーへの流れ の中で、ナビゲーション と同じ所有権の境界を共有します。