import Dexie from "https://esm.sh/v120/dexie@3.2.2/dist/dexie.js"; import { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts"; import type { FileContent } from "../common/spaces/indexeddb_space_primitives.ts"; import { simpleHash } from "../common/crypto.ts"; const CACHE_NAME = "{{CACHE_NAME}}"; const precacheFiles = Object.fromEntries([ "/", "/.client/logout.html", "/.client/client.js", "/.client/favicon.png", "/.client/iAWriterMonoS-Bold.woff2", "/.client/iAWriterMonoS-BoldItalic.woff2", "/.client/iAWriterMonoS-Italic.woff2", "/.client/iAWriterMonoS-Regular.woff2", "/.client/logo.png", "/.client/logo-dock.png", "/.client/main.css", "/.client/manifest.json", ].map((path) => [path, path + "?v=" + CACHE_NAME, path])); // Cache busting self.addEventListener("install", (event: any) => { console.log("[Service worker]", "Installing service worker..."); event.waitUntil( caches.open(CACHE_NAME) .then((cache) => { console.log( "[Service worker]", "Now pre-caching client files", ); return cache.addAll(Object.values(precacheFiles)).then(() => { console.log( "[Service worker]", Object.keys(precacheFiles).length, "client files cached", ); // @ts-ignore: No need to wait self.skipWaiting(); }); }), ); }); self.addEventListener("activate", (event: any) => { console.log("[Service worker]", "Activating new service worker!!!"); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log("[Service worker]", "Removing old cache", cacheName); return caches.delete(cacheName); } }), ).then(() => { // Let's activate ourselves for all existing clients // @ts-ignore: No need to wait, clients is a serviceworker thing return clients.claim(); }); }), ); }); let db: Dexie | undefined; let fileContentTable: Dexie.Table | undefined; self.addEventListener("fetch", (event: any) => { const url = new URL(event.request.url); // Use the custom cache key if available, otherwise use the request URL const cacheKey = precacheFiles[url.pathname] || event.request.url; event.respondWith( // Try the static (client) file cache first caches.match(cacheKey) .then((response) => { // Return the cached response if found if (response) { return response; } const requestUrl = new URL(event.request.url); const pathname = requestUrl.pathname; // console.log("In service worker, pathname is", pathname); // If this is a /.fs request, this can either be a plug worker load or an attachment load if (pathname.startsWith("/.fs")) { if (fileContentTable && !event.request.headers.has("x-sync-mode")) { // console.log( // "Attempting to serve file from locally synced space:", // pathname, // ); // Don't fetch from DB when in sync mode (because then updates won't sync) const path = decodeURIComponent( requestUrl.pathname.slice("/.fs/".length), ); return fileContentTable.get(path).then( (data) => { if (data) { // console.log("Serving from space", path); return new Response(data.data, { headers: { "Content-type": mime.getType(path) || "application/octet-stream", }, }); } else { console.error( "Did not find file in locally synced space", path, ); return new Response("Not found", { status: 404, }); } }, ); } else { // Just fetch the file directly return fetch(event.request); } } else if (pathname !== "/.auth") { // Must be a page URL, let's serve index.html which will handle it return caches.match(precacheFiles["/"]).then((response) => { // This shouldnt't happen, index.html not in the cache for some reason return response || fetch(event.request); }); } else { return fetch(event.request); } }), ); }); self.addEventListener("message", (event: any) => { if (event.data.type === "flushCache") { caches.delete(CACHE_NAME) .then(() => { console.log("[Service worker]", "Cache deleted"); db?.close(); event.source.postMessage({ type: "cacheFlushed" }); }); } if (event.data.type === "config") { const spaceFolderPath = event.data.config.spaceFolderPath; const dbPrefix = "" + simpleHash(spaceFolderPath); // Setup space db = new Dexie(`${dbPrefix}_space`, { indexedDB: globalThis.indexedDB, }); db.version(1).stores({ fileMeta: "name", fileContent: "name", }); fileContentTable = db.table("fileContent"); } });