fix(sw): point service worker at React shell + bump cache version

The previous SW (pedscribe-v12) precached /js/app.js, /js/auth.js,
and /css/styles.css — all deleted in the previous commit. Without
this fix, every existing PWA install would 404 on the next SW
install attempt and serve stale cached vanilla pages forever.

Changes:
  • CACHE_NAME bumped to pedscribe-v13 → activate event evicts the
    v12 cache (and any earlier).
  • SHELL_ASSETS narrowed to [/, /manifest.json]. Hashed Vite
    assets are not precached because their filenames change on
    every build — they're served fresh from the network with Vite's
    immutable Cache-Control as backup.
  • Fetch handler:
      - /api/* → bypass SW (medical data must always be fresh).
      - /app/assets/* → network-first, cache fallback for offline.
      - everything else → cache-first with stale-while-revalidate.
  • Promise.allSettled around cache.add() so a single missing
    asset can't break the whole SW install.

Existing PWA users get the new shell on their next visit
(skipWaiting + clients.claim).
This commit is contained in:
Daniel 2026-04-24 02:57:23 +02:00
parent 39d764e1ff
commit 990cbf97ac

View file

@ -1,31 +1,29 @@
// ============================================================
// SERVICE WORKER — Cache shell, network-first for API
// Provides offline fallback for app shell while keeping
// API calls always fresh (critical for medical data accuracy)
// SERVICE WORKER — Cache the React shell + manifest.
// Network-first for API (medical data must always be fresh).
// Hashed Vite assets are NOT precached — they change every build,
// the browser HTTP cache plus immutable Cache-Control handles them.
// ============================================================
var CACHE_NAME = 'pedscribe-v12';
var CACHE_NAME = 'pedscribe-v13';
var SHELL_ASSETS = [
'/',
'/index.html',
'/css/styles.css',
'/js/app.js',
'/js/auth.js',
'/manifest.json'
'/manifest.json',
];
// Install: precache app shell
// Install: precache shell. Errors swallowed so a missing single
// asset doesn't break SW install entirely.
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(SHELL_ASSETS);
}).then(function() {
return self.skipWaiting();
})
return Promise.allSettled(SHELL_ASSETS.map(function(url) {
return cache.add(url).catch(function() {});
}));
}).then(function() { return self.skipWaiting(); })
);
});
// Activate: clear old caches
// Activate: clear old caches (deletes pedscribe-v12 and earlier).
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(names) {
@ -33,61 +31,35 @@ self.addEventListener('activate', function(event) {
names.filter(function(name) { return name !== CACHE_NAME; })
.map(function(name) { return caches.delete(name); })
);
}).then(function() {
return self.clients.claim();
})
}).then(function() { return self.clients.claim(); })
);
});
// Fetch: network-first for API, cache-first for static assets
self.addEventListener('fetch', function(event) {
if (event.request.method !== 'GET') return;
var url = new URL(event.request.url);
// Only handle same-origin requests
// Same-origin only; skip cross-origin (CDN, fonts, etc.)
if (url.origin !== self.location.origin) return;
// API calls — always network, never cache (medical data must be fresh)
if (url.pathname.startsWith('/api/')) {
event.respondWith(fetch(event.request));
return;
}
// Component HTML — network-first with cache fallback
if (url.pathname.startsWith('/components/')) {
// API: network only — never cache medical data.
if (url.pathname.startsWith('/api/')) return;
// Hashed Vite assets: serve from network, fall back to cache if offline.
if (url.pathname.startsWith('/app/assets/')) {
event.respondWith(
fetch(event.request).then(function(response) {
var clone = response.clone();
caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, clone); });
return response;
}).catch(function() {
return caches.match(event.request);
})
fetch(event.request).catch(function() { return caches.match(event.request); })
);
return;
}
// Static assets (JS, CSS, icons) — network-first so code updates apply immediately
if (url.pathname.match(/\.(js|css|png|ico|woff2?)$/)) {
event.respondWith(
fetch(event.request).then(function(response) {
var clone = response.clone();
caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, clone); });
return response;
}).catch(function() {
return caches.match(event.request);
})
);
return;
}
// HTML pages — network-first
// SPA shell + everything else: cache-first with network update.
event.respondWith(
fetch(event.request).then(function(response) {
var clone = response.clone();
caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, clone); });
return response;
}).catch(function() {
return caches.match(event.request) || caches.match('/index.html');
caches.match(event.request).then(function(cached) {
var fetchPromise = fetch(event.request).then(function(resp) {
if (resp && resp.status === 200) {
var copy = resp.clone();
caches.open(CACHE_NAME).then(function(cache) { cache.put(event.request, copy); });
}
return resp;
}).catch(function() { return cached; });
return cached || fetchPromise;
})
);
});