Files
2026-05-27 23:29:53 +01:00

3.4 KiB

MODIFIED Requirements

Requirement: Interceptor API

The package SHALL export bus.use(interceptor) registering an interceptor of shape <K extends keyof TMap>(envelope: EventEnvelope<TMap[K]>, next: Next<TMap, K>) => Promise<EmitResult>. Interceptor<TMap> and Next<TMap, K> are generic over the bus's event map. Next accepts an optional replacement envelope so an interceptor MAY rewrite the envelope before downstream interceptors and listeners observe it.

Scenario: Interceptor envelope is typed

  • WHEN a consumer registers bus.use<{login: {user: string}}> an interceptor on a EventBus<{login: {user: string}}>
  • THEN envelope.payload is typed {user: string} inside the interceptor body (not unknown)

Scenario: Interceptor can short-circuit

  • WHEN an interceptor returns without calling next()
  • THEN registered listeners do not fire AND EmitResult.delivered === 0

Scenario: Interceptor throw aborts emit

  • WHEN an interceptor throws
  • THEN emitAsync rejects with EventsError("EVENTS.INTERCEPTOR.THREW") with cause === the thrown value

Scenario: Interceptor can replace envelope via next()

  • WHEN an interceptor invokes next(replacement) with a new EventEnvelope
  • THEN downstream interceptors and listeners receive the replacement envelope AND replacement.payload is delivered to listeners

Requirement: Structured errors

All thrown errors SHALL be EventsError extends SuaveplanError<EventsCode> with codes prefixed EVENTS.. Listener failures SHALL be collected into EmitResult.errors (synchronous failures) or EmitResult.deferredErrors (failures from listener-returned promises in sync emit), not propagated, unless an interceptor explicitly re-throws.

Scenario: Listener throw is captured

  • WHEN a sync listener throws during emit
  • THEN emit returns EmitResult with errors[0].code === "EVENTS.LISTENER.THREW" AND errors[0].cause === the thrown value AND delivered reflects successful synchronous deliveries only

Scenario: Async-listener rejection captured by sync emit

  • WHEN a listener registered via bus.on() returns a Promise that rejects AND bus.emit() is called
  • THEN the synchronous return of emit() resolves with delivered counting the dispatched listener (call did not throw) AND EmitResult.deferredErrors resolves to a list containing an EventsError("EVENTS.LISTENER.THREW") whose cause is the rejection value

ADDED Requirements

Requirement: Sync emit bypasses interceptors with detection hook

Sync emit() SHALL NOT run async interceptors (interceptors are asynchronous and emit() is synchronous). The bus SHALL accept an optional onSyncEmitWithInterceptors callback in EventBusOptions that fires once per sync emit whenever interceptors are registered, allowing adapters to detect mixed usage.

Scenario: Sync emit with interceptors triggers warning hook

  • WHEN bus.emit("x", v) is called on a bus with at least one registered interceptor AND onSyncEmitWithInterceptors is provided
  • THEN onSyncEmitWithInterceptors(count) is invoked with the registered interceptor count AND interceptors do not run AND listeners still fire in registration order

Scenario: Sync emit without interceptors does not invoke hook

  • WHEN bus.emit("x", v) is called and no interceptors are registered
  • THEN the hook is not invoked