fed-sx-types Phase 3: /types/<cid> route + discovery_type_fetch
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
Wire format for serving + fetching type docs (plans/fed-sx-host-types.md
step 3).
http_server.erl:
- new type_doc Accept format + content type
(application/vnd.fed-sx.type-doc), distinct from actor-doc.
- GET /types/<cid> -> the cached TypeRecord term_codec-encoded, 404 if
not in the peer_types cache. Reads peer_types via a Cfg
{peer_types, peer_types} guard (hardcoded registered atom, mirroring
the actor-doc route's kernel guard).
discovery_type_fetch.erl — sibling of discovery_fetch. make_fetch_fn
produces the fun/2 peer_types:lookup_or_fetch calls: GET
<base>/types/<cid> with the type-doc Accept header, returning the RAW
bytes (peer_types owns the term_codec decode, so the wire format lives
in one place — the route encodes, the cache decodes). Cfg carries
type_url / type_url_fn for TypeCid -> base URL resolution.
Tests: next/tests/peer_types_route.sh (13, in-process route dispatch),
next/tests/discovery_type_fetch.sh (9, closure vs a python type-doc
stub, end-to-end through peer_types:lookup_or_fetch).
No regression: http_accept, http_actors, http_get_format,
discovery_fetch all still green. Conformance 771/771.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
118
next/kernel/discovery_type_fetch.erl
Normal file
118
next/kernel/discovery_type_fetch.erl
Normal file
@@ -0,0 +1,118 @@
|
||||
-module(discovery_type_fetch).
|
||||
-export([make_fetch_fn/0, make_fetch_fn/1,
|
||||
fetch/2,
|
||||
type_doc_url/2,
|
||||
resolve_type_url/2,
|
||||
accept_header/0]).
|
||||
|
||||
%% Live type-doc fetch for peer_types — host-type federation Step 3,
|
||||
%% the sibling of discovery_fetch.erl. peer_types:lookup_or_fetch/3
|
||||
%% calls a Cfg-supplied type_fetch_fn :: fun ((TypeCid, Cfg) -> {ok,
|
||||
%% Bytes} | {error, _}) on a cache miss; this module produces that
|
||||
%% closure for live federation. It GETs <base>/types/<cid> with an
|
||||
%% Accept header that asks for the type-doc format (http_server.erl
|
||||
%% Step 3) and returns the RAW response bytes — peer_types decodes
|
||||
%% them via term_codec into the TypeRecord. (This is the one shape
|
||||
%% difference from discovery_fetch, whose closure returns an already-
|
||||
%% decoded actor-state: there the cache stores the decoded AS, here
|
||||
%% peer_types owns the decode so the type-doc wire format lives in one
|
||||
%% place — the /types/ route encodes, peer_types decodes.)
|
||||
%%
|
||||
%% Cfg shape (parallels discovery_fetch's peer URL resolution):
|
||||
%% {type_url, [{TypeCid, BaseUrl}, ...]}
|
||||
%% {type_url_fn, fun ((TypeCid) -> {ok, BaseUrl} | not_found)}
|
||||
%%
|
||||
%% BaseUrl shape: <<"http://host:port">> (no trailing slash; this
|
||||
%% module appends the path). TypeCid is the type's CID bytes.
|
||||
%%
|
||||
%% Outcomes:
|
||||
%% 2xx -> {ok, Bytes}
|
||||
%% non-2xx -> {error, {status, N}}
|
||||
%% resolver miss -> {error, no_type_url}
|
||||
%% transport -> {error, Reason}
|
||||
|
||||
%% ── Accept header ────────────────────────────────────────────
|
||||
%% "application/vnd.fed-sx.type-doc" — same MIME http_server's
|
||||
%% content_type_for(type_doc) emits, so the Accept negotiation routes
|
||||
%% the served bytes to the term_codec-encoded TypeRecord arm.
|
||||
accept_header() ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
116,121,112,101,45,100,111,99>>.
|
||||
|
||||
%% ── public API ───────────────────────────────────────────────
|
||||
|
||||
%% make_fetch_fn/0 — the fun/2 peer_types:lookup_or_fetch calls. It
|
||||
%% reads the type-URL resolver out of the Cfg passed at call time, so
|
||||
%% the same Cfg threads through peer_types and this closure.
|
||||
make_fetch_fn() ->
|
||||
fun (TypeCid, Cfg) ->
|
||||
case resolve_type_url(TypeCid, Cfg) of
|
||||
{error, R} -> {error, R};
|
||||
{ok, BaseUrl} -> fetch(type_doc_url(BaseUrl, TypeCid), Cfg)
|
||||
end
|
||||
end.
|
||||
|
||||
%% make_fetch_fn/1 — variant that closes over a static Cfg for the
|
||||
%% resolver while still honouring the call-time Cfg for transport.
|
||||
%% Lets a caller bake the type_url map once and reuse the closure.
|
||||
make_fetch_fn(StaticCfg) ->
|
||||
fun (TypeCid, Cfg) ->
|
||||
case resolve_type_url(TypeCid, StaticCfg) of
|
||||
{error, R} -> {error, R};
|
||||
{ok, BaseUrl} -> fetch(type_doc_url(BaseUrl, TypeCid), Cfg)
|
||||
end
|
||||
end.
|
||||
|
||||
fetch(Url, _Cfg) ->
|
||||
AcceptKey = <<97,99,99,101,112,116>>, % "accept"
|
||||
Headers = [{AcceptKey, accept_header()}],
|
||||
try httpc:request(Url, get, Headers, <<>>) of
|
||||
{ok, Status, _H, Body} when Status >= 200, Status < 300 ->
|
||||
{ok, Body};
|
||||
{ok, Status, _H, _B} ->
|
||||
{error, {status, Status}};
|
||||
Other ->
|
||||
{error, {bad_response, Other}}
|
||||
catch
|
||||
error:Reason -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% type_doc_url/2 — <BaseUrl>/types/<cid>. TypeCid is the cid bytes,
|
||||
%% appended verbatim as the path segment (matches the "/types/" prefix
|
||||
%% http_server.erl registers).
|
||||
type_doc_url(BaseUrl, TypeCid) when is_binary(TypeCid) ->
|
||||
%% "/types/" — 7 bytes
|
||||
Prefix = <<47,116,121,112,101,115,47>>,
|
||||
<<BaseUrl/binary, Prefix/binary, TypeCid/binary>>.
|
||||
|
||||
%% resolve_type_url/2 — map a TypeCid to its serving node's base URL.
|
||||
%% type_url_fn (a 1-arity closure) takes precedence over the static
|
||||
%% type_url proplist; absent both -> {error, no_type_url}.
|
||||
resolve_type_url(TypeCid, Cfg) ->
|
||||
case field(type_url_fn, Cfg) of
|
||||
Fn when is_function(Fn, 1) ->
|
||||
case Fn(TypeCid) of
|
||||
{ok, BaseUrl} -> {ok, BaseUrl};
|
||||
_ -> {error, no_type_url}
|
||||
end;
|
||||
_ ->
|
||||
case field(type_url, Cfg) of
|
||||
nil -> {error, no_type_url};
|
||||
Map ->
|
||||
case find_keyed(TypeCid, Map) of
|
||||
{ok, BaseUrl} -> {ok, BaseUrl};
|
||||
_ -> {error, no_type_url}
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
%% ── helpers ──────────────────────────────────────────────────
|
||||
|
||||
field(K, [{K, V} | _]) -> V;
|
||||
field(K, [_ | Rest]) -> field(K, Rest);
|
||||
field(_, []) -> nil.
|
||||
|
||||
find_keyed(_, []) -> {error, not_found};
|
||||
find_keyed(K, [{K, V} | _]) -> {ok, V};
|
||||
find_keyed(K, [_ | Rest]) -> find_keyed(K, Rest).
|
||||
@@ -4,6 +4,7 @@
|
||||
welcome_body/0, capabilities_body/0,
|
||||
capabilities_path/0,
|
||||
match_prefix/2, actors_prefix/0, actor_doc_response/1,
|
||||
types_prefix/0, type_doc_response_for/2,
|
||||
artifacts_prefix/0, artifact_response/1,
|
||||
projections_list_path/0, projections_prefix/0,
|
||||
projections_list_response/0, projection_response/1,
|
||||
@@ -156,7 +157,12 @@ dispatch(<<71, 69, 84>>, Path, F, Cfg) ->
|
||||
{ok, Name} when byte_size(Name) > 0 ->
|
||||
projection_response_for(Name, F);
|
||||
_ ->
|
||||
not_found_response()
|
||||
case match_prefix(types_prefix(), Path) of
|
||||
{ok, Cid} when byte_size(Cid) > 0 ->
|
||||
type_doc_response_for(Cid, Cfg);
|
||||
_ ->
|
||||
not_found_response()
|
||||
end
|
||||
end
|
||||
end
|
||||
end;
|
||||
@@ -289,6 +295,10 @@ artifact_response(Cid) ->
|
||||
Body = <<Pre/binary, Cid/binary, 10>>,
|
||||
ok_response(Body).
|
||||
|
||||
%% "/types/" — 7 bytes: 47 116 121 112 101 115 47 (host-type fed Step 3)
|
||||
types_prefix() ->
|
||||
<<47,116,121,112,101,115,47>>.
|
||||
|
||||
%% "/projections" — 12 bytes (no trailing slash; the list endpoint)
|
||||
projections_list_path() ->
|
||||
<<47,112,114,111,106,101,99,116,105,111,110,115>>.
|
||||
@@ -488,9 +498,20 @@ actor_doc_prefix() ->
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
97,99,116,111,114,45,100,111,99>>.
|
||||
|
||||
%% "application/vnd.fed-sx.type-doc" — 31 bytes (host-type fed Step 3).
|
||||
%% Distinct from actor-doc: the body is a term_codec-encoded
|
||||
%% TypeRecord (peer_types cache entry), not a peer-actor-state.
|
||||
type_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,
|
||||
116,121,112,101,45,100,111,99>>.
|
||||
|
||||
accept_format(nil) -> text;
|
||||
accept_format(<<>>) -> text;
|
||||
accept_format(V) when is_binary(V) ->
|
||||
case match_prefix(type_doc_prefix(), V) of
|
||||
{ok, _} -> type_doc;
|
||||
_ ->
|
||||
case match_prefix(actor_doc_prefix(), V) of
|
||||
{ok, _} -> actor_doc;
|
||||
_ ->
|
||||
@@ -510,6 +531,7 @@ accept_format(V) when is_binary(V) ->
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end;
|
||||
accept_format(_) -> text.
|
||||
|
||||
@@ -586,6 +608,11 @@ 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>>;
|
||||
%% "application/vnd.fed-sx.type-doc" — 31 bytes (host-type fed Step 3).
|
||||
content_type_for(type_doc) ->
|
||||
<<97,112,112,108,105,99,97,116,105,111,110,47,
|
||||
118,110,100,46,102,101,100,45,115,120,46,
|
||||
116,121,112,101,45,100,111,99>>;
|
||||
content_type_for(_) ->
|
||||
content_type_for(text).
|
||||
|
||||
@@ -714,6 +741,42 @@ kernel_actor_state(_Kernel, Id) ->
|
||||
_ -> nil
|
||||
end.
|
||||
|
||||
%% ── host-type fed Step 3: GET /types/<cid> ──────────────────────
|
||||
%%
|
||||
%% Serves a TypeRecord the node has cached (its own published types or
|
||||
%% types fetched from peers) so a federated peer running
|
||||
%% discovery_type_fetch can decode it directly into the shape
|
||||
%% peer_types + the object-schema pipeline stage consume. The wire
|
||||
%% body is term_codec:encode(TypeRecord) under the
|
||||
%% application/vnd.fed-sx.type-doc content type; a cache miss is a 404.
|
||||
%%
|
||||
%% Cid is the path segment after "/types/" (the type's CID bytes). Cfg
|
||||
%% carries `{peer_types, peer_types}` to opt the route into the cache —
|
||||
%% absent (or the gen_server down) short-circuits to 404, matching the
|
||||
%% kernel_actor_state guard for the actor-doc route. This port can't
|
||||
%% dispatch `Mod:Fun` on a variable module, so the registered
|
||||
%% `peer_types` atom is hardcoded; the Cfg field flags "no cache wired".
|
||||
|
||||
type_doc_response_for(Cid, Cfg) ->
|
||||
case type_record_for(Cfg, Cid) of
|
||||
nil -> not_found_response();
|
||||
TR -> ok_response(term_codec:encode(TR), type_doc)
|
||||
end.
|
||||
|
||||
type_record_for(Cfg, Cid) ->
|
||||
case field(peer_types, Cfg) of
|
||||
nil -> nil;
|
||||
_ ->
|
||||
case erlang:whereis(peer_types) of
|
||||
undefined -> nil;
|
||||
_ ->
|
||||
case peer_types:lookup(Cid) of
|
||||
{ok, TR} -> TR;
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
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