Add conflict resolution diff UI and app branding

This commit is contained in:
2026-05-13 16:25:28 -04:00
parent 780ff3a02c
commit d359baa010
83 changed files with 4523 additions and 3735 deletions

View File

@@ -0,0 +1,204 @@
const STATIC_CACHE = "md-hub-static-v1";
const DB_NAME = "md-hub-cache";
const DB_VERSION = 3;
const STORE_PAGES = "pages";
const STATIC_ASSETS = [
"/static/site.css",
"/static/cache.js",
"/static/sync.js",
"/static/realtime.js",
"/static/editor.js",
"/static/render.js",
"/static/favicon.png",
"/static/cairnquire%20logo%402x.webp",
"/",
"/docs",
];
self.addEventListener("install", function (event) {
event.waitUntil(
caches.open(STATIC_CACHE).then(function (cache) {
return cache.addAll(STATIC_ASSETS);
}).then(function () {
return self.skipWaiting();
})
);
});
self.addEventListener("activate", function (event) {
event.waitUntil(
caches.keys().then(function (keys) {
return Promise.all(
keys.map(function (key) {
if (key === STATIC_CACHE) {
return null;
}
return caches.delete(key);
})
);
}).then(function () {
return self.clients.claim();
})
);
});
self.addEventListener("fetch", function (event) {
const request = event.request;
if (request.method !== "GET") {
return;
}
const url = new URL(request.url);
if (url.origin !== self.location.origin) {
return;
}
if (request.mode === "navigate") {
event.respondWith(handleNavigation(request, url));
return;
}
if (url.pathname.startsWith("/static/")) {
event.respondWith(handleStaticAsset(request));
}
});
async function handleNavigation(request, url) {
try {
const response = await fetch(request);
cachePageResponse(url.pathname, response.clone());
return response;
} catch (_error) {
const cachedPage = await getCachedPage(url.pathname);
if (cachedPage && cachedPage.html) {
return new Response(markOfflineHTML(cachedPage.html), {
headers: {
"Content-Type": "text/html; charset=utf-8",
"X-MDHub-Cache": "offline",
},
status: 200,
});
}
const fallback = await caches.match("/");
if (fallback) {
return fallback;
}
return new Response("Offline", {
status: 503,
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
}
async function handleStaticAsset(request) {
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(request);
if (cached) {
fetch(request).then(function (response) {
if (response && response.ok) {
cache.put(request, response.clone());
}
}).catch(function () {});
return cached;
}
const response = await fetch(request);
if (response && response.ok) {
cache.put(request, response.clone());
}
return response;
}
async function cachePageResponse(pathname, response) {
if (!response || !response.ok) {
return;
}
const contentType = response.headers.get("Content-Type") || "";
if (contentType.indexOf("text/html") === -1) {
return;
}
try {
const html = await response.text();
await putCachedPage(normalizePath(pathname), {
path: normalizePath(pathname),
html: html,
title: extractTitle(html),
cachedAt: Date.now(),
});
} catch (_error) {}
}
function markOfflineHTML(html) {
const marker = "<script>window.__MDHUB_OFFLINE_CACHED__=true;<\/script>";
if (html.indexOf("window.__MDHUB_OFFLINE_CACHED__") !== -1) {
return html;
}
if (html.indexOf("</head>") !== -1) {
return html.replace("</head>", marker + "</head>");
}
return marker + html;
}
function extractTitle(html) {
const match = html.match(/<title>([^<]*)<\/title>/i);
return match ? match[1] : "Cairnquire";
}
function normalizePath(path) {
if (!path || path === "/") {
return "/";
}
return path.replace(/\/$/, "") || "/";
}
function openDB() {
return new Promise(function (resolve, reject) {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = function () {
reject(request.error);
};
request.onsuccess = function () {
resolve(request.result);
};
request.onupgradeneeded = function (event) {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_PAGES)) {
db.createObjectStore(STORE_PAGES, { keyPath: "path" });
}
};
});
}
async function putCachedPage(path, record) {
const db = await openDB();
return new Promise(function (resolve, reject) {
const tx = db.transaction(STORE_PAGES, "readwrite");
const store = tx.objectStore(STORE_PAGES);
store.put(record);
tx.oncomplete = function () {
resolve();
};
tx.onerror = function () {
reject(tx.error);
};
});
}
async function getCachedPage(path) {
const db = await openDB();
return new Promise(function (resolve, reject) {
const tx = db.transaction(STORE_PAGES, "readonly");
const store = tx.objectStore(STORE_PAGES);
const request = store.get(normalizePath(path));
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject(request.error);
};
});
}