diff options
| author | Adam Soltys <[email protected]> | 2026-11-02 09:31:22 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-11-02 09:31:22 -0700 |
| commit | 071f8db3bd7799abe4a6acf7c32f107eef0d00c2 (patch) | |
| tree | 3ccfff716224a3e9801064cb8439fe94b9fd86c3 | |
| parent | df4d28e29776349c0b2aef6d5367cace6aa25822 (diff) | |
| download | coinosv3-071f8db3bd7799abe4a6acf7c32f107eef0d00c2.tar.gz coinosv3-071f8db3bd7799abe4a6acf7c32f107eef0d00c2.zip | |
A quoted note, or the post a reply answers, is asked for from the near relays first — the reference's own hints, ours, its author's — and from the wide net of public and archive relays only once those have come back empty, or after 1.2 s if they are slow about it. Asked all at once, as before, every quote and every reply's parent went out to a dozen relays, and in the Conversations view, where each row has a parent, half of all REQs in twenty seconds on the live app — three hundred, to 47 relays — were lookups of notes our own relays answered in the first beat. tools/relay-budget-test.js adds a far relay: the six quotes our relay holds are never asked of it, and the one only it holds is still found thereHEADmaster
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMan6TFgisGKUDhdStLUvs
| -rw-r--r-- | src/features/messages.js | 33 | ||||
| -rw-r--r-- | tools/relay-budget-test.js | 17 |
2 files changed, 39 insertions, 11 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 79526b8..b7982c6 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -8584,20 +8584,35 @@ export function messagesFeature(ctx) { } } } - // All at once, first answer wins: asked in turn, a note on a far relay - // took longer than the feed waits for a row. + // The near relays first — the reference's hints, ours, the author's own + // — and the wide net only once those have come back empty, or after a + // moment if they are slow about it (asked in turn, a note on a far relay + // took longer than the feed waits for a row; asked all at once, every + // quoted note and every reply's parent went to a dozen relays, and the + // Conversations view spent half its REQs — three hundred in twenty + // seconds — on notes our own relays answered in the first beat). + const WIDE_AFTER_MS = 1200; function findNote(ref) { const ask = (relays) => askNote(relays, ref.id); const near = [...new Set([...(ref.relays || []), ...zapRelays()])]; const authors = [...new Set([ref.author, ...(ref.authors || [])].filter(Boolean))]; - const asks = [ - askNote(near, ref.id, ref.relays || []), - ...authors.map((a) => relaysOf(a).catch(() => []).then((rs) => ask(rs.filter((r) => !near.includes(r))))), - ask(WIDE_RELAYS.filter((r) => !near.includes(r))), - ]; return new Promise((resolve) => { - let left = asks.length; - for (const a of asks) a.then((ev) => { if (ev) resolve(ev); else if (--left === 0) resolve(null); }); + let done = false; + const found = (ev) => { if (!done && ev) { done = true; resolve(ev); } }; + const nearAsks = [ + askNote(near, ref.id, ref.relays || []), + ...authors.map((a) => relaysOf(a).catch(() => []).then((rs) => ask(rs.filter((r) => !near.includes(r))))), + ]; + for (const a of nearAsks) a.then(found); + const nearDone = Promise.all(nearAsks); + let wide = null; + const goWide = () => { + if (done || wide) return; + wide = ask(WIDE_RELAYS.filter((r) => !near.includes(r))); + wide.then(found); + }; + const timer = setTimeout(goWide, WIDE_AFTER_MS); + nearDone.then(() => { clearTimeout(timer); if (done) return; goWide(); wide.then((ev) => { if (!done) { done = true; resolve(ev || null); } }); }); }); } function quotedNote(ref) { diff --git a/tools/relay-budget-test.js b/tools/relay-budget-test.js index 600f60b..bb1dd11 100644 --- a/tools/relay-budget-test.js +++ b/tools/relay-budget-test.js @@ -28,6 +28,12 @@ const quotedNotes = quoters.map((q, i) => post(q.sk, `quoted thought ${i}`, 7200 const stored = [...quotedNotes, post(SK, 'morning one', 3600), post(SK, 'morning two', 3000)]; const quotePosts = quotedNotes.map((n, i) => post(SK, `look at this ${i} nostr:${neventEncode({ id: n.id, author: n.pubkey })}`, 1200 + i * 10, [['q', n.id, '', n.pubkey]])); stored.push(...quotePosts); +// one quoted note only a far relay holds (the wide net's job) +const farQ = generateSecretKey(); +const farNote = finalizeEvent({ kind: 1, created_at: now() - 9000, tags: [], content: 'kept only on the far relay' }, farQ); +const farPost = post(SK, `and this nostr:${neventEncode({ id: farNote.id, author: farNote.pubkey })}`, 1100, [['q', farNote.id, '', farNote.pubkey]]); +stored.push(farPost); +const wideStore = [farNote]; const cached = [...quotePosts, ...stored.filter((e) => e.pubkey === AUTHOR && !quotePosts.includes(e))].sort((a, b) => b.created_at - a.created_at); const match = (f, e) => (!f.kinds || f.kinds.includes(e.kind)) && (!f.authors || f.authors.includes(e.pubkey)) && (!f.ids || f.ids.includes(e.id)) && (!f.since || e.created_at >= f.since); @@ -38,7 +44,7 @@ const sockets = new Set(); let maxOpen = { fast: 0, slow: 0 }; const server = Bun.serve({ port: 0, fetch: (req, srv) => { const path = new URL(req.url).pathname; - if (path === '/fast' || path === '/slow') return srv.upgrade(req, { data: { relay: path.slice(1), subs: new Map() } }) ? undefined : new Response('ws', { status: 426 }); + if (path === '/fast' || path === '/slow' || path === '/wide') return srv.upgrade(req, { data: { relay: path.slice(1), subs: new Map() } }) ? undefined : new Response('ws', { status: 426 }); if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path)); return new Response(html, { headers: { 'content-type': 'text/html' } }); }, websocket: { @@ -54,7 +60,7 @@ const server = Bun.serve({ port: 0, fetch: (req, srv) => { log.push({ t: Date.now(), relay, kind: 'REQ', id: m[1], filters }); const answer = () => { if (!ws.data.subs.has(m[1])) return; - for (const e of stored) if (filters.some((f) => match(f, e))) ws.send(JSON.stringify(['EVENT', m[1], e])); + for (const e of (relay === 'wide' ? wideStore : stored)) if (filters.some((f) => match(f, e))) ws.send(JSON.stringify(['EVENT', m[1], e])); ws.send(JSON.stringify(['EOSE', m[1]])); log.push({ t: Date.now(), relay, kind: 'EOSE', id: m[1] }); }; @@ -73,6 +79,7 @@ await page.evaluateOnNewDocument((port) => { window.WebSocket = function (url, p) { if (/relay\.coinos\.io/.test(String(url))) url = `ws://localhost:${port}/fast`; else if (/relay\.damus\.io/.test(String(url))) url = `ws://localhost:${port}/slow`; + else if (/nostr\.wine/.test(String(url))) url = `ws://localhost:${port}/wide`; return p ? new Real(url, p) : new Real(url); }; window.WebSocket.prototype = Real.prototype; @@ -119,6 +126,12 @@ try { const covered = new Set(listAsks.flatMap((x) => x.filters.flatMap((f) => f.authors || [])).filter((a) => qpks.has(a))); check('asked for in one REQ, not one per author', listAsks.length === 1 && covered.size === 6, listAsks.length + ' REQ(s) covering ' + covered.size + ' of 6 authors'); + console.log('\n[quoted notes]'); + const wideIds = new Set(reqs('wide', (fs) => fs.some((f) => f.ids)).flatMap((x) => x.filters.flatMap((f) => f.ids || []))); + const nearHeld = quotedNotes.filter((n) => wideIds.has(n.id)).length; + check('the ones our relay holds were never asked of the wide net', nearHeld === 0, nearHeld + ' of 6 asked on the far relay'); + check('the one only the far relay holds was found there', wideIds.has(farNote.id) && (await page.evaluate(() => document.body.innerText.includes('kept only on the far relay'))), wideIds.has(farNote.id) ? 'asked' : 'never asked'); + console.log('\n[the live subscriptions]'); const live = reqs('fast', isLive); const liveClosed = live.filter((x) => log.some((y) => y.kind === 'CLOSE' && y.relay === 'fast' && y.id === x.id)); |
