diff options
Diffstat (limited to 'src/nostr.js')
| -rw-r--r-- | src/nostr.js | 94 |
1 files changed, 72 insertions, 22 deletions
diff --git a/src/nostr.js b/src/nostr.js index f0b47d1..731c076 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -407,12 +407,77 @@ async function authRelay(relay, onauth) { } return relay.auth(onauth); } +// ---- one-shot reads: per relay, closed on its own EOSE, a few at a time -- +// The pool's own querySync keeps every relay's REQ open until the SLOWEST +// relay has answered or the wait runs out: asked beside a relay still +// dialing, our own relay — done in fifty milliseconds — carried each REQ +// for five seconds, and a feed's opening asks stacked up on it by the +// dozen. Each relay's REQ now closes on that relay's EOSE. And no relay +// carries more than REQ_SLOTS of them at once: the rest wait their turn, +// which is a beat on a relay that answers and no longer than the wait on +// one that doesn't. (A Following feed opened from the laptop sent some 560 +// REQs in twenty seconds across 57 relays, over a hundred each to damus +// and primal; damus answered the home IP "banned: too many rate-limit +// violations" on 2026-11-02, and primal notices past twenty at once.) +const REQ_SLOTS = 10; +const slots = new Map(); // normalized url -> { n, q: [resolve] } +function takeSlot(url) { + const k = relayKey(url); + let s = slots.get(k); + if (!s) slots.set(k, (s = { n: 0, q: [] })); + if (s.n < REQ_SLOTS) { s.n++; return Promise.resolve(false); } + const at = Date.now(); + return new Promise((resolve) => s.q.push(() => resolve(Date.now() - at))); // how long it waited +} +function freeSlot(url) { + const s = slots.get(relayKey(url)); + if (!s) return; + const next = s.q.shift(); + if (next) next(); // the slot passes straight to the next in line + else s.n--; +} +export const reqSlotsOf = (url) => { const s = slots.get(relayKey(url)); return s ? { busy: s.n, waiting: s.q.length } : { busy: 0, waiting: 0 }; }; +// One REQ carrying `filters` on one relay. Settles at the relay's EOSE, its +// CLOSED, a failed dial, or maxWait — whichever is first — and sends CLOSE. +function askRelay(url, filters, maxWait, onEvent) { + return new Promise((resolve) => { + let finished = false, sub = null, timer = 0; + const finish = () => { + if (finished) return; + finished = true; + clearTimeout(timer); + if (sub) { try { sub.close('done'); } catch {} } + freeSlot(url); + resolve(); + }; + takeSlot(url).then((waited) => { + // a turn that came only after the ask's own wait had run out: the + // caller has moved on, and a storm of late REQs helps nobody + if (waited > maxWait) { finish(); return; } + timer = setTimeout(finish, maxWait + 1000); + try { + sub = pool.subscribeMap(filters.map((filter) => ({ url, filter })), { maxWait, onevent: onEvent, oneose: finish, onclose: finish }); + if (finished) { try { sub.close('done'); } catch {} } + } catch { finish(); } + }); + }); +} +// All relays asked at once; an event is counted once however many hold it. +async function askRelays(relays, filters, maxWait, onEvent) { + const seen = new Set(); + await Promise.all(relays.map((url) => askRelay(url, filters, maxWait, (ev) => { + if (!ev || seen.has(ev.id)) return; + seen.add(ev.id); + onEvent(ev); + }))); +} export async function queryOn(relays, filter, maxWait = 1500) { probeRelays(relays); try { const live = readRelays(relays); if (!live.length) return []; - const evs = await pool.querySync(live, filter, { maxWait }); + const evs = []; + await askRelays(live, [filter], maxWait, (ev) => evs.push(ev)); const oks = await verifyEventsAsync(evs); return evs.filter((_, i) => oks[i]); } catch { return []; } @@ -517,17 +582,8 @@ export async function queryManyOn(relays, filters, maxWait = 1500) { try { const live = readRelays(relays); if (!live.length || !filters.length) return []; - const evs = await new Promise((resolve) => { - const got = []; - let sub = null, done = false; - const finish = () => { if (!done) { done = true; resolve(got); } }; - sub = pool.subscribeMap(live.flatMap((url) => filters.map((filter) => ({ url, filter }))), { - maxWait, - onevent: (e) => got.push(e), - oneose: () => { try { sub?.close('closed automatically on eose'); } catch {} finish(); }, - onclose: finish, - }); - }); + const evs = []; + await askRelays(live, filters, maxWait, (ev) => evs.push(ev)); const oks = await verifyEventsAsync(evs); return evs.filter((_, i) => oks[i]); } catch { return []; } @@ -540,14 +596,8 @@ export async function queryStreamOn(relays, filter, onEvent, maxWait = 1500) { try { const live = readRelays(relays); if (!live.length) return; - await new Promise((resolve) => { - pool.subscribeManyEose(live, filter, { - maxWait, - onevent(event) { - pending.push(verifyOneAsync(event).then((ok) => ok && onEvent(event)).catch(() => {})); - }, - onclose: resolve, - }); + await askRelays(live, [filter], maxWait, (event) => { + pending.push(verifyOneAsync(event).then((ok) => ok && onEvent(event)).catch(() => {})); }); } catch {} finally { await Promise.all(pending); } } @@ -657,7 +707,7 @@ export const PROFILE_RELAYS = HOME_RELAYS; // Fetch a recipient's profile (name + picture) for a pubkey, newest across relays. export async function fetchNostrProfile(pubkeyHex, relays = PROFILE_RELAYS) { let events; - try { events = await pool.querySync(relays, { kinds: [0], authors: [pubkeyHex] }, { maxWait: 5000 }); } catch { return null; } + try { events = await queryOn(relays, { kinds: [0], authors: [pubkeyHex] }, 5000); } catch { return null; } const best = events.sort((a, b) => b.created_at - a.created_at)[0]; if (!best) return null; try { @@ -679,7 +729,7 @@ export async function fetchNostrProfile(pubkeyHex, relays = PROFILE_RELAYS) { // 10050), else their NIP-65 read relays (kind 10002), else [] (caller falls back). export async function fetchInboxRelays(pubkeyHex, relays = PROFILE_RELAYS) { let events; - try { events = await pool.querySync(relays, { kinds: [10002, 10050], authors: [pubkeyHex] }, { maxWait: 5000 }); } catch { return []; } + try { events = await queryOn(relays, { kinds: [10002, 10050], authors: [pubkeyHex] }, 5000); } catch { return []; } const newest = (kind) => events.filter((e) => e.kind === kind).sort((a, b) => b.created_at - a.created_at)[0]; const dm = newest(10050); if (dm) { const r = dm.tags.filter((t) => t[0] === 'relay' && t[1]).map((t) => t[1]); if (r.length) return r; } |
