Firefox gives an extension page in a private window locks of its own. A popup there could save between the background's read and its write of the stored record, and undo a network switch the background had saved. manifest/firefox.json now sets "incognito": "not_allowed", so Firefox never runs AutistMask in a private window. tests/manifest.test.js pins the key. The README and five code comments no longer describe a private-window exception to the lock. Model: opus-5-5
88 lines
4.4 KiB
JavaScript
88 lines
4.4 KiB
JavaScript
// The background's access to the persisted profile.
|
|
//
|
|
// There is no in-memory copy here, and that is the whole design. The MV3
|
|
// service worker is terminated when idle and revived by the next message, so
|
|
// anything held at module scope is either absent or arbitrarily stale, and
|
|
// src/shared/state.js's module-level `state` singleton — which nothing in the
|
|
// worker ever populates — silently served DEFAULT_STATE to whoever read it.
|
|
// Five defects came out of that (https://git.eeqj.de/sneak/AutistMask/issues/324),
|
|
// and every point fix for one of them added a loadState() that created the
|
|
// next: loading detaches the objects an in-flight handler is holding.
|
|
//
|
|
// So the background reads per call and writes read-modify-write:
|
|
//
|
|
// getState() one storage read, normalized, detached. Nothing else
|
|
// holds the object it returns, so a handler may keep it
|
|
// across any number of awaits and no concurrent work can
|
|
// move it.
|
|
// updateState(fn) read fresh, apply fn to that fresh record, write it
|
|
// back — all holding the lock every write of the stored
|
|
// record takes, so no other write lands in between, by
|
|
// the background or by any page (README, "Stored state
|
|
// and its version"). The read is one storage round trip
|
|
// ahead of the write rather than a page lifetime ahead of
|
|
// it (which is what made the popup's saveState() need a
|
|
// per-field merge against a baseline at all).
|
|
//
|
|
// A handler that must both read and write therefore does its network work
|
|
// against a snapshot it owns, and applies the RESULT inside updateState().
|
|
// It never publishes an object other in-flight work is holding.
|
|
|
|
const { storageGet, storageSet, withLock } = require("../shared/browserApi");
|
|
const { normalizePersisted } = require("../shared/persistedState");
|
|
const {
|
|
STATE_SCHEMA_VERSION,
|
|
assertStateUsable,
|
|
} = require("../shared/stateSchema");
|
|
|
|
// A fresh, fully-normalized, detached copy of the persisted profile.
|
|
//
|
|
// Normalized rather than raw: a legacy or malformed record is self-healed the
|
|
// same way loadState() heals it for the popup, so the background is never the
|
|
// one context reasoning about a shape the rest of the extension repairs.
|
|
//
|
|
// Throws StateUnusableError for a record this build cannot make sense of,
|
|
// before normalization gets a chance to paper over it — the same gate, in the
|
|
// same place, as the popup's loadState(). Every handler that consults the
|
|
// profile comes through here, so a dApp call against such a record is answered
|
|
// with the specific error the dispatcher maps that to (src/background/index.js)
|
|
// rather than dereferencing its way into a generic -32603.
|
|
async function getState() {
|
|
const result = await storageGet("autistmask");
|
|
assertStateUsable(result.autistmask);
|
|
return normalizePersisted(result.autistmask);
|
|
}
|
|
|
|
async function updateStateOnce(mutate) {
|
|
const s = await getState();
|
|
await mutate(s);
|
|
s.hasWallet = Boolean(s.wallets && s.wallets.length > 0);
|
|
// Stamped on every write, exactly as the popup's saveState() stamps it:
|
|
// whichever context writes last, the record in storage is in this build's
|
|
// shape and says so.
|
|
s.schemaVersion = STATE_SCHEMA_VERSION;
|
|
await storageSet({ autistmask: s });
|
|
return s;
|
|
}
|
|
|
|
// Apply `mutate` to a record read fresh from storage and write the result
|
|
// back. `mutate` receives a detached, normalized profile and mutates it in
|
|
// place; it may be async, but it must not do anything slow. The write is the
|
|
// WHOLE record, which is safe only because the lock (withLock() in
|
|
// src/shared/browserApi.js) is held from the read to the write: two
|
|
// background turns, or a turn and a page's save, never interleave
|
|
// (https://git.eeqj.de/sneak/AutistMask/issues/641). Every page's save waits
|
|
// while it is held (README, "Stored state and its version").
|
|
//
|
|
// `mutate` must also not call updateState() itself, directly or through
|
|
// anything it awaits: the inner turn waits for the lock the outer one holds,
|
|
// and the outer one is waiting on the inner one. That deadlocks every write of
|
|
// the stored record, not just the caller. Mutate the record you were handed.
|
|
//
|
|
// Resolves with the record that was written.
|
|
function updateState(mutate) {
|
|
return withLock("autistmask", () => updateStateOnce(mutate));
|
|
}
|
|
|
|
module.exports = { getState, updateState };
|