1 Commits
Author SHA1 Message Date
sneak fa83b96722 fix: a refresh asked for while another runs is no longer dropped (closes #581)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s
doRefreshAndRender() returned at once when a refresh was already running. An
import asks for a refresh after its address scan, so the addresses the scan
found showed 0.0000 until the next ten-second refresh.

While a refresh runs, a request through ctx now sets a flag. One more refresh
runs once the running one ends. Several requests in that time lead to one more
refresh, not several. A tick of the ten-second timer that finds a refresh
running is still dropped.

Model: opus-5-5
2026-10-27 18:05:24 +00:00
2 changed files with 42 additions and 17 deletions

No files matched your search

+10 -9
View File
@@ -65,7 +65,7 @@ function renderWalletList() {
const pageClosed = new AbortController(); const pageClosed = new AbortController();
window.addEventListener("pagehide", () => pageClosed.abort()); window.addEventListener("pagehide", () => pageClosed.abort());
// A refresh asked for while another runs is not dropped. The request sets // While a refresh runs, a request through ctx is not dropped. The request sets
// refreshAgain, and one more refresh runs once the running one ends. Several // refreshAgain, and one more refresh runs once the running one ends. Several
// requests in that time lead to one more refresh, not several. An import asks // requests in that time lead to one more refresh, not several. An import asks
// for a refresh after its address scan. A refresh already running does not // for a refresh after its address scan. A refresh already running does not
@@ -142,13 +142,11 @@ async function doRefreshAndRender() {
}); });
renderWalletList(); renderWalletList();
} catch (e) { } catch (e) {
// Every call site fires this and walks away — the boot below, the ten // No caller waits for this function, so it must never reject. An
// second interval, eight views through ctx, and the end of this // unhandled rejection reports nothing. The save inside it reports its
// function for one more refresh — so it must never reject: an // own failure through onSaveFailure() (see src/shared/state.js); what
// unhandled rejection is not a report of anything. The save // is left here is a failed network round trip, which the next tick
// inside it reports its own failure through onSaveFailure() (see // retries.
// src/shared/state.js); what is left here is a failed network round
// trip, which the next tick retries.
log.errorf("popup: background refresh failed:", e); log.errorf("popup: background refresh failed:", e);
} finally { } finally {
refreshInFlight = false; refreshInFlight = false;
@@ -169,7 +167,10 @@ function startRefresh() {
if (!$("view-state-recovery").classList.contains("hidden")) return; if (!$("view-state-recovery").classList.contains("hidden")) return;
doRefreshAndRender(); doRefreshAndRender();
if (refreshTimer === null) { if (refreshTimer === null) {
refreshTimer = setInterval(doRefreshAndRender, 10000); // A tick that finds a refresh running is dropped.
refreshTimer = setInterval(() => {
if (!refreshInFlight) return doRefreshAndRender();
}, 10000);
} }
} }
+32 -8
View File
@@ -1,9 +1,10 @@
// A refresh asked for while another runs is not dropped. One more refresh runs // While a refresh runs, a request through ctx is not dropped. One more refresh
// once the running one ends, however many were asked for in that time // runs once the running one ends, however many were asked for in that time
// (https://git.eeqj.de/sneak/AutistMask/issues/581). Each test boots the real // (https://git.eeqj.de/sneak/AutistMask/issues/581). A tick of the ten-second
// popup over a stored profile and holds the ten-second refresh open. The // timer that finds a refresh running is dropped. Each test boots the real popup
// balance stand-in writes what the chain holds onto every address, as the real // over a stored profile and holds the ten-second refresh open. `readChain()`
// refreshBalances() does. // sets each address's balance from `CHAIN`, as the real `refreshBalances()`
// sets it from the node.
const { const {
bootPopup, bootPopup,
@@ -100,6 +101,16 @@ async function importPhrase(page) {
await page.click("btn-add-wallet-confirm"); await page.click("btn-add-wallet-confirm");
} }
// Imports the private key `key` on AddWallet.
async function importKey(page, key) {
await page.click("btn-add-wallet-bottom");
await page.click("tab-privkey");
page.node("import-private-key").value = key;
page.node("add-wallet-password").value = PASSWORD;
page.node("add-wallet-password-confirm").value = PASSWORD;
await page.click("btn-add-wallet-confirm");
}
test("addresses an import finds while a refresh runs show their balances once one more refresh returns", async () => { test("addresses an import finds while a refresh runs show their balances once one more refresh returns", async () => {
const { page } = await boot(); const { page } = await boot();
const refresh = await holdRefresh(page); const refresh = await holdRefresh(page);
@@ -121,8 +132,8 @@ test("several refreshes asked for while one runs lead to exactly one more", asyn
const refresh = await holdRefresh(page); const refresh = await holdRefresh(page);
const calls = refreshBalances.mock.calls.length; const calls = refreshBalances.mock.calls.length;
await page.tick(); await importKey(page, "0x" + "11".repeat(32));
await page.tick(); await importKey(page, "0x" + "22".repeat(32));
await importPhrase(page); await importPhrase(page);
expect(refreshBalances).toHaveBeenCalledTimes(calls); expect(refreshBalances).toHaveBeenCalledTimes(calls);
@@ -131,3 +142,16 @@ test("several refreshes asked for while one runs lead to exactly one more", asyn
expect(refreshBalances).toHaveBeenCalledTimes(calls + 1); expect(refreshBalances).toHaveBeenCalledTimes(calls + 1);
}); });
test("a tick of the ten-second timer while a refresh runs queues no other refresh", async () => {
const { page, refreshBalances } = await boot();
const refresh = await holdRefresh(page);
const calls = refreshBalances.mock.calls.length;
await page.tick();
await refresh.finish();
await page.settle();
expect(refreshBalances).toHaveBeenCalledTimes(calls);
});