I'm still doing experiments in order to master service workers, and I'm facing a problem, probably because of my lack of expertise in JavaScript and service workers.
The problem happens when I want the new service worker to skipWaiting()
using postMessage()
. If I show a popup with a button and I bind a call to postMessage()
there, everything works. If I call postMessage()
directly, it doesn't work. It's a race condition because SOMETIMES it works, but I can't identify the race condition.
BTW, the postMessage()
call WORKS, the service worker is logging what it should when getting the message:
// Listen to messages from clients.
self.addEventListener('message', event => {
switch(event.data) {
case 'skipWaiting': self.skipWaiting(); console.log('I skipped waiting... EXTRA');
break;
}
});
Here is the code. The important bit is on the if (registration.waiting)
conditional. The uncommented code works, the commented one doesn't:
// Register service worker.
if ('serviceWorker' in navigator) {
// Helpers to show and hide the update toast.
let hideUpdateToast = () => {
document.getElementById('update_available').style.visibility = 'hidden';
};
let showUpdateToast = (serviceworker) => {
document.getElementById('update_available').style.visibility = 'visible';
document.getElementById('force_install').onclick = () => {
serviceworker.postMessage('skipWaiting');
hideUpdateToast();
};
document.getElementById('close').onclick = () => hideUpdateToast();
};
window.addEventListener('load', () => {
let refreshing = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
navigator.serviceWorker.register('/sw.js').then(registration => {
// A new service worker has been fetched, watch for state changes.
//
// This event is fired EVERY TIME a service worker is fetched and
// succesfully parsed and goes into 'installing' state. This
// happens, too, the very first time the page is visited, the very
// first time a service worker is fetched for this page, when the
// page doesn't have a controller, but in that case there's no new
// version available and the notification must not appear.
//
// So, if the page doesn't have a controller, no notification shown.
registration.addEventListener('updatefound', () => {
// return; // FIXME
registration.installing.onstatechange = function () { // No arrow function because 'this' is needed.
if (this.state == 'installed') {
if (!navigator.serviceWorker.controller) {
console.log('First install for this service worker.');
} else {
console.log('New service worker is ready to activate.');
showUpdateToast(this);
}
}
};
});
// If a service worker is in 'waiting' state, then maybe the user
// dismissed the notification when the service worker was in the
// 'installing' state or maybe the 'updatefound' event was fired
// before it could be listened, or something like that. Anyway, in
// that case the notification has to be shown again.
//
if (registration.waiting) {
console.log('New service worker is waiting.');
// showUpdateToast(registration.waiting);
// The above works, but this DOESN'T WORK.
registration.waiting.postMessage('skipWaiting');
}
}).catch(error => {
console.log('Service worker registration failed!');
console.log(error);
});
});
}
Why does the indirect call using a button onclick
event works, but calling postMessage()
doesn't?
I'm absolutely at a loss and I bet the answer is simple and I'm just too blind to see it.
Thanks a lot in advance.
I've run into exactly the same issue. Turns out skipWaiting
never resolves if it's called on an installed service worker while there's still a pending fetch request on the active service worker. Funnily enough the active service worker is reloaded once the request settles, so this is clearly a bug with Chrome. That's also the reason why it only happens sometimes and why a timeout or a delayed request for skipWaiting
has a higher chance to succeed, especially if it's called upon page load.
So you'll want to wait for all open network requests to settle before calling skipWaiting
. Maybe someone else is more creative, but I've found two ways to prevent the race condition:
If your application only ever has one client, you can monkey-patch XMLHttpRequest
to track open network requests and hold back the request for skipWaiting
until all requests are settled.
If there are multiple clients you'll want to introduce a "shutdown" routine which will make the active SW defer all new fetch requests (return false
instead of respondWith
in your fetch handler) while waiting for all open requests to be settled upon which you can safely call skipWaiting
in your installed SW.
It's a bit much, but the only way I was able to prevent my app from sporadically running into this using Chrome.