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

@@ -940,6 +940,22 @@ test('adding a page adds the feed it links, and a page with no feed is refused',
await expect(page.locator('.feed', { hasText: 'Linked Site' })).toBeVisible({ timeout: 20_000 });
});
test('reading an item updates its feed\'s count without reloading the list', async ({ page }) => {
const unread = page.locator('.feed:not(.group)', { has: page.locator('.badge:not(.zero)') }).first();
await expect(unread).toBeVisible({ timeout: 20_000 });
const id = await unread.getAttribute('data-id');
const badge = page.locator(`.feed[data-id="${id}"] .badge`);
const before = Number(await badge.textContent());
await unread.click();
await page.locator('.tabs button', { hasText: 'Unread' }).click();
await expect(page.locator('.ep').first()).toBeVisible();
const lists = [];
page.on('request', r => { if (new URL(r.url()).pathname === '/api/feeds') lists.push(r.url()); });
await page.locator('.ep').first().click(); // opening an item reads it
await expect(badge).toHaveText(String(before - 1));
expect(lists, 'the row came back with the read, not by reloading the list').toEqual([]);
});
test('a deleted file looks as if it was never downloaded', async ({ page }) => {
// Other people subscribe to Picture Blog by now, so both prompts come; take them.
page.on('dialog', d => d.accept());