UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

323 lines (280 loc) 9.07 kB
import type { EventPayloadDef, ListenerDef } from "../events/index.js"; import type { JobDef } from "../jobs/index.js"; import type { OutboxRegistry } from "../outbox/index.js"; import type { ScheduleDef } from "../schedules/index.js"; import type { TaskDef } from "../tasks/index.js"; export type RuntimeIntegrityArtifact = | "listener" | "schedule" | "task" | "outboxEvent" | "outboxJob"; export type RuntimeIntegrityFindingCode = | "missing_listener_registration" | "missing_schedule_registration" | "missing_task_registration" | "missing_outbox_event_registration" | "missing_outbox_job_registration" | "duplicate_runtime_manifest_entry" | "duplicate_runtime_registry_entry"; export type RuntimeIntegrityFindingLocation = "manifest" | "registry"; export interface RuntimeIntegrityFinding { readonly code: RuntimeIntegrityFindingCode; readonly artifact: RuntimeIntegrityArtifact; readonly name: string; readonly message: string; readonly location?: RuntimeIntegrityFindingLocation; } export interface RuntimeIntegrityResult { readonly ok: boolean; readonly findings: readonly RuntimeIntegrityFinding[]; } export interface RuntimeOutboxManifest { readonly events?: readonly EventPayloadDef[]; readonly jobs?: readonly JobDef[]; } export interface RuntimeManifestInput { readonly listeners?: readonly ListenerDef[]; readonly schedules?: readonly ScheduleDef[]; readonly tasks?: readonly TaskDef[]; readonly outbox?: RuntimeOutboxManifest; } export interface RuntimeManifest { readonly listeners: readonly ListenerDef[]; readonly schedules: readonly ScheduleDef[]; readonly tasks: readonly TaskDef[]; readonly outbox: { readonly events: readonly EventPayloadDef[]; readonly jobs: readonly JobDef[]; }; } export interface RuntimeRegistriesInput { readonly listeners?: readonly ListenerDef[]; readonly schedules?: readonly ScheduleDef[]; readonly tasks?: readonly TaskDef[]; readonly outbox?: OutboxRegistry; } export interface RuntimeRegistries { readonly listeners: readonly ListenerDef[]; readonly schedules: readonly ScheduleDef[]; readonly tasks: readonly TaskDef[]; readonly outbox?: OutboxRegistry; } export type RuntimeIntegrityMode = "error" | "warn"; export interface RuntimeIntegrityLogger { warn(message: string): void; } export interface CreateRuntimeIntegrityOptions { readonly manifest: RuntimeManifest; readonly registries: RuntimeRegistries; /** * How to handle integrity findings. * * @default "error" */ readonly mode?: RuntimeIntegrityMode; /** * Logger used when `mode` is `"warn"`. * * @default console */ readonly logger?: RuntimeIntegrityLogger; } export interface RuntimeIntegrityCheck { readonly kind: "runtime-integrity"; check(): RuntimeIntegrityResult; } type NamedDefinition = { readonly name: string; }; type IntegrityArtifactConfig = { readonly artifact: RuntimeIntegrityArtifact; readonly label: string; readonly manifestNames: readonly string[]; readonly registryNames: readonly string[]; readonly missingCode: RuntimeIntegrityFindingCode; readonly registryDescription: string; }; export class RuntimeIntegrityError extends Error { readonly findings: readonly RuntimeIntegrityFinding[]; constructor(findings: readonly RuntimeIntegrityFinding[]) { super(formatRuntimeIntegrityFailure(findings)); this.name = "RuntimeIntegrityError"; this.findings = findings; } } export function defineRuntimeManifest( input: RuntimeManifestInput, ): RuntimeManifest { return { listeners: input.listeners ?? [], schedules: input.schedules ?? [], tasks: input.tasks ?? [], outbox: { events: input.outbox?.events ?? [], jobs: input.outbox?.jobs ?? [], }, }; } export function defineRuntimeRegistries( input: RuntimeRegistriesInput, ): RuntimeRegistries { return { listeners: input.listeners ?? [], schedules: input.schedules ?? [], tasks: input.tasks ?? [], outbox: input.outbox, }; } export function inspectRuntimeIntegrity( manifest: RuntimeManifest, registries: RuntimeRegistries, ): RuntimeIntegrityResult { const configs: readonly IntegrityArtifactConfig[] = [ { artifact: "listener", label: "listener", manifestNames: namesFromDefinitions(manifest.listeners), registryNames: namesFromDefinitions(registries.listeners), missingCode: "missing_listener_registration", registryDescription: "runtime registries.listeners", }, { artifact: "schedule", label: "schedule", manifestNames: namesFromDefinitions(manifest.schedules), registryNames: namesFromDefinitions(registries.schedules), missingCode: "missing_schedule_registration", registryDescription: "runtime registries.schedules", }, { artifact: "task", label: "task", manifestNames: namesFromDefinitions(manifest.tasks), registryNames: namesFromDefinitions(registries.tasks), missingCode: "missing_task_registration", registryDescription: "runtime registries.tasks", }, { artifact: "outboxEvent", label: "outbox event", manifestNames: namesFromDefinitions(manifest.outbox.events), registryNames: namesFromMap(registries.outbox?.events), missingCode: "missing_outbox_event_registration", registryDescription: "runtime registries.outbox.events", }, { artifact: "outboxJob", label: "outbox job", manifestNames: namesFromDefinitions(manifest.outbox.jobs), registryNames: namesFromMap(registries.outbox?.jobs), missingCode: "missing_outbox_job_registration", registryDescription: "runtime registries.outbox.jobs", }, ]; const findings = configs.flatMap((config) => inspectArtifactIntegrity(config), ); return { ok: findings.length === 0, findings, }; } export function createRuntimeIntegrity( options: CreateRuntimeIntegrityOptions, ): RuntimeIntegrityCheck { const mode = options.mode ?? "error"; const logger = options.logger ?? console; return { kind: "runtime-integrity", check() { const result = inspectRuntimeIntegrity( options.manifest, options.registries, ); if (result.ok) return result; if (mode === "warn") { logger.warn( `[beignet] ${formatRuntimeIntegrityFailure(result.findings)}`, ); return result; } throw new RuntimeIntegrityError(result.findings); }, }; } export function runRuntimeIntegrityCheck( check: RuntimeIntegrityCheck | undefined, ): RuntimeIntegrityResult | undefined { return check?.check(); } function inspectArtifactIntegrity( config: IntegrityArtifactConfig, ): RuntimeIntegrityFinding[] { const findings: RuntimeIntegrityFinding[] = []; const manifestDuplicates = duplicateNames(config.manifestNames); const registryDuplicates = duplicateNames(config.registryNames); for (const name of manifestDuplicates) { findings.push({ code: "duplicate_runtime_manifest_entry", artifact: config.artifact, name, location: "manifest", message: `Duplicate ${config.label} "${name}" in runtime manifest.`, }); } for (const name of registryDuplicates) { findings.push({ code: "duplicate_runtime_registry_entry", artifact: config.artifact, name, location: "registry", message: `Duplicate ${config.label} "${name}" in ${config.registryDescription}.`, }); } const registryNameSet = new Set(config.registryNames); for (const name of uniqueNames(config.manifestNames)) { if (registryNameSet.has(name)) continue; findings.push({ code: config.missingCode, artifact: config.artifact, name, message: `Missing ${config.label} registration "${name}": declared in runtime manifest but not present in ${config.registryDescription}.`, }); } return findings; } function namesFromDefinitions( definitions: readonly NamedDefinition[] | undefined, ): string[] { return (definitions ?? []).map((definition) => definition.name); } function namesFromMap<T extends NamedDefinition>( definitions: ReadonlyMap<string, T> | undefined, ): string[] { return [...(definitions?.keys() ?? [])]; } function duplicateNames(names: readonly string[]): string[] { const seen = new Set<string>(); const duplicates = new Set<string>(); for (const name of names) { if (seen.has(name)) { duplicates.add(name); continue; } seen.add(name); } return [...duplicates].sort(); } function uniqueNames(names: readonly string[]): string[] { return [...new Set(names)].sort(); } function formatRuntimeIntegrityFailure( findings: readonly RuntimeIntegrityFinding[], ): string { const summary = `Runtime integrity check failed with ${findings.length} finding${findings.length === 1 ? "" : "s"}.`; const details = findings .map((finding) => `- [${finding.code}] ${finding.message}`) .join("\n"); return `${summary}\n${details}`; }