# 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.