wallet-kit / test / helpers / fake-storage.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
 * 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;
    },
  };
}

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/wallet-kit.git