self.skipWaiting() not working in Service Worker - javascript

I have a service worker. Here's the install event:
self.addEventListener('install', function (event) {
console.log('Installing Service Worker ...', event);
return self.skipWaiting()
.then(() => caches.open(CACHE_STATIC_NAME))
.then(function (cache) {
return cache.addAll([
'./file1.html',
'./file2.html'
])
})
});
For some reason, when I edit the service worker code and update the query parameter in the service worker file URL, it installs but does not activate (according to Chrome DevTools) — even though I've called self.skipWaiting().
Oddly if I go into the console, go to the scope of the service worker and type self.skipWaiting() myself, it activates immediately.
I've been trying to work out what's going on for many hours now, and I'm completely stumped. Is there something I'm missing here?

The old SW might not stop while it's still running tasks - for example if it had a long running fetch request (e.g server sent events / event source, or fetch streams; although I don't think websockets can cause this as SW will ignore them I think).
I find the behaviour to be different between browers however. Chrome seems to wait while the existing task is running (so skipWaiting will fail...), but Safari seems to kill the task and activate the new SW.
A good way to test if this is causing your issue would be to kill your server just after you request the skipWaiting (to kill the network connections). (Just clicking "Offline" in Dev Tools doesn't seem to kill all running connections, for example EventSources stay running.)
You can have the SW ignore certain routes (below), or you could try and force the requests to terminate (maybe using AbortController).
self.addEventListener('fetch', function(event) {
const { method, url } = event.request;
if(event.request.method !== "GET") return false;
if(url === "https://example.com/poll") return false;
event.respondWith(
caches.match(match).then(function(response) {
return response || fetch(event.request);
})
);
});
The process for skipWaiting is in this spec:
https://w3c.github.io/ServiceWorker/#try-activate-algorithm
But I don't find it very clear about whether the browser should wait for tasks or terminate them (or transfer them to the new SW?), before activating the new SW; and as mentioned, it seems to work differently between browsers at the moment...

I had experienced the same issue.
I validated my issue by the trick given by #ej.daly that I should stop the server & the WAITING service-worker will become active in few minutes.
After that, I applied a hack in my code: Reload the window if the WAITING service worker isn't activated in next 3 seconds.
BTW, I am developing a library to make service-worker installation easy for the various (commonly used) scenarios in our company. Have a look at https://github.com/DreamworldSolutions/workbox-installer

Related

Serviceworker registration in Chrome sometimes hangs

Sometimes registration of the service worker (SW) hangs. 
Since our PWA relies on the SW, and won't start without it, it looks like our App hung on startup. 
It seems I need to code an escape hatch. 
I seek suggestions.
The fact that the registration hangs was established by inserting 90 second timeout, as you see below.
const serviceWorker = navigator.serviceWorker;
if( serviceWorker && typeof serviceWorker.register === 'function' ) {
swRegisterTimeout = setTimeout(swRegTimedOut,90000);
serviceWorker.register('serviceworker.js')
.then(onServiceworkerRegistered)
.catch(function(err){
clearTimeout(swRegisterTimeout);
message("Service Worker Register Failure");
})
} else {
if ( serviceWorker ) {
message("'serviceWorker.register' is not a function")
}else{
message("'navigator.serviceWorker' does not exist")
}
}
});
function swRegTimedOut(){
message("ServiceWorker unable to register in 90 seconds")
}
function onServiceworkerRegistered(){
var serviceWorker = navigator.serviceWorker,
postBox = serviceWorker.controller;
clearTimeout(swRegisterTimeout);
message("Service Worker Registered");
serviceWorker.ready.then(function(reg){
if(reg.active){
// More stuff
}
})
}
This happens only when our internet is operable, but barely. 
Then I get the hang - with the timeout message ninety seconds later - every time, and the App does not launch.
If there is no internet, no hang; everything starts instantly. 
In fact, I can break the hang by turning off Wifi on the R&D machine. 
Boom! 
Everything loads immediately.
I speculate that somewhere in the registration code, I/O is being attempted. 
If said I/O does not fail or succeed, the registration does not come back.
In order to selectively bypass the registration step, our code would need to be able to recognize the difference between a hang and a valid registration. 
We gotta be careful here; I don't wanna write something that will break support for other browsers. 
EDIT: This is only a concern if a serviceworker is in place. 
It goes without saying that I do not expect to load a fresh service worker and the initial files on a near-dead network.
Background
Let's break down what navigator.serviceWorker.register() does, and what the promise that it returns represents.
The definitive set of steps is documented in the service worker specification, beginning with the "Start Register" job. There is slightly different behavior depending on whether the service worker scriptURL and scope have been previously registered or not, so the first thing to ask yourself is whether you're seeing this only when you're registering for the first time, in a "clean" browser, or whether you also see it when there's an identical, pre-existing service worker registration.
Either way, the promise that navigator.serviceWorker.register() returns fulfills once it's clear that the registration is for a script resource that's valid JavaScript with a scope that follows the security restrictions.
The promise will reject if you attempt to register a JavaScript file with invalid syntax, or if you use a scope that's not compatible with the service worker restrictions, or if there is a network error that prevents the JavaScript from being requested.
It's important to note that the way the navigator.serviceWorker.register() resolves does not reflect whether the service worker being registered actually installed correctly. It's possible to register a service worker script that does not contain any syntax errors and has a valid scope (so the navigator.serviceWorker.register() will fulfill), but which becomes redundant during the install phase, perhaps because the promise it passes to installEvent.waitUntil() ends up rejecting. The fact that navigator.serviceWorker.register() fulfills does imply that you'll always have an installed service worker!
Using navigator.serviceWorker.ready independently of registration
So, with that out of the way, let's get to your specific code snippet. If you're seeing the promise returned by navigator.serviceWorker.register() rejected after a long delay when you have a flaky network connection, it's almost certainly being rejected because the network request for the service worker script ends up timing out. There's really not much you could do about this scenario—that's the definition of what happens when you have a flaky network, and to ensure that your service worker is always "fresh", the request for the service worker script will bypass any caches by default.
I think the problem you're running into, though, is that you're using navigator.serviceWorker.ready execute some code once there's an active service worker, but you're only calling it if the navigator.serviceWorker.register() promise fulfills. For the reasons explained above, I don't think you should structure your code that way.
Instead, I would just break up your code into two steps—one that calls navigator.serviceWorker.register(), and one that waits for navigator.serviceWorker.ready to fulfill, independent of each other.
navigator.serviceWorker.register()
.then(() => console.log('Registration succeeded.')
.catch((error) => console.log('Registration failed: ', error));
navigator.serviceWorker.ready.then(() => {
// Put whatever code requires an active service worker here.
});

What happens if I add URLs to service worker cache outside of the waitUntil() flow?

Can I add items to my service worker cache outside of the waitUntil workflow of the "install" event and what happens if I do? Is this a bad idea?
I have a single-page app where I want to cache the main components and assets of the app immediately upon service worker install and then lazy-cache some secondary pages that the user may not get to. Without service workers I would be dynamically importing these secondary assets so the user doesn't need to download them on first-load.
What I am doing currently is this:
self.addEventListener("install", e => {
e.waitUntil(
(async () => {
const cache = await caches.open(cacheName);
return cache.addAll(appShell);
})()
);
caches.open(cacheName).then(cache => {
return cache.addAll(secondaryPages);
});
});
secondaryPages is being cached by the SW, but I'm not sure if this is the best way to go about it. I'm expecting the above code to say "don't finish installing until appShell is cached, but don't wait on secondaryPages".
Does this give me the lazy performance boost I'm looking for or should I be adding the secondaryPages to the cache at a later time, say a message event fired from the app once things are loaded in the client?
I realised I could use a setTimeout to mimic a slow response to the secondary cache to see how the app would behave.
caches.open(cacheName).then(cache => {
setTimeout(() => {
cache.addAll(secondaryManifest);
console.log("added secondary cache");
}, 1000 * 20);
});
With this, the SW installs correctly, caching the main app shell. 20 seconds later, the secondary files are cached, but in meantime the app is fully useful. If the pages that need those lazy-loaded files are requested, they go through the regular fetch handling that I've set up (check cache, then go to the network if needed). This could lead to the cache.addAll in the setTimeout overwriting those items in the cache, but that shouldn't really be an issue, at least not from a UX point of view, though the SW may be double downloading.
So yes, using cache.addAll outside of waitUntil works, and will just cache the files in the background while the app and SW continue on their merry way. This allows for more efficient SW installation.

What are the consequences of unregistering the service worker in create-react-app project?

I have a create-react-app project and I am running into an issue when there is a new production release the users don't get the latest version unless the clear the cache or in the best scenario when refreshing the page after they access it.
My Cloudflare cache expiry is set to 4 hours, but obviously the users still get the old version after this period. This leaves me thinking that it is a service worker issue.
Are there any other reasons that lead to this behaviour?
What are the possible solutions?
Is unregistering the SW considered a good solution for this issue? Knowing that I don't need my app to run offline at the moment.
If it is a good solution what are the consequences of unregistering it?
Do I need to use cache-control headers (ie max-age=0) in my index.html?
I know it is a lot of questions, but I wanted to show the directions I am thinking of and the areas I am bit confused about.
Thank you for your time and help in advance.
Adding versioning to your service worker cache is one of the ways you can ensure the new service worker gets installed whenever there is a new build. Just add a script which increments the version of the cache with each new build which causes a byte difference in service worker which in enough for the browser to trigger the new install event.
In your service worker file add something like
const version = 1;
let cacheV = 'foo' + version;
In your Activate event add logic that if there is a version mismatch delete the old cache.
self.addEventListener("activate", function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheV!== cacheName && cacheName.startsWith("foo")) {
return caches.delete(cacheName);
}
})
);
})
);
});
Also you can add update logic to your fetch listener which will fetch the latest file from network like
event.waituntil(update(request));

