fed-sx-m1: Step 3a — in-memory log:open/append/tip/replay + 12 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 20s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 20s
This commit is contained in:
63
next/kernel/log.erl
Normal file
63
next/kernel/log.erl
Normal file
@@ -0,0 +1,63 @@
|
||||
-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)].
|
||||
Reference in New Issue
Block a user