Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 30s
83 lines
3.0 KiB
Erlang
83 lines
3.0 KiB
Erlang
-module(nx_kernel).
|
|
-export([new/3, publish/2,
|
|
actor_id/1, log_state/1, log_tip/1,
|
|
key_spec/1, actor_state/1, projections/1,
|
|
next_published/1, with_projections/2]).
|
|
|
|
%% Kernel orchestrator — the long-lived runtime state held by the
|
|
%% running fed-sx instance. The HTTP layer (Step 8c-post-publish
|
|
%% follow-up) will park this in a gen_server and dispatch the POST
|
|
%% /activity request through `publish/2`.
|
|
%%
|
|
%% State shape (property list):
|
|
%% [{actor_id, A},
|
|
%% {key_spec, KS}, % proplist: key_id / algorithm / value
|
|
%% {actor_state, AS}, % proplist: public_keys
|
|
%% {log, L}, % log:open/2 return value
|
|
%% {projections, [Name]}, % list of registered projection process names
|
|
%% {next_published, N}] % monotonic counter we feed as :published
|
|
%%
|
|
%% Step 6c's stage_replay catches duplicates by `:id`; the `:id`
|
|
%% is derived from the unsigned envelope contents. Same Request +
|
|
%% same `:published` -> same CID, so the next_published counter
|
|
%% gives every publish a distinct timestamp without needing a
|
|
%% wall-clock BIF.
|
|
|
|
new(ActorId, KeySpec, ActorStateProplist) ->
|
|
{ok, L0} = log:open(ActorId, base_stub()),
|
|
[{actor_id, ActorId},
|
|
{key_spec, KeySpec},
|
|
{actor_state, ActorStateProplist},
|
|
{log, L0},
|
|
{projections, []},
|
|
{next_published, 1}].
|
|
|
|
%% publish/2 — pure state transition. Returns either:
|
|
%% {ok, Result, NewState} — log + counter advanced
|
|
%% {error, Reason, State} — state unchanged on validation halt
|
|
publish(Request, State) ->
|
|
P = field(next_published, State),
|
|
Ctx = [{actor_id, field(actor_id, State)},
|
|
{published, P},
|
|
{key_spec, field(key_spec, State)},
|
|
{actor_state, field(actor_state, State)},
|
|
{log, field(log, State)},
|
|
{projections, field(projections, State)}],
|
|
case outbox:publish(Request, Ctx) of
|
|
{ok, Result, NewLog} ->
|
|
State1 = set(log, NewLog, State),
|
|
State2 = set(next_published, P + 1, State1),
|
|
{ok, Result, State2};
|
|
{error, Reason, _} ->
|
|
{error, Reason, State}
|
|
end.
|
|
|
|
%% Accessors
|
|
|
|
actor_id(State) -> field(actor_id, State).
|
|
key_spec(State) -> field(key_spec, State).
|
|
actor_state(State) -> field(actor_state, State).
|
|
log_state(State) -> field(log, State).
|
|
log_tip(State) -> log:tip(field(log, State)).
|
|
projections(State) -> field(projections, State).
|
|
next_published(State) -> field(next_published, State).
|
|
|
|
%% with_projections — return a new state with :projections replaced.
|
|
with_projections(Names, State) ->
|
|
set(projections, Names, State).
|
|
|
|
%% Internal
|
|
|
|
%% "base_stub" — placeholder base path for the in-memory log
|
|
%% in v1 (the in-memory log ignores the base argument).
|
|
base_stub() ->
|
|
<<98,97,115,101,95,115,116,117,98>>.
|
|
|
|
field(K, [{K, V} | _]) -> V;
|
|
field(K, [_ | Rest]) -> field(K, Rest);
|
|
field(_, []) -> nil.
|
|
|
|
set(K, V, []) -> [{K, V}];
|
|
set(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
|
set(K, V, [P | Rest]) -> [P | set(K, V, Rest)].
|