I'm using Workbox and the BroadcastUpdatePlugin() in my serviceWorker to prompt the user to refresh the page when a cached file is updated. It works great when there's only one file updated, but when I publish multiple updates at once (HTML, CSS and JS files), the user is prompted to refresh the page for each file.
How can I update all files in the cache, then prompt the user to refresh the page only once, when the event listener has stopped receiving update messages?
ServiceWorker code
const {BroadcastUpdatePlugin} = workbox.broadcastUpdate;
registerRoute(
({request}) => request.destination === 'document',
new StaleWhileRevalidate({
//new NetworkOnly({
cacheName: 'pages',
plugins: [
new BroadcastUpdatePlugin(),
],
})
)
registerRoute(
({request}) => request.destination === 'script' || request.destination === 'style',
new StaleWhileRevalidate({
//new NetworkOnly({
cacheName: 'assets',
plugins: [
new BroadcastUpdatePlugin(),
],
})
)
JavaScript code
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('sw.js', { scope: '/' }).then(swReg => {
console.log('Service Worker Registered', swReg);
}).catch(error => {
console.log('There was an error!', error);
})
})
// Listen for cache updates and prompt a page reload
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.meta === 'workbox-broadcast-update') {
const {cacheName, updatedURL} = event.data.payload;
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedURL);
const updatedText = await updatedResponse.text();
console.log('Updated: '+cacheName+', '+updatedURL);
// prompts for every update
if(confirm('Content Updated. Please refresh the page.')){
window.location.reload;
}
}
})
}
I could imagine there potentially being logic in your service worker that could help with this, by, for instance, looking at the cliendId in the FetchEvent that triggered an update and only conditionally sending the message if it hasn't seen that clientId before.
But that would require a bunch of custom logic beyond what BroadcastUpdatePlugin provides already, and it might be error prone—if your service worker broadcasts an update, then the user acts on the update, and then there's another update that could be broadcast, you probably want to give the user another chance to act on it, but it would be difficult for the service worker to know whether to broadcast a message to the same clientId in that scenario.
A cleaner approach would be to move the logic that prevented multiple prompt to the window client code.
There are a couple of ways to go there, but what seems the cleanest to me would rely on the resolution of a promise to trigger the prompt. You can call the resolve function of a promise multiple times, but the then() in the promise chain will only be invoked once, giving you the behavior you're looking for.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js');
});
const waitForUpdate = new Promise((resolve) => {
navigator.serviceWorker.addEventListener('message', async (event) => {
if (event.data.meta === 'workbox-broadcast-update') {
const {cacheName, updatedURL} = event.data.payload;
const cache = await caches.open(cacheName);
const updatedResponse = await cache.match(updatedURL);
const updatedText = await updatedResponse.text();
console.log(`Updated ${updatedURL} in ${cacheName}: ${updatedText}`);
// Calling resolve() will trigger the promise's then() once.
resolve();
}
});
});
waitForUpdate.then(() => {
if (confirm('Content updated. Please refresh the page.')) {
window.location.reload();
}
});
}
(You could also use an approach that relied on a global variable or something similar that gets flipped the first time you show the prompt, and short-circuits the prompt display every subsequent time, but I like using promises for this sort of thing.)