This document defines Convergent Replicated Struct (CR-Struct), a delta CRDT for a fixed-key object-like data structure. Each field resolves to one visible value. Every overwrite is assigned a UUIDv7, and overwritten identifiers are retained as per-field tombstones.
The specification defines the data structure, merge rules, snapshot and delta formats, garbage-collection rules, acknowledgement formats, and one conforming JavaScript binding.
CR-Struct is designed to serve as a small replicated core for fixed-key object state, configuration state, and nested CRDT compositions.
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 fixed-key replica shape, field overwrite semantics, convergence properties, and per-field garbage-collection safety defined by the algorithms in this specification.
A CR-Struct 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 field key is one own property name present in the replica's default object.
A field value is the consumer-defined payload presently selected for one field key.
A field entry is the internal record for one
field key, consisting of a current winning uuidv7,
current winning value, current winning
predecessor, and a set of retained tombstones.
A predecessor identifier is an UUIDv7 identifier naming the immediately previous winning identifier for the same field key.
A field tombstone is one overwritten UUIDv7 identifier retained by a field entry.
An acknowledgement frontier is, for one field key, the largest retained field tombstone under lexicographic comparison.
A snapshot is the serializable object form of the materialized field entries in the current CR-Struct replica.
A delta is a partial snapshot containing only the fields a replica wishes to share.
A CR-Struct replica consists of a fixed set of field key(s) defined at construction time. Each materialized key stores exactly one current visible field value, one current winning UUIDv7 identifier, one current winning predecessor identifier, and a set of retained field tombstone(s).
The replica shape MUST remain fixed after construction. A conforming implementation MUST ignore unknown keys arriving from snapshots, deltas, and acknowledgement frontiers, and MUST preserve only keys present in the default object supplied at construction time.
A binding MAY support an allow-missing construction mode. In that mode, known field keys without valid snapshot entries remain unmaterialized until a local overwrite or valid incoming merge materializes them. Unmaterialized fields are omitted from snapshots.
Each local overwrite produces a fresh winning UUIDv7 identifier, moves the previous winning identifier into the field's tombstone set, and records the previous winning identifier as the new predecessor identifier.
On merge, each field is resolved independently. Incoming tombstones extend local knowledge. Incoming winners may be adopted directly, may be rejected with a reply delta, or may trigger a fresh local overwrite that collapses a conflicting same-UUID state into one newer winning event.
A snapshot or delta input MAY contain malformed or unknown members. A conforming implementation processes only the known top-level field keys and ignores entries that do not successfully parse against the local default field runtime type.
The same logical replica can be viewed through three useful representations: the serializable snapshot form used for transport and storage, one possible runtime projection used internally by a binding, and the final consumer-facing visible object.
structuredClone. If copying fails, then fail.
entries member is empty and whose
defaults member is the copied defaults object.
uuidv7 is the fresh
winning UUIDv7, whose predecessor is the fresh
root UUIDv7, whose value is
defaultValue, and whose tombstones set
initially contains only the fresh root UUIDv7.
value.
uuidv7 is the fresh winning
UUIDv7, whose predecessor is the fresh root UUIDv7,
whose value is that key's default value, and whose
tombstones set initially contains only the fresh root
UUIDv7.
uuidv7.
uuidv7 with a fresh
UUIDv7, replace its value with the supplied value,
replace its predecessor with oldUuidv7,
and add oldUuidv7 to its tombstones.
structuredClone. If copying fails, then fail.
uuidv7, then continue.
uuidv7 equals the
candidate uuidv7:
predecessor is
lexicographically less than the candidate
predecessor, then adopt the candidate
value and predecessor, add the
candidate predecessor to the tombstone set, record the
visible field change, and continue.
uuidv7 equals the
candidate predecessor, or the target tombstones
already contain current's uuidv7, or
the candidate uuidv7 is lexicographically greater
than current's uuidv7, then:
uuidv7,
value, and predecessor.
uuidv7 to the target
tombstone set and append the current local field entry to the
reply delta.
predecessor.
This algorithm is only safe when the caller supplies a membership-complete set of 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.
[Exposed=*]
interface CRStruct {
constructor(
object defaults,
optional object snapshot = {},
optional boolean allowMissing = false
);
undefined merge(optional object delta = {});
undefined acknowledge();
undefined garbageCollect(sequence<object> frontiers);
undefined snapshot();
sequence<DOMString> keys();
undefined clear();
object clone();
sequence<any> values();
sequence<sequence<any>> entries();
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 CRStructSnapshotEntry {
required DOMString uuidv7;
required any value;
required DOMString predecessor;
required sequence<DOMString> tombstones;
};
typedef record<DOMString, CRStructSnapshotEntry> CRStructSnapshot;
typedef record<DOMString, CRStructSnapshotEntry> CRStructDelta;
typedef record<DOMString, any> CRStructChange;
typedef record<DOMString, DOMString> CRStructAck;
The binding additionally exposes direct property access through a
JavaScript Proxy. Reading struct.key returns
a detached copy of the current value for that field. Writing
struct.key = value performs a local overwrite, and
delete struct.key resets that field to its default value.
In allow-missing mode, reading a known field that has not yet been
materialized returns undefined.
Only keys present in the constructor's defaults object participate in proxy field access. The binding resolves those known field keys before ordinary reflective property access.
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
CRStructSnapshot and CRStructDelta as
defined by the declarations above.
When the constructor's allowMissing argument is true,
missing or invalid snapshot entries remain absent instead of being
initialized from defaults. Direct writes and valid merges materialize
those absent known fields.
When the current snapshot is JSON-compatible,
JSON.stringify(struct) serializes the binding through
that snapshot. for...of iterates detached
[key, value] pairs for the current live fields, and
toString() attempts to return the JSON string form of the
current snapshot.
The JavaScript binding throws CRStructError with a string
code attribute for local API misuse.
DEFAULTS_NOT_CLONEABLEstructuredClone.
VALUE_NOT_CLONEABLEstructuredClone.
VALUE_TYPE_MISMATCH
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 field
mutations and for merges that need to emit a reply delta.
CustomEvent whose detail is a partial
object mapping changed field keys to their new visible values.
CustomEvent whose detail is the current
per-field acknowledgement frontier produced when
acknowledge() is invoked.
CustomEvent whose detail is the full
current snapshot produced when
snapshot() is invoked.
Direct property writes, direct property deletes, 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 ack event, and snapshot() MUST dispatch a snapshot event.
uuidv7,
value, predecessor, and
tombstones members, then return failure.
uuidv7 member is not a valid
UUIDv7 identifier, then return failure.
predecessor member is not a
valid UUIDv7 identifier, then return failure.
tombstones member is not a
list, then return failure.
value member
with structuredClone. If copying fails, then return
failure.
uuidv7, then return failure.
predecessor, then return failure.
Return a new serializable object whose uuidv7,
value, predecessor, and
tombstones members equal the current live field entry
after converting the tombstone set into a list and after detaching
the value with structuredClone.
Construct a new binding whose internal state is the result of the
core create algorithm applied to the supplied defaults object and
optional snapshot. If allowMissing is true,
missing or invalid snapshot entries remain absent until first local
write or valid merge.
Reading struct.key returns the current visible
field value copy for that key. Writing
struct.key = value runs a one-field local overwrite.
Deleting delete struct.key resets that field to its
default value. A known unmaterialized key reads as
undefined.
Merge the supplied delta into the internal replica and dispatch any resulting delta event or change event.
Compute the current per-field acknowledgement frontier and dispatch an ack event.
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 field key(s) as a list of strings.
Reset every field in the replica back to its default value and dispatch the resulting delta event and change event.
Return a detached plain object containing the current visible field values keyed by their field names.
Return detached copies of the current visible field values.
Return detached [key, value] pairs for the current
visible fields.
Iterate detached [key, value] pairs for the current
visible fields.
Return the same detached structured-clone shape as snapshot.
This is the value used by JSON.stringify.
Attempt to return the JSON string form of the current snapshot. This can fail when field values are not JSON-compatible.
Forward listener registration and listener removal to the binding's
internal EventTarget object using the provided type,
listener, and options values.