diff options
| author | Adam Soltys <[email protected]> | 2026-10-16 20:48:44 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-16 20:48:44 -0700 |
| commit | edc9d03fc94f5059e98ee453d19116adf70d03ae (patch) | |
| tree | afac0bcd30cb8014f3ae0f20e4f8da219f296b05 /src/features/messages.js | |
| parent | 33c721898be82fb50c38db7fa6d67114b10a20a7 (diff) | |
| download | coinosv3-edc9d03fc94f5059e98ee453d19116adf70d03ae.tar.gz coinosv3-edc9d03fc94f5059e98ee453d19116adf70d03ae.zip | |
The feed pages to the reader's appetite, and asks the relays for older posts ahead of running out.
Appetite: the screens the reader has scrolled by their own hand over the last ten seconds (a scroll within a moment of an input — a page held still under an insert above is not travel). Five screens in that time is someone flying through: the page grows from ten posts to twenty, thirty, forty at once, the lead above the foot of the feed at which the next page is asked for grows the same way (from four screens to sixteen), and each relay round trip asks for proportionally more (30 → up to 100 per chunk). Standing still ten seconds brings the ordinary page back. (Adam, 2026-10-16: "could we load posts faster if it seems like the user is scrolling a lot? … try always loading more posts and loading them sooner when we're further away from the bottom".)
The relays run ahead of the rows now. The older page used to be asked for only once every post in hand was on screen, and shown only once the whole pass had answered — so the reader who reached the end of what we held waited a relay round trip, up to the five seconds the slowest relay in the pass is given. A pass for older posts (fetchOlderAhead) starts as soon as what we hold beyond the shown rows thins to a page or two (more with the appetite), its rows warming at the door while the reader is still on the posts before them; the showing step (loadOlderFeed) takes from hand and only waits on a pass already underway when the hand is empty. The in-memory window keeps up to fifty posts beyond the shown rows for that (it kept one page).
tools/feed-appetite-test.js: an ordinary pace pages ten at a time with the relays asked while posts are still in hand, so the end of what was held finds more already there; two flights of eleven screens page twenty then thirty at once, from further above the foot than the ordinary lead, with the relays asked for 60 and 90 per trip; ten seconds still and the page is ten again. The .notes-feed card carries data-shown / data-held for it. Pre-existing failures unchanged: feed-live "picture already decoded" and feed-streaming "tap reveals the oldest ready new post" fail on a clean worktree of ca3b61e too; feed-stability's two known ones.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01J1Kon8bxJoHRpJdSzshCkp
Diffstat (limited to 'src/features/messages.js')
| -rw-r--r-- | src/features/messages.js | 98 |
1 files changed, 77 insertions, 21 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index ffdf0e3..a40f7d5 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -4855,6 +4855,7 @@ export function messagesFeature(ctx) { // DOWN, and that must not count as having gone up to look at them. const y = window.scrollY || 0, up = y < lastFeedY; lastFeedY = y; + if (!ui.profilePk && Date.now() - lastInputAt < 2500) noteScrollTrail(y); // the reader's own travel (not a held-still insert) // Scrolling recounts the pill: posts you've gone up past drop off it, // and once none are left above you the notice is done. if (!ui.profilePk && feed && feed.ahead?.size && up && !pillFrame) pillFrame = requestAnimationFrame(() => { @@ -4867,7 +4868,7 @@ export function messagesFeature(ctx) { else if (n !== feed.pillN) render(); }); if (!ui.profilePk && feed?.deferred?.length) admitFeed(feed.deferred, feed); - if (window.innerHeight + window.scrollY < (document.documentElement.scrollHeight || 0) - (ui.profilePk ? 600 : Math.max(2400, window.innerHeight * 3))) return; + if (window.innerHeight + window.scrollY < (document.documentElement.scrollHeight || 0) - (ui.profilePk ? 600 : feedLead())) return; if (ui.profilePk) loadOlderNotes(ui.profilePk).catch(() => {}); else loadOlderFeed().catch(() => {}); }, { passive: true }); @@ -7051,7 +7052,7 @@ export function messagesFeature(ctx) { } const result = mergeFeedWindow(c.notes, c.shown, add, { rows: rows.map((r) => ({ id: r.getAttribute('data-key'), top: r.getBoundingClientRect().top, bottom: r.getBoundingClientRect().bottom })), - height: typeof window === 'undefined' ? 0 : window.innerHeight, keep: FEED_KEEP, page: FEED_PAGE, + height: typeof window === 'undefined' ? 0 : window.innerHeight, keep: FEED_KEEP, page: FEED_PAGE * (1 + APPETITE_MAX), // room for the pass fetched ahead holdHead: headBusy(), }); c.deferred = result.deferred; @@ -7506,36 +7507,91 @@ export function messagesFeature(ctx) { if (!g || g.getBoundingClientRect().bottom < window.innerHeight * 3) (mediaMode === 'articles' ? crawlArticles : crawlMedia)(c).catch(() => {}); return; } + fetchOlderAhead(c); // the relays, meanwhile, if what we hold beyond the rows is thin if (c.win && c.win.bottom) return; // rows below are held back by the window, not by paging const rows = document.querySelectorAll('.notes-feed > .row[data-key]'); const last = rows[rows.length - 1]; - if (last && last.getBoundingClientRect().bottom < window.innerHeight * 4) loadOlderFeed().catch(() => {}); + if (last && last.getBoundingClientRect().bottom < feedLead()) loadOlderFeed().catch(() => {}); }, 100); } + // ---- the reader's appetite ------------------------------------------- + // How hungrily the feed is being scrolled: the screens travelled, by the + // reader's own hand, over the last ten seconds. A page of posts is read + // at a screen or two in that time; five screens in it is someone flying + // through, and the feed answers by paging more posts at a time and + // asking for them from further up — up to four pages and four times the + // lead at once. (Adam, 2026-10-16: "could we load posts faster if it + // seems like the user is scrolling a lot? … try always loading more + // posts and loading them sooner when we're further away from the + // bottom".) The trail counts only scrolls within a moment of an input, + // so a page held still under an insert above doesn't look like travel. + const APPETITE_MAX = 4, APPETITE_MS = 10_000, APPETITE_SCREENS = 5; + const scrollTrail = []; // [at, scrollY] + function noteScrollTrail(y) { + const now = Date.now(); + scrollTrail.push([now, y]); + while (scrollTrail.length && now - scrollTrail[0][0] > APPETITE_MS) scrollTrail.shift(); + } + function appetite() { + const now = Date.now(); + while (scrollTrail.length && now - scrollTrail[0][0] > APPETITE_MS) scrollTrail.shift(); + if (scrollTrail.length < 2) return 1; + const H = (typeof window !== 'undefined' && window.innerHeight) || 800; + const down = scrollTrail[scrollTrail.length - 1][1] - scrollTrail[0][1]; + return Math.max(1, Math.min(APPETITE_MAX, 1 + Math.floor(down / (APPETITE_SCREENS * H)))); + } + // how far above the foot of the feed the next page is asked for + const feedLead = () => Math.max(3000, ((typeof window !== 'undefined' && window.innerHeight) || 800) * 4) * appetite(); + // The relays run AHEAD of the rows. The older page used to be asked for + // only once every post in hand was on screen, and shown only once the + // whole pass had answered — so the reader who reached the end of what we + // held waited a relay round trip, up to the five seconds the slowest + // relay in the pass is given. Now a pass for older posts starts as soon + // as what we hold beyond the shown rows thins to a page or two (more + // with the appetite), its rows warming at the door while the reader is + // still on the posts before them; the showing step takes from hand. + function fetchOlderAhead(c) { + if (c.fetching) return c.fetching; + if (c.end || c.booting || c.stopped || c.status !== 'ready') return null; + if (c.notes.length - c.shown >= FEED_PAGE * (1 + appetite())) return null; + const oldest = c.notes.at(-1); + if (!oldest || !feedHasQuery(feedDef(c.id))) { c.end = true; return null; } + c.fetching = (async () => { + let until = oldest.created_at - 1; + const limit = Math.min(100, FEED_LIMIT * appetite()); // a hungrier reader is fetched more per round trip + for (let tries = 0; tries < 3; tries++) { + c.sawAny = 0; + if (await feedPass({ until }, {}, c, FEED_KINDS, limit)) return true; + // nothing older at all (or no step back): that really is the end + if (!c.sawAny || !(c.seenBack <= until)) { c.end = true; return false; } + until = c.seenBack - 1; + } + return false; + })().catch(() => false).finally(() => { + c.fetching = null; + if (c === feed && !c.stopped) { scheduleRepaint(); prepareFeedAhead(c); } + }); + return c.fetching; + } async function loadOlderFeed() { const c = feedNow(); if (c.booting || c.loadingMore || (c.shown >= c.notes.length && (c.end || c.status !== 'ready'))) return; c.loadingMore = true; if (c === feed) render(); // the footer says so while it happens try { - if (c.shown < c.notes.length) { - const end = Math.min(c.shown + FEED_PAGE, c.notes.length); - const boundary = c.notes[end - 1].id; - await notesReady(c.notes.slice(c.shown, Math.min(end + FEED_PAGE, c.notes.length)), { parent: feedReplies(c) }); - c.shown = Math.max(c.shown, c.notes.findIndex((e) => e.id === boundary) + 1); - return; - } - if (c.status !== 'ready' || c.end) return; - const oldest = c.notes.at(-1); - if (!oldest || !feedHasQuery(feedDef(c.id))) { c.end = true; return; } - let until = oldest.created_at - 1; - for (let tries = 0; tries < 3; tries++) { - c.sawAny = 0; - if (await feedPass({ until }, {}, c)) { c.shown = Math.min(c.shown + FEED_PAGE, c.notes.length); return; } - // nothing older at all (or no step back): that really is the end - if (!c.sawAny || !(c.seenBack <= until)) { c.end = true; return; } - until = c.seenBack - 1; + fetchOlderAhead(c); + if (c.shown >= c.notes.length) { + // nothing in hand: the pass underway (started just now, if need be) brings the next page + if (!c.fetching) return; + await c.fetching; + if (c.shown >= c.notes.length) return; } + const page = FEED_PAGE * appetite(); + const end = Math.min(c.shown + page, c.notes.length); + const boundary = c.notes[end - 1].id; + await notesReady(c.notes.slice(c.shown, Math.min(end + page, c.notes.length)), { parent: feedReplies(c) }); + c.shown = Math.max(c.shown, c.notes.findIndex((e) => e.id === boundary) + 1); + fetchOlderAhead(c); // and the relays, if that thinned what we hold } finally { c.loadingMore = false; if (c === feed) render(); @@ -13376,7 +13432,7 @@ export function messagesFeature(ctx) { ? h('div', { class: 'small faint', style: 'text-align:center;padding:12px 0' }, t('feedEmpty')) : mediaMode === 'articles' ? articleList(c) : !listView() ? mediaGrid(c) - : h('div', { class: 'card col notes-feed', style: 'gap:0', 'data-booting': c.booting ? '1' : undefined }, ...rows), + : h('div', { class: 'card col notes-feed', style: 'gap:0', 'data-booting': c.booting ? '1' : undefined, 'data-shown': c.shown, 'data-held': c.notes.length }, ...rows), // The foot of the feed says what is happening down there: older // posts on their way, or nothing older left to fetch. (Older pages // prepare offscreen, so the line is mostly seen by a reader who |
