-module(flow_store). -export([start_link/0, start_link/1, stop/0, register_flow/2, resolve_flow/1, registered_flows/0, start/2, resume/2, status/1, instances/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). -behaviour(gen_server). %% flow-on-erlang durable store — the named-flow registry plus the %% instance table that makes suspend/resume durable. flow.erl is the %% pure replay driver; this gen_server is the stateful shell around it, %% holding the registry (so triggers can reference flows by name, and %% so an instance can be re-resolved + replayed after a restart) and %% each instance's accumulated replay log. %% %% Crucially the driver stays OUT of any blocking context: start/resume %% call flow:drive/3 (pure — no receive, no gen_server:call) from inside %% handle_call, and the only message-passing is the caller's %% gen_server:call into this store. (A blocking receive inside a `try` %% deadlocks this cooperative scheduler, so the engine never does one.) %% %% State: {Registry, Instances, NextId} %% Registry = [{Name, FlowFun}, ...] %% Instances = [{Id, {Name, Input, Log, Status}}, ...] %% Status = {suspended, Tag} | {done, Result} %% Log = [{Tag, ResolvedValue}, ...] (the replay log — plain %% data, so an instance is fully described by its log and %% survives process restart by re-driving the named flow) %% %% v1 backs the store in gen_server memory; persisting the instance %% logs to the kernel's durable log (so flows survive an OS restart) is %% a later layer — the data shape is already restart-ready. start_link() -> start_link([]). start_link(InitialFlows) -> Pid = gen_server:start_link(flow_store, [InitialFlows]), erlang:register(flow_store, Pid), Pid. stop() -> R = gen_server:call(flow_store, '$gen_stop'), erlang:unregister(flow_store), R. %% register_flow(Name, Flow) — register a named flow (a node fun). Named %% rather than `register` to avoid the erlang:register/2 auto-import. register_flow(Name, Flow) -> gen_server:call(flow_store, {register_flow, Name, Flow}). resolve_flow(Name) -> gen_server:call(flow_store, {resolve_flow, Name}). registered_flows() -> gen_server:call(flow_store, registered_flows). %% start(Name, Input) -> {ok, Id, Result} | {error, no_such_flow}. %% Result is {flow_done, V} | {flow_suspended, Tag}; the instance is %% recorded either way so a suspended flow can be resumed by Id. start(Name, Input) -> gen_server:call(flow_store, {start, Name, Input}). %% resume(Id, Value) -> {ok, Result} | {error, Reason}. Resolves the %% instance's current suspend tag with Value (appends {Tag, Value} to %% its replay log) and re-drives from the top. resume(Id, Value) -> gen_server:call(flow_store, {resume, Id, Value}). %% status(Id) -> {ok, {suspended, Tag}} | {ok, {done, Result}} | not_found status(Id) -> gen_server:call(flow_store, {status, Id}). instances() -> gen_server:call(flow_store, instances). %% ── gen_server ────────────────────────────────────────────────── init([InitialFlows]) -> {ok, {InitialFlows, [], 1}}. handle_call({register_flow, Name, Flow}, _From, {Reg, Ins, N}) -> {reply, ok, {set_keyed(Name, Flow, Reg), Ins, N}}; handle_call({resolve_flow, Name}, _From, {Reg, Ins, N}) -> {reply, find_keyed(Name, Reg), {Reg, Ins, N}}; handle_call(registered_flows, _From, {Reg, Ins, N}) -> {reply, [Name || {Name, _} <- Reg], {Reg, Ins, N}}; handle_call({start, Name, Input}, _From, {Reg, Ins, N}) -> case find_keyed(Name, Reg) of not_found -> {reply, {error, no_such_flow}, {Reg, Ins, N}}; {ok, Flow} -> R = flow:drive(Flow, Input, []), Status = result_status(R), Ins2 = set_keyed(N, {Name, Input, [], Status}, Ins), {reply, {ok, N, R}, {Reg, Ins2, N + 1}} end; handle_call({resume, Id, Value}, _From, {Reg, Ins, N}) -> case find_keyed(Id, Ins) of not_found -> {reply, {error, no_such_instance}, {Reg, Ins, N}}; {ok, {_Name, _Input, _Log, {done, _}}} -> {reply, {error, already_done}, {Reg, Ins, N}}; {ok, {Name, Input, Log, {suspended, Tag}}} -> case find_keyed(Name, Reg) of not_found -> {reply, {error, no_such_flow}, {Reg, Ins, N}}; {ok, Flow} -> NewLog = log_append(Log, Tag, Value), R = flow:drive(Flow, Input, NewLog), Status = result_status(R), Ins2 = set_keyed(Id, {Name, Input, NewLog, Status}, Ins), {reply, {ok, R}, {Reg, Ins2, N}} end end; handle_call({status, Id}, _From, {Reg, Ins, N}) -> case find_keyed(Id, Ins) of {ok, {_Name, _Input, _Log, Status}} -> {reply, {ok, Status}, {Reg, Ins, N}}; not_found -> {reply, not_found, {Reg, Ins, N}} end; handle_call(instances, _From, {Reg, Ins, N}) -> {reply, [Id || {Id, _} <- Ins], {Reg, Ins, N}}. handle_cast(_, S) -> {noreply, S}. handle_info(_, S) -> {noreply, S}. %% ── helpers ───────────────────────────────────────────────────── result_status({flow_done, R}) -> {done, R}; result_status({flow_suspended, T}) -> {suspended, T}. log_append([], Tag, Value) -> [{Tag, Value}]; log_append([H | T], Tag, Value) -> [H | log_append(T, Tag, Value)]. find_keyed(_, []) -> not_found; find_keyed(K, [{K, V} | _]) -> {ok, V}; find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest). set_keyed(K, V, []) -> [{K, V}]; set_keyed(K, V, [{K, _} | Rest]) -> [{K, V} | Rest]; set_keyed(K, V, [P | Rest]) -> [P | set_keyed(K, V, Rest)].