This document specifies Observed Overwrite Struct (OO-Struct), a state-based CRDT for fixed-key object structs. Each field maintains one visible value together with authoritative knowledge of overwritten UUIDv7 identifiers. The specification defines the data model, merge rules, snapshot and delta formats, acknowledgement frontiers, overwrite-set compaction, and one conforming JavaScript binding.

This is an unofficial specification maintained alongside a reference implementation.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in BCP 14 [[RFC2119]] [[RFC8174]] when, and only when, they appear in all capitals.

Unless otherwise stated, neutral data-structure and algorithm terms such as struct, map, list, set, contains, and remove are used with the meanings defined by Infra [[INFRA]].

A conforming implementation MUST preserve the replica convergence and overwrite semantics defined by the algorithms in this specification.

Introduction

OO-Struct is designed for static object shapes where keys are known up front and each key keeps one visible value of a stable runtime kind.

OO-Struct is informed by map CRDTs with observed-remove semantics [[RIAKDTMAP]] [[DELTACRDTS]] and by LWW-register-style one-visible- winner conflict resolution [[CRDTOVERVIEW]], but it is not an OR-Map or a plain last-writer-wins register. Its field set is fixed by the default struct, and its distinguishing replicated state is the per-field overwrite set that records which UUIDv7 identifiers have already been superseded.

Taken together, the fixed field set, one-visible-value policy, per-field overwrite tracking, and UUIDv7 identifiers yield the following operational properties in the reference binding:

Terminology

An OO-Struct replica is one independently mutable instance of the data type.

A default struct is the application-supplied struct whose keys and default values define the replica's field space.

A field key is one own property name from the default struct.

A field value is the visible payload presently selected for a given field key.

A field state entry is the per-key replicated state record containing a current identifier, a predecessor identifier, a current field value, and a set of overwritten identifiers.

A UUIDv7 identifier is an identifier that is a valid UUID version 7 as defined by [[RFC9562]].

An overwritten identifier is a UUIDv7 that appears in a field state entry's set of overwritten identifiers.

A snapshot is the full current replica state containing one field state entry for every field key.

A delta is a partial snapshot containing only the keys a replica wishes to upstream.

A well-formed snapshot entry is a serialized field value that parses as a valid field state entry for the corresponding default struct field.

A snapshot input MAY contain malformed or unknown members. The reference binding processes known keys individually and ignores entries that do not parse.

Core Model

Each OO-Struct replica maintains two logical objects:

Each current field state entry records one current UUIDv7 identifier, one predecessor UUIDv7 identifier, one current field value, and one set of overwritten identifiers.

A UUIDv7 that appears in a field's overwrite set is an overwritten identifier. If an identifier is overwritten, a conforming implementation MUST NOT later make it current again for that same field key.

The predecessor identifier names one overwritten predecessor and MUST itself be present in the field's overwrite set. The reference binding bootstraps each field with a synthetic predecessor UUIDv7 so that every field state entry always names an already overwritten predecessor.

UUIDv7 Identifier Requirements

A conforming OO-Struct implementation MUST assign one valid UUIDv7 identifier to each current field state entry.

The reference binding generates fresh UUIDv7 identifiers for local overwrites and compares canonical textual UUIDv7 representations lexicographically when it needs an order for merge resolution.

Ordering does not override overwrite knowledge. If an identifier is in a field's overwrite set, it is an overwritten identifier regardless of any later lexical comparison.

Methods

CRUD

Create

A create operation constructs a new replica from a default struct and an optional snapshot.

If a snapshot record is supplied, the replica attempts to adopt each known field whose incoming value is a well-formed snapshot entry. Unknown or malformed entries are ignored. Any field not adopted from the snapshot is initialized from the default struct with fresh UUIDv7 state.

Read

A read operation returns the current visible field value for a known field key.

The reference binding returns a detached clone rather than the live stored value reference.

Update

An update operation MUST mint a fresh UUIDv7 identifier for the target field key, make the previously current UUIDv7 the new predecessor identifier, add that predecessor to the overwrite set, and set the field value to the supplied payload.

