feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
-45
View File
@@ -1,45 +0,0 @@
import { createApplication } from "../application/create-application.js";
/**
* This is the only module allowed to join concrete adapters to application
* ports. Boot phases are explicit so failures can stop before product mount.
*
* @template Config
* @template Release
* @template {Parameters<typeof createApplication>[0]} OutputPorts
* @template Infrastructure
* @param {{
* loadConfig(): Promise<Config>,
* loadRelease(config: Config): Promise<Release>,
* createAdapters(context: {
* config: Config,
* release: Release
* }): Promise<{
* outputPorts: OutputPorts,
* infrastructure: Infrastructure,
* featureInputs?: Readonly<Record<string, unknown>>
* }>
* }} factories
* @returns {Promise<Readonly<{
* config: Config,
* release: Release,
* infrastructure: Infrastructure,
* application: ReturnType<typeof createApplication>
* }>>}
*/
export async function createCompositionRoot(factories) {
const config = await factories.loadConfig();
const release = await factories.loadRelease(config);
const adapters = await factories.createAdapters({ config, release });
const application = createApplication(
adapters.outputPorts,
adapters.featureInputs,
);
return Object.freeze({
config,
release,
infrastructure: adapters.infrastructure,
application,
});
}
+51
View File
@@ -0,0 +1,51 @@
import { createApplication } from "../application/create-application.ts";
import type {
ApplicationApi,
ApplicationFeatureInputs,
} from "../application/ports/in/application-api.ts";
import type { ApplicationOutputPorts } from "../application/ports/out/application-output-ports.ts";
export type CompositionRoot<Config, Release, Infrastructure> = Readonly<{
config: Config;
release: Release;
infrastructure: Infrastructure;
application: ApplicationApi;
}>;
type AdapterBundle<Infrastructure> = Readonly<{
outputPorts: ApplicationOutputPorts;
infrastructure: Infrastructure;
featureInputs?: Readonly<Partial<ApplicationFeatureInputs>>;
}>;
type CompositionFactories<Config, Release, Infrastructure> = Readonly<{
loadConfig(): Promise<Config>;
loadRelease(config: Config): Promise<Release>;
createAdapters(context: Readonly<{
config: Config;
release: Release;
}>): Promise<AdapterBundle<Infrastructure>>;
}>;
/**
* This is the only module allowed to join concrete adapters to application
* ports. Boot phases are explicit so failures can stop before product mount.
*/
export async function createCompositionRoot<Config, Release, Infrastructure>(
factories: CompositionFactories<Config, Release, Infrastructure>,
): Promise<CompositionRoot<Config, Release, Infrastructure>> {
const config = await factories.loadConfig();
const release = await factories.loadRelease(config);
const adapters = await factories.createAdapters({ config, release });
const application = createApplication(
adapters.outputPorts,
adapters.featureInputs,
);
return Object.freeze({
config,
release,
infrastructure: adapters.infrastructure,
application,
});
}
@@ -1,32 +0,0 @@
import { createCompositionRoot } from "./composition-root.js";
import { loadReleaseManifest } from "./load-release-manifest.js";
import { loadRuntimeConfig } from "./load-runtime-config.js";
import { createRuntimeAdapters } from "./runtime-adapters.js";
/**
* @param {{
* fetcher?: typeof fetch,
* host?: Record<string, unknown>
* }} [dependencies]
*/
export function createRuntimeComposition(dependencies = {}) {
return createCompositionRoot({
loadConfig: () => loadRuntimeConfig({ fetcher: dependencies.fetcher }),
loadRelease: (runtime) =>
loadReleaseManifest(
/** @type {Awaited<ReturnType<typeof loadRuntimeConfig>>} */ (runtime),
{ fetcher: dependencies.fetcher },
),
createAdapters: ({ config: runtime, release }) =>
createRuntimeAdapters({
runtime:
/** @type {Awaited<ReturnType<typeof loadRuntimeConfig>>} */ (runtime),
release:
/** @type {Awaited<ReturnType<typeof loadReleaseManifest>>} */ (
release
),
fetcher: dependencies.fetcher,
host: dependencies.host,
}),
});
}
@@ -0,0 +1,30 @@
import { createCompositionRoot } from "./composition-root.ts";
import { loadReleaseManifest } from "./load-release-manifest.ts";
import { loadRuntimeConfig } from "./load-runtime-config.ts";
import { createRuntimeAdapters } from "./runtime-adapters.ts";
export type RuntimeCompositionDependencies = Readonly<{
fetcher?: typeof fetch;
host?: Record<string, unknown>;
}>;
export function createRuntimeComposition(
dependencies: RuntimeCompositionDependencies = {},
) {
return createCompositionRoot({
loadConfig: () => loadRuntimeConfig({ fetcher: dependencies.fetcher }),
loadRelease: (runtime) =>
loadReleaseManifest(runtime, { fetcher: dependencies.fetcher }),
createAdapters: ({ config: runtime, release }) =>
createRuntimeAdapters({
runtime,
release,
fetcher: dependencies.fetcher,
host: dependencies.host,
}),
});
}
export type RuntimeComposition = Awaited<
ReturnType<typeof createRuntimeComposition>
>;
@@ -1,24 +1,25 @@
import {
normalizeColorSchemePreference,
resolveColorScheme,
} from "../application/policies/color-scheme.js";
} from "../application/policies/color-scheme.ts";
import type { ApplicationApi } from "../application/ports/in/application-api.ts";
/**
* Applies the persisted public preference before React paints.
*
* @param {Pick<import("../application/ports/in/application-api.js").ApplicationApi["preferences"], "getColorScheme">} preferences
* @param {{
* documentElement?: HTMLElement,
* matchMedia?: (query: string) => MediaQueryList
* }} [browser]
*/
export function initializeColorScheme(preferences, browser = {}) {
type ColorSchemeBrowser = Readonly<{
documentElement?: HTMLElement;
matchMedia?: (query: string) => Pick<MediaQueryList, "matches">;
}>;
/** Applies the persisted public preference before React paints. */
export function initializeColorScheme(
preferences: Pick<ApplicationApi["preferences"], "getColorScheme">,
browser: ColorSchemeBrowser = {},
) {
const documentElement = browser.documentElement ?? document.documentElement;
const matchMedia =
browser.matchMedia ??
(typeof window.matchMedia === "function"
? window.matchMedia.bind(window)
: () => /** @type {MediaQueryList} */ ({ matches: false }));
: () => ({ matches: false }));
const preference = normalizeColorSchemePreference(
preferences.getColorScheme(),
);
@@ -1,5 +1,7 @@
import { z } from "zod";
import type { RuntimeConfigLoadResult } from "./load-runtime-config.ts";
const version = z.string().regex(/^\d+(?:\.\d+){0,2}$/);
export const releaseManifestSchema = z
.object({
@@ -16,19 +18,65 @@ export const releaseManifestSchema = z
})
.strict();
export type ReleaseManifest = z.output<typeof releaseManifestSchema>;
export type ReleaseManifestErrorCode =
| "MANIFEST_BUILD_MISMATCH"
| "MANIFEST_CONFIG_SCHEMA_MISMATCH"
| "MANIFEST_API_CONTRACT_MISMATCH"
| "MANIFEST_RELEASE_MISMATCH"
| "MANIFEST_ASSET_MISMATCH"
| "MANIFEST_FETCH_FAILED"
| "MANIFEST_HTTP_FAILED"
| "MANIFEST_JSON_INVALID"
| "MANIFEST_SCHEMA_INVALID";
export type ReleaseManifestFailureKind =
| "BUILD_MISMATCH"
| "CONFIG_MISMATCH"
| "API_CONTRACT_MISMATCH"
| "RELEASE_MISMATCH"
| "ASSET_MISMATCH"
| "RELEASE_MANIFEST_FAILURE";
export type ReleaseManifestSafe = Readonly<{
kind: ReleaseManifestFailureKind;
code: ReleaseManifestErrorCode;
buildId: string;
releaseId?: string;
supportReference: string;
}>;
type ReleaseManifestSafeInput = Readonly<{
buildId: string;
releaseId?: string;
}>;
function failureKindFor(
code: ReleaseManifestErrorCode,
): ReleaseManifestFailureKind {
switch (code) {
case "MANIFEST_BUILD_MISMATCH":
return "BUILD_MISMATCH";
case "MANIFEST_CONFIG_SCHEMA_MISMATCH":
return "CONFIG_MISMATCH";
case "MANIFEST_API_CONTRACT_MISMATCH":
return "API_CONTRACT_MISMATCH";
case "MANIFEST_RELEASE_MISMATCH":
return "RELEASE_MISMATCH";
case "MANIFEST_ASSET_MISMATCH":
return "ASSET_MISMATCH";
default:
return "RELEASE_MANIFEST_FAILURE";
}
}
export class ReleaseManifestError extends Error {
/** @param {string} code @param {{buildId: string, releaseId?: string}} safe */
constructor(code, safe) {
readonly kind: ReleaseManifestFailureKind;
readonly code: ReleaseManifestErrorCode;
readonly safe: ReleaseManifestSafe;
constructor(code: ReleaseManifestErrorCode, safe: ReleaseManifestSafeInput) {
super("Release manifest could not be loaded");
this.name = "ReleaseManifestError";
this.kind =
{
MANIFEST_BUILD_MISMATCH: "BUILD_MISMATCH",
MANIFEST_CONFIG_SCHEMA_MISMATCH: "CONFIG_MISMATCH",
MANIFEST_API_CONTRACT_MISMATCH: "API_CONTRACT_MISMATCH",
MANIFEST_RELEASE_MISMATCH: "RELEASE_MISMATCH",
MANIFEST_ASSET_MISMATCH: "ASSET_MISMATCH",
}[code] ?? "RELEASE_MANIFEST_FAILURE";
this.kind = failureKindFor(code);
this.code = code;
this.safe = Object.freeze({
kind: this.kind,
@@ -40,20 +88,22 @@ export class ReleaseManifestError extends Error {
}
}
export type FetchReleaseManifestOptions = Readonly<{
fetcher?: typeof fetch;
buildId: string;
releaseId?: string;
}>;
/**
* Fetches and validates the active manifest without imposing the current
* build tuple. Chunk recovery uses this no-store view to detect a new release.
*
* @param {string} url
* @param {{
* fetcher?: typeof fetch,
* buildId: string,
* releaseId?: string
* }} options
*/
export async function fetchReleaseManifest(url, options) {
export async function fetchReleaseManifest(
url: string,
options: FetchReleaseManifestOptions,
): Promise<Readonly<ReleaseManifest>> {
const fetcher = options.fetcher ?? fetch;
let response;
let response: Response;
try {
response = await fetcher(url, {
cache: "no-store",
@@ -65,7 +115,7 @@ export async function fetchReleaseManifest(url, options) {
if (!response.ok) {
throw new ReleaseManifestError("MANIFEST_HTTP_FAILED", options);
}
let raw;
let raw: unknown;
try {
raw = await response.json();
} catch {
@@ -78,11 +128,15 @@ export async function fetchReleaseManifest(url, options) {
return Object.freeze(structuredClone(parsed.data));
}
/**
* @param {Awaited<ReturnType<typeof import("./load-runtime-config.js").loadRuntimeConfig>>} runtime
* @param {{fetcher?: typeof fetch, expectedAssetManifestHash?: string}} [options]
*/
export async function loadReleaseManifest(runtime, options = {}) {
export type LoadReleaseManifestOptions = Readonly<{
fetcher?: typeof fetch;
expectedAssetManifestHash?: string;
}>;
export async function loadReleaseManifest(
runtime: RuntimeConfigLoadResult,
options: LoadReleaseManifestOptions = {},
): Promise<Readonly<ReleaseManifest>> {
const manifest = await fetchReleaseManifest(
runtime.config.RELEASE_MANIFEST_URL,
{
@@ -91,7 +145,7 @@ export async function loadReleaseManifest(runtime, options = {}) {
releaseId: runtime.config.RELEASE_ID,
},
);
let mismatchCode = null;
let mismatchCode: ReleaseManifestErrorCode | null = null;
if (manifest.buildId !== runtime.build.buildId) {
mismatchCode = "MANIFEST_BUILD_MISMATCH";
}
@@ -1,15 +1,32 @@
import { assertSafeConfigNames, getBuildConfig } from "../contracts/env.js";
import { validateRuntimeConfig } from "./runtime-config-schema.js";
import { assertSafeConfigNames, getBuildConfig } from "../contracts/env.ts";
import {
validateRuntimeConfig,
type RuntimeConfig,
} from "./runtime-config-schema.ts";
export type BootConfigSafe = Readonly<{
kind: "BOOT_CONFIG_FAILURE";
code: string;
buildId: string;
configSchemaVersion?: string;
releaseId?: string;
supportReference: string;
}>;
type BootConfigSafeInput = Readonly<{
buildId: string;
configSchemaVersion?: string;
releaseId?: string;
}>;
export class BootConfigError extends Error {
/**
* @param {string} code
* @param {{ buildId: string, configSchemaVersion?: string, releaseId?: string }} safe
*/
constructor(code, safe) {
readonly kind = "BOOT_CONFIG_FAILURE" as const;
readonly code: string;
readonly safe: BootConfigSafe;
constructor(code: string, safe: BootConfigSafeInput) {
super("Runtime configuration could not be loaded");
this.name = "BootConfigError";
this.kind = "BOOT_CONFIG_FAILURE";
this.code = code;
this.safe = Object.freeze({
kind: this.kind,
@@ -22,20 +39,31 @@ export class BootConfigError extends Error {
}
}
/**
* @param {{
* fetcher?: typeof fetch,
* buildConfig?: ReturnType<typeof getBuildConfig>,
* now?: () => number
* }} [options]
*/
export async function loadRuntimeConfig(options = {}) {
export type RuntimeConfigLoadOptions = Readonly<{
fetcher?: typeof fetch;
buildConfig?: ReturnType<typeof getBuildConfig>;
now?: () => number;
}>;
export type RuntimeConfigLoadResult = Readonly<{
config: RuntimeConfig;
build: ReturnType<typeof getBuildConfig>;
validationDurationMs: number;
}>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
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 startedAt = now();
let response;
let response: Response;
try {
response = await fetcher(buildConfig.runtimeConfigUrl, {
cache: "no-store",
@@ -53,7 +81,7 @@ export async function loadRuntimeConfig(options = {}) {
});
}
let rawConfig;
let rawConfig: unknown;
try {
rawConfig = await response.json();
} catch {
@@ -62,7 +90,7 @@ export async function loadRuntimeConfig(options = {}) {
});
}
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
if (!isRecord(rawConfig)) {
throw new BootConfigError("CONFIG_SHAPE_INVALID", {
buildId: buildConfig.buildId,
});
@@ -84,7 +112,10 @@ export async function loadRuntimeConfig(options = {}) {
typeof rawConfig.CONFIG_SCHEMA_VERSION === "string"
? rawConfig.CONFIG_SCHEMA_VERSION
: undefined,
releaseId: typeof rawConfig.RELEASE_ID === "string" ? rawConfig.RELEASE_ID : undefined,
releaseId:
typeof rawConfig.RELEASE_ID === "string"
? rawConfig.RELEASE_ID
: undefined,
});
}
@@ -1,13 +1,13 @@
import { createRoot } from "react-dom/client";
import { recordBootFailure } from "../adapters/diagnostics/bounded-diagnostics.js";
import { BootErrorShell } from "../presentation/boundaries/boot-error-shell.jsx";
import { createRuntimeComposition } from "./create-runtime-composition.js";
import { initializeColorScheme } from "./initialize-color-scheme.js";
import { BootConfigError } from "./load-runtime-config.js";
import { ReleaseManifestError } from "./load-release-manifest.js";
import { RuntimeApplication } from "./runtime-application.jsx";
import { recordBootFailure } from "../adapters/diagnostics/bounded-diagnostics.ts";
import { BootErrorShell } from "../presentation/boundaries/boot-error-shell.tsx";
import "../presentation/styles/theme.css";
import { createRuntimeComposition } from "./create-runtime-composition.ts";
import { initializeColorScheme } from "./initialize-color-scheme.ts";
import { ReleaseManifestError } from "./load-release-manifest.ts";
import { BootConfigError } from "./load-runtime-config.ts";
import { RuntimeApplication } from "./runtime-application.tsx";
const rootElement = document.getElementById("root");
@@ -17,14 +17,17 @@ if (!rootElement) {
const root = createRoot(rootElement);
async function boot() {
async function boot(): Promise<void> {
try {
const composition = await createRuntimeComposition();
document.documentElement.dataset.buildId = composition.release.buildId;
document.documentElement.dataset.releaseId = composition.release.releaseId;
initializeColorScheme(composition.application.preferences);
root.render(<RuntimeApplication composition={composition} />);
} catch (error) {
import.meta.hot?.dispose(() => {
composition.infrastructure.dispose();
});
} catch (error: unknown) {
const safe =
error instanceof BootConfigError || error instanceof ReleaseManifestError
? error.safe
-167
View File
@@ -1,167 +0,0 @@
import {
createDemoSessionAdapter,
createExternalAuthSessionAdapter,
createUnavailableSessionAdapter,
} from "../adapters/auth/external-session-adapter.js";
import { createHttpClient } from "../adapters/http/client.js";
import { createDiagnosticsAdapter } from "../adapters/diagnostics/bounded-diagnostics.js";
import {
createQueryClient,
} from "../adapters/query-cache/tanstack-query-cache.js";
import { createBrowserStorageAdapter } from "../adapters/storage/browser-storage-adapter.js";
import { createTelemetryAdapter } from "../adapters/telemetry/best-effort-telemetry.js";
import { fetchReleaseManifest } from "./load-release-manifest.js";
import { createInstalledFeatureInputs } from "../features/installed-feature-adapters.js";
/**
* @param {Record<string, unknown>} host
* @returns {Parameters<typeof createExternalAuthSessionAdapter>[0] | null}
*/
function externalOwnerFrom(host) {
const candidate = host.__CA_FRONTEND_AUTH_OWNER__;
if (!candidate || typeof candidate !== "object") return null;
const owner = /** @type {Record<string, unknown>} */ (candidate);
const required = [
"readState",
"subscribe",
"beginSignIn",
"signOut",
"attachCredential",
"recoverSession",
"notifyUnauthenticated",
];
return required.every((name) => typeof owner[name] === "function")
? /** @type {Parameters<typeof createExternalAuthSessionAdapter>[0]} */ (
candidate
)
: null;
}
/** @param {unknown} value */
function storageOrUndefined(value) {
return typeof Storage !== "undefined" && value instanceof Storage
? value
: undefined;
}
/**
* Runtime-aware transport factory. Feature gateway composition calls this
* factory when a registered API capability is installed.
*
* @param {{
* runtime: Awaited<ReturnType<typeof import("./load-runtime-config.js").loadRuntimeConfig>>,
* authSession: import("../application/ports/auth-session-port.js").AuthSessionPort,
* fetcher?: typeof fetch,
* clock?: import("../application/ports/clock-port.js").ClockPort,
* scheduler?: Parameters<typeof createHttpClient>[0]["scheduler"]
* diagnostics?: Parameters<typeof createHttpClient>[0]["diagnostics"],
* telemetry?: Parameters<typeof createHttpClient>[0]["telemetry"]
* }} context
*/
export function createRuntimeHttpClient(context, contract = {}) {
return createHttpClient({
baseUrl: context.runtime.config.API_BASE_URL,
timeoutMs: context.runtime.config.REQUEST_TIMEOUT_MS,
maxRetryAttempts: context.runtime.config.MAX_RETRY_ATTEMPTS,
authSession: context.authSession,
fetcher: context.fetcher,
clock: context.clock,
scheduler: context.scheduler,
diagnostics: context.diagnostics,
telemetry: context.telemetry,
...contract,
});
}
/**
* @param {{
* runtime: Awaited<ReturnType<typeof import("./load-runtime-config.js").loadRuntimeConfig>>,
* release: Awaited<ReturnType<typeof import("./load-release-manifest.js").loadReleaseManifest>>,
* host?: Record<string, unknown>,
* fetcher?: typeof fetch
* }} context
*/
export async function createRuntimeAdapters(context) {
const host = context.host ?? /** @type {Record<string, unknown>} */ (globalThis);
const config = context.runtime.config;
const externalOwner = externalOwnerFrom(host);
const authSession =
config.AUTH_MODE === "demo"
? createDemoSessionAdapter()
: externalOwner
? createExternalAuthSessionAdapter(externalOwner)
: createUnavailableSessionAdapter();
const diagnostics = createDiagnosticsAdapter();
const telemetry = createTelemetryAdapter({
enabled: config.TELEMETRY_ENABLED,
endpoint: config.TELEMETRY_ENDPOINT,
fetcher: context.fetcher,
onDrop(event) {
const attributes =
event.attributes && typeof event.attributes === "object"
? event.attributes
: {};
diagnostics.record({
level: "warn",
eventId: "telemetry.delivery.dropped",
context: /** @type {Record<string, unknown>} */ (attributes),
});
},
});
const queryClient = createQueryClient({ diagnostics });
const storage = createBrowserStorageAdapter({
localStorage: storageOrUndefined(host.localStorage),
sessionStorage: storageOrUndefined(host.sessionStorage),
diagnostics,
});
const releaseInfo = Object.freeze({
async getCurrent() {
return structuredClone(context.release);
},
async refresh() {
return fetchReleaseManifest(config.RELEASE_MANIFEST_URL, {
fetcher: context.fetcher,
buildId: context.release.buildId,
releaseId: context.release.releaseId,
});
},
});
const navigation = Object.freeze({
reload() {
const location =
/** @type {{reload?: () => void} | undefined} */ (host.location);
if (typeof location?.reload !== "function") {
throw new Error("Browser reload is unavailable");
}
location.reload();
},
});
const featureInputs = createInstalledFeatureInputs({
createHttpClient: (contract) =>
createRuntimeHttpClient(
{
runtime: context.runtime,
authSession,
fetcher: context.fetcher,
diagnostics,
telemetry,
},
contract,
),
});
return Object.freeze({
outputPorts: Object.freeze({
session: authSession,
preferences: storage,
diagnostics,
telemetry,
releaseInfo,
navigation,
}),
infrastructure: Object.freeze({
queryClient,
}),
featureInputs,
});
}
+268
View File
@@ -0,0 +1,268 @@
import {
createDemoSessionAdapter,
createExternalAuthSessionAdapter,
createUnavailableSessionAdapter,
type ExternalSessionOwner,
} from "../adapters/auth/external-session-adapter.ts";
import { createDiagnosticsAdapter } from "../adapters/diagnostics/bounded-diagnostics.ts";
import { createHttpClient } from "../adapters/http/client.ts";
import { createBrowserCrossContextInvalidationFromHost } from "../adapters/cross-context-invalidation/index.ts";
import { createTanStackCacheCoordinator } 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 { 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 {
fetchReleaseManifest,
type ReleaseManifest,
} from "./load-release-manifest.ts";
import type { RuntimeConfigLoadResult } from "./load-runtime-config.ts";
type HttpClientDependencies = Parameters<typeof createHttpClient>[0];
export type RuntimeHttpContract = Pick<
HttpClientDependencies,
"getOperation" | "validatePayload" | "validateRequest" | "mapPayload"
>;
type RuntimeHttpContext = Readonly<{
runtime: RuntimeConfigLoadResult;
authSession: AuthSessionPort;
fetcher?: typeof fetch;
clock?: ClockPort;
scheduler?: HttpClientDependencies["scheduler"];
diagnostics?: HttpClientDependencies["diagnostics"];
telemetry?: HttpClientDependencies["telemetry"];
}>;
export type RuntimeAdaptersContext = Readonly<{
runtime: RuntimeConfigLoadResult;
release: Readonly<ReleaseManifest>;
host?: Record<string, unknown>;
fetcher?: typeof fetch;
}>;
const EXTERNAL_OWNER_METHODS = Object.freeze([
"readState",
"subscribe",
"beginSignIn",
"signOut",
"attachCredential",
"recoverSession",
"notifyUnauthenticated",
] as const);
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object";
}
function externalOwnerFrom(
host: Record<string, unknown>,
): ExternalSessionOwner | null {
const candidate = host.__CA_FRONTEND_AUTH_OWNER__;
if (!isRecord(candidate)) return null;
return EXTERNAL_OWNER_METHODS.every(
(name) => typeof candidate[name] === "function",
)
? (candidate as ExternalSessionOwner)
: null;
}
function hostValue(
host: Record<string, unknown>,
property: string,
): unknown {
try {
return Reflect.get(host, property);
} catch {
return undefined;
}
}
function storageOrUndefined(value: unknown): Storage | undefined {
if (!value || typeof value !== "object") return undefined;
const candidate = value as Record<string, unknown>;
try {
return ["getItem", "setItem", "removeItem"].every(
(method) => typeof Reflect.get(candidate, method) === "function",
)
? (value as Storage)
: undefined;
} catch {
return undefined;
}
}
/**
* Runtime-aware transport factory. Feature gateway composition calls this
* factory when a registered API capability is installed.
*/
export function createRuntimeHttpClient(
context: RuntimeHttpContext,
contract: RuntimeHttpContract = {},
) {
return createHttpClient({
baseUrl: context.runtime.config.API_BASE_URL,
timeoutMs: context.runtime.config.REQUEST_TIMEOUT_MS,
maxRetryAttempts: context.runtime.config.MAX_RETRY_ATTEMPTS,
authSession: context.authSession,
providerProfile: createRestProviderProfile(
"PRIMARY_API",
context.runtime.config.API_BASE_URL,
["omit"],
),
fetcher: context.fetcher,
clock: context.clock,
scheduler: context.scheduler,
diagnostics: context.diagnostics,
telemetry: context.telemetry,
...contract,
});
}
export async function createRuntimeAdapters(
context: RuntimeAdaptersContext,
) {
const host =
context.host ?? (globalThis as unknown as Record<string, unknown>);
const config = context.runtime.config;
const externalOwner = externalOwnerFrom(host);
const authSession =
config.AUTH_MODE === "demo"
? createDemoSessionAdapter()
: externalOwner
? createExternalAuthSessionAdapter(externalOwner)
: createUnavailableSessionAdapter();
const diagnostics = createDiagnosticsAdapter();
const telemetry = createTelemetryAdapter({
enabled: config.TELEMETRY_ENABLED,
endpoint: config.TELEMETRY_ENDPOINT,
fetcher: context.fetcher,
onDrop(event) {
const attributes =
event.attributes && typeof event.attributes === "object"
? event.attributes
: {};
diagnostics.record({
level: "warn",
eventId: "telemetry.delivery.dropped",
context: attributes,
});
},
});
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,
},
});
},
});
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();
});
const storage = createBrowserStorageAdapter({
localStorage: storageOrUndefined(hostValue(host, "localStorage")),
sessionStorage: storageOrUndefined(
hostValue(host, "sessionStorage"),
),
diagnostics,
});
const releaseInfo = Object.freeze({
async getCurrent() {
return structuredClone(context.release);
},
async refresh() {
return fetchReleaseManifest(config.RELEASE_MANIFEST_URL, {
fetcher: context.fetcher,
buildId: context.release.buildId,
releaseId: context.release.releaseId,
});
},
});
const navigation = Object.freeze({
reload() {
const location = host.location;
if (!isRecord(location) || typeof location.reload !== "function") {
throw new Error("Browser reload is unavailable");
}
location.reload();
},
});
const featureInputs = createInstalledFeatureInputs({
createHttpClient: (contract) =>
createRuntimeHttpClient(
{
runtime: context.runtime,
authSession,
fetcher: context.fetcher,
diagnostics,
telemetry,
},
contract,
),
});
return Object.freeze({
outputPorts: Object.freeze({
session: authSession,
preferences: storage,
diagnostics,
telemetry,
releaseInfo,
navigation,
}),
infrastructure: Object.freeze({
queryClient,
queryInvalidation,
serverStateScope,
conditionalValidators,
crossContextInvalidationStatus: () =>
crossContextInvalidation?.getStatus() ?? "DEGRADED_LOCAL_ONLY",
dispose() {
unsubscribeConditionalScope();
conditionalValidators.clear();
serverStateScope.dispose();
queryInvalidation.dispose();
},
}),
featureInputs,
});
}
-28
View File
@@ -1,28 +0,0 @@
import { StrictMode } from "react";
import { QueryClientProvider } from "@tanstack/react-query";
import { ApplicationProvider } from "../presentation/providers/application-provider.js";
import { AppRouter } from "../presentation/routes/app-router.jsx";
/**
* Production provider tree. Tests import this component so the validated
* composition is proven against the same provider order used by main.
*
* @param {{
* composition: Awaited<ReturnType<typeof import("./create-runtime-composition.js").createRuntimeComposition>>
* }} props
*/
export function RuntimeApplication({ composition }) {
return (
<StrictMode>
<QueryClientProvider client={composition.infrastructure.queryClient}>
<ApplicationProvider application={composition.application}>
<AppRouter
basename={composition.config.build.routerBasePath}
buildId={composition.release.buildId}
/>
</ApplicationProvider>
</QueryClientProvider>
</StrictMode>
);
}
+37
View File
@@ -0,0 +1,37 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } 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 { AppRouter } from "../presentation/routes/app-router.tsx";
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.
*/
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>
</StrictMode>
);
}
@@ -35,11 +35,10 @@ export const runtimeConfigSchema = z
message: "demo authentication is limited to local environments",
});
}
const endpointEntries =
/** @type {Array<[string, string | undefined]>} */ ([
["API_BASE_URL", config.API_BASE_URL],
["TELEMETRY_ENDPOINT", config.TELEMETRY_ENDPOINT],
]);
const endpointEntries = [
["API_BASE_URL", config.API_BASE_URL],
["TELEMETRY_ENDPOINT", config.TELEMETRY_ENDPOINT],
] as const;
for (const [key, value] of endpointEntries) {
if (value && !local && new URL(value).protocol !== "https:") {
@@ -52,13 +51,20 @@ export const runtimeConfigSchema = z
}
});
/** @param {unknown} value */
export function validateRuntimeConfig(value) {
export type RuntimeConfig = z.output<typeof runtimeConfigSchema>;
export type RuntimeConfigValidation =
| Readonly<{ success: true; data: RuntimeConfig }>
| Readonly<{
success: false;
issues: readonly Readonly<{ path: string; code: string }>[];
}>;
export function validateRuntimeConfig(value: unknown): RuntimeConfigValidation {
const result = runtimeConfigSchema.safeParse(value);
if (!result.success) {
return {
success: /** @type {false} */ (false),
success: false,
issues: result.error.issues.map((issue) => ({
path: issue.path.join("."),
code: issue.code,
@@ -67,7 +73,7 @@ export function validateRuntimeConfig(value) {
}
return {
success: /** @type {true} */ (true),
success: true,
data: structuredClone(result.data),
};
}