/**
* An in-memory localStorage for node, with a switch for making writes fail.
*
* LedgerStore reads storage in its constructor and writes on every mutation, so
* the tests need both halves: a store that behaves, and one that throws the way
* a real browser throws when the origin's quota is gone or the user has turned
* site data off. The second is the interesting one — the kit's rule is that a
* failed write costs durability and never history.
*
* `raceOnce` is the third half, and it exists because a read-modify-write over
* localStorage is not atomic and node cannot show that on its own. A second tab
* is a second AGENT: its write can complete anywhere inside our read → write
* window, which node's single thread will never do by itself. `raceOnce(fn)`
* runs `fn` exactly once, immediately after the next `getItem` returns — which
* is the widest and most damaging position in that window, since everything
* after it (parse, merge, reindex, stringify) is computed from bytes that are
* already stale.
*/
export function installFakeStorage() {
const map = new Map();
const state = { failWrites: null, race: null };
globalThis.localStorage = {
getItem: (k) => {
const v = map.has(k) ? map.get(k) : null;
const race = state.race;
if (race) {
state.race = null;
race(k, v);
}
return v;
},
setItem: (k, v) => {
if (state.failWrites) throw state.failWrites;
map.set(k, String(v));
},
removeItem: (k) => void map.delete(k),
clear: () => map.clear(),
};
return {
map,
/** Make every subsequent write throw what a browser would throw. */
failWrites(on = true) {
if (!on) {
state.failWrites = null;
return;
}
const e = new Error("The quota has been exceeded.");
e.name = "QuotaExceededError";
state.failWrites = e;
},
/**
* Run `fn(key, valueRead)` once, right after the next getItem returns — a
* concurrent writer landing inside somebody's read-modify-write window.
* Cleared before `fn` runs, so a writer that reads storage itself does not
* re-arm it.
*/
raceOnce(fn) {
state.race = fn;
},
reset() {
map.clear();
state.failWrites = null;
state.race = null;
},
};
}