-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())}.