In the reference binding, the payload MUST be structuredClone-compatible and MUST have the same runtime prototype category as the corresponding default value. Every successful local update creates a new per-field delta and a new per-field change event.

Delete

In the reference binding, delete() resets fields to their default values. It does not remove the field key from the struct shape.

If a key is supplied, only that field is reset. If no key is supplied, every field in the default struct is reset. Unknown explicit keys are ignored.

MAGS

Merge

Merge operates over a delta or a full snapshot. A conforming implementation MUST process the incoming state one field key at a time.

When merging one incoming field state entry:

  1. If the field key is unknown locally, continue.
  2. If the incoming value does not parse as a valid field state entry for that key, continue.
  3. Merge incoming overwrite knowledge into the local overwrite set before making any winner decision.
  4. If the local overwrite set now contains the incoming UUIDv7, continue.
  5. If the current and incoming entries share the same UUIDv7, then the incoming entry wins only when its predecessor identifier is lexicographically greater. Otherwise the replica MUST mint a fresh local overwrite using the current visible field value and emit that repair state as delta.
  6. Otherwise the incoming entry wins if any of the following is true: its predecessor identifier equals the local current UUIDv7; the local overwrite set already contains the local current UUIDv7; or the incoming UUIDv7 is lexicographically greater than the local current UUIDv7.
  7. If the incoming entry does not win, its UUIDv7 MUST be added to the local overwrite set and the local field state MAY be emitted back out as rebuttal delta.

Acknowledge

An acknowledge operation emits one acknowledgement frontier per known field key.

In the reference binding, the acknowledgement frontier for a field is the lexicographically greatest overwritten UUIDv7 currently known for that field.

Garbage Collect

A garbage-collection operation accepts a list of acknowledgement frontiers and compacts overwrite history that is already dead.

In the reference binding, garbage collection determines the smallest acknowledged UUIDv7 per known key and removes overwritten identifiers that are less than or equal to that frontier, except for the current predecessor identifier named by __after.

Snapshot

A snapshot MUST contain enough information to recreate the current field state entry for every known field key.

A delta MUST use the same per-field state-entry format as a snapshot but MAY include only a subset of keys.

Additional

keys(), values(), entries()

These operations expose resolved live state without mutating replica history.

In the reference binding, keys() returns the known field keys, values() returns detached clones of the current visible values, and entries() returns key-value pairs whose values are likewise detached clones.

Overwrite-Set Compaction

The reference binding exposes compaction explicitly through acknowledge() and garbageCollect().

Acknowledgement and garbage collection operate only on identifiers that are already known to be overwritten. They do not remove the current winning UUIDv7 for a field, and they do not remove the current predecessor identifier named by after.

Consequently, compaction can shrink overwrite history without changing the resolved visible live state of the replica.

Payload Guidance

OO-Struct is best suited to static object shapes whose fields hold stable runtime kinds. It is not, by itself, a field-level CRDT for recursively mutating nested object graphs.

The reference binding performs a shallow runtime-kind validation against the corresponding default value. It does not define deep schema validation for nested payload structure.

Reference JavaScript Binding

This section defines one conforming JavaScript binding for the model above.

API Summary

[Exposed=*]
interface OOStruct {
  constructor(object defaults, optional object snapshot);
  static OOStruct create(object defaults, optional object snapshot);
  any read(DOMString key);
  undefined update(DOMString key, any value);
  undefined delete(optional DOMString key);
  undefined merge(object replica);
  undefined acknowledge();
  undefined garbageCollect(sequence<object> frontiers);
  undefined snapshot();
  sequence<DOMString> keys();
  sequence<any> values();
  sequence<any> entries();
  undefined addEventListener(DOMString type, any listener, optional any options);
  undefined removeEventListener(DOMString type, any listener, optional any options);
};

dictionary OOStructSnapshotEntry {
  required DOMString uuidv7;
  required DOMString after;
  required any value;
  required FrozenArray<DOMString> overwrites;
};
        

Snapshot and delta payloads are plain JavaScript objects keyed by known fields. Change payloads are plain JavaScript objects keyed by changed fields, and acknowledgement payloads are plain JavaScript objects keyed by acknowledged fields.

