diff options
Diffstat (limited to 'src/content/sw.js')
-rw-r--r-- | src/content/sw.js | 270 |
1 files changed, 145 insertions, 125 deletions
diff --git a/src/content/sw.js b/src/content/sw.js index 03a09e4..51089f2 100644 --- a/src/content/sw.js +++ b/src/content/sw.js @@ -1,11 +1,6 @@ -const CACHE_NAME = "static-shared-assets"; +const IMG_PATHS = []; -const FALLBACK_PATHS = { - img: "/fallback-image-FIXME.svg", - data: { - static: "/fallback-data-FIXME.json", - }, -}; +const CACHE_NAME = "static-shared-assets"; const leafValues = tree => Object.values(tree).map(x => @@ -17,65 +12,86 @@ const leafValues = tree => 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; +const matchRoute = (segments, table) => { + if (segments.length === 0) { + return table ?? null; } - if (segments.length === 0) { + const simpleMatch = matchRoute(segments.slice(1), table?.[segments[0]]); + if (simpleMatch !== null) { + return simpleMatch; + } + + const hasGlob = table?.["*"] !== undefined; + if (!hasGlob) { return null; } - if (typeof paths === "string") { - return paths; + const keys = Object.keys(table); + const isTrailing = keys.length === 1 || ( + keys.length === 2 && + table[""] !== undefined + ); + if (isTrailing) { + return table["*"][""]; + } + + return matchRoute(segments.slice(1), table["*"]); +}; + +const fromCache = async ({ caches }, { event: { request } }) => { + const cache = await caches.open(CACHE_NAME); + return await cache.match(request); +} + +const fromNetwork = async ( + _env, + { fetch, event: { request, preloadResponse } }, +) => { + const preloadedResponse = await preloadResponse; + if (preloadResponse) { + return preloadResponse; } - return getPrefixIn(paths[segments[0]], segments.slice(1)); + return await fetch(request); +}; + +const networkThanCache = async (env, ctx) => { + const networkResponse = fromNetwork(env, ctx); + store(env.caches, ctx.event.request, networkResponse.clone()); + return networkResponse; }; -const fromCache = async (caches, fetch, { request, preloadResponse }) => { - const cachedResponse = await caches.match(request); +const immutableGet = async (env, ctx) => { + const cachedResponse = await fromCache(env, ctx); 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; - } + const response = fromNetwork(env, ctx); + store(env.caches, ctx.event.request, response); + return response; +} +const staleOK = async (env, ctx) => { + return Promise.any([ + fromCache(env, ctx), + networkThanCache(env, ctx), + ]); +}; + +const staleOrFallback = async (env, ctx) => { try { - const fetchedResponse = await fetch(request); - store(caches, request, fetchedResponse.clone()); - return fetchedResponse; + return await staleOK(env, ctx); } catch (e) { - const fallbackResponse = await maybeFallback(caches, request); + const fallbackRoute = ctx.strategy.match; + const fallbackResponse = await env.caches.match(fallbackRoute); if (fallbackResponse) { return fallbackResponse; } @@ -84,121 +100,105 @@ const fromCache = async (caches, fetch, { request, preloadResponse }) => { } }; -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 CONFIG_PATHS = { - immutable: { - "some": { - "path": true, - }, - }, - staleOK: { - }, -}; -const PRIORITY_ORDERING = [ - "immutable", - "staleFallback", - "stale", -]; - -const pullThroughCache = (env, ctx) => { -}; - -/* -// FIXME: handle base paths - const STRATEGIES = [ +const STRATEGIES = [ { - name: "immutable", + fn: immutableGet, routes: new Set([ - "/cas", + "cas/*", ]), - fn: immutable, }, { - name: "staleFallback", - routes: { - "/img/icon/": "", - }, - fn: staleFallback, + fn: staleOK, + routes: new Set([ + "", // FIXME: should be equivalent to "./", not "/" + "index.html", + "style.css", + "papo.js", + ...IMG_PATHS, + ]), }, { - name: "staleOnly", + fn: staleOrFallback, routes: new Set([ - // FIXME: add "/" - "/index.html": true, - "/style.css": true, - "/papo.js": true, + ["img/profile/*", "img/profile/fallback.svg"], ]), - img: "/fallback-image-FIXME.svg", - data: { - static: "/fallback-data-FIXME.json", }, - fn: staleOnly, + { + fn: fromNetwork, + routes: new Set([ + "*" + ]), }, -}; -*/ - -const asSegments = pathname => - pathname.split("/").filter(s => !!s); - -const requestSegments = ({ url }) => - asSegments(new URL(url).pathname); +]; -/* - const strategyFor = (segments, paths = CONFIG_PATHS) => { - for (const strategy of PRIORITY_ORDERING) { - if (getPrefixIn(paths[strategy.name], segments)) { - return strategy; +const strategyFor = (segments, baseSegments, strategies = STRATEGIES) => { + const effectiveSegments = segments.slice(baseSegments.length); + for (const strategy of strategies) { + const match = matchRoute(effectiveSegments, strategy.routes); + if (match) { + return { ...strategy, match }; } } }; -*/ -const networkOnly = async (_env, { fetch, event }) => { - const preloaded = await event.preloadResponse; - if (preloaded) { - return preloaded; - } +const normalizeSegments = segments => + segments.concat( + segments.length === 1 && segments[0] === "" + ? [] + : [""] + ); + +const pathToSegments = path => + normalizeSegments(path + .replace(/^\/*/, "") + .replace( /\/*$/, "") + .replace(/\/+/, "/") + .split("/")); - return await fetch(event.request); +const hasPathParams = segments => + segments.some(s => s.startsWith(":")); + +const flatten = arr => + arr.flat(Infinity); + + +/// We don't use `cache.addAll()` since we can tolerate failure to add individual +/// items, and we'd rather have a partially filled cache over an empty one. +const populateCache = async ({ caches }, paths = DEFAULT_INSTALL_PATHS) => { + const cache = await caches.open(CACHE_NAME); + return Promise.all(paths.map(cache.add)); }; -const strategyFor = (segments, paths = CONFIG_PATHS) => { - return networkOnly; +/// We don't `event.waitUntil()`, so that we can be activated sooner. +const mkInstallHandler = env => async event => { + self.skipWaiting(); + populateCache(env); }; +const mkActivateHandler = ({ self, clients }) => event => + event.waitUntil(Promise.all([ + self.registration.navigationPreload?.enable(), + clients.claim(), + ])); + // FIXME: noop if "Connection": "Upgrade" is in header // if (event.request.headers.get("Connection").toLowerCase() == "upgrade"); // out([...event.request.headers.entries()]); -const mkFetchHandler = env => (event, mkfetch = () => fetch) => { +const mkFetchHandler = env => async (event, mkfetch = () => fetch) => { if (event.request.method !== "GET") { return; } - const segments = requestSegments(event.request); - const strategyFn = strategyFor(segments); - event.respondWith(strategyFn(env, { + const segments = pathToSegments(new URL(event.request.url).pathname); + const strategy = strategyFor(segments, env.baseSegments); + event.respondWith(strategy.fn(env, { event, segments, + strategy, fetch: mkfetch(), })); }; -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)); @@ -220,3 +220,23 @@ const main = ({ out, err, }); + +// handy resources for pushes: +// https://web.dev/articles/offline-cookbook +// https://web.dev/articles/push-notifications-overview +// https://developer.chrome.com/blog/background-sync +// https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/push +// https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/background-syncs + +// https://web.dev/learn/forms + +// https://alistapart.com/article/responsive-web-design/ + +// https://developer.chrome.com/docs/workbox/modules/workbox-background-sync + +// https://www.npmjs.com/package/idb +// https://web.dev/articles/indexeddb-best-practices-app-state + +// https://web.dev/learn/pwa/app-design +// https://web.dev/articles/accent-color +// https://jakearchibald.com/2016/caching-best-practices/#max-age-on-mutable-content-is-often-the-wrong-choice |