5.6 KiB
Capability Spec Delta: api-sse (api-sse)
Package: @suaveplan/api-sse
Status: PROPOSED
Every requirement carries a
**Verified by:**line.
ADDED Requirements
Requirement: SseConnection contract
The package SHALL export SseConnection with: id (ULID), principal: Principal, tenantId: TenantId, lastEventId: string | null, state: "open" | "closing" | "closed", methods send(event: SseEvent), close(). SSE is uni-directional (host → client); there is no onMessage.
Scenario: every documented field present
- WHEN
expectTypeOf<SseConnection>()is checked - THEN every documented field is declared
Verified by: src/connection/connection.spec-conformance.test.ts::"SseConnection declares every documented field"
Requirement: import z from "zod" everywhere
Every Zod import MUST use the default form.
Scenario: no named import of z
- WHEN the test greps
import zfrom"zod" - THEN zero matches
Verified by: tests/zod-import-form.test.ts::"every src file uses default-import z"
Requirement: SseEvent shape
The package SHALL export sseEventSchema: z.ZodSchema<SseEvent> describing { id: string, event?: string, data: string, retry?: number }. Unknown keys MUST be rejected.
Scenario: minimal event parses
- WHEN
{ id: "1", data: "hello" }is parsed - THEN it succeeds AND
eventis undefined (not defaulted)
Scenario: unknown key rejected
- WHEN
{ id: "1", data: "x", extra: 1 }is parsed - THEN Zod reports an issue at
extra
Verified by: src/event/event.test.ts::"minimal event parses" AND src/event/event.test.ts::"unknown keys rejected"
Requirement: SseBroker contract
The package SHALL export SseBroker with publish(topic, envelope), subscribe(topic, handler), dispose(). Same shape as WsBroker but a distinct type so engines can supply different transports.
Scenario: every method present
- WHEN
expectTypeOf<SseBroker>()is checked - THEN all three methods are declared
Verified by: src/broker/broker.spec-conformance.test.ts::"SseBroker declares publish subscribe dispose"
Requirement: Replay buffer with Last-Event-Id resume
The package SHALL export ReplayBuffer with: append(event: SseEvent), since(lastEventId: string): readonly SseEvent[], evictExpired(now: number). The buffer is per (tenant, topic) and bounded by eventBufferSize (default 256) and eventBufferMs (default 30000). since SHALL return every event whose id sorts AFTER lastEventId in append order. An unknown lastEventId (evicted or never seen) MUST surface SSE.LAST_EVENT_ID_UNKNOWN so the engine can fall back to live subscription.
Scenario: since(id) returns events after the marker
- WHEN events A, B, C are appended in order AND
since(B.id)is called - THEN the result is
[C]
Scenario: unknown id surfaces error code
- WHEN
since("never-seen-id")is called - THEN the call throws
SSE.LAST_EVENT_ID_UNKNOWN
Scenario: time-based eviction drops expired entries
- WHEN the fake clock advances past
eventBufferMs - THEN the expired entries are dropped on the next
evictExpired(now)
Verified by: src/replay-buffer/replay-buffer.test.ts::"since returns events after marker" AND src/replay-buffer/last-event-id.test.ts::"unknown lastEventId surfaces LAST_EVENT_ID_UNKNOWN" AND src/replay-buffer/replay-buffer.test.ts::"time-based eviction drops expired entries"
Requirement: Connection registry multi-index
The package SHALL export SseConnectionRegistry with O(1) lookup by tenantId and by (topic, tenantId). add, remove, lookupByTenant, lookupByTopic, countByTenant. remove MUST be idempotent.
Scenario: lookup returns matching set
- WHEN 2 SSE connections subscribe to tenant T
- THEN
lookupByTenant("T").length === 2
Scenario: remove is idempotent
- WHEN
remove(conn)is called twice - THEN the second call returns without throwing
Verified by: src/connection-registry/connection-registry.test.ts::"lookupByTenant returns matching set" AND src/connection-registry/connection-registry.test.ts::"remove is idempotent"
Requirement: CapabilityId augmentation contributes "sse"
The package SHALL augment the CapabilityId union to include "sse".
Scenario: sse is assignable to CapabilityId
- WHEN
expectTypeOf<"sse">().toMatchTypeOf<CapabilityId>()is checked - THEN the assertion passes
Verified by: src/capability-id/capability-id.spec-conformance.test.ts::"sse is a valid CapabilityId after augmentation"
Requirement: Error codes registry frozen
The package SHALL export SSE_CODES containing exactly: HANDSHAKE_FAILED, PRINCIPAL_EXPIRED, BUFFER_OVERFLOW, LAST_EVENT_ID_UNKNOWN, CONNECTION_CAP_EXCEEDED, BROKER_UNAVAILABLE.
Scenario: every code reachable
- WHEN the test enumerates
SSE_CODES - THEN every code has a production throw site
Verified by: src/error-codes/error-codes.spec-conformance.test.ts::"every SSE_CODES entry thrown by production path"
Requirement: Conformance harness for engines
The package SHALL export runSseEngineConformance({ factory, label }). Every concrete SSE engine MUST pass.
Scenario: harness covers handshake, send, last-event-id replay, eviction
- WHEN the harness runs against a stub engine
- THEN test categories exist for each documented behavior
Verified by: src/testing/conformance.test.ts::"harness covers handshake send replay eviction"