Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View 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.