// 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 inside a queue, so two background writes // never interleave, and 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 } = require("../shared/browserApi"); const { normalizePersisted } = require("../shared/persistedState"); // 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. async function getState() { const result = await storageGet("autistmask"); return normalizePersisted(result.autistmask); } // Serializes the read-modify-write turns below. Two of them interleaved would // each read before the other wrote, and the second write would carry the first // one's fields back to their pre-turn values. let updateQueue = Promise.resolve(); async function updateStateOnce(mutate) { const s = await getState(); await mutate(s); s.hasWallet = Boolean(s.wallets && s.wallets.length > 0); 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 window // between the read and the write is the window in which another context's // write is lost, and keeping it to one storage round trip is what makes a // whole-record write safe here. // // `mutate` must also not call updateState() itself, directly or through // anything it awaits: the queue is strictly serial, so the inner turn waits on // the outer one, which is waiting on the inner one. That deadlocks the whole // background, not just the caller. Mutate the record you were handed. // // Resolves with the record that was written. function updateState(mutate) { const turn = updateQueue.then(() => updateStateOnce(mutate)); // The queue must advance even when a turn rejects, or every update after // it queues behind a promise that never settles. updateQueue = turn.catch(() => {}); return turn; } module.exports = { getState, updateState };