The actual field-entry payload member names are __uuidv7, __after, __value, and __overwrites. The Web IDL above uses IDL-safe member names because dictionary members cannot use the double-underscore form.

The Web IDL above is a coarse binding summary. The precise key- and value-level behavior is defined by the prose and algorithms below.

Errors

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

DEFAULTS_NOT_CLONEABLE
Thrown when constructor defaults are not supported by structuredClone.
VALUE_NOT_CLONEABLE
Thrown when update() receives a value that is not supported by structuredClone.
VALUE_TYPE_MISMATCH
Thrown when update() receives a value whose runtime prototype category does not match the corresponding default field value.

Events

The JavaScript binding maintains an internal EventTarget object as defined by DOM [[DOM]].

This binding is not specified as inheriting from EventTarget. Instead, its listener methods forward to that internal event target, and its local mutations and merges dispatch synthetic events through it.

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 per-field delta. The binding dispatches this event for local overwrites and for merge-time repair or rebuttal state that should be upstreamed.
change event
A CustomEvent whose detail maps changed keys to their new visible values.
ack event
A CustomEvent whose detail maps known keys to their current acknowledgement frontier values produced when acknowledge() is invoked.
snapshot event
A CustomEvent whose detail is the full current snapshot produced when snapshot() is invoked.

update() and delete() MUST dispatch delta event before change event. A merge MUST dispatch delta event only when it produces egress delta material and MUST dispatch change event only when visible field values change. acknowledge() MUST dispatch ack event, and snapshot() MUST dispatch snapshot event.

Auxiliary Algorithms

parse a snapshot entry

  1. If the candidate value is not a record, then return failure.
  2. If the candidate value does not have its own __value member, then return failure.
  3. If the candidate value's __uuidv7 member is not a valid UUIDv7 identifier, then return failure.
  4. If the candidate value's __after member is not a valid UUIDv7 identifier, then return failure.
  5. If the candidate value's __overwrites member is not a list, then return failure.
  6. If the candidate value's __value member cannot be copied with structuredClone, then return failure.
  7. If the cloned candidate value does not have the same runtime prototype category as the corresponding default value, then return failure.
  8. Let overwrites be a new empty set.
  9. For each listed overwrite, ignore non-UUIDv7 values and the candidate's own __uuidv7; add the rest to overwrites.
  10. If overwrites does not contain __after, then return failure.
  11. Return the normalized field entry.

serialize a state entry

Return a new snapshot entry whose scalar members equal the field entry, whose __value member is a detached structured clone of the current field value, and whose __overwrites is a list created from the overwrite set in iteration order.

Algorithms

CRUD

Constructor
  1. Attempt to copy defaults with structuredClone.
  2. If copying fails, throw OOStructError with code DEFAULTS_NOT_CLONEABLE.
  3. Store the cloned defaults as the default struct.
  4. Initialize live state and state entries as empty objects.
  5. If snapshot is a record, then for each field key in the default struct attempt to parse a snapshot entry from the matching incoming value. If parsing succeeds, adopt that entry.
  6. For every field key not adopted from the incoming snapshot, set the live value to the default value and create a fresh current field entry with a fresh __uuidv7, a fresh synthetic root in __after, and an overwrite set containing only that synthetic root.
create()

Return a new OOStruct constructed from the arguments.

read()

Return a detached structured clone of the current visible value for key.

update()
  1. Attempt to copy the supplied value with structuredClone.
  2. If copying fails, throw OOStructError with code VALUE_NOT_CLONEABLE.
  3. If the copied value does not have the same runtime prototype category as the corresponding default value, throw OOStructError with code VALUE_TYPE_MISMATCH.
  4. Let delta be a new empty delta.
  5. Let changes be a new empty changes object.
  6. Mint a fresh UUIDv7 for key, move the previous UUIDv7 into __after and __overwrites, and set the field's current value to the copied payload.
  7. Set delta[key] to the result of serialize a state entry on the current field entry.
  8. Set changes[key] to a detached structured clone of the copied payload.
  9. Dispatch a delta event whose detail is delta.
  10. Dispatch a change event whose detail is changes.
