(function (root, factory) { const createUnfiledDraftSync = factory(); if (typeof module === 'object' && module.exports) module.exports = createUnfiledDraftSync; if (root) root.createUnfiledDraftSync = createUnfiledDraftSync; })(typeof globalThis !== 'undefined' ? globalThis : this, function () { function createUnfiledDraftSync(options, mountedFetch) { if (mountedFetch) return createUnfiledDraftSync.mount(options, mountedFetch); const { captures, fetchJson, storage, getLogin = () => '', onState = () => {}, createConflictId = () => 'conflict-' + (globalThis.crypto?.randomUUID?.() || Date.now().toString(36)), } = options; const key = 'stackchain.unfiled-draft-sync.v1'; let inFlight = null; let rerun = false; function read(login) { try { const all = JSON.parse(storage?.getItem(key) || '{}'); const state = all?.[login]; return { all:all && typeof all === 'object' ? all : {}, known:new Set(Array.isArray(state?.known) ? state.known : []), deleted:new Set(Array.isArray(state?.deleted) ? state.deleted : []), base:state?.base && typeof state.base === 'object' ? state.base : {}, }; } catch (_error) { return {all:{}, known:new Set(), deleted:new Set(), base:{}}; } } function write(login, state) { state.all[login] = {known:[...state.known], deleted:[...state.deleted], base:state.base}; storage?.setItem(key, JSON.stringify(state.all)); } function publish(status, message) { onState({status, ...(message ? {message} : {})}); const visibleStatus = globalThis.document?.querySelector?.('#my-work-action-status'); if (message && visibleStatus) visibleStatus.textContent = message; } function sameDrafts(left, right) { return JSON.stringify(left) === JSON.stringify(right); } async function fingerprint(draft) { if (!draft) return null; const text = JSON.stringify(draft); if (globalThis.crypto?.subtle && globalThis.TextEncoder) { const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)); return [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join(''); } let first = 2166136261, second = 3335557771; for (let index = 0; index < text.length; index += 1) { first = Math.imul(first ^ text.charCodeAt(index), 16777619); second = Math.imul(second ^ text.charCodeAt(index), 2246822519); } return text.length + ':' + (first >>> 0).toString(16) + (second >>> 0).toString(16); } async function reconcile(captures, drafts, owner) { if (typeof captures.reconcileRemote === 'function') { await captures.reconcileRemote(drafts, owner); return; } const finalIds = new Set(drafts.map(item => item.id)); for (const item of await captures.exportOwned(owner)) { if (!finalIds.has(item.id)) await captures.discard(item.id); } await captures.mergeRemote(drafts, owner); } async function runSync(login) { const owner = String(login || '').trim(); if (!owner) return false; publish('syncing', 'Syncing Drafts…'); const state = read(owner); try { const remote = await fetchJson('api/v1/unfiled-drafts'); const remoteDrafts = Array.isArray(remote?.drafts) ? remote.drafts : []; const local = await captures.exportOwned(owner); const merged = []; const localById = new Map(local.map(item => [item.id, item])); const remoteById = new Map(remoteDrafts.map(item => [item.id, item])); const ids = new Set([...state.known, ...localById.keys(), ...remoteById.keys()]); let preservedConflict = false; const usedIds = new Set(ids); for (const id of ids) { const localDraft = state.deleted.has(id) ? null : localById.get(id); const remoteDraft = remoteById.get(id); const localHash = await fingerprint(localDraft); const remoteHash = await fingerprint(remoteDraft); const baseHash = state.base[id] || null; const localChanged = localHash !== baseHash; const remoteChanged = remoteHash !== baseHash; if (localChanged && remoteChanged && localHash !== remoteHash) { preservedConflict = true; if (remoteDraft) merged.push(remoteDraft); if (localDraft) { let conflictId = String(createConflictId()).slice(0, 100); for (let suffix = 2; usedIds.has(conflictId); suffix += 1) { conflictId = (String(createConflictId()).slice(0, 95) + '-' + suffix).slice(0, 100); } usedIds.add(conflictId); const suffix = ' (conflict copy)'; merged.push({...localDraft, id:conflictId, title:(String(localDraft.title).slice(0, 255 - suffix.length) + suffix)}); } continue; } const selected = localChanged ? localDraft : (remoteChanged ? remoteDraft : (localDraft || remoteDraft)); if (selected) merged.push(selected); } merged.sort((left, right) => Number(right.saved_at) - Number(left.saved_at)); let finalSnapshot = remote; if (!sameDrafts(merged, remoteDrafts)) { finalSnapshot = await fetchJson('api/v1/unfiled-drafts', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({revision:Number(remote.revision || 0), drafts:merged}), }); } await reconcile(captures, finalSnapshot.drafts || [], owner); state.known = new Set((finalSnapshot.drafts || []).map(item => item.id)); state.deleted.clear(); state.base = {}; for (const item of finalSnapshot.drafts || []) state.base[item.id] = await fingerprint(item); write(owner, state); publish(preservedConflict ? 'conflict-preserved' : 'ready', preservedConflict ? 'Draft changed on two devices · both versions were saved.' : 'Drafts synced across devices.'); return true; } catch (error) { if (error?.status === 409) { publish('conflict', 'Drafts changed on another device. Sync again to combine them.'); } else { publish('pending', 'Saved on this device · sync pending'); } throw error; } } function sync(login) { const owner = String(login || '').trim(); if (!owner) return Promise.resolve(false); if (inFlight) { rerun = true; return inFlight; } inFlight = (async () => { let result = false; do { rerun = false; result = await runSync(owner); } while (rerun); return result; })().finally(() => { inFlight = null; }); return inFlight; } async function remove(id, login) { const owner = String(login || '').trim(); if (!owner) return false; const state = read(owner); state.deleted.add(String(id)); write(owner, state); await captures.discard(String(id)); return sync(owner); } captures.subscribe?.((id, removed) => { const login = getLogin(); if (login) void (removed ? remove(id, login) : sync(login)).catch(() => {}); }); return {sync, remove}; } createUnfiledDraftSync.mount = (captures, fetchJson) => { const getLogin = captures.currentLogin; const controller = createUnfiledDraftSync({captures, fetchJson, storage:globalThis.localStorage, getLogin}); const connect = () => { const login = getLogin(); if (login) void controller.sync(login).catch(() => {}); else globalThis.setTimeout(connect, 250); }; globalThis.setTimeout(connect, 0); globalThis.addEventListener?.('online', connect); return controller; }; return createUnfiledDraftSync; });