fix(ui): a read-ahead waits out a background refetch, and a revealed row is mounted before it is focused

NEXT_PAGE joins ANY fetch in flight: a read-ahead during an invalidation
or a poll joined that refetch and appended no page, and nothing asked
again until the reader moved the range (the channel page during a
backfill). The read-ahead now waits while the list fetches anything
(mayReadAhead, re-run on isFetching); a Load older click cancels a poll
instead (nextPageOnClick).

A page turn to a revealed row restarted the progressive mount at 30
rows; Channels focuses that row in the next commit, and at index 40 of
a 50-row page it was not there, so focus fell to <body>. The mount now
reaches through the revealed row.
This commit is contained in:
2026-10-02 23:42:31 +02:00
parent ac53a3b757
commit 15ec67b029
11 changed files with 165 additions and 28 deletions
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
// Every infinite list's read-ahead waits while the list fetches ANYTHING and runs again when the fetch
// settles (E31 S5). A read-ahead that only checks `isFetchingNextPage` calls during a background
// refetch, joins it through NEXT_PAGE and loads no page: the grid stalls at its loaded end until the
// reader scrolls the range. The jsdom virtualizer measures nothing, so the wiring is pinned in source.
const files = import.meta.glob(["../components/**/*.tsx", "!../components/**/*.test.tsx"], {
query: "?raw",
import: "default",
eager: true,
});
const source = (name: string) => {
const hit = Object.entries(files).find(([path]) => path.endsWith(`/${name}`));
if (!hit) throw new Error(`no ${name}`);
return hit[1] as string;
};
describe("an infinite list's read-ahead", () => {
it.each(["ui/VirtualGrid.tsx", "PlexBrowse.tsx"])(
"%s gates on mayReadAhead and re-runs on isFetching",
(file) => {
const src = source(file);
expect(src).toMatch(/mayReadAhead\(hasNextPage, isFetching\)/);
expect(src).toMatch(/\[[^\]]*\bisFetching\b[^\]]*fetchNextPage\]\);/);
expect(src).not.toMatch(/hasNextPage && !isFetchingNextPage/);
},
);
it("the feed hands the grid the query's isFetching", () => {
expect(source("Feed.tsx")).toMatch(/isFetching=\{query\.isFetching\}/);
});
});