// The prayer journal, kept per account. Each prayer carries its own `updatedAt`, so two devices that each add or // change prayers merge cleanly: the newer version of each prayer wins, the days prayed are combined, and a // deleted prayer stays as a small marker for a while so another device can't bring it back. const ID = /^[\w-]{1,64}$/; const DAY = /^\d{4}-\d{2}-\d{2}$/; const BOOK = /^[1-3A-Z]{3}$/; const MAX_PRAYERS = 2000; const MAX_DAYS = 400; const KEEP_DELETED_MS = 90 * 86_400_000; const text = (v, max) => (typeof v === 'string' ? v.slice(0, max) : ''); const int = (v, min, max) => Number.isInteger(v) && v >= min && v <= max; const time = (v, fallback) => (Number.isFinite(v) && v > 0 ? v : fallback); // One prayer from the browser, rebuilt field by field; null when it isn't one. export function cleanPrayer(raw, now = Date.now()) { if (!raw || typeof raw !== 'object' || !ID.test(String(raw.id ?? ''))) return null; const updatedAt = Math.min(time(raw.updatedAt, 0), now + 86_400_000); if (raw.deleted === true) return { id: raw.id, deleted: true, updatedAt }; const body = text(raw.text, 2000).trim(); if (!body) return null; const verses = (Array.isArray(raw.verses) ? raw.verses : []).slice(0, 8) .filter((v) => v && BOOK.test(String(v.book)) && int(v.chapter, 1, 200) && (v.verse == null || int(v.verse, 1, 200))) .map((v) => ({ book: v.book, chapter: v.chapter, verse: v.verse ?? null, label: text(v.label, 60) })); const prayedOn = [...new Set((Array.isArray(raw.prayedOn) ? raw.prayedOn : []).filter((d) => typeof d === 'string' && DAY.test(d)))].sort().slice(-MAX_DAYS); return { id: raw.id, text: body, forWhom: text(raw.forWhom, 80).trim(), verses, prayedOn, createdAt: time(raw.createdAt, updatedAt), updatedAt, answeredAt: Number.isFinite(raw.answeredAt) && raw.answeredAt > 0 ? raw.answeredAt : null, answerNote: text(raw.answerNote, 1000).trim(), }; } export function cleanPrayers(list, now = Date.now()) { const seen = new Set(); const out = []; for (const raw of (Array.isArray(list) ? list : []).slice(0, MAX_PRAYERS)) { const p = cleanPrayer(raw, now); if (!p || seen.has(p.id)) continue; seen.add(p.id); out.push(p); } return out; } // Two copies of the journal -> one. The newer version of each prayer wins; the days prayed are the union of both. export function mergePrayers(a, b, now = Date.now()) { const byId = new Map(); for (const p of [...cleanPrayers(a, now), ...cleanPrayers(b, now)]) { const have = byId.get(p.id); if (!have) { byId.set(p.id, p); continue; } const [newer, older] = p.updatedAt >= have.updatedAt ? [p, have] : [have, p]; byId.set(p.id, newer.deleted ? newer : { ...newer, prayedOn: [...new Set([...(newer.prayedOn ?? []), ...(older.deleted ? [] : older.prayedOn ?? [])])].sort().slice(-MAX_DAYS) }); } return [...byId.values()] .filter((p) => !(p.deleted && now - p.updatedAt > KEEP_DELETED_MS)) // old markers are forgotten .sort((x, y) => (y.updatedAt ?? 0) - (x.updatedAt ?? 0)); } export const openPrayers = (list) => list.filter((p) => !p.deleted && !p.answeredAt);