Files
suaveplan/openspec/archive/2026-05-25-api-sse/proposal.md
2026-05-27 23:29:53 +01:00

4.3 KiB

@suaveplan/api-sse — abstract SSE Connection + Broker + Last-Event-Id resume contract

Package: @suaveplan/api-sse Status: PROPOSED Type: new-package Stage: 1 (Contracts) — see umbrella openspec/changes/api-plugin-foundation/

Why

SSE is the simpler cousin of WebSocket (design §11.6): same subscription model, no client→server channel, no ack semantics. Useful for activity feeds, dashboards, notifications, and as a fallback on networks that block WebSocket. Plugins declare SSE endpoints in the manifest; the host runs the engine, manages connection lifecycle, fans out subscriptions, replays buffered events on Last-Event-Id reconnect. Concrete engines (api-sse-bun) plug behind this contract.

Per design §11.6: "Reuses the same bus + broker; just a different transport." The bus shape is shared with api-ws's SubscriptionBus — this package re-declares the SSE-specific connection/broker surface and the Last-Event-Id replay contract.

What changes

Net-new package. Contract types + the replay-buffer interface + the conformance harness.

  • ADDED: ./connection — abstract SseConnection: id, principal, tenantId, lastEventId: string | null, state, send(event: SseEvent), close().
  • ADDED: ./event — SseEvent: { id: string, event?: string, data: string, retry?: number } (standard SSE event field shape).
  • ADDED: ./broker — abstract SseBroker: same shape as WsBroker (publish/subscribe/dispose) but distinct type so engines can supply different transports.
  • ADDED: ./replay-buffer — abstract ReplayBuffer: per-(tenant, topic) ring buffer (eventBufferSize, eventBufferMs); append(event), since(lastEventId), evictExpired(now). The host's Last-Event-Id reconnect contract reads from here.
  • ADDED: ./connection-registry — abstract SseConnectionRegistry: same multi-index shape as ConnectionRegistry in api-ws but typed for SseConnection.
  • ADDED: ./capability-id — adds "sse" to the CapabilityId union via type-level augmentation.
  • ADDED: ./error-codes — frozen SSE_CODES: HANDSHAKE_FAILED, PRINCIPAL_EXPIRED, BUFFER_OVERFLOW, LAST_EVENT_ID_UNKNOWN, CONNECTION_CAP_EXCEEDED, BROKER_UNAVAILABLE.
  • ADDED: ./testing — runSseEngineConformance(factory) reusable harness.

CapabilityId additions

This package contributes "sse" to the CapabilityId union. (No "broadcast-cross-tenant" SSE variant — SSE never accepts client input, so the host owns broadcast tenant scoping uniformly.)

Out of scope

  • Concrete api-sse-bun engine — Stage 7.
  • WebSocket — separate contract in api-ws.
  • The shared SubscriptionBus semantics — re-used type lives in api-ws; this package's broker is engine-side.

Source mapping

Genesis-original. Source of design intent: docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md.

Requirement Design doc section
SSE simpler-cousin shape §11.6
Last-Event-Id replay buffer §11.5 (last paragraph) + §11.6
OTel surface (sse.connections, sse.events_sent_total) §11.7
Worker-tier proxying §11.8

Requirements with no source line

Requirement Justification
runSseEngineConformance harness Stage-5 deliverable (§13.4).

Verification approach

  • src/connection/connection.spec-conformance.test.ts — expectTypeOf<SseConnection>() declares every documented field.
  • src/event/event.test.ts — SseEvent shape; id/event/data/retry field handling.
  • src/broker/broker.spec-conformance.test.ts — SseBroker interface declared.
  • src/replay-buffer/replay-buffer.test.ts — append, query since(id), time-based eviction, size cap.
  • src/replay-buffer/last-event-id.test.ts — unknown Last-Event-Id → SSE.LAST_EVENT_ID_UNKNOWN AND fall back to live subscription (no replay).
  • src/connection-registry/connection-registry.test.ts — multi-index lookup; idempotent remove.
  • src/capability-id/capability-id.spec-conformance.test.ts — "sse" in CapabilityId.
  • tests/dep-graph.test.ts — deps: @suaveplan/api-types, @suaveplan/error, @suaveplan/types, @suaveplan/api-ws (for shared SubscriptionBus type only, peer dep), zod.

Migration impact

None — additive only.

Impact

Affected dependents

  • (none) — [x] no action needed (reason: net-new; engines land Stage 7)