-module(reki_server). -behaviour(gen_server). -export([start_link/2, start_child/4, whereis_server/1]). -export([new_unique_atom/0, lookup/2]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). %% Start the registry gen_server, registered under ServerName. -spec start_link(atom(), atom()) -> {ok, pid()} | {error, term()}. start_link(ServerName, TableName) -> case gen_server:start_link({local, ServerName}, ?MODULE, TableName, []) of {ok, Pid} -> {ok, Pid}; {error, {already_started, _}} -> {error, {init_failed, <<"already started">>}}; {error, Reason} -> {error, {init_exited, {abnormal, Reason}}} end. %% Call the registry to look up or start a child. Wraps gen_server:call %% with exception handling so Gleam gets a clean Result back. -spec start_child(atom(), term(), fun((term()) -> term()), integer()) -> term(). start_child(ServerName, Key, StartFn, Timeout) -> try gen_server:call(ServerName, {start_child, Key, StartFn}, Timeout) catch exit:{timeout, {gen_server, call, _Args}} -> {error, init_timeout}; exit:{normal, {gen_server, call, _Args}} -> {error, {init_exited, normal}}; exit:{killed, {gen_server, call, _Args}} -> {error, {init_exited, killed}}; exit:{noproc, {gen_server, call, _Args}} -> {error, {init_exited, {abnormal, noproc}}}; exit:{Reason, {gen_server, call, _Args}} -> {error, {init_exited, {abnormal, Reason}}} end. %% Look up the pid of the gen_server by registered name. -spec whereis_server(atom()) -> {ok, pid()} | {error, nil}. whereis_server(ServerName) -> case erlang:whereis(ServerName) of undefined -> {error, nil}; Pid -> {ok, Pid} end. %% Creates a unique atom for use as an ETS table name or server name. %% WARNING: Atoms are never garbage collected by the BEAM. This function %% must only be called a bounded number of times (e.g. at app startup). -spec new_unique_atom() -> atom(). new_unique_atom() -> Suffix = integer_to_binary(erlang:unique_integer([positive])), Name = <<"reki$", Suffix/bits>>, binary_to_atom(Name). %% Direct ETS lookup, callable from any process for the fast read path. -spec lookup(atom(), term()) -> {some, term()} | none. lookup(Table, Key) -> try case ets:lookup(Table, Key) of [{Key, Value}] -> {some, Value}; [] -> none end catch error:badarg -> none end. %% Internal helpers child_pids() -> [Pid || Pid <- erlang:get_keys(), is_pid(Pid)]. start_and_register(Table, Key, StartFn) -> case safe_start(Key, StartFn) of {ok, {started, Pid, Subject}} -> link(Pid), ets:insert(Table, {Key, Subject}), erlang:put(Pid, Key), {ok, Subject}; {error, Reason} -> {error, Reason} end. safe_start(Key, StartFn) -> try StartFn(Key) catch Class:Reason -> {error, {init_exited, {abnormal, {Class, Reason}}}} end. %% gen_server callbacks init(TableName) -> process_flag(trap_exit, true), ets:new(TableName, [set, named_table, protected, {read_concurrency, true}]), {ok, #{table => TableName}}. handle_call({start_child, Key, StartFn}, _From, #{table := Table} = State) -> Reply = case lookup(Table, Key) of {some, Subject} -> {ok, Subject}; none -> start_and_register(Table, Key, StartFn) end, {reply, Reply, State}; handle_call(which_children, _From, State) -> Children = [{undefined, Pid, worker, dynamic} || Pid <- child_pids()], {reply, Children, State}; handle_call(count_children, _From, State) -> Count = length(child_pids()), Reply = [{specs, 1}, {active, Count}, {supervisors, 0}, {workers, Count}], {reply, Reply, State}; handle_call(_Msg, _From, State) -> {reply, {error, unknown_call}, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info({'EXIT', Pid, _Reason}, #{table := Table} = State) -> case erlang:erase(Pid) of undefined -> ok; Key -> ets:delete(Table, Key) end, {noreply, State}; handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> lists:foreach(fun(Pid) -> unlink(Pid), exit(Pid, kill) end, child_pids()), ok.