aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-11-02 09:31:22 -0700
committerAdam Soltys <[email protected]>2026-11-02 09:31:22 -0700
commit071f8db3bd7799abe4a6acf7c32f107eef0d00c2 (patch)
tree3ccfff716224a3e9801064cb8439fe94b9fd86c3
parentdf4d28e29776349c0b2aef6d5367cace6aa25822 (diff)
downloadcoinosv3-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.js33
-rw-r--r--tools/relay-budget-test.js17
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));