# 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` 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` 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"`.