What exactly triggers this service worker code to run? - javascript

Create-react-app comes with a registerServiceWorker.js file that contains code to register a service worker. I'm just a bit confused as to how it works. The code in question is:
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
What needs to happen for that first console log, the one that displays "New content is available; please refresh," to display?
More specifically, how can I trigger this code to run when index.html changes (in the event that a script filename changes).

Let's break it down step by step.
navigator.serviceWorker.register Promise is resolved when the valid Service Worker existence has been established
registration.onupdatefound registers a listener for an event that is fired when the HTTP request for Service Worker has been resolved to some other file than previously (or when the SW has been found for the first time)
registration.installing.onstatechange registers a listener for the new Service Worker's lifecycle changes (from installing to installed, from installed to activating etc.)
if (installingWorker.state === 'installed') filters out all the states other than installed - so its positive branch will be executed after each new SW has been installed
if (navigator.serviceWorker.controller) checks if the page is currently controlled by any (previous) Service Worker. If true then we're handling the aforementioned update scenario here.
So summing up - this console.log will execute after the updated (not the first one) Service Worker has been correctly installed.
It will not be triggered after index.html change. It's only Service Worker code (pointed to by serviceWorker.register method) that is checked against. Note also that normally browsers (or Chrome at least?) do not check for the new SW version for 24h after the current one was downloaded. Note also that plain old HTTP cache set for the Service Worker file might mess up here if it was send with too aggresive cache header.

Related

Service Worker problems: cannot get web app to use newly installed cache version of JS file

I'm trying to get a basic service worker-based updating capability going for my web app.
(You'll see in the code below that I clear the cache by specific name - 4.7 - and by dynamic name. This is because it seems to me that the code I'm using clears the NEW cache name, not the old one. Maybe a separate issue. Right now I'm clearing both.)
If I change the cacheName and the annotateAccount.js file (this running on localServer) I can see that the service worker does its job: downloads a new version from the server (I see a status of "200 OK" in the console vs. "200 OK (from service worker)" when running a page refresh without an update to the service worker file.
Even though the code downloads a new version of the JS, my actual app (running in Chrome) pulls from an old cached version of the file.
If I "empty cache and hard reload" I get the new version. If I immediately do a regular refresh I get the old (browser cached) version of the file.
I've read enough to know there is browser caching on top of service worker caching, but what's the F-ing point of using a service worker if you can't overcome the browser's caching? I'm using XAMMP/Apache to run my localhost dev environment. I haven't tried this in a staging or production environment on my actual servers. Since I seem to be fighting Chrome's local caching features I'm not sure that matters.
I'd previously just put "?v=xxx" after filenames throughout my app, and changed the parameter when I released updates, in an attempt to give users the latest code. This doesn't work well.
What am I missing? Any direction/help appreciated.
Here's my service worker JS file
var urlsToCache = [
'annotateAccount.html',
'annotateAccount.js',
'web_background.js'
];
const cacheName = '4.8';
self.addEventListener('install', event => {
console.log('Install event...', urlsToCache);
event.waitUntil(
caches.open(cacheName)
.then(function(cache) {
console.log('Opened cache', cacheName);
return cache.addAll(urlsToCache);
})
);
});
var url;
self.addEventListener('fetch', (event) => {
//Cache falling back to network
url = event.request.url.split(".").pop(); // check php
if (url !== "php"){
event.respondWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request);
})
);
} else { // END check for PHP - skip trying cache/SW.
console.log('Trying a php file - always let code run it...');
}
});
self.addEventListener('message', function (event) {
console.log('ServiceWorker cache version: ', cacheName, event);
console.log('Received msg1: ', event.data.action);
if (event.data.action === 'skipWaiting') {
console.log('ccClearing cache: ', cacheName);
caches.delete(cacheName); // actually removes cached versions
caches.delete("4.7"); // delete NAMED cache...
self.skipWaiting();
} else {
console.log('Do not clear cache...');
}
});

