lattice_registers/lww_register
A last-writer-wins register (LWW-Register) CRDT.
Stores a single value with an associated timestamp. When two replicas
conflict, the value with the strictly higher timestamp wins. On equal
timestamps, the replica with the lexicographically greater replica_id
wins, ensuring fully commutative merge.
Example
import lattice_core/replica_id
import lattice_registers/lww_register
let a = lww_register.new("hello", 1, replica_id.new("node-a"))
let b = lww_register.new("world", 2, replica_id.new("node-b"))
let merged = lww_register.merge(a, b)
lww_register.value(merged) // -> "world"
Types
A register holding a single value alongside its write timestamp and replica identifier.
value is the stored payload, timestamp is an integer logical clock
used to resolve conflicts, and replica_id provides a deterministic
tie-breaker when timestamps are equal.
pub opaque type LWWRegister(a)
Values
pub fn from_json(
json_string: String,
) -> Result(LWWRegister(String), json.DecodeError)
Decode a LWWRegister(String) from a JSON string produced by to_json.
Supports both v1 (no replica_id, uses the legacy "" placeholder) and v2
(requires a string replica_id) envelopes. The v1 placeholder does not prove
the identity of the historical writer; pass the local replica ID to set
for every subsequent write. Returns Ok(LWWRegister(String)) on success, or
Error(json.DecodeError) if the input is not a valid LWW-Register JSON
envelope.
pub fn from_json_with(
json_string: String,
decoder: decode.Decoder(a),
) -> Result(LWWRegister(a), json.DecodeError)
Decode a register with a custom payload decoder.
Accepts v1 and v2 envelopes, with the same metadata rules as from_json.
V2 requires replica_id to be present and contain a string, including when
that string is empty. Invalid payloads or envelopes return Error.
Examples
let register = lww_register.new(42, 1, replica_id.new("A"))
let encoded = lww_register.to_json_with(register, json.int) |> json.to_string
lww_register.from_json_with(encoded, decode.int) // -> Ok(register)
pub fn merge(
a: LWWRegister(a),
b: LWWRegister(a),
) -> LWWRegister(a)
Merge two LWW-Registers by returning the one with the higher timestamp.
When a.timestamp > b.timestamp, returns a. When b.timestamp > a.timestamp, returns b. On equal timestamps, the register whose
replica_id is lexicographically greater wins, providing a fully
commutative, associative, and idempotent merge.
pub fn new(
value value: a,
timestamp timestamp: Int,
replica_id replica_id: replica_id.ReplicaId,
) -> LWWRegister(a)
Create a new LWW-Register with an initial value, timestamp, and replica ID.
timestamp should be a positive integer representing the logical time of
the write. Use a monotonically increasing source (e.g., wall-clock
milliseconds or a Lamport clock) so that later writes have higher values.
replica_id identifies the writing node and is used as a deterministic
tie-breaker when two registers have equal timestamps during merge.
pub fn replica_id(
register: LWWRegister(a),
) -> replica_id.ReplicaId
Return the replica that owns the value the register currently holds.
set records its supplied author only for an accepted write. After merge
this is the replica whose write won, which makes it useful for provenance
and for tie-breaking consistently with merge in downstream code.
pub fn set(
register register: LWWRegister(a),
value value: a,
timestamp timestamp: Int,
replica_id replica_id: replica_id.ReplicaId,
) -> LWWRegister(a)
Write a value as replica_id if timestamp is strictly greater.
If timestamp > register.timestamp, replaces the stored value and
write metadata. Otherwise returns the register unchanged. Supplying the
writer explicitly prevents a local write after merge from inheriting the
winning remote writer’s identity.
Note that the comparison is strict, so a wall clock is not a safe source
on its own: it stalls for a millisecond at a time, and a second write
inside the same tick is silently dropped even when it came from this same
replica. Callers that write faster than their clock ticks should stamp
int.max(wall_clock, timestamp(register) + 1), which keeps every local
write ordered while leaving merge commutative.
A writer must not reuse the same (timestamp, replica_id) for different
values. After a restart, use a fresh replica ID or restore a durable logical
clock that advances beyond every prior write from that ID.
See set_with_delta for the delta-state variant that also returns a
small payload suitable for incremental sync (e.g. over websockets).
Examples
let local = replica_id.new("B")
let adopted = lww_register.new("old", 1, replica_id.new("A"))
let updated = lww_register.set(adopted, "new", 2, local)
lww_register.replica_id(updated) // -> local
pub fn set_with_delta(
register register: LWWRegister(a),
value value: a,
timestamp timestamp: Int,
replica_id replica_id: replica_id.ReplicaId,
) -> #(LWWRegister(a), LWWRegister(a))
Set a value and return both the new state and a delta.
The returned delta is an LWWRegister carrying the write that was
actually accepted locally. When timestamp strictly exceeds the current
timestamp the delta carries the new (value, timestamp, replica_id);
otherwise the local set is a no-op and the delta carries the unchanged
register so that no rejected write can win on a remote replica.
Merging the delta into a remote via merge produces the same result as
merging the new local state, preserving convergence.
pub fn timestamp(register: LWWRegister(a)) -> Int
Return the timestamp of the write the register currently holds.
Because set accepts only strictly greater timestamps, a caller stamping
writes from a wall clock needs to know the timestamp already held in order
to stay ahead of it — two writes inside the same clock tick are otherwise
unordered and the second is dropped. Reading this back from a decoded
snapshot lets such a caller seed its logical clock before its first write.
Examples
let register = lww_register.new("hello", 42, replica_id.new("node-a"))
lww_register.timestamp(register) // -> 42
// Stamp the next write so it cannot collide with the one held.
let next = int.max(wall_clock_ms(), lww_register.timestamp(register) + 1)
pub fn to_json(register: LWWRegister(String)) -> json.Json
Encode a LWWRegister(String) as a self-describing JSON value.
Produces an envelope with type, v (schema version = 2), and state.
Format: {"type": "lww_register", "v": 2, "state": {"value": "...", "timestamp": ..., "replica_id": "..."}}
Use from_json to decode the result back into a LWWRegister(String).
pub fn to_json_with(
register: LWWRegister(a),
encode: fn(a) -> json.Json,
) -> json.Json
Encode a register with a custom payload encoder, preserving write metadata.
Uses the same v2 envelope as to_json.
Examples
let register = lww_register.new(42, 1, replica_id.new("A"))
lww_register.to_json_with(register, json.int)
pub fn value(register: LWWRegister(a)) -> a
Return the current value of the register.
Provided for a uniform functional API since the type is opaque.