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.

Search Document