Packages
lattice_core
1.0.0
Core types for lattice CRDTs — version vectors, dot contexts, and causal infrastructure
Current section
Files
Jump to
Current section
Files
src/lattice_core@version_vector.erl
-module(lattice_core@version_vector).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lattice_core/version_vector.gleam").
-export([new/0, increment/2, get/2, compare/2, dominates/2, is_empty/1, set_max/3, merge/2, to_json/1, from_json/1, decoder/0, to_dict/1, from_dict/1]).
-export_type([order/0, version_vector/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A version vector for tracking causal ordering between replicas.\n"
"\n"
" Each replica has a logical clock (monotonically increasing integer). Version\n"
" vectors enable detecting whether two states are causally ordered (one happened\n"
" before the other) or concurrent (neither dominates). Merge takes the pairwise\n"
" maximum of all clocks.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import lattice_core/replica_id\n"
" import lattice_core/version_vector\n"
"\n"
" let node_a = replica_id.new(\"node-a\")\n"
" let node_b = replica_id.new(\"node-b\")\n"
" let a = version_vector.new()\n"
" |> version_vector.increment(node_a)\n"
" |> version_vector.increment(node_a)\n"
" let b = version_vector.new()\n"
" |> version_vector.increment(node_b)\n"
" version_vector.compare(a, b) // -> Concurrent\n"
" ```\n"
).
-type order() :: before | 'after' | concurrent | equal.
-opaque version_vector() :: {version_vector,
gleam@dict:dict(lattice_core@replica_id:replica_id(), integer())}.
-file("src/lattice_core/version_vector.gleam", 60).
?DOC(
" Create a new empty version vector.\n"
"\n"
" All replica clocks start at zero (missing entries are treated as zero).\n"
).
-spec new() -> version_vector().
new() ->
{version_vector, maps:new()}.
-file("src/lattice_core/version_vector.gleam", 68).
?DOC(
" Increment the clock for a specific replica.\n"
"\n"
" Returns a new version vector with `replica_id`'s clock increased by one.\n"
" This is the standard way to record a new event at `replica_id`.\n"
).
-spec increment(version_vector(), lattice_core@replica_id:replica_id()) -> version_vector().
increment(Vv, Replica_id) ->
{version_vector, Dict} = Vv,
Current = gleam@result:unwrap(gleam_stdlib:map_get(Dict, Replica_id), 0),
{version_vector, gleam@dict:insert(Dict, Replica_id, Current + 1)}.
-file("src/lattice_core/version_vector.gleam", 78).
?DOC(
" Get the clock value for a specific replica.\n"
"\n"
" Returns `0` if `replica_id` has not been seen (missing entries default\n"
" to zero, consistent with the version vector semantics).\n"
).
-spec get(version_vector(), lattice_core@replica_id:replica_id()) -> integer().
get(Vv, Replica_id) ->
{version_vector, Dict} = Vv,
gleam@result:unwrap(gleam_stdlib:map_get(Dict, Replica_id), 0).
-file("src/lattice_core/version_vector.gleam", 88).
?DOC(
" Compare two version vectors and return their causal ordering.\n"
"\n"
" Returns `Equal` if all clocks match, `Before` if `a` is strictly dominated\n"
" by `b`, `After` if `a` strictly dominates `b`, or `Concurrent` if neither\n"
" dominates the other.\n"
).
-spec compare(version_vector(), version_vector()) -> order().
compare(A, B) ->
{version_vector, Da} = A,
{version_vector, Db} = B,
{Greater, Less} = gleam@dict:fold(
Da,
{false, false},
fun(Acc, K, V_a) ->
{G, L} = Acc,
V_b = gleam@result:unwrap(gleam_stdlib:map_get(Db, K), 0),
{G orelse (V_a > V_b), L orelse (V_a < V_b)}
end
),
{Greater@1, Less@1} = gleam@dict:fold(
Db,
{Greater, Less},
fun(Acc@1, K@1, _) ->
{G@1, _} = Acc@1,
case gleam@dict:has_key(Da, K@1) of
true ->
Acc@1;
false ->
{G@1, true}
end
end
),
case {Greater@1, Less@1} of
{false, false} ->
equal;
{true, false} ->
'after';
{false, true} ->
before;
{true, true} ->
concurrent
end.
-file("src/lattice_core/version_vector.gleam", 124).
?DOC(
" Check whether version vector `a` dominates `b`.\n"
"\n"
" Returns `True` when every clock in `a` is greater than or equal to the\n"
" corresponding clock in `b`. Equivalently, `compare(a, b)` is `Equal` or\n"
" `After`.\n"
).
-spec dominates(version_vector(), version_vector()) -> boolean().
dominates(A, B) ->
case compare(A, B) of
equal ->
true;
'after' ->
true;
before ->
false;
concurrent ->
false
end.
-file("src/lattice_core/version_vector.gleam", 132).
?DOC(" Check whether a version vector is empty (has no clock entries).\n").
-spec is_empty(version_vector()) -> boolean().
is_empty(Vv) ->
{version_vector, D} = Vv,
gleam@dict:is_empty(D).
-file("src/lattice_core/version_vector.gleam", 141).
?DOC(
" Set the clock for a replica to the maximum of the current value and `value`.\n"
"\n"
" If the replica has no entry, `value` is used. This avoids round-tripping\n"
" through `to_dict`/`from_dict` when building a version vector incrementally.\n"
).
-spec set_max(version_vector(), lattice_core@replica_id:replica_id(), integer()) -> version_vector().
set_max(Vv, Replica_id, Value) ->
{version_vector, D} = Vv,
Current = gleam@result:unwrap(gleam_stdlib:map_get(D, Replica_id), 0),
case Value > Current of
true ->
{version_vector, gleam@dict:insert(D, Replica_id, Value)};
false ->
Vv
end.
-file("src/lattice_core/version_vector.gleam", 158).
?DOC(
" Merge two version vectors using pairwise maximum.\n"
"\n"
" For each replica, the merged clock is the maximum of the two inputs.\n"
" This operation is commutative, associative, and idempotent.\n"
).
-spec merge(version_vector(), version_vector()) -> version_vector().
merge(A, B) ->
{version_vector, Da} = A,
{version_vector, Db} = B,
Merged = gleam@dict:fold(
Db,
Da,
fun(Acc, K, V_b) -> case gleam_stdlib:map_get(Acc, K) of
{ok, V_a} ->
gleam@dict:insert(Acc, K, gleam@int:max(V_a, V_b));
{error, nil} ->
gleam@dict:insert(Acc, K, V_b)
end end
),
{version_vector, Merged}.
-file("src/lattice_core/version_vector.gleam", 179).
?DOC(
" Encode a VersionVector as a self-describing JSON value.\n"
"\n"
" Produces an envelope with `type`, `v` (schema version), and `state`.\n"
" Format: `{\"type\": \"version_vector\", \"v\": 1, \"state\": {\"clocks\": {...}}}`\n"
"\n"
" Use `from_json` to decode the result back into a `VersionVector`.\n"
).
-spec to_json(version_vector()) -> gleam@json:json().
to_json(Vv) ->
{version_vector, D} = Vv,
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"version_vector"/utf8>>)},
{<<"v"/utf8>>, gleam@json:int(1)},
{<<"state"/utf8>>,
gleam@json:object(
[{<<"clocks"/utf8>>,
gleam@json:dict(
D,
fun(K) ->
lattice_core@replica_id:to_string(K)
end,
fun gleam@json:int/1
)}]
)}]
).
-file("src/lattice_core/version_vector.gleam", 197).
?DOC(
" Decode a VersionVector from a JSON string produced by `to_json`.\n"
"\n"
" Returns `Ok(VersionVector)` on success, or `Error(json.DecodeError)` if\n"
" the input is not a valid version-vector JSON envelope.\n"
).
-spec from_json(binary()) -> {ok, version_vector()} |
{error, gleam@json:decode_error()}.
from_json(Json_string) ->
State_decoder = begin
gleam@dynamic@decode:field(
<<"state"/utf8>>,
begin
gleam@dynamic@decode:field(
<<"clocks"/utf8>>,
gleam@dynamic@decode:dict(
lattice_core@replica_id:decoder(),
{decoder, fun gleam@dynamic@decode:decode_int/1}
),
fun(Clocks) ->
gleam@dynamic@decode:success({version_vector, Clocks})
end
)
end,
fun(State) -> gleam@dynamic@decode:success(State) end
)
end,
Envelope_decoder = begin
gleam@dynamic@decode:field(
<<"type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Type_tag) ->
gleam@dynamic@decode:field(
<<"v"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(Version) ->
gleam@dynamic@decode:success({Type_tag, Version})
end
)
end
)
end,
case gleam@json:parse(Json_string, Envelope_decoder) of
{error, E} ->
{error, E};
{ok, {Type_tag@1, Version@1}} ->
case (Type_tag@1 =:= <<"version_vector"/utf8>>) andalso (Version@1
=:= 1) of
true ->
gleam@json:parse(Json_string, State_decoder);
false ->
{error,
{unable_to_decode,
[{decode_error,
<<"type=version_vector and v=1"/utf8>>,
<<<<Type_tag@1/binary, " v="/utf8>>/binary,
(erlang:integer_to_binary(Version@1))/binary>>,
[]}]}}
end
end.
-file("src/lattice_core/version_vector.gleam", 237).
?DOC(
" A JSON decoder for VersionVector values.\n"
"\n"
" Decodes the self-describing envelope format produced by `to_json`.\n"
" Useful as a building block in `from_json` decoders when a VersionVector\n"
" is embedded inline within another JSON structure.\n"
).
-spec decoder() -> gleam@dynamic@decode:decoder(version_vector()).
decoder() ->
gleam@dynamic@decode:field(
<<"type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(_) ->
gleam@dynamic@decode:field(
<<"v"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_int/1},
fun(_) ->
gleam@dynamic@decode:field(
<<"state"/utf8>>,
begin
gleam@dynamic@decode:field(
<<"clocks"/utf8>>,
gleam@dynamic@decode:dict(
lattice_core@replica_id:decoder(),
{decoder,
fun gleam@dynamic@decode:decode_int/1}
),
fun(Clocks) ->
gleam@dynamic@decode:success(Clocks)
end
)
end,
fun(Clocks@1) ->
gleam@dynamic@decode:success(
{version_vector, Clocks@1}
)
end
)
end
)
end
).
-file("src/lattice_core/version_vector.gleam", 256).
?DOC(
" Extract the internal clock dictionary from a VersionVector.\n"
"\n"
" Returns a `Dict(ReplicaId, Int)` mapping replica IDs to their clock values.\n"
" Useful for serialization or when you need direct access to the raw clock\n"
" data. Prefer the higher-level API (`get`, `compare`, `merge`) for most\n"
" use cases.\n"
).
-spec to_dict(version_vector()) -> gleam@dict:dict(lattice_core@replica_id:replica_id(), integer()).
to_dict(Vv) ->
{version_vector, D} = Vv,
D.
-file("src/lattice_core/version_vector.gleam", 267).
?DOC(
" Construct a VersionVector from a raw clock dictionary.\n"
"\n"
" Creates a version vector from a `Dict(ReplicaId, Int)` mapping replica IDs\n"
" to clock values. Useful for deserialization or constructing a version\n"
" vector from external data. Prefer `new` and `increment` for most use\n"
" cases.\n"
).
-spec from_dict(
gleam@dict:dict(lattice_core@replica_id:replica_id(), integer())
) -> version_vector().
from_dict(D) ->
{version_vector, D}.