Files
suaveplan/openspec/archive/2026-05-24-game-server-network/specs/game-server/spec.md
2026-05-27 23:29:53 +01:00

7.9 KiB

Capability Spec Delta: game-server (game-server-network)

Package: @suaveplan/game-server Status: PROPOSED

ADDED Requirements

Requirement: Unified network adapter contract

The package SHALL expose a NetworkAdapter class that wraps a structural Transport interface and provides single-initialisation, multi-subscriber fan-out, Zod-validated outbound + inbound frames, and OpenTelemetry counters for messages sent / received / errors.

Scenario: single-initialisation guard

  • WHEN initialize(...) is called twice without an intervening dispose() / disconnect()
  • THEN the second call throws GameServerError with code GAME_SERVER.TRANSPORT_ALREADY_INITIALIZED.

Scenario: send before initialise

  • WHEN send(...) is called before initialize(...)
  • THEN it rejects with code GAME_SERVER.TRANSPORT_NOT_INITIALIZED.

Scenario: send while transport disconnected

  • WHEN the underlying transport never fires onConnect and send is invoked
  • THEN it rejects with code GAME_SERVER.TRANSPORT_NOT_CONNECTED.

Scenario: outbound message validation

  • WHEN a frame with empty type is passed to send
  • THEN it rejects with code GAME_SERVER.INVALID_MESSAGE_FORMAT.

Scenario: fan-out and handler isolation

  • WHEN multiple subscribers are attached via onMessage and one of them throws
  • THEN every other subscriber still receives the frame and the failure is logged through the injected logger.

Verified by: src/network/network-adapter/network-adapter.test.ts::"initialises a transport and connects" AND src/network/network-adapter/network-adapter.test.ts::"rejects double-initialisation" AND src/network/network-adapter/network-adapter.test.ts::"rejects send before initialize" AND src/network/network-adapter/network-adapter.test.ts::"rejects send when not connected" AND src/network/network-adapter/network-adapter.test.ts::"rejects malformed outgoing message" AND src/network/network-adapter/network-adapter.test.ts::"fans inbound messages to every subscriber" AND src/network/network-adapter/network-adapter.test.ts::"isolates handler errors and still delivers to others" AND src/network/network-adapter/network-adapter.test.ts::"drops malformed inbound frames" AND src/network/network-adapter/network-adapter.test.ts::"exposes stats counts".


Requirement: Reconnection with exponential backoff

The package SHALL expose a ReconnectionManager that schedules reconnection attempts through an injectable TimersLike, delegates backoff calculation to @suaveplan/retry's createExponentialBackoff, and reports outcomes through OTel counter + histogram.

Scenario: success on first attempt

  • WHEN the supplied reconnect callback resolves on the first try
  • THEN the manager resets to { attempts: 0, isReconnecting: false } and stops scheduling.

Scenario: exponential retry until success

  • WHEN reconnect fails twice and then succeeds
  • THEN the manager invokes the callback exactly three times, doubling the envelope between attempts.

Scenario: exhaustion

  • WHEN maxAttempts is n and reconnect always rejects
  • THEN the manager emits network:reconnect-exhausted after n failures and stops scheduling further attempts.

Scenario: double-start guard

  • WHEN start() is invoked while a reconnection is already in progress
  • THEN it throws GameServerError with code GAME_SERVER.RECONNECT_IN_PROGRESS.

Scenario: use after dispose

  • WHEN start() or forceReconnect() is invoked after dispose()
  • THEN it throws GameServerError with code GAME_SERVER.RECONNECT_EXHAUSTED.

Verified by: src/network/reconnection-manager/reconnection-manager.test.ts::"succeeds on first attempt" AND src/network/reconnection-manager/reconnection-manager.test.ts::"retries with exponential backoff on failure" AND src/network/reconnection-manager/reconnection-manager.test.ts::"exhausts after maxAttempts and emits exhausted event" AND src/network/reconnection-manager/reconnection-manager.test.ts::"rejects start when already reconnecting" AND src/network/reconnection-manager/reconnection-manager.test.ts::"start throws after dispose" AND src/network/reconnection-manager/reconnection-manager.test.ts::"forceReconnect throws after dispose".


