56 lines
3.4 KiB
Markdown
56 lines
3.4 KiB
Markdown
## 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
|