// A user's synced display preferences: theme, default Reader translation, verse layout, the // commentary they last chose, and where they left off in the Reader. Each preference carries // its own timestamp ({ v, at }) so the most recent change to each one wins, device by device. const ENUMS = { theme: ['paper', 'sepia', 'dark'], translation: ['BSB', 'ESV', 'NLT', 'CSB', 'NASB', 'NET'], layout: ['flow', 'verses'], commentarySource: ['matthew-henry', 'john-gill', 'jamieson-fausset-brown', 'tyndale', 'adam-clarke', 'john-calvin', 'keil-delitzsch'], }; const DAY_MS = 86_400_000; // A cleaned value, or undefined when it isn't acceptable for that preference. export function cleanPrefValue(key, value) { if (ENUMS[key]) return ENUMS[key].includes(value) ? value : undefined; if (key === 'position') { if (!value || typeof value !== 'object') return undefined; const book = String(value.book ?? '').toUpperCase(); if (!/^[1-3A-Z]{3}$/.test(book) || !Number.isInteger(value.chapter) || value.chapter < 1 || value.chapter > 200) return undefined; return { book, chapter: value.chapter }; } return undefined; } // Body -> { key: { v, at } } with only known keys, valid values and sane timestamps. export function cleanPrefsPatch(body, now = Date.now()) { const out = {}; const prefs = body?.prefs; if (!prefs || typeof prefs !== 'object' || Array.isArray(prefs)) return out; for (const [key, entry] of Object.entries(prefs)) { const v = cleanPrefValue(key, entry?.v); const at = entry?.at; if (v === undefined || !Number.isFinite(at) || at < 0 || at > now + DAY_MS) continue; out[key] = { v, at }; } return out; } // Per key, the entry with the newer timestamp wins (the stored one on a tie). export function mergePrefs(existing = {}, patch = {}) { const merged = { ...existing }; for (const [key, entry] of Object.entries(patch)) { if (!merged[key] || entry.at > merged[key].at) merged[key] = entry; } return merged; }