# DeltaManager _(no submodules)_ ## Purpose `DeltaManager` is the field-level delta detector for bit-packed objects. Given a current state and a `BitPackerSchema` describing it, it returns the ordered list of fields whose value has changed since the previous snapshot stored under the same state key. Encoders use the returned `FieldChange[]` to build a fixed-width bitmask and send only the changed fields, dramatically reducing bandwidth for high-frequency state-sync workloads (game replication, multi-peer document sync, telemetry deltas). This is the port of suaveplan's upstream `core/delta/delta-manager.ts`. hardening replaces upstream's `Date.now()` with injectable `Clock` (CLAUDE.md determinism rule), upstream's `JSON.stringify`-based equality with a structural walker that respects `BitPackerSchema` field order (avoiding subtle false-positive diffs from key-order changes in nested objects), upstream's plain `Error` throws with `BitpackerError` codes, and validates `DeltaConfig` via Zod with strict-mode key rejection. ## Features - Field-indexed `FieldChange[]` output matching `Object.keys(schema)` order for stable bit offsets - Per-state-key snapshot store with LRU eviction (delegates to `DeltaStateManager`) - Configurable `maxFieldCount` cap with `BITPACKER.DELTA_FIELD_LIMIT_EXCEEDED` enforcement - Structural equality default (matches schema semantics) with opt-in `equalityMode: "json"` for upstream byte-compat - Zod-validated `DeltaConfig`; unknown keys rejected with `BITPACKER.DELTA_INVALID_CONFIG` - Threshold-driven `shouldUseDelta(originalBits, deltaBits)` for bandwidth-savings decisions - Zero `Math.random` / `Date.now` in the implementation — fully deterministic given identical inputs ## Usage: Basic Example ```typescript import { DeltaManager } from "@suaveplan/bitpacker-delta"; const schema = { x: "u32", y: "u32" } as const; const dm = new DeltaManager({ mode: "field-level" }); // First call: no previous state, so no changes. dm.detectChanges({ x: 0, y: 0 }, schema); // → [] dm.storePreviousState({ x: 0, y: 0 }); // Second call: x changed. dm.detectChanges({ x: 5, y: 0 }, schema); // → [{ index: 0, key: "x", previousValue: 0, currentValue: 5 }] ``` ## Usage: Advanced Example ```typescript import { DeltaManager } from "@suaveplan/bitpacker-delta"; const schema = { pos: { x: "f32", y: "f32" }, vel: { x: "f32", y: "f32" }, health: "u8", } as const; // Per-peer state streams: one DeltaManager, many state keys. const dm = new DeltaManager( { mode: "field-level", threshold: 25, maxFieldCount: 16 }, { maxStates: 256 } // 256 simultaneous peers ); for (const peer of peers) { // Each peer is its own state stream. const previous = dm.getPreviousState(peer.id); const changes = dm.detectChanges(peer.state, schema, peer.id); // Only send a delta if it saves at least 25% of bits. const fullSize = bitWidthOf(schema); const deltaSize = changes.length * 32 + Math.ceil(3 / 8) * 8; if (dm.shouldUseDelta(fullSize, deltaSize)) { sendDelta(peer.id, changes); } else { sendFull(peer.id, peer.state); } dm.storePreviousState(peer.state, peer.id); } ``` ## API Reference ### `new DeltaManager(config?: DeltaConfig, options?: DeltaManagerOptions)` `config.mode` ∈ `"none" | "xor" | "string-diff" | "field-level"`. Default `"none"`. `config.threshold` — bandwidth-savings %, 0–100. Default `0`. `config.maxFieldCount` — max fields the bitmask can address. Default `64`. `config.stateKey` — default key for `detectChanges` calls without one. Default `"default"`. `config.equalityMode` — `"structural"` (default) or `"json"` (upstream-compat). `options.maxStates` — capacity of the underlying state store. Default `100`. Throws `BitpackerError(BITPACKER.DELTA_INVALID_CONFIG)` on invalid config. ### `detectChanges(current, schema, stateKey?) → FieldChange[]` Returns the ordered list of fields whose `current` value differs from the stored snapshot under `stateKey`. First call (no snapshot) returns `[]`. Schema with more keys than `maxFieldCount` throws `BITPACKER.DELTA_FIELD_LIMIT_EXCEEDED`. ### `storePreviousState(state, stateKey?) → void` Stores a deep-cloned snapshot of `state` for the next diff. ### `getPreviousState(stateKey?) → unknown | undefined` ### `hasPreviousState(stateKey?) → boolean` Snapshot inspection helpers. ### `calculateSavings(originalBits, deltaBits) → number` ### `shouldUseDelta(originalBits, deltaBits) → boolean` Bandwidth-savings calculation against the configured `threshold`. ### `clear() → void` ### `clearState(stateKey) → void` ### `getConfig() → Readonly>` Lifecycle and inspection helpers. ## Implementation Notes The structural-equality walker treats every `ArrayBuffer.isView` value (every typed-array variant) as a byte-by-byte comparison. `Date` instances compare via `getTime()`. Plain objects compare key-sets first (size + key membership), then recurse on values. Arrays compare length-then-elements. This matches `BitPackerSchema` semantics: two values that pack to identical bytes are equal. The `"json"` equality mode is provided for upstream byte-compat but has known limitations: `JSON.stringify({ a: 1, b: 2 })` and `JSON.stringify({ b: 2, a: 1 })` produce different strings even though the values are semantically equal in a `BitPackerSchema` context. Consumers porting from upstream should switch to `"structural"` once they've verified no callers depend on key-order-sensitive equality. The `maxFieldCount` cap is a defence: a hostile peer that sends a schema with thousands of fields could exhaust memory if the bitmask were unbounded. The default (64) accommodates typical state-sync workloads; consumers with larger schemas raise it explicitly. Threshold semantics: `shouldUseDelta` returns `true` when `(originalBits - deltaBits) / originalBits * 100 >= threshold`. A `threshold` of `0` means "always prefer delta when smaller"; a `threshold` of `25` means "only prefer delta if it saves at least a quarter of the bits."