Serviceworker registration in Chrome sometimes hangs - javascript

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.
});

Related

self.skipWaiting() not working in Service Worker

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

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]);
// });
});

Firebase Messaging service worker on chrome

I am trying to implement FCM chrome client on Eclipse IDE.
Using Javascript, I am trying to register the default service worker : firebase-messaging-sw.js but the path for this js file cannot be resolved by project.
The SDK is searching for this file at hosting level: /firebase-messaging-sw.js i.e. https://localhost:8080/firebase-messaging-sw.js but not at https://localhost:8080/myapp/firebase-messaging-sw.js
Therefore, I get Error :
Firebase Service worker not found while using GWT (404 Error)
How can I resolve this issue ?
Service workers by default should reside on the root of the application. To register a service worker st some other location than the default location, you can use the following code.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-test/sw.js', {scope: '/sw-test/'})
.then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}
From MDN,
The outer block performs a feature detection test to make sure service workers are supported before trying to register one.
Next, we use the ServiceWorkerContainer.register() function to register the service worker for this site, which is just a JavaScript
file residing inside our app (note this is the file's URL relative to
the origin, not the JS file that references it.)
The scope parameter is optional, and can be used to specify the subset of your content that you want the service worker to control. In
this case, we have specified '/sw-test/', which means all content
under the app's origin. If you leave it out, it will default to this
value anyway, but we specified it here for illustration purposes.
The .then() promise function is used to chain a success case onto our promise structure. When the promise resolves successfully, the
code inside it executes.
Finally, we chain a .catch() function onto the end that will run if the promise is rejected.
This registers a service worker, which runs in a worker context, and
therefore has no DOM access. You then run code in the service worker
outside of your normal pages to control their loading.

BreezeJs: SaveChanges() server response getting dropped

I have breezeJs running in an angular app on mobile device (cordova), which talks to .Net WebApi.
Everything works great, except once in a while the device will get PrimaryKey violations (from my SQL Server).
I think I narrowed it down to only happening when data connection is shakey on the device.
The only way I can figure these primary key violations are happening is somehow the server is Saving Changes, but the mobile connection drops out before the response can come back from server that everything saved OK.
What is supposed to happen when BreezeJS doesn't hear back from server after calling SaveChanges?
Anyone familiar with BreezeJS know of a way to handle this scenario?
I've had to handle the same scenario in my project. The approach I took was two part:
Add automatic retries to failed ajax requests. I'm using breeze with jQuery, so I googled "jQuery retry ajax". There's many different implementations, mine is somewhat custom, all center around hijacking the onerror callback as well as the deferred's fail handler to inject retry logic. I'm sure Angular will have similar means of retrying dropped requests.
In the saveChanges fail handler, add logic like this:
...
function isConcurrencyException(reason: any) {
return reason && reason.message && /Store update, insert, or delete statement affected an unexpected number of rows/.test(reason.message);
}
function isConnectionFailure(reason: any): boolean {
return reason && reason.hasOwnProperty('status') && reason.status === 0
}
entityManager.saveChanges()
.then(... yay ...)
.fail(function(reason) {
if (isConnectionFailure(reason)) {
// retry attempts failed to reach server.
// notify user and save to local storage....
return;
}
if (isConcurrencyException(reason)) {
// EF is not letting me save the entities again because my previous save (or another user's save) moved the concurrency stamps on the record. There's also the possibility that a record I'm try to save was deleted by another user.
// recover... in my case I kept it simple and simply attempt to reload the entity. If nothing is returned I know the entity was deleted. Otherwise I now have the latest version. In either case a message is shown to the user.
return;
}
if (reason.entityErrors) {
// We have an "entityErrors" property... this means the saved failed due to server-side validation errors.
// do whatever you do to handle validation errors...
return;
}
// an unexpected exception. let it bubble up.
throw reason;
})
.done(); // terminate the promise chain (may not be an equivalent in Angular, not sure).
One of the ways you can test spotty connections is to use Fiddler's AutoResponder tab. Set up a *.drop rule with a regex that matches your breeze route and check the "Enable Automatic Responses" box when you want to simulate dropped requests.
This is a somewhat messy problem to solve- no one size fits all answer, hope this helps.
NOTE
Ward makes a good point in the comments below. This approach is not suitable in situations where the entity's primary key is generated on the server (which would be the case if your db uses identity columns for PKs) because the retry logic could cause duplicate inserts.

Categories

Resources