Inconsistent behaviour with workbox-window.update()

1.1k views Asked by At

Platform

"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36"

Workbox version

5.0.0-rc.0

I am using the workbox-window update() method to trigger a service worker update check as described in workbox issue #2130. This question also seems related to observations made in workbox issue #2301.

<script type="module">
import {Workbox} from 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0-rc.0/workbox-window.prod.mjs';

if ('serviceWorker' in navigator) {
    const wb = new Workbox('/sw.js');
    // Grab the update button from the UI using jQuery and add a listener to it.
    $('#update-button').on('click', function () {
        wb.update();
    });
    ....
}

</script>

If an update is found then a new service worker is loaded and goes into the waiting state. I listen for this event (both waiting and externalwaiting, doing the same thing in both cases), and ask the user if they want to install the update now or later.

function handleWaiting(wb) {
    if (confirm("An update is available for this app. Install now?")) {
        wb.messageSW({type: 'SKIP_WAITING'});
    }
}
wb.addEventListener('waiting', event => {
    handleWaiting(wb);
});
wb.addEventListener('externalwaiting', event => {
    handleWaiting(wb);
});

Most of the time the waiting event is fired, and if the user accepts the update the service worker is activated and we can reload the page to complete the update process:

function handleActivated(wb, event) {
    if (event.isUpdate) {
        window.location.reload();
    } else {
        wb.messageSW({ type: 'CLIENTS_CLAIM' });
    }
}
wb.addEventListener('activated', event => {
    handleActivated(wb, event);
});
wb.addEventListener('externalactivated', event => {
    handleActivated(wb, event);
});

If the waiting event is fired this process works fine.

  1. Make a small update to the app that results in the service worker file changing.
  2. Press the 'Check for Updates' button.
  3. New service worker is found & user is prompted to install the update
  4. Page is reloaded.

But if the externalwaiting event is fired then this code does not work. The new service worker remains in the waiting state, which I can see in Chrome Dev Tools.

Stuck waiting

Skip waiting message received by the old service worker

If the user confirms that they want to update the SKIP_WAITING message is received by the older, activated service worker definition.

Here's an excerpt from my logs that proves it.

  • Log entries from the application/client side are prefixed with 'Application' followed by the app version.
  • Log entries from the service worker are prefixed with 'Service Worker' followed by a DOB datestamp that is unique to each service worker instance.
01 [Application 0.0.1.2019.11.05-48] Checking for updates...
02 [Service Worker 2019-11-05 @ 15:23:14] <<<<<<<<<<<<<<< STARTING >>>>>>>>>>>>>>>
03 [Service Worker 2019-11-05 @ 15:23:14] Yay! Workbox 5.0.0-rc.0 is loaded 
04 [Service Worker 2019-11-05 @ 15:23:14] Lifecycle event: [install]
05 [Application 0.0.1.2019.11.05-48] Service Worker lifecycle event: 06 [externalinstalled]
07 [Application 0.0.1.2019.11.05-48] Service Worker lifecycle event: [externalwaiting]
08 [Application 0.0.1.2019.11.05-48] handle waiting...
09 [Service Worker 2019-11-05 @ 15:20:03] Message event: [SKIP_WAITING]

On line

  1. The user has just pressed the 'check for updates' button.
  2. A new service worker version has been found on 2019-11-05 @ 15:23:14 and is being parsed. We'll call this SWv2.
  3. Workbox is loaded by SWv2.
  4. SWv2 install event handler is executed.
  5. The Workbox window externalinstalled event handler is executed.
  6. The Workbox window externalwaiting event handler is executed.
  7. The workbox window externalwaiting event handler sends a SKIP_WAITING message using workboxWindow.messageSW()
  8. Service Worker v1 (loaded on 2019-11-05 @ 15:20:03) receives the skip waiting message. Nothing happens. SWv2 remains in the waiting state.

Contrast with with plain waiting event

In the following log you can see that the SKIP_WAITING message is received by the waiting service worker: v 2019-11-05 @ 16:07:32. The update process therefore completes successfully.

[Application 0.0.1.2019.11.05-52] Checking for updates...
[Service Worker 2019-11-05 @ 16:07:32] <<<<<<<<<<<<<<< STARTING >>>>>>>>>>>>>>>
[Service Worker 2019-11-05 @ 16:07:32] Yay! Workbox 5.0.0-rc.0 is loaded 
[Service Worker 2019-11-05 @ 16:07:32] Lifecycle event: [install]
[Application 0.0.1.2019.11.05-52] Service Worker lifecycle event: [installed]
[Application 0.0.1.2019.11.05-52] Service Worker lifecycle event: [waiting]
[Application 0.0.1.2019.11.05-52] handle waiting...
[Service Worker 2019-11-05 @ 16:07:32] Message event: [SKIP_WAITING]
[Application 0.0.1.2019.11.05-52] Service Worker lifecycle event: [redundant]
[Application 0.0.1.2019.11.05-52] Service Worker lifecycle event: [activating]
[Application 0.0.1.2019.11.05-52] Service Worker lifecycle event: [controlling]
[Service Worker 2019-11-05 @ 16:07:32] Lifecycle event: [activate]
[Application 0.0.1.2019.11.05-52] Service Worker lifecycle event: [activated]
[Application 0.0.1.2019.11.05-52] Reloading this window...
[Application 0.0.1.2019.11.05-53] <<<<<<<<<<<<<<< STARTING >>>>>>>>>>>>>>>
[Application 0.0.1.2019.11.05-53] Preloading 125 images...
[Application 0.0.1.2019.11.05-53] ServiceWorker registered!

And sometimes it just hangs...

And sometimes things just get stuck when there is a new service worker version available. The waiting or externalwaiting event handler is never triggered.

Chrome Dev Tools "trying to install"

When this happens there are no errors in the console and there is nothing wrong with the new version of the service worker.

The only way to un-stick things is to stop the active service worker, unregister it and reload.

Why does any of this matter?

I need to make sure that the update process works flawlessly on all platforms before I dare release this app...

If a user encounters either of these issues when I release an update they will struggle to get it and there will be nothing I can do about it. Multiplied by the number of installations == huge headache.

This all boils down to three questions: In order of importance:

  1. How can I handle externalwaiting so that the new version of the service worker is loaded and activated? How do I make sure that the SKIP_WAITING message is received by the waiting service worker?
  2. Why do the lifecycle events vary between the plain and external varieties?
    • For testing purposes I am making the same update to the service worker each time (a new version of a precached file).
    • I only ever have the app loaded in one tab.
    • The switch from plain to external events seems random.
  3. Why does the process sometimes get stuck before the waiting phase of the lifecycle and what can I do about this?
1

There are 1 answers

0
daffinm On

Please note that this has been solved now in Workbox window. There's an updated advanced recipe:

https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users

Note also that there are (or were) issues with the code in this recipe. See the following issue for fixed code.

https://github.com/GoogleChrome/workbox/issues/2430