How to cache bust sw-toolbox?

I have been toying around with service workers and sw-toolbox. Both are great methods but seems to have their weaknesses.
My project started out using Google's method of service workers (link). The way I see this is that you have to manually update the version number for cache busting. I could be wrong also but I don't think the pages that the users has visited will not be cached.
Compared to the sw-toolbox method, all I need to add is the following code:
self.toolbox.router.default = self.toolbox.networkFirst;
self.toolbox.router.get('/(.*)', function (req, vals, opts) {
return self.toolbox.networkFirst(req, vals, opts)
.catch(function (error) {
if (req.method === 'GET' && req.headers.get('accept').includes('text/html')) {
return self.toolbox.cacheOnly(new Request(OFFLINE_URL), vals, opts);
}
throw error;
});
});
Then the problem of caching pages will be solved. Here is my issue: after applying the sw-toolbox to my project, the old service worker doesn't get cleared or replaced by the new one unless I go to the dev tools to clear it.
Any ideas how to get around this?
Here is my issue: after applying the sw-toolbox to my project, the old
service worker doesn't get cleared or replaced by the new one unless I
go to the dev tools to clear it.
The browser checks for updates to the service worker file every time it requests a resource in the service worker scope. If there is a byte difference in the service worker files, the browser will install the new service worker. You only need to update the service worker manually in dev tools because the app is still running, and the browser does not want to activate a new service worker while the old one is still in use.
If you close all pages associated with the service worker (like a user would when leaving your app), the browser will be able to activate the new service worker the next time your page is opened.
If you want to force the new service worker to take over, you can add self.skipWaiting(); to the install event. Here is some documentation with an example.
You can learn just about everything you need to know about the service worker's life cycle from this post by Jake Arichbald.
As far as caching & cache management goes, tools like sw-toolbox will handle cache busting for you. And actually, Workbox is a new tool that is meant to replace sw-toolbox & sw-precache. It will also handle cache busting and cache management (by comparing file hashes & setting/tracking resource expiration dates).
Generally speaking, you should always use a tool like Workbox to write your service workers. Writing them by hand is error prone and you are likely to miss corner cases.
Hope that helps.
P.S. If you end up not using skipWaiting and instead only updating when the page is closed & re-opened by a user, you can still enable automatic updating for development. In Chrome's dev tools, Application > Service Workers has an Update on reload option to automatically update the service worker.
I don't know if sw_toolbox has cache busting built in. Typically when you change the service worker and need to purge the previous version's cache you should do that with in the activate event handler.
The best practice here is to name your caches with the sw version number included. Here is some example code from an online course I have on service worker caching that might get you started:
self.addEventListener("activate", event => {
console.log("service worker activated");
//on activate
event.waitUntil(caches.keys()
.then(function (cacheNames) {
cacheNames.forEach(function (value) {
if (value.indexOf(config.version) < 0) {
caches.delete(value);
}
});
return;
})
);
});