Requirement: WebSocket transport wrapper

The package SHALL expose a WebSocketTransport class implementing the unified Transport contract by wrapping a structural UnderlyingWebSocket client. JSON framing SHALL be applied to outgoing messages and inbound parse failures SHALL surface through onError rather than throwing.

Scenario: connect forwards protocols

  • WHEN connect(url, { protocols: ["v1"] }) is invoked
  • THEN the underlying client receives the URL and protocols.

Scenario: send rejects when not connected

  • WHEN send(...) is invoked before connect(...)
  • THEN it rejects with code GAME_SERVER.TRANSPORT_NOT_CONNECTED.

Scenario: parse error surfaces through onError

  • WHEN the underlying client delivers a non-JSON frame
  • THEN the registered onError handler receives a typed GameServerError with code GAME_SERVER.INVALID_MESSAGE_FORMAT.

Verified by: src/network/websocket-transport/websocket-transport.test.ts::"connects with protocols" AND src/network/websocket-transport/websocket-transport.test.ts::"send serialises message as JSON" AND src/network/websocket-transport/websocket-transport.test.ts::"send rejects when disconnected" AND src/network/websocket-transport/websocket-transport.test.ts::"reports parse errors through onError" AND src/network/websocket-transport/websocket-transport.test.ts::"propagates connect/disconnect callbacks".


Requirement: WebRTC transport wrapper

The package SHALL expose a WebRTCTransport class implementing the unified Transport contract by wrapping a structural UnderlyingWebRTC client. The wrapper SHALL provide pass-through signalling helpers (addIceCandidate, applyAnswer) and SHALL throw a typed error when the underlying client does not implement them.

Scenario: connect with options bag

  • WHEN connect(url, { iceServers: [...] }) is invoked
  • THEN the options bag is forwarded verbatim to the underlying client's open(...).

Scenario: signalling pass-through

  • WHEN addIceCandidate(c) and applyAnswer(a) are invoked on a client that implements them
  • THEN the candidate / answer is delivered verbatim to the client.

Scenario: signalling unsupported

  • WHEN the underlying client omits addIceCandidate / applyAnswer
  • THEN the wrapper throws GameServerError with code GAME_SERVER.TRANSPORT_NOT_INITIALIZED.

Verified by: src/network/webrtc-transport/webrtc-transport.test.ts::"connects with options bag" AND src/network/webrtc-transport/webrtc-transport.test.ts::"send serialises message as JSON" AND src/network/webrtc-transport/webrtc-transport.test.ts::"addIceCandidate forwards to underlying" AND src/network/webrtc-transport/webrtc-transport.test.ts::"applyAnswer forwards to underlying" AND src/network/webrtc-transport/webrtc-transport.test.ts::"addIceCandidate throws when unsupported" AND src/network/webrtc-transport/webrtc-transport.test.ts::"applyAnswer throws when unsupported".


Requirement: Extended error code registry

The package SHALL register six additional GAME_SERVER.* codes introduced by the network slice and SHALL expose them on the GAME_SERVER_CODES constant alongside the existing core codes.

Scenario: registry shape

  • WHEN the test suite inspects GAME_SERVER_CODES
  • THEN it contains TRANSPORT_ALREADY_INITIALIZED, TRANSPORT_NOT_INITIALIZED, TRANSPORT_NOT_CONNECTED, INVALID_MESSAGE_FORMAT, RECONNECT_EXHAUSTED, RECONNECT_IN_PROGRESS, each fully-qualified as GAME_SERVER.<NAME>.

Verified by: src/errors/game-server-error.test.ts::"registers all expected codes under GAME_SERVER namespace".