Packages
macula
0.45.2
7.1.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/macula_routing_system/macula_routing_storage.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% Pure functional helpers for DHT provider storage operations.
%%% Extracted from macula_routing_server to reduce module size.
%%% Handles provider lists, deduplication, expiry, and ETS mirroring.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_routing_storage).
%% Provider list operations
-export([
ensure_provider_list/1,
upsert_provider/3,
deduplicate_providers/1,
get_node_id_from_value/1,
find_provider_index/2,
format_storage_value/1,
delete_provider_from_storage/3,
is_provider_expired/2
]).
%% ETS mirroring
-export([
mirror_to_ets/3,
mirror_delete_to_ets/3
]).
%% Formatting
-export([
format_node_id/1,
get_node_endpoint/1
]).
%% DHT value expiry: values with TTL older than this are expired (default 5 minutes + 60s grace)
-define(VALUE_MAX_AGE_MS, 360000).
%%%===================================================================
%%% Provider List Operations
%%%===================================================================
%% @doc Ensure value is a list for multi-provider storage.
-spec ensure_provider_list(term()) -> [term()].
ensure_provider_list(List) when is_list(List) -> List;
ensure_provider_list(Value) -> [Value].
%% @doc Insert or update a provider in the list.
%% Stamps every entry with stored_at for expiry tracking.
-spec upsert_provider(binary() | undefined, map(), [map()]) -> [map()].
upsert_provider(undefined, Value, ProviderList) ->
[Value#{stored_at => erlang:system_time(millisecond)} | ProviderList];
upsert_provider(NodeId, Value, ProviderList) ->
Stamped = Value#{stored_at => erlang:system_time(millisecond)},
upsert_by_index(find_provider_index(NodeId, ProviderList), Stamped, ProviderList).
%% @doc Deduplicate providers by node_id -- keeps first occurrence.
-spec deduplicate_providers([map()]) -> [map()].
deduplicate_providers(Providers) ->
lists:foldl(fun(P, Acc) ->
NId = get_node_id_from_value(P),
case lists:any(fun(A) -> get_node_id_from_value(A) =:= NId end, Acc) of
true -> Acc;
false -> [P | Acc]
end
end, [], Providers).
%% @doc Extract node_id from value map, handling both atom and binary keys.
-spec get_node_id_from_value(map()) -> binary() | undefined.
get_node_id_from_value(#{node_id := NodeId}) -> NodeId;
get_node_id_from_value(#{<<"node_id">> := NodeId}) -> NodeId;
get_node_id_from_value(_Value) -> undefined.
%% @doc Find index of provider with matching node_id in provider list.
-spec find_provider_index(binary(), [map()]) -> pos_integer() | not_found.
find_provider_index(NodeId, ProviderList) ->
find_provider_index(NodeId, ProviderList, 1).
%% @doc Format storage value for get_local response.
-spec format_storage_value(undefined | term()) -> not_found | {ok, [term()]}.
format_storage_value(undefined) -> not_found;
format_storage_value(Value) when is_list(Value) -> {ok, Value};
format_storage_value(Value) -> {ok, [Value]}.
%% @doc Delete a specific provider from storage by node_id.
-spec delete_provider_from_storage(binary(), binary(), map()) -> map().
delete_provider_from_storage(Key, NodeId, Storage) ->
case maps:get(Key, Storage, undefined) of
undefined -> Storage;
Providers when is_list(Providers) ->
delete_provider_by_node_id(Key, NodeId, Providers, Storage);
_SingleValue -> maps:remove(Key, Storage)
end.
%% @doc Check if a provider entry is expired based on its stored_at timestamp.
-spec is_provider_expired(map(), integer()) -> boolean().
is_provider_expired(#{stored_at := StoredAt}, Now) ->
(Now - StoredAt) > ?VALUE_MAX_AGE_MS;
is_provider_expired(#{<<"stored_at">> := StoredAt}, Now) ->
(Now - StoredAt) > ?VALUE_MAX_AGE_MS;
is_provider_expired(#{ttl := _TTL}, _Now) ->
true;
is_provider_expired(#{<<"ttl">> := _TTL}, _Now) ->
true;
is_provider_expired(_Provider, _Now) ->
false.
%%%===================================================================
%%% ETS Mirroring
%%%===================================================================
%% @doc Mirror a key-value update to ETS for concurrent reads.
-spec mirror_to_ets(atom() | ets:tid() | undefined, binary(), [term()]) -> ok.
mirror_to_ets(undefined, _Key, _Providers) -> ok;
mirror_to_ets(Ets, _Key, []) -> ets:delete(Ets, _Key), ok;
mirror_to_ets(Ets, Key, Providers) -> ets:insert(Ets, {Key, Providers}), ok.
%% @doc Mirror a delete operation to ETS.
-spec mirror_delete_to_ets(atom() | ets:tid() | undefined, binary(), map()) -> ok.
mirror_delete_to_ets(undefined, _Key, _Storage) -> ok;
mirror_delete_to_ets(Ets, Key, Storage) ->
case maps:get(Key, Storage, []) of
[] -> ets:delete(Ets, Key), ok;
V -> ets:insert(Ets, {Key, V}), ok
end.
%%%===================================================================
%%% Formatting
%%%===================================================================
%% @doc Format node_id for logging.
-spec format_node_id(binary() | undefined) -> binary().
format_node_id(undefined) -> <<"?">>;
format_node_id(B) when is_binary(B), byte_size(B) =:= 32 -> binary:encode_hex(B);
format_node_id(B) when is_binary(B) -> B;
format_node_id(_) -> <<"?">>.
%% @doc Extract endpoint from node info.
-spec get_node_endpoint(map()) -> binary() | undefined.
get_node_endpoint(#{endpoint := Endpoint}) when is_binary(Endpoint) ->
Endpoint;
get_node_endpoint(#{address := {Host, Port}}) when is_integer(Port) ->
iolist_to_binary([format_host(Host), <<":">>, integer_to_binary(Port)]);
get_node_endpoint(#{<<"endpoint">> := Endpoint}) when is_binary(Endpoint) ->
Endpoint;
get_node_endpoint(#{address := Address}) when is_binary(Address) ->
Address;
get_node_endpoint(#{<<"address">> := Address}) when is_binary(Address) ->
Address;
get_node_endpoint(_) ->
undefined.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
find_provider_index(_NodeId, [], _Index) ->
not_found;
find_provider_index(NodeId, [Provider | Rest], Index) ->
ProviderNodeId = get_node_id_from_value(Provider),
check_provider_match(NodeId, ProviderNodeId, Rest, Index).
check_provider_match(NodeId, NodeId, _Rest, Index) -> Index;
check_provider_match(NodeId, _Other, Rest, Index) ->
find_provider_index(NodeId, Rest, Index + 1).
upsert_by_index(not_found, Value, ProviderList) ->
[Value | ProviderList];
upsert_by_index(Index, Value, ProviderList) ->
lists:sublist(ProviderList, Index - 1) ++ [Value] ++ lists:nthtail(Index, ProviderList).
delete_provider_by_node_id(Key, NodeId, Providers, Storage) ->
FilterFn = fun(P) -> get_node_id_from_value(P) =/= NodeId end,
UpdatedProviders = lists:filter(FilterFn, Providers),
update_or_remove_key(Key, UpdatedProviders, Storage).
update_or_remove_key(Key, [], Storage) -> maps:remove(Key, Storage);
update_or_remove_key(Key, Providers, Storage) -> Storage#{Key => Providers}.
%% @private Format host for URL.
format_host({A, B, C, D}) when is_integer(A) ->
io_lib:format("~B.~B.~B.~B", [A, B, C, D]);
format_host(Host) when is_list(Host) ->
Host;
format_host(Host) when is_binary(Host) ->
binary_to_list(Host).