Skip to content

Maps (LWWMap, ORMap, GCounterMap)

Three map CRDTs, picked by what the values are:

TypeValue typeWhen
LWWMap<K, V>Plain values (LWW per key)Per-key config — flags, settings.
ORMap<K, C>Other CRDTsPer-key counters, sets, registers.
GCounterMap<K>Implicit GCounter valuesPer-key counts (clicks per page, requests per route).

GCounterMap<K> is sugar for ORMap<K, GCounter> — the common case so common it gets its own type.

import { LWWMap } from 'actor-ts';
let m = LWWMap.empty<string, string>();
m = m.put('node-a', 'theme', 'dark', 1000);
m = m.put('node-b', 'lang', 'en', 2000);
m.get('theme'); // → 'dark'
m.get('lang'); // → 'en'

Per-key LWW semantics — same as LWWRegister, but indexed. Concurrent writes to the same key resolve by timestamp; writes to different keys don’t conflict.

m = m.remove('theme'); // also LWW — most-recent action wins

remove is also tracked as an LWW operation — a recent remove beats an older put, even from a different replica.

Use for per-key single-value config: user preferences, feature flag overrides per-tenant, version per-resource.

import { ORMap, GCounter } from 'actor-ts';
let m = ORMap.empty<string, GCounter>();
m = m.update('node-a', 'clicks',
GCounter.empty,
(c) => c.increment('node-a', 1),
);
m = m.update('node-b', 'views',
GCounter.empty,
(c) => c.increment('node-b', 1),
);
m.get('clicks')?.value(); // → 1

The values are themselves CRDTs. Each key’s value merges using the value-CRDT’s own mergeGCounter keys merge per-replica-max, ORSet keys add-wins, etc.

m = m.update('a', 'tags',
() => ORSet.empty<string>(),
(s) => s.add('a', 'urgent'),
);

Heterogeneous value types are allowed but discouraged — a single ORMap typically holds one CRDT kind per map. If different keys hold different kinds, the framework can’t infer types and you fight the compiler.

Use for per-key replicated state that needs CRDT semantics on each key — per-user shopping carts, per-feature counter + last-changed register.

m = m.remove('clicks');

Removing a key from ORMap uses the OR-semantics — tagged operation that concurrent re-adds can override. Same “add wins” semantics as ORSet.

import { GCounterMap } from 'actor-ts';
let m = GCounterMap.empty<string>();
m = m.increment('node-a', 'page-home', 1);
m = m.increment('node-a', 'page-about', 1);
m = m.increment('node-b', 'page-home', 1);
m.get('page-home'); // → 2 (sum across replicas)
m.get('page-about'); // → 1

A GCounterMap<K> is functionally ORMap<K, GCounter> — a map where each value is a counter. It exists because per-key counts are common enough (page views per URL, clicks per button, requests per route) to warrant a typed shortcut.

The API is flatter:

m.increment(replicaId, key, delta); // bump one key's counter
m.get(key); // → number (sum across replicas)
m.entries(); // → IterableIterator<[K, number]>

Use anywhere you’d type Map<K, number> in regular code and need concurrent-write convergence.

Per-key value type?
├── A plain value with last-writer-wins resolution → LWWMap
├── A CRDT (counter, set, register, another map) → ORMap
└── A counter (very common case) → GCounterMap (sugar over ORMap<K, GCounter>)

Common shapes:

  • Per-user preferencesLWWMap<UserId, UserPrefs> (user pref edits are LWW; conflicts are rare).
  • Per-tenant feature flagsLWWMap<TenantId, FlagSet>.
  • Per-resource view countsGCounterMap<ResourceId>.
  • Per-room online membersORMap<RoomId, ORSet<UserId>>.
  • Per-shard rebalance countsGCounterMap<ShardId>.
// Per-room subscription set + per-room message count
let rooms = ORMap.empty<string, ORSet<string>>();
let counts = GCounterMap.empty<string>();
rooms = rooms.update('a', 'general',
() => ORSet.empty<string>(),
(s) => s.add('a', 'user-1'),
);
counts = counts.increment('a', 'general', 1);

ORMap of ORSet is the common shape for map of sets. ORMap of LWWRegister<T> is a typed version of LWWMap<K, T> with all the same semantics (use LWWMap directly when that’s what you mean).

The LWWMap, ORMap, and GCounterMap API references cover the full surface.