diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 14 | ||||
| -rw-r--r-- | src/nostr.js | 55 |
2 files changed, 63 insertions, 6 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 4e90646..e1468ce 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -15,7 +15,7 @@ import { mediaServers } from '../media-servers.js'; // listens for both identities and decrypts with whichever keys are present. import { - subscribeOn, subscribeAuthed, publishOn, queryOn, queryManyOn, queryStreamOn, fetchInboxRelays, relayAlive, liveRelayList, readRelayList, resetRelay, + subscribeOn, subscribeAuthed, publishOn, queryOn, queryManyOn, queryStreamOn, fetchInboxRelays, relayAlive, liveRelayList, readRelayList, resetRelay, wakeRelays, npubOf, neventOf, parseNostrPubkey, parseNostrRef, generateSecretKey, getPublicKey, finalizeEvent, nip44, PROFILE_RELAYS, openWrapsOffthread, unwrapDMsOffthread, verifyEventsAsync, setSyncConfig, DEFAULT_SYNC_RELAYS, } from '../nostr.js'; @@ -18511,10 +18511,14 @@ export function messagesFeature(ctx) { resumed(awayMs) { // a blink between apps didn't kill anything if (awayMs && awayMs < 5_000) return; - resubscribeStreams(); // DMs and rooms, whether or not chat is on screen - if (!ui.chatOpen || !['feed', 'notifs'].includes(ui.msgView)) return true; - if (ui.msgView === 'notifs') refreshNotifs(true).catch(() => {}); - else refreshFeed({ force: true }).catch(() => {}); + // the sockets first: after a freeze they read open and are dead, and + // a refresh sent on them finds nothing (wakeRelays) + wakeRelays().catch(() => {}).then(() => { + resubscribeStreams(); // DMs and rooms, whether or not chat is on screen + if (!ui.chatOpen || !['feed', 'notifs'].includes(ui.msgView)) return; + if (ui.msgView === 'notifs') refreshNotifs(true).catch(() => {}); + else refreshFeed({ force: true }).catch(() => {}); + }); return true; }, // A zap flow reporting how its payment went, so the chip that appeared diff --git a/src/nostr.js b/src/nostr.js index c77fc27..f0b47d1 100644 --- a/src/nostr.js +++ b/src/nostr.js @@ -186,7 +186,12 @@ pool.ensureRelay = async (url, params) => { relay._coinosRL = true; const notice = relay.onnotice; relay.onnotice = (msg) => { - if (RATE_LIMITED.test(String(msg))) markRelayRateLimited(relay.url, String(msg)); + // Primal's "too many concurrent REQs" is advice, not a refusal: every + // REQ past its twenty is answered all the same (measured: 60 at once, + // 60 EOSEs, 12 notices). Cooling it off for 30 s at every boot — when + // the relay lists, profiles and the feed all ask at once — only lost + // us its answers. A CLOSED with that reason still counts (below). + if (RATE_LIMITED.test(String(msg)) && !/concurrent/i.test(String(msg))) markRelayRateLimited(relay.url, String(msg)); try { notice?.call(relay, msg); } catch {} }; const subscribe = relay.subscribe.bind(relay); @@ -284,6 +289,54 @@ export function resetRelay(url) { try { r.close(); } catch {} pool.relays.delete(n); } +// Back from a freeze (a phone's backgrounded tab: timers stopped, the +// network's idea of every socket long gone), the pool's sockets still read +// OPEN, and anything sent on them vanishes. Left to itself the library +// notices one in up to 29 s (its ping) + 20 s (the ping's timeout), then +// waits 10 s more before redialing — a minute of a feed that won't catch up +// ("no new notes since 11:35?", Adam, 2026-11-01). So on resume every open +// socket is asked one tiny REQ; one that answers within the deadline is +// fine, one that doesn't is closed and redialed now, its subscriptions +// refired by the library with `since` set to the last event each saw. A +// relay already waiting out a reconnect backoff is dialed now too. +const WAKE_PING_MS = 2500; +const DUMMY_ID = 'a'.repeat(64); +function pingRelay(r, ms) { + return new Promise((resolve) => { + let done = false; + const fin = (ok) => { if (done) return; done = true; clearTimeout(t); resolve(ok); }; + const t = setTimeout(() => fin(false), ms); + try { + const sub = r.subscribe([{ ids: [DUMMY_ID], limit: 0 }], { + label: '<wake-ping>', + oneose: () => { fin(true); try { sub.close(); } catch {} }, + onclose: () => fin(true), // a relay refusing the REQ is a relay that answers + eoseTimeout: ms + 1000, // ours fires first: the library's would count as an EOSE + }); + } catch { fin(false); } + }); +} +function redialRelay(r, why) { + console.warn('nostr: ' + r.url + ' ' + why + ' — redialing'); + // the dead socket's close would reach the library long after (a closing + // handshake nobody answers), and undo the new connection: detached first + const ws = r.ws; + try { if (ws) { ws.onopen = ws.onclose = ws.onerror = ws.onmessage = null; ws.close(); } } catch {} + try { r.handleHardClose(why); } catch {} // the library's own path: subscriptions kept, a reconnect queued + if (r.reconnectTimeoutHandle) { clearTimeout(r.reconnectTimeoutHandle); r.reconnectTimeoutHandle = undefined; } + r.connect().catch(() => {}); // now, not after the backoff +} +export async function wakeRelays(ms = WAKE_PING_MS) { + const checks = []; + for (const r of pool.relays.values()) { + if (r.connected && r.ws && r.ws.readyState === 1) checks.push(pingRelay(r, ms).then((ok) => { if (!ok) redialRelay(r, 'silent after resume'); })); + else if (!r.connectionPromise && r.reconnectTimeoutHandle) { + clearTimeout(r.reconnectTimeoutHandle); r.reconnectTimeoutHandle = undefined; + r.connect().catch(() => {}); + } + } + await Promise.all(checks); +} // the relays a subscription on this list actually dials (sick ones skipped) export const liveRelayList = (list) => liveRelays(list); // ...and the ones a one-shot read goes to (rate-limited ones skipped too) |
