Packages

Event-Hub is a Gleam library that provides simple hubs with publishers and subscribers for event-driven observers. It supports asynchronous message handling and event notifications, decoupling components efficiently. It works on Erlang and JavaScript.

Current section

Files

Jump to
event_hub src event_hub_ffi.erl
Raw

src/event_hub_ffi.erl

-module(event_hub_ffi).
-export([
start_stateless/0,
add_stateless/2,
invoke_stateless/2,
remove_stateless/2,
stop_stateless/1,
start_stateful/1,
add_stateful/2,
current_state/1,
invoke_stateful/2,
remove_stateful/2,
stop_stateful/1,
start_topic_based/0,
add_topic_based/3,
invoke_topic_based/3,
remove_topic_based/2,
stop_topic_based/1
]).
%% Helper functions
%% ================
%% Spawns processes to invoke each callback with the provided value,
%% and waits for all of them to complete.
spawn_invoke(Callbacks, Value) ->
Monitors = lists:map(
fun({_Index, Callback}) ->
{_, Ref} = spawn_monitor(fun() -> Callback(Value) end),
Ref
end,
maps:to_list(Callbacks)
),
wait_for_monitors(Monitors).
%% Waits for all monitored processes to complete.
wait_for_monitors([]) ->
ok;
wait_for_monitors([Ref | Rest]) ->
receive
{'DOWN', Ref, process, _, _} ->
wait_for_monitors(Rest)
end.
%% Updates the topic index with a new subscription.
update_topic_index(TopicIndex, TopicsList, Index) ->
case TopicsList of
[] ->
TopicIndex;
[Topics | Rest] ->
lists:foldl(
fun(Topic, Acc) ->
{SubscriberIndices, Next} = maps:get(Topic, Acc, {[], #{}}),
NewSubscriberIndices =
case Rest of
[] ->
[Index | SubscriberIndices];
_ ->
SubscriberIndices
end,
Acc#{Topic => {NewSubscriberIndices, update_topic_index(Next, Rest, Index)}}
end,
TopicIndex,
Topics
)
end.
%% Removes a subscription from the topic index.
remove_from_topic_index(TopicIndex, TopicsList, Index) ->
case TopicsList of
[] ->
TopicIndex;
[Topics | Rest] ->
lists:foldl(
fun(Topic, Acc) ->
{SubscriberIndices, Next} = maps:get(Topic, Acc, {[], #{}}),
NewSubscriberIndices =
case Rest of
[] ->
lists:delete(Index, SubscriberIndices);
_ ->
SubscriberIndices
end,
case {NewSubscriberIndices =:= [], maps:size(Next) == 0} of
{true, true} ->
maps:remove(Topic, Acc);
_ ->
Acc#{
Topic =>
{NewSubscriberIndices,
remove_from_topic_index(Next, Rest, Index)}
}
end
end,
TopicIndex,
Topics
)
end.
%% Finds callbacks whose topics intersect with the provided topics.
find_matching_callbacks(TopicIndex, TopicsList, Callbacks) ->
case TopicsList of
[] ->
#{};
[Topics] ->
SubscriberIndices = lists:flatmap(
fun(Topic) ->
{Indices, _} = maps:get(Topic, TopicIndex, {[], #{}}),
Indices
end,
Topics
),
lists:foldl(
fun(Index, Acc) ->
{_, FoundCallback} = maps:get(Index, Callbacks),
Acc#{Index => FoundCallback}
end,
#{},
SubscriberIndices
);
[Topics | Rest] ->
lists:foldl(
fun(Topic, Acc) ->
{_, Next} = maps:get(Topic, TopicIndex, {[], #{}}),
maps:merge(Acc, find_matching_callbacks(Next, Rest, Callbacks))
end,
#{},
Topics
)
end.
%% Stateless observer
%% ==================
%% Starts the stateless observer process.
start_stateless() ->
spawn(fun() -> stateless_loop(#{}, 0) end).
%% The main loop for the stateless event_hub.
stateless_loop(Callbacks, Index) ->
receive
{add, Callback, From} ->
NewIndex = Index + 1,
NewCallbacks = Callbacks#{NewIndex => Callback},
From ! {ok, NewIndex},
stateless_loop(NewCallbacks, NewIndex);
{invoke, Value, From} ->
spawn_invoke(Callbacks, Value),
From ! {ok},
stateless_loop(Callbacks, Index);
{remove, Id} ->
NewCallbacks = maps:remove(Id, Callbacks),
stateless_loop(NewCallbacks, Index);
stop ->
ok
end.
%% Adds a callback to the stateless observer, returning the index.
add_stateless(Process, Callback) ->
Process ! {add, Callback, self()},
receive
{ok, Index} -> Index
end.
%% Invokes all callbacks in parallel with the given value and waits for all of them to complete.
invoke_stateless(Process, Value) ->
Process ! {invoke, Value, self()},
receive
{ok} -> ok
end.
%% Removes a callback by its index.
remove_stateless(Process, Index) ->
Process ! {remove, Index}.
%% Stops the stateless observer process.
stop_stateless(Process) ->
Process ! stop.
%% Stateful observer
%% =================
%% Starts the stateful observer process with an initial state.
start_stateful(State) ->
spawn(fun() -> stateful_loop(State, #{}, 0) end).
%% The main loop for the stateful event_hub.
stateful_loop(State, Callbacks, Index) ->
receive
{add, Callback, From} ->
NewIndex = Index + 1,
NewCallbacks = Callbacks#{NewIndex => Callback},
From ! {ok, State, NewIndex},
stateful_loop(State, NewCallbacks, NewIndex);
{current, From} ->
From ! {ok, State},
stateful_loop(State, Callbacks, Index);
{invoke, Value, From} ->
spawn_invoke(Callbacks, Value),
From ! {ok},
stateful_loop(Value, Callbacks, Index);
{remove, Id} ->
NewCallbacks = maps:remove(Id, Callbacks),
stateful_loop(State, NewCallbacks, Index);
stop ->
ok
end.
%% Adds a callback to the stateful observer, returning the current state and index.
add_stateful(Process, Callback) ->
Process ! {add, Callback, self()},
receive
{ok, State, Index} -> {State, Index}
end.
%% Retrieves the current state.
current_state(Process) ->
Process ! {current, self()},
receive
{ok, State} -> State
end.
%% Invokes all callbacks in parallel with a new state, updating the state and waits for all callbacks to complete.
invoke_stateful(Process, Value) ->
Process ! {invoke, Value, self()},
receive
{ok} -> ok
end.
%% Removes a callback by its index.
remove_stateful(Process, Index) ->
Process ! {remove, Index}.
%% Stops the stateful observer process.
stop_stateful(Process) ->
Process ! stop.
%% Topic-based observer
%% ====================
%% Starts the topic-based observer process.
start_topic_based() ->
spawn(fun() -> topic_based_loop(#{}, #{}, 0) end).
%% The main loop for the topic-based event_hub.
topic_based_loop(Callbacks, TopicIndex, Index) ->
receive
{add, Topics, Callback, From} ->
NewIndex = Index + 1,
NewCallbacks = Callbacks#{NewIndex => {Topics, Callback}},
NewTopicIndex = update_topic_index(TopicIndex, Topics, NewIndex),
From ! {ok, NewIndex},
topic_based_loop(NewCallbacks, NewTopicIndex, NewIndex);
{invoke, Topics, Value, From} ->
MatchingCallbacks = find_matching_callbacks(TopicIndex, Topics, Callbacks),
spawn_invoke(MatchingCallbacks, Value),
From ! {ok},
topic_based_loop(Callbacks, TopicIndex, Index);
{remove, Id} ->
case maps:get(Id, Callbacks, undefined) of
{Topics, _} ->
NewTopicIndex = remove_from_topic_index(TopicIndex, Topics, Id),
NewCallbacks = maps:remove(Id, Callbacks),
topic_based_loop(NewCallbacks, NewTopicIndex, Index);
undefined ->
topic_based_loop(Callbacks, TopicIndex, Index)
end;
stop ->
ok
end.
%% Adds a callback with topics to the topic-based observer, returning the index.
add_topic_based(Process, Topics, Callback) ->
Process ! {add, Topics, Callback, self()},
receive
{ok, Index} -> Index
end.
%% Invokes all matching callbacks in parallel with the given topics and value, and waits for all of them to complete.
invoke_topic_based(Process, Topics, Value) ->
Process ! {invoke, Topics, Value, self()},
receive
{ok} -> ok
end.
%% Removes a callback by its index.
remove_topic_based(Process, Index) ->
Process ! {remove, Index}.
%% Stops the topic-based observer process.
stop_topic_based(Process) ->
Process ! stop.