Files
skills/docs/foundation/bitpacker/bitpacker-delta/src/delta-manager/delta-manager.md
2026-05-29 08:20:56 +01:00

6.0 KiB
Raw Blame History

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

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

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<Required<DeltaConfig>>

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