How to replace appcache lifecycle event with ServiceWorker lifecycle

I am trying to change appcache to serviceworker in my application for offline work. I have removed manifest tag from html page and created serviceworker.js as of lifecycle events. But, appcache events are being used like this below:
function framework(){
this.appCache = window.applicationCache;
//more other codes
}
var fw = new framework();
Now, lifecycle events are checked like this:
if (fw.appCache) {
/* Checking for an update. Always the first event fired in the sequence.*/ fw.appCache.addEventListener('checking', handleCacheEvent, false);
/* An update was found. The browser is fetching resources.*/ fw.appCache.addEventListener('downloading', handleCacheEvent, false);
/* Fired if the manifest file returns a 404 or 410.
This results in the application cache being deleted. */ fw.appCache.addEventListener('obsolete', handleCacheEvent, false);
/* The manifest returns 404 or 410, the download failed,
or the manifest changed while the download was in progress.*/ fw.appCache.addEventListener('error', handleCacheError, false);
/* Fired for each resource listed in the manifest as it is being fetched.*/fw.appCache.addEventListener('progress', handleProgressEvent, false);
/*Fired after the first cache of the manifest. */ fw.appCache.addEventListener('cached', function(event){handleCacheEvent(event);removeProcessing();$("#processingTextId").html("");}, false);
/* Fired after the first download of the manifest.*/ fw.appCache.addEventListener('noupdate', function(event){handleCacheEvent(event);removeProcessing(); $("#processingTextId").html("");}, false);
/* Fired when the manifest resources have been newly redownloaded. */ fw.appCache.addEventListener('updateready', function(e) {if (fw.appCache.status == fw.appCache.UPDATEREADY) {alert('Successful'); try{fw.appCache.swapCache();window.location.reload();} catch(err){}}}, false);
}
function handleCacheEvent(event) {$("#processingTextId").html(event.type);}
function handleProgressEvent(event) {$("#processingTextId").html("(" + event.loaded + " of "+ event.total +")");}
function handleCacheError(event) {$("#processingTextId").html("Cache failed to update!")
So, my question is how to replace this events with service worker events. My serviceworker is registered and caching the assets properly. Now, i am doing like this in index.html
Registartion
<script type="text/javascript">
if('serviceWorker' in navigator){
navigator.serviceWorker.register('serviceworker.js')
.then(registration =>{
console.log('registration successful:',registration.scope);
})
.catch(err=>{
console.log('registration failed:',err);
});
}
</script>
I have created the seperate serviceworker.js.
How to replace those appcache events with serviceworker?
You won't end up with any of those events automatically when using a service worker. Also, the model for when a service worker populates and updates a cache is much more "open ended" than with AppCache, so translating service worker caching into equivalent AppCache events is not always possible.
In general, though, here are two things that can help:
Read up on the Service Worker Lifecycle. Some events that you might care about could be approximated by listening for equivalent changes to the service worker lifecycle. For instance, if you precache some URLs during service worker installation, then a newly registered service worker leaving the installing state would be roughly equivalent to when an AppCache manifest finishes caching. Similarly, if you detect when there's an update to a previously registered service worker (and that update is due to a change in the list of URLs to precache), then that would roughly correspond to when an AppCache manifest is updated.
If your service worker uses "runtime" caching, where URLs are added to the cache inside of a fetch handler, that you could use the following technique to tell your client page(s) when new items have been cached, using postMessage() to communicate.
Part of your service worker's JavaScript:
const cacheAddAndNotify = async (cacheName, url) => {
const cache = await caches.open(cacheName);
await cache.add(url);
const clients = await self.clients.matchAll();
for (const client of clients) {
client.postMessage({cacheName, url});
}
};
self.addEventListener('fetch', (event) => {
// Use cacheAddAndNotify() to add to your cache and notify all clients.
});
Part of your web app's JavaScript:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
const {cacheName, url} = event.data;
// cacheName and url represent the entry that was just cached.
});
}
Again, the specifics of what you listen for and how you react to it are really going to depend on exactly what logic you have in your service worker. Don't expect there to be a 1:1 mapping between all events. Instead, use this migration as an opportunity to think about what cache-related changes you actually care about, and focus on listening for those (via service worker lifecycle events, or postMessage() communication).

