aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/dm.js6
-rw-r--r--src/features/messages.js53
2 files changed, 48 insertions, 11 deletions
diff --git a/src/dm.js b/src/dm.js
index c610aa3..443dd22 100644
--- a/src/dm.js
+++ b/src/dm.js
@@ -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;