Expand description
SQLite persistence layer (via sqlx, runtime queries).
FeatherReader keeps the source of truth for what a user follows and their
read-position in the user’s own atproto PDS (as community.lexicon.rss.*
records). This module is the local per-DID cache + debounce
buffer: a single SQLite file that holds
feeds+entries— a shared cache of feed metadata and articles, keyed by feed URL / feed-native GUID and shared across every DID that follows the same feed (many users on one instance don’t multiply fetch load), andentry_state+read_cursor— per-DID read/star state and the per-feed read cursor that the (v1.1) batched flusher syncs up to the PDS.
All queries here are runtime queries (sqlx::query / sqlx::query_as),
not the compile-time query! macros — so the crate builds with no
DATABASE_URL and no offline metadata. Schema creation is idempotent
(CREATE TABLE IF NOT EXISTS) and runs inside init.
Errors propagate as anyhow::Result; nothing in the non-test paths panics.
Structs§
- Entry
- A cached article/item belonging to a
Feed. Shared cache (not per-DID). - Entry
List Row - One row of a LIST view — deliberately without
content_html. - Entry
State - Per-
(did, entry)read/star state — the fast in-session working copy that the batched flusher later syncs to the PDS as a per-feed read cursor. - Failing
Feed - One failing feed, named, for the ADMIN view only.
- Feed
- A cached syndication feed, shared across all DIDs that subscribe to its URL.
- Network
Stat - One relay’s observation of how many repos hold a collection
(
design/NETWORK-SPEC.md§4.3). A projection: droppable, rebuildable from the network, and never read by anything on the reading path. - NewEntry
- New-entry payload for
insert_entries(id is assigned by SQLite,fetched_atdefaults to “now” when not supplied). - NewFeed
- New-feed payload for
upsert_feed(id is assigned by SQLite). - Poll
Health - Aggregate poll health, for the public stats page.
- Purge
Counts - The row counts purged by
purge_did_data, for a confirmable success message and for assertions in tests. - Read
Cursor - Per-
(did, feed_url)read cursor — the local mirror of the PDScommunity.lexicon.rss.readStaterecord plus flush bookkeeping.
Enums§
- Auto
Vacuum - A database’s
auto_vacuummode. - List
View - Which list
list_entries(and its siblings) is producing. - Redeem
Error - Typed failure modes for
redeem_code. Distinct variants so the web layer can map each to the right user-facing message / HTTP status without string matching. Everything else (a real SQLite error) still propagates asanyhow::Errorout of theResult. - Starred
Identities - The
(url, guid)identity pairs of every cached starred entry for a DID. - Vacuum
Migration - What
migrate_to_incremental_vacuumdid.
Constants§
- ADOPTION_
STAT_ KEY - The
network_statkey the relay adoption probe writes under. - REDACTED_
DID - Sentinel written into
beta_access.granted_bywhen the granting DID deletes its data: the column isNOT NULL, so we redact rather than NULL it. Keeps the grantee’s seat valid while removing the departed DID’s back-reference.
Functions§
- auto_
vacuum_ mode - Read the database’s
auto_vacuummode. - bump_
feed_ errors - Record a poll FAILURE for a feed: bump its
consecutive_errorsby one and return the NEW count. The count drives the exponential poll backoff, so a persistently-failing feed spaces its retries out toward the ceiling instead of hammering the 5-minute floor forever. Reset to 0 byreset_feed_errorson any success/304. - clear_
cursor_ dirty - Clear the
dirtyflag on a cursor after a successful PDS flush — but ONLY if the row still carries the exactflushed_updated_atsnapshot we flushed. - clear_
star_ by_ identity - Clear
did’s star on any cached entry matchingurlorguid, ignoring the subscription projection. Returns the number ofentry_staterows changed. - compact_
cursor - Fold ids already covered by a high-water-mark into
read_through, so the exception set stops growing. Returns the newread_throughwhen it advanced. - count_
active_ codes - Count
active, unexpired invite codes — the outstanding-but-unredeemed seats a bot has already promised. Added tocount_beta_accessthis is the “seats committed” figure the bot mint path (POST /bot/claims) checks against the cap, so it doesn’t over-promise more claims than seats remain (the redeem-time cap inredeem_codeis the hard backstop; this avoids telling a follower “you’re in” for a seat that will be full by the time they claim it). - count_
beta_ access - Count the beta seats currently granted — the numerator checked against the configured cap on redeem.
- count_
entries_ for_ view - How many entries the same scope + view would return, unpaged. Used for the
“N entries” heading and to decide whether a next-page link is warranted —
both of which used to read
entries.len()off a fully materialized list. - count_
feeds - The number of distinct feeds in the shared cache. Backs the global feeds ceiling checked before a brand-new feed is inserted.
- count_
subscriptions_ for_ did - The number of feeds a
didcurrently subscribes to (itssub_refrows). Backs the per-DID subscription cap enforced at the add/import paths. - db_
size_ bytes - The used size of the SQLite database, in bytes, computed as
(page_count - freelist_count) * page_size. Backs the DB-size watermark that disables new polling. - did_
subscribes_ to_ entry - Whether
didcurrently subscribes to the feedfeed_idowns (i.e. asub_refrow exists). The authorization primitive behind every per-DID scoped read/mutation. - dirty_
cursors - due_
feeds - The scheduler’s hot query: feeds whose
next_pollis due (<= as_of, or never polled), oldest-due first.as_ofis an RFC3339 timestamp. - ensure_
seed - Seed the admin-bootstrap DIDs: for each, insert a
beta_accessrow (granted_by = 'admin') if one does not already exist. Idempotent — an existing seat is left untouched. Returns how many new seats were created. - expire_
old_ codes - Sweep: flip every
activecode whoseexpires_atis in the past toexpired. Returns the number of codes expired. Called periodically by the scheduler. - failing_
feeds - Every currently-failing feed with its recorded cause, worst first.
- feeds_
for_ did - The feeds a
didcurrently subscribes to, per itssub_refprojection. Used by the PDS-unreachable fallback inresolve_subscriptionsto render the sidebar from the caller’s OWN last-known subscriptions (fail closed) rather than every cached feed. - find_
active_ code_ for_ did - The
codeof an outstanding (active, unexpired) invite minted FOR the followerintended_did, if one exists — the app-side idempotency lookup forPOST /bot/claims.Some(code)means “return this existing code, do NOT mint a second”;Nonemeans “no live code for this DID — mint one”. - generate_
invite_ code - Generate a random, unguessable invite code of the form
FEATHER-XXXXXXXX. - get_
cursor - Fetch a single read cursor, if present.
- get_
feed_ by_ url - Fetch a feed by its URL, if present.
- grant_
access - Grant a beta seat directly (admin / seed path — no code consumed). Idempotent
on
did(re-granting updates the row rather than erroring). - has_
beta_ access - Whether a DID currently holds a beta seat.
- init
- Open the per-DID SQLite cache described by
Config(itsdb_path), run schema creation, and return the pool. - init_
schema - Run the idempotent schema creation. Split out so callers/tests can (re)apply it against an already-open pool.
- init_
url - insert_
entries - Insert a batch of entries for
feed_id, deduping on(feed_id, guid), then trim the feed to at mostcrate::config-configuredmax_entries_per_feedrows (newest by published date) so one firehose feed can’t fill the disk. - is_
intended_ active_ conflict - Does this error chain represent the partial-unique-index conflict raised when
a SECOND active claim is minted for a DID that already has one
(
idx_invite_codes_intended_active)? The web layer uses this to recover from a lost mint race (S4): on a conflict it re-reads the winner’s code instead of 500-ing. Matches on the sqlxDatabaseerror’s UNIQUE-constraint code (SQLite 2067 / primary 19) AND the offending COLUMN in the message (invite_codes.intended_did— SQLite names the column(s), not the index), so an unrelated constraint violation (e.g. thecodePRIMARY KEY) is NOT swallowed. - latest_
network_ stat - The highest observation for
keyacross every relay — the number to surface (design/NETWORK-SPEC.md§4.1: relays disagree; show the max).Nonewhen no probe has ever succeeded. - list_
entries - One page of a list view, newest-published first, scoped to
did’s subscriptions (sub_ref) and optionally narrowed tofeed_ids. - list_
entry_ ids - The ordered entry ids for a scope + view — the same ordering
list_entriesrenders, used for the reader’s prev/next links. - mark_
cursor_ pds_ created - Mark a cursor’s PDS
readStaterecord as CREATED after the flush that first created it, so subsequent flushes emit anupdateinstead of anothercreate. Idempotent; a no-op if the row is gone. - mark_
feed_ due - Make a feed due for polling on the next tick.
- mark_
feed_ read - Mark every entry of a feed read (or unread) for a DID in one statement —
backs the “mark-all-read (per feed)” action. Also projects the change into
the feed’s per-DID
ReadCursor(dirty=1) so the batched flusher syncs the new read-state to the PDS. - mark_
read - Mark a single entry read/unread for a DID, upserting the per-DID state row
and stamping
updated_at. Preserves any existingstarredbit. Also projects the change into the per-(did, feed_url)ReadCursorand marks itdirtyso the batched flusher pushes it to the PDS (see [project_entry_into_cursor]). - mark_
starred - Star/unstar a single entry for a DID (upsert, preserving
read). - migrate_
to_ incremental_ vacuum - Move a populated database from
auto_vacuum = NONEtoINCREMENTAL. - mint_
code - Mint a new
activeinvite code owned bycreator_did, expiringttl_secsfrom now. Returns the generated code string. The browser/admin path leaves the bot idempotency key (intended_did) NULL; seemint_code_for_didfor the bot path that records the target follower. - mint_
code_ for_ did - Like
mint_codebut records the followerintended_didthe code is minted FOR, so a laterPOST /bot/claimsfor the same DID can return the SAME code (seefind_active_code_for_did) rather than minting a duplicate. This is the app-side idempotency backstop that survives a bot-host state loss. - parked_
readstate_ dids - The flusher’s hot query: every cursor with
dirty = 1for a DID — the ones whose read-state changed since the last batched PDS flush. How many DIDs hold read-state that cannot currently be flushed: dirty cursors with no OAuth session to send them with. - poll_
health - Compute
PollHealthas ofnow(RFC3339, seconds precision — the same format the scheduler writes, so the comparisons are lexicographic). - prune_
old_ entries - Delete entries whose age exceeds the retention window — the shared cache’s
rolling window — except those a reader has starred or not yet read. “Age” is
COALESCE(published, fetched_at)so an UNDATED entry falls back to when it was fetched (never NULL) rather than being treated as infinitely old.entry_statecascades via itsON DELETE CASCADEFK. - purge_
did_ data - Delete all local rows owned by
didin a single transaction: the per-DID read/star state (entry_state), per-feed read cursors (read_cursor), the subscription projection (sub_ref), the closed-beta seat (beta_access), and any invite codes this DID created (invite_codes). The sharedfeeds/entriescache is intentionally left intact — it is deduped and not owned by any single DID. - reclaim
- Reclaim freed pages so the database file (and its used-page accounting) can actually shrink after a retention/prune sweep DELETEs rows.
- record_
network_ stat - Record one relay’s observation, keyed by
(key, source)so each relay’s number is kept separately (non-archival relays legitimately disagree). - redeem_
code - Atomically redeem an invite code for
did, granting a beta seat. - replace_
sub_ refs - Replace the per-DID subscription projection (
sub_ref) fordidwith exactlyfeed_ids, in one transaction. - reset_
feed_ errors - Reset a feed’s
consecutive_errorsto 0 after a successful poll (or a 304). A no-op UPDATE if the row is missing. - set_
next_ poll - Schedule a feed’s next poll
delayfrom now. - starred_
identities - subscribed_
feed_ ids - The feed ids a
didcurrently subscribes to (itssub_refrows). - unread_
counts_ by_ feed - Unread counts per
feed_idfor a DID — the sidebar’s per-feed badges. - upsert_
cursor - Insert or update a per-
(did, feed_url)read cursor, stampingupdated_at. The write path for local mark-read updates (and the seam a login-time PDS merge would use, once that is wired). - upsert_
feed - Insert a feed by URL, or update its metadata if the URL already exists. Returns the feed’s row id (existing or newly assigned).
Type Aliases§
- Pool
- The SQLite connection pool type the rest of the crate refers to as
Pool. A thin alias overSqlitePoolsocrate::AppStateand the web layer name one stable type; if the backend ever changes, this is the single place to swap it.