lattice_maps/lww_map
Recursive last-writer-wins maps of atomic child snapshots.
Greater timestamps win; ties select tombstones, then modern provenance, then writer identity in UTF-8 byte order. Equal modern write IDs cannot name different active payloads. Competing child snapshots are not merged. Use ORMap for collaborative edits.
Types
A recursive map with one child schema and immutable assignment metadata.
Examples
let revisions: lww_map.LWWMap(Int) =
lww_map.new(replica_id.new("A"), crdt.OrMapSpec(crdt.SequenceSpec))
lww_map.keys(revisions) // -> []
pub type LWWMap(a) =
crdt.LWWMap(a)
Values
pub fn bind(
map: crdt.LWWMap(a),
replica: replica_id.ReplicaId,
) -> crdt.LWWMap(a)
Set the local writer without changing stored assignment payloads or authors.
Queries return bound copies. Use update to prepare edits under the new
assignment’s identity, rather than editing under a historical writer.
Examples
let source = lww_map.new(replica_id.new("A"), crdt.LwwRegisterSpec(""))
let snapshot = source |> lww_map.to_json |> json.to_string
let assert Ok(loaded) = lww_map.from_json(snapshot)
let local = lww_map.bind(loaded, replica_id.new("B"))
lww_map.replica_id(local) // -> replica_id.new("B")
pub fn from_json(
input: String,
) -> Result(crdt.LWWMap(String), json.DecodeError)
Decode a modern String snapshot.
Bind the decoded map before making writes as a different replica.
Examples
let source = lww_map.new(replica_id.new("A"), crdt.LwwRegisterSpec(""))
let encoded = source |> lww_map.to_json |> json.to_string
let assert Ok(loaded) = lww_map.from_json(encoded)
let local = lww_map.bind(loaded, replica_id.new("B"))
lww_map.replica_id(local) // -> replica_id.new("B")
pub fn from_json_with(
input: String,
decoder: decode.Decoder(a),
) -> Result(crdt.LWWMap(a), json.DecodeError)
Decode a generic snapshot, preserving historical writers and legacy provenance.
Legacy scalar snapshots require import_legacy; they are not modern
replication messages.
Examples
let schema = crdt.OrMapSpec(crdt.LwwRegisterSpec(42))
let map = lww_map.new(replica_id.new("A"), schema)
let encoded = lww_map.to_json_with(map, json.int) |> json.to_string
let assert Ok(loaded) = lww_map.from_json_with(encoded, decode.int)
lww_map.spec(loaded) // -> schema
pub fn get(
map: crdt.LWWMap(a),
key: String,
) -> Result(crdt.Crdt(a), Nil)
Get a locally bound copy of an active child, without modifying its assignment.
Examples
let local = replica_id.new("A")
let map = lww_map.new(local, crdt.LwwRegisterSpec(""))
let child = crdt.CrdtLwwRegister(lww_register.new("Ready", 1, local))
let assert Ok(map) = lww_map.set(map, "status", child, 1)
let assert Ok(crdt.CrdtLwwRegister(status)) = lww_map.get(map, "status")
lww_register.value(status) // -> "Ready"
pub fn import_legacy(
input: String,
spec: crdt.CrdtSpec(String),
replica: replica_id.ReplicaId,
) -> Result(crdt.LWWMap(String), json.DecodeError)
Import old scalar String entries as LWWRegister children.
Requires an explicit register schema/default. Original String tie keys are persisted as legacy provenance and compared in lexicographic UTF-8 byte order, without Unicode normalization. Import an agreed baseline before cutover. Distribute the resulting modern snapshot before accepting modern writes; importing old messages repeatedly is not a mixed-version replication scheme.
Examples
import gleam/result
import lattice_core/replica_id
import lattice_maps/crdt
import lattice_maps/lww_map
pub fn migrate(legacy_snapshot: String) {
use map <- result.try(lww_map.import_legacy(
legacy_snapshot, crdt.LwwRegisterSpec(""),
replica_id.new("cutover-writer"),
))
Ok(lww_map.to_json(map))
}
pub fn keys(map: crdt.LWWMap(a)) -> List(String)
Return active keys in unspecified order.
Examples
let map = lww_map.new(replica_id.new("A"), crdt.TextSpec)
lww_map.keys(map) // -> []
pub fn merge(
a: crdt.LWWMap(a),
b: crdt.LWWMap(a),
) -> Result(crdt.LWWMap(a), crdt.MergeError)
Merge atomic assignments using the left map’s local identity.
Child snapshots from competing assignments are never merged together.
At equal timestamps, tombstones win; otherwise modern writes use writer
identity and imported legacy writes use their original String tie keys.
Both comparisons use lexicographic UTF-8 byte order on both targets. Local
set, update, and remove use this same selection.
Examples
let a = lww_map.new(replica_id.new("A"), crdt.LwwRegisterSpec(0))
let b = lww_map.new(replica_id.new("B"), crdt.LwwRegisterSpec(0))
let assert Ok(merged) = lww_map.merge(a, b)
lww_map.replica_id(merged) // -> replica_id.new("A")
pub fn merge_as(
a: crdt.LWWMap(a),
b: crdt.LWWMap(a),
replica: replica_id.ReplicaId,
) -> Result(crdt.LWWMap(a), crdt.MergeError)
Merge into an explicit receiving identity.
The chosen assignment retains its historical writer. The receiving identity applies to future writes and to bound query results.
Examples
let a = lww_map.new(replica_id.new("A"), crdt.TextSpec)
let b = lww_map.new(replica_id.new("B"), crdt.TextSpec)
let local = replica_id.new("C")
let assert Ok(merged) = lww_map.merge_as(a, b, local)
lww_map.replica_id(merged) // -> local
pub fn new(
replica: replica_id.ReplicaId,
spec: crdt.CrdtSpec(a),
) -> crdt.LWWMap(a)
Create an empty map for the local writer.
Examples
lww_map.new(replica_id.new("A"), crdt.TextSpec)
pub fn prune(map: crdt.LWWMap(a), stable: Int) -> crdt.LWWMap(a)
Prune stable tombstones, without compacting child states.
Only advance the floor after every participating replica has observed the covered writes and removals. Older remote-only entries can then be rejected.
Examples
let map = lww_map.new(replica_id.new("A"), crdt.TextSpec)
let assert Ok(map) = lww_map.remove(map, "body", 5)
// After all replicas have observed the removal at timestamp 5:
let pruned = lww_map.prune(map, 5)
lww_map.tombstone_count(pruned) // -> 0
lww_map.pruned_timestamp(pruned) // -> 5
pub fn pruned_timestamp(map: crdt.LWWMap(a)) -> Int
Return the stable timestamp floor.
Examples
let map = lww_map.new(replica_id.new("A"), crdt.TextSpec)
lww_map.prune(map, 10) |> lww_map.pruned_timestamp // -> 10
pub fn remove(
map: crdt.LWWMap(a),
key: String,
timestamp: Int,
) -> Result(crdt.LWWMap(a), crdt.MergeError)
Write a tombstone above the prune floor and at or after the existing write.
At an equal timestamp, the tombstone wins even when the active assignment has the same writer. A timestamp at or below the prune floor is rejected.
Examples
let map = lww_map.new(replica_id.new("A"), crdt.TextSpec)
let assert Ok(removed) = lww_map.remove(map, "body", 1)
lww_map.get(removed, "body") // -> Error(Nil)
pub fn replica_id(map: crdt.LWWMap(a)) -> replica_id.ReplicaId
Return the local writer identity.
Examples
let local = replica_id.new("A")
let map = lww_map.new(local, crdt.TextSpec)
lww_map.replica_id(map) // -> local
pub fn set(
map: crdt.LWWMap(a),
key: String,
value: crdt.Crdt(a),
timestamp: Int,
) -> Result(crdt.LWWMap(a), crdt.MergeError)
Assign a complete immutable child snapshot.
For Sequence/Text edits or fresh replacements, prefer update and its new
assignment context to prevent allocation-ID reuse.
The timestamp must not precede the key’s timestamp and must exceed the prune
floor. An equal-time assignment uses the same tombstone, provenance, and
writer order as merge; generic child payloads are never compared. Reusing
one modern timestamp/writer for different active children returns
ConflictingWrite.
Examples
let local = replica_id.new("A")
let map = lww_map.new(local, crdt.LwwRegisterSpec(0))
let child = crdt.CrdtLwwRegister(lww_register.new(42, 1, local))
let assert Ok(map) = lww_map.set(map, "answer", child, 1)
lww_map.set(map, "answer", child, 1)
// -> Ok(map)
pub fn spec(map: crdt.LWWMap(a)) -> crdt.CrdtSpec(a)
Return the complete child schema.
Examples
let schema = crdt.OrMapSpec(crdt.LwwRegisterSpec(42))
let map = lww_map.new(replica_id.new("A"), schema)
lww_map.spec(map) // -> schema
pub fn to_json(map: crdt.LWWMap(String)) -> json.Json
Encode a modern String-payload snapshot (version 3).
Examples
let map = lww_map.new(replica_id.new("A"), crdt.LwwRegisterSpec(""))
let encoded = map |> lww_map.to_json |> json.to_string
lww_map.from_json(encoded) // -> Ok(map)
pub fn to_json_with(
map: crdt.LWWMap(a),
encode: fn(a) -> json.Json,
) -> json.Json
Encode a generic modern snapshot.
The encoder also handles configured register defaults in recursive schemas.
Examples
let local = replica_id.new("A")
let map = lww_map.new(local, crdt.LwwRegisterSpec(0))
let child = crdt.CrdtLwwRegister(lww_register.new(42, 1, local))
let assert Ok(map) = lww_map.set(map, "answer", child, 1)
let encoded = lww_map.to_json_with(map, json.int) |> json.to_string
lww_map.from_json_with(encoded, decode.int) // -> Ok(map)
pub fn tombstone_count(map: crdt.LWWMap(a)) -> Int
Return the number of retained tombstones.
Examples
let map = lww_map.new(replica_id.new("A"), crdt.TextSpec)
let assert Ok(map) = lww_map.remove(map, "body", 1)
lww_map.tombstone_count(map) // -> 1
pub fn update(
map: crdt.LWWMap(a),
key: String,
timestamp: Int,
callback: fn(crdt.Crdt(a), crdt.EditContext) -> Result(
crdt.Crdt(a),
e,
),
) -> Result(crdt.LWWMap(a), crdt.UpdateError(e))
Edit once under the new assignment’s namespace, then atomically assign.
To replace a child from empty, construct its default with context.replica_id.
Inherited historical IDs remain intact. Failures leave the map unchanged.
Do not use a fixed replica ID to construct a replacement Sequence or Text:
each accepted assignment needs its own allocation namespace.
Examples
Replace Text from empty on each write, using the new assignment’s context:
import gleam/result
import lattice_text/text
let map: lww_map.LWWMap(String) =
lww_map.new(replica_id.new("A"), crdt.TextSpec)
let replace = fn(_, context: crdt.EditContext) {
text.append(text.new(context.replica_id), "Replacement")
|> result.map(crdt.CrdtText)
}
let assert Ok(first) = lww_map.update(map, "body", 1, replace)
let assert Ok(second) = lww_map.update(first, "body", 2, replace)
let assert Ok(crdt.CrdtText(body)) = lww_map.get(second, "body")
text.value(body) // -> "Replacement"
The same pattern gives a generic Sequence fresh item identities:
import gleam/result
import lattice_sequence/sequence
let map: lww_map.LWWMap(Int) =
lww_map.new(replica_id.new("A"), crdt.SequenceSpec)
let assert Ok(map) = lww_map.update(map, "items", 1, fn(_, context) {
sequence.insert_many(sequence.new(context.replica_id), 0, [10, 20])
|> result.map(crdt.CrdtSequence)
})
let assert Ok(crdt.CrdtSequence(items)) = lww_map.get(map, "items")
sequence.values(items) // -> [10, 20]
To edit an existing child instead, use the callback’s first argument. For an
LWWRegister, also pass context.replica_id to lww_register.set.
Equal-time callback results follow set conflict selection.
pub fn values(map: crdt.LWWMap(a)) -> List(crdt.Crdt(a))
Return locally bound active children in unspecified order.
Examples
let map = lww_map.new(replica_id.new("A"), crdt.TextSpec)
lww_map.values(map) // -> []