Packages
reckon_db
2.0.0
5.11.0
5.10.4
5.10.3
5.10.1
5.10.0
5.9.1
5.9.0
5.8.3
5.8.2
5.8.1
5.8.0
5.7.0
5.6.1
5.6.0
5.5.5
5.5.4
5.5.3
5.5.2
5.5.1
5.5.0
5.4.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.0.0
3.1.2
3.1.1
3.0.0
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.2
2.2.0
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
BEAM-native Event Store built on Khepri/Ra with Raft consensus. Event sourcing, persistent subscriptions, snapshots, and automatic cluster formation via UDP multicast discovery. Ships embedded Rust NIFs for 3-15x acceleration of crypto, hashing, compression, aggregation, filter matching, and grap...
Current section
Files
Jump to
Current section
Files
src/reckon_db_subscriptions_store.erl
%% @doc Subscriptions store for reckon-db
%%
%% Manages subscription persistence and retrieval directly via Khepri.
%% This is a facade module that provides direct access to the subscription
%% storage without going through a gen_server, since Khepri/Ra handles
%% concurrency internally.
%%
%% @author rgfaber
-module(reckon_db_subscriptions_store).
-include("reckon_db.hrl").
-include_lib("khepri/include/khepri.hrl").
%% API
-export([
put/2,
get/2,
delete/2,
exists/2,
exists/3,
list/1,
key/1,
key/3,
update_checkpoint/3,
find_by_name/2
]).
%%====================================================================
%% Types
%%====================================================================
-type store_id() :: atom().
%%====================================================================
%% API
%%====================================================================
%% @doc Generate a unique key for a subscription
%%
%% The key is a phash2 hash of {type, selector, subscription_name}
-spec key(subscription()) -> binary().
key(#subscription{type = Type, selector = Selector, subscription_name = Name}) ->
key(Type, Selector, Name).
%% @doc Generate a unique key for a subscription from components
-spec key(subscription_type(), binary() | map(), binary()) -> binary().
key(Type, Selector, SubscriptionName) ->
integer_to_binary(erlang:phash2({Type, Selector, SubscriptionName})).
%% @doc Store a subscription
-spec put(store_id(), subscription()) -> ok | {error, term()}.
put(StoreId, #subscription{} = Subscription) ->
Key = key(Subscription),
Path = ?SUBSCRIPTIONS_PATH ++ [Key],
%% Add the id to the subscription if not set
SubscriptionWithId = case Subscription#subscription.id of
undefined -> Subscription#subscription{id = Key};
_ -> Subscription
end,
case khepri:put(StoreId, Path, SubscriptionWithId) of
ok -> ok;
{error, _} = Error -> Error
end.
%% @doc Get a subscription by key
-spec get(store_id(), binary()) -> subscription() | undefined.
get(StoreId, Key) when is_binary(Key) ->
Path = ?SUBSCRIPTIONS_PATH ++ [Key],
case khepri:get(StoreId, Path) of
{ok, Subscription} when is_record(Subscription, subscription) ->
Subscription;
{ok, SubscriptionMap} when is_map(SubscriptionMap) ->
map_to_subscription(SubscriptionMap);
_ ->
undefined
end.
%% @doc Delete a subscription by key
-spec delete(store_id(), binary()) -> ok | {error, term()}.
delete(StoreId, Key) when is_binary(Key) ->
Path = ?SUBSCRIPTIONS_PATH ++ [Key],
case khepri:delete(StoreId, Path) of
ok -> ok;
{error, _} = Error -> Error
end.
%% @doc Check if a subscription exists by key
-spec exists(store_id(), binary()) -> boolean().
exists(StoreId, Key) when is_binary(Key) ->
Path = ?SUBSCRIPTIONS_PATH ++ [Key],
case khepri:exists(StoreId, Path) of
true -> true;
false -> false;
{error, _} -> false
end.
%% @doc Check if a subscription exists by record
-spec exists(store_id(), subscription_type(), binary()) -> boolean().
exists(StoreId, Type, SubscriptionName) ->
%% We need to search through subscriptions since we don't have
%% the selector to generate the key
case list(StoreId) of
{ok, Subs} ->
lists:any(
fun(#subscription{type = T, subscription_name = N}) ->
T =:= Type andalso N =:= SubscriptionName
end,
Subs
);
_ ->
false
end.
%% @doc List all subscriptions in the store
-spec list(store_id()) -> {ok, [subscription()]} | {error, term()}.
list(StoreId) ->
Path = ?SUBSCRIPTIONS_PATH ++ [?KHEPRI_WILDCARD_STAR],
case khepri:get_many(StoreId, Path) of
{ok, Results} when is_map(Results) ->
Subs = [convert_to_subscription(V) || {_, V} <- maps:to_list(Results)],
{ok, [S || S <- Subs, S =/= undefined]};
{ok, _} ->
{ok, []};
{error, _} = Error ->
Error
end.
%% @doc Find a subscription by name
%%
%% Searches all subscriptions for one matching the given name.
%% Returns the subscription key and record if found.
-spec find_by_name(store_id(), binary()) -> {ok, binary(), subscription()} | {error, not_found}.
find_by_name(StoreId, SubscriptionName) ->
case list(StoreId) of
{ok, Subscriptions} ->
find_matching_subscription(Subscriptions, SubscriptionName);
{error, _} ->
{error, not_found}
end.
%% @doc Update the checkpoint for a subscription
%%
%% Parameters:
%% StoreId - The store identifier
%% Key - The subscription key
%% Position - The new checkpoint position
-spec update_checkpoint(store_id(), binary(), non_neg_integer()) -> ok | {error, term()}.
update_checkpoint(StoreId, Key, Position) ->
case ?MODULE:get(StoreId, Key) of
undefined ->
{error, not_found};
Subscription ->
UpdatedSubscription = Subscription#subscription{checkpoint = Position},
?MODULE:put(StoreId, UpdatedSubscription)
end.
%%====================================================================
%% Internal functions
%%====================================================================
%% @private Find a subscription by name in a list
-spec find_matching_subscription([subscription()], binary()) ->
{ok, binary(), subscription()} | {error, not_found}.
find_matching_subscription([], _Name) ->
{error, not_found};
find_matching_subscription([#subscription{subscription_name = Name} = Sub | _], Name) ->
{ok, key(Sub), Sub};
find_matching_subscription([_ | Rest], Name) ->
find_matching_subscription(Rest, Name).
%% @private Convert value to subscription record
-spec convert_to_subscription(term()) -> subscription() | undefined.
convert_to_subscription(V) when is_record(V, subscription) ->
V;
convert_to_subscription(V) when is_map(V) ->
map_to_subscription(V);
convert_to_subscription(_) ->
undefined.
%% @private Convert map to subscription record
-spec map_to_subscription(map()) -> subscription().
map_to_subscription(Map) ->
#subscription{
id = maps:get(id, Map, undefined),
type = maps:get(type, Map, stream),
selector = maps:get(selector, Map, <<>>),
subscription_name = maps:get(subscription_name, Map, <<>>),
subscriber_pid = maps:get(subscriber_pid, Map, undefined),
created_at = maps:get(created_at, Map, 0),
pool_size = maps:get(pool_size, Map, 1),
checkpoint = maps:get(checkpoint, Map, undefined),
options = maps:get(options, Map, #{})
}.