G/PN-Counter CRDTLoading saved progress…

G/PN-Counter CRDT

A PN-Counter is a conflict-free replicated data type (CRDT): a counter that any number of replicas can increment and decrement on their own, then merge in any order to converge on the exact same total. It solves a problem a plain number cannot — when several servers each accept writes with no central coordinator (a like count, a stock tally, a live-viewer number), you have to reconcile their edits later without losing any and without double-counting. You build it from two grow-only maps: P records how much each node has incremented, N how much each node has decremented, and the value is their difference.

Signature

type State = { P: Record<string, number>; N: Record<string, number> };

type Counter = {
  increment(n?: number): Counter;         // add n (default 1) to this node's increments
  decrement(n?: number): Counter;         // add n (default 1) to this node's decrements
  value(): number;                        // sum(P) - sum(N)
  merge(other: Counter | State): Counter; // fold in another replica by per-node max
  state(): State;                         // a serializable { P, N } snapshot
};

function gPnCounterCrdt(nodeId: string): Counter;

Examples

const c = gPnCounterCrdt('node-a');
c.increment();   // +1
c.increment(4);  // +4
c.decrement(2);  // -2
c.value();       // 3
const a = gPnCounterCrdt('a');
const b = gPnCounterCrdt('b');

a.increment(3); // a only knows its own +3
b.increment(5); // b only knows its own +5

a.merge(b);     // a folds in b by per-node maximum
a.value();      // 8 — both increments counted, neither lost

a.merge(b);     // the same state redelivered...
a.value();      // 8 — merge is idempotent

Notes

  • Per-node slots — each replica owns one slot in P and one in N, keyed by its nodeId. A replica writes only its own slot; every other slot arrives through merge.
  • value() is a difference — add up every node's increments in P, then subtract every node's decrements in N. Both maps only ever grow.
  • Undo with decrement, never subtraction — to lower the count you decrement (which grows N); you never subtract from P. That grow-only shape is what lets replicas merge safely.
  • merge keeps the larger total — for every nodeId in either replica, keep the bigger P value and the bigger N value. This per-node maximum is what makes the merge convergent, commutative, and idempotent.
  • Deterministic ids — pass a stable, unique nodeId into the factory (a host name or uuid in production). Never key a slot by Math.random() or a timestamp, or two replicas could clobber each other.
  • Do not worry about — network transport, garbage-collecting retired nodes, or integer overflow; the whole exercise is the in-memory merge logic.

FAQ

What is the difference between a G-Counter and a PN-Counter?
A G-Counter is grow-only: it can increment but never decrement. A PN-Counter pairs two G-Counters, one for increments (P) and one for decrements (N), so its value is sum(P) minus sum(N) and it supports both directions.
Why does merge take the maximum per node instead of adding the totals?
Maximum is idempotent and order-independent, so a redelivered or reordered merge never changes the result. Adding the totals would double-count every time the same state arrives again, so the counter would drift instead of converging.
What are the time and space complexity of value and merge?
Both value() and merge() are O(k) where k is the number of nodes that have ever written. The state holds one P slot and one N slot per such node, so space is also O(k).
Loading editor…