-module(backfill). -export([slice/2, slice/3, wrap_backfill/1, parse_mode/1, all_entries/1, last_n_entries/2, last_t_entries/3, since_cid_entries/2, none_entries/0]). %% Backfill mode slicing per design §13.3 / Step 9. When A follows B %% with a backfill spec, B's kernel slices the outbox log into the %% appropriate window and delivers each entry as %% `{backfilled, true}`-marked envelopes alongside forward-going %% activity. %% %% Mode shapes (per the Follow activity's `:backfill` field): %% none — newer follower sees only forward content %% {last_n, N} — backfill last N activities (FIFO order) %% {last_t, T, NowFn} — backfill activities with :published in %% (Now - T .. Now]. NowFn is a 0-arity fun %% so tests can fake-time it. %% full — backfill the entire outbox %% %% slice/2 returns the activity list. slice/3 also wraps each entry %% with `{backfilled, true}` so projections can decide whether to %% re-fold or skip (the §13.3 Backfilled bodies preserve the %% original `:id` so replay defence still works on the receiver). %% %% parse_mode/1 lifts the Follow activity's `:backfill` proplist %% (or atom) into the internal mode tuple. Unknown shapes fall back %% to `none` — the default open-world policy. slice(Mode, LogState) -> slice(Mode, LogState, false). slice(Mode, LogState, Wrap) -> Entries = log:entries(LogState), Slice = case Mode of none -> none_entries(); full -> all_entries(Entries); {last_n, N} -> last_n_entries(N, Entries); {last_t, T, NowFn} -> last_t_entries(T, NowFn, Entries); {since_cid, Cid} -> since_cid_entries(Cid, Entries); _ -> none_entries() end, case Wrap of true -> wrap_backfill(Slice); _ -> Slice end. %% ── Mode-specific entry selection ───────────────────────────── all_entries(Entries) -> Entries. none_entries() -> []. %% last_n_entries/2 — tail N entries in FIFO order. last_n_entries(N, _) when N =< 0 -> []; last_n_entries(N, Entries) -> Len = length(Entries), case Len =< N of true -> Entries; false -> drop_n(Len - N, Entries) end. drop_n(0, L) -> L; drop_n(_, []) -> []; drop_n(N, [_ | Rest]) -> drop_n(N - 1, Rest). %% last_t_entries/3 — entries whose :published is within the last %% T units of (NowFn() - T .. NowFn()]. T and :published are %% integers (seconds-since-epoch in production; opaque ints in tests). last_t_entries(T, NowFn, Entries) when is_integer(T), T >= 0 -> Now = NowFn(), Cutoff = Now - T, [E || E <- Entries, in_window(E, Cutoff, Now)]; last_t_entries(_, _, _) -> []. in_window(Activity, Cutoff, Now) -> case envelope:get_field(published, Activity) of {ok, P} when is_integer(P), P > Cutoff, P =< Now -> true; _ -> false end. %% since_cid_entries/2 — every entry after the one with :id = Cid. %% If Cid isn't in the log, returns [] (caller's pointer is stale). %% Used by `GET /actors//outbox?since=Cid` pagination. since_cid_entries(_Cid, []) -> []; since_cid_entries(Cid, [E | Rest]) -> case envelope:get_field(id, E) of {ok, Cid} -> Rest; _ -> since_cid_entries(Cid, Rest) end. %% wrap_backfill/1 — append `{backfilled, true}` to each entry. %% The receiving projection scheduler reads this field and chooses %% whether to fold (re-emit) or skip (already known via replay %% defence on `:id`). wrap_backfill([]) -> []; wrap_backfill([E | Rest]) -> [E ++ [{backfilled, true}] | wrap_backfill(Rest)]. %% parse_mode/1 — Lift a Follow activity's `:backfill` value into the %% internal mode tuple. Accepts: %% nil / not_found -> none %% none -> none %% full -> full %% {last_n, N} -> {last_n, N} (already-parsed shape) %% {last_t, T, NowFn} -> pass-through %% Proplist with :mode + :limit / :duration -> parsed %% Unknown shape -> none (open-world default). parse_mode(nil) -> none; parse_mode(none) -> none; parse_mode(full) -> full; parse_mode({last_n, N}) -> {last_n, N}; parse_mode({last_t, T, NowFn}) -> {last_t, T, NowFn}; parse_mode({since_cid, Cid}) -> {since_cid, Cid}; parse_mode(List) when is_list(List) -> case envelope:get_field(mode, List) of {ok, last_n} -> case envelope:get_field(limit, List) of {ok, N} when is_integer(N) -> {last_n, N}; _ -> none end; {ok, last_t} -> case envelope:get_field(duration, List) of {ok, T} when is_integer(T) -> {last_t, T, fun () -> 0 end}; _ -> none end; {ok, full} -> full; {ok, none} -> none; _ -> none end; parse_mode(_) -> none.