diff options
| -rw-r--r-- | src/dm.js | 6 | ||||
| -rw-r--r-- | src/features/messages.js | 53 |
2 files changed, 48 insertions, 11 deletions
@@ -121,4 +121,8 @@ export async function unwrapDM(wrap, skOrSigner) { return null; } } -export const SIGNER_SILENT = /did not answer|timed? ?out|connect|closed|socket|relay|network/i; +// Also silent: a bunker request every relay refused to carry ("All promises +// were rejected" — rate-limited on one, PoW demanded by another) and a call +// that reached a signer object selfHealing had just closed ("not open +// anymore"). Neither is the signer saying "not yours". +export const SIGNER_SILENT = /did not answer|timed? ?out|connect|closed|socket|relay|network|not open|all promises were rejected/i; diff --git a/src/features/messages.js b/src/features/messages.js index 38e533b..5702198 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -587,8 +587,13 @@ export function messagesFeature(ctx) { // the log calls them opened: caches written before rows kept file fields, // or clobbered by another tab, are missing messages no relay would ever be // asked for again. Only messages and reactions are re-applied. - const HEAL_VERSION = 1; - let healSince = 0; + // v2 (2026-10-09): every wrap the relays still hold. Until then a login + // signer that hadn't resumed yet counted as "complete", so a boot's whole + // backfill could be given up on the wallet key alone and logged as opened. + // The pass is only marked done once nothing it re-opened is still waiting + // for the signer — a session closed before the bunker answered runs it again. + const HEAL_VERSION = 2; + let healSince = 0, healAt = 0, healTimer = 0; const healed = new Set(); function loadSeenWraps() { if (wrapLog) return; @@ -597,11 +602,23 @@ export function messagesFeature(ctx) { for (const k of wrapLog) seenWraps.add(k); healSince = 0; if ((s.dmHeal || 0) < HEAL_VERSION) { - if (wrapLog.length) healSince = Math.floor(Date.now() / 1000) - 14 * 86400; - s.dmHeal = HEAL_VERSION; - save(s); + if (wrapLog.length) { + healSince = 1; healAt = Date.now(); + // wraps that open at once never reach the drain: check on a timer too + clearTimeout(healTimer); + const tick = () => { healDone(); if (healSince) healTimer = setTimeout(tick, 60_000); }; + healTimer = setTimeout(tick, 90_000); + } else { s.dmHeal = HEAL_VERSION; save(s); } } } + function healDone() { + if (!healSince || Date.now() - healAt < 60_000 || !decryptorsComplete()) return; + for (const p of pendingWraps.values()) if (p.heal) return; + const s = st(); + s.dmHeal = HEAL_VERSION; + save(s); + healSince = 0; + } const healing = (wrap) => !!healSince && wrap.created_at >= healSince && !healed.has(wrap.id) && !!healed.add(wrap.id); function rememberWrap(id) { const k = wrapKey(id); @@ -2863,9 +2880,17 @@ export function messagesFeature(ctx) { // only count against a wrap once the login signer (if one is linked) has // actually had its chance — the wallet key alone can never open a wrap // sealed to the login npub. + // + // A linked login whose signer is NOT live (a reload before the bunker + // resumed, or a session selfHealing just dropped) is incomplete, not + // "nothing to wait for": counting those passes gave every wrap sealed to + // the login npub up after ~40 s with only the wallet key tried, and the + // wrap log then hid it from every later session (a phone kept 9 of 186 + // messages). An interactive signer (Amber) is live from the moment it + // resumes and never decrypts here, so it counts as complete. function decryptorsComplete() { const login = hook('nostrLoginIdentity'); - return !login || !login.signer || !!login.signer.interactive || !!login.signer.decryptFrom; + return !login || !!(login.signer && (login.signer.interactive || login.signer.decryptFrom)); } // Raw-key unwraps (seed-derived / pasted nsec — the common case) run in @@ -2961,8 +2986,11 @@ export function messagesFeature(ctx) { // touch a screen that calls identity(). Resume single-flights and // rate-limits itself, so asking here is cheap. if (!decryptorsComplete()) await Promise.resolve(hook('nostrLoginResume')).catch(() => null); - if (!dmDecryptors().length) return; + // Still no login signer: a pass would only re-run the wallet key, which + // already had its try when each wrap arrived. Wait for the signer + // (nostrSignerLive or the slow retry below) instead. const complete = decryptorsComplete(); + if (!complete || !dmDecryptors().length) return; // Each openInboxWrap decrypts a gift wrap — NIP-44 ECDH, real // secp256k1 work. A backlog decrypted in one tight loop blocks the main // thread for the whole burst (the boot-time jank that made a carousel @@ -2983,9 +3011,13 @@ export function messagesFeature(ctx) { chunkStart = performance.now(); } } - } finally { draining = false; } - // still holding wraps: the signer may connect later — keep a slow retry - if (pendingWraps.size) scheduleDrain(decryptorsComplete() ? 30_000 : 60_000); + } finally { + draining = false; + // still holding wraps: the signer may connect later — keep a slow retry + // (in finally: an early return above must not end the retries) + if (pendingWraps.size) scheduleDrain(decryptorsComplete() ? 30_000 : 60_000); + healDone(); + } } function openDirectBundle(json) { @@ -13405,6 +13437,7 @@ export function messagesFeature(ctx) { wn = null; wnLoading = null; wnReadied = false; wnFolds.clear(); wnReach.clear(); wnReactOf.clear(); wnCands.clear(); seenWraps.clear(); wrapLog = null; // the next account must decrypt wraps this one couldn't + clearTimeout(healTimer); healSince = 0; clearTimeout(drainTimer); pendingWraps.clear(); dmStarted = false; |
