Initial Commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { createEventBusPlugin } from "./plugin.js";
|
||||
120
packages/api-stack/api/api-plugin-event-bus/src/plugin/plugin.md
Normal file
120
packages/api-stack/api/api-plugin-event-bus/src/plugin/plugin.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# `plugin.ts` — `createEventBusPlugin` factory
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Top-level factory exported as the package's main entry point. Builds
|
||||
a typed `PluginDescriptor<EventBusPluginConfig>` produced by
|
||||
`definePlugin` from `@suaveplan/api-plugin`, with an `onInit` that
|
||||
implements the canonical 6-step lifecycle every
|
||||
`@suaveplan/api-plugin-*` package follows. Operators consuming
|
||||
`@suaveplan/api` add one entry (`createEventBusPlugin({...})`) to
|
||||
their plugins array and get a working, instrumented, per-tenant
|
||||
scoped event bus with optional HTTP routes.
|
||||
|
||||
## Features
|
||||
|
||||
- Zod-validated factory boundary (`EventBusError(INVALID_INPUT)` on
|
||||
bad input).
|
||||
- Canonical 6-step `onInit`: resolve services → construct service →
|
||||
register slot → install middleware → mount routes (optional).
|
||||
- Route prefix collision detection via the host's `OpenAPIRegistry`.
|
||||
- Optional auth gate via `requireAuth()` from
|
||||
`@suaveplan/api-plugin-auth`.
|
||||
- Tolerates hosts without an `OpenAPIRegistry` — mounts routes on
|
||||
the Hono app only in that path.
|
||||
- `onClose` clears the cached service so the descriptor can be
|
||||
re-initialised cleanly.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { defineApp } from "@suaveplan/api";
|
||||
import { createEventBusPlugin } from "@suaveplan/api-plugin-event-bus";
|
||||
|
||||
const app = await defineApp()
|
||||
.name("my-api")
|
||||
.use(createEventBusPlugin({}))
|
||||
.start();
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { defineApp } from "@suaveplan/api";
|
||||
import { createAuthPlugin } from "@suaveplan/api-plugin-auth";
|
||||
import { createEventBusPlugin } from "@suaveplan/api-plugin-event-bus";
|
||||
|
||||
const app = await defineApp()
|
||||
.name("my-api")
|
||||
.use(createAuthPlugin({ betterAuth: { /* ... */ } }))
|
||||
.use(
|
||||
createEventBusPlugin({
|
||||
backpressure: { capacity: 4096, policy: "drop-oldest" },
|
||||
idempotency: { enabled: true, cacheCapacity: 16384 },
|
||||
routePrefix: "/internal/bus",
|
||||
requireAuthForRoutes: true,
|
||||
tenantIsolation: true,
|
||||
interceptors: [
|
||||
async (env, next) => {
|
||||
console.log("emit", env.type);
|
||||
return next();
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.start();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createEventBusPlugin(options: EventBusPluginOptions): PluginDescriptor<EventBusPluginConfig>`
|
||||
|
||||
- Validates `options` via `EventBusPluginOptionsSchema`.
|
||||
- Returns a descriptor whose `onInit` runs the 6-step lifecycle.
|
||||
|
||||
### `onInit(ctx)`
|
||||
|
||||
1. Resolves `app` (required) and `openAPIRegistry` (optional) from
|
||||
`ctx`. Throws `SERVICE_NOT_AVAILABLE` when `app` is missing.
|
||||
2. Constructs the canonical `EventBusService` with the resolved
|
||||
interceptors / back-pressure / idempotency / tenant isolation /
|
||||
max topic length.
|
||||
3. Registers the service via `ctx.registerService("eventBusService", ...)`.
|
||||
4. Tenant scoping is baked into the service when
|
||||
`tenantIsolation: true`.
|
||||
5. Installs `requestBusMiddleware` on `app.use("*", ...)` so every
|
||||
request observes `c.get("eventBus")`.
|
||||
6. When `mountRoutes !== false`, prefixes the 3 route entries with
|
||||
`routePrefix` and registers them via `definePluginRoutes` (when
|
||||
an `OpenAPIRegistry` is present) or directly onto Hono. When
|
||||
`requireAuthForRoutes: true`, mounts `requireAuth()` under the
|
||||
route prefix as a sub-path guard.
|
||||
|
||||
### `onClose`
|
||||
|
||||
Drops the cached service so the descriptor can be re-initialised.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Route prefixing happens **before** the routes hit the OpenAPI
|
||||
registry — the entries returned by `buildRoutes` use relative
|
||||
paths so the same builder works for any prefix.
|
||||
- `requireAuth()` is mounted at the sub-path level
|
||||
(`app.use(routePrefix + "/*", requireAuth())`) rather than per
|
||||
route because `RouteBuilder` does not own a `.middleware(...)`
|
||||
slot. Hono's middleware chain ordering still runs the auth gate
|
||||
before each route handler.
|
||||
- The Hono-only mount path (when `openAPIRegistry` is undefined)
|
||||
exists so consumers driving the plugin from a stripped-down test
|
||||
harness (no OpenAPI integration) still get reachable routes.
|
||||
- The fallback `Hono` sub-router is cast through `OpenAPIHono` for
|
||||
type compatibility with `definePluginRoutes`'s `app` argument
|
||||
shape — at runtime the methods we call (`get`, `post`, `delete`,
|
||||
`route`) all exist on plain Hono.
|
||||
- The descriptor's `defaults` field carries the fully-resolved
|
||||
config so consumers reading `plugin.defaults` see the same shape
|
||||
the registry stores at boot.
|
||||
@@ -0,0 +1,313 @@
|
||||
import { OpenAPIHono } from "@hono/zod-openapi";
|
||||
import { createOpenAPIRegistry } from "@suaveplan/api-openapi";
|
||||
import { createPluginContext } from "@suaveplan/api-plugin";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { EVENT_BUS_CODES, EventBusError } from "../error-codes/error-codes.js";
|
||||
import { resetEventBusInstruments } from "../otel/instruments.js";
|
||||
import type { EventBusService } from "../services/event-bus-service.types.js";
|
||||
import type { EventBusPluginConfig } from "../types/types.js";
|
||||
import { createEventBusPlugin } from "./plugin.js";
|
||||
|
||||
const noopLogger = {
|
||||
debug: () => undefined,
|
||||
info: () => undefined,
|
||||
warn: () => undefined,
|
||||
error: () => undefined,
|
||||
};
|
||||
|
||||
function makeContext(
|
||||
services: Map<string, unknown>,
|
||||
defaults: EventBusPluginConfig
|
||||
) {
|
||||
return createPluginContext<EventBusPluginConfig>({
|
||||
logger: noopLogger,
|
||||
eventBus: new EventBus(),
|
||||
signal: new AbortController().signal,
|
||||
config: defaults,
|
||||
services,
|
||||
});
|
||||
}
|
||||
|
||||
describe("createEventBusPlugin", () => {
|
||||
it("createEventBusPlugin resolves defaults and returns a typed PluginDescriptor", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const plugin = createEventBusPlugin({});
|
||||
expect(plugin.name).toBe("event-bus");
|
||||
expect(plugin.version).toBe("0.1.0");
|
||||
const defaults = plugin.defaults as EventBusPluginConfig;
|
||||
expect(defaults.routePrefix).toBe("/event-bus");
|
||||
expect(defaults.mountRoutes).toBe(true);
|
||||
expect(defaults.requireAuthForRoutes).toBe(true);
|
||||
expect(defaults.tenantIsolation).toBe(true);
|
||||
expect(defaults.backpressure.policy).toBe("drop-oldest");
|
||||
expect(defaults.backpressure.capacity).toBe(1024);
|
||||
expect(defaults.idempotency.enabled).toBe(false);
|
||||
expect(defaults.maxTopicLength).toBe(256);
|
||||
});
|
||||
|
||||
it("createEventBusPlugin throws EVENT_BUS.INVALID_INPUT for invalid options", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
createEventBusPlugin({
|
||||
routePrefix: "no-leading-slash",
|
||||
} as never);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(EventBusError);
|
||||
expect((err as EventBusError).eventBusCode).toBe(
|
||||
EVENT_BUS_CODES.INVALID_INPUT
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("createEventBusPlugin.onInit", () => {
|
||||
it("onInit throws SERVICE_NOT_AVAILABLE when the app slot is missing", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const plugin = createEventBusPlugin({ mountRoutes: false });
|
||||
const ctx = makeContext(
|
||||
new Map<string, unknown>(),
|
||||
plugin.defaults as EventBusPluginConfig
|
||||
);
|
||||
try {
|
||||
await plugin.onInit?.(ctx);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(EventBusError);
|
||||
expect((err as EventBusError).eventBusCode).toBe(
|
||||
EVENT_BUS_CODES.SERVICE_NOT_AVAILABLE
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("onInit registers eventBusService in the ServiceRegistry", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", new OpenAPIHono());
|
||||
const plugin = createEventBusPlugin({ mountRoutes: false });
|
||||
const ctx = makeContext(
|
||||
services,
|
||||
plugin.defaults as EventBusPluginConfig
|
||||
);
|
||||
await plugin.onInit?.(ctx);
|
||||
const svc = services.get("eventBusService") as EventBusService;
|
||||
expect(svc).toBeDefined();
|
||||
expect(typeof svc.publish).toBe("function");
|
||||
expect(typeof svc.subscribe).toBe("function");
|
||||
});
|
||||
|
||||
it("onInit installs request-bus middleware that exposes ScopedEventBusHandle", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
const plugin = createEventBusPlugin({
|
||||
mountRoutes: false,
|
||||
requireAuthForRoutes: false,
|
||||
});
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
let observedTenant: string | null | undefined;
|
||||
app.get("/probe", (c) => {
|
||||
const handle = c.get("eventBus");
|
||||
observedTenant = handle?.tenantId ?? null;
|
||||
return c.text("ok");
|
||||
});
|
||||
const res = await app.request("/probe");
|
||||
expect(res.status).toBe(200);
|
||||
expect(observedTenant).toBeNull();
|
||||
});
|
||||
|
||||
it("onInit mounts routes onto OpenAPIRegistry when present", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const registry = createOpenAPIRegistry({
|
||||
info: { title: "test", version: "0.1.0" },
|
||||
});
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
services.set("openAPIRegistry", registry);
|
||||
const plugin = createEventBusPlugin({
|
||||
requireAuthForRoutes: false,
|
||||
});
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
expect(registry.getRoutesByPlugin("event-bus")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("mountRoutes=false skips route registration", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const registry = createOpenAPIRegistry({
|
||||
info: { title: "test", version: "0.1.0" },
|
||||
});
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
services.set("openAPIRegistry", registry);
|
||||
const plugin = createEventBusPlugin({
|
||||
mountRoutes: false,
|
||||
requireAuthForRoutes: false,
|
||||
});
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
expect(registry.getRoutesByPlugin("event-bus")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("onInit throws ROUTE_PREFIX_COLLISION when routes already occupy the prefix", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const registry = createOpenAPIRegistry({
|
||||
info: { title: "test", version: "0.1.0" },
|
||||
});
|
||||
registry.registerRoute({
|
||||
method: "get",
|
||||
path: "/event-bus/x",
|
||||
tags: [],
|
||||
pluginName: "other",
|
||||
});
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
services.set("openAPIRegistry", registry);
|
||||
const plugin = createEventBusPlugin({
|
||||
requireAuthForRoutes: false,
|
||||
});
|
||||
try {
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
throw new Error("expected throw");
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(EventBusError);
|
||||
expect((err as EventBusError).eventBusCode).toBe(
|
||||
EVENT_BUS_CODES.ROUTE_PREFIX_COLLISION
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("mounts onto Hono only when openAPIRegistry is absent", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
const plugin = createEventBusPlugin({
|
||||
requireAuthForRoutes: false,
|
||||
});
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
// The publish route should now answer on /event-bus/publish.
|
||||
const res = await app.request("/event-bus/publish", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ topic: "order.created", payload: {} }),
|
||||
});
|
||||
// Should be 2xx (handler ran).
|
||||
expect(res.status).toBeGreaterThanOrEqual(200);
|
||||
expect(res.status).toBeLessThan(400);
|
||||
});
|
||||
|
||||
it("onClose clears the cached service", { timeout: 30_000 }, async () => {
|
||||
resetEventBusInstruments();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", new OpenAPIHono());
|
||||
const plugin = createEventBusPlugin({ mountRoutes: false });
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
await plugin.onClose?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
// The state-internal cleared field is private; we observe via
|
||||
// the lack of throw and the descriptor's metadata.
|
||||
expect(plugin.name).toBe("event-bus");
|
||||
});
|
||||
|
||||
it("mounts under custom routePrefix '/'", { timeout: 30_000 }, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
const plugin = createEventBusPlugin({
|
||||
mountRoutes: true,
|
||||
requireAuthForRoutes: false,
|
||||
routePrefix: "/",
|
||||
});
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
// Publish reachable at top-level /publish.
|
||||
const res = await app.request("/publish", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ topic: "x", payload: {} }),
|
||||
});
|
||||
expect(res.status).toBeGreaterThanOrEqual(200);
|
||||
expect(res.status).toBeLessThan(400);
|
||||
});
|
||||
|
||||
it("requireAuthForRoutes=true installs requireAuth() under the prefix", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const registry = createOpenAPIRegistry({
|
||||
info: { title: "test", version: "0.1.0" },
|
||||
});
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
services.set("openAPIRegistry", registry);
|
||||
const plugin = createEventBusPlugin({
|
||||
mountRoutes: true,
|
||||
requireAuthForRoutes: true,
|
||||
});
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
// Anonymous request — requireAuth() raises AuthError because
|
||||
// `c.get("user")` is null. The default Hono error handler maps
|
||||
// that to 500 (the api error-handler middleware would map to
|
||||
// 401, but the bare app does not have it wired). Either way the
|
||||
// request did not succeed — the gate fired.
|
||||
const res = await app.request("/event-bus/subscriptions");
|
||||
expect(res.status).not.toBe(200);
|
||||
});
|
||||
|
||||
it("requireAuthForRoutes=true with root prefix mounts /* gate", {
|
||||
timeout: 30_000,
|
||||
}, async () => {
|
||||
resetEventBusInstruments();
|
||||
const app = new OpenAPIHono();
|
||||
const services = new Map<string, unknown>();
|
||||
services.set("app", app);
|
||||
const plugin = createEventBusPlugin({
|
||||
mountRoutes: true,
|
||||
requireAuthForRoutes: true,
|
||||
routePrefix: "/",
|
||||
});
|
||||
await plugin.onInit?.(
|
||||
makeContext(services, plugin.defaults as EventBusPluginConfig)
|
||||
);
|
||||
const res = await app.request("/subscriptions");
|
||||
expect(res.status).not.toBe(200);
|
||||
});
|
||||
});
|
||||
204
packages/api-stack/api/api-plugin-event-bus/src/plugin/plugin.ts
Normal file
204
packages/api-stack/api/api-plugin-event-bus/src/plugin/plugin.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import type { OpenAPIHono } from "@hono/zod-openapi";
|
||||
import type { OpenAPIRegistry } from "@suaveplan/api-openapi";
|
||||
import { definePluginRoutes } from "@suaveplan/api-openapi";
|
||||
import {
|
||||
definePlugin,
|
||||
type PluginContext,
|
||||
type PluginDescriptor,
|
||||
} from "@suaveplan/api-plugin";
|
||||
import { requireAuth } from "@suaveplan/api-plugin-auth";
|
||||
import { Hono } from "hono";
|
||||
import { EVENT_BUS_CODES, EventBusError } from "../error-codes/error-codes.js";
|
||||
import { requestBusMiddleware } from "../middleware/request-bus.js";
|
||||
import { buildRoutes } from "../routes/build-routes.js";
|
||||
import { EventBusPluginOptionsSchema } from "../schemas/options.js";
|
||||
import { createEventBusService } from "../services/event-bus-service.js";
|
||||
import type { EventBusService } from "../services/event-bus-service.types.js";
|
||||
import "../services/registry-augmentation.js";
|
||||
import type {
|
||||
EventBusPluginConfig,
|
||||
EventBusPluginOptions,
|
||||
} from "../types/types.js";
|
||||
|
||||
interface PluginState {
|
||||
service: EventBusService | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory — produces a typed `PluginDescriptor` for the event-bus
|
||||
* plugin.
|
||||
*
|
||||
* Zod-validates the supplied options at the boundary so invalid input
|
||||
* fails fast with `EventBusError(EVENT_BUS.INVALID_INPUT)` carrying
|
||||
* the `ZodError` as `cause`. The returned descriptor's `onInit`
|
||||
* executes the canonical 6-step lifecycle:
|
||||
*
|
||||
* 1. Resolve `app` + (optional) `openAPIRegistry` from the host
|
||||
* `ServiceRegistry`.
|
||||
* 2. Construct an `EventBus` from `@suaveplan/events` and apply
|
||||
* interceptors / back-pressure / idempotency.
|
||||
* 3. Register `eventBusService` in the `ServiceRegistry`.
|
||||
* 4. Per-tenant scoping is built into the service via
|
||||
* `tenantIsolation`.
|
||||
* 5. Install the per-request middleware that puts the
|
||||
* `ScopedEventBusHandle` on `c.set("eventBus", ...)`.
|
||||
* 6. Optionally mount the 3 HTTP routes under `routePrefix`,
|
||||
* gated by `requireAuth()` when `requireAuthForRoutes !== false`.
|
||||
*/
|
||||
export function createEventBusPlugin(
|
||||
options: EventBusPluginOptions
|
||||
): PluginDescriptor<EventBusPluginConfig> {
|
||||
const parsed = EventBusPluginOptionsSchema.safeParse(options);
|
||||
if (!parsed.success) {
|
||||
throw new EventBusError(
|
||||
EVENT_BUS_CODES.INVALID_INPUT,
|
||||
"createEventBusPlugin received invalid options",
|
||||
{ cause: parsed.error, context: { statusCode: 500 } }
|
||||
);
|
||||
}
|
||||
const resolved = parsed.data as EventBusPluginConfig;
|
||||
const state: PluginState = { service: null };
|
||||
|
||||
return definePlugin<EventBusPluginConfig>({
|
||||
name: "event-bus",
|
||||
version: "0.1.0",
|
||||
description:
|
||||
"Typed event-bus plugin for @suaveplan/api. Wraps @suaveplan/events with per-tenant topic scoping, backpressure, idempotent subscriptions, and 3 optional auth-gated OpenAPI routes.",
|
||||
defaults: resolved,
|
||||
tags: ["events", "infrastructure"],
|
||||
onInit: async (ctx) => runOnInit(ctx, resolved, state),
|
||||
onClose: async () => {
|
||||
state.service = null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function runOnInit(
|
||||
ctx: PluginContext<EventBusPluginConfig>,
|
||||
config: EventBusPluginConfig,
|
||||
state: PluginState
|
||||
): Promise<void> {
|
||||
// (1) Resolve required services from the host ServiceRegistry.
|
||||
const app = ctx.getService("app");
|
||||
if (app === undefined) {
|
||||
throw new EventBusError(
|
||||
EVENT_BUS_CODES.SERVICE_NOT_AVAILABLE,
|
||||
"event-bus plugin requires the `app` service in ServiceRegistry",
|
||||
{ context: { missing: ["app"], statusCode: 500 } }
|
||||
);
|
||||
}
|
||||
const openAPIRegistry = ctx.getService("openAPIRegistry");
|
||||
|
||||
// (2) Construct the event-bus service with the configured
|
||||
// interceptors / backpressure / idempotency.
|
||||
const service = createEventBusService({
|
||||
interceptors: config.interceptors,
|
||||
backpressure: config.backpressure,
|
||||
idempotency: config.idempotency,
|
||||
tenantIsolation: config.tenantIsolation,
|
||||
maxTopicLength: config.maxTopicLength,
|
||||
});
|
||||
state.service = service;
|
||||
|
||||
// (3) Register the eventBusService slot.
|
||||
ctx.registerService("eventBusService", service);
|
||||
|
||||
// (4) Per-tenant scoping is baked into the service when
|
||||
// `tenantIsolation` is true — no additional wiring needed.
|
||||
|
||||
// (5) Install the per-request middleware that surfaces the
|
||||
// `ScopedEventBusHandle` on `c.set("eventBus", ...)`.
|
||||
app.use(
|
||||
"*",
|
||||
requestBusMiddleware({
|
||||
service,
|
||||
tenantIsolation: config.tenantIsolation,
|
||||
})
|
||||
);
|
||||
|
||||
// (6) Optionally mount the HTTP routes.
|
||||
if (config.mountRoutes) {
|
||||
mountRoutes({
|
||||
app,
|
||||
openAPIRegistry,
|
||||
service,
|
||||
tenantIsolation: config.tenantIsolation,
|
||||
requireAuthForRoutes: config.requireAuthForRoutes,
|
||||
routePrefix: config.routePrefix,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface MountRoutesArgs {
|
||||
readonly app: OpenAPIHono;
|
||||
readonly openAPIRegistry: OpenAPIRegistry | undefined;
|
||||
readonly service: EventBusService;
|
||||
readonly tenantIsolation: boolean;
|
||||
readonly requireAuthForRoutes: boolean;
|
||||
readonly routePrefix: string;
|
||||
}
|
||||
|
||||
function mountRoutes(args: MountRoutesArgs): void {
|
||||
const {
|
||||
app,
|
||||
openAPIRegistry,
|
||||
service,
|
||||
tenantIsolation,
|
||||
requireAuthForRoutes,
|
||||
routePrefix,
|
||||
} = args;
|
||||
const entries = buildRoutes({ service, tenantIsolation });
|
||||
const prefixedEntries = entries.map((entry) => ({
|
||||
...entry,
|
||||
route: {
|
||||
...entry.route,
|
||||
path: `${routePrefix === "/" ? "" : routePrefix}${entry.route.path}`,
|
||||
},
|
||||
}));
|
||||
|
||||
if (requireAuthForRoutes) {
|
||||
const gateMount = routePrefix === "/" ? "/*" : `${routePrefix}/*`;
|
||||
app.use(gateMount, requireAuth());
|
||||
}
|
||||
|
||||
if (openAPIRegistry !== undefined) {
|
||||
const existing = openAPIRegistry
|
||||
.getRoutes()
|
||||
.filter((r) => r.path.startsWith(routePrefix));
|
||||
if (existing.length > 0) {
|
||||
throw new EventBusError(
|
||||
EVENT_BUS_CODES.ROUTE_PREFIX_COLLISION,
|
||||
`Route prefix '${routePrefix}' collides with previously registered routes`,
|
||||
{
|
||||
context: {
|
||||
prefix: routePrefix,
|
||||
existing: existing.map((r) => r.path),
|
||||
statusCode: 500,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
definePluginRoutes(prefixedEntries, {
|
||||
app,
|
||||
registry: openAPIRegistry,
|
||||
pluginName: "event-bus",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// No OpenAPI registry — mount onto Hono only.
|
||||
const sub = new Hono() as OpenAPIHono;
|
||||
for (const entry of entries) {
|
||||
const method = entry.route.method as
|
||||
| "get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "patch"
|
||||
| "delete";
|
||||
sub[method](
|
||||
entry.route.path,
|
||||
async (c) => (await entry.handler(c as never)) as Response
|
||||
);
|
||||
}
|
||||
app.route(routePrefix === "/" ? "/" : routePrefix, sub);
|
||||
}
|
||||
Reference in New Issue
Block a user