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

5.7 KiB

@suaveplan/api-ws — abstract WebSocket Connection, Registry, Bus, Broker contract

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

Why

Persistent connections are first-class in the API foundation (design §11). Plugins declare WebSocket endpoints in their manifest; the host runs the engine, manages connection lifecycle, fans out subscriptions. Concrete engines (api-ws-bun) and multi-host brokers (api-ws-broker-redis, api-ws-broker-in-memory) plug behind a stable contract. This package owns that contract: Connection, ConnectionRegistry, SubscriptionBus, WsBroker, plus the cross-tenant gate that requires ws.broadcast-cross-tenant capability.

Per design §11.2: "conn.subscribe(topic, { tenant?, filter? }) registers the connection with the host's subscription bus." Per §11.3: brokers make bus.broadcast visible across host replicas without migrating connections. Both shapes live here.

What changes

Net-new package. Contract types + Zod schemas for the wire envelope + the cross-tenant capability gate + the conformance harness.

  • ADDED: ./connection — abstract WsConnection: principal, tenantId, subscribe(topic, opts), unsubscribe(topic), send(message), close(code, reason), ack(id), id, lastEventId, state (open | closing | closed).
  • ADDED: ./connection-registry — abstract ConnectionRegistry: add(conn), remove(conn), lookupByTenant(tenant), lookupByTopic(topic, tenant?), countByTenant(tenant). Backed by a multi-index.
  • ADDED: ./subscription-bus — abstract SubscriptionBus: subscribe(conn, topic, { tenant?, filter? }), unsubscribe(conn, topic), broadcast(topic, { tenant, payload, filter? }), asyncIterator(topic, opts).
  • ADDED: ./broker — abstract WsBroker for multi-host fan-out: publish(topic, envelope), subscribe(topic, handler), dispose(). Connections are NOT migrated across hosts — the broker only makes broadcasts visible.
  • ADDED: ./envelope — Zod schema for the wire WsEnvelope ({ id, type, topic?, payload? }); type-narrowing helpers.
  • ADDED: ./backpressure — BackpressurePolicy: bounded send queue (maxQueuedMessages, default 256); eviction emits BACKPRESSURE_EVICTED close.
  • ADDED: ./heartbeat — HeartbeatPolicy: heartbeatMs (default 30000), two missed pongs → close ABNORMAL.
  • ADDED: ./reconnect-token — ReconnectToken HMAC envelope ({ tenant, principalId, lastEventId, exp }); 5-min TTL constant.
  • ADDED: ./capability-id — adds "ws" AND "ws.broadcast-cross-tenant" to the CapabilityId union via type-level augmentation.
  • ADDED: ./error-codes — frozen WS_CODES: HANDSHAKE_FAILED, PRINCIPAL_EXPIRED, BACKPRESSURE_EVICTED, HEARTBEAT_FAILED, CONNECTION_CAP_EXCEEDED, CROSS_TENANT_BROADCAST_DENIED, RECONNECT_TOKEN_INVALID, BROKER_UNAVAILABLE.
  • ADDED: ./testing — runWsEngineConformance(factory) reusable harness.

CapabilityId additions

This package contributes "ws" and "ws.broadcast-cross-tenant" to the CapabilityId union. The cross-tenant id is a separate capability so the manifest declares it explicitly and the host audit-logs every use (design §11.2 "audit-logged on every use").

Out of scope

  • Concrete engines (api-ws-bun) — Stage 7.
  • Concrete brokers (api-ws-broker-redis, api-ws-broker-in-memory) — Stages 5 and 7.
  • The graphql-ws protocol wiring (subscriptions over WebSocket) — that lives in api-graphql engine adapters, which read this package's SubscriptionBus.

Source mapping

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

Requirement Design doc section
WsConnection shape + handshake flow §11.1
SubscriptionBus.broadcast + filter predicate §11.2
Multi-host fan-out via broker §11.3
Backpressure (maxQueuedMessages: 256) §11.4
Heartbeat (heartbeatMs: 30000, two missed pongs) §11.4
Per-tenant caps (maxConnectionsPerTenant) §11.4
Reconnect tokens (HMAC, 5-min TTL) §11.5
Cross-tenant gate (ws.broadcast-cross-tenant) §11.2

Requirements with no source line

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

Verification approach

  • src/connection/connection.spec-conformance.test.ts — expectTypeOf<WsConnection>() declares every documented method.
  • src/connection-registry/connection-registry.test.ts — add/remove/lookup; multi-index correctness; count-by-tenant.
  • src/subscription-bus/subscription-bus.test.ts — subscribe/unsubscribe; broadcast fans out to matching (topic, tenant); filter predicate applied per-connection.
  • src/subscription-bus/cross-tenant.test.ts — broadcast WITHOUT ws.broadcast-cross-tenant is rejected with WS.CROSS_TENANT_BROADCAST_DENIED when tenant arg is omitted or differs from emitter's tenant.
  • src/broker/broker.spec-conformance.test.ts — WsBroker interface declared.
  • src/envelope/envelope.test.ts — Zod accept/reject suite over the wire envelope.
  • src/backpressure/backpressure.test.ts — queue saturation triggers BACKPRESSURE_EVICTED.
  • src/heartbeat/heartbeat.test.ts — two missed pongs → close ABNORMAL.
  • src/reconnect-token/reconnect-token.test.ts — HMAC verify, TTL boundary.
  • src/capability-id/capability-id.spec-conformance.test.ts — both ids in CapabilityId.
  • tests/dep-graph.test.ts — deps: @suaveplan/api-types, @suaveplan/error, @suaveplan/types, zod. No transport deps.

Migration impact

None — additive only.

Impact

Affected dependents

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