fix: a change made while an earlier save is running is stored (closes #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, and so was a wallet added, a site revoked or an endpoint changed during the write. The save now copies the page's fields when it starts, writes from that copy, and keeps the copy as the baseline, so anything changed after the copy is still a difference for the next save. Model: opus-5-5
This commit is contained in:
@@ -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