Files
skills/docs/api-stack/api/api-plugin-event-bus/src/middleware/request-bus.md
2026-05-29 08:20:56 +01:00

103 lines
3.6 KiB
Markdown

# `request-bus.ts` — request-scoped event-bus handle middleware
## Purpose
Provides the Hono middleware that installs a `ScopedEventBusHandle`
on every request via `c.set("eventBus", handle)`. The handle is bound
to the request's tenant id (read from `c.get("tenant")?.id`) so
handler code can publish/subscribe without threading the tenant id
through every call. Pulling the handle construction into a free
function (`bindScopedHandle`) lets the HTTP routes reuse the same
binding logic for their own per-request execution.
## Features
- `requestBusMiddleware({ service, tenantIsolation })` — Hono
middleware factory.
- `bindScopedHandle(service, tenantId)` — pure binding helper used by
both the middleware AND the HTTP routes.
- `extractTenantId(c)` — defensive tenant-id reader that tolerates
missing / malformed `tenant` slots.
- Tenant binding is opt-out via `tenantIsolation: false`.
## Usage: Basic
```ts
import { requestBusMiddleware } from "@suaveplan/api-plugin-event-bus/middleware/request-bus";
app.use(
"*",
requestBusMiddleware({ service: eventBusService, tenantIsolation: true })
);
app.get("/me/events", async (c) => {
const bus = c.get("eventBus");
const list = bus.listSubscriptions();
return c.json({ list });
});
```
## Usage: Advanced
```ts
import {
bindScopedHandle,
requestBusMiddleware,
} from "@suaveplan/api-plugin-event-bus/middleware/request-bus";
// Inside a sibling plugin's route handler that runs OUTSIDE the
// standard middleware chain (e.g. a SSE handler) — bind directly.
const handle = bindScopedHandle(eventBusService, "acme");
await handle.publish("order.created", { id: "o1" });
// Standard wiring — the middleware installs the handle on every
// request and auto-extracts the tenant from c.get("tenant").
app.use("*", requestBusMiddleware({ service, tenantIsolation: true }));
```
## API Reference
### `requestBusMiddleware(options): MiddlewareHandler`
Returns a Hono middleware that:
1. Extracts the tenant id via `extractTenantId(c)` (when
`tenantIsolation` is `true`).
2. Builds the scoped handle via `bindScopedHandle`.
3. Calls `c.set("eventBus", handle)`.
4. Awaits `next()`.
### `bindScopedHandle(service, tenantId): ScopedEventBusHandle`
Returns a handle whose `publish` / `subscribe` / `unsubscribe` /
`listSubscriptions` calls auto-inject the bound `tenantId`. The
`unsubscribe` handler swallows `EventBusError(TENANT_MISMATCH)` and
returns `false` so handler code does not have to special-case the
cross-tenant detach attempt.
### `extractTenantId(c): string | undefined`
Reads `c.get("tenant")?.id` defensively. Returns `undefined` when the
slot is missing, the tenant has no id, or the id is empty.
### `interface ScopedEventBusHandle`
Same surface as `EventBusService` minus the explicit `tenantId`
parameter. See `services/event-bus-service.md`.
## Implementation Notes
- The cast in `extractTenantId` is required because the Hono context
type widens `c.get(name)` to `unknown` without an augmentation —
the `tenant` slot itself is owned by `@suaveplan/api-plugin-tenant`
and we deliberately do not depend on that package at runtime.
- The `unsubscribe` wrapper swallows `TENANT_MISMATCH` because the
HTTP layer needs to distinguish "not found" from "owned by another
tenant" only at the route handler — the middleware-level handle
treats both as "false". The HTTP route surface uses
`service.unsubscribe(id, tenantId)` directly and translates the
thrown error into a 404 response.
- When `tenantIsolation: false`, every request observes
`tenantId: undefined` on the handle and the service-level scoping
becomes a no-op.