65 lines
5.9 KiB
Markdown
65 lines
5.9 KiB
Markdown
# `@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 <path>` 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.
|