diff options
| author | Adam Soltys <[email protected]> | 2026-10-07 21:27:40 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-07 21:27:40 -0700 |
| commit | 654e489cb438c62c09d291cc781707323914f8f1 (patch) | |
| tree | 70720196609718e2c8b651ceeb9d350948044c1f /src/features | |
| parent | 6dfb59316865093eda01307ee3c95f87511cdf56 (diff) | |
| download | coinosv3-654e489cb438c62c09d291cc781707323914f8f1.tar.gz coinosv3-654e489cb438c62c09d291cc781707323914f8f1.zip | |
Gentler on relays: a relay that says rate-limited (CLOSED/OK/NOTICE) gets no one-shot reads for 30 s, doubling to 10 min; zap/reply tallies flush at most every 2 s in one REQ (two filters) and the live #e sub swaps at most every 10 s; quoted-note lookups batch per relay over 150 ms — scrolling a 300-follow feed went from ~21 to ~8.5 REQs per 10 s on the busiest relay, primal's "too many concurrent REQs" from 19 to 0
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PCGjEEMD23CweKrefbTDJp
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 69 |
1 files changed, 59 insertions, 10 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index d4eec34..53f6c99 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -13,7 +13,7 @@ // listens for both identities and decrypts with whichever keys are present. import { - subscribeOn, publishOn, queryOn, queryStreamOn, fetchInboxRelays, relayAlive, liveRelayList, resetRelay, + subscribeOn, publishOn, queryOn, queryManyOn, queryStreamOn, fetchInboxRelays, relayAlive, liveRelayList, readRelayList, resetRelay, npubOf, neventOf, parseNostrPubkey, parseNostrRef, generateSecretKey, getPublicKey, finalizeEvent, nip44, PROFILE_RELAYS, openWrapsOffthread, unwrapDMsOffthread, verifyEventsAsync, setSyncConfig, DEFAULT_SYNC_RELAYS, } from '../nostr.js'; @@ -3841,6 +3841,12 @@ export function messagesFeature(ctx) { const zapWho = new Map(); // id -> Map(receipt id -> { pk, sats, ts, text }) — the tally, by person const zapAsked = new Set(); let zapQueue = new Set(), zapTimer = null, zapLiveUnsub = null, zapRecent = []; + // Scrolling brings new ids every few hundred ms; asking (two queries) and + // swapping the live sub (CLOSE + REQ) per batch put ~4 REQs a second on + // every zap relay. Flush at most every 2 s, swap the live sub at most + // every 10 s — the tallies still land within a couple of seconds. + let zapLastFlush = 0, zapLiveAt = 0, zapLiveTimer = null; + const ZAP_FLUSH_MS = 2000, ZAP_LIVE_MS = 10_000; const zapRelays = () => [...new Set([...NOTE_RELAYS, ...((wallet.nostrRelays && wallet.nostrRelays()) || [])])]; function noteCountSnapshot(id) { const rm = reacts.get(id); @@ -3981,14 +3987,15 @@ export function messagesFeature(ctx) { if (!fresh || zapTimer) return; zapTimer = setTimeout(() => { zapTimer = null; + zapLastFlush = Date.now(); const batch = [...zapQueue]; zapQueue = new Set(); const relays = zapRelays(); for (let i = 0; i < batch.length; i += 150) { const slice = batch.slice(i, i + 150); - queryOn(relays, { kinds: REPLY_KINDS, '#e': slice }, 4000).then((evs) => evs.forEach(noteReply)).catch(() => {}); - queryOn(relays, { kinds: NOTE_KINDS, '#e': slice }, 4000) - .then((evs) => { - evs.forEach(noteEvent); + // replies and tallies in one REQ (two filters), not one each + queryManyOn(relays, [{ kinds: REPLY_KINDS, '#e': slice }, { kinds: NOTE_KINDS, '#e': slice }], 4000) + .then((all) => { + for (const e of all) if (REPLY_KINDS.includes(e.kind)) noteReply(e); else noteEvent(e); // the relays have spoken for these ids: their live tally is the // truth now, remembered or not (a zap that was deleted has to be // able to disappear) @@ -4004,9 +4011,13 @@ export function messagesFeature(ctx) { }).catch(() => {}); } zapRecent = [...batch.reverse(), ...zapRecent.filter((x) => !batch.includes(x))].slice(0, 200); - if (zapLiveUnsub) { try { zapLiveUnsub(); } catch {} } - zapLiveUnsub = subscribeOn(relays, { kinds: [...NOTE_KINDS, ...REPLY_KINDS], '#e': zapRecent, since: Math.floor(Date.now() / 1000) - 60 }, noteEvent); - }, 250); + if (!zapLiveTimer) zapLiveTimer = setTimeout(() => { + zapLiveTimer = null; + zapLiveAt = Date.now(); + if (zapLiveUnsub) { try { zapLiveUnsub(); } catch {} } + zapLiveUnsub = subscribeOn(zapRelays(), { kinds: [...NOTE_KINDS, ...REPLY_KINDS], '#e': zapRecent, since: Math.floor(Date.now() / 1000) - 60 }, noteEvent); + }, Math.max(0, zapLiveAt + ZAP_LIVE_MS - Date.now())); + }, Math.max(250, zapLastFlush + ZAP_FLUSH_MS - Date.now())); } // Tallies are asked of the relays again on every boot, and that takes // seconds — long enough that a room full of zapped messages paints bare and @@ -6973,11 +6984,48 @@ export function messagesFeature(ctx) { 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']; + // Lookups wait 150 ms and go out together: one REQ per relay carrying + // every id wanted there, not one per quoted note (a scrolled feed sent a + // dozen relays a fresh REQ for each quote it met). + const findQ = new Map(); // relay -> Map(id -> waiters) + let findTimer = null; + function askNote(relays, id) { + relays = readRelayList(relays); + if (!relays.length) return Promise.resolve(null); + return new Promise((resolve) => { + const w = { left: relays.length, resolve, done: false }; + for (const r of relays) { + let q = findQ.get(r); + if (!q) findQ.set(r, (q = new Map())); + if (!q.has(id)) q.set(id, []); + q.get(id).push(w); + } + if (!findTimer) findTimer = setTimeout(flushFind, 150); + }); + } + function flushFind() { + findTimer = null; + const batch = [...findQ]; findQ.clear(); + for (const [relay, wanted] of batch) { + const ids = [...wanted.keys()]; + for (let i = 0; i < ids.length; i += 100) { + const slice = ids.slice(i, i + 100); + queryOn([relay], { ids: slice }, 4500).catch(() => []).then((evs) => { + const got = new Map(evs.map((e) => [e.id, e])); + for (const id of slice) for (const w of wanted.get(id)) { + if (w.done) continue; + const ev = got.get(id); + if (ev) { w.done = true; w.resolve(ev); } + else if (--w.left === 0) { w.done = true; w.resolve(null); } + } + }); + } + } + } // 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) { - const pick = (evs) => (evs || []).find((e) => e.id === ref.id) || null; - const ask = (relays) => (relays.length ? queryOn(relays, { ids: [ref.id] }, 4500).then(pick).catch(() => null) : Promise.resolve(null)); + const ask = (relays) => askNote(relays, ref.id); const near = [...new Set([...(ref.relays || []), ...zapRelays()])]; const asks = [ ask(near), @@ -13195,6 +13243,7 @@ export function messagesFeature(ctx) { listsSynced = false; if (zapLiveUnsub) { try { zapLiveUnsub(); } catch {} zapLiveUnsub = null; } clearTimeout(zapTimer); zapTimer = null; zapQueue = new Set(); zapRecent = []; + clearTimeout(zapLiveTimer); zapLiveTimer = null; zapLiveAt = 0; stopFeedWatch(); ++feedWarmSession; clearTimeout(feedWarmTimer); feedWarmTimer = null; |
