205 lines
4.9 KiB
JavaScript
205 lines
4.9 KiB
JavaScript
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);
|
|
};
|
|
});
|
|
}
|