Load the feed list in five queries, not six per feed (#94)

GET /api/feeds called feed_summary, http_state, blocklist and unread_count for every feed:
about 950 round trips to Postgres for 160 feeds, 320 ms on every page load. Db::feed_list asks
for the feed rows, entry counts, download counts, the person's unread counts and block lists
once each, and the handler reads from that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 17:24:09 +00:00
parent 448e557272
commit e8fd3fe9ed
3 changed files with 106 additions and 6 deletions

View File

@@ -744,6 +744,7 @@ async fn feeds(
.collect();
let counts = state.ctx.db.subscriber_counts().await?;
let pinned = state.ctx.db.pinned_feeds(user.id).await?;
let mut listed = state.ctx.db.feed_list(user.id).await?;
let mut out = Vec::with_capacity(mine.len());
for sub in &subs {
let (id, feed) = (&sub.id, &sub.cfg);
@@ -751,8 +752,8 @@ async fn feeds(
// the same fallback the scanner uses (`Db::subscribers`).
let up = feed.group.as_deref().and_then(|g| mine.get(g));
let Some(mine) = mine.get(id) else { continue };
let s = state.ctx.db.feed_summary(id).await?;
let st = state.ctx.db.http_state(id).await?;
// A feed not scanned yet has no row: blank, as feed_summary gave it.
let crate::db::FeedListing { summary: s, ttl_mins, blocked, unread } = listed.remove(id).unwrap_or_default();
out.push(FeedRow {
id: id.clone(),
url: feed.url.clone(),
@@ -766,7 +767,7 @@ async fn feeds(
.clone()
.or_else(|| up.and_then(|u| u.keywords.clone()))
.unwrap_or_else(|| feed.keywords.clone()),
blocked: state.ctx.db.blocklist(user.id, id).await?,
blocked,
allow_explicit: mine
.allow_explicit
.or(up.and_then(|u| u.allow_explicit))
@@ -789,11 +790,11 @@ async fn feeds(
.schedule
.as_deref()
.and_then(crate::config::parse_interval),
every_mins: crate::due_after(&cfg, feed, st.ttl_mins) / 60,
every_mins: crate::due_after(&cfg, feed, ttl_mins) / 60,
last_checked: s.last_checked,
next_check: s
.last_checked
.map(|t| t + crate::due_after(&cfg, feed, st.ttl_mins) as i64),
.map(|t| t + crate::due_after(&cfg, feed, ttl_mins) as i64),
failing: s
.error_since
.filter(|since| crate::db::now() - since >= FLAG_AFTER_SECS)
@@ -803,7 +804,7 @@ async fn feeds(
last_error: s.last_error,
entries: s.entries,
downloaded: s.downloaded,
unread: state.ctx.db.unread_count(user.id, id).await?,
unread,
subscribers: counts.get(id).copied().unwrap_or(0),
pinned: pinned.contains(id),
});