% Copyright 2025 BradBot_1 % Licensed under the Apache License, Version 2.0 (the "License"); % you may not use this file except in compliance with the License. % You may obtain a copy of the License at % http://www.apache.org/licenses/LICENSE-2.0 % Unless required by applicable law or agreed to in writing, software % distributed under the License is distributed on an "AS IS" BASIS, % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. % See the License for the specific language governing permissions and % limitations under the License. -module(futureFfi). -behaviour(gen_server). -export([is_complete/1]). -export([status/1]). -export([release/1]). -export([create_failure_handler/2]). -export([wait/1]). -export([auto_release/1]). -export([once_complete/2]). -export([start/1]). -export([start/2]). -export([init/1]). -export([handle_call/3]). -export([handle_cast/2]). -export([handle_info/2]). -export([terminate/2]). -export([code_change/3]). %% If the future has completed, IE a result is available or the task failed is_complete(Instance) when is_pid(Instance) -> case catch gen_server:call(Instance, is_complete) of {'EXIT', _} -> true; Result -> Result end. %% Returns the current status of the future %% %% Possible statuses are: %% - {completed, {ok | error, Value}} %% - running %% - {failed, binstring()} %% - released status(Instance) when is_pid(Instance) -> case catch gen_server:call(Instance, status) of {'EXIT', _} -> released; Status -> Status end. %% Kills the process (only the gen_server process, not the child process) release(Instance) when is_pid(Instance) -> try gen_server:cast(Instance, release), nil catch _:_ -> nil end. %% Creates a process that will execute the given function when the future fails create_failure_handler(Instance, Handler) when is_pid(Instance), is_function(Handler, 1) -> try gen_server:cast(Instance, {create_failure_handler, Handler}), Instance catch _:_ -> Instance end. %% Blocks execution until the future is complete wait(Instance) when is_pid(Instance) -> case catch gen_server:call(Instance, get_child) of {'EXIT', _} -> ok; nil -> ok; ChildPid when is_pid(ChildPid) -> MonitorRef = monitor(process, ChildPid), receive {'DOWN', MonitorRef, process, ChildPid, _Reason} -> ok end; _ -> error end. auto_release(Instance) when is_pid(Instance) -> spawn(fun() -> wait(Instance), release(Instance) end), Instance. once_complete(Instance, Executable) when is_pid(Instance), is_function(Executable, 1) -> spawn(fun() -> wait(Instance), Executable(status(Instance)) end), Instance. start({ok, Value}) -> {ok, Pid} = gen_server:start(?MODULE, {complete, {ok, Value}}, []), Pid; start({error, Reason}) -> {ok, Pid} = gen_server:start(?MODULE, {complete, {error, Reason}}, []), Pid; start({failed, Reason}) -> {ok, Pid} = gen_server:start(?MODULE, {complete, {failed, Reason}}, []), Pid. start(Executable, StartingValue) when is_function(Executable, 1) -> {ok, Pid} = gen_server:start(?MODULE, {run, Executable, StartingValue}, []), Pid; start(Instance, Executable) when is_pid(Instance), is_function(Executable, 1) -> case catch gen_server:call(Instance, {start, Executable}) of {'EXIT', _} -> {ok, Pid} = gen_server:start(?MODULE, {complete, {failed, <<"Prior future was released before initialisation"/utf8>>}}, []), Pid; Result -> Result end. init({complete, Result}) when is_tuple(Result) -> case Result of {ok, _} -> {ok, Result}; {error, _} -> {ok, Result}; {failed, _} -> {ok, Result}; _ -> {error, <<"Invalid result (not ok or error)"/utf8>>} end; init({run, Executable, StartingValue}) when is_function(Executable, 1) -> process_flag(trap_exit, true), Parent = self(), {ChildPid, MonitorRef} = spawn_monitor(fun() -> Result = try Executable(StartingValue) catch _:ErrorReason -> {caught, ErrorReason} end, exit({shutdown, case catch gen_server:call(Parent, {future_completed, Result}) of {'EXIT', _} -> % Parent process was released, will pass on value to chained futures Result; ok -> Result; {error, Reason} -> {failed, Reason}; {failed, Reason} -> {failed, Reason}; _ -> {failed, <<"Failed to send result to parent process"/utf8>>} end}) end), {ok, {waiting, ChildPid, MonitorRef}}. handle_call({future_completed, Result}, _From, {waiting, ChildPid, MonitorRef}) when is_pid(ChildPid), is_reference(MonitorRef) -> demonitor(MonitorRef, [flush]), case Result of {ok, _} -> {reply, ok, Result}; {error, _} -> {reply, ok, Result}; {failed, _} -> {reply, ok, Result}; {caught, Reason} when is_binary(Reason) -> Message = {failed, Reason}, {reply, Message, Message}; {caught, #{gleam_error := panic, message := Reason}} when is_binary(Reason) -> Message = {failed, Reason}, {reply, Message, Message}; {caught, Reason} -> Converted = list_to_binary(io_lib:format("~p", [Reason])), Message = {failed, <<"Subprocess caught an exception: "/utf8, Converted/binary>>}, {reply, Message, Message}; _ -> Message = {failed, <<"Subprocess returned an invalid result (not ok or error tuple)"/utf8>>}, {reply, Message, Message} end; handle_call({future_completed, _Result}, _From, State) -> {reply, {failed, <<"Subprocess contact was unexpected, result will be lost"/utf8>>}, State}; handle_call(is_complete, _From, State) -> {reply, case State of {ok, _} -> true; {error, _} -> true; {failed, _} -> true; _ -> false end, State}; handle_call(status, _From, State) -> {reply, case State of {ok, _} -> {completed, State}; {error, _} -> {completed, State}; {failed, _} -> State; {waiting, _, _} -> running; _ -> {failed, <<"Unkown state"/utf8>>} end, State}; handle_call({start, Executable}, _From, State) when is_function(Executable, 1) -> {reply, case State of {failed, Reason} -> {ok, Pid} = gen_server:start(?MODULE, {complete, {failed, <<"Prior future failed: "/utf8, Reason/binary>>}}, []), Pid; {ok, _} -> {ok, Pid} = gen_server:start(?MODULE, {run, Executable, State}, []), Pid; {error, _} -> {ok, Pid} = gen_server:start(?MODULE, {run, Executable, State}, []), Pid; {waiting, ChildPid, _} when is_pid(ChildPid) -> {ok, Pid} = gen_server:start(?MODULE, {run, fun({ProvidedChildPid, ProvidedExecutable}) -> MonitorRef = monitor(process, ProvidedChildPid), receive {'DOWN', MonitorRef, process, ProvidedChildPid, Reason} -> case Reason of {shutdown, Result} -> case Result of {ok, _} -> ProvidedExecutable(Result); {error, _} -> ProvidedExecutable(Result); {failed, FailureReason} when is_binary(FailureReason) -> {failed, <<"Prior future failed: "/utf8, FailureReason/binary>>}; _ -> {failed, <<"Prior future failed: Invalid result from subprocess"/utf8>>} end; _ -> {noreply, PriorState} = handle_info({'DOWN', MonitorRef, process, ProvidedChildPid, Reason}, {waiting, ProvidedChildPid, MonitorRef}), case PriorState of {failed, Reason} -> {failed, <<"Prior future failed: "/utf8, Reason/binary>>}; _ -> {failed, <<"Prior future failed: Invalid handle_info response"/utf8>>} end end end end, {ChildPid, Executable}}, []), Pid; _ -> {ok, Pid} = gen_server:start(?MODULE, {complete, {failed, <<"Prior future failed: Invalid prior state"/utf8>>}}, []), Pid end, State}; handle_call(get_child, _From, State) -> {reply, case State of {waiting, ChildPid, _} when is_pid(ChildPid) -> ChildPid; _ -> nil end, State}; handle_call(_Request, _From, State) -> {stop, {error, unsupported_operation}, State}. handle_cast(release, State) -> {stop, {shutdown, State}, State}; handle_cast({create_failure_handler, Handler}, State) when is_function(Handler, 1) -> case State of {failed, Reason} -> spawn(fun() -> Handler(Reason) end); {waiting, ChildPid, _} when is_pid(ChildPid) -> spawn(fun() -> MonitorRef = monitor(process, ChildPid), receive {'DOWN', MonitorRef, process, ChildPid, Reason} -> case Reason of {shutdown, Result} -> case Result of {failed, FailedReason} -> Handler(FailedReason); _ -> ok end; _ -> {noreply, PriorState} = handle_info({'DOWN', MonitorRef, process, ChildPid, Reason}, {waiting, ChildPid, MonitorRef}), case PriorState of {failed, Reason} -> Handler(Reason); _ -> Handler(<<"Invalid handle_info response"/utf8>>) end end end end); _ -> ok end, {noreply, State}; handle_cast(_Request, State) -> {noreply, State}. handle_info(Info, {waiting, ChildPid, MonitorRef}) when is_pid(ChildPid), is_reference(MonitorRef) -> case Info of {'DOWN', MonitorRef, process, ChildPid, Reason} -> case Reason of {shutdown, _} -> % Ignore {noreply, {waiting, ChildPid, MonitorRef}}; normal -> {noreply, {failed, <<"Subprocess exited abnormally"/utf8>>}}; noproc -> {noreply, {failed, <<"Subprocess was never spawned (unreachable)"/utf8>>}}; FailureReason when is_binary(FailureReason) -> {noreply, {failed, FailureReason}}; _ -> {noreply, {failed, <<"Subprocess returned an invalid exit reason (not a binary)"/utf8>>}} end; _ -> % Ignore {noreply, {waiting, ChildPid, MonitorRef}} end; handle_info(_Info, Result) when is_tuple(Result) -> {noreply, Result}. terminate(_Reason, _State) -> ok. code_change(_OldVsn, State, _Extra) -> {ok, State}.