Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
# Game-server enhanced infra: service-discovery + state-sync + timing
**Package**: `@suaveplan/game-server`
**Status**: PROPOSED
**Type**: feature
## Why
Wave C-4 lands the three "enhanced" subsystems that ride on top of the wave-A/B core: a heart-beating game-instance registry for multi-host
discovery + load-balancing, a binary delta state-sync layer driven by `@suaveplan/bitpacker` (complementary to the JSON-delta `state/state-sync`
shipped in wave B-3), and a vector-clock causality tracker for distributed event ordering. Without these, the higher-level orchestrator cannot
fan a session across processes, ship compact state updates over WebRTC, or reason about who-saw-what when two clients race a move.
## What changes
- ADDED: `GameInstanceRegistry` — TTL-evicting in-memory registry of live instances with four load-balancing strategies (`least-players`, `random`, `region-based`, `round-robin`), region-latency map, capacity / health updates, filter+select discovery API, and 6 lifecycle event keys wired through the existing `GameEventEmitter`.
- ADDED: `BitPackerStateSync<TState, TSchema>` — bitpacker-backed binary state-sync with client-side prediction, server reconciliation, and frame interpolation. Uses `@suaveplan/bitpacker`'s `BitPacker.pack/unpack` for the wire format. Coexists with the JSON-delta `state/state-sync` from wave B-3.
- ADDED: `VectorClockSync<TEventData>` — distributed event-ordering layer built over `@suaveplan/timing`'s `createVectorClock`. Records game actions per session, detects concurrent actions, runs `last-write-wins | merge | custom` conflict resolution, and supports request/response sync rounds against remote nodes.
- ADDED: 5 error codes — `REGISTRY_INSTANCE_NOT_FOUND`, `REGISTRY_HEARTBEAT_EXPIRED`, `BITPACK_ENCODE_FAILED`, `BITPACK_DECODE_FAILED`, `VECTOR_CLOCK_NODE_UNKNOWN` — appended to `GAME_SERVER_CODES`.
- ADDED: 3 OTel instruments — `game_server.registry.instance_lifecycle_total` (counter), `game_server.state.bitpack_bytes` (histogram), `game_server.vector_clock.actions_total` (counter).
- ADDED: 3 package subpath exports: `./enhanced/service-discovery/game-instance-registry`, `./enhanced/state-sync/bitpacker-state-sync`, `./enhanced/timing/vector-clock-sync`.
- ADDED: `@suaveplan/bitpacker` and `@suaveplan/timing` as `workspace:^` runtime deps.
## Out of scope
- Cross-process IPC for the registry (HTTP/gRPC façade) — the registry is the in-memory primitive; a transport façade is a separate package.
- Lamport-clock variant (only vector clocks ship; Lamport is the degenerate single-counter case).
- Bitpacker schema authoring helpers — callers supply their own `BitPackerSchema`.
## Source mapping
This change derives from `../suaveplan/packages/universal/game-server/src/enhanced/`.
| Requirement | Source lines | Notes |
|---|---|---|
| `GameInstanceRegistry` lifecycle (start/stop/register/deregister) | `service-discovery/game-instance-registry/game-instance-registry.ts` L33-L94 | Reworded; genesis injects `Clock` (no `Date.now`) and routes events through `GameEventEmitter`. |
| Heartbeat-TTL eviction | same file L340-L351 | Verbatim semantics; injectable `Clock` substituted for `Date.now`. |
| Load-balancing strategies | same file L258-L338 | All four strategies preserved; `Rng` injected for `random`. |
| Filter / select discovery | same file L138-L208 | Preserved. |
| `BitPackerStateSync` pack/unpack/prediction/reconciliation/interpolation | `state-sync/bitpacker-state-sync/bitpacker-state-sync.ts` L36-L307 | Reworded; logger dep dropped (genesis OTel-only); `enableDelta` flag plumbed via `BitPackerOptions`. |
| `VectorClockSync.recordAction` / `receiveAction` | `timing/vector-clock-sync/vector-clock-sync.ts` L86-L147 | Rewritten on top of genesis `createVectorClock` (no upstream `VectorClockManager`); causality compare uses `vectorClockCompare`. |
| Conflict resolution strategies | same file L320-L350 | Preserved (`last-write-wins`, `merge`, `custom`). |
| Causality chain + concurrent-group detection | same file L170-L205, L383-L414 | Preserved. |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| Injectable `Clock` + `IdGenerator` everywhere a timestamp / id is minted | Genesis enterprise rule — no implicit `Date.now` or `Math.random`. |
| OTel counters / histogram on every public path | Genesis observability baseline. |
| Zod validation on `GameInstanceRegistration` and `BitPackerStateSyncConfig` input boundaries | Genesis rule: typed boundary at every public entry. |
| Five new error codes routed through `GameServerError` | Upstream throws plain `Error`; genesis policy is typed errors with the stable code registry. |
## Risks
- The genesis `timing` package exposes only `createVectorClock` (not the upstream `VectorClockManager`). The rewrite preserves the upstream public surface of `VectorClockSync` but is built on the simpler primitive — this is the documented divergence.
- Coverage target is 100/100/100/100; `BitPackerStateSync.interpolate` has multi-branch lerp logic that requires careful test design.

