fix: a popup save no longer undoes a network switch the background saved during it (closes #641)
check / check (push) Canceled after 0s
e2e / e2e-chrome (push) Canceled after 0s
e2e / e2e-firefox (push) Canceled after 0s

The popup's saveState() and the background's updateState() each read the
stored record, change it and write it back, and each was ordered only within
its own context, so a write by one could land between the other's read and
write and be lost. Both now hold the browser's lock (navigator.locks, shared
by every page and the background except a page in a Firefox private window)
from the read to the write, through withLock() in src/shared/browserApi.js;
the recovery screen's reset takes it too. The lock also orders a page's own
saves, so both per-context queues are gone. The unit tests get a small lock
from a Jest setup file.

Model: opus-5-5
This commit is contained in:
2026-10-20 16:58:12 +00:00
parent 4472746b89
commit db0d81e78e
16 changed files with 231 additions and 62 deletions
+13
View File
@@ -1389,6 +1389,19 @@ carries a `schemaVersion` — `STATE_SCHEMA_VERSION` in
`saveState()` and the background's `updateState()` both do, so whichever context
wrote last, the record says which build's shape it is in.
Each write reads the record, changes it and writes it back, holding a lock from
the read to the write: the browser's own (`navigator.locks`), taken through
`withLock()` in `src/shared/browserApi.js`. Every page of the extension and its
background share it, except a page in a Firefox private window, which Firefox
gives locks of its own (see https://git.eeqj.de/sneak/AutistMask/issues/646).
The popup's `saveState()`, the background's `updateState()` and the
StateRecovery reset all take it, so among the pages and the background that
share it, no write lands between another write's read and its write, where it
would be lost. Chrome has it from version 69, older than any Chrome that runs a
Manifest V3 extension, in pages and in the service worker; Firefox from version
96, older than the 115 `manifest/firefox.json` declares. Where it is missing, a
write fails rather than going ahead without it.
Version 1 is the shape that shipped before versions existed, so a stored record
with no `schemaVersion` is version 1 rather than a defect: it loads normally and
is migrated in place by being stamped on the first write. An upgrade never shows