import type { CapabilityId, ModuleId } from "./identifiers.js"; import { moduleIdSchema } from "./identifiers.js"; import type { ModuleRuntimeServices } from "./module-database.js"; import type { ModuleHttpRouteRegistrar } from "./module-http.js"; import { semanticVersionSchema } from "./versions.js"; export type Awaitable = Value | PromiseLike; export interface RegisteredCapability { readonly id: CapabilityId; readonly implementation: unknown; readonly moduleId: ModuleId; readonly version: string; } export interface CapabilityResolver { resolve(capabilityId: CapabilityId): RegisteredCapability | undefined; } export interface CapabilityRegistrar extends CapabilityResolver { provide(capabilityId: CapabilityId, implementation: unknown): void; } export interface ModuleRegistrationContext { readonly capabilities: CapabilityRegistrar; /** Available only during `register` — never on `ModuleRuntimeContext` — so a route can only ever * be declared while Core is still building the immutable startup snapshot, never added or removed * afterward from `start`, `stop`, or any other post-registration code path. */ readonly http: ModuleHttpRouteRegistrar; readonly moduleId: ModuleId; } /** The same context shape for both `start` and `stop`: whichever hook runs, it receives the exact * same frozen `services` object identity as the other, for a given API runtime — never a fresh * `ModuleRuntimeServices` built per hook invocation. Unlike `ModuleRegistrationContext`, this never * carries `http`: a route can only ever be declared during `register`. */ export interface ModuleRuntimeContext { readonly capabilities: CapabilityResolver; readonly moduleId: ModuleId; readonly services: ModuleRuntimeServices; } export interface ModuleRuntimeDefinition { readonly id: ModuleId; readonly register?: (context: ModuleRegistrationContext) => Awaitable; readonly start?: (context: ModuleRuntimeContext) => Awaitable; readonly stop?: (context: ModuleRuntimeContext) => Awaitable; readonly version: string; } const runtimeDefinitionKeys = new Set(["id", "register", "start", "stop", "version"]); function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } export function isModuleRuntimeDefinition(value: unknown): value is ModuleRuntimeDefinition { if ( !isRecord(value) || !moduleIdSchema.safeParse(value.id).success || !semanticVersionSchema.safeParse(value.version).success ) { return false; } if (Object.keys(value).some((key) => !runtimeDefinitionKeys.has(key))) { return false; } return [value.register, value.start, value.stop].every( (hook) => hook === undefined || typeof hook === "function", ); } export function parseModuleRuntimeDefinition(value: unknown): ModuleRuntimeDefinition { if (!isModuleRuntimeDefinition(value)) { throw new TypeError("Invalid Ekklesia module runtime definition."); } return Object.freeze({ id: value.id, version: value.version, ...(value.register ? { register: value.register } : {}), ...(value.start ? { start: value.start } : {}), ...(value.stop ? { stop: value.stop } : {}), }); }