This document defines Convergent Replicated Map (CR-Map), a delta CRDT
for indexed membership views keyed by non-empty string member
identifiers. A common presentation is a contacts map whose
keys are random UUID strings and whose values are detached member
objects. Each key resolves to at most one visible value. Every accepted
write is assigned a UUIDv7, and replaced or deleted identifiers are
retained as tombstones.
The specification defines the replica model, snapshot and delta formats, merge rules, acknowledgement and garbage-collection rules, and one conforming JavaScript binding.
CR-Map is designed to serve as a small replicated core for indexed membership state, gossip protocols, and local-first synchronization.
This specification is maintained alongside a reference implementation.
To use the reference implementation see:
Inspired by [[MARTIN_KLEPPMANN]].
Unless otherwise stated, data-structure and algorithm terms such as list, set, map, record, parameters, variables, and iterate are used with the meanings defined by Infra [[INFRA]].
A conforming implementation MUST preserve the visible membership convergence, reply-delta behavior, and tombstone-safety constraints defined by the algorithms in this specification.
A CR-Map replica is one independently mutable instance of the data structure.
A UUIDv7 identifier is an identifier that is a valid UUID version 7 as defined by [[RFC9562]].
A serializable object is an object that supports structured serialization and later structured deserialization as defined by [[HTML]].
A map key is a non-empty string naming one visible member in a CR-Map replica. In the intended use case it is typically a randomly generated UUID string.
A map value is the consumer-defined member payload presently selected for one map key.
A state entry is the internal record for one visible key,
consisting of a winning uuidv7, a winning
value, and a winning predecessor.
A predecessor identifier is a UUIDv7 identifier referenced by a state entry as the immediate causal predecessor of that winning write for the same map key.
A tombstone entry is a retained UUIDv7 identifier representing a replaced, deleted, or otherwise dominated write.
A live projection is the current indexed membership view formed by the visible state entry(s) of a CR-Map replica.
A snapshot is the full serializable object representation of a CR-Map replica.
A delta is a partial snapshot containing only the value entries or tombstones a replica wishes to share.
An acknowledgement frontier is the largest retained tombstone entry of one replica under lexicographic comparison.
A reply delta is a delta emitted by merge when local state already dominates an incoming contender and the replica chooses to rebut with its current winning information.
A CR-Map replica stores zero or more visible state entry(s) keyed by member identifiers. Each visible key has at most one winning entry at a time.
Every accepted local write mints a fresh winning UUIDv7 identifier. If the key already had a winner, that previous winning identifier becomes the new write's predecessor identifier and is retained as a tombstone entry. If the key did not yet exist, the replica still mints a fresh predecessor identifier and retains it as a tombstone so that the first write has a stable predecessor anchor.
Deleting a key removes its visible entry from the live projection and retains that deleted winner's UUIDv7 as a tombstone entry.
On merge, incoming tombstones can immediately delete locally visible winners. Incoming value entries are resolved independently per key. Direct descendants win over the winner they descend from, same-UUID contenders can advance through a larger predecessor, and otherwise the lexicographically larger UUIDv7 wins.
When an incoming contender loses against already-dominant local state, a conforming implementation MAY emit a reply delta containing the local winner and, when applicable, the losing contender's UUIDv7 as a tombstone.
The convergence target is the visible live projection. Internal tombstone sets need not be identical at every intermediate step, but acknowledgement-driven garbage collection MUST NOT remove tombstones still required for future convergence.
Snapshot and delta ingress is intentionally tolerant. Unknown top-level members, malformed entries, invalid UUIDs, invalid keys, and non-cloneable value payloads are ignored rather than becoming visible state.
The same logical replica can be viewed through three useful representations: the detached structured-clone snapshot used for transport and storage, one possible runtime projection used internally by a binding, and the final consumer-facing indexed membership view.
values and relations are empty maps and
whose tombstones and predecessors are
empty sets.
tombstones member and that
member is a list:
values member, or
that member is not a list, then return replica.
uuidv7, then continue.
uuidv7 as the candidate, and its
predecessor is lexicographically greater than or
equal to the candidate's predecessor, then continue.
uuidv7, is not the candidate's direct predecessor,
and its uuidv7 is lexicographically greater than or
equal to the candidate's uuidv7, then continue.
uuidv7 from the
relations index.
predecessor from
the predecessors set.
uuidv7 back to its key in the
relations index.
predecessor in the
predecessors set.
structuredClone. If that fails, return failure.
uuidv7, if old
exists.
uuidv7 is a newly minted UUIDv7 identifier,
whose value stores the key and detached payload, and
whose predecessor is predecessor.
uuidv7 to the key in the
relations index.
values list contains the new
winning snapshot value entry and whose
tombstones list contains predecessor;
and
CRMapChange mapping the key to a detached clone
of the new visible value.
uuidv7 as a
tombstone entry.
tombstones list contains
only the deleted winner's UUIDv7; and
CRMapChange mapping that key to
undefined.
tombstones list is empty.
CRMapChange.uuidv7 as a
tombstone entry.
tombstones list.
undefined.CRMapChange.tombstones member and
that member is a list:
uuidv7 is not equal to the tombstone being
processed:
undefined.
values member,
or that member is not a list:
uuidv7, then continue.
uuidv7 to the relations
index.
predecessor to the
predecessors set.
predecessor as a
tombstone.
uuidv7 equals the
candidate's uuidv7:
values list.
uuidv7 is lexicographically greater than the
current winner's uuidv7:
uuidv7 to the key in the
relations index.
tombstones list.
values list.
''.''.This algorithm is only safe when the caller supplies acknowledgement frontiers covering every replica whose future convergence must still be preserved. Supplying only a partial frontier set is caller misuse and does not guarantee that an offline replica can later catch up from deltas alone.
values list
is empty and whose tombstones list contains all
retained tombstones.
values list.
This section defines one conforming JavaScript binding for the model above.
callback CRMapForEachCallback = undefined (any value, DOMString key, CRMap map);
[Exposed=*]
interface CRMap {
constructor(optional object snapshot = {});
readonly attribute unsigned long size;
any get(DOMString key);
boolean has(DOMString key);
undefined set(DOMString key, any value);
undefined delete(DOMString key);
undefined clear();
undefined merge(optional object delta = {});
undefined acknowledge();
undefined garbageCollect(sequence<DOMString> frontiers);
undefined snapshot();
sequence<DOMString> keys();
sequence<any> values();
sequence<sequence<any>> entries();
undefined forEach(CRMapForEachCallback callback, optional any thisArg);
undefined addEventListener(
DOMString type,
EventListener? listener,
optional (AddEventListenerOptions or boolean) options = {}
);
undefined removeEventListener(
DOMString type,
EventListener? listener,
optional (EventListenerOptions or boolean) options = {}
);
object toJSON();
stringifier DOMString ();
iterable<DOMString, any>;
};
dictionary CRMapKeyValue {
required DOMString key;
required any value;
};
dictionary CRMapSnapshotValueEntry {
required DOMString uuidv7;
required CRMapKeyValue value;
required DOMString predecessor;
};
dictionary CRMapSnapshot {
required sequence<CRMapSnapshotValueEntry> values;
required sequence<DOMString> tombstones;
};
dictionary CRMapDelta {
sequence<CRMapSnapshotValueEntry> values;
sequence<DOMString> tombstones;
};
typedef record<DOMString, any> CRMapChange;
typedef DOMString CRMapAck;
The constructor and merge() method are
typed as taking generic objects in the coarse Web IDL summary above so
that the IDL matches the permissive JavaScript ingress behavior. The
expected object shapes are CRMapSnapshot and
CRMapDelta as defined earlier.
size returns the current number of visible members.
get(),
values(),
entries(),
forEach(), and for...of
expose detached structured clones rather than mutable references into
replica state.
When the current snapshot is JSON-compatible,
JSON.stringify(map) serializes the binding through that
snapshot. for...of iterates detached
[key, value] pairs for the current live projection, and
toString() attempts to return the JSON string form of the
current snapshot.
The JavaScript binding throws CRMapError with a string
code attribute for local API misuse.
INVALID_KEYVALUE_NOT_CLONEABLEstructuredClone.
The JavaScript binding maintains an internal
EventTarget object as defined by DOM [[DOM]].
The binding forwards addEventListener() and removeEventListener() to that internal event target. Local mutations, merges, acknowledgement emission, and snapshot emission dispatch synthetic events through the same target.
The JavaScript binding defines four synthetic event types. Each is a
CustomEvent object whose detail attribute is
initialized as described below [[DOM]].
CustomEvent whose detail is a
delta. The binding dispatches this event for local writes and
for merges that emit a non-empty reply delta.
CustomEvent whose detail is a sparse
visible patch mapping changed member identifiers to their new
visible values, with deleted keys mapped to undefined.
CustomEvent whose detail is the current
acknowledgement frontier produced when
acknowledge() observes at least one
retained valid tombstone.
CustomEvent whose detail is the full
current snapshot produced when
snapshot() is invoked.
set(), delete(), and clear() MUST dispatch delta event before change event. merge() MAY dispatch delta event, change event, or both, and when both are dispatched the delta event MUST be dispatched first. acknowledge() MUST dispatch an ack event only when the computed frontier is non-empty, and snapshot() MUST dispatch a snapshot event.
value member,
then return failure.
uuidv7 is not a valid
UUIDv7 identifier, then return failure.
predecessor is not a valid
UUIDv7 identifier, then return failure.
value member is not a record, then
return failure.
structuredClone. If cloning fails, then return
failure.
Return a new serializable object whose uuidv7,
value.key, value.value, and
predecessor members equal the current
state entry after detaching the payload with
structuredClone.
Construct a new binding whose internal state is the result of the core create algorithm applied to the optional snapshot.
Return the current number of visible members in the live projection.
Return a detached copy of the visible map value for the
supplied key, or undefined when the key is absent.
Return true when the supplied key is presently visible in the live projection.
Run the core update algorithm for the supplied key and visible
value, then dispatch the resulting delta event and
change event when the write is accepted. If the key is not a
non-empty string or the value is not supported by
structuredClone, throw CRMapError.
Delete the single visible key supplied by the caller and dispatch
the resulting delta event and change event when a live
key was removed. If the key is not a non-empty string, throw
CRMapError.
Delete every visible key from the replica and dispatch the resulting delta event and change event when the map was non-empty.
Merge the supplied delta into the internal replica and dispatch any resulting delta event or change event.
Compute the current acknowledgement frontier and dispatch an ack event when that frontier is non-empty.
Run the core garbage-collection algorithm with the supplied list of acknowledgement frontiers.
Create the current full snapshot and dispatch a
snapshot event. The method returns undefined.
Return the current visible member identifiers in map iteration order.
Return detached copies of the current visible map value(s) in map iteration order.
Return detached [key, value] pairs for the current
live projection in map iteration order.
Iterate detached [key, value] pairs for the current
live projection in map iteration order.
Call the supplied callback once for each detached visible map value in map iteration order, passing value, key, and the map object.
Return the same detached structured-clone-compatible shape as
snapshot. This is the value used by
JSON.stringify.
Attempt to return the JSON string form of the current snapshot.
Forward listener registration and listener removal to the binding's
internal EventTarget object using the provided type,
listener, and options values.