lattice_maps/crdt

A tagged union over all leaf CRDT types with dynamic dispatch.

The Crdt type wraps individual CRDTs (counters, registers, sets) so they can be stored and merged uniformly — this is how ORMap holds heterogeneous values. For direct use, prefer the individual modules (e.g., g_counter, or_set) for type-safe access.

Maps (LWWMap, ORMap) are not included in this union to avoid circular module dependencies.

Example

import lattice_maps/crdt
import lattice_core/replica_id
import lattice_counters/g_counter

let a = crdt.CrdtGCounter(g_counter.new(replica_id.new("node-a")) |> g_counter.increment(1))
let b = crdt.CrdtGCounter(g_counter.new(replica_id.new("node-b")) |> g_counter.increment(2))
let assert Ok(merged) = crdt.merge(a, b)

Types

A tagged union wrapping every leaf CRDT type in this library.

Variants:

  • CrdtGCounter — grow-only counter
  • CrdtPnCounter — increment/decrement counter
  • CrdtLwwRegister — last-writer-wins register (String)
  • CrdtMvRegister — multi-value register (String)
  • CrdtGSet — grow-only set (String)
  • CrdtTwoPSet — two-phase set (String)
  • CrdtOrSet — observed-remove set (String)
  • CrdtVersionVector — version vector

Parameterized types are fixed to String for v1. Maps (LWWMap, ORMap) are composite containers and are not included in this union to avoid circular module dependencies.

pub type Crdt {
  CrdtGCounter(g_counter.GCounter)
  CrdtPnCounter(pn_counter.PNCounter)
  CrdtLwwRegister(lww_register.LWWRegister(String))
  CrdtMvRegister(mv_register.MVRegister(String))
  CrdtGSet(g_set.GSet(String))
  CrdtTwoPSet(two_p_set.TwoPSet(String))
  CrdtOrSet(or_set.ORSet(String))
  CrdtVersionVector(version_vector.VersionVector)
}

Constructors

Specifies which leaf CRDT type an ORMap holds as its values.

When or_map.update is called on a key that does not yet exist, the map uses this spec to auto-create a default value via default_crdt. Choosing the right spec at or_map.new time is important because changing the value type after the fact would require migrating all existing values.

pub type CrdtSpec {
  GCounterSpec
  PnCounterSpec
  LwwRegisterSpec
  MvRegisterSpec
  GSetSpec
  TwoPSetSpec
  OrSetSpec
}

Constructors

  • GCounterSpec
  • PnCounterSpec
  • LwwRegisterSpec
  • MvRegisterSpec
  • GSetSpec
  • TwoPSetSpec
  • OrSetSpec

Error returned when merging two Crdt values of different types.

The expected and found fields contain human-readable type names (e.g., "g_counter", "or_set").

pub type MergeError {
  TypeMismatch(expected: String, found: String)
}

Constructors

  • TypeMismatch(expected: String, found: String)

Values

pub fn default_crdt(
  spec: CrdtSpec,
  replica_id: replica_id.ReplicaId,
) -> Crdt

Create a new default (bottom) value of the specified CRDT type.

The replica_id is passed to CRDT constructors that require it (counters, registers, OR-Set). For types that don’t use a replica identifier (G-Set, 2P-Set), the argument is ignored.

Default values per spec:

  • GCounterSpec / PnCounterSpec — new counter for replica_id
  • LwwRegisterSpec — empty string "" at timestamp 0 for replica_id (bottom element)
  • MvRegisterSpec — new MV-Register for replica_id
  • GSetSpec / TwoPSetSpec — empty set (no replica needed)
  • OrSetSpec — new OR-Set for replica_id
pub fn default_delta(
  spec: CrdtSpec,
  replica_id: replica_id.ReplicaId,
) -> Crdt

Return an empty/identity delta for the given spec.

An empty delta is the join-semilattice bottom: merging it into any state returns that state unchanged. ORMap uses this when accumulating deltas across multiple mutations and as the per-key default when no value-CRDT change is needed.

For most types this is identical to default_crdt. The exception is LwwRegisterSpec, where the bottom is the same (value="", timestamp=0) register; the merge semantics ensure it is dominated by any subsequent real write at any positive timestamp.

pub fn from_json(
  json_string: String,
) -> Result(Crdt, json.DecodeError)

Decode a Crdt from a JSON string produced by to_json.

Reads the "type" field to determine which type-specific decoder to use. Returns Error if the string is not valid JSON, the "type" field is missing, or the type tag is not recognized.

pub fn is_empty_delta(
  value: Crdt,
  spec: CrdtSpec,
  replica_id: replica_id.ReplicaId,
) -> Bool

Return True when a wrapped CRDT carries no observable change relative to the bottom (default) state for the given spec and replica.

Used by ORMap to decide whether a value-CRDT delta is worth packaging into the surrounding map delta. An empty delta merged into a remote is a no-op, so emitting it would just waste bandwidth.

Implementation: structural equality against default_delta(spec, rid). A delta from a no-op mutation may still appear “non-empty” (e.g. a GCounter carrying {self_id: 0} is structurally distinct from the fresh empty counter); such cases produce a small but harmless delta.

pub fn matches_spec(value: Crdt, spec: CrdtSpec) -> Bool

Return True when a wrapped CRDT matches the expected CrdtSpec.

pub fn merge(a: Crdt, b: Crdt) -> Result(Crdt, MergeError)

Dispatch merge to the type-specific merge function for matching variants.

If a and b hold the same variant, their inner values are merged using the type-specific merge function and returned as Ok(merged).

If a and b hold different variants, returns Error(TypeMismatch(expected: ..., found: ...)) where expected is the type name of a and found is the type name of b.

pub fn to_json(crdt: Crdt) -> json.Json

Dispatch to_json to the type-specific serializer for the wrapped CRDT.

Each variant delegates to its module’s to_json. The resulting JSON includes a "type" field (e.g., "g_counter") that from_json uses to select the correct decoder on deserialization.

pub fn type_name(value: Crdt) -> String

Return a human-readable type name for a wrapped Crdt value.

Search Document