diff options
Diffstat (limited to 'src/content/sw.js')
-rw-r--r-- | src/content/sw.js | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/src/content/sw.js b/src/content/sw.js new file mode 100644 index 0000000..9e17707 --- /dev/null +++ b/src/content/sw.js @@ -0,0 +1,131 @@ +const CACHE_NAME = "static-shared-assets"; + +const FALLBACK_PATHS = { + img: "/fallback-image-FIXME.svg", + data: { + static: "/fallback-data-FIXME.json", + }, +}; + +const leafValues = tree => + Object.values(tree).map(x => + typeof x !== "object" + ? x + : leafValues(x) + ); + +const collectLeaves = tree => + leafValues(tree).flat(Infinity); + +const DEFAULT_INSTALL_PATHS = [ + "/", + "index.html", + "style.css", + "img/favicon.svg", + "papo.js", +].concat(collectLeaves(FALLBACK_PATHS)); + +const mkInstallHandler = ({ self, caches }) => event => { + self.skipWaiting(); + event.waitUntil( + caches.open(CACHE_NAME).then( + cache => cache.addAll(DEFAULT_INSTALL_PATHS), + ), + ); +}; + +const store = async (caches, request, response) => { + const cache = await caches.open(CACHE_NAME); + await cache.put(request, response); +}; + +const getPrefixIn = (paths, segments) => { + if (paths === undefined) { + return null; + } + + if (segments.length === 0) { + return null; + } + + if (typeof paths === "string") { + return paths; + } + + return getPrefixIn(paths[segments[0]], segments.slice(1)); +}; + +const maybeFallback = async (caches, request, paths = FALLBACK_PATHS) => { + const url = new URL(request.url) + const segments = url.pathname.split("/").filter(s => !!s) + const fallbackPath = getPrefixIn(paths, segments); + if (fallbackPath) { + return await caches.match(fallbackPath); + } + + return null; +} + +const fromCache = async (caches, fetch, { request, preloadResponse }) => { + const cachedResponse = await caches.match(request); + if (cachedResponse) { + fetch(request).then(async freshResponse => + store(caches, request, freshResponse)); + return cachedResponse; + } + + const preloadedResponse = await preloadResponse; + if (preloadedResponse) { + store(caches, request, preloadeResponse.clone()); + return preloadedResponse; + } + + try { + // FIXME: do integration test with real lack of internet. + const fetchedResponse = await fetch(request); + store(caches, request, fetchedResponse.clone()); + return fetchedResponse; + } catch (e) { + const fallbackResponse = await maybeFallback(caches, request); + if (fallbackResponse) { + return fallbackResponse; + } + + throw e; + } +}; + +const mkFetchHandler = ({ caches }) => (event, mkfetch = () => fetch) => { + if (event.request.method !== "GET") { + return; + } + + event.respondWith(fromCache(caches, mkfetch(), event)); +}; + +const mkActivateHandler = ({ self, clients }) => event => + event.waitUntil(Promise.all([ + clients.claim(), + self.registration.navigationPreload?.enable(), + ])); + +const registerListeners = env => { + env.self.addEventListener("install", mkInstallHandler(env)); + env.self.addEventListener("activate", mkActivateHandler(env)); + env.self.addEventListener("fetch", mkFetchHandler(env)); +}; + +const main = ({ + self, + caches, + clients, + out = console.log, + err = console.warn, +} = {}) => + registerListeners({ + self, + caches, + clients, + out, + err, + }); |