# StateDiffer _(no submodules)_ ## Purpose `StateDiffer` is a pure structural diff and patch utility for plain-object game state. It produces a compact, deterministic `StateDelta` between two snapshots and applies that delta back onto a snapshot to reconstruct the target — the symmetric pair `patch(a, diff(a, b)) ≡ b` is the round-trip invariant on which `DeltaCompressor` and `StateSync` depend. Wave B-3 ships this as a leaf utility under `@suaveplan/game-server/state/state-differ`. The migration replaces the upstream's `"unchanged"` operation type — which inflated wire size for no benefit — with a delta that records only the operations that actually change something. Arrays use index-aligned diffing (positional), objects use key-set diffing, and primitives fall through to a single `replace`. Every traversal failure is wrapped in `GameServerError` with code `GAME_SERVER.STATE_DIFF_FAILED` or `STATE_PATCH_FAILED`, so callers never see a bare runtime exception. ## Features - Three-op grammar: `add`, `remove`, `replace` (no `unchanged` noise). - Object diff via key-set comparison; array diff via positional alignment. - Injectable `Clock` from `@suaveplan/types` for deterministic timestamps. - `mergeDeltas` collapses a sequence of deltas into a last-writer-wins set. - `isEmpty` predicate for skipping zero-change sync ticks. - `parsePath` handles nested arrays (`outer[0][1].inner`). - Plain-object guard rejects class instances (preserves identity semantics). - Typed errors via `GameServerError` — `STATE_DIFF_FAILED`, `STATE_PATCH_FAILED`. ## Usage: Basic ```typescript import { StateDiffer } from "@suaveplan/game-server/state/state-differ"; const differ = new StateDiffer(); const delta = differ.diff( { score: 0, players: ["a"] }, { score: 1, players: ["a", "b"] } ); console.log(delta.operations); // [{ type: "replace", path: "score", value: 1, oldValue: 0 }, // { type: "add", path: "players[1]", value: "b" }] const restored = differ.patch({ score: 0, players: ["a"] }, delta); // { score: 1, players: ["a", "b"] } ``` ## Usage: Advanced ```typescript import { StateDiffer } from "@suaveplan/game-server/state/state-differ"; const clock = () => 1_700_000_000_000; const differ = new StateDiffer({ clock }); // Compose multiple ticks into one wire packet. const a = differ.diff({ x: 0 }, { x: 1 }); const b = differ.diff({ x: 1 }, { x: 2 }); const c = differ.diff({ x: 2 }, { x: 3 }); const merged = differ.mergeDeltas([a, b, c]); // merged.operations === [{ type: "replace", path: "x", value: 3, oldValue: 2 }] ``` ## API Reference ### `class StateDiffer` #### `constructor(options?: StateDifferOptions)` `options.clock` — `Clock` from `@suaveplan/types`. Defaults to `Date.now`. #### `diff(oldState: T, newState: T): StateDelta` Throws `GameServerError(GAME_SERVER.STATE_DIFF_FAILED)` on traversal failure. #### `patch(state: T, delta: StateDelta): T` Returns a deeply-cloned, patched copy. Throws `GameServerError(GAME_SERVER.STATE_PATCH_FAILED)` on apply failure. #### `isEmpty(delta: StateDelta): boolean` #### `mergeDeltas(deltas: StateDelta[]): StateDelta` ### Types - `DiffOperation` — discriminated union of `add | remove | replace`. - `StateDelta` — `{ timestamp, operations, size }`. ## Implementation Notes The omission of an `unchanged` operation is deliberate: the upstream emitted one for every untouched leaf, ballooning a single-field update into a delta proportional to the snapshot size. Removing it shrinks typical 1-KB snapshots to 30-byte deltas without losing round-trip fidelity, because `patch` only walks the operations that exist. `Object.is` (rather than `===`) is used for equality so `NaN` and `-0` diff correctly. The plain-object guard rejects class instances — game states routinely carry `Date`, `Map`, `Set`, or domain classes whose internal slots would not survive a structural diff; treating them as opaque `replace` values is safer than producing a half-mutated clone. `deepClone` is a hand-rolled recursive walker rather than `structuredClone` because the diffed values are constrained to plain JSON shapes and the recursion limit on game state (depth ≤ 16 in practice) is far below the V8 stack budget.