UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

144 lines 4.44 kB
import { runWithResolvedTracingContext } from "../tracing/execution.js"; import { parseTraceCarrier, } from "../tracing/index.js"; /** * Error thrown when event payload validation fails. */ export class EventValidationError extends Error { /** * Raw Standard Schema validation issues. */ issues; constructor(args) { super(`Event "${args.name}" payload validation failed: ${formatIssues(args.issues)}`); this.name = "EventValidationError"; this.issues = args.issues; } } function formatPath(path) { if (!path?.length) return ""; return path .map((segment) => typeof segment === "object" && segment !== null && "key" in segment ? String(segment.key) : String(segment)) .join("."); } function formatIssues(issues) { return issues .map((issue) => { const path = formatPath(issue.path); return path ? `${path}: ${issue.message}` : issue.message; }) .join("; "); } async function parsePayload(schema, input, args) { const result = await schema["~standard"].validate(input); if (result.issues?.length) { throw new EventValidationError({ name: args.name, issues: result.issues, }); } if ("value" in result) { return result.value; } throw new Error("Invalid Standard Schema result: missing value"); } /** * Define a typed event. * * Event payloads are validated before publishing through `publishEvent(...)` * and before registered listeners run. */ export function defineEvent(name, options) { return { kind: "event", name, payload: options.payload, description: options.description, }; } function defineListenerImpl(name, options) { return { kind: "listener", name, event: options.event, handle: options.handle, }; } /** * Validate and parse an event payload with the event's Standard Schema. */ export async function parseEventPayload(event, payload) { return (await parsePayload(event.payload, payload, { name: event.name, })); } /** * Validate an event payload and publish it through an event bus. */ export async function publishEvent(eventBus, event, payload, options) { await parseEventPayload(event, payload); await eventBus.publish(event, payload, options); } /** * Register listeners against an event bus and return an unsubscribe function. * * Payloads are validated before listener handlers run. Listener context is * resolved per delivery when `options.ctx` is a factory. */ export function registerListeners(eventBus, listeners, options = {}) { const unsubscribes = listeners.map((listener) => eventBus.subscribe(listener.event, async (rawPayload, publishOptions) => { try { const payload = await parseEventPayload(listener.event, rawPayload); const traceAttributes = { "beignet.listener.name": listener.name, "beignet.event.name": listener.event.name, }; await runWithResolvedTracingContext({ tracing: options.tracing, ctx: options.ctx, operation: { name: `beignet.listener ${listener.name}`, type: "listener", kind: "consumer", parent: parseTraceCarrier(publishOptions?.trace), attributes: traceAttributes, metricAttributes: traceAttributes, }, run: (ctx) => listener.handle({ event: listener.event, payload, ctx, }), }); } catch (error) { options.onError?.(error, listener); if (!options.onError) throw error; } })); return () => { for (const unsubscribe of [...unsubscribes].reverse()) { unsubscribe(); } }; } /** * Create listener helper methods bound to an application context type. * * Call it once in `lib/listeners.ts`: * * ```ts * export const { defineListener } = createListeners<AppContext>(); * ``` */ export function createListeners() { return { defineListener(name, options) { return defineListenerImpl(name, options); }, }; } //# sourceMappingURL=index.js.map