This document defines Convergent Replicated Set (CR-Set), a delta CRDT for a membership collection whose logical value is a list without duplicates. Each value is identified by a content key derived from its deterministic MessagePack bytes, SHA-256 digest, and unpadded Base64URL digest string.
The specification defines the content-addressing rules, snapshot and delta shapes, merge and garbage-collection behavior inherited from CR-Map, and one conforming JavaScript binding.
This specification is maintained alongside a reference implementation.
You can check the reference implementation docs:
See it in action:
Or use the reference implementation:
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 content-key identity rules, visible membership convergence, reply-delta behavior, and tombstone-safety constraints defined by this specification and by its CR-Map substrate [[CRMAP]].
A CR-Set replica is one independently mutable instance of the data structure.
A set value is a consumer-defined payload that can be encoded into deterministic MessagePack bytes and copied with structured clone.
A value byte sequence is the byte sequence produced by the value encoding algorithm defined in this specification. The encoding is based on MessagePack [[MESSAGEPACK]].
A content digest is the SHA-256 digest of a value byte sequence, computed as defined by RFC 4634 [[RFC4634]] and FIPS 180-4 [[FIPS1804]].
A content key is the unpadded Base64URL string form of a content digest, using the URL and filename safe alphabet defined by RFC 4648 [[RFC4648]].
A set value list is the consumer-facing collection returned by reads and iteration. It contains at most one visible set value for each content key.
A UUIDv7 identifier is an identifier that is a valid UUID version 7 as defined by [[RFC9562]].
The underlying CR-Map key used to store one set member is always a content key.
A state entry is the underlying CR-Map state entry for one
content key, consisting of a uuidv7,
value.key, value.value, and
predecessor.
A tombstone entry is a retained UUIDv7 identifier representing a replaced, deleted, or otherwise dominated underlying CR-Map write.
A live projection is the set of currently visible state entry(s), keyed by content key.
A snapshot is the full structured-clone-compatible serialized state of a CR-Set replica.
A delta is a partial snapshot containing only the values 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 return its current local state.
A CR-Set replica is a content-addressed projection over CR-Map [[CRMAP]]. The underlying CR-Map key is not supplied by callers. It is derived from the current content of the set value.
The logical membership target is a set value list: a collection that behaves as a list of visible values without duplicate content keys. The order exposed by collection views is an implementation projection over the current CR-Map state. Convergence is defined over membership by content key, not over a separate user-visible ordering relation.
Adding a value computes its content key. If that key is already visible, the operation is a no-op. Otherwise the replica writes the value into the underlying CR-Map under that key. Deleting a value computes the same key and removes that one visible map entry when it exists. Clearing removes every visible map entry.
Snapshot, delta, merge, acknowledgement, reply-delta, and garbage-collection semantics are inherited from CR-Map. Incoming data is tolerant: malformed top-level values, invalid UUIDv7 identifiers, invalid tombstones, non-string keys, and non-cloneable payloads are ignored by the underlying merge and hydration algorithms rather than becoming visible state.
A SHA-256 collision causes two different encoded values to share one content key. CR-Set treats such values as the same logical member, because the content key is the identity boundary of the data structure.
The same logical replica can be viewed through three useful representations: the detached snapshot used for transport and storage, the underlying CR-Map runtime projection, and the consumer-facing duplicate-free value list.
{
"values": [
{
"uuidv7": "019b4f64-3a52-7000-8000-000000000001",
"value": {
"key": "h4tE2gk7qfP0QhF3vOVH1q2T7bPBmDOs5wv0ZrC9uQM",
"value": { "id": "alpha", "active": true }
},
"predecessor": "019b4f64-3a52-7000-8000-000000000000"
}
],
"tombstones": ["019b4f64-3a52-7000-8000-000000000000"]
}
{
values: Map<ContentKey, StateEntry>,
relations: Map<UUIDv7, ContentKey>,
tombstones: Set<UUIDv7>,
predecessors: Set<UUIDv7>
}
[
{ "id": "alpha", "active": true },
{ "id": "beta", "active": false }
]
VALUE_NOT_ENCODABLE.
value.value.
VALUE_NOT_CLONEABLE.
value.key is not a string
or whose value.value cannot be structured-cloned are
ignored by the
transform a snapshot value entry to a state entry
algorithm.
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.
This section defines one conforming JavaScript binding for the model
above. The binding is a collection interface over the replicated
content-key model. It intentionally does not adopt every ECMAScript
Set behavior [[ECMA262]].
callback CRSetForEachCallback = undefined (any value, CRSet set);
[Exposed=*]
interface CRSet {
constructor(optional object snapshot = {});
readonly attribute unsigned long size;
undefined add(any value);
boolean has(any value);
undefined delete(any value);
undefined clear();
sequence<any> values();
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(CRSetForEachCallback callback, optional any thisArg);
object toJSON();
stringifier DOMString ();
iterable<any>;
};
dictionary CRSetSnapshotValuePayload {
required DOMString key;
required any value;
};
dictionary CRSetSnapshotValueEntry {
required DOMString uuidv7;
required CRSetSnapshotValuePayload value;
required DOMString predecessor;
};
dictionary CRSetSnapshot {
required sequence<CRSetSnapshotValueEntry> values;
required sequence<DOMString> tombstones;
};
dictionary CRSetDelta {
sequence<CRSetSnapshotValueEntry> values;
sequence<DOMString> tombstones;
};
typedef record<DOMString, any> CRSetChange;
typedef DOMString CRSetAck;
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 CRSetSnapshot and
CRSetDelta as defined earlier.
The key member exposed in snapshot entries is the derived
content key, not a caller-provided property. The
value member holds the original set member payload.
size returns the current number of visible content keys.
values(),
forEach(), and for...of
expose detached structured clones rather than mutable references into
replica state.
When the current snapshot is JSON-compatible,
JSON.stringify(set) serializes the binding through that
snapshot. toString() attempts to return the JSON string
form of the current snapshot.
The JavaScript binding throws CRSetError with a string
code attribute for local API misuse.
VALUE_NOT_ENCODABLEVALUE_NOT_CLONEABLE
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
deletes, and for merges that emit a non-empty reply delta.
CustomEvent whose detail is a sparse
visible membership patch mapping changed content key(s) 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.
add(), delete(), and clear() MUST dispatch delta event before change event when the operation changes membership. merge() MAY dispatch a delta event, a 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.
value.key is not a non-empty string,
then return failure.
value.value payload
with structuredClone [[HTML]]. If cloning fails, then
return failure.
Return a new structured-clone-compatible object whose
uuidv7, value.key,
value.value, and predecessor members equal
the current state entry after detaching the payload with
structuredClone [[HTML]].
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 content keys in the live projection.
Run the content-key derivation algorithm for the supplied value. If the key is absent, write the value into the underlying CR-Map state, then dispatch the resulting delta event and change event. If the key is already visible, return without dispatching events.
Return true when the supplied value's derived content key is presently visible in the live projection.
Run the content-key derivation algorithm for the supplied value, delete that visible member when present, and dispatch the resulting delta event and change event. If the key is absent, return without dispatching events.
Delete every visible member from the replica and dispatch the resulting delta event and change event when the set was non-empty.
Return detached copies of the current visible set value(s).
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.
Iterate detached copies of the current visible set value(s).
Call the supplied callback once for each detached visible set value, passing value and the set 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.