Skip to content

Maps

Maps are provided by the lattice_maps package. If you installed the lattice_crdt umbrella, they are already available.

lattice ships with two map CRDTs:

  • LWWMap for timestamp-based last-writer-wins key/value storage
  • ORMap for add-wins maps whose values are themselves CRDTs

LWWMap stores String keys and String values with timestamps.

import lattice_maps/lww_map
pub fn main() {
let profile =
lww_map.new()
|> lww_map.set("name", "Ada", 1)
|> lww_map.set("role", "admin", 2)
lww_map.get(profile, "name")
// -> Ok("Ada")
}
  • lww_map.set(map, key, value, timestamp) only replaces an existing entry when timestamp is strictly greater than the current timestamp for that key.
  • lww_map.remove(map, key, timestamp) inserts a tombstone. Equal or older timestamps are ignored for the same reason.

Tombstones participate in merges, so a remove can still win later if it has the newest timestamp.

LWWMap resolves equal timestamps deterministically:

  • tombstones win over active values
  • when both sides hold active values, the lexicographically greater string wins
import lattice_maps/lww_map
pub fn main() {
let left = lww_map.new() |> lww_map.set("label", "apple", 7)
let right = lww_map.new() |> lww_map.set("label", "zebra", 7)
let merged = lww_map.merge(left, right)
lww_map.get(merged, "label")
// -> Ok("zebra")
}

This tie-break keeps merges replica-order independent even when timestamps are equal.

ORMap is for maps whose values should also converge as CRDTs. You choose the value type up front with a CrdtSpec.

Because ORMap uses types from multiple lattice packages, it is a good example of where the lattice_crdt umbrella is convenient.

import lattice_core/replica_id
import lattice_maps/crdt
import lattice_maps/or_map
let counters = or_map.new(replica_id.new("node-a"), crdt.GCounterSpec)

When you call or_map.update, lattice looks up the current value for that key. If the key does not exist yet, it creates a default value from the chosen CrdtSpec and passes that into your update function.

A case expression is clearer than assert for destructuring the Crdt union and keeps the example safe if you refactor later.

import lattice_core/replica_id
import lattice_counters/g_counter
import lattice_maps/crdt
import lattice_maps/or_map
fn add_stock(value: crdt.Crdt, delta: Int) -> crdt.Crdt {
case value {
crdt.CrdtGCounter(counter) ->
crdt.CrdtGCounter(g_counter.increment(counter, delta))
other -> other
}
}
pub fn main() {
let inventory =
or_map.new(replica_id.new("node-a"), crdt.GCounterSpec)
|> or_map.update("widgets", fn(value) { add_stock(value, 4) })
|> or_map.update("widgets", fn(value) { add_stock(value, 1) })
case or_map.get(inventory, "widgets") {
Ok(crdt.CrdtGCounter(counter)) -> g_counter.value(counter)
_ -> 0
}
// -> 5
}

Because the map was created with crdt.GCounterSpec, the "widgets" key starts at the default GCounter value of zero.

or_map.merge returns a Result because it checks that both maps use the same CrdtSpec:

let assert Ok(merged) = or_map.merge(map_a, map_b)

The merge combines two pieces of state:

  1. the key tracker, using add-wins observed-remove semantics
  2. the value at each key, using crdt.merge

That means a concurrent update and remove of the same key keeps the key in the merged map, and concurrent updates to the nested CRDT converge using that CRDT's own merge rules.

ORMap exposes a dedicated ORMapDelta API for incremental sync:

  • or_map.update_with_delta
  • or_map.remove_with_delta
  • or_map.apply_delta
  • or_map.merge_deltas
  • or_map.empty_delta
  • or_map.delta_to_json
  • or_map.delta_from_json

Use update_with_delta or remove_with_delta for local changes, send the returned delta to peers, and apply it remotely with apply_delta. merge_deltas can batch multiple pending deltas before transmission. See Delta-State Replication for examples.