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.
- Make a small update to the app that results in the service worker file changing.
- Press the 'Check for Updates' button.
- New service worker is found & user is prompted to install the update
- 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.
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
- The user has just pressed the 'check for updates' button.
- A new service worker version has been found on
2019-11-05 @ 15:23:14
and is being parsed. We'll call this SWv2. - Workbox is loaded by SWv2.
- SWv2
install
event handler is executed. - The Workbox window
externalinstalled
event handler is executed. - The Workbox window
externalwaiting
event handler is executed. - The workbox window
externalwaiting
event handler sends aSKIP_WAITING
message usingworkboxWindow.messageSW()
- 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.
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:
- How can I handle
externalwaiting
so that the new version of the service worker is loaded and activated? How do I make sure that theSKIP_WAITING
message is received by the waiting service worker? - 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.
- Why does the process sometimes get stuck before the waiting phase of the lifecycle and what can I do about this?
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