// Combines two copies of one study (this device's and the other device's) chunk by chunk, so a // conflict doesn't force a choice between them. Chunks are matched by id; each chunk carries the time it // was last edited here (editedAt). Nothing the person wrote is dropped: where both copies changed the // same note, the newer one stays and the older one is appended under a marker. const TEXT_FIELDS = ['observation', 'interpretation', 'application', 'generalNotes', 'finalScript', 'episodeNumber', 'episodeTitle']; const LIST_FIELDS = ['tags', 'crossReferences', 'greekWords']; export const MERGE_MARKER = '— from your other device —'; const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); const blank = (s) => !(s ?? '').toString().trim(); function mergeText(newer, older) { if (same(newer, older) || blank(older)) return newer ?? ''; if (blank(newer)) return older; if (newer.includes(older)) return newer; if (older.includes(newer)) return older; return `${newer.replace(/\s+$/, '')}\n\n${MERGE_MARKER}\n${older}`; } function union(a = [], b = []) { const seen = new Set(); return [...a, ...b].filter((item) => { const k = JSON.stringify(item); if (seen.has(k)) return false; seen.add(k); return true; }); } export function mergeChunk(mine, theirs) { if (same(mine, theirs)) return { chunk: mine, changed: false }; const mineNewer = (mine.editedAt ?? 0) >= (theirs.editedAt ?? 0); const [newer, older] = mineNewer ? [mine, theirs] : [theirs, mine]; const out = { ...older, ...newer }; TEXT_FIELDS.forEach((f) => { out[f] = mergeText(newer[f], older[f]); }); LIST_FIELDS.forEach((f) => { out[f] = union(newer[f], older[f]); }); out.editedAt = Math.max(mine.editedAt ?? 0, theirs.editedAt ?? 0) || undefined; return { chunk: out, changed: !same(out, mine) }; } const chapterKey = (ch) => `${ch.bookAbbrev}|${ch.chapter}`; // -> { project, stats: { added, merged } } where `added` counts chunks that only the other copy had and // `merged` counts chunks both copies had that differed. Settings such as the title and the open chunk stay mine. export function mergeStudies(mine, theirs) { const stats = { added: 0, merged: 0 }; const theirChapters = new Map((theirs.chapters ?? []).map((ch) => [chapterKey(ch), ch])); const chapters = (mine.chapters ?? []).map((ch) => { const other = theirChapters.get(chapterKey(ch)); theirChapters.delete(chapterKey(ch)); if (!other) return ch; const theirChunks = new Map((other.chunks ?? []).map((c) => [c.id, c])); const chunks = (ch.chunks ?? []).map((c) => { const o = theirChunks.get(c.id); theirChunks.delete(c.id); if (!o) return c; const { chunk, changed } = mergeChunk(c, o); if (changed) stats.merged += 1; return chunk; }); const extra = [...theirChunks.values()]; stats.added += extra.length; return { ...ch, chunks: [...chunks, ...extra] }; }); for (const ch of theirChapters.values()) { stats.added += (ch.chunks ?? []).length; chapters.push(ch); } const ids = new Set(chapters.flatMap((c) => c.chunks.map((k) => k.id))); return { project: { ...mine, chapters, selectedChunkId: ids.has(mine.selectedChunkId) ? mine.selectedChunkId : [...ids][0] ?? null }, stats, }; }