Send a changed feed's row to the page instead of it reloading the list (#105)

After a feed was checked, failed or downloaded a file, and after every item read, the page
fetched /api/feeds whole, about 60 ms for 160 rows, though one row had changed. The live event
stream now knows who is connected and, after an event that changes a feed, sends that person
its row (feed_row), built by the same code as the list (feed_rows, with Db::feed_list asked for
one feed). Marking an item read answers with the feed's row. The page puts the row in place
and redraws once a frame. A routine skip of a feed not due sends nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 20:41:43 +00:00
parent 7a134b810f
commit c7f13eea2f
7 changed files with 107 additions and 25 deletions

View File

@@ -338,8 +338,14 @@ impl Db {
/// What the feed list shows of every feed, for one person, in five queries whatever the
/// number of feeds. Asked feed by feed (feed_summary, http_state, blocklist, unread_count)
/// it was six round trips a feed, about 950 for 160 feeds and 320 ms a page load (#94).
pub async fn feed_list(&self, user_id: i64) -> Result<std::collections::HashMap<String, FeedListing>> {
pub async fn feed_list(
&self,
user_id: i64,
// One feed only, for the row a live update sends (`web::feed_rows`).
only: Option<&str>,
) -> Result<std::collections::HashMap<String, FeedListing>> {
use std::collections::HashMap;
let one = || sea_orm::Value::from(only.map(str::to_owned));
let counts = |sql: &'static str, args: Vec<sea_orm::Value>| async move {
self.rows(sql, args)
.await?
@@ -347,20 +353,29 @@ impl Db {
.map(|r| Ok((r.try_get::<String>("", "feed_id")?, r.try_get::<i64>("", "n")?)))
.collect::<Result<HashMap<_, _>>>()
};
let entries = counts("SELECT feed_id, count(*) AS n FROM entries GROUP BY feed_id", vec![]).await?;
let downloaded =
counts("SELECT feed_id, count(*) AS n FROM enclosures WHERE path IS NOT NULL GROUP BY feed_id", vec![])
.await?;
let entries = counts(
"SELECT feed_id, count(*) AS n FROM entries
WHERE (CAST($1 AS TEXT) IS NULL OR feed_id = $1) GROUP BY feed_id",
vec![one()],
)
.await?;
let downloaded = counts(
"SELECT feed_id, count(*) AS n FROM enclosures
WHERE path IS NOT NULL AND (CAST($1 AS TEXT) IS NULL OR feed_id = $1) GROUP BY feed_id",
vec![one()],
)
.await?;
let unread = counts(
"SELECT e.feed_id, count(*) AS n FROM entries e
JOIN subscriptions sub ON sub.user_id = $1 AND sub.feed_id = e.feed_id
LEFT JOIN entry_state s
ON s.user_id = $1 AND s.feed_id = e.feed_id AND s.guid = e.guid
WHERE NOT coalesce(s.read, false)
AND (CAST($2 AS TEXT) IS NULL OR e.feed_id = $2)
AND NOT EXISTS (SELECT 1 FROM hidden h
WHERE h.user_id = $1 AND h.feed_id = e.feed_id AND h.guid = e.guid)
GROUP BY e.feed_id",
vec![user_id.into()],
vec![user_id.into(), one()],
)
.await?;
let mut blocked: HashMap<String, Vec<String>> = blocklists::Entity::find()
@@ -370,7 +385,11 @@ impl Db {
.into_iter()
.map(|b| (b.feed_id, keywords(Some(b.words)).unwrap_or_default()))
.collect();
Ok(feeds::Entity::find()
let mut rows = feeds::Entity::find();
if let Some(id) = only {
rows = rows.filter(feeds::Column::Id.eq(id));
}
Ok(rows
.all(&self.orm)
.await?
.into_iter()
@@ -1964,7 +1983,10 @@ mod tests {
INSERT INTO blocklists (user_id, feed_id, words) VALUES (1,'g','[\"spoiler\"]');",
).await
.unwrap();
let list = db.feed_list(1).await.unwrap();
let list = db.feed_list(1, None).await.unwrap();
let g = db.feed_list(1, Some("g")).await.unwrap();
assert_eq!(g.keys().collect::<Vec<_>>(), ["g"]);
assert_eq!((g["g"].unread, g["g"].summary.entries, g["g"].blocked.clone()), (1, 1, vec!["spoiler".to_string()]));
for id in ["f", "g"] {
let one = &list[id];
let s = db.feed_summary(id).await.unwrap();