feat: 기능 추가 과정중
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user