◐ Off-By-One · answer catalog

js-lww-element-set-convergence

2 answer(s)jsnode20jsnode20

js-lww-element-set-convergence

📦 Source in repository (JSON)

Answer 1

Key insight: a single "last-writer-wins" timestamp per value is broken for LWW sets — it lets an older remove clobber a newer add, and lets a stale add resurrect a tombstoned value. The correct LWW-Element-Set keeps two monotonic maps per value (add-timestamps and remove-timestamps), and every operation/merge is a per-key max. Membership is decided by comparing the two timestamps.

File: ~/js-lww-element-set-convergence/lww.js

class LWWElementSet {
  constructor() {
    this._adds = new Map(); // value -> highest add timestamp
    this._rems = new Map(); // value -> highest remove timestamp (tombstone)
  }

  add(value, ts) {                    // idempotent: only raises the timestamp
    const cur = this._adds.get(value);
    if (cur === undefined || ts > cur) this._adds.set(value, ts);
  }

  remove(value, ts) {                 // tombstone: only raises the timestamp
    const cur = this._rems.get(value);
    if (cur === undefined || ts > cur) this._rems.set(value, ts);
  }

  has(value) {                        // present iff newest add beats newest tombstone
    const a = this._adds.get(value);
    if (a === undefined) return false;
    const r = this._rems.get(value);
    return r === undefined || a > r;
  }

  values() {
    const out = new Set();
    for (const [v, a] of this._adds) {
      const r = this._rems.get(v);
      if (r === undefined || a > r) out.add(v);
    }
    return out;
  }

  merge(other) {                      // pointwise join: commutative, associative, idempotent
    for (const [v, ts] of other._adds) {
      const cur = this._adds.get(v);
      if (cur === undefined || ts > cur) this._adds.set(v, ts);
    }
    for (const [v, ts] of other._rems) {
      const cur = this._rems.get(v);
      if (cur === undefined || ts > cur) this._rems.set(v, ts);
    }
    return this;
  }

  static merge(a, b) { return new LWWElementSet().merge(a).merge(b); }
  // equals() compares both maps for exact state equality
}

Why it's correct: add, remove, and merge all reduce to max per (set, value) key. Max is commutative, associative, and idempotent, so any interleaving of the same operation multiset yields identical internal maps — convergence follows directly.

Evidence & signatures

Verified in `~/js-lww-element-set-convergence/test.js` with Node v22 (Node 20-compatible), **510/510 tests passing**:

1. **Subtle cases (explicit, 9 tests)** — demonstrated above, including cross-replica merge forms, remove→re-add→remove cycles, tombstones for never-locally-added values, deterministic tie-breaking (`add`/`remove` at equal timestamp → removed), and `values()` accuracy.
2. **Algebraic laws (150 seeds)** — random sets A, B, C; asserts `merge(A,B) == merge(B,A)` (commutative), `merge(merge(A,B),C) == merge(A,merge(B,C))` (associative), `merge(A,A) == A` and 4× repeated self-merge unchanged (idempotent), checked on *internal state*, not just visible membership.
3. **Interleaving convergence (200 seeds)** — 300 ops each (5 values, overlapping timestamps, mixed add/remove) applied to 3 replicas in three different Fisher–Yates-shuffled orders; all replicas equal the ground-truth state and each other.
4. **Merge-graph convergence (150 seeds)** — each op delivered to a random subset of replicas (partial replication), 40 randomized directed merges, then full fan-in; all replicas converge to the join of delivered ops.
5. **Large stress (1 test)** — 5000 ops, each delivered to 1–3 replicas, 200 random merges; full convergence and state equality.

One test-harness bug was caught and fixed during verification (the merge-graph reference originally included ops delivered to *zero* replicas — the replicas themselves converged correctly), confirming the assertions are meaningful.
{"model": "deepseek-v4-flash", "problem_class": "js-lww-element-set-convergence", "result": "passed", "tests": 510}

Answer 2

Key insight: a single "last-writer-wins" timestamp per value is broken for LWW sets — it lets an older remove clobber a newer add, and lets a stale add resurrect a tombstoned value. The correct LWW-Element-Set keeps two monotonic maps per value (add-timestamps and remove-timestamps), and every operation/merge is a per-key max. Membership is decided by comparing the two timestamps.

File: ~/js-lww-element-set-convergence/lww.js

class LWWElementSet {
  constructor() {
    this._adds = new Map(); // value -> highest add timestamp
    this._rems = new Map(); // value -> highest remove timestamp (tombstone)
  }

  add(value, ts) {                    // idempotent: only raises the timestamp
    const cur = this._adds.get(value);
    if (cur === undefined || ts > cur) this._adds.set(value, ts);
  }

  remove(value, ts) {                 // tombstone: only raises the timestamp
    const cur = this._rems.get(value);
    if (cur === undefined || ts > cur) this._rems.set(value, ts);
  }

  has(value) {                        // present iff newest add beats newest tombstone
    const a = this._adds.get(value);
    if (a === undefined) return false;
    const r = this._rems.get(value);
    return r === undefined || a > r;
  }

  values() {
    const out = new Set();
    for (const [v, a] of this._adds) {
      const r = this._rems.get(v);
      if (r === undefined || a > r) out.add(v);
    }
    return out;
  }

  merge(other) {                      // pointwise join: commutative, associative, idempotent
    for (const [v, ts] of other._adds) {
      const cur = this._adds.get(v);
      if (cur === undefined || ts > cur) this._adds.set(v, ts);
    }
    for (const [v, ts] of other._rems) {
      const cur = this._rems.get(v);
      if (cur === undefined || ts > cur) this._rems.set(v, ts);
    }
    return this;
  }

  static merge(a, b) { return new LWWElementSet().merge(a).merge(b); }
  // equals() compares both maps for exact state equality
}

Why it's correct: add, remove, and merge all reduce to max per (set, value) key. Max is commutative, associative, and idempotent, so any interleaving of the same operation multiset yields identical internal maps — convergence follows directly.

Evidence & signatures

Verified in `~/js-lww-element-set-convergence/test.js` with Node v22 (Node 20-compatible), **510/510 tests passing**:

1. **Subtle cases (explicit, 9 tests)** — demonstrated above, including cross-replica merge forms, remove→re-add→remove cycles, tombstones for never-locally-added values, deterministic tie-breaking (`add`/`remove` at equal timestamp → removed), and `values()` accuracy.
2. **Algebraic laws (150 seeds)** — random sets A, B, C; asserts `merge(A,B) == merge(B,A)` (commutative), `merge(merge(A,B),C) == merge(A,merge(B,C))` (associative), `merge(A,A) == A` and 4× repeated self-merge unchanged (idempotent), checked on *internal state*, not just visible membership.
3. **Interleaving convergence (200 seeds)** — 300 ops each (5 values, overlapping timestamps, mixed add/remove) applied to 3 replicas in three different Fisher–Yates-shuffled orders; all replicas equal the ground-truth state and each other.
4. **Merge-graph convergence (150 seeds)** — each op delivered to a random subset of replicas (partial replication), 40 randomized directed merges, then full fan-in; all replicas converge to the join of delivered ops.
5. **Large stress (1 test)** — 5000 ops, each delivered to 1–3 replicas, 200 random merges; full convergence and state equality.

One test-harness bug was caught and fixed during verification (the merge-graph reference originally included ops delivered to *zero* replicas — the replicas themselves converged correctly), confirming the assertions are meaningful.
{"model": "deepseek-v4-flash", "problem_class": "js-lww-element-set-convergence", "result": "passed", "tests": 510}
Generated from the verified corpus · MIT licensedBack to the catalog