@beignet/core
Version:
Core framework primitives for Beignet
323 lines (280 loc) • 9.07 kB
text/typescript
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}`;
}