diff options
| author | Adam Soltys <[email protected]> | 2026-09-09 23:12:13 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-09 23:12:13 -0700 |
| commit | 57289fd3f87554e9981ddc2c63c3bba805d599bd (patch) | |
| tree | 9a307f9d5382c8f727e9ae12a56709fc41a226a4 /src/features | |
| parent | 839ab937afbd0c68d1a4366e10145274b6f62d31 (diff) | |
| download | coinosv3-57289fd3f87554e9981ddc2c63c3bba805d599bd.tar.gz coinosv3-57289fd3f87554e9981ddc2c63c3bba805d599bd.zip | |
Phone boot: remember opened DM wraps across boots; supersede stuck relay-only sync records.
A remote-signer (bunker) wallet re-opened the relay's last ~400 gift wraps
on EVERY boot — the seen-set lived in memory only — and each wrap costs two
NIP-46 round trips with a schnorr sign+verify apiece on the client. On the
Pixel that was ~4s of a 15s pegged boot. Opened (and given-up) wrap ids are
now kept in the messages feature state, bounded to 1500 id prefixes, and
loaded before the inbox subscription.
The outbox also replaces a record whose route changed even when its digest
matches: the 874KB relay-only ark record that could never publish would
otherwise have stayed in front of its Blossom successor forever.
Verified live: the wallet's 633KB ark domain uploaded to blossom.coinos.io
and its pointer was acknowledged by relay.coinos.io on the next boot.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/messages.js | 34 |
1 files changed, 29 insertions, 5 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 6e4c781..0a828bb 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -245,6 +245,29 @@ export function messagesFeature(ctx) { const pendingDirect = new Map(); // rumor id -> { bundle, from } const profiles = new Map(); // pubkey -> profile | null while loading const seenWraps = new Set(); + // Wraps this device already opened (or gave up on), remembered ACROSS + // boots: relays re-serve their last ~400 wraps on every subscribe, and a + // remote-signer wallet paid two NIP-46 round trips (sign + verify each) + // per wrap to rediscover DMs it already had — ~4s of a phone's boot. + // Keyed by an id prefix (64 bits is plenty for "skip this one"). + const WRAPS_MAX = 1500; + const wrapKey = (id) => String(id).slice(0, 16); + let wrapLog = null, wrapsTimer = 0; + function loadSeenWraps() { + if (wrapLog) return; + wrapLog = (st().wraps || []).slice(-WRAPS_MAX); + for (const k of wrapLog) seenWraps.add(k); + } + function rememberWrap(id) { + const k = wrapKey(id); + if (!wrapLog) wrapLog = []; + if (!seenWraps.has(k)) wrapLog.push(k); + seenWraps.add(k); + clearTimeout(wrapsTimer); + wrapsTimer = setTimeout(() => { + try { const s = st(); s.wraps = wrapLog.slice(-WRAPS_MAX); wrapLog = s.wraps; save(s); } catch {} + }, 1500); + } let dmStarted = false; let allUnsubs = []; @@ -1399,9 +1422,9 @@ export function messagesFeature(ctx) { } async function handleInboxWrap(wrap) { - if (seenWraps.has(wrap.id)) return; + if (seenWraps.has(wrap.id) || seenWraps.has(wrapKey(wrap.id))) return; seenWraps.add(wrap.id); - if (await openInboxWrap(wrap)) return; + if (await openInboxWrap(wrap)) { rememberWrap(wrap.id); return; } if (pendingWraps.size >= PENDING_MAX) pendingWraps.delete(pendingWraps.keys().next().value); pendingWraps.set(wrap.id, { wrap, tries: decryptorsComplete() ? 1 : 0 }); scheduleDrain(8000); @@ -1436,8 +1459,8 @@ export function messagesFeature(ctx) { // full-strength failures would evict messages that were never really // tried. Stop here — the slow retry picks the rest up. if (complete && !decryptorsComplete()) break; - if (await openInboxWrap(p.wrap)) pendingWraps.delete(id); - else if (complete && ++p.tries >= PENDING_TRIES) pendingWraps.delete(id); + if (await openInboxWrap(p.wrap)) { pendingWraps.delete(id); rememberWrap(id); } + else if (complete && ++p.tries >= PENDING_TRIES) { pendingWraps.delete(id); rememberWrap(id); } if (performance.now() - chunkStart > 8) { await new Promise((r) => setTimeout(r)); chunkStart = performance.now(); @@ -1464,6 +1487,7 @@ export function messagesFeature(ctx) { const pks = dmSubKeys(); if (!pks.length) return; dmStarted = true; + loadSeenWraps(); // warm threads from the local cache. A self-thread of our own messages is // the residue of the old sent-copy misthreading (each welcome DM landed // under our own key) — sweep it rather than resurrect it; the cached @@ -3463,7 +3487,7 @@ export function messagesFeature(ctx) { rooms.clear(); threads.clear(); pendingDirect.clear(); - seenWraps.clear(); // the next account must decrypt wraps this one couldn't + seenWraps.clear(); wrapLog = null; // the next account must decrypt wraps this one couldn't clearTimeout(drainTimer); pendingWraps.clear(); dmStarted = false; |
