213 lines
7.9 KiB
Markdown
213 lines
7.9 KiB
Markdown
# 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"`.
|