Skip to content

Commit 9005d3f

Browse files
lifeartclaude
andcommitted
Fix PWA auto-update: stale-while-revalidate, update detection, auto-reload
The installed PWA was not auto-updating because: - Cache-first strategy served old assets forever until manual reinstall - No update detection or user notification Fixes: - Change asset fetch strategy from cache-first to stale-while-revalidate: serve cached version immediately, fetch fresh version in background, update cache for next load - Add SW message listener for skipWaiting command - Add update detection in index.html: checks for updates every 60s, auto-reloads when new SW activates (seamless update) - Move clients.claim() into activate handler's promise chain Users will now get updates automatically on next app open after a deploy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 94a477e commit 9005d3f

2 files changed

Lines changed: 33 additions & 11 deletions

File tree

public/index.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,21 @@
5050
<script src="js/bundle.js"></script>
5151
<script>
5252
if ('serviceWorker' in navigator) {
53-
navigator.serviceWorker.register('sw.js').catch(() => {});
53+
navigator.serviceWorker.register('sw.js').then(function(reg) {
54+
// Check for updates every 60 seconds
55+
setInterval(function() { reg.update(); }, 60000);
56+
// When a new SW is waiting, auto-activate it and reload
57+
reg.addEventListener('updatefound', function() {
58+
var newWorker = reg.installing;
59+
if (newWorker) {
60+
newWorker.addEventListener('statechange', function() {
61+
if (newWorker.state === 'activated' && navigator.serviceWorker.controller) {
62+
window.location.reload();
63+
}
64+
});
65+
}
66+
});
67+
}).catch(function() {});
5468
}
5569
</script>
5670
</body>

public/sw.js

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,47 +20,55 @@ const ASSETS = [
2020
'./manifest.json'
2121
];
2222

23-
// Install - cache all assets
23+
// Install - cache all assets, activate immediately
2424
self.addEventListener('install', (event) => {
2525
event.waitUntil(
2626
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
2727
);
2828
self.skipWaiting();
2929
});
3030

31-
// Activate - clean old caches
31+
// Activate - clean old caches, take control immediately
3232
self.addEventListener('activate', (event) => {
3333
event.waitUntil(
3434
caches.keys().then((keys) =>
3535
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
36-
)
36+
).then(() => self.clients.claim())
3737
);
38-
self.clients.claim();
3938
});
4039

41-
// Fetch - cache-first for assets, network-first for navigation
40+
// Fetch strategy
4241
self.addEventListener('fetch', (event) => {
4342
const { request } = event;
4443

44+
// Navigation: network-first (SPA routing)
4545
if (request.mode === 'navigate') {
46-
// Network-first for navigation (SPA)
4746
event.respondWith(
4847
fetch(request).catch(() => caches.match('./index.html'))
4948
);
5049
return;
5150
}
5251

53-
// Cache-first for assets
52+
// Assets: stale-while-revalidate
53+
// Serve from cache immediately, then update cache in background
5454
event.respondWith(
5555
caches.match(request).then((cached) => {
56-
if (cached) return cached;
57-
return fetch(request).then((response) => {
56+
const fetchPromise = fetch(request).then((response) => {
5857
if (response.ok) {
5958
const clone = response.clone();
6059
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
6160
}
6261
return response;
63-
});
62+
}).catch(() => cached);
63+
64+
return cached || fetchPromise;
6465
})
6566
);
6667
});
68+
69+
// Listen for skip-waiting message from the app
70+
self.addEventListener('message', (event) => {
71+
if (event.data === 'skipWaiting') {
72+
self.skipWaiting();
73+
}
74+
});

0 commit comments

Comments
 (0)