Skip to main content

max / goingson

5.4 KB · 139 lines History Blame Raw
1 /**
2 * GoingsOn - App config cache.
3 *
4 * Settings used to live in localStorage. Step 6 of the portable-config migration
5 * moves the known config keys into the backend `user_config` table (synced by
6 * posture), while keeping a synchronous read API so the rest of the frontend does
7 * not have to become async.
8 *
9 * `GoingsOn.config` is a superset drop-in for the old localStorage calls:
10 * - a KNOWN config key (by its clean store name or its legacy localStorage
11 * name) is read from an in-memory cache and written through to the backend;
12 * - any other key passes through to localStorage unchanged.
13 *
14 * The cache is filled once at startup from `get_all_config`; consumers that read
15 * during app init must `await GoingsOn.config.ready` first. The one paint-critical
16 * key, `ui_mode`, is mirrored back to localStorage on every write so the
17 * pre-stylesheet `bootstrap-uimode.js` can still read it synchronously before the
18 * first paint.
19 */
20 (function () {
21 'use strict';
22
23 const invoke = window.__TAURI__.core.invoke;
24
25 // Legacy localStorage key -> clean store key. Callers may pass either.
26 const LEGACY = {
27 'goingson-theme': 'theme',
28 'goingson.uiMode': 'ui_mode',
29 'go-welcomed': 'welcomed',
30 'go-hint-shortcuts': 'hint_shortcuts',
31 'goingson-event-lead-minutes': 'event_lead_minutes',
32 'goingson-plan-nudges': 'plan_nudges',
33 'goingson-review-nudges': 'review_nudges',
34 'goingson-work-start-hour': 'work_start_hour',
35 'goingson-work-end-hour': 'work_end_hour',
36 };
37 // Store key -> its localStorage mirror key, for paint-critical keys the
38 // pre-paint bootstrap reads synchronously.
39 const MIRROR = { ui_mode: 'goingson.uiMode' };
40
41 const STORE_KEYS = new Set(Object.values(LEGACY));
42 const cache = new Map();
43 let resolveReady;
44 const ready = new Promise((r) => { resolveReady = r; });
45
46 /** The clean store key for a caller key (store name or legacy name), or null
47 * when the key is not a known config key. */
48 function norm(key) {
49 if (STORE_KEYS.has(key)) return key;
50 return LEGACY[key] || null;
51 }
52
53 function readLocal(key) {
54 try { return localStorage.getItem(key); } catch (e) { return null; }
55 }
56 function writeLocal(key, value) {
57 try { localStorage.setItem(key, value); } catch (e) { /* storage disabled */ }
58 }
59 function removeLocal(key) {
60 try { localStorage.removeItem(key); } catch (e) { /* storage disabled */ }
61 }
62
63 async function load() {
64 try {
65 const all = await invoke('get_all_config');
66 for (const k of Object.keys(all)) cache.set(k, all[k]);
67 // One-time migration: for any known key the backend does not yet have,
68 // copy the old localStorage value up. Idempotent (skipped once the
69 // backend has the key); localStorage is left intact as the ui_mode
70 // mirror and a harmless fallback.
71 for (const legacyKey of Object.keys(LEGACY)) {
72 const storeKey = LEGACY[legacyKey];
73 if (cache.has(storeKey)) continue;
74 const v = readLocal(legacyKey);
75 if (v === null) continue;
76 cache.set(storeKey, v);
77 try {
78 await invoke('set_config', { key: storeKey, value: v });
79 } catch (e) {
80 console.error('config migrate failed for', storeKey, e);
81 }
82 }
83 } catch (e) {
84 console.error('Failed to load config:', e);
85 } finally {
86 resolveReady();
87 }
88 }
89
90 GoingsOn.config = {
91 /** Resolves once the cache is populated. Await before reading at init. */
92 ready,
93
94 /** Synchronous read. Known keys come from the cache (falling back to the
95 * localStorage mirror, then `fallback`); unknown keys pass through to
96 * localStorage. Returns a string or `fallback` (default null). */
97 get(key, fallback = null) {
98 const storeKey = norm(key);
99 if (storeKey) {
100 if (cache.has(storeKey)) return cache.get(storeKey);
101 const mirrored = MIRROR[storeKey] ? readLocal(MIRROR[storeKey]) : null;
102 return mirrored !== null ? mirrored : fallback;
103 }
104 const v = readLocal(key);
105 return v !== null ? v : fallback;
106 },
107
108 /** Write. Known keys go to the cache + backend (+ localStorage mirror for
109 * paint-critical keys); unknown keys pass through to localStorage. */
110 set(key, value) {
111 value = String(value);
112 const storeKey = norm(key);
113 if (storeKey) {
114 cache.set(storeKey, value);
115 if (MIRROR[storeKey]) writeLocal(MIRROR[storeKey], value);
116 invoke('set_config', { key: storeKey, value }).catch((e) =>
117 console.error('set_config failed for', storeKey, e));
118 return;
119 }
120 writeLocal(key, value);
121 },
122
123 /** Remove a key. */
124 remove(key) {
125 const storeKey = norm(key);
126 if (storeKey) {
127 cache.delete(storeKey);
128 if (MIRROR[storeKey]) removeLocal(MIRROR[storeKey]);
129 invoke('delete_config', { key: storeKey }).catch((e) =>
130 console.error('delete_config failed for', storeKey, e));
131 return;
132 }
133 removeLocal(key);
134 },
135 };
136
137 load();
138 })();
139