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.js270
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