From 00d02cc83954c691d9809461a9fb499e10f686c2 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 3 Oct 2026 21:55:12 -0700 Subject: Media views: one empty answer is not the bottom; a crawl reads on until it finds a few MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single empty relay answer (a timeout, a frozen socket) marked a feed's media finished for a day — 'You're all caught up' after four videos. Now it takes three in a row, old unconfirmed ends are ignored, and a crawl reads up to five pages until it has six new items of the mode. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR --- src/features/messages.js | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) (limited to 'src/features') diff --git a/src/features/messages.js b/src/features/messages.js index ef4819d..8f6eb2d 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -9609,7 +9609,7 @@ export function messagesFeature(ctx) { const key = mediaIdxKey(feedId); let r = mediaIdx.get(key); if (r) return r; - r = { key, images: new Map(), videos: new Map(), checked: new Set(), until: 0, end: false, loaded: false }; + r = { key, images: new Map(), videos: new Map(), checked: new Set(), until: 0, end: false, empty: 0, loaded: false }; mediaIdx.set(key, r); loadMediaIndex(key).then(async (rec) => { if (rec) { @@ -9617,7 +9617,9 @@ export function messagesFeature(ctx) { if (rec.until && (!r.until || rec.until < r.until)) r.until = rec.until; // the bottom of a feed is looked for again after a day (a relay // added, a follow back-filled) - if (rec.end && Date.now() - rec.end < 86_400_000) r.end = r.end || rec.end; + // (only an end confirmed by repeated empty answers: one empty + // answer was a timeout as often as the bottom, and held for a day) + if (rec.end && rec.endSure && Date.now() - rec.end < 86_400_000) r.end = r.end || rec.end; } // kept frames first, so the first paint of the grid is pictures await warmVideoThumbs([...r.videos.values()]); @@ -9630,7 +9632,7 @@ export function messagesFeature(ctx) { clearTimeout(r.saveT); r.saveT = setTimeout(() => { const list = (m) => [...m.values()].sort((a, b) => b.created_at - a.created_at); - saveMediaIndex(r.key, { images: list(r.images), videos: list(r.videos), until: r.until, end: r.end }); + saveMediaIndex(r.key, { images: list(r.images), videos: list(r.videos), until: r.until, end: r.end, endSure: !!r.end }); }, 1500); } // file the posts that carry media; each post is looked at once @@ -9657,18 +9659,28 @@ export function messagesFeature(ctx) { if (!r.loaded || r.end || mediaCrawling) return mediaCrawling || Promise.resolve(); mediaCrawling = (async () => { scheduleRepaint(); - const from = [r.until, c.notes.at(-1)?.created_at].filter(Boolean); - const until = (from.length ? Math.min(...from) : Math.floor(Date.now() / 1000) + 1) - 1; - let oldest = Infinity, saw = 0; - const sink = (ev) => { - saw++; - if (ev.created_at < oldest) oldest = ev.created_at; - noteForSpam(ev); - indexMedia(r, [ev]); - }; - try { await feedPass({ until, limit: 100 }, { sink }, c); } catch {} - if (!saw) r.end = Date.now(); - else r.until = Math.min(oldest, until); + // Clips are rare (one post in a hundred, on a busy relay), so a crawl + // keeps reading pages until it has found a few, or read several. + const kind = mediaMode === 'videos' ? 'videos' : 'images'; + const had = r[kind].size; + for (let pass = 0; pass < 5 && r[kind].size - had < 6; pass++) { + const from = [r.until, c.notes.at(-1)?.created_at].filter(Boolean); + const until = (from.length ? Math.min(...from) : Math.floor(Date.now() / 1000) + 1) - 1; + let oldest = Infinity, saw = 0; + const sink = (ev) => { + saw++; + if (ev.created_at < oldest) oldest = ev.created_at; + noteForSpam(ev); + indexMedia(r, [ev]); + }; + try { await feedPass({ until, limit: 100 }, { sink }, c); } catch {} + if (c.stopped) break; + if (saw) { r.empty = 0; r.until = Math.min(oldest, until); continue; } + // an empty answer is the bottom only when it repeats: a relay that + // timed out, or a socket the phone froze, answers empty too + if (++r.empty >= 3) { r.end = Date.now(); break; } + await new Promise((ok) => setTimeout(ok, 1500 * r.empty)); + } saveMediaIdx(r); })().finally(() => { mediaCrawling = null; scheduleRepaint(); }); return mediaCrawling; -- cgit v1.2.3