# plugin _(no submodules)_ ## Purpose Factory producing a typed `PluginDescriptor` for the SSE plugin. Wraps the `@suaveplan/api-plugin` `definePlugin` foundation with Zod-validated options at the boundary and a 6-step `onInit` lifecycle that resolves the host's Hono `app`, constructs the ring buffer / fanout / service triple, registers `sseService` in the host `ServiceRegistry`, and mounts the default streaming route. Mirrors the 3-way plug-and-play contract pioneered by `@suaveplan/api-plugin-auth` so consumers compose any combination of `@suaveplan/api-plugin-*` packages with a single `definePlugin` array in their app boot. ## Features - Zod-validated options at the factory boundary — invalid input throws `SseError(SSE_INVALID_INPUT)` carrying the `ZodError` as `cause`. - 6-step `onInit`: (1) resolve `app` from registry (or throw `SSE_SERVICE_NOT_AVAILABLE`), (2) build ring buffer + in-process fanout, (3) build `SseService`, (4) register `sseService`, (5) optionally mount `GET /:channel`, (6) defer to descriptor's `onClose` for teardown. - `onClose` terminates every open connection by calling `service.close()`; idempotent on second call. - `attachDefaultRoute: false` skips route registration so consumers can mount their own handler under any path / middleware stack. - `routePrefix: "/"` mounts at `/:channel` so callers can use SSE as their entire app surface when desired. - Defaults reflected in `descriptor.defaults` so a downstream introspection tool sees every resolved value without re-parsing. ## Usage: Basic ```ts import { createSsePlugin } from "@suaveplan/api-plugin-sse/plugin/plugin"; const plugin = createSsePlugin({ heartbeatIntervalMs: 15_000, maxConnectionsPerPrincipal: 8, }); // Pass to your @suaveplan/api registry along with other plugins. ``` ## Usage: Advanced ```ts import { createSsePlugin } from "@suaveplan/api-plugin-sse/plugin/plugin"; import { createPluginRegistry } from "@suaveplan/api-plugin"; import { Hono } from "hono"; const app = new Hono(); const registry = createPluginRegistry({ seedServices: { app }, }); await registry.register( createSsePlugin({ heartbeatIntervalMs: 15_000, retentionPerChannel: 500, maxConnectionsPerPrincipal: 4, maxTotalConnections: 5_000, queueHighWaterMark: 512, overflowPolicy: "drop-newest", attachDefaultRoute: false, // mount manually under /v1/events/:channel }) ); await registry.init(); const sse = registry.getService("sseService"); sse?.broadcast("welcome", { data: { message: "boot complete" } }); // Graceful shutdown closes every open SSE connection. process.on("SIGTERM", () => registry.shutdown()); ``` ## API Reference - `createSsePlugin(options?: SsePluginOptions): PluginDescriptor` — factory. `options` is optional; defaults are applied by `SsePluginOptionsSchema`. ## Implementation Notes - The descriptor's `onInit` captures its `service` instance in a closure so `onClose` can call `service.close()` without re-resolving the `ServiceRegistry` (the registry instance is the same across phases, but the closure is more robust to lifecycle edge cases). - Default `idGenerator` uses `globalThis.crypto.randomUUID()` when available; falls back to a timestamp + random suffix on legacy runtimes. The plugin layer is the canonical place to apply this default — `SseService` itself takes the generator verbatim. - `routePrefix === "/"` is special-cased to avoid a double-slash in the mount path; the plugin uses `app.get("/:channel", handler)` directly in that case. - `onClose` checks `state.service` for `undefined` so it is safe to call before `onInit` has completed (e.g. when `onInit` throws and the registry sweeps up).