Files
2026-06-09 21:06:27 +01:00

9.1 KiB

@suaveplan/api-sse-bun - Bun.serve concrete engine for @suaveplan/api-sse: text/event-stream framing, Last-Event-Id replay, per-(tenant,topic) ring buffer with broker delegation, gzip streaming, keep-alive comments, per-tenant connection cap, shared subscription bus with WS engine, and the runSseEngineConformance harness bound to createBunSseEngine.

Bun.serve-backed Server-Sent Events engine for the @suaveplan/api plugin system. Implements the abstract @suaveplan/api-sse contract and passes runSseEngineConformance end-to-end. Lands at api-plugin-foundation umbrella.

SSE complements api-ws-bun: where WebSocket carries bi-directional duplex traffic, SSE is the simpler server→client stream useful for activity feeds, dashboards, notifications, and any payload pattern that does not need client→server acks. SSE also falls back gracefully on networks that block WebSocket (corporate proxies, restrictive firewalls) so a single broadcast can reach clients via either transport.

Documentation tree

  • compression — The compression module owns the engine's optional gzip path. SSE streams compress well — UTF-8 text framed with repea…
    • compression — The compression module owns the engine's optional gzip path. SSE streams compress well — UTF-8 text framed with repea…
  • engine — The bun-sse-engine module exports createBunSseEngine, the factory the host calls to mount Server-Sent Events on top…
    • bun-sse-engine — The bun-sse-engine module exports createBunSseEngine, the factory the host calls to mount Server-Sent Events on top…
  • error-codes — The error-codes module defines the stable SSE_BUN_* error registry used across the Bun SSE engine. Every failure mo…
    • error-codes — The error-codes module defines the stable SSE_BUN_* error registry used across the Bun SSE engine. Every failure mo…
  • event-buffer — The event-buffer module owns the per-(tenant, topic) replay storage the engine consults on Last-Event-Id reconnec…
    • event-buffer — The event-buffer module owns the per-(tenant, topic) replay storage the engine consults on Last-Event-Id reconnec…
  • handshake — The handshake module implements the 7-step SSE handshake pipeline: topic validation, Last-Event-Id parsing, authent…
    • handshake — The handshake module implements the 7-step SSE handshake pipeline: topic validation, Last-Event-Id parsing, authent…
  • instruments — The instruments module wires the engine's OpenTelemetry surface. Every metric mandated by design doc §11.7 — connecti…
    • instruments — The instruments module wires the engine's OpenTelemetry surface. Every metric mandated by design doc §11.7 — connecti…
  • keep-alive — The keep-alive module emits invisible SSE comment lines on a fixed cadence so intermediary proxies (corporate gateway…
    • keep-alive — The keep-alive module emits invisible SSE comment lines on a fixed cadence so intermediary proxies (corporate gateway…
  • last-event-id — The last-event-id module parses and validates the Last-Event-Id header browsers send automatically on EventSource…
    • last-event-id — The last-event-id module parses and validates the Last-Event-Id header browsers send automatically on EventSource…
  • limits — The tenant-cap module owns the per-tenant SSE connection counter the engine consults during handshake. A tenant that…
    • tenant-cap — The tenant-cap module owns the per-tenant SSE connection counter the engine consults during handshake. A tenant that…
  • options — The options module declares the public configuration surface accepted by createBunSseEngine and exposes `resolveOpt…
    • options — The options module declares the public configuration surface accepted by createBunSseEngine and exposes `resolveOpt…
  • stream — The sse-stream module owns the byte-level framing for the SSE wire protocol. Every event the engine pushes to a clien…
    • sse-stream — The sse-stream module owns the byte-level framing for the SSE wire protocol. Every event the engine pushes to a clien…
  • subscription — The subscription-bus module owns the shared fan-out surface both the WS and SSE engines plug into. The host construct…
    • subscription-bus — The subscription-bus module owns the shared fan-out surface both the WS and SSE engines plug into. The host construct…
  • testing — The testing module exposes runBunSseEngineConformance — a thin binding that hands createBunSseEngine to the abstr…
    • conformance — The testing module exposes runBunSseEngineConformance — a thin binding that hands createBunSseEngine to the abstr…

Modules

  • engine/bun-sse-engine — createBunSseEngine(options) factory returning the public engine value; wires options + buffer + bus + cap + instruments + handshake into one cohesive instance.
  • handshake/handshake — 7-step pipeline (topic validation, Last-Event-Id parse, auth, cap acquire, compression negotiate, prologue + replay, subscribe + keep-alive).
  • stream/sse-stream — WHATWG-compliant byte-level framing for id/event/data/retry/:comment lines.
  • event-buffer/event-buffer — in-memory ring buffer + broker delegate behind one EventBuffer surface.
  • subscription/subscription-bus — shared topic + tenant bus the WS engine reuses.
  • limits/tenant-cap — per-tenant connection counter enforced at handshake time.
  • compression/compression — Accept-Encoding negotiation + node:zlib streaming with sync-flush after every chunk.
  • keep-alive/keep-alive — periodic :keepalive comment scheduler with injectable timer for FakeClock tests.
  • last-event-id/last-event-id — header parser enforcing decimal format.
  • instruments/instruments — OTel meter bundle: connections gauge, events-sent counter, handshake-duration histogram, replay-hit/miss counter, compression-ratio histogram.
  • error-codes/error-codes — 10 stable SSE_BUN_* codes + SseBunError subclass.
  • testing/conformance — runBunSseEngineConformance() binding to the abstract runSseEngineConformance harness.

Defaults

Knob Default Notes
eventBufferSize 256 Per-(tenant, topic) ring capacity.
eventBufferMs 300_000 5 min — pinned to WS reconnect window.
keepAliveCommentMs 15_000 15 s — inside the 30 s proxy idle window.
maxConnectionsPerTenant 1_000 Per-tenant cap.
maxEventBytes 65_536 64 KiB per encoded frame.
compressionEnabled true Honours Accept-Encoding: gzip.
retryMs 3_000 Initial retry: directive.

Boundary vs api-ws-bun

SSE is server→client only. It does not carry client→server messages, acks, or close codes — the client opens a long-lived GET, the server streams text/event-stream until the client disconnects or the server tears down. Use SSE when you need a uni-directional broadcast stream that defaults to working through restrictive intermediaries; use WS when you need duplex semantics, binary frames, or per-message acks. The two engines share the same subscription bus and event-buffer surface so the choice is per-route, not per-deploy.

Install + use

bun add @suaveplan/api-sse-bun
import { createBunSseEngine } from "@suaveplan/api-sse-bun";

const engine = createBunSseEngine({
    authenticate: async (req) => ({
        tenantId: (req.headers.get("x-tenant") ?? "anon") as never,
        principal: null,
    }),
});

Bun.serve({
    port: 0,
    async fetch(request) {
        const url = new URL(request.url);
        if (url.pathname === "/sse/audit") {
            return engine.acceptRequest({
                request: {
                    headers: request.headers,
                    url: request.url,
                    method: request.method,
                },
                path: url.pathname,
                topic: "audit",
            });
        }
        return new Response("not found", { status: 404 });
    },
});

Status

Stage-7 keystone. Conformance is pinned by the abstract runSseEngineConformance harness from @suaveplan/api-sse; this package's runBunSseEngineConformance() re-runs it bound to createBunSseEngine.