-module(discovery_fetch). -export([make_fetch_fn/1, fetch/2, actor_doc_url/2, decode_body/1, accept_header/0]). %% Live peer-actor-doc fetch for peer_actors — Step 10c per design %% §13.6. The peer_actors gen_server already exposes %% lookup_or_fetch_srv/2(PeerId, FetchFn) where FetchFn is a %% 1-arity closure that returns {ok, PeerAS} | {error, Reason} on %% cache miss. For tests we wire a fake FetchFn that returns a %% pre-baked AS; for live federation we wire the closure this %% module produces — it GETs /actors/ with an Accept %% header that asks for the actor_doc format %% (http_server.erl Step 10c), decodes the response body via %% term_codec, and returns the AS proplist. %% %% Cfg shape (reuses dispatch_http's peer URL resolution so a %% single Cfg threads through both delivery and discovery): %% {peer_url, [{PeerId, BaseUrl}, ...]} %% {peer_url_fn, fun ((PeerId) -> {ok, BaseUrl} | not_found)} %% %% BaseUrl shape: <<"http://host:port">> (no trailing slash; this %% module appends the path). PeerId is the actor atom. %% %% Outcomes: %% 2xx + decodable body -> {ok, PeerAS} %% 2xx + bad body -> {error, bad_actor_doc} %% non-2xx -> {error, {status, N}} %% resolver miss -> {error, no_peer_url} %% transport -> {error, Reason} %% %% Cache write semantics live in peer_actors:lookup_or_fetch/3 — %% successful fetches store; errors do NOT poison so callers can %% retry on transients. %% ── Accept header ──────────────────────────────────────────── %% "application/vnd.fed-sx.actor-doc" — same MIME the http_server %% content_type_for(actor_doc) emits, so the Accept negotiation %% in accept_format/1 routes the peer's response to the term_codec %% serializer 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, 97,99,116,111,114,45,100,111,99>>. %% ── public API ─────────────────────────────────────────────── make_fetch_fn(Cfg) -> fun (PeerId) -> case dispatch_http:resolve_peer_url(PeerId, Cfg) of {error, R} -> {error, R}; {ok, BaseUrl} -> fetch(actor_doc_url(BaseUrl, PeerId), 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 -> decode_body(Body); {ok, Status, _H, _B} -> {error, {status, Status}}; Other -> {error, {bad_response, Other}} catch error:Reason -> {error, Reason} end. %% actor_doc_url/2 — /actors/. PeerId is the actor %% atom; rendered to a binary via its name (matches the same path %% layout http_server.erl uses for the route registration at %% prefix "/actors/"). actor_doc_url(BaseUrl, PeerId) when is_atom(PeerId) -> PeerBin = list_to_binary(atom_to_list(PeerId)), %% "/actors/" — 8 bytes Prefix = <<47,97,99,116,111,114,115,47>>, <>. %% decode_body/1 — round the wire body back through term_codec. %% Returns {ok, AS} on a proplist-shaped decode (matching the %% peer-actor-state schema), {error, bad_actor_doc} otherwise. decode_body(Body) -> case term_codec:decode(Body) of {ok, AS, _} when is_list(AS) -> {ok, AS}; _ -> {error, bad_actor_doc} end.