fed-sx-m2: Step 7c — outbox delivery_set integration + 4 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 12m51s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 12m51s
outbox:publish/2 now computes the audience-resolved delivery set
after sign + log and stashes it in the Result proplist as
{delivery_set, [ActorId, ...]}. Step 8's delivery-queue worker
reads it off the publish result.
New compute_delivery_set/3(Request, Signed, Context):
- Pulls :follower_graph from Context (defaults to empty graph)
- Calls recipients_envelope/2 to synthesise a minimal envelope
from Request's :to / :cc + Signed's :actor
- Routes through delivery:delivery_set/3 unchanged
The envelope construct/4 surface doesn't carry :to / :cc (only
type / actor / published / object), and changing that ripples
through every envelope shape test. recipients_envelope/2 keeps
the compute boundary local to outbox.
4 new cases in outbox_publish.sh (17/17 total):
- Result :delivery_set empty default
- explicit :to -> [bob] in set
- followers symbol expands via Context :follower_graph
- self-suppression (alice in :to drops to []bob])
Module loads rebumped: follower_graph + delivery added as
dependencies; outbox shifts from epoch 5 to epoch 7. Internal
sx_server timeout bumped 240s -> 480s to fit the larger module
set.
Step 7 fully closed (7a delivery module + 7b public expansion
+ 7c outbox integration). Federation now has the end-to-end
audience resolution: an outbound activity's :to / :cc plus any
follower_graph expansion becomes a deduped recipient list ready
for Step 8 to dispatch.
Conformance running + adjacent gate running.
This commit is contained in:
@@ -92,12 +92,52 @@ publish(Request, Context) ->
|
|||||||
ok ->
|
ok ->
|
||||||
{ok, NewLog, _Seq} = log:append(LogState, Signed),
|
{ok, NewLog, _Seq} = log:append(LogState, Signed),
|
||||||
broadcast(Signed, envelope_field(projections, Context)),
|
broadcast(Signed, envelope_field(projections, Context)),
|
||||||
Result = [{cid, cid_of(Signed)}, {activity, Signed}],
|
DeliverySet = compute_delivery_set(Request, Signed, Context),
|
||||||
|
Result = [{cid, cid_of(Signed)},
|
||||||
|
{activity, Signed},
|
||||||
|
{delivery_set, DeliverySet}],
|
||||||
{ok, Result, NewLog};
|
{ok, Result, NewLog};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
{error, Reason, LogState}
|
{error, Reason, LogState}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
%% compute_delivery_set/3 — Step 7c. Pulls the audience-resolved
|
||||||
|
%% recipient list off the Request's `:to` / `:cc` fields (the
|
||||||
|
%% envelope itself doesn't carry them — construct/4 only takes
|
||||||
|
%% type / actor / published / object). Context's optional
|
||||||
|
%% `:follower_graph` field carries a follower_graph state for
|
||||||
|
%% `public` / `followers` audience expansion; absent -> empty graph,
|
||||||
|
%% so explicit `:to` / `:cc` lists still resolve. Synthesises a
|
||||||
|
%% recipient-shaped envelope from Request + Signed so the existing
|
||||||
|
%% delivery:delivery_set/3 (which reads `:actor`, `:to`, `:cc`) can
|
||||||
|
%% process it as-is.
|
||||||
|
%%
|
||||||
|
%% Step 8's delivery-queue worker reads `{delivery_set, [ActorId, ...]}`
|
||||||
|
%% off the publish result and routes one HTTP POST per entry.
|
||||||
|
|
||||||
|
compute_delivery_set(Request, Signed, Context) ->
|
||||||
|
Graph = case envelope_field(follower_graph, Context) of
|
||||||
|
nil -> follower_graph:new();
|
||||||
|
G -> G
|
||||||
|
end,
|
||||||
|
Recipients = recipients_envelope(Request, Signed),
|
||||||
|
delivery:delivery_set(Recipients, [], Graph).
|
||||||
|
|
||||||
|
recipients_envelope(Request, Signed) ->
|
||||||
|
Base = case envelope:get_field(actor, Signed) of
|
||||||
|
{ok, A} -> [{actor, A}];
|
||||||
|
_ -> []
|
||||||
|
end,
|
||||||
|
To = case envelope:get_field(to, Request) of
|
||||||
|
{ok, T} -> [{to, T}];
|
||||||
|
_ -> []
|
||||||
|
end,
|
||||||
|
Cc = case envelope:get_field(cc, Request) of
|
||||||
|
{ok, C} -> [{cc, C}];
|
||||||
|
_ -> []
|
||||||
|
end,
|
||||||
|
Base ++ To ++ Cc.
|
||||||
|
|
||||||
%% broadcast/2 — fire-and-forget cast to each named projection.
|
%% broadcast/2 — fire-and-forget cast to each named projection.
|
||||||
%% Missing/nil/empty list is a no-op; the publish API does not
|
%% Missing/nil/empty list is a no-op; the publish API does not
|
||||||
%% require projections to exist. Activity is the post-sign Signed
|
%% require projections to exist. Activity is the post-sign Signed
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ cat > "$TMPFILE" <<EPOCHS
|
|||||||
(epoch 4)
|
(epoch 4)
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
(eval "(get (erlang-load-module (file-read \"next/kernel/pipeline.erl\")) :name)")
|
||||||
(epoch 5)
|
(epoch 5)
|
||||||
|
(eval "(get (erlang-load-module (file-read \"next/kernel/follower_graph.erl\")) :name)")
|
||||||
|
(epoch 6)
|
||||||
|
(eval "(get (erlang-load-module (file-read \"next/kernel/delivery.erl\")) :name)")
|
||||||
|
(epoch 7)
|
||||||
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
(eval "(get (erlang-load-module (file-read \"next/kernel/outbox.erl\")) :name)")
|
||||||
|
|
||||||
;; Happy path: publish returns {ok, Result, NewLog}, log tip advances
|
;; Happy path: publish returns {ok, Result, NewLog}, log tip advances
|
||||||
@@ -82,9 +86,25 @@ cat > "$TMPFILE" <<EPOCHS
|
|||||||
;; CID stable: same Request twice (across fresh logs) -> same CID
|
;; CID stable: same Request twice (across fresh logs) -> same CID
|
||||||
(epoch 18)
|
(epoch 18)
|
||||||
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, R1, _} = outbox:publish(Req, Ctx), {ok, L0b} = log:open(alice, base), Ctx_b = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0b}], {ok, R2, _} = outbox:publish(Req, Ctx_b), {ok, C1} = envelope:get_field(cid, R1), {ok, C2} = envelope:get_field(cid, R2), C1 =:= C2\") :name)")
|
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, R1, _} = outbox:publish(Req, Ctx), {ok, L0b} = log:open(alice, base), Ctx_b = [{actor_id,alice},{published,100},{key_spec,KS},{actor_state,AS},{log,L0b}], {ok, R2, _} = outbox:publish(Req, Ctx_b), {ok, C1} = envelope:get_field(cid, R1), {ok, C2} = envelope:get_field(cid, R2), C1 =:= C2\") :name)")
|
||||||
|
|
||||||
|
;; Step 7c: Result has :delivery_set, empty when no :to/:cc + no graph
|
||||||
|
(epoch 20)
|
||||||
|
(eval "(get (erlang-eval-ast \"${PRELUDE} {ok, R, _} = outbox:publish(Req, Ctx), envelope:get_field(delivery_set, R) =:= {ok, []}\") :name)")
|
||||||
|
|
||||||
|
;; Step 7c: explicit :to -> delivery_set carries the recipient
|
||||||
|
(epoch 21)
|
||||||
|
(eval "(get (erlang-eval-ast \"${PRELUDE} ReqTo = [{type, note}, {object, [{content, hi}]}, {to, bob}], {ok, R, _} = outbox:publish(ReqTo, Ctx), envelope:get_field(delivery_set, R) =:= {ok, [bob]}\") :name)")
|
||||||
|
|
||||||
|
;; Step 7c: followers symbol expands via graph in Context
|
||||||
|
(epoch 22)
|
||||||
|
(eval "(get (erlang-eval-ast \"${PRELUDE} F = [{actor, bob}, {type, follow}, {object, alice}], A = [{actor, alice}, {type, accept}, {object, F}], Graph = follower_graph:fold(A, follower_graph:fold(F, follower_graph:new())), CtxG = Ctx ++ [{follower_graph, Graph}], ReqFol = [{type, note}, {object, [{content, hi}]}, {to, followers}], {ok, R, _} = outbox:publish(ReqFol, CtxG), envelope:get_field(delivery_set, R) =:= {ok, [bob]}\") :name)")
|
||||||
|
|
||||||
|
;; Step 7c: self-suppression — alice's :to including alice drops it
|
||||||
|
(epoch 23)
|
||||||
|
(eval "(get (erlang-eval-ast \"${PRELUDE} ReqSelf = [{type, note}, {object, [{content, hi}]}, {to, [alice, bob]}], {ok, R, _} = outbox:publish(ReqSelf, Ctx), envelope:get_field(delivery_set, R) =:= {ok, [bob]}\") :name)")
|
||||||
EPOCHS
|
EPOCHS
|
||||||
|
|
||||||
OUTPUT=$(timeout 240 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
OUTPUT=$(timeout 480 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||||
|
|
||||||
check() {
|
check() {
|
||||||
local epoch="$1" desc="$2" expected="$3"
|
local epoch="$1" desc="$2" expected="$3"
|
||||||
@@ -108,11 +128,15 @@ check() {
|
|||||||
check 2 "envelope module loaded" "envelope"
|
check 2 "envelope module loaded" "envelope"
|
||||||
check 3 "log module loaded" "log"
|
check 3 "log module loaded" "log"
|
||||||
check 4 "pipeline module loaded" "pipeline"
|
check 4 "pipeline module loaded" "pipeline"
|
||||||
check 5 "outbox module loaded" "outbox"
|
check 7 "outbox module loaded" "outbox"
|
||||||
check 10 "happy path tip advances to 1" "true"
|
check 10 "happy path tip advances to 1" "true"
|
||||||
check 11 "result :cid matches activity" "true"
|
check 11 "result :cid matches activity" "true"
|
||||||
check 12 "signed activity in log entries" "true"
|
check 12 "signed activity in log entries" "true"
|
||||||
check 13 "duplicate publish -> replay" "ok"
|
check 13 "duplicate publish -> replay" "ok"
|
||||||
|
check 20 "Result :delivery_set empty default" "true"
|
||||||
|
check 21 "explicit :to -> [bob] in set" "true"
|
||||||
|
check 22 "followers symbol expands via graph" "true"
|
||||||
|
check 23 "self-suppression on alice in :to" "true"
|
||||||
check 14 "replay leaves log tip at 1" "true"
|
check 14 "replay leaves log tip at 1" "true"
|
||||||
check 15 "bad key material -> bad_signature" "ok"
|
check 15 "bad key material -> bad_signature" "ok"
|
||||||
check 16 "distinct timestamps -> tip 2" "true"
|
check 16 "distinct timestamps -> tip 2" "true"
|
||||||
|
|||||||
@@ -503,10 +503,27 @@ expansion via the audience predicates from M1's genesis bundle.
|
|||||||
in the same audience deduplicates because both symbols
|
in the same audience deduplicates because both symbols
|
||||||
expand identically. 19/19 in `delivery_set.sh` (2 new cases
|
expand identically. 19/19 in `delivery_set.sh` (2 new cases
|
||||||
+ 1 case updated from the v2 placeholder behavior).
|
+ 1 case updated from the v2 placeholder behavior).
|
||||||
- [ ] **7c** — Outbox-side integration: `outbox:publish/2`
|
- [x] **7c** — Outbox-side integration. `outbox:publish/2`
|
||||||
computes the delivery set after sign + log and stashes it in
|
now computes the delivery set after sign + log and stashes it
|
||||||
the Result proplist as `{delivery_set, [ActorId, ...]}`. Step
|
in the Result proplist as `{delivery_set, [ActorId, ...]}`.
|
||||||
8's delivery-queue worker reads it off the publish result.
|
Context's optional `:follower_graph` field carries a
|
||||||
|
follower_graph state for `public` / `followers` audience
|
||||||
|
expansion; absent -> empty graph (explicit `:to`/`:cc`
|
||||||
|
recipients still resolve). New helper
|
||||||
|
`compute_delivery_set/3(Request, Signed, Context)` and
|
||||||
|
`recipients_envelope/2` synthesise a minimal recipient
|
||||||
|
envelope from Request's `:to`/`:cc` + Signed's `:actor` so
|
||||||
|
`delivery:delivery_set/3` can process it unchanged
|
||||||
|
(outbox:construct/4 doesn't carry `:to`/`:cc` through the
|
||||||
|
envelope shape, and changing that surface would ripple to
|
||||||
|
every existing envelope test). Step 8's delivery-queue
|
||||||
|
worker will read `{delivery_set, [ActorId, ...]}` off the
|
||||||
|
publish result. 17/17 in `outbox_publish.sh` (+4 new cases:
|
||||||
|
empty-default, explicit-:to, followers-symbol-via-graph,
|
||||||
|
self-suppression). Module load chain rebumped from epoch 5
|
||||||
|
to epoch 7 (adds follower_graph + delivery as dependencies)
|
||||||
|
and the test's internal sx_server timeout bumped 240s →
|
||||||
|
480s to fit the larger module set.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -850,6 +867,23 @@ proceed.
|
|||||||
|
|
||||||
Newest first.
|
Newest first.
|
||||||
|
|
||||||
|
- **2026-06-07** — Step 7c (closes Step 7): outbox-side
|
||||||
|
delivery_set integration. `outbox:publish/2` computes the
|
||||||
|
audience-resolved delivery set after sign + log and stashes
|
||||||
|
it in the Result proplist as `{delivery_set, [ActorId, ...]}`.
|
||||||
|
New `compute_delivery_set/3(Request, Signed, Context)`
|
||||||
|
threads `:follower_graph` from Context through to
|
||||||
|
`delivery:delivery_set/3`. `recipients_envelope/2` synthesises
|
||||||
|
a minimal envelope from the Request's `:to`/`:cc` + Signed's
|
||||||
|
`:actor` so the existing delivery API works unchanged
|
||||||
|
(envelope construct/4 doesn't carry the audience fields
|
||||||
|
through). 17/17 in `outbox_publish.sh` (+4 new: empty-default,
|
||||||
|
explicit-:to, followers-symbol-via-graph, self-suppression).
|
||||||
|
Module load order shifted from epoch 5 to epoch 7 to make
|
||||||
|
room for follower_graph + delivery; internal sx_server
|
||||||
|
timeout bumped 240s → 480s. Step 7 fully closed (7a delivery
|
||||||
|
module + 7b public expansion + 7c outbox integration).
|
||||||
|
|
||||||
- **2026-06-06** — Step 7b: public audience expansion.
|
- **2026-06-06** — Step 7b: public audience expansion.
|
||||||
`delivery:expand_audience(public, Sender, Graph)` now returns
|
`delivery:expand_audience(public, Sender, Graph)` now returns
|
||||||
the sender's followers (same as `followers`) — per design
|
the sender's followers (same as `followers`) — per design
|
||||||
|
|||||||
Reference in New Issue
Block a user