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,167 @@
# Capability Spec Delta: game-server (game-server-state)
**Package**: `@suaveplan/game-server`
**Status**: PROPOSED
## ADDED Requirements
### Requirement: Observable state container
The package SHALL expose a `StateManager<TState extends object>` class
that holds a single typed state value, returns deep-frozen snapshots
through `getState`/`get`, and emits a `StateChangeEvent` on every
mutation. Listener faults SHALL be isolated and counted but MUST NOT
propagate or skip remaining listeners.
#### Scenario: getState returns deep-frozen snapshot
- **WHEN** a caller invokes `getState()`
- **THEN** the returned value is `Object.isFrozen`-true at every level
reachable through plain object/array traversal.
#### Scenario: listener errors do not interrupt other listeners
- **WHEN** one subscribed listener throws and another is registered
- **THEN** the non-throwing listener is still invoked exactly once per
mutation and the `listener_error_total` OTel counter increments.
**Verified by:**
`src/state/state-manager/state-manager.test.ts::"getState returns deep-frozen current state"`
AND
`src/state/state-manager/state-manager.test.ts::"listener errors are isolated and counted"`
AND
`src/state/state-manager/state-manager.test.ts::"event carries previous and current snapshots and id+timestamp"`.
---
### Requirement: Snapshot restore with typed validation
`StateManager.restore(snapshot, validator?)` SHALL run the optional
predicate before cloning. A failing predicate SHALL throw
`GameServerError(GAME_SERVER.INVALID_SNAPSHOT)` and leave the manager
state unchanged. A clone failure SHALL be wrapped as
`GameServerError(GAME_SERVER.STATE_RESTORE_FAILED)`.
#### Scenario: validator rejects malformed snapshot
- **WHEN** `restore` is invoked with a snapshot the validator rejects
- **THEN** a `GameServerError` with code `GAME_SERVER.INVALID_SNAPSHOT`
is thrown and `getState()` returns the previous state unchanged.
#### Scenario: clone failure wrapped as STATE_RESTORE_FAILED
- **WHEN** the snapshot has a throwing getter that triggers during clone
- **THEN** a `GameServerError` with code
`GAME_SERVER.STATE_RESTORE_FAILED` is thrown.
**Verified by:**
`src/state/state-manager/state-manager.test.ts::"restore throws INVALID_SNAPSHOT when validator fails"`
AND
`src/state/state-manager/state-manager.test.ts::"restore wraps clone failures as STATE_RESTORE_FAILED"`.
---
### Requirement: Structural diff and patch utility
The package SHALL expose a `StateDiffer` class with `diff(a, b)`,
`patch(s, d)`, `isEmpty(d)`, and `mergeDeltas(deltas)` methods that
produce and consume a discriminated `DiffOperation` grammar (`add`,
`remove`, `replace`). For any plain-JSON pair `(a, b)`, `patch(a, diff(a, b))`
SHALL be deep-equal to `b`.
#### Scenario: round-trip on nested object + array state
- **WHEN** `patch(a, diff(a, b))` is computed for any pair of plain
JSON-serialisable values `a` and `b`
- **THEN** the result is deep-equal to `b`.
#### Scenario: typed errors on diff/patch failure
- **WHEN** an internal traversal throws
- **THEN** the error surfaces as `GameServerError` with code
`GAME_SERVER.STATE_DIFF_FAILED` or `GAME_SERVER.STATE_PATCH_FAILED`
respectively.
**Verified by:**
`src/state/state-differ/state-differ.test.ts::"round-trips: patch(a, diff(a, b)) deep-equals b"`
AND
`src/state/state-differ/state-differ.test.ts::"wraps internal failures as STATE_DIFF_FAILED"`
AND
`src/state/state-differ/state-differ.test.ts::"wraps patch failures as STATE_PATCH_FAILED"`.
---
### Requirement: Versioned, Zod-validated delta compressor
The package SHALL expose a `DeltaCompressor` class that produces a
`CompressedDelta` packet `{ version, timestamp, data: Uint8Array, stats }`
from any pair of plain-JSON snapshots, and decompresses such a packet
back to a snapshot. The wire payload SHALL be Zod-validated on receipt.
Version mismatches and parse failures SHALL surface as
`GameServerError(GAME_SERVER.STATE_DECOMPRESS_FAILED)`.
#### Scenario: round-trip via compress + decompress
- **WHEN** a delta produced by `compress(a, b)` is applied with
`decompress(a, packet)`
- **THEN** the result equals `b`.
#### Scenario: version mismatch raises typed error
- **WHEN** `decompress` receives a packet whose `version` differs from
`COMPRESSED_DELTA_VERSION`
- **THEN** a `GameServerError` with code
`GAME_SERVER.STATE_DECOMPRESS_FAILED` is thrown.
#### Scenario: schema-invalid payload raises typed error
- **WHEN** `decompress` receives a packet whose data parses to a value
that fails the operation schema
- **THEN** a `GameServerError` with code
`GAME_SERVER.STATE_DECOMPRESS_FAILED` is thrown.
**Verified by:**
`src/state/delta-compressor/delta-compressor.test.ts::"round-trips compress + decompress"`
AND
`src/state/delta-compressor/delta-compressor.test.ts::"throws STATE_DECOMPRESS_FAILED on version mismatch"`
AND
`src/state/delta-compressor/delta-compressor.test.ts::"throws STATE_DECOMPRESS_FAILED on schema-invalid payload"`.
---
### Requirement: Transport-agnostic state synchroniser
The package SHALL expose a `StateSync<TState>` class that orchestrates
delta and full-snapshot synchronisation between a `StateManager` and an
injectable `StateSyncTransport`. The first call to `sync()` SHALL send
a full snapshot. Subsequent calls SHALL send a delta unless the delta
payload exceeds the configurable `maxDeltaSize`, in which case a full
snapshot SHALL be sent instead. Transport send failures SHALL surface
as `GameServerError(GAME_SERVER.STATE_SYNC_TRANSPORT_FAILED)`.
#### Scenario: initial sync sends full snapshot, subsequent sends delta
- **WHEN** `sync()` is called twice on a freshly-constructed `StateSync`
with an intervening `StateManager.update`
- **THEN** the first transport message has `kind === "full"` and the
second has `kind === "delta"`.
#### Scenario: large delta forces full-snapshot fallback
- **WHEN** the computed delta exceeds `maxDeltaSize`
- **THEN** a full snapshot is sent instead.
#### Scenario: transport failure wrapped as STATE_SYNC_TRANSPORT_FAILED
- **WHEN** `transport.send` rejects or throws
- **THEN** a `GameServerError` with code
`GAME_SERVER.STATE_SYNC_TRANSPORT_FAILED` propagates to the caller.
**Verified by:**
`src/state/state-sync/state-sync.test.ts::"first sync() sends a full snapshot"`
AND
`src/state/state-sync/state-sync.test.ts::"subsequent sync() sends a delta"`
AND
`src/state/state-sync/state-sync.test.ts::"falls back to full snapshot when delta exceeds maxDeltaSize"`
AND
`src/state/state-sync/state-sync.test.ts::"wraps transport send errors as STATE_SYNC_TRANSPORT_FAILED"`.