This document defines Convergent Replicated List (CR-List), a delta CRDT for an ordered sequence of entries. Each index resolves to a single visible value. Every update is assigned a UUIDv7, and deletions are tracked as a list of UUIDv7 identifiers.
The specification defines the data structure, merge rules, snapshot and delta formats, garbage-collection rules, acknowledgement formats, and one conforming JavaScript binding.
CR-List is designed to serve as a core for use-case-optimized CRDT interfaces, such as text or arrays.
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, map, struct, parameters, variables, and iterate are used with the meanings defined by Infra [[INFRA]].
A conforming implementation MUST preserve the replica convergence and deletion semantics defined by the algorithms in this specification.
A CR-List 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 linked list is a list implemented by each item having a link to the next item. [[LINKED_LIST]].
a doubly linked list is a variant of a linked list in which each item has a link to the previous item as well as the next. [[DOUBLY_LINKED_LIST]].
a serializable object is an object that supports being serialized, and later deserialized as defined by [[HTML]].
A predecessor identifier is an UUIDv7 identifier or
the root sentinel '\0' pointing to what list value a
list value comes after.
A list index is the position of a list value, starting at 0 and ending at the list length, in the resolved conflict-free ordering of a CR-List replica.
A list value is the consumer defined payload presently selected for a given list index.
The list length is the number of list value(s) in a CR-List replica.
A replica state is a struct consisting of replica values and replica tombstones tracked by CR-List replica.
The replica values is the set of 0 or more values entry(s) tracked by replica state.
A values entry is a struct consisting of a
UUIDv7 Identifier, list value, and
predecessor identifier tracked by replica values. A
conforming runtime MAY additionally project those same entries into a
local doubly linked list by tracking members such as
prev, next, and index.
The replica tombstones is a list of 0 or more overwritten tombstones entry(s) tracked by replica state.
A tombstones entry is a UUIDv7 identifier representing a deleted list value tracked by replica tombstones.
An acknowledgement is a serializable string containing the current acknowledgement frontier of a single CR-List replica.
An acknowledgement frontier is the largest tombstones entry presently retained by replica tombstones under lexicographic comparison.
A snapshot is a serializable object presentation of a replica state.
A delta is a partial snapshot containing only the entries a CR-List replica wishes to share.
A CR-List replica stores live values entry(s) indexed by their UUIDv7 identifier, groups them by predecessor identifier, and stores deleted identifiers in replica tombstones. The visible ordered list is a deterministic projection derived from that state.
To resolve the visible projection, a conforming implementation ignores
every live candidate whose own UUIDv7 identifier is listed in
replica tombstones. It then clears every live entry's
prev and next members, sorts the live
sibling-bucket keys lexicographically by predecessor identifier,
and resolves those buckets in repeated passes. A bucket is resolvable
when its predecessor is the root predecessor '\0', when its
predecessor is not presently found in live state, or when its
predecessor has already been linked into the visible projection.
Within a resolved bucket, siblings sharing the same predecessor identifier are sorted lexicographically by UUIDv7 and linked immediately after their visible predecessor, if any, otherwise at the current visible tail. After all resolvable buckets have been linked, the implementation walks to the visible tail and then back toward the head, assigning descending indexes so that the final head-to-tail visible order is zero-based and starts at 0.
Reads and local mutations MAY reuse a mutable cursor into that linked list. This gives best-case O(1) local access when the cursor is already at or near the target index and worst-case O(n) access when a full walk of the visible list is required.
A snapshot or delta input MAY contain malformed or unknown members. A conforming implementation processes only the known top-level members and ignores entries that do not successfully parse.
The same logical replica can be viewed through three useful representations: the serializable snapshot used for transport and storage, one possible runtime projection used internally by a binding, and the final consumer-facing visible sequence.
tombstones member whose value
is a list, then for each listed item:
values member whose
value is a list, then return the result.
prev and next members.
undefined.
structuredClone. If copying fails, then fail.
after, insert the copied values in the
given order after the target index. If the target index equals the
current list length, append at the tail.
before, insert the copied values in
the given order before the target index.
overwrite, replace the visible range
beginning at the target index and extending for the number of
supplied values. If the visible list ends first, append the
remaining copied values at the tail.
[startIndex, endIndex).
undefined.
tombstones member whose
value is a list, then for each listed item:
values member
whose value is a list, then return the tombstone-only change, if
any.
uuidv7, value, and
predecessor members, or its
uuidv7 or predecessor member is not
valid, continue.
uuidv7, continue.
prev and
next members.
This algorithm is only safe when the caller supplies acknowledgement(s) 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.
tombstones list.
This section defines one conforming JavaScript binding for the model above.
callback CRListForEachCallback = undefined (any value, unsigned long index, CRList list);
[Exposed=*]
interface CRList {
constructor(optional object snapshot = {});
readonly attribute unsigned long size;
undefined append(any value, optional unsigned long afterIndex);
undefined prepend(any value, optional unsigned long beforeIndex);
undefined remove(unsigned long index);
undefined merge(optional object delta = {});
undefined acknowledge();
undefined garbageCollect(sequence<DOMString> frontiers);
undefined snapshot();
undefined addEventListener(
DOMString type,
EventListener? listener,
optional (AddEventListenerOptions or boolean) options = {}
);
undefined removeEventListener(
DOMString type,
EventListener? listener,
optional (EventListenerOptions or boolean) options = {}
);
undefined forEach(CRListForEachCallback callback, optional any thisArg);
object toJSON();
iterable<any>;
};
dictionary CRListSnapshotValueEntry {
required DOMString uuidv7;
required any value;
required DOMString predecessor;
};
dictionary CRListSnapshot {
required sequence<CRListSnapshotValueEntry> values;
required sequence<DOMString> tombstones;
};
dictionary CRListDelta {
sequence<CRListSnapshotValueEntry> values;
sequence<DOMString> tombstones;
};
The binding additionally exposes numeric property access through a
JavaScript Proxy. Reading list[i] returns
a detached copy of the visible value at index i. Writing
list[i] = value performs an overwrite-style local mutation, and
delete list[i] removes one visible entry.
The constructor and merge() methods 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 CRListSnapshot and
CRListDelta as defined by the dictionaries above.
The root predecessor is represented by the null character
'\0', which serializes to "\u0000" in JSON.
JSON.stringify(list) serializes the binding through its
current snapshot, and for...of iterates detached
copies of the current visible values in index order.
The binding also exposes a JavaScript toString() method
whose return value is the JSON string form of the current snapshot.
The JavaScript binding throws CRListError with a string
code attribute for local API misuse.
VALUE_NOT_CLONEABLEstructuredClone.
INDEX_OUT_OF_BOUNDSUPDATE_EXPECTED_AN_ARRAYLIST_INTEGRITY_VIOLATION
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 and merges 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 visible
mutations that produce gossip material.
CustomEvent whose detail maps changed
list indexes to their new visible values, or to
undefined when an entry was removed.
CustomEvent whose detail is the current
acknowledgement frontier produced when
acknowledge() is invoked.
CustomEvent whose detail is the full
current snapshot produced when
snapshot()
is invoked.
Numeric writes, numeric deletes, append(), prepend(), and remove() MUST dispatch delta event before change event. A merge MUST dispatch only change event when the visible live projection changes. acknowledge() MUST dispatch ack event when a frontier exists, and snapshot() MUST dispatch a snapshot event.
uuidv7,
value, or predecessor member, then
return failure.
uuidv7 member is not a valid
UUIDv7 identifier, then return failure.
predecessor member is
neither a valid UUIDv7 identifier nor '\0',
then return failure.
value member cannot be
copied with structuredClone, then return failure.
uuidv7, then return failure.
Return a new serializable object whose uuidv7,
value, and predecessor members equal the
live value entry, after shedding any local projection members such
as prev, next, and index. The
copied value MUST be a detached structured clone.
Construct a new binding whose internal state is the result of the core create algorithm applied to the optional snapshot.
Return the current list length.
Reading list[i] returns the current visible
list value copy at index i, or
undefined when the index is out of bounds. Writing
list[i] = value runs a one-value local overwrite
mutation at that index and appends when i equals the
current list length. Deleting
delete list[i] deletes the single-item visible range
[i, i + 1).
Insert one visible value after the supplied index. If no index is supplied, append at the current tail.
Insert one visible value before the supplied index. If no index is supplied, insert at the current head.
Delete the single visible entry at the supplied index.
Merge the supplied delta into the internal replica and, if the visible list changes, dispatch a change event whose detail is the minimal visible patch.
Compute the current acknowledgement frontier and dispatch an ack event when a frontier exists.
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.
Iterate detached copies of the current visible list value(s) in ascending list index order.
Call the supplied callback once for each visible list value copy in ascending list index order, passing value, index, and the list object.
Return the same serialized shape as snapshot. This is the
value used by JSON.stringify.
Forward listener registration and listener removal to the binding's
internal EventTarget object using the provided type,
listener, and options values.