How do I test updating a service worker in a development environment?

What I'd like to achieve is the ability to push a notification to the user when an update is available to my React webpage, and I am trying to do so via serviceWorkers.
The challenge I am facing is to get the installation property of the serviceWorkerRegistration interface to not return null. I just want to trigger
console.log('New content is available; please refresh.')
I am quite new to working with serviceWorkers in React. Does anyone have an idea on how to go about testing the update functionality of serviceWorkers in a development environment?
console.log('I am in registerValidSW!')
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
console.log(registration)
registration.onupdatefound = () => {
const installingWorker = registration.installing //This returns null
if (installingWorker) {
console.log('installingServiceWorker!!!')
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a 'New content is
// available; please refresh.' message in your web app.
console.log('New content is available; please refresh.')
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// 'Content is cached for offline use.' message.
console.log('Content is cached for offline use.')
}
}
}
}
}
})
.catch((error) => {
console.error('Error during service worker registration:', error)
})
#krish according to new documentation-> The service worker is only enabled in the production environment, e.g. the output of npm run build. It's recommended that you do not enable an offline-first service worker in a development environment, as it can lead to frustration when previously cached assets are used and do not include the latest changes you've made locally. refer this link :- https://create-react-app.dev/docs/making-a-progressive-web-app/

Polymer app refresh strategy

Our app is a Polymer 2 single page app, where we have some custom build steps to generate versioned files of the resources (gulp-rev-all). All is working well, but we need to have implement a safe refresh of the application. What we are doing is that we keep latest installed git commit in a file that is served together with the application, and we pull for this file in intervals and alerts the user that there is a new version available and ask them to click on a button to refresh the application.
The problem is that we are using service worker with pre-cache as the default Polymer build provides. This means when we do location.reload() we actually get the content (index.html) from the service worker, and not from the server.
So the question is: how can we enforce that the service worker would be invalidated and force a new refresh of service-worker.js and index.html?
Here the ready code, all described how to handle service worker ;
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').then(function(reg) {
// updatefound is fired if sw.js changes.
reg.onupdatefound = function() {
// The updatefound event implies that reg.installing is set; see
// https://w3c.github.io/ServiceWorker/#service-worker-registration-updatefound-event
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and the fresh content will
// have been added to the cache.
// It's the perfect time to display a "New content is available; please refresh."
// message in the page's interface.
console.log('New or updated content is available.');
// Need to hard refresh when new content is available
location.reload(true);
} else {
// At this point, everything has been precached.
// It's the perfect time to display a "Content is cached for offline use." message.
console.log('Content is now available offline!');
}
break;
case 'redundant':
console.error('The installing service worker became redundant.');
break;
}
};
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}

Service Worker is "installing" with every page reload though the sw.js did not change

