215 lines
6.4 KiB
JavaScript
215 lines
6.4 KiB
JavaScript
(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,
|
|
};
|
|
})();
|