diff options
| author | Adam Soltys <[email protected]> | 2026-09-30 09:42:54 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-30 09:42:54 -0700 |
| commit | 93c07bec934e8e8d847349b7792f4cc9089adbb7 (patch) | |
| tree | 098864eccfe77502ae76181aad9f095260bf3771 | |
| parent | e96c445e73d26e946cf2a3d515b5f0ab697ef5f0 (diff) | |
| download | coinosv3-93c07bec934e8e8d847349b7792f4cc9089adbb7.tar.gz coinosv3-93c07bec934e8e8d847349b7792f4cc9089adbb7.zip | |
Topic feeds go back as far as the relays remember, instead of "all caught up" after a handful.
#gardenstr: 5 posts across damus/primal/nos.lol/ours (they prune), 774 once
archive-keeping relays are asked too (ditto, nostr.mom, oxtr, nostrplebs,
nostr21, offchain) — topic feeds now read those. And a page whose posts
were all filtered out (replies, muted, spam) no longer ends the feed:
paging goes on from the oldest event the relays returned, up to three
asks per scroll. The quoted-note wide net gains ditto/nostrplebs/nostr21.
topic-depth-test (/feed/t/gardenstr): 9 posts then "caught up" before,
73+ and still going after.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Du1qT1wbkYwYK6zkGH79Rp
| -rw-r--r-- | src/features/messages.js | 34 | ||||
| -rw-r--r-- | tools/poll-test.js | 2 | ||||
| -rw-r--r-- | tools/topic-depth-test.js | 36 |
3 files changed, 66 insertions, 6 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index b14cb75..6a68c38 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -4093,7 +4093,13 @@ export function messagesFeature(ctx) { }; // posts on a topic come from the big public relays as well as ours: a // hashtag has no author whose outbox we could read - const TOPIC_RELAYS = [...new Set([...NOTE_RELAYS, 'wss://relay.damus.io', 'wss://relay.primal.net', 'wss://nos.lol'])]; + // A topic has no authors whose relays to read, and the big relays prune: + // #gardenstr was 5 posts across damus/primal/nos.lol/ours, 774 once these + // archive-keeping relays were asked too (ditto 500+, nostr.mom 448, + // oxtr 247, nostrplebs 175, nostr21 102, offchain 74 — 2026-09-30). + const TOPIC_ARCHIVE_RELAYS = ['wss://relay.ditto.pub', 'wss://nostr.mom', 'wss://nostr.oxtr.dev', + 'wss://relay.nostrplebs.com', 'wss://nostr21.com', 'wss://offchain.pub']; + const TOPIC_RELAYS = [...new Set([...NOTE_RELAYS, 'wss://relay.damus.io', 'wss://relay.primal.net', 'wss://nos.lol', ...TOPIC_ARCHIVE_RELAYS])]; // ---- lists (NIP-51 follow sets, kind 30000) ------------------------------ // A feed of hand-picked people IS a nostr list. Saved here it is published @@ -4948,6 +4954,13 @@ export function messagesFeature(ctx) { // as disruptive as a live arrival if you were reading halfway down, so it // goes behind the pill too. A first load, or paging older posts onto the // bottom, does not. + // The oldest event the relays handed back, admitted or not: a page of + // replies, muted or spam posts is not the end of the feed — the next ask + // goes on from there. + function sawBack(c, evs) { + for (const e of evs || []) if (e && e.created_at && !(c.seenBack <= e.created_at)) c.seenBack = e.created_at; + c.sawAny = (c.sawAny || 0) + ((evs || []).length); + } async function feedPass(extra = {}, merge = {}, c = feedNow()) { const def = feedDef(c.id); if (!def) return false; @@ -4961,6 +4974,7 @@ export function messagesFeature(ctx) { if (!topics.length && !def.all) return false; const relays = feedRelaysOr(def, def.all ? FIREHOSE_RELAYS : TOPIC_RELAYS); const evs = await queryOn(relays, { kinds: FEED_KINDS, ...tag, limit: FEED_LIMIT, ...extra }, 5000).catch(() => []); + sawBack(c, evs); if (await mergeFeed(evs, merge, c)) { got = true; scheduleRepaint(); } return got; } @@ -4972,6 +4986,7 @@ export function messagesFeature(ctx) { for (let i = 0; i < a.length; i += REQ_AUTHORS) chunks.push(a.slice(i, i + REQ_AUTHORS)); return chunks.map(async (chunk) => { const evs = await queryOn(relays, { kinds: FEED_KINDS, authors: chunk, ...tag, limit: FEED_LIMIT, ...extra }, 5000).catch(() => []); + sawBack(c, evs); if (await mergeFeed(evs, merge, c)) { got = true; scheduleRepaint(); } }); })); @@ -5182,8 +5197,14 @@ export function messagesFeature(ctx) { if (c.status !== 'ready' || c.end) return; const oldest = c.notes.at(-1); if (!oldest || !feedHasQuery(feedDef(c.id))) { c.end = true; return; } - if (await feedPass({ until: oldest.created_at - 1 }, {}, c)) c.shown = Math.min(c.shown + FEED_PAGE, c.notes.length); - else c.end = true; + 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; + } } finally { c.loadingMore = false; if (c === feed) render(); @@ -5406,8 +5427,11 @@ export function messagesFeature(ctx) { // relays, ours — and when none of those has it, a wider net of big public // relays (a quoted note in the wild sat only on nostr.mom, which neither // the reference nor its author's relay list named). - const WIDE_RELAYS = ['wss://nostr.mom', 'wss://relay.nostr.band', 'wss://nostr.wine', 'wss://relay.snort.social', - 'wss://offchain.pub', 'wss://relay.nostr.bg', 'wss://nostr.oxtr.dev', 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net']; + // (the archive-keeping ones hold what the big relays have pruned — a poll + // two days old was only on ditto and nostrplebs by 2026-09-30) + const WIDE_RELAYS = ['wss://nostr.mom', 'wss://relay.ditto.pub', 'wss://relay.nostrplebs.com', 'wss://nostr21.com', + 'wss://nostr.wine', 'wss://relay.snort.social', 'wss://offchain.pub', 'wss://nostr.oxtr.dev', + 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net']; // All at once, first answer wins: asked in turn, a note on a far relay // took longer than the feed waits for a row. function findNote(ref) { diff --git a/tools/poll-test.js b/tools/poll-test.js index e9cb0d6..b88a4db 100644 --- a/tools/poll-test.js +++ b/tools/poll-test.js @@ -15,7 +15,7 @@ const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' const POLL_ID = '000002f7c6a38223a9843d6e93ce8c15a3048bc95003690d74341cf9de372bfc'; const pool = new SimplePool(); -const [poll] = await pool.querySync(['wss://relay.coinos.io', 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net'], { ids: [POLL_ID] }, { maxWait: 6000 }); +const [poll] = await pool.querySync(['wss://relay.coinos.io', 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.ditto.pub', 'wss://relay.nostrplebs.com'], { ids: [POLL_ID] }, { maxWait: 6000 }); if (!poll) { console.log(' ✗ the poll is not on the relays'); process.exit(1); } const labels = poll.tags.filter((x) => x[0] === 'option').map((x) => x[2]); diff --git a/tools/topic-depth-test.js b/tools/topic-depth-test.js new file mode 100644 index 0000000..9876224 --- /dev/null +++ b/tools/topic-depth-test.js @@ -0,0 +1,36 @@ +// A topic feed (#gardenstr) goes back as far as the relays remember: the +// big relays prune, so topics also ask archive-keeping ones, and a page +// whose posts were all filtered out (replies, muted, spam) is not the end. +// Before: ~5 posts, then "You're all caught up". +// +// Run: bun tools/topic-depth-test.js +import puppeteer from 'puppeteer-core'; +import { buildHtml } from '../build.js'; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +let ok = true; +const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 5285, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); +const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +try { + await page.setViewport({ width: 390, height: 844 }); + await page.goto('http://localhost:5285/feed/t/gardenstr', { waitUntil: 'domcontentloaded' }); + const count = () => page.evaluate(() => new Set([...document.querySelectorAll('.notes-feed [data-key]')].map((n) => n.getAttribute('data-key'))).size); + for (let i = 0; i < 60 && (await count()) < 3; i++) await sleep(500); + const first = await count(); + check('the topic opens with posts', first >= 3, first + ' posts'); + let seen = new Set(), ended = false; + const t0 = Date.now(); + while (Date.now() - t0 < 90_000 && seen.size < 60) { + for (const k of await page.evaluate(() => [...document.querySelectorAll('.notes-feed [data-key]')].map((n) => n.getAttribute('data-key')))) seen.add(k); + ended = await page.evaluate(() => document.body.innerText.includes('all caught up')); + if (ended) break; + await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight)); + await sleep(1500); + } + check('scrolling keeps finding older posts', seen.size >= 40, seen.size + ' posts' + (ended ? ', then "caught up"' : '')); + check('...without claiming to be caught up after a handful', !ended || seen.size >= 40); +} finally { await browser.close(); server.stop(true); } +console.log(ok ? '\n✅ a topic goes back' : '\n❌ failed'); +process.exit(ok ? 0 : 1); |
