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
+56 -1
View File
@@ -5,9 +5,22 @@
import { InfiniteQueryObserver } from "@tanstack/react-query";
import { describe, expect, it } from "vitest";
import { NEXT_PAGE } from "./nextPage";
import { mayReadAhead, NEXT_PAGE, nextPageOnClick } from "./nextPage";
import { createQueryClient } from "./queryClient";
function probe(sent: (string | null)[], delay = 5) {
return new InfiniteQueryObserver(createQueryClient(), {
queryKey: ["probe"],
initialPageParam: null as string | null,
queryFn: async ({ pageParam }) => {
sent.push(pageParam);
await new Promise((r) => setTimeout(r, delay));
return { next: `c${sent.length}` };
},
getNextPageParam: (last: { next: string }) => last.next,
});
}
describe("NEXT_PAGE", () => {
it("sends each page once when the read-ahead calls twice in a row", async () => {
const sent: (string | null)[] = [];
@@ -32,4 +45,46 @@ describe("NEXT_PAGE", () => {
expect(observer.getCurrentResult().data?.pages).toHaveLength(2);
unsubscribe();
});
// E31 S5: the join takes ANY fetch in flight. During a background refetch (an invalidation, a
// poll) it reloads the pages already there and appends none — why a read-ahead waits it out.
it("joins a background refetch and appends no page", async () => {
const sent: (string | null)[] = [];
const observer = probe(sent);
const unsubscribe = observer.subscribe(() => {});
await observer.refetch();
void observer.refetch();
await observer.fetchNextPage(NEXT_PAGE);
expect(observer.getCurrentResult().data?.pages).toHaveLength(1);
unsubscribe();
});
});
describe("a read-ahead", () => {
it("waits while the list fetches anything", () => {
expect(mayReadAhead(true, false)).toBe(true);
expect(mayReadAhead(true, true)).toBe(false);
expect(mayReadAhead(false, false)).toBe(false);
});
});
describe("a next page asked for by a click", () => {
it("loads the page even while a background refetch runs", async () => {
const sent: (string | null)[] = [];
const observer = probe(sent);
const unsubscribe = observer.subscribe(() => {});
await observer.refetch();
void observer.refetch();
await observer.fetchNextPage(nextPageOnClick(false));
expect(observer.getCurrentResult().data?.pages).toHaveLength(2);
unsubscribe();
});
it("joins a next page already loading", () => {
expect(nextPageOnClick(true)).toBe(NEXT_PAGE);
});
});