fed-sx-m2: Step 11b — Announce + Endorse projection folds + 19 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
Two new projection modules for the rich verbs landed in Step 11a:
next/kernel/announce_state.erl
Per-target-Cid announcer set.
State: [{TargetCid, [AnnouncerActorId, ...]}, ...]
Set semantics — duplicate Announce by the same actor on the
same target is a no-op.
Public API:
new/0, fold/2, fold_fn/0
announcers_for/2, announce_count/2, announced_cids/1
has_announced/3
next/kernel/endorsement_state.erl
Per-target-Cid + per-kind + per-actor endorsement counter.
State: [{TargetCid, [{Kind, [{ActorId, Count}, ...]}, ...]}, ...]
Additive semantics — re-endorse by the same actor under the
same kind bumps the counter. Undo{Endorse} retraction defers
to a follow-up.
Public API:
new/0, fold/2, fold_fn/0
counters_for/2, total_for/2, kinds_for/2
endorsers_for/3, has_endorsed/4
Both fold_fn/0 returns a 2-arity Erlang fun for
projection:start_link/3 (same plug shape as actor_state /
follower_graph / delivery_state). Non-matching activity types
pass through unchanged.
Read-side accessors cover both enumeration (announcers_for,
endorsers_for) and predicates (has_announced, has_endorsed) so
the feed/timeline projection layer doesn't have to re-implement
that logic on every consumer.
19/19 in next/tests/rich_verbs.sh:
announce_state:
- new/0 -> []
- Announce -> announcer added
- Two announces same target -> both in set
- Duplicate announce by same actor -> no-op
- announce_count + announced_cids
- has_announced predicate
- fold_fn/0 is fun/2
- Non-Announce activity passes through
endorsement_state:
- new/0 -> []
- Endorse -> counter 1
- Two likes by different actors -> total 2
- like + share -> two kinds tracked
- endorsers_for(Cid, Kind)
- has_endorsed predicate
- fold_fn/0 is fun/2
- Non-Endorse activity passes through
- Same actor endorsing twice -> total = 2 (additive)
Conformance preserved at 761/761.
This commit is contained in:
79
next/kernel/announce_state.erl
Normal file
79
next/kernel/announce_state.erl
Normal file
@@ -0,0 +1,79 @@
|
||||
-module(announce_state).
|
||||
-export([new/0, fold/2, fold_fn/0,
|
||||
announcers_for/2, announce_count/2, announced_cids/1,
|
||||
has_announced/3]).
|
||||
|
||||
%% Announce-fanout projection. Folds Announce activities into a
|
||||
%% per-target-Cid set of announcer ActorIds so projections can
|
||||
%% answer "who re-broadcast this activity" / "how many announces
|
||||
%% does this Note have" / "what activities has X announced".
|
||||
%%
|
||||
%% Announce envelope shape (per next/genesis/activity-types/announce.sx):
|
||||
%% [{type, announce},
|
||||
%% {actor, AnnouncerActorId},
|
||||
%% {object, TargetCidBinary},
|
||||
%% ...]
|
||||
%%
|
||||
%% State shape:
|
||||
%% [{TargetCid, [Announcer1, Announcer2, ...]}, ...]
|
||||
%%
|
||||
%% Set semantics — the same actor announcing the same target twice
|
||||
%% is a no-op (already in the list). Undo{Announce} retraction
|
||||
%% defers to a follow-up.
|
||||
|
||||
new() -> [].
|
||||
|
||||
fold_fn() ->
|
||||
fun (Activity, State) -> fold(Activity, State) end.
|
||||
|
||||
fold(Activity, State) ->
|
||||
case envelope:get_field(type, Activity) of
|
||||
{ok, announce} -> fold_announce(Activity, State);
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
fold_announce(Activity, State) ->
|
||||
case {envelope:get_field(actor, Activity),
|
||||
envelope:get_field(object, Activity)} of
|
||||
{{ok, Actor}, {ok, Cid}} -> add_announcer(Cid, Actor, State);
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
add_announcer(Cid, Actor, State) ->
|
||||
Current = case find_keyed(Cid, State) of
|
||||
{ok, Set} -> Set;
|
||||
_ -> []
|
||||
end,
|
||||
case contains(Actor, Current) of
|
||||
true -> State;
|
||||
false -> set_keyed(Cid, Current ++ [Actor], State)
|
||||
end.
|
||||
|
||||
%% ── Read-side accessors ───────────────────────────────────────
|
||||
|
||||
announcers_for(Cid, State) ->
|
||||
case find_keyed(Cid, State) of
|
||||
{ok, Set} -> Set;
|
||||
_ -> []
|
||||
end.
|
||||
|
||||
announce_count(Cid, State) -> length(announcers_for(Cid, State)).
|
||||
|
||||
announced_cids(State) -> [C || {C, _} <- State].
|
||||
|
||||
has_announced(Actor, Cid, State) ->
|
||||
contains(Actor, announcers_for(Cid, State)).
|
||||
|
||||
%% ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
contains(_, []) -> false;
|
||||
contains(X, [X | _]) -> true;
|
||||
contains(X, [_ | Rest]) -> contains(X, Rest).
|
||||
|
||||
find_keyed(_, []) -> {error, not_found};
|
||||
find_keyed(K, [{K, V} | _]) -> {ok, V};
|
||||
find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest).
|
||||
|
||||
set_keyed(K, V, []) -> [{K, V}];
|
||||
set_keyed(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
||||
set_keyed(K, V, [P | Rest]) -> [P | set_keyed(K, V, Rest)].
|
||||
118
next/kernel/endorsement_state.erl
Normal file
118
next/kernel/endorsement_state.erl
Normal file
@@ -0,0 +1,118 @@
|
||||
-module(endorsement_state).
|
||||
-export([new/0, fold/2, fold_fn/0,
|
||||
counters_for/2, total_for/2, kinds_for/2,
|
||||
endorsers_for/3, has_endorsed/4]).
|
||||
|
||||
%% Endorsement counter projection. Folds Endorse activities into a
|
||||
%% per-target-Cid + per-kind counter so projections can serve
|
||||
%% "how many likes does this Note have" / "list everyone who shared
|
||||
%% this Announce" queries.
|
||||
%%
|
||||
%% Endorse envelope shape (per next/genesis/activity-types/endorse.sx):
|
||||
%% [{type, endorse},
|
||||
%% {actor, ActorId},
|
||||
%% {object, TargetCidBinary},
|
||||
%% {kind, KindAtomOrBinary},
|
||||
%% ...]
|
||||
%%
|
||||
%% State shape:
|
||||
%% [{TargetCid, [{Kind, [{ActorId, Count}, ...]}, ...]}, ...]
|
||||
%%
|
||||
%% Each ActorId can endorse the same target multiple times under
|
||||
%% the same kind (e.g. like → unlike → like → ...); the counter
|
||||
%% tracks how many *net* endorsement events fired. Step 11b ships
|
||||
%% the additive counter only; the unlike / un-endorse semantics
|
||||
%% (Undo{Endorse}) and reaction-toggling defer to a follow-up.
|
||||
|
||||
new() -> [].
|
||||
|
||||
fold_fn() ->
|
||||
fun (Activity, State) -> fold(Activity, State) end.
|
||||
|
||||
fold(Activity, State) ->
|
||||
case envelope:get_field(type, Activity) of
|
||||
{ok, endorse} -> fold_endorse(Activity, State);
|
||||
_ -> State
|
||||
end.
|
||||
|
||||
fold_endorse(Activity, State) ->
|
||||
case {envelope:get_field(actor, Activity),
|
||||
envelope:get_field(object, Activity),
|
||||
envelope:get_field(kind, Activity)} of
|
||||
{{ok, Actor}, {ok, Cid}, {ok, Kind}} ->
|
||||
bump(Cid, Kind, Actor, State);
|
||||
_ ->
|
||||
State
|
||||
end.
|
||||
|
||||
bump(Cid, Kind, Actor, State) ->
|
||||
KindMap = case find_keyed(Cid, State) of
|
||||
{ok, KM} -> KM;
|
||||
_ -> []
|
||||
end,
|
||||
ActorMap = case find_keyed(Kind, KindMap) of
|
||||
{ok, AM} -> AM;
|
||||
_ -> []
|
||||
end,
|
||||
Current = case find_keyed(Actor, ActorMap) of
|
||||
{ok, N} -> N;
|
||||
_ -> 0
|
||||
end,
|
||||
ActorMap1 = set_keyed(Actor, Current + 1, ActorMap),
|
||||
KindMap1 = set_keyed(Kind, ActorMap1, KindMap),
|
||||
set_keyed(Cid, KindMap1, State).
|
||||
|
||||
%% ── Read-side accessors ───────────────────────────────────────
|
||||
|
||||
%% counters_for(Cid, State) -> [{Kind, TotalCount}, ...]
|
||||
%% Sum per-kind across all endorsers.
|
||||
|
||||
counters_for(Cid, State) ->
|
||||
case find_keyed(Cid, State) of
|
||||
{ok, KindMap} ->
|
||||
[{K, sum_counts(AM)} || {K, AM} <- KindMap];
|
||||
_ -> []
|
||||
end.
|
||||
|
||||
total_for(Cid, State) ->
|
||||
lists:foldl(fun ({_, N}, Acc) -> N + Acc end, 0, counters_for(Cid, State)).
|
||||
|
||||
kinds_for(Cid, State) ->
|
||||
[K || {K, _} <- counters_for(Cid, State)].
|
||||
|
||||
endorsers_for(Cid, Kind, State) ->
|
||||
case find_keyed(Cid, State) of
|
||||
{ok, KindMap} ->
|
||||
case find_keyed(Kind, KindMap) of
|
||||
{ok, AM} -> [A || {A, _} <- AM];
|
||||
_ -> []
|
||||
end;
|
||||
_ -> []
|
||||
end.
|
||||
|
||||
has_endorsed(Actor, Cid, Kind, State) ->
|
||||
case find_keyed(Cid, State) of
|
||||
{ok, KindMap} ->
|
||||
case find_keyed(Kind, KindMap) of
|
||||
{ok, AM} ->
|
||||
case find_keyed(Actor, AM) of
|
||||
{ok, N} -> N > 0;
|
||||
_ -> false
|
||||
end;
|
||||
_ -> false
|
||||
end;
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
%% ── Internal ──────────────────────────────────────────────────
|
||||
|
||||
sum_counts([]) -> 0;
|
||||
sum_counts([{_, N} | Rest]) -> N + sum_counts(Rest).
|
||||
|
||||
find_keyed(_, []) -> {error, not_found};
|
||||
find_keyed(K, [{K, V} | _]) -> {ok, V};
|
||||
find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest).
|
||||
|
||||
set_keyed(K, V, []) -> [{K, V}];
|
||||
set_keyed(K, V, [{K, _} | Rest]) -> [{K, V} | Rest];
|
||||
set_keyed(K, V, [P | Rest]) -> [P | set_keyed(K, V, Rest)].
|
||||
Reference in New Issue
Block a user