summaryrefslogtreecommitdiff
path: root/src/content/sw.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/content/sw.js')
-rw-r--r--src/content/sw.js131
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,
+ });