60 lines
3.7 KiB
Markdown
60 lines
3.7 KiB
Markdown
# Change: enterprise-events
|
|
|
|
## Why
|
|
|
|
`@suaveplan/events` is the foundation pub/sub substrate — typed event bus, deterministic ordering, backpressure-aware async dispatch, and a public companion/interceptor API that the sibling packages (`events-metrics`, `events-otel`, `events-schema`, `events-transport`) plug into without monkey-patching. Every domain-event-emitting package will depend on it.
|
|
|
|
Per the foundation-100-coverage rule (see memory `foundation-100-coverage`), this ships at **100%** coverage from day one.
|
|
|
|
## What Changes
|
|
|
|
### New package: `packages/universal/events`
|
|
|
|
Ships as a focused, zero-Node / zero-browser / zero-React TypeScript library. Runtime deps limited to `@suaveplan/error`, `tslib`, and `zod`.
|
|
|
|
#### Core surface
|
|
|
|
- **`EventBus<TMap>`** — typed bus parameterized by an event-name → payload map. Methods: `on`, `once`, `off`, `emit` (sync), `emitAsync` (async with AbortSignal), `use` (interceptor), `clear`
|
|
- **`EventEnvelope<T>`** — `{ id, type, payload, timestamp, traceId? }`, produced by `wrap(type, payload, options?)`
|
|
- **`createIdGenerator()`** — deterministic-under-injection id generator (default: `crypto.randomUUID()` via a single module-local reference that tests can swap)
|
|
- **`Interceptor<TMap>`** — `(envelope, next) => R | Promise<R>` chain-of-responsibility. Registration order defines execution order; `next()` advances the chain; interceptors can replace, drop (by not calling `next`), or observe
|
|
- **`BoundedQueue<T>`** — in-memory FIFO with `{ capacity, policy: "drop-oldest" | "drop-newest" | "block" | "throw" }`, used internally by `emitAsync` under backpressure and exposed for consumers
|
|
- **`idempotent(fn, { keyOf, cache? })`** — wraps a handler so a given `keyOf(envelope)` only fires once; the cache is a `Map` by default, pluggable
|
|
|
|
#### Error surface
|
|
|
|
- **`EventsError`** extends `SuaveplanError<EventsCode>` with codes like `EVENTS.LISTENER.THREW`, `EVENTS.QUEUE.OVERFLOW`, `EVENTS.QUEUE.ABORTED`, `EVENTS.SCHEMA.REJECTED`, `EVENTS.INTERCEPTOR.THREW`
|
|
- Errors capture `{ type, envelopeId, listenerIndex?, queueSize? }` in `context` and preserve the original `cause`
|
|
|
|
#### Schemas
|
|
|
|
- **`EventEnvelopeSchemas`** (`.schemas.ts`) — Zod schema for the envelope shape with generic payload; consumers opt in by importing `@suaveplan/events/envelope.schemas`
|
|
|
|
#### Observability hook
|
|
|
|
- **`withObservability(bus, adapter)`** — wraps an `EventBus` with a passive adapter implementing `{ onEmit?, onEmitError?, onEmitComplete?, onListenerThrew? }`; `events-metrics` / `events-otel` register here
|
|
|
|
#### Companion/interceptor API
|
|
|
|
- `bus.use(interceptor)` is the one public extension point; `events-schema` validates envelopes via an interceptor, `events-transport` forwards to an external sink via an interceptor, `events-metrics` times via an interceptor, `events-otel` creates spans via an interceptor. The core ships no direct coupling to any of those.
|
|
|
|
### Determinism
|
|
|
|
- No `Math.random()` / `Date.now()` at module scope
|
|
- `createBus({ clock, idGenerator })` accepts injections; defaults are the thin wrappers `defaultClock = () => Date.now()` and `defaultIdGenerator = () => crypto.randomUUID()`
|
|
|
|
### Coverage
|
|
|
|
Vitest thresholds locked at 100/100/100/100 in `vitest.config.ts`. Property-based tests (fast-check) cover:
|
|
|
|
- dispatch ordering (FIFO across listeners and across emits)
|
|
- interceptor composition (associativity, drop-propagation)
|
|
- backpressure policies (bounded queue invariants)
|
|
- idempotency (given key-collisions, handler called exactly once)
|
|
|
|
## Impact
|
|
|
|
- Unblocks `events-metrics`, `events-otel`, `events-schema`, `events-transport` (each a Layer 2 adapter on top of `@suaveplan/events`)
|
|
- Depends on `@suaveplan/error` (already published) for `SuaveplanError` and `defineCodes`
|
|
- No existing genesis consumers affected
|