From 46181214dd880934f620424376d8aa29ba8794eb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 16 Oct 2026 17:41:20 -0700 Subject: Threads open at once from the replies already in hand, and fill in as the relays answer. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adam, 2026-10-16: "Still seeing a spinner sometimes when I click into a thread." Three things stood between a tap and the conversation. (1) The feed already fetches every reply to each post on screen — the count on the reply button comes from them (watchZaps, kinds 1 and 1111 by #e) — and then kept only their ids. Now the replies themselves are kept (replyEvents: the 400 targets most recently heard from, 60 each) and a thread is seeded from them when it is opened (repliesInHand: those naming the root, and those naming only the reply they answer), so a post that has been on screen opens as the whole conversation, no relay asked; a thread fetched ahead before the count answered takes them when opened, and the open thread takes them when the count fetch lands (adoptRepliesInHand). (2) The loader waited for every relay's EOSE — the slowest relay, 4.5 s at worst — before the thread replaced its spinner; now replies go in as they arrive (queryStreamOn), the page repainted in place, and the root the moment it is found. (3) The spinner stood under whatever was already there while the load ran; now it shows only while there is nothing to read, and "no replies" only once the relays have spoken. And the fetch-ahead fires after 600 ms of reading instead of 1.2 s (a quick reader beat it to the tap), a dozen a minute. tools/thread-instant-test.js (the relays answered in the page): a post read for a moment is fetched ahead, the count answer released only after that entry exists, and the thread opens with its reply 12 ms after the tap, not after its own 6 s ask; a post on screen opens whole in 16 ms with the three replies the count brought, before the relays answer the thread's ask, and their answer adds nothing twice; a post with nothing in hand shows a spinner, then its first reply at ~470 ms with the slow one still on its way, the spinner gone, the slow one landing in place. thread-prefetch-test follows the new dwell and rate; thread-cache, thread-pager, thread-back-scroll, feed-conversations, moderation, feed-live, feed-switch-seed, profile-pager, notif-report-fold and reply-count pass (thread-reply-test's 30 s wait fails on clean HEAD the same way). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp --- src/thread-prefetch.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'src/thread-prefetch.js') diff --git a/src/thread-prefetch.js b/src/thread-prefetch.js index 6d34db6..c21289c 100644 --- a/src/thread-prefetch.js +++ b/src/thread-prefetch.js @@ -1,5 +1,9 @@ // Dwell is measured on mounted rows, including posts taller than the viewport. // Sampling also catches fast movement within a large post (IO alone cannot). +// A post read for half a second is a post that may be opened: its thread is +// fetched then (it was 1.2 s, which a quick reader beat to the tap), one at +// a time, a dozen a minute. +const DWELL_MS = 600, PER_MINUTE = 12; export function createThreadPrefetch({ load, key, cached, doc = globalThis.document, nav = globalThis.navigator, now = Date.now, schedule = setTimeout, cancel = clearTimeout }) { const seeds = new Map(), dwell = new Map(); @@ -23,12 +27,12 @@ export function createThreadPrefetch({ load, key, cached, doc = globalThis.docum if (!d || Math.abs(r.top - d.top) > 25) d = { since: time, top: r.top }; d.top = r.top; dwell.set(id, d); - if (time - d.since >= 1200) ready.push(seed); + if (time - d.since >= DWELL_MS) ready.push(seed); } for (const id of seeds.keys()) if (!mounted.has(id)) seeds.delete(id); for (const id of dwell.keys()) if (!visible.has(id)) dwell.delete(id); starts = starts.filter((t) => time - t < 60000); - if (!busy && starts.length < 6 && ready.length) { + if (!busy && starts.length < PER_MINUTE && ready.length) { const seed = ready[0], token = generation; busy = true; starts.push(time); // The loader inserts synchronously, so clicks share its in-flight work. -- cgit v1.2.3