1// store.js: window.ermiteStore, a small client for what the pages remember in
2// the browser (localStorage): the preferences. Loaded before ermite.js, which
3// builds on it (ermiteKeys, ermiteLayout).
4//
5// It never throws. Without storage (disabled, private mode, sandboxed frame)
6// or when the browser refuses a write (quota), the value is held in memory:
7// the choice lasts for this page, and the page works as if it were saved.
8// Values are strings, as in localStorage; null says "nothing stored".
9//
10// get(key, fallback) the value, or fallback (null) when there is none
11// set(key, value) stores it; null or undefined removes the key
12// remove(key) forgets the key
13// watch(key, fn) fn(value or null) when another tab changes the key
14// (the "storage" event); answers a function that stops
15// pref(key, opts) a preference among a closed list:
16// pref(key, { values, fallback }) answers
17// { get, set, watch }, where get is always one of
18// values (a stored value that is not falls back) and
19// set ignores what is not
20//
21// Self-contained: jstest/store.test.mjs evaluates this file alone.
22window.ermiteStore = (() => {
23 // What storage could not keep, by key. A key mapped to null is removed for
24 // this page although storage would not say so.
25 const held = new Map();
26
27 const area = () => {
28 try { return localStorage; } catch { return null; }
29 };
30
31 function get(key, fallback = null) {
32 if (held.has(key)) return held.get(key) ?? fallback;
33 try { return area().getItem(key) ?? fallback; } catch { return fallback; }
34 }
35
36 function set(key, value) {
37 if (value === null || value === undefined) return remove(key);
38 value = String(value);
39 try {
40 area().setItem(key, value);
41 held.delete(key);
42 } catch { held.set(key, value); }
43 }
44
45 function remove(key) {
46 try {
47 area().removeItem(key);
48 held.delete(key);
49 } catch { held.set(key, null); }
50 }
51
52 function watch(key, fn) {
53 const on = (e) => {
54 // key is null when another tab clears the whole storage.
55 if (e.key !== null && e.key !== key) return;
56 if (e.storageArea && e.storageArea !== area()) return;
57 held.delete(key); // the other tab's word is newer than ours
58 fn(e.key === null ? null : (e.newValue ?? null));
59 };
60 addEventListener('storage', on);
61 return () => removeEventListener('storage', on);
62 }
63
64 function pref(key, { values, fallback }) {
65 const valid = (v) => (values.includes(v) ? v : fallback);
66 return {
67 get: () => valid(get(key)),
68 set(v) {
69 if (values.includes(v)) set(key, v);
70 },
71 watch: (fn) => watch(key, (v) => fn(valid(v))),
72 };
73 }
74
75 return { get, set, remove, watch, pref };
76})();