Files
2026-05-27 23:29:53 +01:00

6.5 KiB

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