103 lines
3.6 KiB
Markdown
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.
|