4.2 KiB
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(nounchangednoise). - Object diff via key-set comparison; array diff via positional alignment.
- Injectable
Clockfrom@suaveplan/typesfor deterministic timestamps. mergeDeltascollapses a sequence of deltas into a last-writer-wins set.isEmptypredicate for skipping zero-change sync ticks.parsePathhandles 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
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
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<T>(oldState: T, newState: T): StateDelta
Throws GameServerError(GAME_SERVER.STATE_DIFF_FAILED) on traversal failure.
patch<T>(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 ofadd | 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.