Add conflict resolution diff UI and app branding
This commit is contained in:
204
apps/server/internal/httpserver/static/sw.js
Normal file
204
apps/server/internal/httpserver/static/sw.js
Normal 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);
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user