fix: a change made while an earlier save is running is stored #450
@@ -45,6 +45,16 @@ but the review is broader than any of them.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-05: A change made while an earlier save from the same page is still
|
||||||
|
running is stored ([#448](https://git.eeqj.de/sneak/AutistMask/issues/448)).
|
||||||
|
`saveStateOnce()` took its baseline from the page's state after the write, so
|
||||||
|
a change made while the save waited on storage counted as already stored and
|
||||||
|
the save queued after it wrote nothing. A setting changed during the read was
|
||||||
|
lost; so was a wallet added, a site revoked or an endpoint changed during the
|
||||||
|
write, and a wallet deleted then stayed in storage. The save now copies the
|
||||||
|
page's fields when it starts, writes from that copy, and keeps the copy as the
|
||||||
|
baseline.
|
||||||
|
|
||||||
- 2026-10-05: The extension no longer opens a window for a site-connection
|
- 2026-10-05: The extension no longer opens a window for a site-connection
|
||||||
prompt already answered
|
prompt already answered
|
||||||
([#287](https://git.eeqj.de/sneak/AutistMask/issues/287)). When the prompt was
|
([#287](https://git.eeqj.de/sneak/AutistMask/issues/287)). When the prompt was
|
||||||
|
|||||||
+12
-5
@@ -122,9 +122,9 @@ function currentNetwork() {
|
|||||||
return networkById(state.networkId);
|
return networkById(state.networkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The persisted fields as they stood at the end of this page's last
|
// The persisted fields as this page held them when its last loadState()
|
||||||
// loadState() or saveState(). saveState() diffs the live state against this
|
// finished, or when its last successful saveState() began. saveState() diffs
|
||||||
// to find only the fields THIS page actually changed.
|
// the live state against this to find only the fields THIS page changed since.
|
||||||
//
|
//
|
||||||
// Deep-cloned, not a reference: callers mutate persisted objects and arrays
|
// Deep-cloned, not a reference: callers mutate persisted objects and arrays
|
||||||
// in place (state.wallets.push(...)), and a reference baseline would mutate
|
// in place (state.wallets.push(...)), and a reference baseline would mutate
|
||||||
@@ -464,7 +464,10 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
|||||||
// does not own goes on being whatever its last loadState() saw, same as
|
// does not own goes on being whatever its last loadState() saw, same as
|
||||||
// before this fix; only the persisted record is guaranteed current.
|
// before this fix; only the persisted record is guaranteed current.
|
||||||
async function saveStateOnce() {
|
async function saveStateOnce() {
|
||||||
const current = snapshotPersisted();
|
// A copy, so what this save compares and writes is the page's state as it
|
||||||
|
// stood when the save began. A change made while it waits on storage is
|
||||||
|
// left for the next save, which compares against this copy.
|
||||||
|
const current = structuredClone(snapshotPersisted());
|
||||||
const result = await storageGet("autistmask");
|
const result = await storageGet("autistmask");
|
||||||
// The record in storage right now is about to be merged into and written
|
// The record in storage right now is about to be merged into and written
|
||||||
// back, so it is validated exactly like a load validates it. Without this,
|
// back, so it is validated exactly like a load validates it. Without this,
|
||||||
@@ -521,7 +524,11 @@ async function saveStateOnce() {
|
|||||||
// exactly as it stood; see the note above.
|
// exactly as it stood; see the note above.
|
||||||
rawState.hasWallet = rawState.wallets.length > 0;
|
rawState.hasWallet = rawState.wallets.length > 0;
|
||||||
|
|
||||||
baseline = structuredClone(snapshotPersisted());
|
// What this save compared and wrote, not the page's state now: a change
|
||||||
|
// made during the save must still differ from the baseline, or the save
|
||||||
|
// queued after it finds nothing to store
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/448).
|
||||||
|
baseline = current;
|
||||||
}
|
}
|
||||||
|
|
||||||
// showView() calls saveState() on every navigation without awaiting it, so
|
// showView() calls saveState() on every navigation without awaiting it, so
|
||||||
|
|||||||
@@ -423,3 +423,80 @@ describe("two wallets independently created with a colliding identity", () => {
|
|||||||
expect(secrets).toContain("secret-b");
|
expect(secrets).toContain("secret-b");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// showView() saves on every navigation without waiting, so the user can change
|
||||||
|
// something while that save is still waiting on storage. The change is followed
|
||||||
|
// by its own saveState(), which runs after the first save; it must be stored
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/448).
|
||||||
|
describe("a change made while an earlier save from the same page is running", () => {
|
||||||
|
// Runs `change` inside the next call to `op` (the stub's get or set),
|
||||||
|
// before that call does its work.
|
||||||
|
function runInside(op, change) {
|
||||||
|
const real = op.getMockImplementation();
|
||||||
|
op.mockImplementationOnce(async (arg) => {
|
||||||
|
change();
|
||||||
|
return real(arg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a network switched during the earlier save's read is stored", async () => {
|
||||||
|
const storage = makeStorageStub({
|
||||||
|
autistmask: { wallets: [W1], networkId: "sepolia" },
|
||||||
|
});
|
||||||
|
const { state, saveState, loadState } = loadPage(storage).state;
|
||||||
|
await loadState();
|
||||||
|
|
||||||
|
let queued;
|
||||||
|
runInside(storage.get, () => {
|
||||||
|
state.networkId = "mainnet";
|
||||||
|
queued = saveState();
|
||||||
|
});
|
||||||
|
state.theme = "dark";
|
||||||
|
await saveState();
|
||||||
|
await queued;
|
||||||
|
|
||||||
|
const stored = storage.read("autistmask");
|
||||||
|
expect(stored.theme).toBe("dark");
|
||||||
|
expect(stored.networkId).toBe("mainnet");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wallet added during the earlier save's read is stored", async () => {
|
||||||
|
const storage = makeStorageStub({ autistmask: { wallets: [W1] } });
|
||||||
|
const { state, saveState, loadState } = loadPage(storage).state;
|
||||||
|
await loadState();
|
||||||
|
|
||||||
|
let queued;
|
||||||
|
runInside(storage.get, () => {
|
||||||
|
state.wallets.push(W2);
|
||||||
|
queued = saveState();
|
||||||
|
});
|
||||||
|
await saveState();
|
||||||
|
await queued;
|
||||||
|
|
||||||
|
const stored = storage.read("autistmask");
|
||||||
|
expect(stored.wallets.map((w) => w.encryptedSecret)).toEqual([
|
||||||
|
"secret-one",
|
||||||
|
"secret-two",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a wallet added during the earlier save's write is stored", async () => {
|
||||||
|
const storage = makeStorageStub({ autistmask: { wallets: [W1] } });
|
||||||
|
const { state, saveState, loadState } = loadPage(storage).state;
|
||||||
|
await loadState();
|
||||||
|
|
||||||
|
let queued;
|
||||||
|
runInside(storage.set, () => {
|
||||||
|
state.wallets.push(W2);
|
||||||
|
queued = saveState();
|
||||||
|
});
|
||||||
|
await saveState();
|
||||||
|
await queued;
|
||||||
|
|
||||||
|
const stored = storage.read("autistmask");
|
||||||
|
expect(stored.wallets.map((w) => w.encryptedSecret)).toEqual([
|
||||||
|
"secret-one",
|
||||||
|
"secret-two",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user