@beignet/core
Version:
Core framework primitives for Beignet
144 lines • 4.44 kB
JavaScript
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