Passing state info into a service worker before `install`

Background
I'm new to service workers but working on a library that is intended to become "offline-first" (really, almost "offline-only") (FWIW, the intent is to allow consumers of the library to provide JSON config representing tabular multilinear texts and get in return an app which allows their users to browse these texts in a highly customizable manner by paragraph/verse ranges.)
Other projects are to install the library as a dependency and then supply information via our JavaScript API such as the path of a JSON config file indicating the files that our app will consume to produce an (offline) app for them.
While I know we could do any of the following:
require users provide a hard-coded path from which our service worker's install script could use waitUntil with its own JSON request to retrieve the user's necessary files
skip the service worker's install step of the service worker for the JSON file, and rely on fetch events to update the cache, providing a fallback display if the user completed the install and went offline before the fetches could occur.
Post some state info from our main script to a server which the service worker, once registered, would query before completing its install event.
...but all choices seems less than ideal because, respectively:
Our library's consumers may prefer to be able to designate their own location for their JSON config.
Given that the JSON config designates files critical to showing their users anything useful, I'd rather not allow an install to complete only to say that the user has to go back online to get the rest of the files if they were not able to remain online after the install event to see all the required fetches occur.
Besides wanting to avoid more trips to the server and extra code, I'd prefer for our code to be so offline-oriented as to be able to work entirely on mere static file servers.
Question:
Is there some way to pass a message or state information into a service worker before the install event occurs, whether as part of the query string of the service worker URL, or through a messaging event? The messaging event could even technically arrive after the install event begins as long as it can occur before a waitUntil within the install is complete.
I know I could test this myself, but I'd like to know what best practices might be anyways when the critical app files must themselves be dynamically obtained as in such libraries as ours.
I'm guessing indexedDB might be the sole alternative here (i.e., saving the config info or path of the JSON config to indexedDB, registering a service worker, and retrieving the indexedDB data from within the install event)? Even this would not be ideal as I'm letting users define a namespace for their storage, but I need a way for it too to be passed into the worker, or otherwise, multiple such apps on the origin could clash.
Using a Query Parameter
If you find it useful, then yes, you can provide state during service worker installation by including a query parameter to your service worker when you register it, like so:
// Inside your main page:
const pathToJson = '/path/to/file.json';
const swUrl = '/sw.js?pathToJson=' + encodeURIComponent(pathToJson);
navigator.serviceWorker.register(swUrl);
// Inside your sw.js:
self.addEventListener('install', event => {
const pathToJson = new URL(location).searchParams.get('pathToJson');
event.waitUntil(
fetch(pathToJson)
.then(response => response.json())
.then(jsonData => /* Do something with jsonData */)
);
});
A few things to note about this approach:
If you fetch() the JSON file in your install handler (as in the code sample), that will effectively happen once per version of your service worker script (sw.js). If the contents of the JSON file change, but everything else stays the same, the service worker won't automatically detect that and repopulate your caches.
Following from the first point, if you work around that by, e.g., including hash-based versioning in your JSON file's URL, each time you change that URL, you'll end up installing a new service worker. This isn't a bad thing, per se, but you need to keep it in mind if you have logic in your web app that listens for service worker lifecycle events.
Alternative Approaches
You also might find it easier to just add files to your caches from within the context of your main page, since browsers that support the Cache Storage API expose it via window.caches. Precaching the files within the install handler of a service worker does have the advantage of ensuring that all the files have been cached successfully before the service worker installs, though.
Another approach is to write the state information to IndexedDB from the window context, and then read from IndexedDB inside of your service worker's install handler.
Update 3:
And since it is not supposed to be safe to rely on globals within the worker, my messaging solution seems even less sound. I think it either has to be Jeff Posnick's solution (in some cases, importScripts may work).
Update 2:
Although not directly related to the topic of this thread relating to "install" event, as per a discussion starting at https://github.com/w3c/ServiceWorker/issues/659#issuecomment-384919053 , there are some issues, particularly with using this message-passing approach for the activate event. Namely, the activate event may never fail, and thus never be tried again, leaving one's application in an unstable state. (A failure of install will at least not apply the new service worker to old pages, whereas activate will keep fetches on hold until the event completes, which it may never do if it is left waiting for a message that was not received, and which anything but a new worker will fail to correct since new pages won't be able to load to send that message again.)
Update:
Although I got the client from within the install script in Chrome, I wasn't able to receive the message back with navigator.serviceWorker.onmessage for some reason.
However, I was able to fully confirm the following approach in its place:
In the service worker:
self.addEventListener('install', e => {
e.waitUntil(
new Promise((resolve, reject) => {
self.addEventListener('message', ({data: {
myData
}}) => {
// Do something with `myData` here
// then when ready, `resolve`
});
})
);
});
In the calling script:
navigator.serviceWorker.register('sw.js').then((r) => {
r.installing.postMessage({myData: 100});
});
#JeffPosnick 's is the best answer for the simple case I described in the OP, but I thought I'd present my discovering that one can get messages from and into a service worker script early (tested on Chrome) by such as the following:
In the service worker:
self.addEventListener('install', e => {
e.waitUntil(self.clients.matchAll({
includeUncontrolled: true,
type: 'window'
}).then((clients) => new Promise((resolve, reject) => {
if (clients && clients.length) {
const client = clients.pop();
client.postMessage('send msg to main script');
// One should presumably be able to poll to check for a
// variable set in the SW message listener below
// and then `resolve` when set
// Despite the unreliability of setting globals in SW's
// I believe this could be safe here as the `install`
// event is to run while the main script is still open.
}
})));
});
self.addEventListener('message', e => {
console.log('SW receiving main script msg', e.data);
e.ports[0].postMessage('sw response');
});
In the calling script:
navigator.serviceWorker.addEventListener('message', (e) => {
console.log('msg recd in main script', e.data);
e.source.postMessage('sending back to sw');
});
return navigator.serviceWorker.register(
'sw.js'
).then((r) => {
// navigator.serviceWorker.ready.then((r) => { // This had been necessary at some point in my testing (with r.active.postMessage), but not working for me atm...
// Sending a subsequent message
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (e) => {
if (e.data.error) {
console.log('err', e.data.error);
} else {
console.log('data', e.data);
}
};
navigator.serviceWorker.controller.postMessage('sending to sw', [messageChannel.port2]);
// });
});

Categories

Resources