Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
98 lines
3.0 KiB
Erlang
98 lines
3.0 KiB
Erlang
-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}.
|