3.0 KiB
Change: events-generics-and-interceptor-replace
Why
@suaveplan/events shipped at 0.0.0 with 100% coverage but the implementation drifts from the spec in four places that matter for downstream events-schema, events-otel, events-metrics, and events-transport:
Interceptoris non-generic — every interceptor seesEventEnvelope<unknown>even when the bus isEventBus<TMap>. Forces unsafe casts in every adapter and undermines the typed-bus value proposition.- Spec scenario "Interceptor can replace payload" is unimplemented —
Nexttakes no args, so an interceptor cannot pass a replacement envelope down the chain.composealready plumbsenvthroughrun(index, env), only the public type prevents it. - Sync
emit()silently drops rejections from listeners that return Promises — they become unhandled rejections instead of landing inEmitResult.errors. Listener<T>is duplicated inidempotent.tsinstead of importing from the bus types — a drift trap.
These are correctness fixes, not new features. Public-surface-compatible: existing call sites keep working because the new generic parameter has a default and the new Next envelope arg is optional.
What Changes
Interceptor becomes Interceptor<TMap = EventMap>
type Interceptor<TMap extends EventMap = EventMap> = <K extends keyof TMap>(
envelope: EventEnvelope<TMap[K]>,
next: Next<TMap, K>
) => Promise<EmitResult>;
EventBus<TMap>.use(interceptor: Interceptor<TMap>) narrows to the bus's map. Existing untyped interceptors keep working through the default.
Next accepts an optional replacement envelope
type Next<TMap extends EventMap = EventMap, K extends keyof TMap = keyof TMap> =
(envelope?: EventEnvelope<TMap[K]>) => Promise<EmitResult>;
Calling next() advances with the current envelope. Calling next(replacement) advances with the replacement; downstream listeners observe replacement.payload.
Sync emit captures async rejections
When a listener returns a thenable, emit attaches a .catch() that wraps the rejection as EventsError("EVENTS.LISTENER.THREW") and pushes it onto a deferred-error channel exposed via EmitResult.deferredErrors: Promise<readonly EventsError[]>. Synchronous throws still land in errors[] as before. This is additive — errors semantics unchanged.
Sync emit interceptor behavior — documented bypass
Sync emit does NOT run async interceptors. Document this explicitly in event-bus.md and surface a runtime warning hook (onSyncEmitWithInterceptors?: (count) => void) on EventBusOptions so adapters can detect mixed usage. Consumers wanting interceptor coverage use emitAsync.
idempotent reuses Listener<T> from bus types
Drop the local Listener<T> declaration; import from ../bus/event-bus.types.js.
Impact
@suaveplan/events: minor (semver-additive within 0.0.x)- Unblocks proper typing in
events-schema,events-otel,events-metrics,events-transport - No external consumers yet — published only to Verdaccio at 0.0.0