-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>>, <<< 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}.