refactor: 리펙토링

This commit is contained in:
DongHyeonka
2026-08-01 19:39:59 +09:00
parent 9c959ea2a5
commit c6da03369c
171 changed files with 20329 additions and 782 deletions
+44 -2
View File
@@ -1,6 +1,12 @@
import { resolveRuntimeCapabilities } from "../contracts/runtime-capabilities.ts";
import { INSTALLED_RUNTIME_CAPABILITIES } from "../features/installed-runtime-capabilities.ts";
import { createCompositionRoot } from "./composition-root.ts";
import { loadReleaseManifest } from "./load-release-manifest.ts";
import { loadRuntimeConfig } from "./load-runtime-config.ts";
import {
createOptionalRuntimeHost,
type OptionalRuntimeHost,
} from "./optional-runtime-host.ts";
import { createRuntimeAdapters } from "./runtime-adapters.ts";
export type RuntimeCompositionDependencies = Readonly<{
@@ -8,10 +14,16 @@ export type RuntimeCompositionDependencies = Readonly<{
host?: Record<string, unknown>;
}>;
export function createRuntimeComposition(
/**
* §6.7 steps 7 and 14. The static capability selection is compiled against the
* runtime overrides, and the optional host objects are created without any
* start side effect. Nothing observable happens until the first committed React
* effect calls `optional.startAfterMount()`.
*/
export async function createRuntimeComposition(
dependencies: RuntimeCompositionDependencies = {},
) {
return createCompositionRoot({
const root = await createCompositionRoot({
loadConfig: () => loadRuntimeConfig({ fetcher: dependencies.fetcher }),
loadRelease: (runtime) =>
loadReleaseManifest(runtime, { fetcher: dependencies.fetcher }),
@@ -23,6 +35,36 @@ export function createRuntimeComposition(
host: dependencies.host,
}),
});
const capabilities = resolveRuntimeCapabilities(
INSTALLED_RUNTIME_CAPABILITIES,
root.config.config.CAPABILITY_OVERRIDES,
);
const optional: OptionalRuntimeHost = createOptionalRuntimeHost({
capabilities,
routerBasePath: root.config.build.routerBasePath,
buildId: root.release.buildId,
...(dependencies.host
? {
host: dependencies.host as Parameters<
typeof createOptionalRuntimeHost
>[0]["host"],
}
: {}),
});
return Object.freeze({
...root,
capabilities,
optional,
async dispose(): Promise<void> {
// §20.3. Optional runtime first, then the base infrastructure: the query
// cache is cleared only after realtime and worker admission has closed,
// so a late effect cannot repopulate a cleared cache.
await optional.stop("APPLICATION_SHUTDOWN");
root.infrastructure.dispose();
},
});
}
export type RuntimeComposition = Awaited<
+152 -50
View File
@@ -1,41 +1,77 @@
import { z } from "zod";
import {
verifyContractSet,
type ContractSet,
type ContractSetFailureCode,
} from "../contracts/contract-set.ts";
import type { ContractSetPackage } from "../contracts/contract-set-canonical.ts";
import {
releaseManifestV1ArtifactSchema,
releaseManifestV2ArtifactSchema,
type ReleaseManifestV1Artifact,
type ReleaseManifestV2Artifact,
} from "../contracts/release-artifacts.ts";
import { EXPECTED_CONTRACT_SET_PACKAGES } from "../features/installed-contract-contributions.ts";
import {
BOOT_JSON_POLICIES,
readBoundedBootJson,
type BootLoadFailure,
} from "./read-bounded-boot-json.ts";
import type { RuntimeConfigLoadResult } from "./load-runtime-config.ts";
const version = z.string().regex(/^\d+(?:\.\d+){0,2}$/);
export const releaseManifestSchema = z
.object({
schemaVersion: z.literal(1),
appVersion: z.string().min(1),
buildId: z.string().min(1),
commitSha: z.string().min(1),
configSchemaVersion: version,
apiContractVersion: version,
assetManifestHash: z.string().min(1),
releaseId: z.string().min(1),
builtAt: z.string().min(1),
routeChunks: z.record(z.string().min(1), z.string().min(1)),
})
.strict();
/** §5.8. Retained for one compatibility window; carries the removed scalar. */
export const releaseManifestV1Schema = releaseManifestV1ArtifactSchema;
/** §5.2. The frontend build's compiled external contract package set. */
export const releaseManifestV2Schema = releaseManifestV2ArtifactSchema;
export type ReleaseManifestV1 = ReleaseManifestV1Artifact;
export type ReleaseManifestV2 = ReleaseManifestV2Artifact;
/**
* The composition-facing manifest. A V1 document is normalized onto it with a
* null contract set so downstream runtime never branches on schema version.
*/
export type ReleaseManifest = Readonly<{
schemaVersion: 1 | 2;
appVersion: string;
buildId: string;
commitSha: string;
configSchemaVersion: string;
assetManifestHash: string;
releaseId: string;
builtAt: string;
routeChunks: Readonly<Record<string, string>>;
contractSet: ContractSet | null;
legacyApiContractVersion?: string;
}>;
export type ReleaseManifest = z.output<typeof releaseManifestSchema>;
export type ReleaseManifestErrorCode =
| "MANIFEST_BUILD_MISMATCH"
| "MANIFEST_PROTOCOL_PAIR_MISMATCH"
| "MANIFEST_CONFIG_SCHEMA_MISMATCH"
| "MANIFEST_API_CONTRACT_MISMATCH"
| "MANIFEST_RELEASE_MISMATCH"
| "MANIFEST_ASSET_MISMATCH"
| "MANIFEST_FETCH_FAILED"
| "MANIFEST_TIMEOUT"
| "MANIFEST_HTTP_FAILED"
| "MANIFEST_CONTENT_TYPE_INVALID"
| "MANIFEST_BODY_TOO_LARGE"
| "MANIFEST_UTF8_INVALID"
| "MANIFEST_JSON_INVALID"
| "MANIFEST_SCHEMA_INVALID";
| "MANIFEST_SCHEMA_INVALID"
| ContractSetFailureCode;
export type ReleaseManifestFailureKind =
| "BUILD_MISMATCH"
| "PROTOCOL_PAIR_MISMATCH"
| "CONFIG_MISMATCH"
| "API_CONTRACT_MISMATCH"
| "RELEASE_MISMATCH"
| "ASSET_MISMATCH"
| "CONTRACT_SET_MISMATCH"
| "RELEASE_MANIFEST_FAILURE";
export type ReleaseManifestSafe = Readonly<{
kind: ReleaseManifestFailureKind;
code: ReleaseManifestErrorCode;
@@ -55,6 +91,8 @@ function failureKindFor(
switch (code) {
case "MANIFEST_BUILD_MISMATCH":
return "BUILD_MISMATCH";
case "MANIFEST_PROTOCOL_PAIR_MISMATCH":
return "PROTOCOL_PAIR_MISMATCH";
case "MANIFEST_CONFIG_SCHEMA_MISMATCH":
return "CONFIG_MISMATCH";
case "MANIFEST_API_CONTRACT_MISMATCH":
@@ -64,7 +102,9 @@ function failureKindFor(
case "MANIFEST_ASSET_MISMATCH":
return "ASSET_MISMATCH";
default:
return "RELEASE_MANIFEST_FAILURE";
return code.startsWith("CONTRACT_")
? "CONTRACT_SET_MISMATCH"
: "RELEASE_MANIFEST_FAILURE";
}
}
@@ -88,63 +128,111 @@ export class ReleaseManifestError extends Error {
}
}
const READ_FAILURE_CODE: Readonly<
Record<BootLoadFailure, ReleaseManifestErrorCode>
> = Object.freeze({
FETCH_FAILED: "MANIFEST_FETCH_FAILED",
TIMEOUT: "MANIFEST_TIMEOUT",
HTTP_STATUS_INVALID: "MANIFEST_HTTP_FAILED",
CONTENT_TYPE_INVALID: "MANIFEST_CONTENT_TYPE_INVALID",
BODY_TOO_LARGE: "MANIFEST_BODY_TOO_LARGE",
UTF8_INVALID: "MANIFEST_UTF8_INVALID",
JSON_INVALID: "MANIFEST_JSON_INVALID",
SHAPE_INVALID: "MANIFEST_SCHEMA_INVALID",
SECRET_NAME_REJECTED: "MANIFEST_SCHEMA_INVALID",
SCHEMA_INVALID: "MANIFEST_SCHEMA_INVALID",
BUILD_MISMATCH: "MANIFEST_BUILD_MISMATCH",
RELEASE_MISMATCH: "MANIFEST_RELEASE_MISMATCH",
ASSET_MISMATCH: "MANIFEST_ASSET_MISMATCH",
CONTRACT_SET_MISMATCH: "CONTRACT_SET_DIGEST_MISMATCH",
});
export type FetchReleaseManifestOptions = Readonly<{
fetcher?: typeof fetch;
buildId: string;
releaseId?: string;
signal?: AbortSignal;
}>;
/**
* Fetches and validates the active manifest without imposing the current
* build tuple. Chunk recovery uses this no-store view to detect a new release.
* Fetches and validates the active manifest without imposing the current build
* tuple. Chunk recovery uses this no-store view to detect a new release.
*/
export async function fetchReleaseManifest(
url: string,
options: FetchReleaseManifestOptions,
): Promise<Readonly<ReleaseManifest>> {
const fetcher = options.fetcher ?? fetch;
let response: Response;
try {
response = await fetcher(url, {
cache: "no-store",
headers: { Accept: "application/json" },
});
} catch {
throw new ReleaseManifestError("MANIFEST_FETCH_FAILED", options);
): Promise<ReleaseManifest> {
const outcome = await readBoundedBootJson(
url,
BOOT_JSON_POLICIES.RELEASE_MANIFEST,
{
...(options.fetcher ? { fetcher: options.fetcher } : {}),
...(options.signal ? { signal: options.signal } : {}),
},
);
if (!outcome.ok) {
throw new ReleaseManifestError(READ_FAILURE_CODE[outcome.failure], options);
}
if (!response.ok) {
throw new ReleaseManifestError("MANIFEST_HTTP_FAILED", options);
if (outcome.value.schemaVersion === 2) {
const parsed = releaseManifestV2Schema.safeParse(outcome.value);
if (!parsed.success) {
throw new ReleaseManifestError("MANIFEST_SCHEMA_INVALID", options);
}
return Object.freeze(structuredClone(parsed.data));
}
let raw: unknown;
try {
raw = await response.json();
} catch {
throw new ReleaseManifestError("MANIFEST_JSON_INVALID", options);
}
const parsed = releaseManifestSchema.safeParse(raw);
const parsed = releaseManifestV1Schema.safeParse(outcome.value);
if (!parsed.success) {
throw new ReleaseManifestError("MANIFEST_SCHEMA_INVALID", options);
}
return Object.freeze(structuredClone(parsed.data));
const { apiContractVersion, ...rest } = structuredClone(parsed.data);
return Object.freeze({
...rest,
contractSet: null,
legacyApiContractVersion: apiContractVersion,
});
}
export type LoadReleaseManifestOptions = Readonly<{
fetcher?: typeof fetch;
expectedAssetManifestHash?: string;
expectedContractSetPackages?: readonly ContractSetPackage[];
signal?: AbortSignal;
}>;
export async function loadReleaseManifest(
runtime: RuntimeConfigLoadResult,
options: LoadReleaseManifestOptions = {},
): Promise<Readonly<ReleaseManifest>> {
): Promise<ReleaseManifest> {
const identity: ReleaseManifestSafeInput = {
buildId: runtime.build.buildId,
...(runtime.config.RELEASE_ID
? { releaseId: runtime.config.RELEASE_ID }
: {}),
};
const manifest = await fetchReleaseManifest(
runtime.config.RELEASE_MANIFEST_URL,
{
fetcher: options.fetcher,
...(options.fetcher ? { fetcher: options.fetcher } : {}),
...(options.signal ? { signal: options.signal } : {}),
buildId: runtime.build.buildId,
releaseId: runtime.config.RELEASE_ID,
...(runtime.config.RELEASE_ID
? { releaseId: runtime.config.RELEASE_ID }
: {}),
},
);
const expectedManifestVersion = runtime.configSchema === "V1" ? 1 : 2;
if (manifest.schemaVersion !== expectedManifestVersion) {
throw new ReleaseManifestError(
"MANIFEST_PROTOCOL_PAIR_MISMATCH",
identity,
);
}
// §6.7 steps 5-6, in order: build, config, release, assets, then contractSet.
let mismatchCode: ReleaseManifestErrorCode | null = null;
if (manifest.buildId !== runtime.build.buildId) {
mismatchCode = "MANIFEST_BUILD_MISMATCH";
@@ -164,7 +252,11 @@ export async function loadReleaseManifest(
}
if (
!mismatchCode &&
manifest.apiContractVersion !== runtime.config.API_CONTRACT_VERSION
runtime.configSchema === "V1" &&
(manifest.legacyApiContractVersion === undefined ||
runtime.config.LEGACY_API_CONTRACT_VERSION === undefined ||
manifest.legacyApiContractVersion !==
runtime.config.LEGACY_API_CONTRACT_VERSION)
) {
mismatchCode = "MANIFEST_API_CONTRACT_MISMATCH";
}
@@ -183,10 +275,20 @@ export async function loadReleaseManifest(
mismatchCode = "MANIFEST_ASSET_MISMATCH";
}
if (mismatchCode) {
throw new ReleaseManifestError(mismatchCode, {
buildId: runtime.build.buildId,
releaseId: runtime.config.RELEASE_ID,
});
throw new ReleaseManifestError(mismatchCode, identity);
}
if (manifest.schemaVersion === 2 && manifest.contractSet) {
const verification = await verifyContractSet({
expected:
options.expectedContractSetPackages ??
(EXPECTED_CONTRACT_SET_PACKAGES as readonly ContractSetPackage[]),
manifest: manifest.contractSet,
});
if (!verification.ok) {
throw new ReleaseManifestError(verification.code, identity);
}
}
return manifest;
}
+53 -38
View File
@@ -1,9 +1,19 @@
import { assertSafeConfigNames, getBuildConfig } from "../contracts/env.ts";
import {
BOOT_JSON_POLICIES,
readBoundedBootJson,
type BootLoadFailure,
} from "./read-bounded-boot-json.ts";
import {
validateRuntimeConfig,
type RuntimeConfig,
} from "./runtime-config-schema.ts";
/**
* §6.6. A safe boot error carries the failure kind, the build identity and a
* support reference. It never carries a URL, a response body, a validation
* value, an endpoint hostname or a stack trace.
*/
export type BootConfigSafe = Readonly<{
kind: "BOOT_CONFIG_FAILURE";
code: string;
@@ -43,58 +53,57 @@ export type RuntimeConfigLoadOptions = Readonly<{
fetcher?: typeof fetch;
buildConfig?: ReturnType<typeof getBuildConfig>;
now?: () => number;
signal?: AbortSignal;
}>;
export type RuntimeConfigLoadResult = Readonly<{
config: RuntimeConfig;
configSchema: "V1" | "V2";
build: ReturnType<typeof getBuildConfig>;
validationDurationMs: number;
}>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
const READ_FAILURE_CODE: Readonly<Record<BootLoadFailure, string>> =
Object.freeze({
FETCH_FAILED: "CONFIG_FETCH_FAILED",
TIMEOUT: "CONFIG_TIMEOUT",
HTTP_STATUS_INVALID: "CONFIG_HTTP_FAILED",
CONTENT_TYPE_INVALID: "CONFIG_CONTENT_TYPE_INVALID",
BODY_TOO_LARGE: "CONFIG_BODY_TOO_LARGE",
UTF8_INVALID: "CONFIG_UTF8_INVALID",
JSON_INVALID: "CONFIG_JSON_INVALID",
SHAPE_INVALID: "CONFIG_SHAPE_INVALID",
SECRET_NAME_REJECTED: "CONFIG_SECRET_NAME_REJECTED",
SCHEMA_INVALID: "CONFIG_SCHEMA_INVALID",
BUILD_MISMATCH: "CONFIG_BUILD_MISMATCH",
RELEASE_MISMATCH: "CONFIG_RELEASE_MISMATCH",
ASSET_MISMATCH: "CONFIG_ASSET_MISMATCH",
CONTRACT_SET_MISMATCH: "CONFIG_CONTRACT_SET_MISMATCH",
});
export async function loadRuntimeConfig(
options: RuntimeConfigLoadOptions = {},
): Promise<RuntimeConfigLoadResult> {
const fetcher = options.fetcher ?? fetch;
const buildConfig = options.buildConfig ?? getBuildConfig();
const now = options.now ?? performance.now.bind(performance);
const outcome = await readBoundedBootJson(
buildConfig.runtimeConfigUrl,
BOOT_JSON_POLICIES.RUNTIME_CONFIG,
{
...(options.fetcher ? { fetcher: options.fetcher } : {}),
...(options.signal ? { signal: options.signal } : {}),
},
);
if (!outcome.ok) {
throw new BootConfigError(READ_FAILURE_CODE[outcome.failure], {
buildId: buildConfig.buildId,
});
}
const startedAt = now();
let response: Response;
try {
response = await fetcher(buildConfig.runtimeConfigUrl, {
cache: "no-store",
headers: { Accept: "application/json" },
});
} catch {
throw new BootConfigError("CONFIG_FETCH_FAILED", {
buildId: buildConfig.buildId,
});
}
if (!response.ok) {
throw new BootConfigError("CONFIG_HTTP_FAILED", {
buildId: buildConfig.buildId,
});
}
let rawConfig: unknown;
try {
rawConfig = await response.json();
} catch {
throw new BootConfigError("CONFIG_JSON_INVALID", {
buildId: buildConfig.buildId,
});
}
if (!isRecord(rawConfig)) {
throw new BootConfigError("CONFIG_SHAPE_INVALID", {
buildId: buildConfig.buildId,
});
}
const rawConfig = outcome.value;
try {
assertSafeConfigNames(rawConfig);
@@ -119,7 +128,10 @@ export async function loadRuntimeConfig(
});
}
if (validated.data.BUILD_ID && validated.data.BUILD_ID !== buildConfig.buildId) {
if (
validated.data.BUILD_ID &&
validated.data.BUILD_ID !== buildConfig.buildId
) {
throw new BootConfigError("CONFIG_BUILD_MISMATCH", {
buildId: buildConfig.buildId,
configSchemaVersion: validated.data.CONFIG_SCHEMA_VERSION,
@@ -127,8 +139,11 @@ export async function loadRuntimeConfig(
});
}
// §6.10: the validated snapshot is frozen. Nothing re-reads or mutates it;
// a kill-switch change only takes effect on a new boot.
return Object.freeze({
config: validated.data,
configSchema: validated.schema,
build: buildConfig,
validationDurationMs: now() - startedAt,
});
+1 -1
View File
@@ -25,7 +25,7 @@ async function boot(): Promise<void> {
initializeColorScheme(composition.application.preferences);
root.render(<RuntimeApplication composition={composition} />);
import.meta.hot?.dispose(() => {
composition.infrastructure.dispose();
void composition.dispose();
});
} catch (error: unknown) {
const safe =
+152
View File
@@ -0,0 +1,152 @@
import {
createBrowserLifecycleRuntime,
type BrowserLifecycleRuntime,
} from "../adapters/platform/browser-lifecycle.ts";
import type {
ResolvedRuntimeCapabilities,
RuntimeHealth,
RuntimeStopReason,
} from "../contracts/runtime-capabilities.ts";
import type { ServiceWorkerRuntimeHost } from "../contracts/service-worker.ts";
import { createServiceWorkerRuntimeHost } from "./register-service-worker.ts";
/**
* §3.4 / §20.3. Optional runtime host.
*
* Creating this object has no side effect: no listener, timer, network call,
* IndexedDB open or worker is created until `startAfterMount()` runs from the
* first committed React effect. `stop()` unwinds in reverse order.
*/
export type OptionalRuntimeHost = Readonly<{
readonly realtime: null;
readonly webWorkers: null;
readonly serviceWorker: ServiceWorkerRuntimeHost | null;
readonly offlineCommands: null;
browserLifecycle(): BrowserLifecycleRuntime | null;
health(): Readonly<Record<string, RuntimeHealth>>;
startAfterMount(): Promise<void>;
stop(reason?: RuntimeStopReason): Promise<void>;
}>;
export type OptionalRuntimeHostInput = Readonly<{
capabilities: ResolvedRuntimeCapabilities;
routerBasePath: string;
buildId: string;
/** Explicit host seam for deterministic lifecycle tests and platform shells. */
serviceWorkerHost?: ServiceWorkerRuntimeHost | null;
browserLifecycleHost?: Parameters<typeof createBrowserLifecycleRuntime>[0];
host?: Parameters<typeof createServiceWorkerRuntimeHost>[0]["host"];
blockers?: readonly (() => boolean)[];
observe?: (observation: Readonly<{ event: string; outcome: string }>) => void;
}>;
export function createOptionalRuntimeHost(
input: OptionalRuntimeHostInput,
): OptionalRuntimeHost {
const { capabilities } = input;
const serviceWorker = Object.hasOwn(input, "serviceWorkerHost")
? (input.serviceWorkerHost ?? null)
: createServiceWorkerRuntimeHost({
capabilities,
routerBasePath: input.routerBasePath,
buildId: input.buildId,
...(input.host ? { host: input.host } : {}),
...(input.blockers ? { blockers: input.blockers } : {}),
...(input.observe ? { observe: input.observe } : {}),
});
let lifecycle: BrowserLifecycleRuntime | null = null;
let started = false;
let startPromise: Promise<void> | null = null;
let stopPromise: Promise<void> | null = null;
let stopRequested = false;
const health: Record<string, RuntimeHealth> = {
realtime: capabilities.realtime.length === 0 ? "DISABLED" : "UNAVAILABLE",
webWorkers: capabilities.webWorkers.length === 0 ? "DISABLED" : "UNAVAILABLE",
serviceWorker: serviceWorker ? "UNAVAILABLE" : "DISABLED",
offlineCommands: capabilities.offlineCommands ? "UNAVAILABLE" : "DISABLED",
};
/**
* §3.4 start order:
* 1. offline foreground browser lifecycle observer
* 2. realtime runtime
* 3. no Web Worker prewarm
* 4. Service Worker active/cleanup controller
*/
async function startAfterMount(): Promise<void> {
// A stopped host is terminal. Its children (notably the Service Worker page
// controller) own one-shot listeners and timers and cannot be resurrected.
if (stopPromise) return stopPromise;
if (startPromise) return startPromise;
startPromise = (async () => {
// 1. The lifecycle observer is the single window listener owner. It is
// only created when something downstream can actually consume it.
if (serviceWorker || capabilities.realtime.length > 0) {
lifecycle = createBrowserLifecycleRuntime(input.browserLifecycleHost);
}
// 2. Realtime stays NOT_SELECTED until a product contribution exists
// (§13.6), so there is nothing to start and nothing to observe.
// 3. Web Workers are created lazily on first task; there is no prewarm.
// 4. Service Worker registration or the exact-owned cleanup action.
if (serviceWorker) {
const outcome = await serviceWorker.start();
// `stop()` may have fenced this generation while start was awaiting a
// browser operation. A late result must never reactivate health.
if (!stopRequested) {
health.serviceWorker =
outcome.kind === "ACTIVE"
? "AVAILABLE"
: outcome.kind === "DISABLED"
? "DISABLED"
: outcome.kind === "INCOMPATIBLE"
? "INCOMPATIBLE"
: "DEGRADED";
}
}
if (!stopRequested) started = true;
})();
return startPromise;
}
async function stop(
reason: RuntimeStopReason = "APPLICATION_SHUTDOWN",
): Promise<void> {
void reason;
stopRequested = true;
if (stopPromise) return stopPromise;
stopPromise = (async () => {
// Serialize teardown behind any in-flight browser registration. Cleanup
// then observes the final acquired resources and unwinds them exactly once.
await startPromise?.catch(() => {});
// Reverse of the start order.
if (serviceWorker && (started || startPromise)) {
await serviceWorker.stop().catch(() => {});
}
if (serviceWorker) {
health.serviceWorker = "DISABLED";
}
lifecycle?.dispose();
lifecycle = null;
started = false;
startPromise = null;
})();
return stopPromise;
}
return Object.freeze({
realtime: null,
webWorkers: null,
serviceWorker,
offlineCommands: null,
browserLifecycle: () => lifecycle,
health: () => Object.freeze({ ...health }),
startAfterMount,
stop,
});
}
+189
View File
@@ -0,0 +1,189 @@
/**
* §6.4–§6.5. The only boot-time JSON reader.
*
* `response.json()` and unbounded `response.text()` are prohibited: a hostile or
* misconfigured origin must not be able to amplify boot memory, and an HTML
* error page must not reach `JSON.parse` as if it were configuration.
*/
export type BootJsonOperation = "RUNTIME_CONFIG" | "RELEASE_MANIFEST";
export interface BootJsonPolicy {
readonly operation: BootJsonOperation;
readonly maximumBytes: number;
readonly totalDeadlineMs: 5_000;
}
export const BOOT_JSON_POLICIES = Object.freeze({
RUNTIME_CONFIG: Object.freeze({
operation: "RUNTIME_CONFIG" as const,
maximumBytes: 65_536,
totalDeadlineMs: 5_000 as const,
}),
RELEASE_MANIFEST: Object.freeze({
operation: "RELEASE_MANIFEST" as const,
maximumBytes: 1_048_576,
totalDeadlineMs: 5_000 as const,
}),
} satisfies Readonly<Record<BootJsonOperation, BootJsonPolicy>>);
export type BootLoadFailure =
| "FETCH_FAILED"
| "TIMEOUT"
| "HTTP_STATUS_INVALID"
| "CONTENT_TYPE_INVALID"
| "BODY_TOO_LARGE"
| "UTF8_INVALID"
| "JSON_INVALID"
| "SHAPE_INVALID"
| "SECRET_NAME_REJECTED"
| "SCHEMA_INVALID"
| "BUILD_MISMATCH"
| "RELEASE_MISMATCH"
| "ASSET_MISMATCH"
| "CONTRACT_SET_MISMATCH";
export type BootJsonOutcome =
| Readonly<{ ok: true; value: Readonly<Record<string, unknown>> }>
| Readonly<{ ok: false; failure: BootLoadFailure }>;
export type ReadBoundedBootJsonOptions = Readonly<{
fetcher?: typeof fetch;
signal?: AbortSignal;
}>;
function isJsonMediaType(headerValue: string | null): boolean {
if (!headerValue) return false;
const essence = headerValue.split(";", 1)[0]?.trim().toLowerCase() ?? "";
return essence === "application/json" || essence.endsWith("+json");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
export async function readBoundedBootJson(
url: string,
policy: BootJsonPolicy,
options: ReadBoundedBootJsonOptions = {},
): Promise<BootJsonOutcome> {
if (options.signal?.aborted) return fail("FETCH_FAILED");
const fetcher = options.fetcher ?? fetch;
const controller = new AbortController();
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
controller.abort();
}, policy.totalDeadlineMs);
const forwardAbort = () => controller.abort();
options.signal?.addEventListener("abort", forwardAbort, { once: true });
try {
let response: Response;
try {
response = await fetcher(url, {
method: "GET",
cache: "no-store",
credentials: "same-origin",
redirect: "error",
referrerPolicy: "no-referrer",
headers: { Accept: "application/json" },
signal: controller.signal,
});
} catch {
return fail(timedOut ? "TIMEOUT" : "FETCH_FAILED");
}
if (response.status !== 200 || response.redirected) {
await discard(response);
return fail("HTTP_STATUS_INVALID");
}
if (!isJsonMediaType(response.headers.get("content-type"))) {
await discard(response);
return fail("CONTENT_TYPE_INVALID");
}
const declared = Number(response.headers.get("content-length"));
if (Number.isFinite(declared) && declared > policy.maximumBytes) {
await discard(response);
return fail("BODY_TOO_LARGE");
}
const bytes = await readBoundedBytes(response, policy.maximumBytes, controller);
if (bytes === "TOO_LARGE") return fail("BODY_TOO_LARGE");
if (bytes === "STREAM_FAILED") return fail(timedOut ? "TIMEOUT" : "FETCH_FAILED");
let text: string;
try {
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} catch {
return fail("UTF8_INVALID");
}
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
return fail("JSON_INVALID");
}
if (!isRecord(parsed)) return fail("SHAPE_INVALID");
return Object.freeze({ ok: true as const, value: parsed });
} finally {
clearTimeout(timer);
options.signal?.removeEventListener("abort", forwardAbort);
}
}
async function readBoundedBytes(
response: Response,
maximumBytes: number,
controller: AbortController,
): Promise<Uint8Array | "TOO_LARGE" | "STREAM_FAILED"> {
const body = response.body;
if (!body) {
// A body-less 200 cannot satisfy any boot document.
return new Uint8Array(0);
}
const reader = body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
total += value.byteLength;
if (total > maximumBytes) {
await reader.cancel().catch(() => {});
controller.abort();
return "TOO_LARGE";
}
chunks.push(value);
}
} catch {
await reader.cancel().catch(() => {});
return "STREAM_FAILED";
}
const output = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
output.set(chunk, offset);
offset += chunk.byteLength;
}
return output;
}
async function discard(response: Response): Promise<void> {
try {
await response.body?.cancel();
} catch {
// Cancelling an already-settled body is not a boot failure.
}
}
function fail(failure: BootLoadFailure): BootJsonOutcome {
return Object.freeze({ ok: false as const, failure });
}
+57
View File
@@ -0,0 +1,57 @@
import type {
ResolvedRuntimeCapabilities,
} from "../contracts/runtime-capabilities.ts";
import type { ServiceWorkerRuntimeHost } from "../contracts/service-worker.ts";
import { createServiceWorkerPageController } from "../adapters/service-worker/service-worker-page-controller.ts";
/**
* §17.5. Composition-side factory. The host object is created without any
* side effect; `start()` is only called from the post-mount runtime starter,
* after Runtime Config, the release manifest and the contract set have all
* validated and React has committed its first render.
*/
export type ServiceWorkerHostInput = Readonly<{
capabilities: ResolvedRuntimeCapabilities;
routerBasePath: string;
buildId: string;
host?: Readonly<{
location?: Pick<Location, "origin">;
navigator?: Pick<Navigator, "serviceWorker">;
caches?: CacheStorage;
document?: Pick<Document, "visibilityState">;
}>;
blockers?: readonly (() => boolean)[];
observe?: (observation: Readonly<{ event: string; outcome: string }>) => void;
}>;
export function createServiceWorkerRuntimeHost(
input: ServiceWorkerHostInput,
): ServiceWorkerRuntimeHost | null {
const { capabilities } = input;
// §3.6. `null` selection with no disable-cleanup obligation means zero
// registration lookups and zero Cache Storage access: no host at all.
if (!capabilities.serviceWorker && !capabilities.serviceWorkerDisabledCleanup) {
return null;
}
const host =
input.host ??
(globalThis as unknown as NonNullable<ServiceWorkerHostInput["host"]>);
const container = host?.navigator?.serviceWorker;
const origin = host?.location?.origin;
if (!origin) return null;
return createServiceWorkerPageController({
selection: capabilities.serviceWorker,
disabledCleanup: capabilities.serviceWorkerDisabledCleanup,
routerBasePath: input.routerBasePath,
origin,
buildId: input.buildId,
...(container ? { container } : {}),
...(host?.caches ? { caches: host.caches } : {}),
...(input.blockers ? { blockers: input.blockers } : {}),
...(input.observe ? { observe: input.observe } : {}),
});
}
+213 -61
View File
@@ -6,23 +6,32 @@ import {
} from "../adapters/auth/external-session-adapter.ts";
import { createDiagnosticsAdapter } from "../adapters/diagnostics/bounded-diagnostics.ts";
import { createHttpClient } from "../adapters/http/client.ts";
import { createContractHttpExecutor } from "../adapters/http/http-execution-v3.ts";
import { createBrowserCrossContextInvalidationFromHost } from "../adapters/cross-context-invalidation/index.ts";
import { createTanStackCacheCoordinator } from "../adapters/query-cache/tanstack-cache-coordinator.ts";
import {
createTanStackCacheCoordinator,
type InstalledQueryInvalidationDefinition,
} from "../adapters/query-cache/tanstack-cache-coordinator.ts";
import { createQueryClient } from "../adapters/query-cache/tanstack-query-cache.ts";
import { createServerStateScopeRuntime } from "../adapters/query-cache/server-state-scope-runtime.ts";
import { createConditionalValidatorStore } from "../adapters/query-cache/conditional-validator-store.ts";
import { createBrowserStorageAdapter } from "../adapters/storage/browser-storage-adapter.ts";
import { createTelemetryAdapter } from "../adapters/telemetry/best-effort-telemetry.ts";
import type { AuthSessionPort } from "../application/ports/auth-session-port.ts";
import type { ReleaseInfo } from "../application/ports/release-info-port.ts";
import { createRestProviderProfile } from "../contracts/rest-profiles.ts";
import type { ClockPort } from "../application/ports/clock-port.ts";
import { createInstalledFeatureInputs } from "../features/installed-feature-adapters.ts";
import { QUERY_REGISTRY } from "../features/installed-feature-contracts.ts";
import { INSTALLED_RUNTIME_CAPABILITIES } from "../features/installed-runtime-capabilities.ts";
import { describeRuntimeCapabilities } from "../contracts/runtime-capabilities.ts";
import {
fetchReleaseManifest,
type ReleaseManifest,
} from "./load-release-manifest.ts";
import type { RuntimeConfigLoadResult } from "./load-runtime-config.ts";
import { createServerStateGenerationStore } from "./server-state-generation-store.ts";
import { COMPOSED_CONTRACT_CONTRIBUTIONS } from "../features/installed-contract-contributions.ts";
type HttpClientDependencies = Parameters<typeof createHttpClient>[0];
export type RuntimeHttpContract = Pick<
@@ -47,6 +56,26 @@ export type RuntimeAdaptersContext = Readonly<{
fetcher?: typeof fetch;
}>;
/**
* §5.2. The composition root is where a manifest becomes release info. A V2
* manifest states contract identity as a verified contract set and a V1
* manifest as the legacy scalar; the application layer sees one shape and never
* branches on the schema version to find the identity.
*/
function toReleaseInfo(manifest: Readonly<ReleaseManifest>): ReleaseInfo {
const { contractSet, legacyApiContractVersion, ...rest } =
structuredClone(manifest);
return Object.freeze({
...rest,
...(legacyApiContractVersion === undefined
? {}
: { apiContractVersion: legacyApiContractVersion }),
...(contractSet === null
? {}
: { contractSetDigest: contractSet.setDigest }),
});
}
const EXTERNAL_OWNER_METHODS = Object.freeze([
"readState",
"subscribe",
@@ -155,48 +184,69 @@ export async function createRuntimeAdapters(
});
},
});
const queryClient = createQueryClient({ diagnostics });
const crossContextInvalidation =
createBrowserCrossContextInvalidationFromHost({
...(context.host === undefined ? {} : { host: context.host }),
cacheEpoch: `release.${context.release.releaseId}`,
topics: Object.values(QUERY_REGISTRY).map((definition) =>
Object.freeze({
topic: definition.invalidationTopic,
topicVersion: definition.version,
}),
),
observe(observation) {
if (
observation.outcome !== "FAILED" &&
observation.outcome !== "DEGRADED"
) {
return;
}
diagnostics.record({
level: "warn",
eventId: "cache.operation.failed",
context: {
operation: observation.operation,
outcome: observation.outcome,
reason: observation.reason,
},
});
},
// The composition root states the registry shape it consumes rather than
// inferring it from whichever features happen to be installed, so a build
// with zero installed features still type-checks.
const queryRegistry: Readonly<
Record<string, InstalledQueryInvalidationDefinition>
> = QUERY_REGISTRY;
const conditionalValidators = createConditionalValidatorStore();
const serverStateGeneration = createServerStateGenerationStore(() => {
const queryClient = createQueryClient({ diagnostics });
const crossContextInvalidation =
createBrowserCrossContextInvalidationFromHost({
...(context.host === undefined ? {} : { host: context.host }),
cacheEpoch: `release.${context.release.releaseId}`,
topics: Object.values(queryRegistry).map((definition) =>
Object.freeze({
topic: definition.invalidationTopic,
topicVersion: definition.version,
}),
),
observe(observation) {
if (
observation.outcome !== "FAILED" &&
observation.outcome !== "DEGRADED"
) {
return;
}
diagnostics.record({
level: "warn",
eventId: "cache.operation.failed",
context: {
operation: observation.operation,
outcome: observation.outcome,
reason: observation.reason,
},
});
},
});
const queryInvalidation = createTanStackCacheCoordinator({
queryClient,
queryRegistry,
crossContext: crossContextInvalidation,
diagnostics,
});
return Object.freeze({
queryClient,
queryInvalidation,
crossContextStatus: () =>
crossContextInvalidation?.getStatus() ?? "DEGRADED_LOCAL_ONLY",
});
const queryInvalidation = createTanStackCacheCoordinator({
queryClient,
queryRegistry: QUERY_REGISTRY,
crossContext: crossContextInvalidation,
diagnostics,
});
const serverStateScope = createServerStateScopeRuntime({
session: authSession,
queryInvalidation,
});
const conditionalValidators = createConditionalValidatorStore();
const unsubscribeConditionalScope = serverStateScope.subscribe(() => {
conditionalValidators.clear();
queryInvalidation: {
resetLocal: () => serverStateGeneration.resetCurrent(),
},
participants: [
{
order: 4,
label: "conditional-validators",
close: () => conditionalValidators.clear(),
},
],
activateNextGeneration: () => serverStateGeneration.activateNext(),
});
const storage = createBrowserStorageAdapter({
localStorage: storageOrUndefined(hostValue(host, "localStorage")),
@@ -207,14 +257,24 @@ export async function createRuntimeAdapters(
});
const releaseInfo = Object.freeze({
async getCurrent() {
return structuredClone(context.release);
return toReleaseInfo(context.release);
},
async refresh() {
return fetchReleaseManifest(config.RELEASE_MANIFEST_URL, {
fetcher: context.fetcher,
buildId: context.release.buildId,
releaseId: context.release.releaseId,
});
return toReleaseInfo(
await fetchReleaseManifest(config.RELEASE_MANIFEST_URL, {
fetcher: context.fetcher,
buildId: context.release.buildId,
releaseId: context.release.releaseId,
}),
);
},
});
const runtimeCapabilities = Object.freeze({
getSnapshot() {
return describeRuntimeCapabilities(
INSTALLED_RUNTIME_CAPABILITIES,
config.CAPABILITY_OVERRIDES,
);
},
});
const navigation = Object.freeze({
@@ -226,18 +286,105 @@ export async function createRuntimeAdapters(
location.reload();
},
});
const contractHttp = createContractHttpExecutor({
baseUrl: config.API_BASE_URL,
maxRetryAttempts: config.MAX_RETRY_ATTEMPTS,
fetcher: context.fetcher,
async attachCredentials(operation) {
if (serverStateScope.getPhase() !== "READY") {
return Object.freeze({ kind: "SCOPE_FENCED" as const });
}
const state = authSession.getState();
if (state === "integration-failed") {
return Object.freeze({ kind: "UNAVAILABLE" as const });
}
if (state !== "authenticated") {
return Object.freeze({ kind: "UNAUTHENTICATED" as const });
}
try {
const patch = await authSession.credentialPatch({
origin: new URL(config.API_BASE_URL).origin,
method: operation.method,
operationId: operation.operationId,
});
if (serverStateScope.getPhase() !== "READY") {
return Object.freeze({ kind: "SCOPE_FENCED" as const });
}
return Object.freeze({
kind: "READY" as const,
headers: patch.headers,
credentials: "omit" as const,
});
} catch {
return Object.freeze({ kind: "UNAVAILABLE" as const });
}
},
observe(observation) {
try {
diagnostics.record({
level:
observation.outcome === "SUCCESS" ? "info" : "warn",
eventId: "http.request.completed",
context: {
operation_id: observation.diagnosticsOperation,
outcome: observation.outcome,
attempts: observation.attempts,
certainty: observation.certainty,
},
});
} catch {
// Diagnostics cannot change a contract execution outcome.
}
},
});
let contractExecutionSequence = 0;
const contractOperations = Object.freeze({
async execute(
operationId: string,
input: unknown,
executionContext: Readonly<{ signal?: AbortSignal }> = {},
) {
const operation =
COMPOSED_CONTRACT_CONTRIBUTIONS.httpByOperationId.get(operationId);
if (!operation) {
return Object.freeze({
kind: "CONTRACT_VIOLATION" as const,
effect: "NOT_STARTED" as const,
violation: Object.freeze({
kind: "FINAL_REQUEST_INVARIANT_FAILED" as const,
operation: "REQUEST" as const,
}),
});
}
contractExecutionSequence += 1;
const intentId = `http-intent-${contractExecutionSequence}`;
const isCommand = operation.contract.commandEffect !== null;
const requiresKey = operation.contract.retrySemantics === "KEYED";
const outcome = await contractHttp.execute(operation, input, {
scope: serverStateScope.getSnapshot(),
...(executionContext.signal === undefined
? {}
: { signal: executionContext.signal }),
...(isCommand
? {
intent: Object.freeze({
intentId,
startedBy: "USER" as const,
...(requiresKey
? { idempotencyKey: `http-key-${contractExecutionSequence}` }
: {}),
}),
}
: {}),
});
if (outcome.kind === "UNAUTHENTICATED") {
authSession.onUnauthenticated();
}
return outcome;
},
});
const featureInputs = createInstalledFeatureInputs({
createHttpClient: (contract) =>
createRuntimeHttpClient(
{
runtime: context.runtime,
authSession,
fetcher: context.fetcher,
diagnostics,
telemetry,
},
contract,
),
contractOperations,
});
return Object.freeze({
@@ -247,20 +394,25 @@ export async function createRuntimeAdapters(
diagnostics,
telemetry,
releaseInfo,
runtimeCapabilities,
navigation,
}),
infrastructure: Object.freeze({
queryClient,
queryInvalidation,
get queryClient() {
return serverStateGeneration.getSnapshot().queryClient;
},
get queryInvalidation() {
return serverStateGeneration.getSnapshot().queryInvalidation;
},
serverStateGeneration,
serverStateScope,
conditionalValidators,
crossContextInvalidationStatus: () =>
crossContextInvalidation?.getStatus() ?? "DEGRADED_LOCAL_ONLY",
serverStateGeneration.getSnapshot().crossContextStatus(),
dispose() {
unsubscribeConditionalScope();
conditionalValidators.clear();
serverStateScope.dispose();
queryInvalidation.dispose();
serverStateGeneration.dispose();
},
}),
featureInputs,
+35 -20
View File
@@ -1,9 +1,7 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react";
import { StrictMode, useEffect } from "react";
import { ApplicationProvider } from "../presentation/providers/application-provider.tsx";
import { QueryInvalidationProvider } from "../presentation/adapters/query/query-invalidation-provider.tsx";
import { ServerStateScopeProvider } from "../presentation/adapters/query/server-state-scope-provider.tsx";
import { ServerStateGenerationProvider } from "../presentation/adapters/query/server-state-generation-provider.tsx";
import { AppRouter } from "../presentation/routes/app-router.tsx";
import type { RuntimeComposition } from "./create-runtime-composition.ts";
@@ -11,27 +9,44 @@ import type { RuntimeComposition } from "./create-runtime-composition.ts";
* Production provider tree. Tests import this component so the validated
* composition is proven against the same provider order used by main.
*/
/**
* §6.7 step 16 / §17.5. Optional runtime starts from the first committed
* effect, never during render and never during composition. StrictMode double
* invocation is safe: `startAfterMount` returns the same in-flight promise.
*/
function PostMountRuntimeStarter({
composition,
}: Readonly<{ composition: RuntimeComposition }>) {
useEffect(() => {
void composition.optional.startAfterMount();
}, [composition]);
return null;
}
export function RuntimeApplication({
composition,
}: Readonly<{ composition: RuntimeComposition }>) {
return (
<StrictMode>
<QueryClientProvider client={composition.infrastructure.queryClient}>
<ServerStateScopeProvider
runtime={composition.infrastructure.serverStateScope}
>
<QueryInvalidationProvider
coordinator={composition.infrastructure.queryInvalidation}
>
<ApplicationProvider application={composition.application}>
<AppRouter
basename={composition.config.build.routerBasePath}
buildId={composition.release.buildId}
/>
</ApplicationProvider>
</QueryInvalidationProvider>
</ServerStateScopeProvider>
</QueryClientProvider>
<ServerStateGenerationProvider
store={composition.infrastructure.serverStateGeneration}
scope={composition.infrastructure.serverStateScope}
transitionFallback={
<div
aria-busy="true"
className="state-surface state-surface--loading"
data-scope-state="scope-transition"
/>
}
>
<ApplicationProvider application={composition.application}>
<PostMountRuntimeStarter composition={composition} />
<AppRouter
basename={composition.config.build.routerBasePath}
buildId={composition.release.buildId}
/>
</ApplicationProvider>
</ServerStateGenerationProvider>
</StrictMode>
);
}
+123 -64
View File
@@ -1,79 +1,138 @@
import { z } from "zod";
import {
runtimeConfigV1ArtifactSchema,
runtimeConfigV2ArtifactSchema,
type CapabilityOverrideArtifact,
type RuntimeConfigV1Artifact,
type RuntimeConfigV2Artifact,
} from "../contracts/release-artifacts.ts";
const version = z.string().regex(/^\d+(?:\.\d+){0,2}$/);
export { isValidReleaseManifestUrl } from "../contracts/release-artifacts.ts";
export const runtimeConfigSchema = z
.object({
APP_ENV: z.enum(["local", "development", "staging", "production"]),
API_BASE_URL: z.url(),
REQUEST_TIMEOUT_MS: z.int().min(100).max(60_000).default(10_000),
MAX_RETRY_ATTEMPTS: z.int().min(0).max(2).default(2),
TELEMETRY_ENABLED: z.boolean(),
TELEMETRY_ENDPOINT: z.url().optional(),
AUTH_MODE: z.enum(["external", "demo"]),
CONFIG_SCHEMA_VERSION: version,
API_CONTRACT_VERSION: version,
RELEASE_MANIFEST_URL: z.string().min(1).default("/release-manifest.json"),
RELEASE_ID: z.string().min(1).optional(),
BUILD_ID: z.string().min(1).optional(),
})
.strict()
.superRefine((config, context) => {
if (config.TELEMETRY_ENABLED && !config.TELEMETRY_ENDPOINT) {
context.addIssue({
code: "custom",
path: ["TELEMETRY_ENDPOINT"],
message: "required when telemetry is enabled",
});
}
export type CapabilityOverrides = CapabilityOverrideArtifact;
const local = config.APP_ENV === "local" || config.APP_ENV === "development";
if (!local && config.AUTH_MODE === "demo") {
context.addIssue({
code: "custom",
path: ["AUTH_MODE"],
message: "demo authentication is limited to local environments",
});
}
const endpointEntries = [
["API_BASE_URL", config.API_BASE_URL],
["TELEMETRY_ENDPOINT", config.TELEMETRY_ENDPOINT],
] as const;
/**
* §6.1. Runtime Config V2 is deployment and browser operational setting only.
* `API_CONTRACT_VERSION` is gone: a scalar cannot describe a multi-package
* contract set, and Release Manifest V2 `contractSet` owns that meaning.
*/
export const runtimeConfigV2Schema = runtimeConfigV2ArtifactSchema;
for (const [key, value] of endpointEntries) {
if (value && !local && new URL(value).protocol !== "https:") {
context.addIssue({
code: "custom",
path: [key],
message: "HTTPS is required outside local environments",
});
}
}
});
/**
* §5.8 / §24.6 RC-2. The V1 reader is retained for one compatibility window so
* a release never swaps source shape, manifest shape and runtime behaviour at
* the same time. Only a V1 document may carry the scalar contract version.
*/
export const runtimeConfigV1Schema = runtimeConfigV1ArtifactSchema;
export type RuntimeConfigV2 = RuntimeConfigV2Artifact;
export type RuntimeConfigV1 = RuntimeConfigV1Artifact;
/**
* The composition-facing shape. V1 documents are normalized onto it so the rest
* of the runtime never branches on config schema version.
*/
export type RuntimeConfig = Readonly<{
APP_ENV: RuntimeConfigV2["APP_ENV"];
API_BASE_URL: string;
REQUEST_TIMEOUT_MS: number;
MAX_RETRY_ATTEMPTS: number;
TELEMETRY_ENABLED: boolean;
TELEMETRY_ENDPOINT?: string;
AUTH_MODE: RuntimeConfigV2["AUTH_MODE"];
CONFIG_SCHEMA_VERSION: string;
RELEASE_MANIFEST_URL: string;
RELEASE_ID?: string;
BUILD_ID?: string;
CAPABILITY_OVERRIDES: CapabilityOverrides;
/** Present only while a V1 document is still accepted. */
LEGACY_API_CONTRACT_VERSION?: string;
}>;
export type RuntimeConfig = z.output<typeof runtimeConfigSchema>;
export type RuntimeConfigValidation =
| Readonly<{ success: true; data: RuntimeConfig }>
| Readonly<{ success: true; data: RuntimeConfig; schema: "V1" | "V2" }>
| Readonly<{
success: false;
issues: readonly Readonly<{ path: string; code: string }>[];
}>;
export function validateRuntimeConfig(value: unknown): RuntimeConfigValidation {
const result = runtimeConfigSchema.safeParse(value);
const DEFAULT_OVERRIDES: CapabilityOverrides = Object.freeze({
REALTIME: "DEFAULT" as const,
WEB_WORKER: "DEFAULT" as const,
SERVICE_WORKER: "DEFAULT" as const,
OFFLINE_COMMANDS: "DEFAULT" as const,
});
if (!result.success) {
return {
success: false,
issues: result.error.issues.map((issue) => ({
path: issue.path.join("."),
code: issue.code,
})),
};
function canonicalUrl(value: string): string {
return new URL(value).href;
}
export function validateRuntimeConfig(value: unknown): RuntimeConfigValidation {
const declared =
value && typeof value === "object"
? (value as Record<string, unknown>).CONFIG_SCHEMA_VERSION
: undefined;
// §5.8: no precedence between V1 and V2. The declared version selects exactly
// one parser, and a V2 document carrying the removed scalar is rejected.
if (declared !== "1" && declared !== "2.0") {
return Object.freeze({
success: false as const,
issues: Object.freeze([
Object.freeze({
path: "CONFIG_SCHEMA_VERSION",
code: "unsupported_value",
}),
]),
});
}
return {
success: true,
data: structuredClone(result.data),
};
const isV2 = declared === "2.0";
const result = isV2
? runtimeConfigV2Schema.safeParse(value)
: runtimeConfigV1Schema.safeParse(value);
if (!result.success) {
return Object.freeze({
success: false as const,
issues: Object.freeze(
result.error.issues.map((issue) =>
Object.freeze({ path: issue.path.join("."), code: issue.code }),
),
),
});
}
const parsed = result.data;
const normalized: RuntimeConfig = Object.freeze({
APP_ENV: parsed.APP_ENV,
API_BASE_URL: canonicalUrl(parsed.API_BASE_URL),
REQUEST_TIMEOUT_MS: parsed.REQUEST_TIMEOUT_MS,
MAX_RETRY_ATTEMPTS: parsed.MAX_RETRY_ATTEMPTS,
TELEMETRY_ENABLED: parsed.TELEMETRY_ENABLED,
...(parsed.TELEMETRY_ENDPOINT
? { TELEMETRY_ENDPOINT: canonicalUrl(parsed.TELEMETRY_ENDPOINT) }
: {}),
AUTH_MODE: parsed.AUTH_MODE,
CONFIG_SCHEMA_VERSION: parsed.CONFIG_SCHEMA_VERSION,
RELEASE_MANIFEST_URL: parsed.RELEASE_MANIFEST_URL,
...(parsed.RELEASE_ID ? { RELEASE_ID: parsed.RELEASE_ID } : {}),
...(parsed.BUILD_ID ? { BUILD_ID: parsed.BUILD_ID } : {}),
CAPABILITY_OVERRIDES: Object.freeze({
...(isV2
? (parsed as RuntimeConfigV2).CAPABILITY_OVERRIDES
: DEFAULT_OVERRIDES),
}),
...(isV2
? {}
: {
LEGACY_API_CONTRACT_VERSION: (parsed as RuntimeConfigV1)
.API_CONTRACT_VERSION,
}),
});
return Object.freeze({
success: true as const,
data: normalized,
schema: isV2 ? ("V2" as const) : ("V1" as const),
});
}
@@ -0,0 +1,74 @@
import type { QueryClient } from "@tanstack/react-query";
import type { CrossContextInvalidationStatus } from "../adapters/cross-context-invalidation/index.ts";
import type { QueryInvalidationCoordinator } from "../contracts/query-invalidation.ts";
export type ServerStateGenerationResources = Readonly<{
queryClient: QueryClient;
queryInvalidation: QueryInvalidationCoordinator;
crossContextStatus(): CrossContextInvalidationStatus;
}>;
export type ServerStateGenerationSnapshot =
ServerStateGenerationResources & Readonly<{ generation: number }>;
export type ServerStateGenerationStore = Readonly<{
getSnapshot(): ServerStateGenerationSnapshot;
subscribe(listener: () => void): () => void;
resetCurrent(): Promise<void>;
activateNext(): void;
dispose(): void;
}>;
export function createServerStateGenerationStore(
createResources: (generation: number) => ServerStateGenerationResources,
): ServerStateGenerationStore {
const listeners = new Set<() => void>();
let disposed = false;
let current = snapshot(1, createResources(1));
function publish(): void {
for (const listener of [...listeners]) {
try {
listener();
} catch {
// Provider defects cannot change generation ownership.
}
}
}
return Object.freeze({
getSnapshot: () => current,
subscribe(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
},
async resetCurrent() {
if (disposed) throw new TypeError("Server-state generations are disposed.");
await current.queryInvalidation.resetLocal();
},
activateNext() {
if (disposed) throw new TypeError("Server-state generations are disposed.");
const previous = current;
previous.queryInvalidation.dispose();
previous.queryClient.clear();
const generation = previous.generation + 1;
current = snapshot(generation, createResources(generation));
publish();
},
dispose() {
if (disposed) return;
disposed = true;
listeners.clear();
current.queryInvalidation.dispose();
current.queryClient.clear();
},
});
}
function snapshot(
generation: number,
resources: ServerStateGenerationResources,
): ServerStateGenerationSnapshot {
return Object.freeze({ generation, ...resources });
}