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]].

Terminology

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.

Core Model

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.

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.

Snapshot

{
  "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"]
}
        

Runtime

{
  values: Map<ContentKey, StateEntry>,
  relations: Map<UUIDv7, ContentKey>,
  tombstones: Set<UUIDv7>,
  predecessors: Set<UUIDv7>
}
        

Consumer

[
  { "id": "alpha", "active": true },
  { "id": "beta", "active": false }
]
        

Core Algorithms

Derive Content Key

  1. Let value be the supplied set value.
  2. Attempt to encode value as MessagePack with deterministic map-key sorting enabled. If encoding fails, then fail with VALUE_NOT_ENCODABLE.
  3. Let bytes be the resulting value byte sequence.
  4. Let digest be SHA-256(bytes) [[RFC4634]] [[FIPS1804]].
  5. Let key be the unpadded Base64URL encoding of digest [[RFC4648]].
  6. Return key.

Create

  1. Let replica be a new underlying CR-Map replica hydrated from the supplied optional snapshot according to CR-Map's create algorithm [[CRMAP]].
  2. Ignore snapshot entries that the CR-Map create algorithm rejects.
  3. Return replica as a CR-Set replica.

Read

  1. To test membership for value, run derive content key with value.
  2. Return true if the underlying live projection contains that content key; otherwise return false.
  3. To enumerate values, iterate the underlying visible CR-Map state and return detached structured clones of each stored value.value.

Update

  1. Run derive content key with the supplied value.
  2. If the derived content key is already visible in the underlying live projection, then return no change.
  3. Run CR-Map's local update algorithm with the derived content key as key and the supplied value as payload [[CRMAP]].
  4. If the underlying update cannot structured-clone the value, then fail with VALUE_NOT_CLONEABLE.
  5. Return the CR-Map update result as a CR-Set delta and membership change.

Delete

  1. Run derive content key with the supplied value.
  2. If the derived content key is not visible in the underlying live projection, then return no change.
  3. Run CR-Map's keyed delete algorithm with the derived content key [[CRMAP]].
  4. Return the resulting tombstone-only delta and change.

Clear

  1. If the underlying live projection is empty, then return no change.
  2. Run CR-Map's clear algorithm [[CRMAP]].
  3. Return the resulting tombstone-only delta and membership change.

Merge

  1. If the supplied candidate is not a record, then return no change.
  2. Run CR-Map's merge algorithm over the underlying replica state [[CRMAP]].
  3. Incoming value entries whose 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.
  4. If merge accepts remote values or tombstones, return the visible membership change. If local state dominates incoming contenders, return any reply delta.
  5. A conforming implementation MAY dispatch a reply delta, a visible change event, or both, matching the CR-Map merge result.

Acknowledge

  1. Compute the underlying CR-Map acknowledgement frontier [[CRMAP]].
  2. Return the largest retained valid tombstone entry, or the empty string when no valid tombstone exists.

Garbage Collect

  1. If the supplied list of acknowledgement frontiers is not a list or is empty, then return.
  2. Run CR-Map's garbage-collection algorithm with the supplied frontiers [[CRMAP]].
  3. Remove only tombstones at or below the smallest valid supplied frontier, while preserving identifiers still referenced as live predecessors by the underlying CR-Map state.

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.

Snapshot

  1. Run CR-Map's snapshot algorithm over the underlying replica state [[CRMAP]].
  2. Return a new snapshot containing all visible value entries produced by the transform a state entry to a snapshot value entry algorithm and all retained tombstones.

Reference JavaScript Binding

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]].

API Summary

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.

Errors

The JavaScript binding throws CRSetError with a string code attribute for local API misuse.

VALUE_NOT_ENCODABLE
Thrown when add(), has(), or delete() receives a value that cannot be encoded as deterministic MessagePack for content-key derivation.
VALUE_NOT_CLONEABLE
Thrown when add() receives a value that can be content-keyed but cannot be copied into replica state with structured clone.

Events

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]].

delta event
A 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.
change event
A 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.
ack event
A CustomEvent whose detail is the current acknowledgement frontier produced when acknowledge() observes at least one retained valid tombstone.
snapshot event
A 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.

Auxiliary Algorithms

Transform a Snapshot Value Entry to a State Entry

  1. If the candidate is not a record, then return failure.
  2. If the candidate does not have its own value member, then return failure.
  3. If the candidate uuidv7 is not a valid UUIDv7 identifier, then return failure.
  4. If the candidate predecessor is not a valid UUIDv7 identifier, then return failure.
  5. If the candidate value member is not a record, then return failure.
  6. If the candidate value.key is not a non-empty string, then return failure.
  7. Attempt to detach the candidate value.value payload with structuredClone [[HTML]]. If cloning fails, then return failure.
  8. Return a new state entry populated from the candidate fields.

Transform a State Entry to a Snapshot Value Entry

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]].

Methods

Constructor

Construct a new binding whose internal state is the result of the core create algorithm applied to the optional snapshot.

size

Return the current number of visible content keys in the live projection.

add()

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.

has()

Return true when the supplied value's derived content key is presently visible in the live projection.

delete()

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.

clear()

Delete every visible member from the replica and dispatch the resulting delta event and change event when the set was non-empty.

values()

Return detached copies of the current visible set value(s).

merge()

Merge the supplied delta into the internal replica and dispatch any resulting delta event or change event.

acknowledge()

Compute the current acknowledgement frontier and dispatch an ack event when that frontier is non-empty.

garbageCollect()

Run the core garbage-collection algorithm with the supplied list of acknowledgement frontiers.

snapshot()

Create the current full snapshot and dispatch a snapshot event. The method returns undefined.

@@iterator

Iterate detached copies of the current visible set value(s).

forEach()

Call the supplied callback once for each detached visible set value, passing value and the set object.

toJSON()

Return the same detached structured-clone-compatible shape as snapshot. This is the value used by JSON.stringify.

toString()

Attempt to return the JSON string form of the current snapshot.

addEventListener() and removeEventListener()

Forward listener registration and listener removal to the binding's internal EventTarget object using the provided type, listener, and options values.