Expand description
Read-only queries against the public atproto network.
Where crate::atproto reads and writes one user’s own PDS repo, this
module asks the public relays a single question: how many repos on the
network hold a given collection? Today that collection is
community.lexicon.rss.subscription, and the answer is FeatherReader’s
adoption metric — the one number that says whether the portability claim on
every page is being exercised by anybody but us.
Three properties are load-bearing, and each is a rule rather than an
intention (design/NETWORK-SPEC.md §4):
- It is never a source of truth. The result is a projection: drop the
network_stattable and the next probe rebuilds it. Nothing in the reader path reads it, and no reader surface may depend on it. - It counts, it does not collect. The relay answers with a list of DIDs;
we read
repos.len()and drop the page. Persisting the DID list would build a durable register of “accounts that use an RSS reader” on our disk, for a feature whose entire output is an integer. - The number is a LOWER BOUND, not a census. Since sync v1.1 relays are non-archival: a relay’s index only covers hosts it actually crawls, so a PDS no relay crawls is invisible to it. Two relays can therefore disagree; we query every configured host, record each observation separately, surface the max, and log the disagreement. Any copy derived from this number must say “at least”, never “exactly”.
Why the SSRF guard, when the relay host is operator-configured? Not
because the operator is the threat — they can already edit the code. It earns
its place for three other reasons. The shared reqwest::Client has no
timeouts, so a hung relay would pin a background task forever, while
crate::net’s per-hop pinned client bounds both the total request and the
idle read. The shared client also follows up to ten redirects with no
re-validation, whereas crate::net::guarded_get_no_privacy follows at most
five and re-checks scheme + resolved IP on each — a relay that 302s (DNS
takeover, a misconfigured proxy, a captive portal on a self-hoster’s network)
is the realistic rebinding path. And it is the coherent choice: the same
milestone that routes PdsClient::list_records through the guard should not
open a fresh unguarded http.get() next door. It is free besides —
crate::USER_AGENT and the body cap come along with it.
Structs§
- Adoption
Observation - One relay’s answer to “how many repos hold this collection?”.
- Adoption
Report - The outcome of one probe run across every configured relay.
- Relay
Client - A thin, read-only client over the public relays.
- Relay
Failure - One relay that did not answer, and why.
Enums§
- Relay
Error - Why a relay query failed. Every variant carries
host, so a fan-out failure is attributable without the caller threading context back in.
Constants§
- DEFAULT_
PAGE_ LIMIT - Repos requested per page. The relay’s documented ceiling is 1000; 500 keeps a page body small (~30 KB) while making the whole current network one page.
- DEFAULT_
RELAY_ HOSTS - The public Bluesky relays queried by default, in order. Lives here (mirroring
crate::atproto::DEFAULT_PLC_DIRECTORY) socrate::configimports the network fact rather than re-typing the hostnames. - LIST_
REPOS_ BY_ COLLECTION - The relay XRPC method that answers “which repos hold this collection?”.
- MAX_
PAGES - Hard cap on pages walked per host: 25 000 repos at
DEFAULT_PAGE_LIMIT, i.e. ~25 000× today’s network. Hitting it means something went wrong (a cursor loop, a wrong collection) — the run is recorded astruncated, so the number reads as “at least N”.
Functions§
- normalize_
relay_ host - Normalize one configured relay host into an origin URL.