Packages

macula

0.4.2
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
macula src macula_rpc_cache.erl
Raw

src/macula_rpc_cache.erl

%%%-------------------------------------------------------------------
%%% @doc
%%% LRU cache for RPC procedure results.
%%% Caches results of idempotent procedures to avoid repeated execution.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_rpc_cache).
%% API
-export([
new/1,
put/5,
put_with_timestamp/6,
get/3,
invalidate/3,
clear/1,
is_expired/3,
size/1,
max_size/1,
make_key/2
]).
%% Types
-type cache_entry() :: #{
key := binary(),
result := term(),
timestamp := integer(),
ttl := pos_integer()
}.
-type cache() :: #{
entries := [cache_entry()], % Ordered: head = most recent, tail = oldest
max_size := pos_integer()
}.
-export_type([cache/0, cache_entry/0]).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Create new cache with max size.
-spec new(pos_integer()) -> cache().
new(MaxSize) ->
#{
entries => [],
max_size => MaxSize
}.
%% @doc Make cache key from URI and args.
%% Uses hash of URI and args for consistent key generation.
-spec make_key(binary(), map()) -> binary().
make_key(Uri, Args) ->
%% Sort args by key for consistent hashing
SortedArgs = lists:sort(maps:to_list(Args)),
Data = term_to_binary({Uri, SortedArgs}),
crypto:hash(sha256, Data).
%% @doc Put entry in cache.
-spec put(cache(), binary(), map(), term(), pos_integer()) -> cache().
put(Cache, Uri, Args, Result, TTL) ->
Timestamp = erlang:system_time(millisecond),
put_with_timestamp(Cache, Uri, Args, Result, TTL, Timestamp).
%% @doc Put entry with custom timestamp (for testing).
-spec put_with_timestamp(cache(), binary(), map(), term(), pos_integer(), integer()) -> cache().
put_with_timestamp(#{entries := Entries, max_size := MaxSize} = Cache, Uri, Args, Result, TTL, Timestamp) ->
Key = make_key(Uri, Args),
Entry = #{
key => Key,
result => Result,
timestamp => Timestamp,
ttl => TTL
},
%% Remove existing entry for this key (if any)
EntriesWithoutKey = lists:filter(
fun(E) -> maps:get(key, E) =/= Key end,
Entries
),
%% Add new entry at head (most recent)
NewEntries = [Entry | EntriesWithoutKey],
%% Enforce max size (evict oldest)
FinalEntries = case length(NewEntries) > MaxSize of
true -> lists:sublist(NewEntries, MaxSize);
false -> NewEntries
end,
Cache#{entries => FinalEntries}.
%% @doc Get entry from cache.
%% Returns {ok, Result, UpdatedCache} or not_found.
%% The updated cache has the entry moved to front (LRU).
-spec get(cache(), binary(), map()) -> {ok, term(), cache()} | not_found.
get(#{entries := Entries} = Cache, Uri, Args) ->
Key = make_key(Uri, Args),
case find_entry(Entries, Key) of
{found, Entry} ->
%% Check if expired
case is_entry_expired(Entry) of
true ->
%% Entry expired, remove it
_UpdatedCache = invalidate(Cache, Uri, Args),
not_found;
false ->
%% Move to front (most recently used)
Result = maps:get(result, Entry),
TTL = maps:get(ttl, Entry),
Timestamp = erlang:system_time(millisecond),
UpdatedCache = put_with_timestamp(Cache, Uri, Args, Result, TTL, Timestamp),
{ok, Result, UpdatedCache}
end;
not_found ->
not_found
end.
%% @doc Invalidate (remove) entry.
-spec invalidate(cache(), binary(), map()) -> cache().
invalidate(#{entries := Entries} = Cache, Uri, Args) ->
Key = make_key(Uri, Args),
NewEntries = lists:filter(
fun(E) -> maps:get(key, E) =/= Key end,
Entries
),
Cache#{entries => NewEntries}.
%% @doc Clear all entries.
-spec clear(cache()) -> cache().
clear(Cache) ->
Cache#{entries => []}.
%% @doc Check if entry is expired.
-spec is_expired(cache(), binary(), map()) -> boolean().
is_expired(#{entries := Entries}, Uri, Args) ->
Key = make_key(Uri, Args),
case find_entry(Entries, Key) of
{found, Entry} ->
is_entry_expired(Entry);
not_found ->
true % Missing entry is considered expired
end.
%% @doc Get number of entries.
-spec size(cache()) -> non_neg_integer().
size(#{entries := Entries}) ->
length(Entries).
%% @doc Get max size.
-spec max_size(cache()) -> pos_integer().
max_size(#{max_size := MaxSize}) ->
MaxSize.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @doc Find entry by key.
-spec find_entry([cache_entry()], binary()) -> {found, cache_entry()} | not_found.
find_entry([], _Key) ->
not_found;
find_entry([Entry | Rest], Key) ->
case maps:get(key, Entry) of
Key -> {found, Entry};
_ -> find_entry(Rest, Key)
end.
%% @doc Check if entry is expired based on its TTL.
-spec is_entry_expired(cache_entry()) -> boolean().
is_entry_expired(Entry) ->
Timestamp = maps:get(timestamp, Entry),
TTL = maps:get(ttl, Entry),
Now = erlang:system_time(millisecond),
Age = Now - Timestamp,
Age > (TTL * 1000). % TTL in seconds, Age in milliseconds