Packages
Grow-only, two-phase, and observed-remove sets for Gleam — conflict-free replicated set types
Current section
Files
Jump to
Current section
Files
src/lattice_sets@two_p_set.erl
-module(lattice_sets@two_p_set).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lattice_sets/two_p_set.gleam").
-export([new/0, add/2, remove/2, contains/2, value/1, merge/2, to_json/1, from_json/1]).
-export_type([two_p_set/1]).
-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 two-phase set (2P-Set) CRDT.\n"
"\n"
" Supports both add and remove, but an element can only be removed once. Once\n"
" removed (tombstoned), an element can never be re-added. Internally tracks\n"
" two sets: `added` and `removed`. An element is active if it is in `added`\n"
" but not in `removed`. Use `ORSet` if you need re-add after remove.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import lattice_sets/two_p_set\n"
"\n"
" let set = two_p_set.new()\n"
" |> two_p_set.add(\"alice\")\n"
" |> two_p_set.add(\"bob\")\n"
" |> two_p_set.remove(\"bob\")\n"
" two_p_set.contains(set, \"alice\") // -> True\n"
" two_p_set.contains(set, \"bob\") // -> False (tombstoned)\n"
" ```\n"
).
-opaque two_p_set(PXK) :: {two_p_set, gleam@set:set(PXK), gleam@set:set(PXK)}.
-file("src/lattice_sets/two_p_set.gleam", 37).
?DOC(" Create a new empty 2P-Set.\n").
-spec new() -> two_p_set(any()).
new() ->
{two_p_set, gleam@set:new(), gleam@set:new()}.
-file("src/lattice_sets/two_p_set.gleam", 46).
?DOC(
" Add an element to the set.\n"
"\n"
" If the element has already been tombstoned (removed), this call records the\n"
" element in `added` but the element will not be considered active because\n"
" the tombstone takes precedence.\n"
).
-spec add(two_p_set(PXN), PXN) -> two_p_set(PXN).
add(Tpset, Element) ->
{two_p_set,
gleam@set:insert(erlang:element(2, Tpset), Element),
erlang:element(3, Tpset)}.
-file("src/lattice_sets/two_p_set.gleam", 54).
?DOC(
" Remove an element from the set by adding it to the tombstone set.\n"
"\n"
" Once tombstoned, the element is permanently inactive. Removing an element\n"
" that was never added is also valid and creates a preemptive tombstone.\n"
).
-spec remove(two_p_set(PXQ), PXQ) -> two_p_set(PXQ).
remove(Tpset, Element) ->
{two_p_set,
erlang:element(2, Tpset),
gleam@set:insert(erlang:element(3, Tpset), Element)}.
-file("src/lattice_sets/two_p_set.gleam", 61).
?DOC(
" Check if the set currently contains the given element.\n"
"\n"
" Returns `True` only if `element` is in `added` and NOT in `removed`.\n"
).
-spec contains(two_p_set(PXT), PXT) -> boolean().
contains(Tpset, Element) ->
gleam@set:contains(erlang:element(2, Tpset), Element) andalso not gleam@set:contains(
erlang:element(3, Tpset),
Element
).
-file("src/lattice_sets/two_p_set.gleam", 69).
?DOC(
" Return the set of all currently active elements.\n"
"\n"
" Active elements are those in `added` that have not been tombstoned.\n"
" Equivalent to `added ∖ removed`.\n"
).
-spec value(two_p_set(PXV)) -> gleam@set:set(PXV).
value(Tpset) ->
gleam@set:filter(
erlang:element(2, Tpset),
fun(Element) ->
not gleam@set:contains(erlang:element(3, Tpset), Element)
end
).
-file("src/lattice_sets/two_p_set.gleam", 77).
?DOC(
" Merge two 2P-Sets by taking the union of both added sets and both removed sets.\n"
"\n"
" A tombstone on any replica propagates to all replicas after merge.\n"
" Merge is commutative, associative, and idempotent (a valid CRDT join).\n"
).
-spec merge(two_p_set(PXY), two_p_set(PXY)) -> two_p_set(PXY).
merge(A, B) ->
{two_p_set,
gleam@set:union(erlang:element(2, A), erlang:element(2, B)),
gleam@set:union(erlang:element(3, A), erlang:element(3, B))}.
-file("src/lattice_sets/two_p_set.gleam", 89).
?DOC(
" Encode a `TwoPSet(String)` as a self-describing JSON value.\n"
"\n"
" Format: `{\"type\": \"two_p_set\", \"v\": 1, \"state\": {\"added\": [...], \"removed\": [...]}}`\n"
"\n"
" The encoded value can be restored with `from_json`.\n"
).
-spec to_json(two_p_set(binary())) -> gleam@json:json().
to_json(Tpset) ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"two_p_set"/utf8>>)},
{<<"v"/utf8>>, gleam@json:int(1)},
{<<"state"/utf8>>,
gleam@json:object(
[{<<"added"/utf8>>,
gleam@json:array(
gleam@set:to_list(erlang:element(2, Tpset)),
fun gleam@json:string/1
)},
{<<"removed"/utf8>>,
gleam@json:array(
gleam@set:to_list(erlang:element(3, Tpset)),
fun gleam@json:string/1
)}]
)}]
).
-file("src/lattice_sets/two_p_set.gleam", 107).
?DOC(
" Decode a `TwoPSet(String)` from a JSON string produced by `to_json`.\n"
"\n"
" Returns `Error` if the string is not valid JSON or does not match the\n"
" expected format.\n"
).
-spec from_json(binary()) -> {ok, two_p_set(binary())} |
{error, gleam@json:decode_error()}.
from_json(Json_string) ->
State_decoder = begin
gleam@dynamic@decode:field(
<<"state"/utf8>>,
begin
gleam@dynamic@decode:field(
<<"added"/utf8>>,
gleam@dynamic@decode:list(
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
fun(Added) ->
gleam@dynamic@decode:field(
<<"removed"/utf8>>,
gleam@dynamic@decode:list(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Removed) ->
gleam@dynamic@decode:success(
{two_p_set,
gleam@set:from_list(Added),
gleam@set:from_list(Removed)}
)
end
)
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 =:= <<"two_p_set"/utf8>>) andalso (Version@1 =:= 1) of
true ->
gleam@json:parse(Json_string, State_decoder);
false ->
{error,
{unable_to_decode,
[{decode_error,
<<"type=two_p_set and v=1"/utf8>>,
<<<<Type_tag@1/binary, " v="/utf8>>/binary,
(erlang:integer_to_binary(Version@1))/binary>>,
[]}]}}
end
end.