Packages

Conflict-free replicated data types (CRDTs) for Gleam — umbrella package that includes all lattice sub-packages

Current section

Files

Jump to
lattice_crdt src lattice@version_vector.erl
Raw

src/lattice@version_vector.erl

-module(lattice@version_vector).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lattice/version_vector.gleam").
-export([new/0, increment/2, get/2, to_json/1, from_json/1, to_dict/1, from_dict/1, compare/2, merge/2]).
-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/version_vector\n"
"\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(binary(), integer())}.
-file("src/lattice/version_vector.gleam", 56).
?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/version_vector.gleam", 64).
?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(), binary()) -> 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/version_vector.gleam", 74).
?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(), binary()) -> integer().
get(Vv, Replica_id) ->
{version_vector, Dict} = Vv,
gleam@result:unwrap(gleam_stdlib:map_get(Dict, Replica_id), 0).
-file("src/lattice/version_vector.gleam", 114).
?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) -> K end,
fun gleam@json:int/1
)}]
)}]
).
-file("src/lattice/version_vector.gleam", 132).
?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) ->
Decoder = begin
gleam@dynamic@decode:field(
<<"state"/utf8>>,
begin
gleam@dynamic@decode:field(
<<"clocks"/utf8>>,
gleam@dynamic@decode:dict(
{decoder, fun gleam@dynamic@decode:decode_string/1},
{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,
gleam@json:parse(Json_string, Decoder).
-file("src/lattice/version_vector.gleam", 151).
?DOC(
" Extract the internal clock dictionary from a VersionVector.\n"
"\n"
" Intended for use by serialization code in sibling modules (e.g.,\n"
" `mv_register`). Prefer the higher-level API (`get`, `compare`, `merge`)\n"
" for all other use cases.\n"
).
-spec to_dict(version_vector()) -> gleam@dict:dict(binary(), integer()).
to_dict(Vv) ->
{version_vector, D} = Vv,
D.
-file("src/lattice/version_vector.gleam", 160).
?DOC(
" Construct a VersionVector from a raw clock dictionary.\n"
"\n"
" Intended for use by deserialization code in sibling modules (e.g.,\n"
" `mv_register`). Prefer `new` and `increment` for all other use cases.\n"
).
-spec from_dict(gleam@dict:dict(binary(), integer())) -> version_vector().
from_dict(D) ->
{version_vector, D}.
-file("src/lattice/version_vector.gleam", 164).
-spec compare_helper(
gleam@dict:dict(binary(), integer()),
gleam@dict:dict(binary(), integer()),
list(binary()),
boolean(),
boolean()
) -> order().
compare_helper(A, B, Keys, Found_less, Found_greater) ->
case Keys of
[] ->
case {Found_less, Found_greater} of
{false, false} ->
equal;
{true, false} ->
before;
{false, true} ->
'after';
{_, _} ->
concurrent
end;
[Key | Rest] ->
A_val = gleam@result:unwrap(gleam_stdlib:map_get(A, Key), 0),
B_val = gleam@result:unwrap(gleam_stdlib:map_get(B, Key), 0),
case A_val < B_val of
true ->
compare_helper(A, B, Rest, true, Found_greater);
false ->
case A_val > B_val of
true ->
compare_helper(A, B, Rest, Found_less, true);
false ->
compare_helper(
A,
B,
Rest,
Found_less,
Found_greater
)
end
end
end.
-file("src/lattice/version_vector.gleam", 84).
?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, Dict_a} = A,
{version_vector, Dict_b} = B,
A_keys = maps:keys(Dict_a),
B_keys = maps:keys(Dict_b),
All_keys = gleam@list:unique(lists:append(A_keys, B_keys)),
compare_helper(Dict_a, Dict_b, All_keys, false, false).
-file("src/lattice/version_vector.gleam", 196).
-spec merge_helper(
gleam@dict:dict(binary(), integer()),
gleam@dict:dict(binary(), integer()),
list(binary()),
gleam@dict:dict(binary(), integer())
) -> gleam@dict:dict(binary(), integer()).
merge_helper(A, B, Keys, Acc) ->
case Keys of
[] ->
Acc;
[Key | Rest] ->
A_val = gleam@result:unwrap(gleam_stdlib:map_get(A, Key), 0),
B_val = gleam@result:unwrap(gleam_stdlib:map_get(B, Key), 0),
Merged_val = case A_val > B_val of
true ->
A_val;
false ->
B_val
end,
New_acc = gleam@dict:insert(Acc, Key, Merged_val),
merge_helper(A, B, Rest, New_acc)
end.
-file("src/lattice/version_vector.gleam", 98).
?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, Dict_a} = A,
{version_vector, Dict_b} = B,
A_keys = maps:keys(Dict_a),
B_keys = maps:keys(Dict_b),
All_keys = gleam@list:unique(lists:append(A_keys, B_keys)),
{version_vector, merge_helper(Dict_a, Dict_b, All_keys, maps:new())}.