service worker doesn't skip waiting state

Viewed 635

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.

1 Answers

Looks like a bug in Chromium or WebKit, because this code works all the time in Firefox but fails in Chrome and Edge most of the time.

I've reported that bug to Chromium, let's see if it is a bug or my code is weird. I've managed to build the smallest code possible that still reproduces the issue, it's quite small and it can be found on the bug report.

The report can be found here.

Sorry for the noise, I'll keep investigating the issue but no matter how I tried, the code still fails from time to time, I can't spot the race condition on my code.

Related