lattice/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/mv_register
let a = mv_register.new("node-a") |> mv_register.set("hello")
let b = mv_register.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: String) -> 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.
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).