View File

@@ -0,0 +1,114 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: game-server (game-server-enhanced-infra)
**Package**: `@suaveplan/game-server`
**Status**: PROPOSED
## ADDED Requirements
### Requirement: Game instance registry with TTL eviction and load-balancing
The package SHALL expose a `GameInstanceRegistry<TCustom>` class that registers live game-server instances, evicts them when their last heartbeat is older than `instanceTTL` milliseconds (as reported by an injected `Clock`), supports filter-based discovery, and selects one healthy instance per request using one of four load-balancing strategies (`least-players`, `random`, `region-based`, `round-robin`). All clock and randomness reads SHALL flow through injected `Clock` and `Rng` dependencies.
#### Scenario: register / discover / deregister round-trip
- **WHEN** the caller registers an instance, then calls `discover()` with no filter
- **THEN** the registered instance appears in the result, defaulted to `HEALTHY`
- **WHEN** the caller deregisters
- **THEN** the next `discover()` returns an empty array
#### Scenario: heartbeat expiry triggers eviction
- **WHEN** the injected clock advances past `instanceTTL * 2` since the last heartbeat
- **THEN** the registry transitions the instance to `UNHEALTHY` first, then deregisters it on the second sweep
- **AND** the corresponding `GameInstanceEvent.HEALTH_CHANGED` and `GameInstanceEvent.DEREGISTERED` events fire
#### Scenario: least-players strategy picks lowest player count
- **WHEN** two healthy instances exist with `playerCount = 5` and `playerCount = 2`
- **THEN** `selectInstance(..., 'least-players')` returns the second one with `score = 2`
#### Scenario: region-based strategy honours preferred region
- **WHEN** the caller passes `filter.region = "eu-west"` and at least one EU instance exists
- **THEN** the selected instance has `metadata.region === "eu-west"`
#### Scenario: round-robin rotates across calls
- **WHEN** three instances are available and `selectInstance(..., 'round-robin')` is called three times
- **THEN** each call returns a different instance, then the rotation repeats
**Verified by:** `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"registers and discovers an instance"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"evicts after TTL expiry"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"least-players selects lowest count"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"region-based prefers configured region"` AND `src/enhanced/service-discovery/game-instance-registry/game-instance-registry.test.ts::"round-robin rotates across calls"`
---
### Requirement: BitPacker-backed binary state synchronisation
The package SHALL expose a `BitPackerStateSync<TState, TSchema extends BitPackerSchema>` class that packs `TState` snapshots to `Uint8Array` via `@suaveplan/bitpacker`, supports client-side prediction with a bounded history window, supports server reconciliation against a tick-tagged authoritative state, supports linear interpolation between two history snapshots, and aggregates wire-byte counters into a `SyncStats` view. All timestamps SHALL flow through an injected `Clock`. Pack/unpack failures SHALL throw `GameServerError` with `BITPACK_ENCODE_FAILED` / `BITPACK_DECODE_FAILED`.
#### Scenario: pack then unpack round-trip preserves state
- **WHEN** the caller packs a `TState` value and unpacks the resulting `Uint8Array`
- **THEN** the unpacked value equals the original
#### Scenario: prediction records a history frame
- **WHEN** `predict(state, input, simulate)` is called
- **THEN** the returned state equals `simulate(state, input.data)`
- **AND** `getStats().predictionsMade` increments by one
#### Scenario: reconciliation re-applies pending inputs above server tick
- **WHEN** the client has predicted three frames at ticks 11, 12, 13 and the server delivers a state at tick 10
- **AND** the client's snapshot at tick 10 differs from the server state beyond the threshold
- **THEN** `reconcile(...)` returns `needed: true` with `framesResimulated = 3` and a state derived from re-applying the three pending inputs
#### Scenario: interpolation lerps between snapshots
- **WHEN** the caller passes two snapshots at timestamps 100 and 200 and a render time bracketing the targetTime
- **THEN** the returned state equals `lerp(before, after, t)` for some `t ∈ [0, 1]`
#### Scenario: bitpack failure routes to typed error
- **WHEN** `BitPacker.pack` throws
- **THEN** `packState` re-throws a `GameServerError` carrying code `GAME_SERVER.BITPACK_ENCODE_FAILED`
**Verified by:** `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"packs and unpacks state"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"prediction records a history frame"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"reconciliation re-applies pending inputs above the server tick"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"interpolation lerps between two snapshots"` AND `src/enhanced/state-sync/bitpacker-state-sync/bitpacker-state-sync.test.ts::"throws BITPACK_ENCODE_FAILED when bitpacker throws"`
---
### Requirement: Vector-clock distributed action ordering
The package SHALL expose a `VectorClockSync<TEventData>` class that records game actions per session, tags each action with a vector-clock snapshot, detects concurrent actions, resolves conflicts via one of three strategies (`last-write-wins`, `merge`, `custom`), produces a causality chain in happens-before order, and exposes request/response sync round-trips between nodes. The internal vector clock SHALL be the `createVectorClock` primitive from `@suaveplan/timing`. Action IDs SHALL be minted through an injected `IdGenerator`; timestamps SHALL flow through an injected `Clock`.
#### Scenario: recordAction tags with current vector clock
- **WHEN** `recordAction(sessionId, "move", data, tick)` is called
- **THEN** the returned `GameAction.vectorClock` carries this node's counter at >= 1
- **AND** `getStats().totalActions` equals the count of recordings
#### Scenario: receiveAction detects concurrent conflict
- **WHEN** two actions are recorded by different nodes with vector clocks that are mutually concurrent (`compare === "concurrent"`)
- **THEN** `receiveAction(remote)` returns a `ConflictResolution` with `conflictDetected: true`
- **AND** the chosen winner under `last-write-wins` is the one with the larger timestamp
#### Scenario: custom resolver overrides default strategy
- **WHEN** the registry is constructed with a `conflictResolver`
- **THEN** the resolver function is called with each (winner, candidate) pair and its return value becomes the resolved action
- **AND** `ConflictResolution.strategy === "custom"`
#### Scenario: causality chain sorts by happens-before
- **WHEN** action A is recorded, then action B is recorded on the same node
- **THEN** `getCausalityChain(sessionId).actions[0]` is A and `[1]` is B
#### Scenario: sync request returns missing actions
- **WHEN** node X has recorded actions [a1, a2] and node Y has recorded none
- **AND** Y issues `requestSync(X, sessionId)` with its current (empty) vector clock
- **THEN** X's `handleSyncRequest(request)` returns a response carrying both `a1` and `a2`
**Verified by:** `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"recordAction tags with the current vector clock"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"receiveAction detects concurrent conflict and resolves last-write-wins"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"custom resolver overrides default strategy"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"causality chain sorts by happens-before"` AND `src/enhanced/timing/vector-clock-sync/vector-clock-sync.test.ts::"sync request returns missing actions"`

