# service _(no submodules)_ ## Purpose Core SSE service registered under the `sseService` slot in the host `ServiceRegistry`. Owns subscription lifecycle (id assignment, per-principal and global caps, abort-signal threading), ring-buffer append on broadcast, fanout delivery to live subscribers, and clean termination of every active iterator on `close`. Route handlers consume this service via `ctx.getService("sseService")` and never touch the underlying buffer/fanout directly. ## Features - Per-subscription `AsyncIterableIterator` that resolves the next event from a backlog queue or a pending promise — so callers can `for await` without polling. - `Last-Event-ID` replay: on subscribe with `lastEventId`, the service pre-fills the backlog from the ring buffer before forwarding live events. - Global `maxTotalConnections` and per-principal `maxConnectionsPerPrincipal` caps with typed errors. - `AbortSignal` threading: client disconnect closes the subscription deterministically; pre-aborted signals are handled via `queueMicrotask`. - `close(channel?)` terminates every iterator (optionally restricted to a single channel) and rejects further `subscribe`/`broadcast` calls with `SseError(SSE_CLOSED)`. - `stats()` returns a live snapshot of active counts globally and per channel. - Every public failure path raises a typed `SseError`; `cause` chains preserve the underlying `ZodError` when relevant. ## Usage: Basic ```ts import { createSseService } from "@suaveplan/api-plugin-sse/services/service"; import { createRingBuffer } from "@suaveplan/api-plugin-sse/buffer/ring-buffer"; import { createInProcessFanout } from "@suaveplan/api-plugin-sse/fanout/in-process"; const service = createSseService({ buffer: createRingBuffer(100), fanout: createInProcessFanout(), idGenerator: () => crypto.randomUUID(), maxConnectionsPerPrincipal: 16, maxTotalConnections: 10_000, }); const handle = service.subscribe({ channel: "notifications" }); service.broadcast("notifications", { data: { kind: "ping" } }); for await (const event of handle.events) { console.log(event); break; } await handle.close(); ``` ## Usage: Advanced ```ts import { createSseService } from "@suaveplan/api-plugin-sse/services/service"; const service = createSseService({ /* ... */ }); // Subscribe with Last-Event-ID resume and an abort signal. const controller = new AbortController(); const handle = service.subscribe({ channel: "notifications", lastEventId: "evt-42", principalId: "user-123", signal: controller.signal, }); // Disconnect on user navigation. window.addEventListener("beforeunload", () => controller.abort()); try { for await (const event of handle.events) { renderEvent(event); } } catch (err) { // Abort signal will resolve the iterator with done:true, not throw. // Errors here come from custom listener implementations. } console.log(service.stats()); // { activeConnections: 1, perChannel: { notifications: 1 } } ``` ## API Reference - `interface CreateSseServiceOptions` — `{ buffer, fanout, idGenerator, maxConnectionsPerPrincipal, maxTotalConnections }`. - `createSseService(options: CreateSseServiceOptions): SseService` — constructs the service. - `interface SseService`: - `broadcast(channel: string, event: SseEventInput): SseEvent` — validates the event, appends to buffer, publishes via fanout. Returns the resolved event (with generated id when not supplied). - `subscribe(args: SubscribeArgs): SubscriptionHandle` — enforces caps, replays from `lastEventId` if supplied, threads `signal`. Returns a handle exposing the iterator and a `close()`. - `close(channel?: string): Promise` — terminates every active subscription (optionally restricted to a single channel). Subsequent `broadcast` / `subscribe` calls raise `SSE_CLOSED` when called without a channel. - `stats(): SseStats` — snapshot `{ activeConnections, perChannel }`. ## Implementation Notes - The iterator's `return()` method is honored so `for await (const ev of handle.events) { break }` cleans up the pending promise rather than leaking it. - Per-principal counts are tracked in a separate `Map` for O(1) cap enforcement. The map is pruned when a principal's count returns to zero. - The fanout listener captures `sub.closed` inside the closure so deliveries after `detach` are silent no-ops — no race between unsubscribe and the next `publish`. - `close()` without a channel sets a `closedAll` latch so subsequent operations fail fast rather than racing the pending teardown. - The `idGenerator` is intentionally caller-provided so tests can use deterministic ids; the plugin layer supplies `crypto.randomUUID()` as the default.