アーキテクチャ

02. アプリケーション定義

アプリケーション定義がページとサービスをリクエスト時の実行につなぐ仕組みを、実装から読み解きます。

アプリケーション定義は、Pageが使えるサービスを取得せずに記述します。 Layerとmiddlewareがリクエスト時にサービスを供給し、共通のアプリケーションidentityが定義と描画runtimeを結び付けます。

サービスを獲得する前に契約を定義する

application/effront.ts の Application.effront<Services>() は、Page、Layout、Routesなどのfactoryを返します。 サービスやHTTPサーバーは起動しません。 返り値の EFFRONT<ApplicationServices, AvailableServices> は、二つの契約を分けています。

  • ApplicationServices はアプリケーションのLayerが供給するサービスです。
  • AvailableServices は、そのfactoryから作った定義が使えるサービスです。Pageの描画とパラメーターSchemaのデコードも対象です。 初期状態ではApplicationServicesと同じですが、middlewareで一つの枝だけを拡張できます。

application/definition.tsx の ApplicationLayerOptions は、Servicesが never でなければLayerを必須にします。 省略可能なLayerを渡さなければ Layer.empty を使います。EFFRONT.make は、この提供元とコンパイル済みRoutesを保持します。 rootはアプリケーションのidentityを共有し、Layoutと少なくとも一つのPageを持ち、予約済みのパスを避ける必要があります。

提供元自身の依存関係は、ApplicationDefinition<Services, ApplicationError, Requirements> の Requirements に残り、構築時の失敗はApplicationErrorに残ります。 RequirementsをPageが自動で使えるわけではありません。 外部のServiceを公開するには、たとえば Layer.effect(Service, Service) で既存のインスタンスを渡すなど、アプリケーションのLayerから提供する必要があります。

middlewareで一つの枝を拡張する

認証済みの枝だけが使うサービスは、その枝のAvailableServicesに加えます。withMiddleware は、追加するサービス型と提供を担当するmiddlewareを記録した新しいfactoryを作ります。

packages/core/src/application/effront.ts の抜粋
  const withMiddleware = <Value extends AnyMiddleware<ApplicationServices>>(
    value: Value & ApplicableMiddleware<AvailableServices, Value>,
  ): EFFRONT<ApplicationServices, AvailableServices | MiddlewareProvidedServices<Value>> => {
    getMiddlewareState(value);
    if (getEFFRONTIdentity(value) !== identity) {
      throw new TypeError("Middleware was created by a different EFFRONT module.");
    }
    if (middleware.includes(value)) {
      throw new TypeError("Middleware cannot appear twice in the same scope.");
    }

    return makeEFFRONT(identity, Object.freeze([...middleware, value]), allocateRouteScopeId, make);
  };

ApplicableMiddleware は、次のmiddlewareが要求するサービスをすでに利用できることを確認します。 返されるfactoryは、その集合に MiddlewareProvidedServices を加えます。 実行時には、不正なmemberの種別、別のアプリケーションidentity、同じスコープ内の重複を拒否します。 元のfactoryは変わらないため、スコープ付きの枝とそうでない枝を共存させられます。

登録も provides の型宣言も、サービスを注入しません。application/middleware.ts のhandlerが、包むHTTP応答のEffectへサービスを提供する必要があります。 拡張したfactoryの定義は、リクエスト時に実行するmiddlewareの鎖を保持します。

  • PageのGET/HEAD:Effect HTTPのネイティブなmiddleware descriptorを使い、HEADフォールバックなどのルーティング動作を保ちます。
  • Server FunctionのPOST:Reactのデコードで関数のスコープを特定してから、applyMiddleware がEffectをhandlerで包みます。reduceRight により先の登録が外側になり、後のmiddlewareへサービスを供給できます。

関連する定義を同じアプリケーションに保つ

一度の Application.effront 呼び出しから派生したfactoryは、identity、ルートスコープのID割り当て関数、make 関数を共有します。withMiddleware は別のアプリケーションを作らず、これらを引き継ぎます。

packages/core/src/application/effront.ts の抜粋
const effront = <Services = never>(): EFFRONT<Services> => {
  const identity = makeEFFRONTIdentity<Services>();
  const make: EFFRONTMake<Services> = (options) => makeApplication(identity, options);
  let nextRouteScopeId = 0;
  const allocateRouteScopeId = () => {
    const scopeId = nextRouteScopeId;
    nextRouteScopeId += 1;
    return scopeId;
  };

  return makeEFFRONT(identity, Object.freeze([]), allocateRouteScopeId, make);
};

呼び出しが別なら、Servicesの型が同じでもidentityは別です。Routes.page、Routes.mount、makeApplication はidentityの混在を TypeError で拒否します。 これは定義の接続ミスであり、リクエスト入力の不正ではありません。

application/effront-identity.ts は、identityとmember種別を表す EFFRONTMember のsymbolで所属を記録します。 identityはServicesを不変に扱う型の印と専用の renderRuntime を持ち、定義時の契約をリクエスト時の実行へ結び付けます。

リクエストの中で定義を実行する

server/application.ts は、リクエスト用にアプリケーションとレンダラーのLayerを構築します。RequestContextMiddleware が取得済みサービスをHTTP Contextへ合流させ、スコープ付きmiddlewareが選択された定義の要求するサービスを追加できます。

ReactはEffectの呼び出しスタックの外からPageやLayoutを呼びます。application/render-runtime.ts は、bind でEffectの実行関数と有効なmiddlewareをAsyncLocalStorageに保持し、run で描画Effectをその実行関数へ渡します。 runtimeの束縛がない場合や必要なmiddlewareが無効な場合は TypeError になります。 正しいサービス型だけでは、これらの実行時スコープ検査を代替できません。

ルートの組み立て は定義を描画先へ接続します。リクエスト処理 はサービスの所有権を説明し、描画 はReactとの境界を追います。