Size / Priority
- Size: S (~40 lines saved + correctness improvement)
- Category: C.2 Simplifications & DRY.
- Risk: low — but watch for behavioural changes if the existing JSON.stringify approach silently masked cycles or non-JSON values.
Affected files
src/crdt/MVRegister.ts — equality check.
src/crdt/LWWRegister.ts — equality check.
- Potentially other CRDTs that compare values.
Background
CRDT registers (MV/LWW) need value-equality to decide whether an incoming update is a true mutation or a noop. Current implementations use:
function valuesEqual(a: T, b: T): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
Problems:
- Performance —
JSON.stringify allocates two strings per comparison.
- Cycles — circular references throw. The CRDT model says values should be plain data, but the framework can't enforce it.
- Key-order sensitivity — JSON.stringify doesn't sort keys.
{a: 1, b: 2} ≠ {b: 2, a: 1} by string compare even though the values are semantically equal.
- Special values —
NaN, Infinity, Symbol, Function, BigInt all behave inconsistently.
Target code
// src/util/DeepEqual.ts (new)
/**
* Structural deep equality. Handles:
* - Primitives (===, with NaN === NaN treated as true).
* - Arrays (length + per-element).
* - Plain objects (key set + per-key recursive).
* - Maps, Sets (size + content).
* - Date, RegExp (by canonical form).
* - Uint8Array (byte-wise).
* Throws on cycles (intentional — CRDT values must be acyclic).
*/
export function deepEqual<T>(a: T, b: T): boolean;
Per-CRDT usage:
// MVRegister:
private isSameValue(a: T, b: T): boolean {
return deepEqual(a, b);
}
Integration / risk
- Behaviour change — what JSON.stringify considered equal vs deepEqual may differ in edge cases:
NaN === NaN → deepEqual: true (JSON: NaN serialises to null, both equal).
- Date — JSON serialises differently from deepEqual's
.getTime() compare.
- Map / Set — JSON.stringify doesn't serialise these meaningfully.
- CRDT correctness — better-defined equality is a correctness improvement. But check user-facing behaviour: if users rely on JSON-stringify-equality, they get a different result.
- Performance — typically faster than JSON.stringify; no allocation.
Note on cycles
The framework documents that CRDT register values must be plain JSON-safe data. deepEqual throws on cycles — this surfaces the violation explicitly rather than hanging or silently misbehaving.
Test plan
- Regression — CRDT test suite passes.
- Equality semantics: comprehensive test pack covering primitives, arrays, nested objects, Maps, Sets, special values.
- Performance: equality benchmark on typical CRDT values.
- Cycle detection — throws cleanly.
Acceptance criteria
Coordination
Coordinate with #267 (audit-listed as MVRegister/LWWRegister equality via JSON.stringify duplicate of this) — same scope, close one as duplicate.
Size / Priority
Affected files
src/crdt/MVRegister.ts— equality check.src/crdt/LWWRegister.ts— equality check.Background
CRDT registers (MV/LWW) need value-equality to decide whether an incoming update is a true mutation or a noop. Current implementations use:
Problems:
JSON.stringifyallocates two strings per comparison.{a: 1, b: 2}≠{b: 2, a: 1}by string compare even though the values are semantically equal.NaN,Infinity,Symbol,Function,BigIntall behave inconsistently.Target code
Per-CRDT usage:
Integration / risk
NaN === NaN→ deepEqual: true (JSON: NaN serialises tonull, both equal)..getTime()compare.Note on cycles
The framework documents that CRDT register values must be plain JSON-safe data.
deepEqualthrows on cycles — this surfaces the violation explicitly rather than hanging or silently misbehaving.Test plan
Acceptance criteria
deepEqual<T>(a, b)exported.Coordination
Coordinate with #267 (audit-listed as MVRegister/LWWRegister equality via JSON.stringify duplicate of this) — same scope, close one as duplicate.