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

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 z from "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 event is 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"