# `@suaveplan/api-sse-bun` — `Bun.serve` Server-Sent Events engine **Package**: `@suaveplan/api-sse-bun` **Status**: PROPOSED **Type**: new-package **Stage**: 7 (GraphQL + WebSocket / SSE engines) — see umbrella `openspec/changes/api-plugin-foundation/` ## Why Per design doc §11.6 SSE is the simpler cousin of WebSocket — useful for activity feeds, dashboards, and notifications where no client-to-server channel and no ack semantics are needed. SSE falls back gracefully on networks that block WebSocket (corporate proxies, restrictive firewalls), making it a deliberately important second transport. This package implements the abstract `api-sse` contract on top of `Bun.serve` and the same broker abstraction as `api-ws-bun`, so multi-host fan-out is handled by the same Redis broker (`api-ws-broker-redis`) without duplication. Without this adapter the foundation has no production SSE story and falls back to single-transport-only deployments. ## What changes Net-new package depending on `@suaveplan/api-types`, `@suaveplan/api-sse` (abstract contract), `@suaveplan/api-router` (HTTP endpoint mount), `@suaveplan/error`, `@suaveplan/logger`, `@suaveplan/opentelemetry`, and `zod`. - ADDED: `createBunSseEngine(opts)` — factory returning the engine value implementing `@suaveplan/api-sse`'s contract. - ADDED: `BunSseEngineOptions` — Zod-validated config: `eventBufferSize` (default 256 events per `(tenant, topic)`), `eventBufferMs` (default 300000 = 5 min, pairs with WS reconnect TTL), `keepAliveCommentMs` (default 15000 — emits SSE comments to prevent intermediary timeouts), `maxConnectionsPerTenant` (default 1000), `compressionEnabled` (default `true` — uses `Content-Encoding: gzip` when the client accepts it), `retryMs` (default 3000 — written into `retry:` field of the SSE stream so clients back off appropriately). - ADDED: Endpoint mount — plugin's manifested SSE endpoints (`sse: [{ path, auth, subscriptions }]`) are mounted via `api-router` as `GET ` returning `Content-Type: text/event-stream`. - ADDED: `Last-Event-Id` resume — when the client supplies `Last-Event-Id` header (HTTP standard), the engine consults the event buffer (broker-backed when Redis broker wired, in-memory otherwise) and replays events with id > supplied value before resuming live delivery. - ADDED: Per-`(tenant, topic)` event buffer — for single-host deployments, in-memory ring buffer of `eventBufferSize` events with `eventBufferMs` TTL. For multi-host deployments, delegates to the wired broker (`api-ws-broker-redis` exposes the same buffer surface; SSE consumes it identically to WS). - ADDED: Keep-alive comments — `:keepalive\n\n` emitted every `keepAliveCommentMs` to prevent intermediary proxy timeouts. This is invisible to clients. - ADDED: Subscription model — `conn.subscribe(topic, { tenant?, filter? })` registers against the host's subscription bus (same bus used by WS). `bus.broadcast` triggers fan-out to SSE connections matching `(topic, tenant)`. - ADDED: Per-tenant cap — `maxConnectionsPerTenant` enforced at request time; exceeding returns 429 with `SSE_BUN_TENANT_CAP_EXCEEDED`. - ADDED: `SSE_BUN_*` error codes: `SSE_BUN_HANDSHAKE_REJECTED`, `SSE_BUN_TENANT_CAP_EXCEEDED`, `SSE_BUN_BUFFER_MISS`, `SSE_BUN_EVENT_TOO_LARGE`, `SSE_BUN_LAST_EVENT_ID_INVALID`, `SSE_BUN_COMPRESSION_FAILED`, `SSE_BUN_STREAM_INTERRUPTED`, `SSE_BUN_PRINCIPAL_EXPIRED`, `SSE_BUN_BROKER_UNAVAILABLE`, `SSE_BUN_INVALID_TOPIC`. - ADDED: OTel surface from design §11.7 — `sse.connections { plugin, path, tenant }` observable gauge, `sse.events_sent_total { plugin, topic }` counter, plus this package adds `sse.handshake.duration_ms`, `sse.buffer_replays_total { hit }`, `sse.compression_ratio` histogram. - ADDED: `./testing` subpath — re-export of `runSseEngineConformance` from `api-sse/testing` bound to `createBunSseEngine`. ## Out of scope - A non-Bun SSE engine — the contract lives in `api-sse`; other engines can implement it. - Cross-host fan-out logic — delegated to `api-ws-broker-redis` (which is also the SSE broker, per §11.6). - Bi-directional channels — SSE is server→client only by design. - Browser polyfills — modern browsers + recent Node clients support SSE natively. ## Source mapping Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`. | Requirement | Design doc section | |---|---| | SSE endpoint declared in manifest | §3 (`sse` block) + §11.6 | | `Last-Event-Id` resume | §11.5 (paragraph 3) + §11.6 | | Event buffer per `(tenant, topic)` with size + ttl | §11.5 (paragraph 3) | | Shared broker with WS for multi-host fan-out | §11.6 | | Same subscription bus as WS | §11.6 | | OTel `sse.connections` gauge + `sse.events_sent_total` counter | §11.7 | | Graceful fall-back when WS blocked | §11.6 | ## Verification approach - `src/engine/bun-sse-engine.test.ts` — handshake, event emission framing, keep-alive comment cadence, principal frozen at handshake. - `src/engine/bun-sse-engine.spec-conformance.test.ts` — runs `runSseEngineConformance("bun", createBunSseEngine)`. - `src/last-event-id/last-event-id.test.ts` — supply `Last-Event-Id: 7`, assert events 8..N replay before live delivery. - `src/event-buffer/event-buffer.test.ts` — in-memory mode: ring-buffer cap, TTL eviction. Broker mode (integration, 30000ms): delegation to broker-backed buffer. - `src/limits/tenant-cap.test.ts` — 1001st connection rejected with 429. - `src/keep-alive/keep-alive.test.ts` (FakeClock) — `:keepalive` comments emitted every `keepAliveCommentMs`. - `tests/integration/end-to-end.test.ts` (30000ms) — real `Bun.serve`, real SSE client, full subscribe → broadcast → keep-alive → disconnect → reconnect-with-Last-Event-Id. - `tests/integration/multi-host-fan-out.test.ts` (60000ms) — two SSE engines behind `api-ws-broker-redis`; broadcast from one delivers on the other. ## Migration impact None — additive only.