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

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 (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

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