197 lines
11 KiB
Markdown
197 lines
11 KiB
Markdown
# @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:<id>:. 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.
|
|
|
|
<!-- begin:auto-docs -->
|
|
## Documentation tree
|
|
|
|
- [error-codes](./src/error-codes/error-codes.md) — Declares the stable error-code registry consumed by every failure path in `@suaveplan/api-plugin-event-bus`. The regist…
|
|
- [error-codes](./src/error-codes/error-codes.md) — Declares the stable error-code registry consumed by every failure path in `@suaveplan/api-plugin-event-bus`. The regist…
|
|
- [context-augmentation](./src/middleware/context-augmentation.md) — Adds a typed `eventBus` slot to Hono's `ContextVariableMap` so request handlers see `c.get("eventBus")` resolve to `Sco…
|
|
- [request-bus](./src/middleware/request-bus.md) — Provides the Hono middleware that installs a `ScopedEventBusHandle` on every request via `c.set("eventBus", handle)`. T…
|
|
- [otel](./src/otel/instruments.md) — Exposes the five OpenTelemetry instruments emitted from the plugin's production paths and a lazy singleton (`getEventBu…
|
|
- [instruments](./src/otel/instruments.md) — Exposes the five OpenTelemetry instruments emitted from the plugin's production paths and a lazy singleton (`getEventBu…
|
|
- [plugin](./src/plugin/plugin.md) — Top-level factory exported as the package's main entry point. Builds a typed `PluginDescriptor<EventBusPluginConfig>` p…
|
|
- [plugin](./src/plugin/plugin.md) — Top-level factory exported as the package's main entry point. Builds a typed `PluginDescriptor<EventBusPluginConfig>` p…
|
|
- [routes](./src/routes/build-routes.md) — Builds the three `PluginRouteEntry` objects the plugin mounts on the host Hono app + `OpenAPIRegistry` via `definePlugi…
|
|
- [build-routes](./src/routes/build-routes.md) — Builds the three `PluginRouteEntry` objects the plugin mounts on the host Hono app + `OpenAPIRegistry` via `definePlugi…
|
|
- [options](./src/schemas/options.md) — Boundary Zod schema validated by `createEventBusPlugin(options)` at the factory entry point. Invalid input produces an…
|
|
- [routes](./src/schemas/routes.md) — Boundary Zod schemas validated by the three OpenAPI routes the plugin optionally mounts: `POST /event-bus/publish`, `GE…
|
|
- [backpressure](./src/services/backpressure.md) — Wraps `@suaveplan/events`' `BoundedQueue` with a small adapter that treats the queue as a pure accounting primitive: a…
|
|
- [event-bus-service](./src/services/event-bus-service.md) — Builds the canonical `EventBusService` registered in the host `ServiceRegistry` by `createEventBusPlugin`. The service…
|
|
- [helpers](./src/services/helpers.md) — Holds the pure helpers extracted from the main service body so the service itself stays under the 300-line cap and each…
|
|
- [idempotency-cache](./src/services/idempotency-cache.md) — Provides the default `IdempotencyCache` implementation used by `eventBusService` to suppress duplicate publishes. The c…
|
|
- [registry-augmentation](./src/services/registry-augmentation.md) — Augments `@suaveplan/api-plugin/service-registry`'s `ServiceRegistry` interface to advertise the `eventBusService` slot…
|
|
- [topic](./src/topic/topic.md) — Pure functions for validating event-bus topics and projecting them into their tenant-scoped form. Every `publish` and `…
|
|
- [topic](./src/topic/topic.md) — Pure functions for validating event-bus topics and projecting them into their tenant-scoped form. Every `publish` and `…
|
|
- [types](./src/types/types.md) — Declares the two top-level shapes consumed by external callers of the plugin: `EventBusPluginOptions` (what `createEven…
|
|
- [types](./src/types/types.md) — Declares the two top-level shapes consumed by external callers of the plugin: `EventBusPluginOptions` (what `createEven…
|
|
<!-- end:auto-docs -->
|
|
|
|
## Install
|
|
|
|
```bash
|
|
bun add @suaveplan/api-plugin-event-bus
|
|
```
|
|
|
|
## Quick start
|
|
|
|
```ts
|
|
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:
|
|
|
|
1. Resolves `app` and (optionally) `openAPIRegistry` from the host
|
|
`ServiceRegistry`.
|
|
2. Constructs an `EventBus` from `@suaveplan/events` and applies
|
|
configured interceptors, bounded-queue backpressure, and idempotent
|
|
listener wiring.
|
|
3. Registers `eventBusService` in the typed `ServiceRegistry`.
|
|
4. Auto-prefixes topics with `tenant:<id>:` when a tenant slot is
|
|
present on the request (provided by `@suaveplan/api-plugin-tenant`).
|
|
5. Installs request middleware so every handler sees a
|
|
`ScopedEventBusHandle` under `c.get("eventBus")`.
|
|
6. Optionally mounts 3 OpenAPI routes (`POST /event-bus/publish`,
|
|
`GET /event-bus/subscriptions`, `DELETE /event-bus/subscriptions/:id`),
|
|
auth-gated by `requireAuth()` when `requireAuthForRoutes: true`
|
|
(the default).
|
|
|
|
## In-handler usage
|
|
|
|
```ts
|
|
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`](./src/error-codes/error-codes.md)
|
|
— `EVENT_BUS_CODES`, `EventBusError`, `EventBusErrorCode`.
|
|
- [`src/middleware/context-augmentation.md`](./src/middleware/context-augmentation.md)
|
|
— Hono `ContextVariableMap.eventBus` declaration.
|
|
- [`src/middleware/request-bus.md`](./src/middleware/request-bus.md)
|
|
— `requestBusMiddleware`, `bindScopedHandle`, `extractTenantId`.
|
|
- [`src/otel/instruments.md`](./src/otel/instruments.md) — 5-metric
|
|
bundle + lazy singleton + reset helper.
|
|
- [`src/plugin/plugin.md`](./src/plugin/plugin.md) — `createEventBusPlugin`
|
|
factory + 6-step `onInit` lifecycle.
|
|
- [`src/routes/build-routes.md`](./src/routes/build-routes.md) — 3
|
|
OpenAPI route entries (publish / list / delete).
|
|
- [`src/schemas/options.md`](./src/schemas/options.md) — boundary
|
|
Zod schema for `EventBusPluginOptions`.
|
|
- [`src/schemas/routes.md`](./src/schemas/routes.md) — Zod schemas
|
|
for the HTTP route surface.
|
|
- [`src/services/backpressure.md`](./src/services/backpressure.md) —
|
|
bounded-queue accounting primitive.
|
|
- [`src/services/event-bus-service.md`](./src/services/event-bus-service.md)
|
|
— `EventBusService` factory + per-tenant scoping + idempotency.
|
|
- [`src/services/helpers.md`](./src/services/helpers.md) — id
|
|
generation, error projection, OTel emission helpers.
|
|
- [`src/services/idempotency-cache.md`](./src/services/idempotency-cache.md)
|
|
— bounded set-based `IdempotencyCache` implementation.
|
|
- [`src/services/registry-augmentation.md`](./src/services/registry-augmentation.md)
|
|
— `ServiceRegistry` slot declaration for `eventBusService`.
|
|
- [`src/topic/topic.md`](./src/topic/topic.md) — `EVENT_TOPIC_PATTERN`
|
|
- `scopeTopic` / `unscopeTopic` / `assertValidTopic`.
|
|
- [`src/types/types.md`](./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 by `requireAuth()` route gate
|
|
when `requireAuthForRoutes: true` (default).
|
|
|