delete()
  1. Let delta be a new empty delta.
  2. Let changes be a new empty changes object.
  3. If key is not undefined and is unknown in the default struct, then return.
  4. If key is provided, overwrite that field with its default value and record the result in delta and changes.
  5. Otherwise, for each field in the default struct, overwrite that field with its default value and record the results in delta and changes.
  6. Dispatch a delta event whose detail is delta.
  7. Dispatch a change event whose detail is changes.

MAGS

merge()
  1. If replica is null, not an object, or a list, return.
  2. Let outgoing delta and changes be empty objects.
  3. Let hasDelta and hasChange be false.
  4. For each own key in the incoming replica:
    1. If the key is unknown locally, continue.
    2. Parse the candidate entry. If parsing fails, continue.
    3. Let target be the local field state entry.
    4. Let current be a copy of target.
    5. Let frontier be the lexicographically greatest identifier currently in target's overwrite set, or the empty string if the set is empty.
    6. For each overwrite in the candidate overwrite set, if the overwrite is less than or equal to frontier or is already present locally, continue; otherwise add it to target's overwrite set.
    7. If target's overwrite set now contains the candidate UUIDv7, continue.
    8. If current and the candidate share the same UUID, then:
      1. If current's predecessor identifier is lexicographically less than the candidate predecessor identifier, then adopt the candidate value and predecessor, add that predecessor to the overwrite set, update live state, write a detached clone into outgoing changes, set hasChange to true, and continue.
      2. Otherwise, mint a fresh local overwrite using the current visible value, place the resulting serialized entry in outgoing delta, set hasDelta to true, and continue.
    9. Otherwise, if any of the following is true, adopt the candidate: the local current UUID equals the candidate predecessor identifier; the local overwrite set already contains the local current UUID; or the candidate UUID is lexicographically greater than the local current UUID.
    10. When the candidate is adopted, set the target UUID, value, and predecessor to the candidate values; add the candidate predecessor and previous local current UUID to the overwrite set; update live state; write a detached clone into outgoing changes; set hasChange to true; and continue.
    11. Otherwise add the candidate UUID to the local overwrite set, place the serialized local state entry in outgoing delta, and set hasDelta to true.
  5. If hasDelta is true, dispatch delta event.
  6. If hasChange is true, dispatch change event.
acknowledge()
  1. Let ack be a new empty acknowledgement object.
  2. For each local field state entry, compute the lexicographically greatest overwritten identifier presently known for that field.
  3. Set the corresponding key in ack to that identifier.
  4. Dispatch an ack event whose detail is ack.
garbageCollect()
  1. If frontiers is not a list or is empty, return.
  2. Let smallestAcknowledgementsPerKey be a new empty acknowledgement object.
  3. For each frontier in frontiers, and for each own entry in that frontier:
    1. If the key is unknown locally, continue.
    2. If the value is not a valid UUIDv7 identifier, continue.
    3. If there is already a stored acknowledgement for that key and it is less than or equal to the incoming value, continue.
    4. Otherwise replace the stored acknowledgement for that key with the incoming value.
  4. For each acknowledged key in smallestAcknowledgementsPerKey, remove every overwritten identifier in the local field state entry that is less than or equal to the stored acknowledgement, except the field's current predecessor identifier.
snapshot()

Run serialize a state entry on every current field entry, assemble the results into a full snapshot, and dispatch a snapshot event. The method returns undefined.

Additional

keys()

Return the known field keys from live state.

values()

Return detached structured clones of the current visible field values.

entries()

Return key-value pairs where each value is a detached structured clone of the current visible field value.

EventTarget

addEventListener() and removeEventListener()

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

Acknowledgements

This specification builds on UUIDv7 [[RFC9562]], map CRDTs with observed-remove semantics [[RIAKDTMAP]] [[DELTACRDTS]], and LWW-register literature [[CRDTOVERVIEW]], while defining a new fixed-key overwrite-tracked struct CRDT.