lattice_registers/mv_register
A multi-value register (MV-Register) CRDT.
Preserves all concurrently written values using causal history tracked by version vectors. When one write causally supersedes another, only the newer value survives. When writes are concurrent, all values are retained — the application decides how to resolve the conflict.
Example
import lattice_core/replica_id
import lattice_registers/mv_register
let a = mv_register.new(replica_id.new("node-a")) |> mv_register.set("hello")
let b = mv_register.new(replica_id.new("node-b")) |> mv_register.set("world")
let merged = mv_register.merge(a, b)
mv_register.value(merged) // -> ["hello", "world"] (concurrent writes)
Types
A multi-value register that preserves concurrent writes.
replica_id identifies this node. entries maps write tags to values;
multiple entries indicate concurrent writes. vclock tracks the causal
history observed by this replica.
This type is opaque: use new, set, value, and merge to interact
with it. Do not pattern-match on the internal fields directly.
pub opaque type MVRegister(a)
Values
pub fn from_json(
json_string: String,
) -> Result(MVRegister(String), json.DecodeError)
Decode a MVRegister(String) from a JSON string produced by to_json.
Returns Ok(MVRegister(String)) on success, or Error(json.DecodeError)
if the input is not a valid MV-Register JSON envelope.
pub fn merge(
a: MVRegister(el),
b: MVRegister(el),
) -> MVRegister(el)
Merge two MV-Registers.
An entry survives the merge if it is not dominated by the other register’s version vector, or if both registers share the same entry (handles self-merge idempotency):
- Entry
Tag(rid, counter)fromasurvives ifb.vclock[rid] < counterORb.entriesalso contains that tag. - Entry
Tag(rid, counter)frombsurvives ifa.vclock[rid] < counterORa.entriesalso contains that tag.
The merged vclock is the pairwise maximum of both vclocks.
The result’s replica_id is taken from a.
This operation is commutative, associative, and idempotent.
pub fn new(replica_id: replica_id.ReplicaId) -> MVRegister(a)
Create a new empty MV-Register for the given replica.
Returns a register with no entries and an empty version vector.
replica_id identifies this node and is used when writing new values.
pub fn set(register: MVRegister(a), val: a) -> MVRegister(a)
Write a new value to the register.
Increments this replica’s logical clock, creates a fresh tag for the write,
clears all prior entries (this write causally supersedes everything in the
current vclock), and inserts the new tag-value pair. After a set, calling
value returns a single-element list containing val.
See set_with_delta for the delta-state variant that also returns a
small payload suitable for incremental sync (e.g. over websockets).
pub fn set_with_delta(
register: MVRegister(a),
val: a,
) -> #(MVRegister(a), MVRegister(a))
Write a new value and return both the new state and a delta.
The returned delta is an MVRegister whose entries contains only the
new tag→value pair, but whose vclock is the full new vclock of the
writing replica. The vclock is essential: it encodes the causal context
the local write supersedes, so that on merge into a remote replica every
dominated tag (whether at the writer or any other replica observed by
the writer) is correctly retracted.
Merging the delta into a remote via merge produces the same result as
merging the full new state, but is much smaller when the local register
holds many concurrent values being collapsed by this write.
pub fn to_json(register: MVRegister(String)) -> json.Json
Encode a MVRegister(String) as a self-describing JSON value.
Entries are serialized as an array of tag+value objects because Tag is a
custom type that cannot serve as a JSON dictionary key.
Format: {"type": "mv_register", "v": 1, "state": {"replica_id": "...", "entries": [...], "vclock": {...}}}
Use from_json to decode the result back into a MVRegister(String).
pub fn value(register: MVRegister(a)) -> List(a)
Return all concurrent values in the register.
Returns a list of all surviving values. An empty list means the register
has never been written. A single-element list is the common case after a
set. Multiple values indicate concurrent writes from different replicas
that have not yet been causally superseded — the application must decide
how to resolve them (e.g., pick one, merge, or surface the conflict).