fed-sx-m2: Step 4c — route/3 with kernel access + 8 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 24s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 24s
http_server:route/3(Req, Cfg, Kernel) is the new extended entry
point: folds the kernel reference (typically the registered
nx_kernel atom) into Cfg as {kernel, Kernel}. route/2 is
unchanged and stays the M1 surface.
The dispatch chain gained Cfg threading all the way down:
dispatch/3 -> dispatch/4 (M, P, F, Cfg)
actor_get/2 -> actor_get/3 (Rest, F, Cfg)
actor_subresource_get/3 -> /4 (Id, Sub, F, Cfg)
actor_outbox_response_for/3 (new) reads :kernel from Cfg and,
when the kernel atom is registered AND the actor exists, renders
'tip: <N>' alongside the actor id in text / JSON / SX content-
negotiated bodies. Unknown actors or unregistered kernels fall
back to the 4a stub.
Inbox / followers / following handlers accept Cfg but ignore it
for now — they layer real state lookup in 4d/4e/Step 5+.
Substrate gotcha logged in the Progress log: try/of/catch around
gen_server:call(nx_kernel, _) deadlocks in this port's scheduler
(probably the catch frame's mask defers reply delivery). The
live kernel_log_tip/2 helper does a bare call + integer guard
instead. nx_kernel_multi.sh already proves bare gen_server:call
into the same kernel works correctly.
8 new cases in next/tests/http_multi_actor.sh (33/33 total):
- route/3 with registered kernel: outbox body includes tip=0
- tip advances after POST publish through route/3 + token map
- unknown actor (ghost) falls back to 4a stub (no tip:)
- unregistered kernel ref falls back to stub
- JSON Accept renders {"outbox":"alice","tip":0}
- SX Accept renders (outbox "alice" :tip 0)
- Bob's outbox tip stays 0 while Alice publishes (per-actor)
- route/2 path unchanged: no tip field in body
Conformance 761/761. 121/121 across 10 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, bootstrap_start, actor_lifecycle).
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
-module(http_server).
|
||||
-export([start/1, start/2]).
|
||||
-export([route/1, route/2, ok_response/1, not_found_response/0,
|
||||
-export([route/1, route/2, route/3, ok_response/1, not_found_response/0,
|
||||
welcome_body/0, capabilities_body/0,
|
||||
capabilities_path/0,
|
||||
match_prefix/2, actors_prefix/0, actor_doc_response/1,
|
||||
@@ -17,7 +17,8 @@
|
||||
cid_response_for/2, post_activity_response_for/1,
|
||||
actor_doc_response_for/2, artifact_response_for/2,
|
||||
projection_response_for/2, projections_list_response_for/1,
|
||||
actor_outbox_response_for/2, actor_inbox_get_response_for/2,
|
||||
actor_outbox_response_for/2, actor_outbox_response_for/3,
|
||||
actor_inbox_get_response_for/2,
|
||||
actor_followers_response_for/2, actor_following_response_for/2,
|
||||
actor_inbox_post_response/0, accepted_response/1,
|
||||
split_first_slash/1]).
|
||||
@@ -60,10 +61,10 @@ start(Port, Cfg) ->
|
||||
route(Req) ->
|
||||
route(Req, []).
|
||||
|
||||
%% route/2 — Cfg proplist carries optional `:publish_token` (binary)
|
||||
%% for POST /activity auth. Other state (logs, projections, etc.) is
|
||||
%% not yet threaded through — POST /activity returns a stub 200
|
||||
%% once auth succeeds; real outbox:publish glue lands separately.
|
||||
%% route/2 — Cfg proplist carries optional `:publish_token` /
|
||||
%% `:tokens` (POST /activity auth) and optional `:kernel`
|
||||
%% (per-actor handlers — Step 4c). route/3 is sugar that puts
|
||||
%% Kernel into Cfg.
|
||||
route(Req, Cfg) ->
|
||||
M = field(method, Req),
|
||||
P = field(path, Req),
|
||||
@@ -76,33 +77,43 @@ 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)
|
||||
dispatch(M, P, F, Cfg)
|
||||
end.
|
||||
|
||||
%% route/3 — Step 4c convenience entry. Kernel is an opaque
|
||||
%% reference (typically the registered `nx_kernel` atom). It's
|
||||
%% folded into Cfg under `:kernel` so handlers can look it up
|
||||
%% without a separate threading argument.
|
||||
route(Req, Cfg, Kernel) ->
|
||||
route(Req, [{kernel, Kernel} | Cfg]).
|
||||
|
||||
%% Backward-compat /2 wrapper — defaults to text format. Route
|
||||
%% computes Format from the Accept header and calls dispatch/3
|
||||
%% directly; dispatch/2 is kept for callers that don't have a
|
||||
%% format in scope.
|
||||
%% computes Format from the Accept header and calls dispatch/4
|
||||
%% directly; dispatch/2 and dispatch/3 are kept for callers that
|
||||
%% don't have a format / Cfg in scope.
|
||||
dispatch(M, P) ->
|
||||
dispatch(M, P, text).
|
||||
dispatch(M, P, text, []).
|
||||
|
||||
dispatch(M, P, F) ->
|
||||
dispatch(M, P, F, []).
|
||||
|
||||
%% 71 69 84 = "GET" | 47 = "/"
|
||||
dispatch(<<71, 69, 84>>, <<47>>, _F) ->
|
||||
dispatch(<<71, 69, 84>>, <<47>>, _F, _Cfg) ->
|
||||
ok_response(welcome_body());
|
||||
%% GET /.well-known/sx-capabilities — Format threaded through
|
||||
dispatch(<<71, 69, 84>>,
|
||||
<<47,46,119,101,108,108,45,107,110,111,119,110,
|
||||
47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>, F) ->
|
||||
47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>, F, _Cfg) ->
|
||||
ok_response(capabilities_body_for(F));
|
||||
%% GET /projections — list stub. Comes before the /projections/{name}
|
||||
%% prefix clause because the bare path has no trailing slash.
|
||||
dispatch(<<71, 69, 84>>, <<47,112,114,111,106,101,99,116,105,111,110,115>>, F) ->
|
||||
dispatch(<<71, 69, 84>>, <<47,112,114,111,106,101,99,116,105,111,110,115>>, F, _Cfg) ->
|
||||
projections_list_response_for(F);
|
||||
%% GET /actors/{id}[/sub] or /artifacts/{cid} or /projections/{name}
|
||||
dispatch(<<71, 69, 84>>, Path, F) ->
|
||||
dispatch(<<71, 69, 84>>, Path, F, Cfg) ->
|
||||
case match_prefix(actors_prefix(), Path) of
|
||||
{ok, Rest} when byte_size(Rest) > 0 ->
|
||||
actor_get(Rest, F);
|
||||
actor_get(Rest, F, Cfg);
|
||||
_ ->
|
||||
case match_prefix(artifacts_prefix(), Path) of
|
||||
{ok, Cid} when byte_size(Cid) > 0 ->
|
||||
@@ -118,40 +129,41 @@ dispatch(<<71, 69, 84>>, Path, F) ->
|
||||
end;
|
||||
%% POST /actors/{id}/inbox — peer-side delivery (Step 4a returns
|
||||
%% 202 Accepted stub; Step 5 lands the real ingestion pipeline).
|
||||
dispatch(<<80, 79, 83, 84>>, Path, _F) ->
|
||||
dispatch(<<80, 79, 83, 84>>, Path, _F, _Cfg) ->
|
||||
case match_prefix(actors_prefix(), Path) of
|
||||
{ok, Rest} when byte_size(Rest) > 0 ->
|
||||
actor_post(Rest);
|
||||
_ ->
|
||||
not_found_response()
|
||||
end;
|
||||
dispatch(_, _, _) ->
|
||||
dispatch(_, _, _, _) ->
|
||||
not_found_response().
|
||||
|
||||
%% actor_get/2 — Rest is the part after "/actors/". If it has no
|
||||
%% actor_get/3 — Rest is the part after "/actors/". If it has no
|
||||
%% inner slash, it's the bare actor doc. Otherwise dispatch on the
|
||||
%% sub-segment.
|
||||
%% sub-segment. Cfg flows through so sub-resource handlers can
|
||||
%% read `:kernel` for per-actor state lookup (Step 4c).
|
||||
|
||||
actor_get(Rest, F) ->
|
||||
actor_get(Rest, F, Cfg) ->
|
||||
case split_first_slash(Rest) of
|
||||
{Id, <<>>} -> actor_doc_response_for(Id, F);
|
||||
{Id, Sub} -> actor_subresource_get(Id, Sub, F);
|
||||
{Id, Sub} -> actor_subresource_get(Id, Sub, F, Cfg);
|
||||
Id -> actor_doc_response_for(Id, F)
|
||||
end.
|
||||
|
||||
%% 111 117 116 98 111 120 = "outbox"
|
||||
actor_subresource_get(Id, <<111,117,116,98,111,120>>, F) ->
|
||||
actor_outbox_response_for(Id, F);
|
||||
actor_subresource_get(Id, <<111,117,116,98,111,120>>, F, Cfg) ->
|
||||
actor_outbox_response_for(Id, F, Cfg);
|
||||
%% 105 110 98 111 120 = "inbox"
|
||||
actor_subresource_get(Id, <<105,110,98,111,120>>, F) ->
|
||||
actor_subresource_get(Id, <<105,110,98,111,120>>, F, _Cfg) ->
|
||||
actor_inbox_get_response_for(Id, F);
|
||||
%% 102 111 108 108 111 119 101 114 115 = "followers"
|
||||
actor_subresource_get(Id, <<102,111,108,108,111,119,101,114,115>>, F) ->
|
||||
actor_subresource_get(Id, <<102,111,108,108,111,119,101,114,115>>, F, _Cfg) ->
|
||||
actor_followers_response_for(Id, F);
|
||||
%% 102 111 108 108 111 119 105 110 103 = "following"
|
||||
actor_subresource_get(Id, <<102,111,108,108,111,119,105,110,103>>, F) ->
|
||||
actor_subresource_get(Id, <<102,111,108,108,111,119,105,110,103>>, F, _Cfg) ->
|
||||
actor_following_response_for(Id, F);
|
||||
actor_subresource_get(_, _, _) ->
|
||||
actor_subresource_get(_, _, _, _) ->
|
||||
not_found_response().
|
||||
|
||||
actor_post(Rest) ->
|
||||
@@ -657,6 +669,76 @@ actor_outbox_response_for(Id, _) ->
|
||||
Pre = <<111,117,116,98,111,120,58,32>>,
|
||||
ok_response(<<Pre/binary, Id/binary, 10>>).
|
||||
|
||||
%% actor_outbox_response_for/3 — Step 4c kernel-aware variant. When
|
||||
%% Cfg carries a `:kernel` reference *and* the kernel has the actor,
|
||||
%% include "tip: <N>\n" after the bare body so callers can verify
|
||||
%% the route landed on the right bucket. Falls back to the /2 stub
|
||||
%% otherwise — same shape, same content-negotiation arms.
|
||||
|
||||
actor_outbox_response_for(Id, F, Cfg) ->
|
||||
case field(kernel, Cfg) of
|
||||
nil ->
|
||||
actor_outbox_response_for(Id, F);
|
||||
Kernel ->
|
||||
case kernel_log_tip(Kernel, Id) of
|
||||
nil ->
|
||||
actor_outbox_response_for(Id, F);
|
||||
Tip ->
|
||||
actor_outbox_with_tip_response_for(Id, F, Tip)
|
||||
end
|
||||
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
|
||||
%% isn't present. Catches everything so a stale Cfg can't break the
|
||||
%% handler.
|
||||
|
||||
kernel_log_tip(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) -> N;
|
||||
_ -> nil
|
||||
end
|
||||
end;
|
||||
kernel_log_tip(_, _) -> nil.
|
||||
|
||||
actor_outbox_with_tip_response_for(Id, text, Tip) ->
|
||||
%% "outbox: <Id>\ntip: <Tip>\n"
|
||||
Pre = <<111,117,116,98,111,120,58,32>>, % "outbox: "
|
||||
Tipp = <<10,116,105,112,58,32>>, % "\ntip: "
|
||||
TipBin = list_to_binary(integer_to_list(Tip)),
|
||||
Body = <<Pre/binary, Id/binary, Tipp/binary, TipBin/binary, 10>>,
|
||||
ok_response(Body);
|
||||
actor_outbox_with_tip_response_for(Id, json, Tip) ->
|
||||
Pre = <<123,34,111,117,116,98,111,120,34,58,34>>,
|
||||
Mid = <<34,44,34,116,105,112,34,58>>, % '","tip":'
|
||||
Suf = <<125,10>>, % '}\n'
|
||||
TipBin = list_to_binary(integer_to_list(Tip)),
|
||||
Body = <<Pre/binary, Id/binary, Mid/binary, TipBin/binary, Suf/binary>>,
|
||||
ok_response(Body, json);
|
||||
actor_outbox_with_tip_response_for(Id, activity_json, Tip) ->
|
||||
Pre = <<123,34,111,117,116,98,111,120,34,58,34>>,
|
||||
Mid = <<34,44,34,116,105,112,34,58>>,
|
||||
Suf = <<125,10>>,
|
||||
TipBin = list_to_binary(integer_to_list(Tip)),
|
||||
Body = <<Pre/binary, Id/binary, Mid/binary, TipBin/binary, Suf/binary>>,
|
||||
ok_response(Body, activity_json);
|
||||
actor_outbox_with_tip_response_for(Id, sx, Tip) ->
|
||||
Pre = <<40,111,117,116,98,111,120,32,34>>, % '(outbox "'
|
||||
Mid = <<34,32,58,116,105,112,32>>, % '" :tip '
|
||||
Suf = <<41,10>>, % ')\n'
|
||||
TipBin = list_to_binary(integer_to_list(Tip)),
|
||||
Body = <<Pre/binary, Id/binary, Mid/binary, TipBin/binary, Suf/binary>>,
|
||||
ok_response(Body, sx);
|
||||
actor_outbox_with_tip_response_for(Id, _, Tip) ->
|
||||
actor_outbox_with_tip_response_for(Id, text, Tip).
|
||||
|
||||
%% "inbox: " — 7 bytes
|
||||
actor_inbox_get_response_for(Id, text) ->
|
||||
Pre = <<105,110,98,111,120,58,32>>,
|
||||
|
||||
Reference in New Issue
Block a user