Packages

Last-writer-wins and observed-remove maps for Gleam — conflict-free replicated map types with generic value support

Current section

Files

Jump to
lattice_maps src lattice_maps@crdt.erl
Raw

src/lattice_maps@crdt.erl

-module(lattice_maps@crdt).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lattice_maps/crdt.gleam").
-export([type_name/1, default_crdt/2, matches_spec/2, merge/2, to_json/1, from_json/1]).
-export_type([crdt/0, merge_error/0, crdt_spec/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 tagged union over all leaf CRDT types with dynamic dispatch.\n"
"\n"
" The `Crdt` type wraps individual CRDTs (counters, registers, sets) so they\n"
" can be stored and merged uniformly — this is how `ORMap` holds heterogeneous\n"
" values. For direct use, prefer the individual modules (e.g., `g_counter`,\n"
" `or_set`) for type-safe access.\n"
"\n"
" Maps (`LWWMap`, `ORMap`) are **not** included in this union to avoid circular\n"
" module dependencies.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import lattice_maps/crdt\n"
" import lattice_core/replica_id\n"
" import lattice_counters/g_counter\n"
"\n"
" let a = crdt.CrdtGCounter(g_counter.new(replica_id.new(\"node-a\")) |> g_counter.increment(1))\n"
" let b = crdt.CrdtGCounter(g_counter.new(replica_id.new(\"node-b\")) |> g_counter.increment(2))\n"
" let assert Ok(merged) = crdt.merge(a, b)\n"
" ```\n"
).
-type crdt() :: {crdt_g_counter, lattice_counters@g_counter:g_counter()} |
{crdt_pn_counter, lattice_counters@pn_counter:p_n_counter()} |
{crdt_lww_register, lattice_registers@lww_register:l_w_w_register(binary())} |
{crdt_mv_register, lattice_registers@mv_register:m_v_register(binary())} |
{crdt_g_set, lattice_sets@g_set:g_set(binary())} |
{crdt_two_p_set, lattice_sets@two_p_set:two_p_set(binary())} |
{crdt_or_set, lattice_sets@or_set:o_r_set(binary())} |
{crdt_version_vector, lattice_core@version_vector:version_vector()}.
-type merge_error() :: {type_mismatch, binary(), binary()}.
-type crdt_spec() :: g_counter_spec |
pn_counter_spec |
lww_register_spec |
mv_register_spec |
g_set_spec |
two_p_set_spec |
or_set_spec.
-file("src/lattice_maps/crdt.gleam", 70).
?DOC(" Return a human-readable type name for a wrapped `Crdt` value.\n").
-spec type_name(crdt()) -> binary().
type_name(Value) ->
case Value of
{crdt_g_counter, _} ->
<<"g_counter"/utf8>>;
{crdt_pn_counter, _} ->
<<"pn_counter"/utf8>>;
{crdt_lww_register, _} ->
<<"lww_register"/utf8>>;
{crdt_mv_register, _} ->
<<"mv_register"/utf8>>;
{crdt_g_set, _} ->
<<"g_set"/utf8>>;
{crdt_two_p_set, _} ->
<<"two_p_set"/utf8>>;
{crdt_or_set, _} ->
<<"or_set"/utf8>>;
{crdt_version_vector, _} ->
<<"version_vector"/utf8>>
end.
-file("src/lattice_maps/crdt.gleam", 111).
?DOC(
" Create a new default (bottom) value of the specified CRDT type.\n"
"\n"
" The `replica_id` is passed to CRDT constructors that require it\n"
" (counters, registers, OR-Set). For types that don't use a replica\n"
" identifier (G-Set, 2P-Set), the argument is ignored.\n"
"\n"
" Default values per spec:\n"
" - `GCounterSpec` / `PnCounterSpec` — new counter for `replica_id`\n"
" - `LwwRegisterSpec` — empty string `\"\"` at timestamp `0` for `replica_id` (bottom element)\n"
" - `MvRegisterSpec` — new MV-Register for `replica_id`\n"
" - `GSetSpec` / `TwoPSetSpec` — empty set (no replica needed)\n"
" - `OrSetSpec` — new OR-Set for `replica_id`\n"
).
-spec default_crdt(crdt_spec(), lattice_core@replica_id:replica_id()) -> crdt().
default_crdt(Spec, Replica_id) ->
case Spec of
g_counter_spec ->
{crdt_g_counter, lattice_counters@g_counter:new(Replica_id)};
pn_counter_spec ->
{crdt_pn_counter, lattice_counters@pn_counter:new(Replica_id)};
lww_register_spec ->
{crdt_lww_register,
lattice_registers@lww_register:new(<<""/utf8>>, 0, Replica_id)};
mv_register_spec ->
{crdt_mv_register, lattice_registers@mv_register:new(Replica_id)};
g_set_spec ->
{crdt_g_set, lattice_sets@g_set:new()};
two_p_set_spec ->
{crdt_two_p_set, lattice_sets@two_p_set:new()};
or_set_spec ->
{crdt_or_set, lattice_sets@or_set:new(Replica_id)}
end.
-file("src/lattice_maps/crdt.gleam", 124).
?DOC(" Return `True` when a wrapped CRDT matches the expected `CrdtSpec`.\n").
-spec matches_spec(crdt(), crdt_spec()) -> boolean().
matches_spec(Value, Spec) ->
case {Value, Spec} of
{{crdt_g_counter, _}, g_counter_spec} ->
true;
{{crdt_pn_counter, _}, pn_counter_spec} ->
true;
{{crdt_lww_register, _}, lww_register_spec} ->
true;
{{crdt_mv_register, _}, mv_register_spec} ->
true;
{{crdt_g_set, _}, g_set_spec} ->
true;
{{crdt_two_p_set, _}, two_p_set_spec} ->
true;
{{crdt_or_set, _}, or_set_spec} ->
true;
{_, _} ->
false
end.
-file("src/lattice_maps/crdt.gleam", 145).
?DOC(
" Dispatch merge to the type-specific merge function for matching variants.\n"
"\n"
" If `a` and `b` hold the same variant, their inner values are merged using\n"
" the type-specific merge function and returned as `Ok(merged)`.\n"
"\n"
" If `a` and `b` hold different variants, returns\n"
" `Error(TypeMismatch(expected: ..., found: ...))` where `expected` is the\n"
" type name of `a` and `found` is the type name of `b`.\n"
).
-spec merge(crdt(), crdt()) -> {ok, crdt()} | {error, merge_error()}.
merge(A, B) ->
case {A, B} of
{{crdt_g_counter, Ca}, {crdt_g_counter, Cb}} ->
{ok, {crdt_g_counter, lattice_counters@g_counter:merge(Ca, Cb)}};
{{crdt_pn_counter, Ca@1}, {crdt_pn_counter, Cb@1}} ->
{ok,
{crdt_pn_counter, lattice_counters@pn_counter:merge(Ca@1, Cb@1)}};
{{crdt_lww_register, Ca@2}, {crdt_lww_register, Cb@2}} ->
{ok,
{crdt_lww_register,
lattice_registers@lww_register:merge(Ca@2, Cb@2)}};
{{crdt_mv_register, Ca@3}, {crdt_mv_register, Cb@3}} ->
{ok,
{crdt_mv_register,
lattice_registers@mv_register:merge(Ca@3, Cb@3)}};
{{crdt_g_set, Ca@4}, {crdt_g_set, Cb@4}} ->
{ok, {crdt_g_set, lattice_sets@g_set:merge(Ca@4, Cb@4)}};
{{crdt_two_p_set, Ca@5}, {crdt_two_p_set, Cb@5}} ->
{ok, {crdt_two_p_set, lattice_sets@two_p_set:merge(Ca@5, Cb@5)}};
{{crdt_or_set, Ca@6}, {crdt_or_set, Cb@6}} ->
{ok, {crdt_or_set, lattice_sets@or_set:merge(Ca@6, Cb@6)}};
{{crdt_version_vector, Ca@7}, {crdt_version_vector, Cb@7}} ->
{ok,
{crdt_version_vector,
lattice_core@version_vector:merge(Ca@7, Cb@7)}};
{_, _} ->
{error, {type_mismatch, type_name(A), type_name(B)}}
end.
-file("src/lattice_maps/crdt.gleam", 169).
?DOC(
" Dispatch `to_json` to the type-specific serializer for the wrapped CRDT.\n"
"\n"
" Each variant delegates to its module's `to_json`. The resulting JSON\n"
" includes a `\"type\"` field (e.g., `\"g_counter\"`) that `from_json` uses\n"
" to select the correct decoder on deserialization.\n"
).
-spec to_json(crdt()) -> gleam@json:json().
to_json(Crdt) ->
case Crdt of
{crdt_g_counter, C} ->
lattice_counters@g_counter:to_json(C);
{crdt_pn_counter, C@1} ->
lattice_counters@pn_counter:to_json(C@1);
{crdt_lww_register, C@2} ->
lattice_registers@lww_register:to_json(C@2);
{crdt_mv_register, C@3} ->
lattice_registers@mv_register:to_json(C@3);
{crdt_g_set, C@4} ->
lattice_sets@g_set:to_json(C@4);
{crdt_two_p_set, C@5} ->
lattice_sets@two_p_set:to_json(C@5);
{crdt_or_set, C@6} ->
lattice_sets@or_set:to_json(C@6);
{crdt_version_vector, C@7} ->
lattice_core@version_vector:to_json(C@7)
end.
-file("src/lattice_maps/crdt.gleam", 198).
-spec dispatch_decode(binary(), binary()) -> {ok, crdt()} |
{error, gleam@json:decode_error()}.
dispatch_decode(Type_tag, Json_string) ->
case Type_tag of
<<"g_counter"/utf8>> ->
case lattice_counters@g_counter:from_json(Json_string) of
{ok, C} ->
{ok, {crdt_g_counter, C}};
{error, E} ->
{error, E}
end;
<<"pn_counter"/utf8>> ->
case lattice_counters@pn_counter:from_json(Json_string) of
{ok, C@1} ->
{ok, {crdt_pn_counter, C@1}};
{error, E@1} ->
{error, E@1}
end;
<<"lww_register"/utf8>> ->
case lattice_registers@lww_register:from_json(Json_string) of
{ok, C@2} ->
{ok, {crdt_lww_register, C@2}};
{error, E@2} ->
{error, E@2}
end;
<<"mv_register"/utf8>> ->
case lattice_registers@mv_register:from_json(Json_string) of
{ok, C@3} ->
{ok, {crdt_mv_register, C@3}};
{error, E@3} ->
{error, E@3}
end;
<<"g_set"/utf8>> ->
case lattice_sets@g_set:from_json(Json_string) of
{ok, C@4} ->
{ok, {crdt_g_set, C@4}};
{error, E@4} ->
{error, E@4}
end;
<<"two_p_set"/utf8>> ->
case lattice_sets@two_p_set:from_json(Json_string) of
{ok, C@5} ->
{ok, {crdt_two_p_set, C@5}};
{error, E@5} ->
{error, E@5}
end;
<<"or_set"/utf8>> ->
case lattice_sets@or_set:from_json(Json_string) of
{ok, C@6} ->
{ok, {crdt_or_set, C@6}};
{error, E@6} ->
{error, E@6}
end;
<<"version_vector"/utf8>> ->
case lattice_core@version_vector:from_json(Json_string) of
{ok, C@7} ->
{ok, {crdt_version_vector, C@7}};
{error, E@7} ->
{error, E@7}
end;
_ ->
{error,
{unable_to_decode,
[{decode_error,
<<"known CRDT type"/utf8>>,
Type_tag,
[<<"type"/utf8>>]}]}}
end.
-file("src/lattice_maps/crdt.gleam", 187).
?DOC(
" Decode a `Crdt` from a JSON string produced by `to_json`.\n"
"\n"
" Reads the `\"type\"` field to determine which type-specific decoder to\n"
" use. Returns `Error` if the string is not valid JSON, the `\"type\"` field\n"
" is missing, or the type tag is not recognized.\n"
).
-spec from_json(binary()) -> {ok, crdt()} | {error, gleam@json:decode_error()}.
from_json(Json_string) ->
Type_decoder = begin
gleam@dynamic@decode:field(
<<"type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Type_tag) -> gleam@dynamic@decode:success(Type_tag) end
)
end,
case gleam@json:parse(Json_string, Type_decoder) of
{error, E} ->
{error, E};
{ok, Type_tag@1} ->
dispatch_decode(Type_tag@1, Json_string)
end.