Files
suaveplan/openspec/archive/2026-05-12-enterprise-network-websockets/proposal.md
2026-05-27 23:29:53 +01:00

5.8 KiB

OpenSpec Change: enterprise-network-websockets

  • Change ID: enterprise-network-websockets
  • Package: @suaveplan/network-websockets
  • Category: universal/network
  • Target path: universal/network/network-websockets
  • Status: SUPERSEDED-BY-GENESIS-WAVE
  • Date: 2026-04-24

Summary

Deliver @suaveplan/network-websockets on the genesis branch from scratch at enterprise quality. The package provides WebSocket protocol utilities: typed message framing, connection lifecycle management (connect/disconnect/reconnect), heartbeat/ping-pong, backpressure-aware message queuing, multiplexing over a single WebSocket connection, and injectable transport adapters. Genesis adds structured errors via @suaveplan/error, Zod schemas for all message and option types, OTel instrumentation, AbortSignal on async operations, retry/reconnect via @suaveplan/retry, event-bus integration for connection state changes, and property-based tests for message framing invariants.


Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Base class for NetworkWebsocketsError; stable error codes
@suaveplan/types (workspace:^) LoggerLike, Clock, EventBusLike
@suaveplan/events (workspace:^) Event bus for connection state changes
@suaveplan/logger (workspace:^) Structured logging
@suaveplan/retry (workspace:^) Reconnect with exponential backoff
@suaveplan/serialize (workspace:^) Message serialization (JSON/binary)
zod (catalog:forms) Zod schemas for all public types
@opentelemetry/api (catalog:observability) Spans + counters on connect/message/disconnect
tslib (catalog:build) Runtime helpers

Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).


Public API surface

Core

function createWebSocketClient(options: WebSocketClientOptions): WebSocketClient

interface WebSocketClient {
  readonly id: string
  readonly url: string
  readonly state: ConnectionState
  connect(signal?: AbortSignal): Promise<void>
  disconnect(code?: number, reason?: string): void
  send(message: WebSocketMessage, signal?: AbortSignal): Promise<void>
  subscribe<T>(channel: string, handler: MessageHandler<T>): () => void
  on(event: ConnectionEvent, handler: (data: unknown) => void): () => void
}

Types

type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'closed'
type ConnectionEvent = 'connect' | 'disconnect' | 'reconnect' | 'error' | 'message'
type MessageHandler<T> = (message: T, meta: MessageMeta) => void

interface WebSocketMessage {
  channel?: string
  type: string
  payload: unknown
  id?: string
  correlationId?: string
}

interface MessageMeta {
  receivedAt: number
  channel?: string
  type: string
}

interface WebSocketClientOptions {
  url: string
  protocols?: string[]
  reconnect?: ReconnectOptions
  heartbeat?: HeartbeatOptions
  maxQueueSize?: number
  serialization?: 'json' | 'msgpack'
  logger?: LoggerLike
  clock?: Clock
  eventBus?: EventBusLike
  adapter?: WebSocketAdapter
}

interface ReconnectOptions {
  enabled?: boolean
  maxAttempts?: number
  initialDelayMs?: number
  maxDelayMs?: number
}

interface HeartbeatOptions {
  enabled?: boolean
  intervalMs?: number
  timeoutMs?: number
}

Transport adapter

interface WebSocketAdapter {
  connect(url: string, protocols?: string[]): WebSocketHandle
}

interface WebSocketHandle {
  readonly readyState: number
  send(data: string | Uint8Array): void
  close(code?: number, reason?: string): void
  onopen: (() => void) | null
  onclose: ((code: number, reason: string) => void) | null
  onmessage: ((data: string | Uint8Array) => void) | null
  onerror: ((error: Error) => void) | null
}

Error codes

type NetworkWebsocketsErrorCode =
  | 'WS_CONNECTION_FAILED'
  | 'WS_SEND_FAILED'
  | 'WS_QUEUE_FULL'
  | 'WS_PROTOCOL_ERROR'
  | 'WS_TIMEOUT'
  | 'WS_ABORT'

function registerNetworkWebsocketsErrorCodes(): void

Genesis additions

  1. Structured NetworkWebsocketsError extending SuaveplanError with stable WS.* codes
  2. Zod schemas for WebSocketClientOptions, WebSocketMessage, ReconnectOptions, HeartbeatOptions
  3. Injectable WebSocketAdapter — decouple from native WebSocket for testing
  4. Multiplexed channels — subscribe(channel, handler) routes messages by message.channel
  5. Backpressure queue — bounded send queue; WS_QUEUE_FULL when maxQueueSize exceeded
  6. Heartbeat/ping-pong — injectable interval via Clock
  7. Reconnect via @suaveplan/retry with configurable backoff
  8. OTel instrumentation — span ws.connect, counter ws.messages_sent_total, ws.messages_received_total, histogram ws.message_size_bytes

File layout

universal/network/network-websockets/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts
    types/
      common.ts
      common.md
    core/
      client/
        client.ts
        client.md
        client.types.ts
        client.test.ts
        index.ts
      queue/
        queue.ts
        queue.md
        queue.test.ts
        index.ts
      heartbeat/
        heartbeat.ts
        heartbeat.md
        heartbeat.test.ts
        index.ts
    adapters/
      native-adapter.ts
      native-adapter.md
      native-adapter.test.ts
      index.ts
  tests/
    integration.test.ts
    property.test.ts

Impact

Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/logger, @suaveplan/retry, @suaveplan/serialize must be published.