Files
rose-ash/next/kernel/log.erl
giles ab159dface
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 20s
fed-sx-m1: Step 3a — in-memory log:open/append/tip/replay + 12 tests
2026-05-27 07:06:40 +00:00

64 lines
2.3 KiB
Erlang

-module(log).
-export([open/2, append/2, tip/1, replay/3, entries/1]).
%% Per-actor activity log — the canonical record of everything an
%% actor has emitted, in chronological order. Per design §15.2 this
%% lives on disk as a JSONL segment file; v1 starts with an in-memory
%% backend so the API and seq-number machinery can be locked down
%% before the on-disk format is added (Step 3b).
%%
%% State shape (a property list):
%% [{actor, ActorId}, {base, BasePath}, {seq, NextSeq}, {entries, [Act|...]}]
%%
%% `entries` stores activities in append order — i.e. oldest first.
%% `seq` is the next sequence number that will be assigned by append.
%% `base` is kept on the state for forward-compatibility with 3b
%% (where it becomes the segment-file directory).
%%
%% open/2 takes ActorId + BasePath and returns {ok, LogState} starting
%% with seq=0 and no entries.
%%
%% append/2 returns {ok, NewLogState, AssignedSeq}.
%%
%% tip/1 returns the next seq the log would assign (== count of entries).
%%
%% replay/3 folds Fun(Activity, AssignedSeq, Acc) over every entry in
%% append order. Three-arity rather than two-arity because the plan's
%% example test is "sequence numbers gap-free across replay" — having
%% the seq number visible in the fold makes that test direct.
%%
%% entries/1 is a debug accessor returning [Activity, ...] in append
%% order. Not part of the public API contract.
open(ActorId, BasePath) ->
{ok, [{actor, ActorId}, {base, BasePath}, {seq, 0}, {entries, []}]}.
append(LogState, Activity) ->
Seq = field(seq, LogState),
Entries = field(entries, LogState),
NewState = replace_field(seq, Seq + 1,
replace_field(entries, Entries ++ [Activity], LogState)),
{ok, NewState, Seq}.
tip(LogState) ->
field(seq, LogState).
replay(LogState, InitAcc, Fun) ->
Entries = field(entries, LogState),
replay_loop(Entries, 0, InitAcc, Fun).
replay_loop([], _, Acc, _) -> Acc;
replay_loop([Act | Rest], Seq, Acc, Fun) ->
replay_loop(Rest, Seq + 1, Fun(Act, Seq, Acc), Fun).
entries(LogState) ->
field(entries, LogState).
field(K, [{K, V} | _]) -> V;
field(K, [_ | Rest]) -> field(K, Rest);
field(_, []) -> erlang:error(badkey).
replace_field(K, V, []) -> [{K, V}];
replace_field(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
replace_field(K, V, [P | Rest]) -> [P | replace_field(K, V, Rest)].