diff options
Diffstat (limited to 'src/features')
| -rw-r--r-- | src/features/ark.js | 39 | ||||
| -rw-r--r-- | src/features/nwc.js | 14 |
2 files changed, 48 insertions, 5 deletions
diff --git a/src/features/ark.js b/src/features/ark.js index f5ba9d8..075cf4b 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -1052,10 +1052,29 @@ export function arkFeature(ctx) { return v; }; } - async function writeBgMirror(connections, offer, prefs) { + // The worker can now pay while this app is alive behind another one (a + // phone with Coinos in the background: the page is frozen or throttled, + // the worker is not). Its change coin and its spent coins then exist only + // in the mirror — so the mirror is never written over the worker's hand: + // one writer at a time (the lock the worker holds for a whole push), and + // whatever the worker did since we last looked is folded in first. + let bgAbsorbedAt = 0; // the worker stamp (rec.workerAt) this app has folded in + const withBgLock = (fn) => (typeof navigator !== 'undefined' && navigator.locks + ? navigator.locks.request('nwc-bg-work', fn) : fn()); + const bgBusy = (mgr) => !!(mgr && mgr._lnDriving && mgr._lnDriving.size); + async function writeBgMirror(connections, offer, prefs) { return withBgLock(() => writeBgMirrorLocked(connections, offer, prefs)); } + async function writeBgMirrorLocked(connections, offer, prefs) { const cfg = getArkConfig(); if (!cfg || !ark || !ark.info || !ark.state) return; - const prev = await loadBg(wallet._cacheKey()); + let prev = await loadBg(wallet._cacheKey()); + if (prev && prev.v >= 3 && prev.mgr && (prev.workerAt || 0) > bgAbsorbedAt) { + // a payment of ours in flight holds the very records a merge replaces: + // leave the worker's state standing and write at the next call + if (bgBusy(ark)) return; + await mergeBgWorkerState(ark).catch((e) => console.warn('ark: worker merge failed', e && e.message)); + if ((prev.workerAt || 0) > bgAbsorbedAt) return; // not folded in: don't write over it + prev = await loadBg(wallet._cacheKey()); + } if (connections === undefined) connections = (prev && prev.connections) || []; if (offer === undefined) offer = (prev && prev.offer) || null; if (prefs === undefined) prefs = (prev && prev.prefs) || undefined; @@ -1068,7 +1087,7 @@ export function arkFeature(ctx) { offer, autowithdraw: await awMirror(prev), }); - if (prev) { rec.spends = prev.spends; rec.pendingPays = prev.pendingPays; } // the worker's log survives rewrites + if (prev) { rec.spends = prev.spends; rec.pendingPays = prev.pendingPays; if (prev.workerAt) rec.workerAt = prev.workerAt; } // the worker's log survives rewrites if (prefs) rec.prefs = prefs; // notification choices the worker honours await saveBg(wallet._cacheKey(), rec); await disarmSiblingRecords(wallet._cacheKey(), rec).catch(() => {}); @@ -1088,6 +1107,7 @@ export function arkFeature(ctx) { } if (!rec.mgr) return; bootPhase('ark-merge'); + const stamp = rec.workerAt || 0; const merged = mergeArkStates(mgr.state, rec.mgr); // the worker's view of OUR vtxos is newer for anything it spent const workerState = new Map((rec.mgr.vtxos || []).map((v) => [v.id, v.state])); @@ -1097,6 +1117,7 @@ export function arkFeature(ctx) { merged.nextKeyIndex = Math.max(merged.nextKeyIndex || 1, rec.mgr.nextKeyIndex || 1); mgr.state = merged; mgr._save(); + if (mgr === ark) bgAbsorbedAt = Math.max(bgAbsorbedAt, stamp); bootPhase('ark-merged'); await mgr.reconcile().catch(() => {}); bootPhase('ark-reconciled'); @@ -4166,6 +4187,18 @@ export function arkFeature(ctx) { // nwc.js calls this (with its connections) whenever the mirror should // refresh: on enable, on connection changes, after payments, on a timer. async arkBgWrite(connections, offer, prefs) { return writeBgMirror(connections, offer, prefs); }, + // Back on screen: what the worker paid while this app sat behind another + // one is folded in now, not at the next start. true when there was any. + async arkBgAbsorb() { + if (!ark || !ark.state) return false; + return withBgLock(async () => { + const rec = await loadBg(wallet._cacheKey()); + if (!rec || rec.v < 3 || !rec.mgr || (rec.workerAt || 0) <= bgAbsorbedAt || bgBusy(ark)) return false; + await mergeBgWorkerState(ark); + render(); + return true; + }); + }, async arkBgSpendableSat() { const rec = await loadBg(wallet._cacheKey()); return ((rec && rec.mgr && rec.mgr.vtxos) || []) diff --git a/src/features/nwc.js b/src/features/nwc.js index dc87388..9768740 100644 --- a/src/features/nwc.js +++ b/src/features/nwc.js @@ -388,7 +388,10 @@ export function nwcFeature(ctx) { return !!(prior && prior.length); }; if (age > 5 && await lookBack()) return dlog('nwc: skip — request already answered elsewhere', ev.id.slice(0, 8)); - if (mobile && age <= 5) await new Promise((r) => setTimeout(r, 1000)); + // (a phone with this app on screen is as awake as any computer: only + // one behind another app holds back — every zap answered by an open + // phone used to wait this second for nobody) + if (mobile && age <= 5 && typeof document !== 'undefined' && document.hidden) await new Promise((r) => setTimeout(r, 1000)); claimed = await claim(ev.id); if (claimed === false) return dlog('nwc: standing down — another device claimed', ev.id.slice(0, 8)); if (claimed === null && age <= 5) { @@ -981,7 +984,14 @@ export function nwcFeature(ctx) { if (typeof window !== 'undefined') { window.addEventListener('online', () => listen()); document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible' && conns().length) { listen(); refreshRegistration(); } + if (document.visibilityState === 'visible' && conns().length) { + listen(); refreshRegistration(); + // the worker may have paid while this app sat behind another one + Promise.resolve(hook('arkBgAbsorb')).then((had) => { if (had) return reconcileBg(); }).catch(() => {}); + } + // going behind another app: the worker answers from the mirror now, so + // it gets this moment's coins, not the last minute-and-a-half's + if (document.visibilityState === 'hidden' && conns().length) writeBg(); }); } } |
