-module(projection). -behaviour(gen_server). -export([new/2, new/3, fold_activity/2, replay/2, name/1, state/1, fold_fn/1]). -export([start_link/3, async_fold/2, query/1, stop/1]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2]). %% Pure-functional projection driver per design §10. %% %% A projection is a property list: %% [{name, atom}, {state, term}, {fold, fun}] %% %% The fold function is `fun (Activity, State) -> NewState`. v1 %% uses Erlang funs as the fold body — the genesis bundle's SX %% `:fold` bodies are stored as binaries; an SX-source eval %% bridge will plug them into the same projection record once %% it lands (Step 7d). For now, callers supply Erlang funs %% directly when constructing a projection. %% %% `replay/2` is the cold-start primitive: fold an activity %% list (e.g. `log:entries/1`) through the projection from its %% initial state. new(Name, InitialState) -> new(Name, InitialState, fun (_Activity, S) -> S end). new(Name, InitialState, FoldFn) -> [{name, Name}, {state, InitialState}, {fold, FoldFn}]. fold_activity(Proj, Activity) -> Fn = fold_fn(Proj), S0 = state(Proj), S1 = Fn(Activity, S0), set_field(state, S1, Proj). replay(Proj, Activities) -> fold_each(Proj, Activities). fold_each(Proj, []) -> Proj; fold_each(Proj, [A | Rest]) -> fold_each(fold_activity(Proj, A), Rest). %% Accessors name(Proj) -> field(name, Proj). state(Proj) -> field(state, Proj). fold_fn(Proj) -> field(fold, Proj). %% Internal field(K, [{K, V} | _]) -> V; field(K, [_ | Rest]) -> field(K, Rest); field(_, []) -> erlang:error(badkey). set_field(K, V, [{K, _} | Rest]) -> [{K, V} | Rest]; set_field(K, V, [P | Rest]) -> [P | set_field(K, V, Rest)]; set_field(K, V, []) -> [{K, V}]. %% ── Step 7b: gen_server wrapper ───────────────────────────────── %% %% Each projection runs in its own gen_server, registered under the %% projection's Name atom. `async_fold/2` casts an activity into the %% process; `query/1` synchronously fetches the current state. %% %% Port notes (mirroring Step 5b on the registry): `gen_server:start_link` %% returns the raw Pid; `?MODULE` macro is unsupported; spawned %% processes don't survive across separate `erlang-eval-ast` calls %% so tests must inline start_link with their operations. start_link(Name, InitialState, FoldFn) -> Pid = gen_server:start_link(projection, [Name, InitialState, FoldFn]), erlang:register(Name, Pid), Pid. async_fold(Name, Activity) -> gen_server:cast(Name, {fold, Activity}). query(Name) -> gen_server:call(Name, get_state). stop(Name) -> R = gen_server:call(Name, '$gen_stop'), erlang:unregister(Name), R. %% gen_server callbacks init([Name, InitialState, FoldFn]) -> {ok, new(Name, InitialState, FoldFn)}. handle_call(get_state, _From, Proj) -> {reply, state(Proj), Proj}. handle_cast({fold, Activity}, Proj) -> {noreply, fold_activity(Proj, Activity)}. handle_info(_, Proj) -> {noreply, Proj}.