Packages
macula
0.42.6
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_discovery.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% Generic DHT-based service discovery.
%%% Provides cache-integrated lookup and announcement operations.
%%% Used by both pub/sub and RPC discovery layers.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_discovery).
%% API
-export([
find/2,
find_with_cache/3,
find_with_cache/4,
announce/2,
unannounce/2,
filter_by_age/3
]).
%% Types
-type key() :: binary().
-type value() :: term().
-type lookup_fun() :: fun((key()) -> {ok, [value()]} | {error, term()}).
-type publish_fun() :: fun((key()) -> ok | {error, term()}).
-type unpublish_fun() :: fun((key()) -> ok | {error, term()}).
-export_type([
key/0,
value/0,
lookup_fun/0,
publish_fun/0,
unpublish_fun/0
]).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Find values for a key via DHT lookup.
-spec find(key(), lookup_fun()) -> {ok, [value()]} | {error, term()}.
find(Key, LookupFun) ->
LookupFun(Key).
%% @doc Find values with cache (default TTL: 300 seconds).
-spec find_with_cache(key(), macula_cache:cache(), lookup_fun()) ->
{ok, [value()], macula_cache:cache()} | {error, term(), macula_cache:cache()}.
find_with_cache(Key, Cache, LookupFun) ->
find_with_cache(Key, Cache, LookupFun, 300).
%% @doc Find values with cache and custom TTL.
-spec find_with_cache(key(), macula_cache:cache(), lookup_fun(), pos_integer()) ->
{ok, [value()], macula_cache:cache()} | {error, term(), macula_cache:cache()}.
find_with_cache(Key, Cache, LookupFun, TTL) ->
CacheResult = macula_cache:get(Cache, Key),
do_find_with_cache(CacheResult, Key, Cache, LookupFun, TTL).
%% @private Cache hit - check expiry
do_find_with_cache({ok, Values, UpdatedCache}, Key, Cache, LookupFun, TTL) ->
Expired = is_cache_entry_expired(UpdatedCache, Key, TTL),
handle_cache_expiry(Expired, Values, UpdatedCache, Key, Cache, LookupFun);
%% @private Cache miss - query DHT
do_find_with_cache(not_found, Key, Cache, LookupFun, _TTL) ->
perform_dht_lookup(Key, Cache, LookupFun).
%% @private Cache entry expired - query DHT
handle_cache_expiry(true, _Values, _UpdatedCache, Key, Cache, LookupFun) ->
perform_dht_lookup(Key, Cache, LookupFun);
%% @private Cache entry valid - return it
handle_cache_expiry(false, Values, UpdatedCache, _Key, _Cache, _LookupFun) ->
{ok, Values, UpdatedCache}.
%% @doc Announce value to DHT.
-spec announce(key(), publish_fun()) -> ok | {error, term()}.
announce(Key, PublishFun) ->
PublishFun(Key).
%% @doc Remove value from DHT.
-spec unannounce(key(), unpublish_fun()) -> ok | {error, term()}.
unannounce(Key, UnpublishFun) ->
UnpublishFun(Key).
%% @doc Filter items by age based on last_seen timestamp and TTL.
%% Items must have #{last_seen := integer()} in their structure.
-spec filter_by_age([map()], pos_integer(), atom()) -> [map()].
filter_by_age(Items, TTL, TimestampField) ->
Now = erlang:system_time(millisecond),
MaxAge = TTL * 1000, % TTL in seconds, convert to milliseconds
lists:filter(fun(Item) -> is_item_fresh(Item, TimestampField, Now, MaxAge) end, Items).
%% @private Check if item is fresh based on timestamp
is_item_fresh(Item, TimestampField, Now, MaxAge) ->
FindResult = maps:find(TimestampField, Item),
check_item_freshness(FindResult, Now, MaxAge).
%% @private Timestamp found - check age
check_item_freshness({ok, LastSeen}, Now, MaxAge) ->
Age = Now - LastSeen,
Age =< MaxAge;
%% @private No timestamp field - assume expired
check_item_freshness(error, _Now, _MaxAge) ->
false.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @doc Perform DHT lookup and update cache.
-spec perform_dht_lookup(key(), macula_cache:cache(), lookup_fun()) ->
{ok, [value()], macula_cache:cache()} | {error, term(), macula_cache:cache()}.
perform_dht_lookup(Key, Cache, LookupFun) ->
LookupResult = LookupFun(Key),
do_dht_lookup(LookupResult, Key, Cache).
%% @private DHT lookup succeeded - cache result
do_dht_lookup({ok, Values}, Key, Cache) ->
UpdatedCache = macula_cache:put(Cache, Key, Values),
{ok, Values, UpdatedCache};
%% @private DHT lookup failed - don't update cache
do_dht_lookup({error, Reason}, _Key, Cache) ->
{error, Reason, Cache}.
%% @doc Check if cache entry is expired based on TTL.
-spec is_cache_entry_expired(macula_cache:cache(), key(), pos_integer()) -> boolean().
is_cache_entry_expired(#{entries := Entries}, Key, TTL) ->
SearchResult = lists:search(fun(E) -> maps:get(key, E) =:= Key end, Entries),
check_entry_expiry(SearchResult, TTL).
%% @private Entry found - check if expired
check_entry_expiry({value, Entry}, TTL) ->
Timestamp = maps:get(timestamp, Entry),
Now = erlang:system_time(millisecond),
Age = Now - Timestamp,
Age > (TTL * 1000); % TTL in seconds, Age in milliseconds
%% @private Entry not found - consider expired
check_entry_expiry(false, _TTL) ->
true.