# `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.