/** * window.MemberState — per-member persistence layer for the site. * * Usage: * await MemberState.ready(); // resolves once member is known * const blob = await MemberState.load(); * await MemberState.merge({ urge_log: [...] }); * const isMember = MemberState.isMember(); * * For signed-out users: falls back to localStorage entirely (no KV writes). * For signed-in members: read/write through worker; localStorage stays as cache. * * Per-widget keys live inside the blob (urge_log, halt_helpful, drink_calc, etc.). */ (function(){ var WORKER = "https://member-state-worker.5vyhp99qcj.workers.dev"; var LOCAL_KEY = "qg_member_state_cache_v1"; var member = null; // { uuid, email } or null var blob = null; // current state (cache) var readyResolvers = []; var ready = false; function readLocal() { try { return JSON.parse(localStorage.getItem(LOCAL_KEY)) || {}; } catch (e) { return {}; } } function writeLocal(b) { try { localStorage.setItem(LOCAL_KEY, JSON.stringify(b)); } catch (e) {} } async function detectMember() { try { var r = await fetch('/members/api/member/', { credentials: 'same-origin' }); if (!r.ok) return null; var m = await r.json(); if (m && m.uuid && m.email) return { uuid: m.uuid, email: m.email }; } catch (e) {} return null; } async function fetchKv(uuid) { try { var r = await fetch(WORKER + '/state?uuid=' + encodeURIComponent(uuid), { method: 'GET' }); if (!r.ok) return null; var j = await r.json(); return (j && j.ok) ? (j.blob || {}) : null; } catch (e) { return null; } } async function writeKv(uuid, b) { try { var r = await fetch(WORKER + '/state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: uuid, blob: b }) }); return r.ok; } catch (e) { return false; } } async function mergeKv(uuid, patch) { try { var r = await fetch(WORKER + '/state/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: uuid, patch: patch }) }); if (!r.ok) return null; var j = await r.json(); return (j && j.ok) ? (j.blob || null) : null; } catch (e) { return null; } } async function init() { member = await detectMember(); var local = readLocal(); if (!member) { // Signed out — localStorage only blob = local; } else { // Signed in — KV is source of truth var kv = await fetchKv(member.uuid); if (kv === null) { // Worker unreachable; fall back to local blob = local; } else if (Object.keys(kv).length === 0 && Object.keys(local).length > 0) { // One-time migration: local has data, KV is empty → upload local await writeKv(member.uuid, local); blob = local; } else { // KV has data (or both empty) → prefer KV blob = kv; writeLocal(kv); // sync local cache } } ready = true; var resolvers = readyResolvers; readyResolvers = []; resolvers.forEach(function(fn){ try { fn(); } catch(e){} }); } window.MemberState = { ready: function() { if (ready) return Promise.resolve(); return new Promise(function(resolve){ readyResolvers.push(resolve); }); }, isMember: function() { return !!member; }, memberUuid: function() { return member ? member.uuid : null; }, memberEmail: function() { return member ? member.email : null; }, load: async function() { await this.ready(); return blob || {}; }, save: async function(newBlob) { blob = newBlob || {}; writeLocal(blob); if (member) await writeKv(member.uuid, blob); return blob; }, merge: async function(patch) { await this.ready(); // Optimistic local merge blob = Object.assign({}, blob || {}, patch || {}); writeLocal(blob); if (member) { var server = await mergeKv(member.uuid, patch); if (server) { blob = server; writeLocal(blob); } } return blob; }, // Convenience wrappers for a single key inside the blob get: async function(key, fallback) { await this.ready(); return (blob && key in blob) ? blob[key] : fallback; }, set: async function(key, value) { var patch = {}; patch[key] = value; return this.merge(patch); } }; init(); })();