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

5.7 KiB

OpenSpec Change: network-transport-websockets-adapter

  • Change ID: network-transport-websockets-adapter
  • Package: @suaveplan/network-transport-websockets (already exists at 0.1.0)
  • Category: universal
  • Target path: packages/network/network-transport-websockets (in place)
  • Status: PROPOSED
  • Date: 2026-05-11
  • Parent: network-transport-family (2026-05-11) — Wave 1, audit finding 2.3

Summary

Refactor @suaveplan/network-transport-websockets from its current client-server WebSocketClient shape into a peer-oriented Transport implementation registered as a NetworkPlugin. The package was migrated to genesis before the Transport contract from @suaveplan/network ✅ existed; today it exposes a connect-to-a-server abstraction (typed framing, heartbeat, backpressure queue, adapters) but does NOT implement connect(peerId) / disconnect(peerId) / send(peerId, envelope) / onMessage / getConnectedPeers / close. This change adds a WebSocketTransport + WebSocketPlugin layer on top of the existing client surface so the package can plug into network-plugin-transport-manager ✅ alongside network-transport-webrtc ✅.


Why this is needed

Per the audit finding recorded in openspec/changes/network-transport-family/tasks.md §2.3:

network-transport-websockets predates the Transport contract and exposes a client-server WebSocketClient rather than a peer transport. Wiring it as a NetworkPlugin requires a non-trivial refactor (per-peer connections, getConnectedPeers() aggregation, transport:connected/disconnected bus emission). Deferred to a dedicated child proposal.

This is that proposal. Three downstream consumers are blocked until it lands:

  1. network-plugin-transport-manager ✅ — cannot register WS as a fallback transport without the adapter.
  2. network-transport-webrtc ✅ — needs WS as a fallback in its signaling channel.
  3. websocket-gateway (Wave 5) — server-side rooms + presence depends on the transport contract being honoured.

What changes

New code

  • src/transport/transport.ts — WebSocketTransport implements Transport (from @suaveplan/network)
    • connect(peerId, address) → creates a new WebSocketClient keyed by peerId
    • disconnect(peerId) → closes the per-peer client
    • send(peerId, envelope) → delegates to the per-peer client's framed send
    • onMessage(handler) → fan-in across every per-peer client
    • getConnectedPeers() → snapshot of keys
    • close() → closes every per-peer client
  • src/plugin/plugin.ts — WebSocketPlugin implements NetworkPlugin mirroring WebRTCPlugin shape:
    • install(node) → node.registerTransport(this.transport)
    • start() → no-op (lazy connect)
    • stop() → close transport + unregister
    • dispose() → idempotent cleanup
  • src/transport/transport.spec-conformance.test.ts — runs the Transport contract conformance suite
  • src/plugin/plugin.spec-conformance.test.ts — runs the NetworkPlugin lifecycle conformance suite
  • Sibling .md docs for both modules

Existing code

  • src/core/client/ — keep. The new WebSocketTransport composes per-peer WebSocketClient instances.
  • src/core/queue/ + src/core/heartbeat/ — keep, consumed per-peer.
  • src/adapters/ — keep, injectable through the new transport.
  • src/types/ — extend with WebSocketTransportConfig (per-peer client config + transport-level limits).

Error codes

  • Extend the existing defineCodes("NET_WS", [...]) registry with: PEER_ALREADY_CONNECTED, PEER_UNKNOWN, TRANSPORT_CLOSED, MAX_PEERS_EXCEEDED.

Observability

  • Spans: ws_transport.connect, ws_transport.disconnect, ws_transport.send.
  • Counters: ws_transport.peers_connected{outcome}, ws_transport.messages_total{direction,outcome}.
  • Gauges: ws_transport.active_peers.

Parity Checklist

Migration from upstream is already complete; this is a refactor adding the Transport adapter. The original WebSocketClient API is preserved.

Capability Before After Notes
WebSocketClient typed framing ✅ ✅ Unchanged.
Heartbeat / ping-pong ✅ ✅ Unchanged; runs per-peer.
Backpressure queue ✅ ✅ Unchanged; runs per-peer.
Injectable transport adapters ✅ ✅ Unchanged.
Transport contract 🚫 ⬆️ NEW — WebSocketTransport.
NetworkPlugin lifecycle 🚫 ⬆️ NEW — WebSocketPlugin.
Multi-peer fan-in/fan-out 🚫 ⬆️ NEW — per-peer client registry.
transport:connected/disconnected bus events 🚫 ⬆️ NEW — on bus when a NetworkBus is provided via plugin.
Backward compatibility ✅ ✅ Existing WebSocketClient consumers (none in genesis, but downstream @suaveplan/network-transport-webrtc signaling client uses the type) keep working — the new transport composes them.

Acceptance gate

  1. bunx turbo lint typecheck test --filter=@suaveplan/network-transport-websockets green.
  2. bunx turbo test:coverage --filter=@suaveplan/network-transport-websockets at 100/100/100/100 (currently 60 tests pass after the rename; adding the transport+plugin adds another conformance suite).
  3. bun run docs:check --filter=@suaveplan/network-transport-websockets green.
  4. network-plugin-transport-manager integration test confirms WS-as-fallback works end-to-end.
  5. openspec/specs/network/spec.md updated under ## MODIFIED Requirements if any NET-* requirement is tightened.

Out of scope

  • Server-side websocket-gateway (Wave 5, separate package).
  • TLS / mTLS cert management for production WSS — that's a deployment concern, not a transport concern.
  • Custom subprotocols beyond what the existing WebSocketClient supports.