Lists open at once from what the page keeps and fetches ahead, or with a spinner (#247)

A click on a feed, All Subscriptions or a filter fetched the list and only then drew it, so
nothing answered the click until the server did. The page now keeps the first page of each list
(the newest 40, by view, filter, sort and search), draws a kept one at once and refreshes it
behind, drawing again only if it changed. It fetches ahead: All Subscriptions and the twenty
feeds with the most unread once the page is idle, two at a time, and a feed or filter when the
pointer or a finger reaches it. A feed_row or a read forgets that feed's lists and All
Subscriptions'; the forgotten are fetched again behind. A list with nothing kept shows a
spinner if its answer takes more than 120 ms.

list_shown (#246) is reported when the list first shows, so a kept one reports its few ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 15:16:19 +00:00
parent 62a3241139
commit 6640ca9612
8 changed files with 120 additions and 10 deletions

View File

@@ -125,6 +125,19 @@ test("a file plays from DeadAir's playlist when it has one", () => {
assert.equal(run(`mediaSrc({ id: 7, deadair: 0 })`), '/media/7/deadair.m3u8');
});
test('kept lists: the newest few dozen, and a changed feed forgets its own and All Subscriptions\'', () => {
run('lists.clear()');
for (let i = 0; i < 45; i++) run(`keepList(listKey('f${i}'), {total: 0, entries: []})`);
assert.equal(run('lists.size'), 40, 'never more than LISTS_KEPT');
assert.equal(run(`lists.has(listKey('f0'))`), false, 'the oldest go first');
run(`keepList(listKey('f10'), {total: 1, entries: []})`);
run(`keepList(listKey(':all'), {total: 0, entries: []}); keepList(listKey(':all', 'unread'), {total: 0, entries: []})`);
run(`forgetLists('f10')`);
assert.equal(run(`lists.has(listKey('f10'))`), false);
assert.equal(run(`lists.has(listKey(':all')) || lists.has(listKey(':all', 'unread'))`), false, 'All Subscriptions holds its items too');
assert.equal(run(`lists.has(listKey('f11'))`), true, 'the rest stay');
});
test('a Patreon feed is told by its address', () => {
assert.equal(run(`isPatreon({ url: 'https://www.patreon.com/rss/show?auth=x' })`), true);
assert.equal(run(`isPatreon({ url: 'https://example.com/feed' })`), false);

View File

@@ -2015,3 +2015,22 @@ test('a downloaded file\'s buttons sit on one row in the files pane', async ({ p
const tops = await box.locator('.btn').evaluateAll(bs => bs.map(b => Math.round(b.getBoundingClientRect().top)));
expect(new Set(tops).size, 'play, save, share and delete wrapped onto two rows').toBe(1);
});
test('a list seen before shows at once, and one not yet seen shows a spinner while it loads', async ({ page }) => {
// #247. Every list's answer held back a second and a half, as a slow connection would.
await page.route(/\/api\/(entries|feeds\/[^/]+\/entries)\?/, async route => {
await new Promise(r => setTimeout(r, 1500));
await route.continue();
});
await page.evaluate(() => { lists.clear(); fetching.clear(); });
await page.locator('.place[data-id=":all"]').click();
await expect(page.locator('#eps .listload'), 'waited for, and said so').toBeVisible();
await expect(page.locator('#eps .ep').first()).toBeVisible({ timeout: 10_000 });
await page.locator('#feedlist .feed', { hasText: 'Test Show' }).first().click();
await expect(page.locator('#eps .ep').first()).toBeVisible({ timeout: 10_000 });
// Back to All Subscriptions: drawn from what was kept, before the slow answer could come.
await page.locator('.place[data-id=":all"]').click();
await expect(page.locator('#eps .ep').first()).toBeVisible({ timeout: 500 });
await expect(page.locator('#eps .listload')).toHaveCount(0);
await page.unroute(/\/api\/(entries|feeds\/[^/]+\/entries)\?/);
});