Files
skills/docs/api-stack/api/api-plugin-sse/src/services/service.md
2026-05-29 08:20:56 +01:00

4.7 KiB

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<SseEvent> 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

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

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<void> — 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<string, number> 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.