feat: connect application input and output boundaries
This commit is contained in:
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Application facade factory. Concrete dependencies are supplied by bootstrap.
|
||||
*
|
||||
* @param {{
|
||||
* resources?: {
|
||||
* query: import("./ports/resource-ports.js").ResourceQueryPort<unknown, unknown>,
|
||||
* command: import("./ports/resource-ports.js").ResourceCommandPort<unknown, unknown>
|
||||
* },
|
||||
* cache: import("./ports/query-cache-port.js").QueryCachePort,
|
||||
* storage: import("./ports/storage-port.js").StoragePort,
|
||||
* telemetry: import("./ports/telemetry-port.js").TelemetryPort
|
||||
* }} ports
|
||||
*/
|
||||
export function createApplication(ports) {
|
||||
/**
|
||||
* @param {unknown} query
|
||||
* @param {import("./ports/resource-ports.js").RequestContext} [context]
|
||||
*/
|
||||
function queryResources(query, context) {
|
||||
return /** @type {NonNullable<typeof ports.resources>} */ (
|
||||
ports.resources
|
||||
).query.execute(query, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} command
|
||||
* @param {import("./ports/resource-ports.js").RequestContext} [context]
|
||||
*/
|
||||
function commandResources(command, context) {
|
||||
return /** @type {NonNullable<typeof ports.resources>} */ (
|
||||
ports.resources
|
||||
).command.execute(command, context);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
resources: ports.resources
|
||||
? Object.freeze({
|
||||
query: queryResources,
|
||||
command: commandResources,
|
||||
})
|
||||
: null,
|
||||
cache: ports.cache,
|
||||
storage: ports.storage,
|
||||
telemetry: ports.telemetry,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { normalizeColorSchemePreference } from "./policies/color-scheme.js";
|
||||
import type {
|
||||
ApplicationApi,
|
||||
ColorSchemePreference,
|
||||
RenderFailureReport,
|
||||
} from "./ports/in/application-api.js";
|
||||
import type { ApplicationOutputPorts } from "./ports/out/application-output-ports.js";
|
||||
|
||||
export type { ApplicationApi, ApplicationOutputPorts };
|
||||
|
||||
/**
|
||||
* Builds the driving API consumed by inbound adapters. Concrete output ports
|
||||
* remain inside these closures and are never returned to React.
|
||||
*/
|
||||
export function createApplication(
|
||||
outputPorts: ApplicationOutputPorts,
|
||||
): ApplicationApi {
|
||||
const session = Object.freeze({
|
||||
getSnapshot: () => outputPorts.session.getState(),
|
||||
subscribe: (listener: () => void) =>
|
||||
outputPorts.session.subscribe(listener),
|
||||
beginSignIn: (returnTo?: string) =>
|
||||
outputPorts.session.beginSignIn(returnTo),
|
||||
signOut: () => outputPorts.session.signOut(),
|
||||
recover: () => outputPorts.session.recover(),
|
||||
});
|
||||
|
||||
const preferences = Object.freeze({
|
||||
getColorScheme(): ColorSchemePreference {
|
||||
const result = outputPorts.preferences.read("COLOR_SCHEME");
|
||||
return normalizeColorSchemePreference(
|
||||
result.ok ? result.value : undefined,
|
||||
);
|
||||
},
|
||||
setColorScheme(preference: ColorSchemePreference) {
|
||||
const normalized = normalizeColorSchemePreference(preference);
|
||||
return outputPorts.preferences.write("COLOR_SCHEME", normalized);
|
||||
},
|
||||
});
|
||||
|
||||
const diagnostics = Object.freeze({
|
||||
reportRenderFailure(report: RenderFailureReport) {
|
||||
try {
|
||||
outputPorts.diagnostics.emit("ui.render.failed", {
|
||||
route_id: report.routeId,
|
||||
build_id: report.buildId,
|
||||
component_boundary: report.boundaryName,
|
||||
});
|
||||
} catch {
|
||||
// Diagnostics are best-effort and cannot become an application failure.
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const runtime = Object.freeze({
|
||||
async getReleaseSummary() {
|
||||
const release = await outputPorts.releaseInfo.getCurrent();
|
||||
return Object.freeze({
|
||||
buildId: release.buildId,
|
||||
releaseId: release.releaseId,
|
||||
configSchemaVersion: release.configSchemaVersion,
|
||||
apiContractVersion: release.apiContractVersion,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
session,
|
||||
preferences,
|
||||
diagnostics,
|
||||
runtime,
|
||||
});
|
||||
}
|
||||
@@ -10,10 +10,24 @@
|
||||
* subscribe(listener: () => void): () => void,
|
||||
* beginSignIn(returnTo?: string): Promise<void>,
|
||||
* signOut(): Promise<void>,
|
||||
* recover(): Promise<"restored" | "no-session">
|
||||
* }} SessionGateway
|
||||
*/
|
||||
|
||||
/**
|
||||
* Credential attachment is an HTTP-adapter collaboration, not an application
|
||||
* input capability.
|
||||
*
|
||||
* @typedef {{
|
||||
* attach(request: Request): Promise<Request>,
|
||||
* recover(): Promise<"restored" | "no-session">,
|
||||
* onUnauthenticated(): void
|
||||
* }} AuthSessionPort
|
||||
* }} CredentialAttacher
|
||||
*/
|
||||
|
||||
/**
|
||||
* External auth adapters implement both segregated capabilities.
|
||||
*
|
||||
* @typedef {SessionGateway & CredentialAttacher} AuthSessionPort
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
||||
@@ -5,25 +5,4 @@
|
||||
* }} ClockPort
|
||||
*/
|
||||
|
||||
/** @type {ClockPort} */
|
||||
export const systemClock = Object.freeze({
|
||||
now: () => Date.now(),
|
||||
sleep(milliseconds, signal) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (signal?.aborted) {
|
||||
reject(signal.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(resolve, milliseconds);
|
||||
signal?.addEventListener(
|
||||
"abort",
|
||||
() => {
|
||||
clearTimeout(timer);
|
||||
reject(signal.reason);
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
export {};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { SessionState } from "../auth-session-port.js";
|
||||
import type { StoragePort } from "../storage-port.js";
|
||||
|
||||
export type { SessionState } from "../auth-session-port.js";
|
||||
|
||||
export type ColorSchemePreference = "system" | "light" | "dark";
|
||||
|
||||
export type RenderFailureReport = Readonly<{
|
||||
routeId: string;
|
||||
buildId: string;
|
||||
boundaryName: "route" | "feature";
|
||||
}>;
|
||||
|
||||
export type ReleaseSummary = Readonly<{
|
||||
buildId: string;
|
||||
releaseId: string;
|
||||
configSchemaVersion: string;
|
||||
apiContractVersion: string;
|
||||
}>;
|
||||
|
||||
export type ApplicationApi = Readonly<{
|
||||
session: Readonly<{
|
||||
getSnapshot(): SessionState;
|
||||
subscribe(listener: () => void): () => void;
|
||||
beginSignIn(returnTo?: string): Promise<void>;
|
||||
signOut(): Promise<void>;
|
||||
recover(): Promise<"restored" | "no-session">;
|
||||
}>;
|
||||
preferences: Readonly<{
|
||||
getColorScheme(): ColorSchemePreference;
|
||||
setColorScheme(
|
||||
preference: ColorSchemePreference,
|
||||
): ReturnType<StoragePort["write"]>;
|
||||
}>;
|
||||
diagnostics: Readonly<{
|
||||
reportRenderFailure(report: RenderFailureReport): void;
|
||||
}>;
|
||||
runtime: Readonly<{
|
||||
getReleaseSummary(): Promise<ReleaseSummary>;
|
||||
}>;
|
||||
}>;
|
||||
@@ -0,0 +1,7 @@
|
||||
export type {
|
||||
ApplicationApi,
|
||||
ColorSchemePreference,
|
||||
ReleaseSummary,
|
||||
RenderFailureReport,
|
||||
SessionState,
|
||||
} from "./application-api.js";
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AuthSessionPort } from "../auth-session-port.js";
|
||||
import type { ReleaseInfoPort } from "../release-info-port.js";
|
||||
import type { StoragePort } from "../storage-port.js";
|
||||
import type { TelemetryPort } from "../telemetry-port.js";
|
||||
|
||||
/**
|
||||
* Capabilities required by application use cases. Implementations live in
|
||||
* outbound adapters and are selected only by bootstrap.
|
||||
*/
|
||||
export type ApplicationOutputPorts = Readonly<{
|
||||
session: Pick<
|
||||
AuthSessionPort,
|
||||
"getState" | "subscribe" | "beginSignIn" | "signOut" | "recover"
|
||||
>;
|
||||
preferences: StoragePort;
|
||||
diagnostics: TelemetryPort;
|
||||
releaseInfo: ReleaseInfoPort;
|
||||
}>;
|
||||
@@ -0,0 +1,17 @@
|
||||
export type { ApplicationOutputPorts } from "./application-output-ports.js";
|
||||
export type {
|
||||
AuthSessionPort,
|
||||
CredentialAttacher,
|
||||
SessionGateway,
|
||||
} from "../auth-session-port.js";
|
||||
export type { ClockPort } from "../clock-port.js";
|
||||
export type { QueryCachePort } from "../query-cache-port.js";
|
||||
export type { ReleaseInfoPort } from "../release-info-port.js";
|
||||
export type {
|
||||
RequestContext,
|
||||
ResourceCommandPort,
|
||||
ResourceQueryPort,
|
||||
Result,
|
||||
} from "../resource-ports.js";
|
||||
export type { StoragePort } from "../storage-port.js";
|
||||
export type { TelemetryPort } from "../telemetry-port.js";
|
||||
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* @typedef {{
|
||||
* getCurrent(): Promise<{
|
||||
* schemaVersion?: number,
|
||||
* appVersion?: string,
|
||||
* buildId: string,
|
||||
* commitSha?: string,
|
||||
* configSchemaVersion: string,
|
||||
* apiContractVersion: string,
|
||||
* assetManifestHash: string,
|
||||
* releaseId: string
|
||||
* releaseId: string,
|
||||
* builtAt?: string
|
||||
* }>
|
||||
* }} ReleaseInfoPort
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user