import { createDemoSessionAdapter, createExternalAuthSessionAdapter, createUnavailableSessionAdapter, } from "../adapters/auth/external-session-adapter.js"; import { createHttpClient } from "../adapters/http/client.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} host * @returns {Parameters[0] | null} */ function externalOwnerFrom(host) { const candidate = host.__CA_FRONTEND_AUTH_OWNER__; if (!candidate || typeof candidate !== "object") return null; const owner = /** @type {Record} */ (candidate); const required = [ "readState", "subscribe", "beginSignIn", "signOut", "attachCredential", "recoverSession", "notifyUnauthenticated", ]; return required.every((name) => typeof owner[name] === "function") ? /** @type {Parameters[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>, * authSession: import("../application/ports/auth-session-port.js").AuthSessionPort, * fetcher?: typeof fetch, * clock?: import("../application/ports/clock-port.js").ClockPort, * scheduler?: Parameters[0]["scheduler"] * }} 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, ...contract, }); } /** * @param {{ * runtime: Awaited>, * release: Awaited>, * host?: Record, * fetcher?: typeof fetch * }} context */ export async function createRuntimeAdapters(context) { const host = context.host ?? /** @type {Record} */ (globalThis); const config = context.runtime.config; const externalOwner = externalOwnerFrom(host); const authSession = config.AUTH_MODE === "demo" ? createDemoSessionAdapter() : externalOwner ? createExternalAuthSessionAdapter(externalOwner) : createUnavailableSessionAdapter(); const queryClient = createQueryClient(); const storage = createBrowserStorageAdapter({ localStorage: storageOrUndefined(host.localStorage), sessionStorage: storageOrUndefined(host.sessionStorage), }); const telemetry = createTelemetryAdapter({ enabled: config.TELEMETRY_ENABLED, endpoint: config.TELEMETRY_ENDPOINT, fetcher: context.fetcher, }); 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, }, contract, ), }); return Object.freeze({ outputPorts: Object.freeze({ session: authSession, preferences: storage, diagnostics: telemetry, releaseInfo, navigation, }), infrastructure: Object.freeze({ queryClient, }), featureInputs, }); }