Crdt.Mval
crdt · API reference
Multi-value register extension built on arr node
Multi-Value Register extension (mval) - Concurrent write visibility.
A Multi-Value Register is a CRDT that preserves all concurrent writes, making conflicts visible rather than silently resolving them with LWW. This allows applications to implement custom conflict resolution.
It's built on an arr node where:
- Each write creates a new element in the array
- Concurrent writes from different sessions are all preserved
- Sequential writes from the same session replace the previous value
- Reading returns all concurrent values The key insight is that arr nodes (RGA) preserve all concurrent inserts. By using the arr's RGA semantics, we get concurrent write visibility for free.
Usage:
let model = Model.create session_id in
let mval = Mval.create model in
Mval.set mval (Value.string "value1");
(* Concurrent set from another session would add another value *)
match Mval.get mval with
| [v] -> (* No conflict - single value *)
| values -> (* Conflict - multiple concurrent values *)Conflict resolution strategies:
- First-wins: List.hd (Mval.get mval)
- Last-wins: List.hd (List.rev (Mval.get mval))
- Custom: Application-specific merge
Types
type t = {
model : Model.t;
arr_node : Node.t;
mutable last_write : Clock.timestamp option; (* Track last write from this session for sequential deduplication *)
}Multi-value register handle wrapping an arr node
Creation
val create : Model.t -> tCreate a new multi-value register. Creates an arr node in the model.
val of_node : Model.t -> Node.t -> tCreate a multi-value register from an existing arr node. Use this when loading from a decoded model.
val id : t -> Clock.timestampGet the arr node's timestamp ID
Reading
val get : t -> Value.t listGet all values in the register.
Returns a list of values representing all concurrent writes. If there's only one value, there are no conflicts. Multiple values indicate concurrent writes from different sessions.
Values are ordered by RGA ordering (timestamp-based).
val count : t -> intGet the number of concurrent values (conflict count).
Returns 0 if empty, 1 if no conflict, >1 if conflict exists.
val has_conflict : t -> boolCheck if there's a conflict (multiple concurrent values).
val is_empty : t -> boolCheck if the register is empty (no values set).
val first : t -> Value.t optionGet the first value (if any). Useful for first-wins conflict resolution.
val last : t -> Value.t optionGet the last value (if any). Useful for last-wins conflict resolution.
Writing
val set : t -> Value.t -> unitSet a value in the register.
If this is a sequential write from the same session, it replaces the previous value. If this is the first write or a concurrent write, it adds to the set of values.
parameter value The value to set
val clear : t -> unitClear all values from the register.
val set_value_only : t -> Value.t -> unitInternal: set a value without clearing our previous write
val set_all : t -> Value.t list -> unitSet multiple values atomically.
Creates entries for all values. Useful for explicitly setting multiple concurrent values during conflict resolution.
Conflict Resolution
type resolve_strategy =
| First (* Keep the first value (by RGA ordering) *)
| Last (* Keep the last value (by RGA ordering) *)
| Custom of Value.t list -> Value.t (* Custom resolver function *)Resolve conflicts by keeping only one value.
parameter strategy How to select the value: `First | `Last | `Custom of (Value.t list -> Value.t)
val resolve : t -> resolve_strategy -> unitResolve conflicts using the given strategy.
After resolution, the register will have exactly one value (or zero if empty).
Inspection
val node : t -> Node.tGet the raw arr node for advanced operations.
val timestamps : t -> Clock.timestamp listGet all element timestamps (useful for debugging).
val pp : Format.formatter -> t -> unitPretty print the multi-value register state.
val to_string : t -> stringConvert to string for debugging.