11 KiB
@suaveplan/api-plugin-event-bus - Typed event-bus plugin for @suaveplan/api. Wraps @suaveplan/events (pub/sub, async dispatch, interceptors, bounded-queue backpressure, idempotent listener wrapper) as a single definePlugin descriptor. Registers a typed eventBusService in the host ServiceRegistry, installs request middleware that exposes a tenant-scoped publish/subscribe handle on c.get("eventBus"), and (optionally) mounts 3 auth-gated OpenAPI routes for publish/list/delete. Per-tenant topic scoping isolates publishes across tenants by prefixing topics with tenant::. Server-tier — composes cleanly with @suaveplan/api-plugin-auth + @suaveplan/api-plugin-tenant.
Typed event-bus plugin for @suaveplan/api. Wraps @suaveplan/events
(pub/sub, async dispatch, interceptors, bounded-queue backpressure,
idempotent listener wrapper) as a single definePlugin consumer so
apps composing @suaveplan/api get a working, instrumented,
per-tenant scoped event bus by adding one entry to their plugins
array.
Naming note: enterprise/api-plugin-events.md documents a different
package (social calendar / RSVP / meetup feed) that is deferred to a
later wave. This event-bus plugin is the in-process event-bus
integration wrapper.
Documentation tree
- error-codes — Declares the stable error-code registry consumed by every failure path in
@suaveplan/api-plugin-event-bus. The regist…- error-codes — Declares the stable error-code registry consumed by every failure path in
@suaveplan/api-plugin-event-bus. The regist…
- error-codes — Declares the stable error-code registry consumed by every failure path in
- context-augmentation — Adds a typed
eventBusslot to Hono'sContextVariableMapso request handlers seec.get("eventBus")resolve to `Sco… - request-bus — Provides the Hono middleware that installs a
ScopedEventBusHandleon every request viac.set("eventBus", handle). T… - otel — Exposes the five OpenTelemetry instruments emitted from the plugin's production paths and a lazy singleton (`getEventBu…
- instruments — Exposes the five OpenTelemetry instruments emitted from the plugin's production paths and a lazy singleton (`getEventBu…
- plugin — Top-level factory exported as the package's main entry point. Builds a typed
PluginDescriptor<EventBusPluginConfig>p…- plugin — Top-level factory exported as the package's main entry point. Builds a typed
PluginDescriptor<EventBusPluginConfig>p…
- plugin — Top-level factory exported as the package's main entry point. Builds a typed
- routes — Builds the three
PluginRouteEntryobjects the plugin mounts on the host Hono app +OpenAPIRegistryvia `definePlugi…- build-routes — Builds the three
PluginRouteEntryobjects the plugin mounts on the host Hono app +OpenAPIRegistryvia `definePlugi…
- build-routes — Builds the three
- options — Boundary Zod schema validated by
createEventBusPlugin(options)at the factory entry point. Invalid input produces an… - routes — Boundary Zod schemas validated by the three OpenAPI routes the plugin optionally mounts:
POST /event-bus/publish, `GE… - backpressure — Wraps
@suaveplan/events'BoundedQueuewith a small adapter that treats the queue as a pure accounting primitive: a… - event-bus-service — Builds the canonical
EventBusServiceregistered in the hostServiceRegistrybycreateEventBusPlugin. The service… - helpers — Holds the pure helpers extracted from the main service body so the service itself stays under the 300-line cap and each…
- idempotency-cache — Provides the default
IdempotencyCacheimplementation used byeventBusServiceto suppress duplicate publishes. The c… - registry-augmentation — Augments
@suaveplan/api-plugin/service-registry'sServiceRegistryinterface to advertise theeventBusServiceslot… - topic — Pure functions for validating event-bus topics and projecting them into their tenant-scoped form. Every
publishand `…- topic — Pure functions for validating event-bus topics and projecting them into their tenant-scoped form. Every
publishand `…
- topic — Pure functions for validating event-bus topics and projecting them into their tenant-scoped form. Every
- types — Declares the two top-level shapes consumed by external callers of the plugin:
EventBusPluginOptions(what `createEven…- types — Declares the two top-level shapes consumed by external callers of the plugin:
EventBusPluginOptions(what `createEven…
- types — Declares the two top-level shapes consumed by external callers of the plugin:
Install
bun add @suaveplan/api-plugin-event-bus
Quick start
import { defineApp } from "@suaveplan/api";
import { createEventBusPlugin } from "@suaveplan/api-plugin-event-bus";
const app = await defineApp()
.name("my-api")
.use(createEventBusPlugin({}))
.start();
createEventBusPlugin returns a typed PluginDescriptor. The host's
PluginRegistry calls onInit(ctx) which:
- Resolves
appand (optionally)openAPIRegistryfrom the hostServiceRegistry. - Constructs an
EventBusfrom@suaveplan/eventsand applies configured interceptors, bounded-queue backpressure, and idempotent listener wiring. - Registers
eventBusServicein the typedServiceRegistry. - Auto-prefixes topics with
tenant:<id>:when a tenant slot is present on the request (provided by@suaveplan/api-plugin-tenant). - Installs request middleware so every handler sees a
ScopedEventBusHandleunderc.get("eventBus"). - Optionally mounts 3 OpenAPI routes (
POST /event-bus/publish,GET /event-bus/subscriptions,DELETE /event-bus/subscriptions/:id), auth-gated byrequireAuth()whenrequireAuthForRoutes: true(the default).
In-handler usage
app.post("/checkout", async (c) => {
const bus = c.get("eventBus");
const result = await bus.publish("order.created", {
id: "o1",
total: 9.99,
});
return c.json({ delivered: result.delivered });
});
app.get("/me/subs", (c) => {
const bus = c.get("eventBus");
return c.json({ subscriptions: bus.listSubscriptions() });
});
The handle is automatically scoped to the request's tenant — a publish on tenant A's request never reaches a subscriber registered through tenant B's handle.
Per-tenant isolation
When tenantIsolation: true (the default) the service prefixes every
publish and subscribe topic with tenant:<id>: extracted from
c.get("tenant")?.id. The HTTP routes follow the same rule and
unscope topics on the way out so wire responses never leak the
internal prefix. Cross-tenant unsubscribe(id) attempts return HTTP
404 to avoid leaking subscription-id ownership.
Set tenantIsolation: false for single-tenant deployments — every
publish flows on the bare topic.
Backpressure
Every publish flows through a BoundedQueue from @suaveplan/events
configured by backpressure: { capacity, policy }. The four
QueuePolicy values (drop-oldest, drop-newest, block, throw)
are forwarded verbatim. Under drop-oldest/drop-newest, the
PublishResult.dropped field reports overflow events alongside an
event_bus.dropped_total{policy} counter increment. Under
policy: "throw", overflow rejects the publish promise with
EventBusError(EVENT_BUS.BACKPRESSURE_DROPPED).
Idempotency
A subscription registered with idempotencyKey: (payload) => string
fires its handler at most once per derived key. The plugin-level
idempotency.enabled: true flag activates the same wrapper for every
subscriber that does not supply its own key function — the default
derivation is JSON.stringify(payload). Duplicates increment
event_bus.duplicates_total.
OTel metrics
| Metric | Type | Labels |
|---|---|---|
event_bus.published_total |
Counter | outcome, topic_root |
event_bus.dropped_total |
Counter | policy |
event_bus.duplicates_total |
Counter | — |
event_bus.subscriptions_active |
UpDownCounter | — |
event_bus.publish_duration_ms |
Histogram | — |
Error codes
EVENT_BUS_CODES exports 8 stable codes: INVALID_INPUT,
INVALID_TOPIC, SERVICE_NOT_AVAILABLE, ROUTE_PREFIX_COLLISION,
SUBSCRIPTION_NOT_FOUND, BACKPRESSURE_DROPPED, TENANT_MISMATCH,
PUBLISH_FAILED. See src/error-codes/error-codes.md for the full
table and context.statusCode mapping.
Modules
src/error-codes/error-codes.md—EVENT_BUS_CODES,EventBusError,EventBusErrorCode.src/middleware/context-augmentation.md— HonoContextVariableMap.eventBusdeclaration.src/middleware/request-bus.md—requestBusMiddleware,bindScopedHandle,extractTenantId.src/otel/instruments.md— 5-metric bundle + lazy singleton + reset helper.src/plugin/plugin.md—createEventBusPluginfactory + 6-steponInitlifecycle.src/routes/build-routes.md— 3 OpenAPI route entries (publish / list / delete).src/schemas/options.md— boundary Zod schema forEventBusPluginOptions.src/schemas/routes.md— Zod schemas for the HTTP route surface.src/services/backpressure.md— bounded-queue accounting primitive.src/services/event-bus-service.md—EventBusServicefactory + per-tenant scoping + idempotency.src/services/helpers.md— id generation, error projection, OTel emission helpers.src/services/idempotency-cache.md— bounded set-basedIdempotencyCacheimplementation.src/services/registry-augmentation.md—ServiceRegistryslot declaration foreventBusService.src/topic/topic.md—EVENT_TOPIC_PATTERNscopeTopic/unscopeTopic/assertValidTopic.
src/types/types.md— public option / config interfaces.
Subpath exports
., ./plugin/plugin, ./middleware/request-bus,
./middleware/context-augmentation, ./services/event-bus-service,
./services/registry-augmentation, ./schemas/options,
./schemas/routes, ./routes/build-routes, ./topic/topic,
./error-codes/error-codes, ./otel/instruments, ./types/types.
Peer deps
hono,@hono/zod-openapi,zod— supplied by the host application.@suaveplan/api-plugin-auth— used byrequireAuth()route gate whenrequireAuthForRoutes: true(default).