Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
# StateDiffer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.