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
+10 -9
View File
@@ -65,7 +65,7 @@ function renderWalletList() {
const pageClosed = new AbortController();
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
// 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
@@ -142,13 +142,11 @@ async function doRefreshAndRender() {
});
renderWalletList();
} catch (e) {
// Every call site fires this and walks away — the boot below, the ten
// second interval, eight views through ctx, and the end of this
// function for one more refresh — so it must never reject: an
// unhandled rejection is not a report of anything. The save
// inside it reports its own failure through onSaveFailure() (see
// src/shared/state.js); what is left here is a failed network round
// trip, which the next tick retries.
// No caller waits for this function, so it must never reject. An
// unhandled rejection reports nothing. The save inside it reports its
// own failure through onSaveFailure() (see 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);
} finally {
refreshInFlight = false;
@@ -169,7 +167,10 @@ function startRefresh() {
if (!$("view-state-recovery").classList.contains("hidden")) return;
doRefreshAndRender();
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
// 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
// popup over a stored profile and holds the ten-second refresh open. The
// balance stand-in writes what the chain holds onto every address, as the real
// refreshBalances() does.
// While a refresh runs, a request through ctx is not dropped. One more refresh
// runs once the running one ends, however many were asked for in that time
// (https://git.eeqj.de/sneak/AutistMask/issues/581). A tick of the ten-second
// timer that finds a refresh running is dropped. Each test boots the real popup
// over a stored profile and holds the ten-second refresh open. `readChain()`
// sets each address's balance from `CHAIN`, as the real `refreshBalances()`
// sets it from the node.
const {
bootPopup,
@@ -100,6 +101,16 @@ async function importPhrase(page) {
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 () => {
const { page } = await boot();
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 calls = refreshBalances.mock.calls.length;
await page.tick();
await page.tick();
await importKey(page, "0x" + "11".repeat(32));
await importKey(page, "0x" + "22".repeat(32));
await importPhrase(page);
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);
});
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);
});