fed-sx-m2: Step 10c — peer-actor doc fetch + cache (+ 11 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 49s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 49s
Closes Step 10 (10a discovery + 10b webfinger + 10c fetch). New
next/kernel/discovery_fetch.erl produces a 1-arity FetchFn closure
suitable for peer_actors:lookup_or_fetch_srv/2, completing the
discovery half that Step 5c's peer_actors cache stubbed out.
discovery_fetch API:
make_fetch_fn(Cfg) -> fun((PeerId) -> {ok, AS} | {error, _})
fetch(Url, Cfg) -> {ok, AS} | {error, _}
actor_doc_url(BaseUrl, PeerAtom) -> <Base>/actors/<peer>
accept_header/0 -> <<"application/vnd.fed-sx.actor-doc">>
decode_body(Body) -> {ok, AS} | {error, bad_actor_doc}
Closure GETs <base>/actors/<peer> via the Step 8e BIF with
Accept = application/vnd.fed-sx.actor-doc, decodes the response
body via term_codec:decode/1, returns the peer-actor-state
proplist (currently [{public_keys, [...]}]) in the shape
envelope:verify_signature consumes.
Cfg reuses dispatch_http's :peer_url / :peer_url_fn resolution so
a single Cfg threads through both delivery (8f) and discovery (10c).
Server side: http_server.erl extended to serve the same MIME.
- accept_format/1 matches application/vnd.fed-sx.actor-doc first
via the new actor_doc_prefix/0 — content negotiation atom is
`actor_doc`.
- content_type_for(actor_doc) emits the MIME on outbound.
- actor_doc_response_for/3 kernel-aware arm: with kernel + actor
-> 200 + term_codec:encode of nx_kernel:state_for/1 result.
Unknown actor -> not_found_response/0. Other formats fall
through to the existing /2 stub variants.
- actor_get/3 route dispatch threads Cfg to the /3 arm.
Port quirks documented:
* This Erlang doesn't support Mod:Fun(X) dispatch on a variable
module — kernel_actor_state/2 hardcodes nx_kernel; the Cfg
:kernel field is just a "no kernel wired" -> nil flag.
* nx_kernel:actor_state/1 is the LEGACY single-bucket accessor
that takes State (not ActorId); the server-side variant we
want is state_for/1 (gen_server:call wrapper). Easy mismatch,
documented in the comment.
Outcome mapping:
2xx + decodable body -> {ok, AS}
2xx + bad body -> {error, bad_actor_doc}
non-2xx -> {error, {status, N}}
resolver miss -> {error, no_peer_url}
transport -> {error, Reason} (BIF re-raises)
Test: next/tests/discovery_fetch.sh 11/11
Server side (in-process via http_server:actor_doc_response_for):
- Accept negotiation
- kernel + actor -> 200 + decodable body w/ :public_keys
- unknown actor -> 404
Closure side (live HTTP against background python stub returning
hand-crafted term_codec bytes):
- URL construction <base>/actors/X
- fetch live -> {ok, AS}
- make_fetch_fn closure -> {ok, AS} via static :peer_url map
- missing peer -> {error, no_peer_url}
- 404 path -> {error, {status, 404}}
- peer_actors:lookup_or_fetch/3 caches the result
Test setup note: Python term_codec encoder uses ELEMENT COUNT
(not byte length) for l/t headers — see encode/1 in term_codec.erl
which does integer_to_list(length(T)). Easy bug, documented in the
test's python source.
No-regression gates green: Erlang conformance 761/761,
httpc_request 10/10, dispatch_http 10/10, http_listen_bif 5/5,
peer_actors 19/19, discovery 12/12, http_accept 13/13,
http_actors 13/13.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
capabilities_body_for/1,
|
||||
content_type_for/1, ok_response/2,
|
||||
cid_response_for/2, post_activity_response_for/1,
|
||||
actor_doc_response_for/2, artifact_response_for/2,
|
||||
actor_doc_response_for/2, actor_doc_response_for/3,
|
||||
artifact_response_for/2,
|
||||
projection_response_for/2, projections_list_response_for/1,
|
||||
actor_outbox_response_for/2, actor_outbox_response_for/3,
|
||||
actor_inbox_get_response_for/2,
|
||||
@@ -172,9 +173,9 @@ dispatch(_, _, _, _) ->
|
||||
|
||||
actor_get(Rest, F, Cfg) ->
|
||||
case split_first_slash(Rest) of
|
||||
{Id, <<>>} -> actor_doc_response_for(Id, F);
|
||||
{Id, <<>>} -> actor_doc_response_for(Id, F, Cfg);
|
||||
{Id, Sub} -> actor_subresource_get(Id, Sub, F, Cfg);
|
||||
Id -> actor_doc_response_for(Id, F)
|
||||
Id -> actor_doc_response_for(Id, F, Cfg)
|
||||
end.
|
||||
|
||||
%% 111 117 116 98 111 120 = "outbox"
|
||||
@@ -481,21 +482,31 @@ sx_prefix() ->
|
||||
cbor_prefix() ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,99,98,111,114>>.
|
||||
|
||||
%% "application/vnd.fed-sx.actor-doc" — 32 bytes (Step 10c)
|
||||
actor_doc_prefix() ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
97,99,116,111,114,45,100,111,99>>.
|
||||
|
||||
accept_format(nil) -> text;
|
||||
accept_format(<<>>) -> text;
|
||||
accept_format(V) when is_binary(V) ->
|
||||
case match_prefix(activity_json_prefix(), V) of
|
||||
{ok, _} -> activity_json;
|
||||
case match_prefix(actor_doc_prefix(), V) of
|
||||
{ok, _} -> actor_doc;
|
||||
_ ->
|
||||
case match_prefix(json_prefix(), V) of
|
||||
{ok, _} -> json;
|
||||
case match_prefix(activity_json_prefix(), V) of
|
||||
{ok, _} -> activity_json;
|
||||
_ ->
|
||||
case match_prefix(sx_prefix(), V) of
|
||||
{ok, _} -> sx;
|
||||
case match_prefix(json_prefix(), V) of
|
||||
{ok, _} -> json;
|
||||
_ ->
|
||||
case match_prefix(cbor_prefix(), V) of
|
||||
{ok, _} -> cbor;
|
||||
_ -> text
|
||||
case match_prefix(sx_prefix(), V) of
|
||||
{ok, _} -> sx;
|
||||
_ ->
|
||||
case match_prefix(cbor_prefix(), V) of
|
||||
{ok, _} -> cbor;
|
||||
_ -> text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -564,6 +575,17 @@ content_type_for(sx) ->
|
||||
content_type_for(cbor) ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
99,98,111,114>>;
|
||||
%% "application/vnd.fed-sx.actor-doc" — 32 bytes. Step 10c content
|
||||
%% type for term_codec-encoded peer-actor docs; the federation fetch
|
||||
%% layer (discovery_fetch.erl) uses this Accept header to ask for a
|
||||
%% peer's :public_keys (and v3+ profile fields) in a wire-decodable
|
||||
%% form. Distinct from application/vnd.fed-sx.activity (dispatch_http
|
||||
%% Step 8f) because the body is a peer-actor-state proplist, not a
|
||||
%% signed activity envelope.
|
||||
content_type_for(actor_doc) ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
97,99,116,111,114,45,100,111,99>>;
|
||||
content_type_for(_) ->
|
||||
content_type_for(text).
|
||||
|
||||
@@ -660,6 +682,38 @@ actor_doc_response_for(Id, cbor) ->
|
||||
actor_doc_response_for(Id, _) ->
|
||||
actor_doc_response(Id).
|
||||
|
||||
%% Step 10c kernel-aware variant. The `actor_doc` format negotiates
|
||||
%% to a term_codec-encoded peer-actor-state proplist (currently just
|
||||
%% `[{public_keys, [...]}]`) so a federated peer running
|
||||
%% discovery_fetch.erl can decode it directly into the shape
|
||||
%% peer_actors and envelope:verify_signature consume. Other formats
|
||||
%% fall through to the /2 stub variants.
|
||||
|
||||
actor_doc_response_for(Id, actor_doc, Cfg) ->
|
||||
case kernel_actor_state(field(kernel, Cfg), Id) of
|
||||
nil -> not_found_response();
|
||||
AS -> ok_response(term_codec:encode(AS), actor_doc)
|
||||
end;
|
||||
actor_doc_response_for(Id, F, _Cfg) ->
|
||||
actor_doc_response_for(Id, F).
|
||||
|
||||
%% kernel_actor_state/2 — bridge to nx_kernel:state_for/1 (the
|
||||
%% server-side variant of actor_state/2). Cfg carries the kernel
|
||||
%% module atom (currently always `nx_kernel`); Id is a binary so
|
||||
%% we round-trip through list_to_atom. This port's Erlang doesn't
|
||||
%% support `Mod:Fun(X)` dispatch on a variable module, so we
|
||||
%% hardcode nx_kernel (the only kernel module in play); the Cfg
|
||||
%% field exists to flag "no kernel wired" -> nil short-circuit.
|
||||
%% nx_kernel:actor_state/1 is the legacy single-bucket accessor
|
||||
%% that takes State, not ActorId — wrong shape here.
|
||||
kernel_actor_state(nil, _Id) -> nil;
|
||||
kernel_actor_state(_Kernel, Id) ->
|
||||
Atom = list_to_atom(binary_to_list(Id)),
|
||||
case nx_kernel:state_for(Atom) of
|
||||
{ok, AS} -> AS;
|
||||
_ -> nil
|
||||
end.
|
||||
|
||||
%% ── Step 4a: per-actor sub-resource stubs ──────────────────────
|
||||
%% Per design §16.1 each actor has /outbox /inbox /followers
|
||||
%% /following routes. v1 returns text-stub bodies so route resolution
|
||||
|
||||
Reference in New Issue
Block a user