Initial Commit
This commit is contained in:
76
openspec/archive/2026-05-24-game-server-network/proposal.md
Normal file
76
openspec/archive/2026-05-24-game-server-network/proposal.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Migration: `@suaveplan/game-server` network slice (Wave B-4)
|
||||
|
||||
**Package**: `@suaveplan/game-server`
|
||||
**Status**: PROPOSED
|
||||
**Type**: extends-package
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/game-server` shipped its core slice (errors, types,
|
||||
di-container) on Wave A. The network module is one of the four parallel
|
||||
B-wave slices that bring the package up to the broader reference surface
|
||||
from `claude/review-game-engine-arch-Kaykr`. Without a unified
|
||||
`NetworkAdapter` + reconnection driver + WebSocket/WebRTC transports,
|
||||
downstream session and matchmaking modules (Waves B-1…B-3, B-5) cannot
|
||||
wire to a transport at all — they all consume the `Transport` contract
|
||||
defined here.
|
||||
|
||||
The reference branch's network module (`network/NetworkAdapter.ts`,
|
||||
`ReconnectionManager.ts`, `WebSocketTransport.ts`, `WebRTCTransport.ts`)
|
||||
mixes runtime concerns (raw `WebSocket` / `RTCPeerConnection`) with
|
||||
framework concerns (event emitter, logger). Genesis splits those: the
|
||||
unified adapter contract lives here, and the actual platform-tier
|
||||
clients live in `@suaveplan/network-transport-websockets` and
|
||||
`@suaveplan/network-transport-webrtc`. The wrappers in this slice are
|
||||
the thin glue layer that bridges those two worlds via a structural
|
||||
boundary so tests can use in-process fakes instead of real sockets.
|
||||
|
||||
## What changes
|
||||
|
||||
- ADDED: `src/network/network-adapter/` — unified `NetworkAdapter` class
|
||||
with single-init guard, Zod boundary validation, multi-subscriber
|
||||
fan-out, isolated handler errors, OTel counters
|
||||
(`game_server.network.messages_sent_total`,
|
||||
`…messages_received_total`, `…errors_total`).
|
||||
- ADDED: `src/network/reconnection-manager/` — exponential-backoff
|
||||
reconnection driver via `@suaveplan/retry`'s
|
||||
`createExponentialBackoff` + `applyJitter`, injectable
|
||||
`Clock` + `TimersLike`, OTel counter + histogram
|
||||
(`reconnect_attempts_total`, `reconnect_delay_ms`).
|
||||
- ADDED: `src/network/websocket-transport/` — thin wrapper implementing
|
||||
`Transport` over a structural `UnderlyingWebSocket` boundary.
|
||||
- ADDED: `src/network/webrtc-transport/` — thin wrapper implementing
|
||||
`Transport` over a structural `UnderlyingWebRTC` boundary, plus
|
||||
pass-through helpers (`addIceCandidate`, `applyAnswer`).
|
||||
- ADDED: Six new `GAME_SERVER.*` error codes:
|
||||
`TRANSPORT_ALREADY_INITIALIZED`, `TRANSPORT_NOT_INITIALIZED`,
|
||||
`TRANSPORT_NOT_CONNECTED`, `INVALID_MESSAGE_FORMAT`,
|
||||
`RECONNECT_EXHAUSTED`, `RECONNECT_IN_PROGRESS`.
|
||||
- ADDED: Subpath exports `./network/network-adapter`,
|
||||
`./network/reconnection-manager`, `./network/websocket-transport`,
|
||||
`./network/webrtc-transport`.
|
||||
- ADDED: Workspace deps `@suaveplan/network-transport-websockets`,
|
||||
`@suaveplan/network-transport-webrtc`, `@suaveplan/retry`.
|
||||
|
||||
## Out of scope (deferred to later waves)
|
||||
|
||||
None. Network slice ships at full functional completeness for the
|
||||
`NetworkAdapter` + Reconnection + WebSocket + WebRTC contract pair.
|
||||
Real-world wiring of the underlying clients to the genesis transport
|
||||
packages is a downstream concern in each application; the wrappers
|
||||
themselves are complete.
|
||||
|
||||
### Affected dependents
|
||||
|
||||
- Downstream session and matchmaking slices (B-1, B-2, B-3, B-5)
|
||||
consume `Transport` and `NetworkAdapter` exclusively through their
|
||||
exported names; no additional change needed.
|
||||
|
||||
## Source mapping
|
||||
|
||||
| Genesis path | Upstream reference |
|
||||
|---|---|
|
||||
| `src/network/network-adapter/network-adapter.ts` | `packages/universal/game-server/src/network/NetworkAdapter.ts` (full rewrite into structural `Transport` contract, Zod boundary, OTel) |
|
||||
| `src/network/reconnection-manager/reconnection-manager.ts` | `packages/universal/game-server/src/network/ReconnectionManager.ts` (rewritten to consume `@suaveplan/retry`; storage persistence dropped — handled by a future state slice) |
|
||||
| `src/network/websocket-transport/websocket-transport.ts` | `packages/universal/game-server/src/network/WebSocketTransport.ts` (wraps `UnderlyingWebSocket` structural client instead of raw `WebSocket`) |
|
||||
| `src/network/webrtc-transport/webrtc-transport.ts` | `packages/universal/game-server/src/network/WebRTCTransport.ts` (wraps `UnderlyingWebRTC` structural client instead of raw `RTCPeerConnection`) |
|
||||
@@ -0,0 +1,212 @@
|
||||
# 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"`.
|
||||
56
openspec/archive/2026-05-24-game-server-network/tasks.md
Normal file
56
openspec/archive/2026-05-24-game-server-network/tasks.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Tasks — game-server-network (Wave B-4)
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm isolated worktree (`pwd` shows
|
||||
`.claude/worktrees/agent-ab405575252114357`).
|
||||
- [x] 0.2 `git worktree list` accounted for; all sibling B-wave agents
|
||||
operate in their own locked worktrees.
|
||||
- [x] 0.3 Read reference branch source at
|
||||
`/mnt/s/web/suaveplan/packages/universal/game-server/src/network/`.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read `openspec/AGENTS.md` and `CLAUDE.md`.
|
||||
- [x] 1.2 Draft `proposal.md` with Source mapping table.
|
||||
- [x] 1.3 Draft `specs/game-server/spec.md` with ADDED requirements and
|
||||
`**Verified by:**` lines naming planned tests.
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Extend `errors/game-server-error.ts` with six new codes and
|
||||
update the registry test.
|
||||
- [x] 2.2 `src/network/network-adapter/{types,impl,index,md,test}`.
|
||||
- [x] 2.3 `src/network/reconnection-manager/{types,impl,index,md,test}`.
|
||||
- [x] 2.4 `src/network/websocket-transport/{types,impl,index,md,test}`.
|
||||
- [x] 2.5 `src/network/webrtc-transport/{types,impl,index,md,test}`.
|
||||
- [x] 2.6 Subpath exports in `package.json` and workspace dependencies.
|
||||
- [x] 2.7 Root `src/index.ts` re-exports the network surface.
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 `network-adapter.test.ts` — 20 cases covering init, send,
|
||||
receive, fan-out, handler isolation, stats, errors, dispose.
|
||||
- [x] 3.2 `reconnection-manager.test.ts` — 18 cases covering success,
|
||||
retry, exhaustion, lifecycle events, config update, stop in
|
||||
flight.
|
||||
- [x] 3.3 `websocket-transport.test.ts` — 11 cases covering connect,
|
||||
send, parse, callbacks.
|
||||
- [x] 3.4 `webrtc-transport.test.ts` — 14 cases covering connect, send,
|
||||
parse, signalling helpers.
|
||||
|
||||
## 4. Verification
|
||||
|
||||
- [x] 4.1 `bunx turbo lint typecheck test --filter=@suaveplan/game-server`
|
||||
green.
|
||||
- [x] 4.2 `bunx turbo test:coverage --filter=@suaveplan/game-server`
|
||||
meets 100/100/100/100 on touched modules.
|
||||
- [x] 4.3 No `vitest|bun:test` direct imports in `src/`.
|
||||
- [x] 4.4 No `biome-ignore` / `TODO` / `FIXME` markers in `src/`.
|
||||
|
||||
## 5. Fold + archive
|
||||
|
||||
- [ ] 5.1 Fold the delta into `openspec/specs/game-server/spec.md` on
|
||||
merge.
|
||||
- [ ] 5.2 Move the change folder to
|
||||
`openspec/archive/<yyyy-mm-dd>-game-server-network/` on merge.
|
||||
Reference in New Issue
Block a user