fed-sx-m2: Step 4d — outbox listing from log + pagination + 8 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Per-actor GET /actors/<id>/outbox now reads the bucket's log via
new nx_kernel:log_state_for/1 gen_server export and renders the
paged CID list.
nx_kernel additions:
log_state_for/1 gen_server call returning {ok, LogState} for
the named actor (mirrors log_tip_for/1's shape).
http_server additions:
- with_request_query/2 bakes Req's :query binary into Cfg as
{request_query, Q} so sub-resource handlers can parse params
without taking the Req as another arg
- kernel_actor_log_data/2 -> {Tip, Entries} via
nx_kernel:log_tip_for + log_state_for + log:entries
- parse_page/1 reads ?page=N (default 1, non-digits -> 1)
- page_size/0 returns 5 (test-friendly; production picks 20+)
- page_slice/2 + drop_take/3 + take/2 for the page extraction
- entry_cids/1 maps entries to :id CID binaries via envelope
- actor_outbox_full_response_for/5 renders text / JSON / SX:
text: outbox: <id>\ntip: N\npage: P\nitem: <cid>\n...
json: {"outbox":"<id>","tip":N,"page":P,"items":[...]}
sx: (outbox "<id>" :tip N :page P :items (...))
Empty page degrades to actor_outbox_with_tip_response_for so
epochs 50-57 from Step 4c still pass — the prefix is preserved.
8 new cases in next/tests/http_multi_actor.sh (41/41 total):
- 1 publish -> body contains outbox/tip=1/page=1/item: prefix
- 3 publishes -> body contains tip=3/page=1/item: prefix
- page=2 with 3 items -> empty page degrades to tip-only body
- 6 publishes page=1 -> tip=6/page=1/item: prefix
- 6 publishes page=2 -> tip=6/page=2/item: prefix
- JSON body shape with items array (1 entry)
- SX body shape with :items list (1 entry)
- bad ?page=bad falls back to page 1
Conformance 761/761. 117/117 across 11 Step-4-adjacent suites
(http_multi_actor, http_route, http_publish, http_post_format,
http_marshal, http_publish_fold, http_listen_bif, http_server_start,
nx_kernel_multi, nx_kernel_server, bootstrap_start, actor_lifecycle).
Substrate gotcha logged: named recursive funs fun F(...) -> F(...)
end aren't supported by the parser ('fun-ref syntax not yet
supported'); binary:matches/2 and lists:foreach/2 aren't registered.
Tests prove behaviour via match_prefix substring checks rather than
counting occurrences.
This commit is contained in:
@@ -69,6 +69,7 @@ route(Req, Cfg) ->
|
||||
M = field(method, Req),
|
||||
P = field(path, Req),
|
||||
F = accept_format_from(Req),
|
||||
Cfg1 = with_request_query(Req, Cfg),
|
||||
case {M, P} of
|
||||
{<<80,79,83,84>>, <<47,97,99,116,105,118,105,116,121>>} ->
|
||||
handle_post_activity(Req, Cfg);
|
||||
@@ -77,7 +78,16 @@ route(Req, Cfg) ->
|
||||
47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>} ->
|
||||
ok_response(capabilities_body_for(F));
|
||||
_ ->
|
||||
dispatch(M, P, F, Cfg)
|
||||
dispatch(M, P, F, Cfg1)
|
||||
end.
|
||||
|
||||
%% with_request_query/2 — bake the Req's :query binary into Cfg as
|
||||
%% `{request_query, Q}` so sub-resource handlers can parse `?page=N`
|
||||
%% etc without taking the Req as an extra argument.
|
||||
with_request_query(Req, Cfg) ->
|
||||
case field(query, Req) of
|
||||
nil -> Cfg;
|
||||
Q -> [{request_query, Q} | Cfg]
|
||||
end.
|
||||
|
||||
%% route/3 — Step 4c convenience entry. Kernel is an opaque
|
||||
@@ -680,14 +690,101 @@ actor_outbox_response_for(Id, F, Cfg) ->
|
||||
nil ->
|
||||
actor_outbox_response_for(Id, F);
|
||||
Kernel ->
|
||||
case kernel_log_tip(Kernel, Id) of
|
||||
case kernel_actor_log_data(Kernel, Id) of
|
||||
nil ->
|
||||
actor_outbox_response_for(Id, F);
|
||||
Tip ->
|
||||
actor_outbox_with_tip_response_for(Id, F, Tip)
|
||||
{Tip, Entries} ->
|
||||
Page = parse_page(field(request_query, Cfg)),
|
||||
Slice = page_slice(Entries, Page),
|
||||
Cids = entry_cids(Slice),
|
||||
actor_outbox_full_response_for(Id, F, Tip, Page, Cids)
|
||||
end
|
||||
end.
|
||||
|
||||
%% kernel_actor_log_data/2 — synchronous query to the kernel for
|
||||
%% the actor's tip + flat entry list. nil when the kernel atom isn't
|
||||
%% registered or the actor isn't present (mirrors kernel_log_tip/2's
|
||||
%% guard pattern).
|
||||
|
||||
kernel_actor_log_data(Kernel, Id) when is_atom(Kernel) ->
|
||||
case erlang:whereis(Kernel) of
|
||||
undefined -> nil;
|
||||
_ ->
|
||||
L = binary_to_list(Id),
|
||||
A = list_to_atom(L),
|
||||
T = nx_kernel:log_tip_for(A),
|
||||
case T of
|
||||
N when is_integer(N) ->
|
||||
case nx_kernel:log_state_for(A) of
|
||||
{ok, LogState} -> {N, log:entries(LogState)};
|
||||
_ -> {N, []}
|
||||
end;
|
||||
_ -> nil
|
||||
end
|
||||
end;
|
||||
kernel_actor_log_data(_, _) -> nil.
|
||||
|
||||
%% page_size/0 — small for v2 (proof of concept). Real outboxes
|
||||
%% pick a larger page size (Mastodon defaults to 20). Tests pin
|
||||
%% this to 5 so 3 publishes fit in one page and 6 publishes
|
||||
%% straddle two pages.
|
||||
|
||||
page_size() -> 5.
|
||||
|
||||
%% parse_page/1 — accept `?page=N` from the query string. `nil` or
|
||||
%% missing param -> page 1. Non-positive values clamp to 1.
|
||||
|
||||
parse_page(nil) -> 1;
|
||||
parse_page(Q) when is_binary(Q) ->
|
||||
case match_prefix(<<112,97,103,101,61>>, Q) of % "page="
|
||||
{ok, Rest} ->
|
||||
case parse_int(Rest) of
|
||||
{ok, N} when N >= 1 -> N;
|
||||
_ -> 1
|
||||
end;
|
||||
_ -> 1
|
||||
end;
|
||||
parse_page(_) -> 1.
|
||||
|
||||
parse_int(Bin) ->
|
||||
L = binary_to_list(Bin),
|
||||
case L of
|
||||
[] -> error;
|
||||
_ ->
|
||||
case all_digits(L) of
|
||||
true -> {ok, list_to_integer(L)};
|
||||
false -> error
|
||||
end
|
||||
end.
|
||||
|
||||
all_digits([]) -> true;
|
||||
all_digits([C | Rest]) when C >= 48, C =< 57 -> all_digits(Rest);
|
||||
all_digits(_) -> false.
|
||||
|
||||
%% page_slice/2 — extract a page-sized slice of Entries. Page is
|
||||
%% 1-indexed; out-of-range pages yield [].
|
||||
|
||||
page_slice(Entries, Page) ->
|
||||
Sz = page_size(),
|
||||
Start = (Page - 1) * Sz,
|
||||
drop_take(Entries, Start, Sz).
|
||||
|
||||
drop_take(_, _, 0) -> [];
|
||||
drop_take([], _, _) -> [];
|
||||
drop_take(L, 0, N) -> take(L, N);
|
||||
drop_take([_ | Rest], K, N) -> drop_take(Rest, K - 1, N).
|
||||
|
||||
take(_, 0) -> [];
|
||||
take([], _) -> [];
|
||||
take([H | Rest], N) -> [H | take(Rest, N - 1)].
|
||||
|
||||
entry_cids([]) -> [];
|
||||
entry_cids([E | Rest]) ->
|
||||
case envelope:get_field(id, E) of
|
||||
{ok, Cid} -> [Cid | entry_cids(Rest)];
|
||||
_ -> entry_cids(Rest)
|
||||
end.
|
||||
|
||||
%% kernel_log_tip/2 — query the kernel for an actor's log tip via
|
||||
%% `nx_kernel:log_tip_for/1`. Returns the tip integer when the actor
|
||||
%% exists, `nil` when the kernel atom isn't registered or the actor
|
||||
@@ -739,6 +836,84 @@ actor_outbox_with_tip_response_for(Id, sx, Tip) ->
|
||||
actor_outbox_with_tip_response_for(Id, _, Tip) ->
|
||||
actor_outbox_with_tip_response_for(Id, text, Tip).
|
||||
|
||||
%% actor_outbox_full_response_for/5 — Step 4d body shape includes
|
||||
%% the actor id, tip, current page number, and the page's CID list.
|
||||
%% Empty Cids degrades to the /tip/ variant — keeps the 4c body
|
||||
%% shape stable when an actor has no entries (e.g. a Bob with zero
|
||||
%% publishes).
|
||||
|
||||
actor_outbox_full_response_for(Id, F, Tip, _Page, []) ->
|
||||
actor_outbox_with_tip_response_for(Id, F, Tip);
|
||||
actor_outbox_full_response_for(Id, text, Tip, Page, Cids) ->
|
||||
Pre = <<111,117,116,98,111,120,58,32>>, % "outbox: "
|
||||
Tipp = <<10,116,105,112,58,32>>, % "\ntip: "
|
||||
Pag = <<10,112,97,103,101,58,32>>, % "\npage: "
|
||||
Itm = <<10,105,116,101,109,58,32>>, % "\nitem: "
|
||||
TipBin = list_to_binary(integer_to_list(Tip)),
|
||||
PageBin = list_to_binary(integer_to_list(Page)),
|
||||
Head = <<Pre/binary, Id/binary,
|
||||
Tipp/binary, TipBin/binary,
|
||||
Pag/binary, PageBin/binary>>,
|
||||
Body = lines_with_prefix(Head, Itm, Cids, <<10>>),
|
||||
ok_response(Body);
|
||||
actor_outbox_full_response_for(Id, json, Tip, Page, Cids) ->
|
||||
Body = json_outbox_body(Id, Tip, Page, Cids),
|
||||
ok_response(Body, json);
|
||||
actor_outbox_full_response_for(Id, activity_json, Tip, Page, Cids) ->
|
||||
Body = json_outbox_body(Id, Tip, Page, Cids),
|
||||
ok_response(Body, activity_json);
|
||||
actor_outbox_full_response_for(Id, sx, Tip, Page, Cids) ->
|
||||
Body = sx_outbox_body(Id, Tip, Page, Cids),
|
||||
ok_response(Body, sx);
|
||||
actor_outbox_full_response_for(Id, _, Tip, Page, Cids) ->
|
||||
actor_outbox_full_response_for(Id, text, Tip, Page, Cids).
|
||||
|
||||
lines_with_prefix(Acc, _, [], Tail) -> <<Acc/binary, Tail/binary>>;
|
||||
lines_with_prefix(Acc, Itm, [C | Rest], Tail) ->
|
||||
lines_with_prefix(<<Acc/binary, Itm/binary, C/binary>>, Itm, Rest, Tail).
|
||||
|
||||
%% {"outbox":"<id>","tip":N,"page":P,"items":["cid1","cid2",...]}
|
||||
json_outbox_body(Id, Tip, Page, Cids) ->
|
||||
Pre = <<123,34,111,117,116,98,111,120,34,58,34>>,
|
||||
Mid1 = <<34,44,34,116,105,112,34,58>>, % '","tip":'
|
||||
Mid2 = <<44,34,112,97,103,101,34,58>>, % ',"page":'
|
||||
Mid3 = <<44,34,105,116,101,109,115,34,58,91>>, % ',"items":['
|
||||
Suf = <<93,125,10>>, % ']}\n'
|
||||
TipBin = list_to_binary(integer_to_list(Tip)),
|
||||
PageBin = list_to_binary(integer_to_list(Page)),
|
||||
Items = json_string_list(Cids),
|
||||
<<Pre/binary, Id/binary,
|
||||
Mid1/binary, TipBin/binary,
|
||||
Mid2/binary, PageBin/binary,
|
||||
Mid3/binary, Items/binary, Suf/binary>>.
|
||||
|
||||
json_string_list([]) -> <<>>;
|
||||
json_string_list([C]) -> <<34, C/binary, 34>>;
|
||||
json_string_list([C | Rest]) ->
|
||||
Tail = json_string_list(Rest),
|
||||
<<34, C/binary, 34, 44, Tail/binary>>.
|
||||
|
||||
%% (outbox "<id>" :tip N :page P :items ("cid1" "cid2" ...))
|
||||
sx_outbox_body(Id, Tip, Page, Cids) ->
|
||||
Pre = <<40,111,117,116,98,111,120,32,34>>, % '(outbox "'
|
||||
Mid1 = <<34,32,58,116,105,112,32>>, % '" :tip '
|
||||
Mid2 = <<32,58,112,97,103,101,32>>, % ' :page '
|
||||
Mid3 = <<32,58,105,116,101,109,115,32,40>>, % ' :items ('
|
||||
Suf = <<41,41,10>>, % '))\n'
|
||||
TipBin = list_to_binary(integer_to_list(Tip)),
|
||||
PageBin = list_to_binary(integer_to_list(Page)),
|
||||
Items = sx_string_list(Cids),
|
||||
<<Pre/binary, Id/binary,
|
||||
Mid1/binary, TipBin/binary,
|
||||
Mid2/binary, PageBin/binary,
|
||||
Mid3/binary, Items/binary, Suf/binary>>.
|
||||
|
||||
sx_string_list([]) -> <<>>;
|
||||
sx_string_list([C]) -> <<34, C/binary, 34>>;
|
||||
sx_string_list([C | Rest]) ->
|
||||
Tail = sx_string_list(Rest),
|
||||
<<34, C/binary, 34, 32, Tail/binary>>.
|
||||
|
||||
%% "inbox: " — 7 bytes
|
||||
actor_inbox_get_response_for(Id, text) ->
|
||||
Pre = <<105,110,98,111,120,58,32>>,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
%% gen_server API
|
||||
-export([start_link/3, publish/1, query/0, log_tip/0,
|
||||
with_projections/1, stop/0,
|
||||
add_actor/3, publish_to/2, log_tip_for/1,
|
||||
add_actor/3, publish_to/2, log_tip_for/1, log_state_for/1,
|
||||
actors/0, state_for/1, bucket_for/1,
|
||||
with_projections_for/2,
|
||||
bootstrap_actor/3]).
|
||||
@@ -321,6 +321,9 @@ publish_to(ActorId, Request) ->
|
||||
log_tip_for(ActorId) ->
|
||||
gen_server:call(nx_kernel, {log_tip_for, ActorId}).
|
||||
|
||||
log_state_for(ActorId) ->
|
||||
gen_server:call(nx_kernel, {log_state_for, ActorId}).
|
||||
|
||||
actors() ->
|
||||
gen_server:call(nx_kernel, get_actors).
|
||||
|
||||
@@ -366,6 +369,8 @@ handle_call({publish_to, ActorId, Request}, _From, State) ->
|
||||
end;
|
||||
handle_call({log_tip_for, ActorId}, _From, State) ->
|
||||
{reply, actor_log_tip(ActorId, State), State};
|
||||
handle_call({log_state_for, ActorId}, _From, State) ->
|
||||
{reply, actor_log_state(ActorId, State), State};
|
||||
handle_call(get_actors, _From, State) ->
|
||||
{reply, actors(State), State};
|
||||
handle_call({state_for, ActorId}, _From, State) ->
|
||||
|
||||
@@ -180,6 +180,43 @@ cat > "$TMPFILE" <<'EPOCHS'
|
||||
(epoch 56)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], BK = <<5,6,7,8>>, BKS = [{key_id,k1},{algorithm,ed25519},{value,BK}], BAS = [{public_keys,[[{id,k1},{created,0},{value,BK}]]}], nx_kernel:start_link(alice, AKS, AAS), nx_kernel:add_actor(bob, BKS, BAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,98,111,98,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,98,111,98,10,116,105,112,58,32,48>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; ── Step 4d: outbox listing from log entries + pagination ──────
|
||||
;; Once entries exist, the outbox body includes a "page: N" line
|
||||
;; and one "item: <cid>" line per CID on the page. Default page = 1,
|
||||
;; page_size = 5. Empty actor still degrades to the 4c tip-only body.
|
||||
|
||||
;; After 1 publish: text body has "outbox: alice\ntip: 1\npage: 1\nitem: <cid>\n" prefix
|
||||
(epoch 60)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,97,108,105,99,101,10,116,105,112,58,32,49,10,112,97,103,101,58,32,49,10,105,116,101,109,58,32>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; After 3 publishes: text body's tip=3 and contains item: substrings
|
||||
(epoch 61)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,97,108,105,99,101,10,116,105,112,58,32,51,10,112,97,103,101,58,32,49,10,105,116,101,109,58,32>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; Page 2 with only 3 publishes -> empty items list, degrades to tip-only body
|
||||
(epoch 62)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {query, <<112,97,103,101,61,50>>}, {headers, []}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<105,116,101,109,58>>, B) =:= nomatch andalso http_server:match_prefix(<<111,117,116,98,111,120,58,32,97,108,105,99,101,10,116,105,112,58,32,51>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; 6 publishes, page=1 -> body shows page: 1 and tip: 6
|
||||
(epoch 63)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,97,108,105,99,101,10,116,105,112,58,32,54,10,112,97,103,101,58,32,49,10,105,116,101,109,58,32>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; 6 publishes, page=2 -> body shows page: 2 and item: prefix (1 item, but body byte_size > page-2-with-empty)
|
||||
(epoch 64)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {query, <<112,97,103,101,61,50>>}, {headers, []}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,97,108,105,99,101,10,116,105,112,58,32,54,10,112,97,103,101,58,32,50,10,105,116,101,109,58,32>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; JSON outbox carries items array with 1 entry after 1 publish
|
||||
(epoch 65)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,106,115,111,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<123,34,111,117,116,98,111,120,34,58,34,97,108,105,99,101,34,44,34,116,105,112,34,58,49,44,34,112,97,103,101,34,58,49,44,34,105,116,101,109,115,34,58,91,34>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; SX outbox carries :items list with 1 entry
|
||||
(epoch 66)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, AcceptKey = <<97,99,99,101,112,116>>, AcceptVal = <<97,112,112,108,105,99,97,116,105,111,110,47,115,120>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, [{AcceptKey, AcceptVal}]}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<40,111,117,116,98,111,120,32,34,97,108,105,99,101,34,32,58,116,105,112,32,49,32,58,112,97,103,101,32,49,32,58,105,116,101,109,115,32,40,34>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; Bad ?page= still defaults to page 1
|
||||
(epoch 67)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), AliceTok = <<97,108,105,99,101,45,116,111,107,101,110>>, AuthKey = <<97,117,116,104,111,114,105,122,97,116,105,111,110>>, AliceAuth = <<66,101,97,114,101,114,32,97,108,105,99,101,45,116,111,107,101,110>>, Cfg = [{tokens, [{AliceTok, alice}]}], PostReq = [{method, <<80,79,83,84>>}, {path, http_server:activity_path()}, {headers, [{AuthKey, AliceAuth}]}, {body, <<104,105>>}], http_server:route(PostReq, Cfg, nx_kernel), GetReq = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {query, <<112,97,103,101,61,98,97,100>>}, {headers, []}, {body, <<>>}], R = http_server:route(GetReq, [], nx_kernel), [_, _, {body, B}] = R, http_server:match_prefix(<<111,117,116,98,111,120,58,32,97,108,105,99,101,10,116,105,112,58,32,49,10,112,97,103,101,58,32,49>>, B) =/= nomatch\") :name)")
|
||||
|
||||
;; route/2 path (no kernel arg) still returns the 4a stub — back-compat
|
||||
(epoch 57)
|
||||
(eval "(get (erlang-eval-ast \"AK = <<1,2,3,4>>, AKS = [{key_id,k1},{algorithm,ed25519},{value,AK}], AAS = [{public_keys,[[{id,k1},{created,0},{value,AK}]]}], nx_kernel:start_link(alice, AKS, AAS), Req = [{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97,108,105,99,101,47,111,117,116,98,111,120>>}, {headers, []}, {body, <<>>}], R = http_server:route(Req, []), [_, _, {body, B}] = R, http_server:match_prefix(<<116,105,112,58>>, B) =:= nomatch\") :name)")
|
||||
@@ -243,6 +280,14 @@ check 54 "JSON outbox carries tip field" "true"
|
||||
check 55 "SX outbox carries :tip field" "true"
|
||||
check 56 "Bob outbox tip independent" "true"
|
||||
check 57 "route/2 unchanged (no tip)" "true"
|
||||
check 60 "outbox tip=1 + page=1 + item:" "true"
|
||||
check 61 "outbox tip=3 + page=1 + item:" "true"
|
||||
check 62 "page=2 with 3 items -> empty page" "true"
|
||||
check 63 "outbox tip=6 page=1 has item:" "true"
|
||||
check 64 "outbox tip=6 page=2 has item:" "true"
|
||||
check 65 "JSON body items array shape" "true"
|
||||
check 66 "SX body :items list shape" "true"
|
||||
check 67 "bad ?page= falls back to page 1" "true"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
|
||||
@@ -300,10 +300,19 @@ token; the token now maps to an `:actor_id` rather than a fixed `alice`.
|
||||
delivery); the live handler does a bare `nx_kernel:log_tip_for/1`
|
||||
+ integer guard instead. 8 new cases in `http_multi_actor.sh`
|
||||
(33/33 total).
|
||||
- [ ] **4d** — Per-actor outbox listing reads from the named
|
||||
bucket's log entries via `nx_kernel:actor_log_state/2`, content-
|
||||
negotiates as today (text / json / sx). `?page=N` pagination
|
||||
layered on top using `log:replay/3`.
|
||||
- [x] **4d** — Per-actor outbox listing reads from the named
|
||||
bucket's log entries via new `nx_kernel:log_state_for/1`
|
||||
gen_server export. `actor_outbox_full_response_for/5` renders
|
||||
text / JSON / SX bodies with `:tip`, `:page`, and the page's
|
||||
`:items` CID list. Empty pages degrade to the 4c tip-only body
|
||||
to preserve back-compat with epochs 50-57. `?page=N` pagination
|
||||
parsed at `route/2` time and threaded via Cfg as
|
||||
`{request_query, Q}`; `page_size/0` returns 5 (proof of concept
|
||||
— production picks 20+). 8 new cases in `http_multi_actor.sh`
|
||||
(41/41 total). Substrate gotcha: named recursive funs
|
||||
`fun F(...) -> ... F(...) end` not supported; `binary:matches/2`
|
||||
and `lists:foreach/2` not registered — tests prove behaviour
|
||||
via `match_prefix` substring checks rather than counting.
|
||||
- [ ] **4e** — POST /actors/<id>/inbox stays a 202 stub for 4a-4d.
|
||||
Step 5 lands the real ingestion pipeline (sig verify + inbox-
|
||||
bucket append + projection broadcast).
|
||||
@@ -749,6 +758,23 @@ proceed.
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-06-06** — Step 4d: per-actor outbox listing + pagination.
|
||||
New `nx_kernel:log_state_for/1` gen_server export returns
|
||||
`{ok, LogState}` for an actor. `actor_outbox_response_for/3`
|
||||
now extracts `{Tip, Entries}` via `kernel_actor_log_data/2`,
|
||||
parses `?page=N` from the Req's `:query` field (threaded
|
||||
through Cfg as `{request_query, Q}`), and renders a paged
|
||||
body. Text body adds `page: N\nitem: <cid>\n...`; JSON adds
|
||||
`"page":N,"items":[...]`; SX adds `:page N :items (...)`.
|
||||
Empty pages (out-of-range or actor-with-no-publishes) degrade
|
||||
back to the 4c tip-only shape, preserving epochs 50-57.
|
||||
`page_size/0` is 5 for tests (production picks 20+). 8 new
|
||||
cases in `http_multi_actor.sh` (41/41 total). Conformance
|
||||
761/761. 117/117 across 11 Step-4-adjacent suites. **Gotcha**
|
||||
noted: named recursive funs `fun F(...) -> ... F(...) end`
|
||||
fail with "fun-ref syntax not yet supported"; `binary:matches/2`
|
||||
and `lists:foreach/2` aren't registered in this substrate.
|
||||
|
||||
- **2026-06-06** — Step 4c: route/3 with kernel access.
|
||||
`http_server:route/3(Req, Cfg, Kernel)` folds the kernel
|
||||
reference into Cfg as `{kernel, _}`. Dispatch chain refactored
|
||||
|
||||
Reference in New Issue
Block a user