aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-09 23:12:13 -0700
committerAdam Soltys <[email protected]>2026-09-09 23:12:13 -0700
commit57289fd3f87554e9981ddc2c63c3bba805d599bd (patch)
tree9a307f9d5382c8f727e9ae12a56709fc41a226a4 /src
parent839ab937afbd0c68d1a4366e10145274b6f62d31 (diff)
downloadcoinosv3-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')
-rw-r--r--src/features/messages.js34
-rw-r--r--src/sync-outbox.js5
2 files changed, 33 insertions, 6 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;
diff --git a/src/sync-outbox.js b/src/sync-outbox.js
index 2062d9e..80042d1 100644
--- a/src/sync-outbox.js
+++ b/src/sync-outbox.js
@@ -52,7 +52,10 @@ export class SyncOutbox {
const key = PREFIX + pubkey + ':' + dtag;
let previous;
try { previous = JSON.parse(this.storage.getItem(key)); } catch {}
- if (previous?.digest === digest) { this.wake(); return; }
+ // Same content, same route: nothing to do. A record that changed route
+ // (an oversized domain now bound for Blossom, or back inline) is replaced
+ // even for identical content — the stuck relay-only copy must go.
+ if (previous?.digest === digest && !!previous.blob === !!blob) { this.wake(); return; }
// A pending, never-sent event can be replaced within the same second.
// Once attempted, use a later timestamp: Nostr resolves equal timestamps
// by event id, which can otherwise leave the OLD snapshot in the relay.