Files
2026-05-27 23:29:53 +01:00

143 lines
9.1 KiB
Markdown

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