Initial Commit
This commit is contained in:
100
packages/foundation/flow/rules/src/events/event-bus.md
Normal file
100
packages/foundation/flow/rules/src/events/event-bus.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# EventBus module — `@suaveplan/rules/events`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Pub/sub bus used by the engine and downstream consumers (Hono middleware, audit logs, observability sinks) to broadcast lifecycle events without coupling. Topics are drawn from the typed `RuleEngineEvent` union — adding a new topic is a type change rather than a string literal. The bus supports plain `on` / `off` / `once` subscriptions, priority-ordered `hook` registrations (higher fires first), a bounded ring-buffer history (default 1000 entries), an awaitable `waitFor(event, { timeoutMs?, signal? })` API, and OTel emission counting every payload by `type`.
|
||||
|
||||
A sibling `HookManager` class scopes the priority-hook surface to a single topic so callers can manage hooks without holding the parent bus — useful for engine plugins that own a handful of lifecycle hooks but should not have a back-channel to the global event stream.
|
||||
|
||||
## Features
|
||||
|
||||
- Typed topic API — every `on` / `emit` is keyed by `RuleEngineEvent`.
|
||||
- Priority hooks — `hook(event, { priority, listener })`; sorted descending by priority on register; finite-priority enforcement raises `RULES.HOOK_INVALID`.
|
||||
- Awaitable `waitFor` — resolves with the payload on first match; rejects with `RULES.EVENT_TIMEOUT` past `timeoutMs` or `RULES.EVENT_ABORTED` when the supplied `AbortSignal` fires (including pre-aborted).
|
||||
- Bounded history — `getHistory()` returns the last N payloads in arrival order; `clearHistory()` drops the buffer.
|
||||
- Injectable `Clock` — `bus.now()` for callers that timestamp their own payloads.
|
||||
- OTel counter — every `emit` / `emitSync` increments `rules.event.emitted_total{type}`.
|
||||
- `HookManager` — standalone priority-hook bag; `register` / `remove` / `invoke` / `size`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createEventBus } from "@suaveplan/rules/events";
|
||||
|
||||
const bus = createEventBus();
|
||||
bus.on("rule.passed", (payload) => {
|
||||
console.log(`rule passed at ${payload.timestamp}`);
|
||||
});
|
||||
await bus.emit({ type: "rule.passed", timestamp: Date.now() });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createEventBus, createHookManager } from "@suaveplan/rules/events";
|
||||
|
||||
const bus = createEventBus({ historySize: 200, clock: () => Date.now() });
|
||||
|
||||
// Priority-ordered hooks — higher fires first
|
||||
bus.hook("rule.passed", {
|
||||
priority: 100,
|
||||
listener: async (payload) => audit.append(payload),
|
||||
});
|
||||
bus.hook("rule.passed", {
|
||||
priority: 10,
|
||||
listener: async (payload) => metrics.increment("rule.passed", { type: payload.type }),
|
||||
});
|
||||
|
||||
// Await the next event with a hard 5s ceiling and caller-side abort
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
try {
|
||||
const payload = await bus.waitFor("ruleset.completed", {
|
||||
timeoutMs: 5000,
|
||||
signal: controller.signal,
|
||||
});
|
||||
console.log("ruleset finished:", payload.timestamp);
|
||||
} catch (err) {
|
||||
console.warn("waitFor failed:", err);
|
||||
}
|
||||
|
||||
// Standalone hook manager — convenient for engine plugin internals
|
||||
const mgr = createHookManager();
|
||||
mgr.register({ priority: 1, listener: () => undefined });
|
||||
await mgr.invoke({ type: "rule.failed", timestamp: bus.now() });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class EventBus`
|
||||
|
||||
- `new EventBus(options?)` — `options.clock` defaults to `() => Date.now()`. `options.historySize` defaults to `DEFAULT_EVENT_HISTORY = 1000`; `<= 0` raises `RULES.HOOK_INVALID`.
|
||||
- `on(event, listener)` → unsubscribe fn.
|
||||
- `off(event, listener)` → boolean.
|
||||
- `once(event, listener)` → unsubscribe fn; auto-removes after first fire.
|
||||
- `hook(event, { priority, listener, id? })` → unsubscribe fn. Sorted descending; non-finite priority raises `RULES.HOOK_INVALID`.
|
||||
- `emit(payload)` → `Promise<void>`. Records history, increments OTel counter, runs hooks first (in priority order), then listeners (in registration order). Awaits every callback.
|
||||
- `emitSync(payload)` — fire-and-forget variant; same ordering but callbacks are not awaited.
|
||||
- `waitFor(event, options?)` → `Promise<EventPayload>`. Rejects with `RULES.EVENT_TIMEOUT` or `RULES.EVENT_ABORTED`.
|
||||
- `getHistory()` / `clearHistory()` — history-buffer accessors.
|
||||
- `now()` — convenience over the injected clock.
|
||||
|
||||
### `class HookManager`
|
||||
|
||||
Standalone priority-hook bag scoped to one topic. Methods: `register`, `remove`, `invoke`, `size`. Same priority + finite-number rules as the bus.
|
||||
|
||||
### Factory aliases
|
||||
|
||||
`createEventBus(options?)`, `createHookManager()`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The bus runs hooks **before** listeners on `emit`. This lets plugins inspect and (in future) decorate the payload before user-land code observes it.
|
||||
- `on` / `hook` snapshots their bucket via `[...bucket]` before iterating so that unsubscribing during emission does not skip subsequent callbacks.
|
||||
- `waitFor` registers a one-shot listener plus optional abort + timeout handlers. Every cleanup path (resolve, reject-timeout, reject-abort, pre-abort) runs every cleanup function exactly once via the local `cleanup: (() => void)[]` queue.
|
||||
- `historyBuffer` is a plain array trimmed with `shift()`. Trimming via a ring index would shave a microsecond per insert but adds bookkeeping; the cost is dominated by listener / hook execution in any realistic workload.
|
||||
- OTel emission is per-`emit` and not per-listener, so the counter measures bus activity rather than work done by subscribers. Subscribers that want their own metrics emit via their own meter.
|
||||
- `emitSync` does not return a value (no `Promise` to await), but callback exceptions are NOT silently swallowed — the runtime will surface them as unhandled promise rejections through the `void` discard. Callers that need synchronous error handling should use `emit` and `await` it.
|
||||
Reference in New Issue
Block a user