View File

@@ -0,0 +1,29 @@
# Tasks — Game-server enhanced infra (service-discovery + state-sync + timing)
## 0. Pre-flight
- [x] 0.1 Clean working tree confirmed at dispatch
- [x] 0.4 §0a Sync — fast-forwarded from orchestrator-supplied SHA to current `genesis` HEAD
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/packages/universal/game-server/src/enhanced/{service-discovery,state-sync,timing}` reference
- [x] 1.4 Draft `proposal.md`
- [x] 1.6 Draft `specs/game-server/spec.md` delta with three ADDED requirements (registry, bitpack-sync, vector-clock-sync)
- [x] 1.7 Every requirement carries a `**Verified by:**` line naming exact `it(...)` titles
## 2. Implementation
- [x] 2.1 `enhanced/service-discovery/game-instance-registry/` — 5 files
- [x] 2.2 `enhanced/state-sync/bitpacker-state-sync/` — 5 files
- [x] 2.3 `enhanced/timing/vector-clock-sync/` — 5 files
- [x] 2.4 Extend `errors/game-server-error.ts` with 5 new codes
- [x] 2.5 Re-export new public surface from `src/index.ts`
- [x] 2.6 Add 3 subpath exports + 2 runtime deps to `package.json`
## 3. Verification
- [x] 3.1 `bunx turbo lint typecheck test --filter=@suaveplan/game-server` green
- [x] 3.2 `bunx turbo test:coverage --filter=@suaveplan/game-server` ≥ 100/100/100/100 on touched modules
- [x] 3.3 `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/gaming/games/game-server/src/` → zero hits
- [x] 3.4 `grep -rn "biome-ignore\|TODO\|FIXME\|XXX" packages/gaming/games/game-server/src/` → zero hits in new code