# `helpers.ts` — small utilities used by `event-bus-service.ts` ## Purpose Holds the pure helpers extracted from the main service body so the service itself stays under the 300-line cap and each helper can be unit-tested in isolation. The helpers cover id generation, payload hashing, error projection, subscription projection, emit-option assembly, and OTel metric emission. ## Features - `nextSubscriptionId` — collision-resistant id generator. - `stableHash` — deterministic-enough payload hash for default idempotency keying. - `toErrorReport` — converts upstream listener errors into the wire-level shape advertised on `PublishResultRecord.errors`. - `projectSubscription` — internal entry → public read-only view. - `buildEmitOptions` — sparse builder for `EventBus.emitAsync` options. - `recordPublishMetric` — one-call OTel emission for every publish. ## Usage: Basic ```ts import { nextSubscriptionId, stableHash, } from "@suaveplan/api-plugin-event-bus/services/event-bus-service"; const id = nextSubscriptionId(); const key = stableHash({ x: 1 }); ``` ## Usage: Advanced ```ts import { buildEmitOptions, recordPublishMetric, toErrorReport, } from "@suaveplan/api-plugin-event-bus/services/event-bus-service"; const emitOptions = buildEmitOptions({ signal: AbortSignal.timeout(2000), timeoutMs: 2000, idempotencyKey: "k1", }); const t0 = Date.now(); try { // ... emit recordPublishMetric("order.created", "success", Date.now() - t0); } catch (err) { recordPublishMetric("order.created", "error", Date.now() - t0); const report = toErrorReport(err); console.error(report.code, report.message); } ``` ## API Reference ### `nextSubscriptionId(): string` Returns `` `sub_${n}_${base36}` `` ids; the base36 suffix keeps two collocated service instances from colliding when the module-level counter desyncs across workers. ### `stableHash(payload: unknown): string` JSON-stringifies the payload (falling back to `String(payload)` on throw). Used as the default idempotency-key derivation when a subscription is registered without an explicit `idempotencyKey` function. ### `toErrorReport(err: unknown): PublishErrorReport` Projects a listener error into `{ code, message }`. Preserves `EventsError.code`; falls back to `EVENT_BUS.LISTENER_FAILED`. ### `projectSubscription(entry): SubscriptionRecordView` Strips internal fields and conditionally omits `tenantId` when absent. ### `buildEmitOptions(opts): EmitAsyncOptions` Sparse builder — only sets fields the caller explicitly provided. ### `recordPublishMetric(topic, outcome, durationMs): void` Increments `event_bus.published_total{outcome,topic_root}` and records the publish duration on `event_bus.publish_duration_ms`. ## Implementation Notes - `nextSubscriptionId`'s counter resets on module reload (test isolation) and per-process. Production collisions are guarded by the random suffix, which gives ~36⁸ ≈ 2.8e12 ids per counter value. - `stableHash` uses `JSON.stringify` which is not key-order stable across all engines for objects with non-enumerable properties. Callers requiring canonical hashing supply an explicit `idempotencyKey` function at subscribe time. - `toErrorReport`'s `EVENT_BUS.LISTENER_FAILED` envelope is a surface-level code — it is NOT registered in `EVENT_BUS_CODES` because nothing in the package throws it (it only appears in `PublishResultRecord.errors`). - `projectSubscription` returns a `readonly` shape so consumers cannot mutate the record they got from `listSubscriptions`.