first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
# plugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 <routePrefix>/: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<SsePluginConfig>` — 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).