(function () { if (!window.MDHubCache) { return; } const syncQueue = document.querySelector("[data-sync-queue]"); const syncQueueTitle = document.querySelector("[data-sync-queue-title]"); const syncQueueSummary = document.querySelector("[data-sync-queue-summary]"); const syncQueueList = document.querySelector("[data-sync-queue-list]"); const syncNow = document.querySelector("[data-sync-now]"); function normalizeDocPath(path) { if (!path) return ""; return path.replace(/^\/+/, ""); } function formatPath(path) { return (path || "").replace(/^\/docs\//, "") || "/"; } function pluralize(count, singular, plural) { return count === 1 ? singular : plural; } function emitSyncState(detail) { window.dispatchEvent(new CustomEvent("mdhub:sync-state", { detail: detail })); } function renderPendingEdits(pending, state) { if (!syncQueue || !syncQueueTitle || !syncQueueSummary || !syncQueueList) { return; } const conflicts = pending.filter(function (edit) { return Boolean(edit.conflict); }); const clean = pending.filter(function (edit) { return !edit.conflict; }); syncQueue.hidden = pending.length === 0; if (pending.length === 0) { syncQueueList.replaceChildren(); syncQueueTitle.textContent = "Synced"; syncQueueSummary.textContent = "No pending changes."; if (syncNow) { syncNow.disabled = true; } return; } const countLabel = pending.length + " pending " + pluralize(pending.length, "change", "changes"); syncQueueTitle.textContent = conflicts.length > 0 ? countLabel + " with conflicts" : countLabel; if (!navigator.onLine) { syncQueueSummary.textContent = "Offline. Changes will sync when the connection returns."; } else if (state === "syncing") { syncQueueSummary.textContent = "Syncing queued edits..."; } else if (conflicts.length > 0) { syncQueueSummary.textContent = conflicts.length + " " + pluralize(conflicts.length, "file needs", "files need") + " conflict resolution."; } else { syncQueueSummary.textContent = "Queued edits are ready to sync."; } syncQueueList.replaceChildren(); pending.slice(0, 5).forEach(function (edit) { const item = document.createElement("li"); item.dataset.state = edit.conflict ? "conflict" : "queued"; const path = document.createElement("span"); path.textContent = formatPath(edit.path); item.appendChild(path); const status = document.createElement("span"); status.textContent = edit.conflict ? "Conflict" : "Queued"; item.appendChild(status); syncQueueList.appendChild(item); }); if (pending.length > 5) { const overflow = document.createElement("li"); overflow.textContent = "+" + (pending.length - 5) + " more"; syncQueueList.appendChild(overflow); } if (syncNow) { syncNow.disabled = !navigator.onLine || clean.length === 0 || state === "syncing"; } } async function refreshSyncQueue(state) { const pending = await window.MDHubCache.getPendingEdits(); pending.sort(function (a, b) { return (b.updatedAt || 0) - (a.updatedAt || 0); }); renderPendingEdits(pending, state || "idle"); emitSyncState({ state: state || "idle", pending: pending.length, conflicts: pending.filter(function (edit) { return Boolean(edit.conflict); }).length, }); return pending; } async function postDocument(path, content, baseHash) { const response = await fetch("/api/documents/" + normalizeDocPath(path), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content: content, baseHash: baseHash || "" }), }); const payload = await response.json().catch(function () { return {}; }); if (!response.ok) { if (response.status === 409) { const conflict = new Error("document conflict"); conflict.name = "DocumentConflictError"; conflict.details = payload; throw conflict; } throw new Error("save failed"); } return payload; } async function saveDocument(path, content, baseHash) { const normalizedPath = window.MDHubCache.normalizePath("/docs/" + normalizeDocPath(path)); try { const result = await postDocument(path, content, baseHash); await window.MDHubCache.deletePendingEdit(normalizedPath); await refreshSyncQueue("saved"); return { status: "saved", result: result, }; } catch (error) { if (error.name === "DocumentConflictError") { await window.MDHubCache.putPendingEdit(normalizedPath, content, baseHash); await window.MDHubCache.markPendingEditConflict(normalizedPath, error.details); await refreshSyncQueue("conflict"); return { status: "conflict", conflict: error.details, }; } await window.MDHubCache.putPendingEdit(normalizedPath, content, baseHash); await refreshSyncQueue("queued"); return { status: "queued", }; } } async function syncPending() { if (!navigator.onLine) { await refreshSyncQueue("offline"); return; } const pending = await refreshSyncQueue("syncing"); pending.sort(function (a, b) { return (a.updatedAt || 0) - (b.updatedAt || 0); }); for (const edit of pending) { if (edit.conflict) { continue; } try { await postDocument(edit.path.replace(/^\/docs\//, ""), edit.content, edit.baseHash); await window.MDHubCache.deletePendingEdit(edit.path); } catch (error) { if (error.name === "DocumentConflictError") { await window.MDHubCache.markPendingEditConflict(edit.path, error.details); } break; } } await refreshSyncQueue("idle"); } window.addEventListener("online", function () { syncPending().catch(function () {}); }); window.addEventListener("offline", function () { refreshSyncQueue("offline").catch(function () {}); }); if (syncNow) { syncNow.addEventListener("click", function () { syncPending().catch(function () { refreshSyncQueue("queued").catch(function () {}); }); }); } syncPending().catch(function () {}); refreshSyncQueue("idle").catch(function () {}); window.MDHubSync = { saveDocument: saveDocument, syncPending: syncPending, refreshSyncQueue: refreshSyncQueue, }; })();