From 2ba40c0a511460e54bc41691629c96095a158a6d Mon Sep 17 00:00:00 2001 From: Alexander Adam Date: Sun, 22 Mar 2026 10:23:41 +0100 Subject: [PATCH 1/3] add service worker for offline asset caching --- packages/web/src/sw.js | 88 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 packages/web/src/sw.js diff --git a/packages/web/src/sw.js b/packages/web/src/sw.js new file mode 100644 index 00000000000..8f37e51ea42 --- /dev/null +++ b/packages/web/src/sw.js @@ -0,0 +1,88 @@ +const CACHE_NAME = 'sn-app-v1' + +// keep in sync with webpack copy patterns in web.webpack.config.js +const APP_SHELL = [ + '/', + '/index.html', + '/app.js', + '/app.css', + '/favicon/favicon.ico', + '/favicon/favicon-32x32.png', + '/favicon/apple-touch-icon.png', + '/manifest.webmanifest', +] + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(APP_SHELL) + }) + ) + // dont wait for old tabs to close + self.skipWaiting() +}) + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((names) => { + return Promise.all( + names + .filter((name) => name !== CACHE_NAME) + .map((name) => caches.delete(name)) + ) + }) + ) + self.clients.claim() +}) + +self.addEventListener('fetch', (event) => { + const { request } = event + + // let API calls and websocket stuff go straight to network + if (request.url.includes('/api/') || request.url.includes('/socket')) { + return + } + + // navigation requests: network first, fall back to cached shell + if (request.mode === 'navigate') { + event.respondWith( + fetch(request) + .then((resp) => { + // stash a fresh copy if it's good + if (resp.ok) { + const clone = resp.clone() + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)) + } + return resp + }) + .catch(() => caches.match('/index.html')) + ) + return + } + + // everything else: cache first, then network + event.respondWith( + caches.match(request).then((cached) => { + if (cached) { + // refresh cache in bg + fetch(request) + .then((resp) => { + if (resp.ok) { + caches.open(CACHE_NAME).then((cache) => cache.put(request, resp)) + } + }) + .catch(() => {}) + return cached + } + + return fetch(request).then((resp) => { + // only cache same-origin stuff + if (resp.ok && new URL(request.url).origin === self.location.origin) { + const clone = resp.clone() + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)) + } + return resp + }) + }) + ) +}) From 3f1be6b83a6284e556a2573509d0099bf879c8df Mon Sep 17 00:00:00 2001 From: Alexander Adam Date: Sun, 22 Mar 2026 10:31:18 +0100 Subject: [PATCH 2/3] register SW in index.html, copy to dist --- packages/web/src/index.html | 9 +++++++++ packages/web/web.webpack.config.js | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/web/src/index.html b/packages/web/src/index.html index bb112e0c9c2..6350e53a4c4 100644 --- a/packages/web/src/index.html +++ b/packages/web/src/index.html @@ -45,5 +45,14 @@ + diff --git a/packages/web/web.webpack.config.js b/packages/web/web.webpack.config.js index a2cf68aba4b..ae11c124dac 100644 --- a/packages/web/web.webpack.config.js +++ b/packages/web/web.webpack.config.js @@ -20,6 +20,7 @@ module.exports = (env) => { { from: 'src/500.html' }, { from: 'src/index.html' }, { from: 'src/manifest.webmanifest' }, + { from: 'src/sw.js' }, { from: 'src/robots.txt' }, { from: 'src/.well-known', to: '.well-known' }, ] From 405809ad2eedd76096c4e1c6e17b9c86accf9430 Mon Sep 17 00:00:00 2001 From: Alexander Adam Date: Sun, 22 Mar 2026 10:42:07 +0100 Subject: [PATCH 3/3] add CSP hash for sw registration script --- packages/web/src/index.html | 2 +- packages/web/web.webpack.dev.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/web/src/index.html b/packages/web/src/index.html index 6350e53a4c4..e7f65ee5e5e 100644 --- a/packages/web/src/index.html +++ b/packages/web/src/index.html @@ -30,7 +30,7 @@ - +