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)

An opaque identifier for a specific write operation.

Tags are generated internally by set and are not meant to be constructed by application code. A tag pairs a replica ID with a counter value, uniquely identifying one write event at one replica.

pub opaque type Tag

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) from a survives if b.vclock[rid] < counter OR b.entries also contains that tag.
  • Entry Tag(rid, counter) from b survives if a.vclock[rid] < counter OR a.entries also 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).

Search Document