I am using a service worker to cache the JS and CSS files of a web app.
I mostly use the code from a Google Chrome example, the only thing I added is a notification and countdown that refreshes the window when "new or updated content is available".
The Google example: https://github.com/GoogleChrome/sw-precache/blob/5699e5d049235ef0f668e8e2aa3bf2646ba3872f/demo/app/js/service-worker-registration.js
However, this happens far too often and I don't understand why.
There has definitely not been any changes to the files on the server, but still sometimes I see the notification and the window reloads.
I expected this would only happen when any of the files governed by the service worker actually changes (they are all hashed via Webpack and have definitely not changed in between).
This is the code I use, inlined in index.html:
/* eslint-env browser */
'use strict';
function reloadApp(delay) {
var t = delay || 0;
var message = 'New or updated content is available. Reloading app in {s} seconds';
var getMessage = function() { return message.replace('{s}', t) }
var div = document.createElement('div');
div.id = 'update-notification';
div.innerHTML = getMessage();
document.body.appendChild(div);
var intervalID = setInterval(function() {
t = t - 1;
if (t <= 0) {
clearInterval(intervalID);
window.location.reload();
}
else {
div.innerHTML = getMessage();
}
}, 1000);
}
if ('serviceWorker' in navigator && window.location.protocol === 'https:') {
navigator.serviceWorker.register('/service-worker.js').then(function(reg) {
console.info('serviceWorker registered');
reg.onupdatefound = function() {
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
reloadApp(5);
} else {
console.log('Content is now available offline!');
}
break;
case 'redundant':
console.error('The installing service worker became redundant.');
break;
}
};
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
This is the change I made:
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and the fresh content will
// have been added to the cache.
// It's the perfect time to display a "New content is available; please refresh."
// message in the page's interface.
console.log('New or updated content is available.');
} else {
became:
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
reloadApp(5);
The service worker itself is generated via sw-precache-webpack-plugin and the hashed files look like this:
var precacheConfig = [["cms.CrudFactory.144344a2.js","6a65526f764f3caa4b8c6e0b84c1b31b"],["cms.routes.c20796b4.js","f8018476ceffa8b8f6ec00b297a6492d"],["common.cms-main.0f2db9ff.js","92017e838aff992e9f47f721cb07b6f0"],["common.licensing-main.8000b17d.js","0d43abd063567d5edaccdcf9c0e4c362"],["common.mediaplayer-main.314be5d2.js","2061501465a56e82d968d7998b9ac364"],["common.ordering-main.783e8605.js","0531c4a90a0fa9ea63fbe1f82e86f8c6"],["common.shared-main.0224b0ea.js","956ae4d2ddbddb09fb51804c09c83b22"],["common.stores-main.98246b60.js","cbdc46bc3abeac89f37e8f64b1737a22"],["component.App.284fec19.js","07f1923f1a0098bf9eba28fc7b307c18"],["component.AppToolbar.00e371de.js","9b542d4a85bdeece9d36ee4e389f6206"],["component.DevToolbar.652bf856.js","1744126e32774a93796146ac878ddd8e"],["component.Grid.4b964e52.js","755819ca2c7f911805e372401d638351"],["component.MediaPlayer.54db6e85.js","d0d8ae269665e810d1b997b473005f76"],["component.Search.05476f89.js","0cae8928aff533a6726dfaeb0661456a"],["data.country-list.d54f29a7.js","e27746418e02f75593a93b958a60807e"],["dev.sendSlackMessage.da8e22f4.js","ccb10829f18a727b79a5e8631bc4b2a2"],["index.html","02ff43eabc33c600e98785cffe7597d9"],["lib.gemini-scrollbar.df2fbf63.js","3941036bacb4a1432d22151ea7da073b"],["lib.isotope-horizontal.1604d974.js","6ac56d4296468c4428b5b5301a65938a"],["lib.isotope-packery.fabb73c3.js","808061641596e4b0ea2158b42d65915a"],["lib.react-color.265a6de0.js","f23f63d7e6934e381ffdc0405ecb449a"],["lib.react-date-picker.0a81fec3.js","778d1626645e439ad01e182ee589971a"],["lib.react-select.c768cf77.js","c8782991a161790ef2c9a2c7677da313"],["main.43e29bc6.js","fe6a6277acaa2a369ef235961be7bbcf"],["route.admin.CleanupPage.8b4bbf8e.js","8ab20412329e1ba4adc327713135be97"],["route.app.CmsPage.8a4303fb.js","0bf1506869db24bb9789c8a46449c8ad"],["route.app.CmsPageWrapper.accdebcc.js","c91e77aa8b518e1878761269deac22b6"],["route.app.ContactPage.75693d32.js","a530b00a5230a44897c2bf0aa9f402a8"],["route.app.PasswordResetExpiredDialog.65431bae.js","b5eef791dbd68edd4769bd6da022a857"],["route.app.SeriesDetailsPage.11a6989b.js","c52178b57767ae1bf94a9594cb32718e"],["route.cms.MetadataFormsPage.636188d2.js","e1e592b7e3dd82af774ac215524465c0"],["route.cms.PermissionsListPage.0e1e3075.js","9a3cc340a64238a1ab3ba1c0d483b7bd"],["route.cms.PermissionsPage.78a69f60.js","4b18e646715d6268e7aba3932f4e04a9"],["route.cms.SysconfigPage.f699b871.js","79bd1275213478f2ff1970e0b7341c49"],["styles.43e29bc620615f854714.css","b2e9e55e9ee2def2ae577ee1aaebda8f"],["styles.e0c12bb1c77e645e92d3.css","626778177949d72221e83b601c6c2c0f"],["translations.en.83fced0e.js","7e5509c23b5aafc1744a28a85c2950bb"],["translations.nl.4ae0b3bb.js","515ec7be7352a0c61e4aba99f0014a39"],["vendor.e0c12bb1.js","36ce9e0a59c7b4577b2ac4a637cb4238"]];
var cacheName = 'sw-precache-v3-nisv-client-' + (self.registration ? self.registration.scope : '');
Questions:
Why is the 'new or updated content' case happening so often?
Is the assumption correct that case 'installed': if (navigator.serviceWorker.controller) { means New or updated content is available?
How should I debug the problem?
A service worker has a life cycle:
- registered
- installing
- installed
- waiting
- activated
- redundant
When you register a service worker, it is fetched from the server, and it's lifespan in the disk cache depends on the cache-control response headers that your web server sends it with.
It is considered best practice, to have a cache-control : no-store must-revalidate along with a max-age : 0 if possible.
If you forget, the service worker is usually considered stale after 24 hours of registration, and is refetched and disk caches updated.
There are manual upgrades you can do, by bumping up a cache version on the service worker, or changing the service worker code. Even a byte change makes the browser view the service worker as a new one, and it installs the new service worker.
This however, does not mean, the old service-worker is discarded. It is imperative that any user close all the tabs / navigate away from your site, for the browser to discard the old service worker, and activate the new one.
you can bypass this behavior, by a self.skipWaiting in your install handler
That said,
While developing, you have the option of opening the browser's (chromium)
dev panel, navigate to the application tab, and check the box that says
update on reload
What this does is, immediately discard the old service worker irregardless of whether there is a change in your service worker code.
Regarding your questions:
Is the assumption correct that case 'installed': if (navigator.serviceWorker.controller) { means New or updated content is available?
NO. This means, that the new service worker, say sw-v1, is installed, and is currently waiting to activate.
Why is the 'new or updated content' case happening so often?
you say, you are using sw-precache. Since, I haven't used it myself, I have to add, a counter question
Is the plugin configured, to pick up changes to your static assets, and automatically trigger a code change or a cache bump in your service-worker ?
If so, then, any new version of your service worker, must only be waiting and not activating on a change.
The probable reason for this skipping of the waiting stage is,
you have a self.skipWaiting inside your install event inside your service worker.
Note the above is true, ONLY if
the change to your content happens. AND
that change triggers a service worker update
In this scenario, you might want to consider commenting out the self.skipWaiting statement. Or, maybe you could configure your plugin, to not skip on update - this matters on your content, and you need to take a call
In lieu of the fact that your assets changed and/ or triggered an update,
The other (most probable) reason would have to be,
you have an update on reload box checked under the application - > service workers tab on your dev console.
Uncheck that box, if checked, and then clear all caches, unregister all workers. After , open a new tab and continue testing the behavior.

Categories