// Local Consts const SW_VERSION = 1; const SW_CACHE_NAME = "OFFLINE_V1"; // Message Consts const PURGE_SW_CACHE = "PURGE_SW_CACHE"; const DEL_SW_CACHE = "DEL_SW_CACHE"; const GET_SW_CACHE = "GET_SW_CACHE"; const GET_SW_VERSION = "GET_SW_VERSION"; // Assets const ASSETS_DOCUMENT = /^\/documents\/[a-zA-Z0-9]{32}\/(cover|file|progress)$/; const ASSETS_OFFLINE = [ // Offline Resources "/offline", "/assets/offline/index.js", "/assets/reader/index.js", "/assets/images/no-cover.jpg", // App Style "/manifest.json", "/assets/style.css", // Reader & Offline Libraries "/assets/js/platform.js", "/assets/js/jszip.min.js", "/assets/js/epub.min.js", "/assets/js/no-sleep.js", "/assets/js/idb-keyval.js", ]; function wantCache(request) { let urlPath = new URL(request.url).pathname; if (ASSETS_OFFLINE.includes(urlPath)) return true; if (urlPath.match(ASSETS_DOCUMENT)) return true; return false; } /** * Nuke Cache **/ function purgeCache() { return caches.keys().then(function (names) { for (let name of names) caches.delete(name); }); } /** * Update Cache **/ async function updateCache(request) { let cache = await caches.open(SW_CACHE_NAME); console.log("UPDATING CACHE:", request.url); return fetch(request).then((response) => { const resClone = response.clone(); if (response.status < 400) cache.put(request, resClone); return response; }); } /** * Pre-Cache Resources on Install **/ function cacheOfflineResources() { return caches.open(SW_CACHE_NAME).then(function (cache) { return cache.addAll(ASSETS_OFFLINE); }); } /** * Install & Update Listener -> Cache Offline Resources **/ self.addEventListener("install", function (event) { console.log("INSTALL:", event); event.waitUntil(cacheOfflineResources()); }); /** * Message Listener -> Communication Channel Page <-> SW **/ self.addEventListener("message", (event) => { console.log("MESSAGE:", event); let { id, data } = event.data; if (data.type === GET_SW_VERSION) { event.source.postMessage({ id, data: SW_VERSION }); } else if (data.type === PURGE_SW_CACHE) { purgeCache() .then(() => event.source.postMessage({ id, data: "SUCCESS" })) .catch(() => event.source.postMessage({ id, data: "FAILURE" })); } else if (data.type === GET_SW_CACHE) { caches.open(SW_CACHE_NAME).then(async (cache) => { let allKeys = await cache.keys(); let docResources = allKeys .map((item) => new URL(item.url).pathname) .filter((item) => item.startsWith("/documents/")); let documentIDs = Array.from( new Set(docResources.map((item) => item.split("/")[2])) ); let cachedDocuments = await Promise.all( documentIDs .filter( (id) => docResources.includes("/documents/" + id + "/file") && docResources.includes("/documents/" + id + "/progress") ) .map(async (id) => { let resp = await cache.match("/documents/" + id + "/progress"); return resp.json(); }) ); event.source.postMessage({ id, data: cachedDocuments }); }); // TODO } else if (data.type === DEL_SW_CACHE) { // TODO } else { event.source.postMessage({ id, data: { pong: 1 } }); } }); /** * Fetch Listener -> Cache * - Covers * - Files * - Assets (Styles, JS Libraries) * * NOTE: We do not cache regular app resources. We will fallback to the * offline reader. **/ self.addEventListener("fetch", (event) => { event.respondWith( (async function () { // Bypass Lazy Caching if (event.request.url.endsWith("/progress")) { return updateCache(event.request).catch((e) => caches.match(event.request) ); } // Get Potential Cache let cachedResponse = await caches.match(event.request); // Update Cache Asynchronously (If Wanted) let newResponse = ( wantCache(event.request) ? updateCache(event.request) : fetch(event.request) ).catch((e) => caches.match("/offline")); return cachedResponse || newResponse; })() ); });