How to update service workers? - javascript

Situation:
On mywebsite.com/game, I registered a service-worker with
navigator.serviceWorker.register('/service-worker.js', {scope: "/"});
On my server, '/service-worker.js' has a maxAge of 1d.
Problem:
service-worker.js has a major bug. It always displays an empty page and can't fetch anything. service-worker.js must be changed.
The problem is whenever a user goes to mywebsite.com/game, it displays the empty page and does nothing more. I am unable to make the client fetch the new service-worker.js.
How can I make the client fetch the new service-worker.js?

What you're describing—a check for updates to /service-worker.js—happens by default, automatically, under the circumstances laid out in this article:
An update is triggered if any of the following happens:
A navigation to an in-scope page.
A functional events such as push and sync, unless there's been an update check within the previous 24 hours.
Calling .register() only if the service worker URL has changed. However, you should avoid changing the worker URL.
All modern web browsers will ignore any Cache-Control headers you set on /service-worker.js by default and go directly against the web server to obtain the latest copy.
This Stack Overflow answer has some best practices for what the revised service-worker.js file should contain if you want it to behave like a "kill switch."

Just add ?v=1 to your script like this.
navigator.serviceWorker.register('/service-worker.js?v=1', {scope: "/"});
And increment the number of script version when you make changes of service worker's script

Related

How to make Angular Universal and PWA work together?

I have a SSR Angular app which I am trying to transform into a PWA. I want it to be server-side rendered for SEO and for the "fast first rendering" that it provides.
The PWA mode works fine when combined with SSR, but once the app is loaded, when we refresh it, the client index HTML file is loaded instead of the server-side rendered page.
I have dug into the code of ngsw-worker.js and I saw this:
// Next, check if this is a navigation request for a route. Detect circular
// navigations by checking if the request URL is the same as the index URL.
if (req.url !== this.manifest.index && this.isNavigationRequest(req)) {
// This was a navigation request. Re-enter `handleFetch` with a request for
// the URL.
return this.handleFetch(this.adapter.newRequest(this.manifest.index), context);
}
I have no control over this file since it's from the framework and not exposed to developers.
Did anybody find a solution or workaround for this?
Up-to-date answer (v11.0.0)
Angular now has a navigationRequestStrategy option which allows to prioritize server requests for navigation. Extract of the changelog:
service-worker: add the option to prefer network for navigation
requests (#38565) (a206852), closes #38194
To be used wisely! This warning appears in the documentation:
The freshness strategy usually results in more requests sent to the
server, which can increase response latency. It is recommended that
you use the default performance strategy whenever possible.
Old answer (for archaeological purposes)
I have found a working solution, the navigationUrls property of ngsw-config.json contains a list of navigation URLs included or excluded (with an exclamation mark) like explained in the documentation.
Then I configured it like this:
"navigationUrls": [
"!/**"
]
This way, none of the URLs redirect to index.html and the server-side rendered app comes into play when the app is first requested (or refreshed), whatever the URL is.
To go further, the three kinds of URLs managed by the service worker are:
Non-navigation URLs: static files cached by the service worker and listed in the generated ngsw.json file with their corresponding hashes
Navigation URLs: redirected to index.html by default, forwarded to the server if the "!/**" configuration is used
GET requests to the backend: forwarded to the backend
In order to distinguish a GET XMLHttpRequest from a navigation request, the service worker uses the Request.mode property and the Accept header that contains text/html when navigating and application/json, text/plain, */* when requesting the backend.
Edit: This is actually not a good practice to do that for two reasons:
Depending on the network quality, there is no guarantee that the server-side version will render faster than the cached browser version
It breaks the "update in background" mechanism. Indeed, the server-side rendered app will always refer to the latest versions of the JavaScript files
For more details on this, please take a look at the Angular's team member answer to my feature request: https://github.com/angular/angular/issues/30861

Force Service Worker to only return cached responses when specific page is open

I have built a portal which provides access to several features, including trouble ticket functionality.
The client has asked me to make trouble ticket functionality available offline. They want to be able to "check out" specific existing tickets while online, which are then accessible (view/edit) while the user's device is out-of-range of any internet connection. Also, they want the ability to create new tickets while offline. Then, when the connection is available, they will check in the changed/newly created tickets.
I have been tinkering with Service Workers and reviewing some good documentation on them, and I feel I have a basic understanding of how to cache the data.
However, since I only want to make the Ticketing portion of the portal available offline, I don't want the service worker caching or returning cached data when any other page of the portal is being accessed. All pages are in the same directory, so the service worker, once loaded, would by default intercept all requests from all pages in the portal.
How can I set up the service worker to only respond with cached data when the Tickets page is open?
Do I have to manually check the window.location value when fetch events occur? For example,
if (window.location == 'https://www.myurl.com/tickets')
{
// try to get the request from network. If successful, cache the result.
// If not successful, try returning the request from the cache.
}
else
{
// only try the network, and don't cache the result.
}
There are many supporting files that need to be loaded for the page (i.e. css files, js files, etc.) so it's not enough to simply check the request.url for the page name. Will 'window.location' be accessible in the service worker event, and is this a reasonable way to accomplish this?
Use service worker scoping
I know that you mentioned that you currently have all pages served from the same directory... but if you have any flexibility over your web app's URL structure at all, then the cleanest approach would be to serve your ticket functionality from URLs that begin with a unique path prefix (like /tickets/) and then host your service worker from /tickets/service-worker.js. The effort to reorganize your URLs may be worthwhile if it means being able to take advantage of the default service worker scoping and just not have to worry about pages outside of /tickets/ being controlled by a service worker.
Infer the referrer
There's information in this answer about determining what the referring window client URL is from within your service worker's fetch handler. You can combine that with an initial check in the fetch handler to see if it's a navigation request and use that to exit early.
const TICKETS = '/tickets';
self.addEventListener('fetch', event => {
const requestUrl = new URL(event.request.url);
if (event.request.mode === 'navigate' && requestUrl.pathname !== TICKETS) {
return;
}
const referrerUrl = ...; // See https://stackoverflow.com/questions/50045641
if (referrerUrl.pathname !== TICKETS) {
return;
}
// At this point, you know that it's either a navigation for /tickets,
// or a request for a subresource from /tickets.
});

Service worker automatic update process

I have a doubt about the service worker update process.
In my project there are 2 files related to sw:
"sw.js", placed in website root, will be NOT cached (by Cache API and Web Browser).
My service worker manages the cache of all statics files and all dynamic url pages.
Sometimes I need to update it and the client must detect that there's an update and do that immediatelly.
"sw_main.js" is the script that installs my sw. This file is cached by Cache API because my app must work offline.
Inside we can find:
var SW_VERSION = '1.2';
navigator.serviceWorker.register("sw.js?v=" + SW_VERSION, { scope: "/" }).then(....
The problem is: because sw_main.js is cached, if I change the SW_VERSION and then deploy online the webapp, all clients will not
update because cannot see the changes in that file.
Which is the best way to manage the SW update process?
As I now, there are 3 ways to trigger sw update:
push and sync events (but I'm not implementing these)
calling .register() only if the service worker URL has changed (but
in my case it's not possible because the sw_main.js is cached so I'm
not able to change the SW url)
navigation to an in-scope page (I think we've the same cache problem
of point 2)
I read also this: "Your service worker is considered updated if it's byte-different to the one the browser already has".
That means that if I change the content of sw.js (that is not cached), the service worker will automatically detect the update?
Thank you
I found 2 possibile solutions.
First of all I wanna say that it's better to cache (using pwa Cache API) also the sw.js because when you're offline, it will be requested by sw_main.js.
FIRST SOLUTION:
Use a the service worker's cache as a fallback and always attempt to go network-first via a fetch().
This only for sw.js and maybe sw_main.js.
You lose some performance gains that a cache-first strategy offers, but the js file size is very light so I don't think it's a big problem.
SECOND SOLUTION:
If your cached sw.js file has changed?
We can hook into "onupdatefound" function on the registered Service Worker.
Even though you can cache tons of files, the Service Worker only checks the hash of your registered service-worker.js.
If that file has only 1 little change in it, it will be treated as a new version.
So this confirm my previous question! I'll try it!
If it works, the second solution is the best

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

oidc-client CheckSessionIFrame fires properly one time, then fails ever interval thereafter

This may not actually be an issue with Identity Server or the oidc-client, but I am having trouble pinning down the problem. I am running this through System.js in an Aurelia application, so it's possible the issue originates from one of these external libraries.
In CheckSessionIFrame.start(session_state), we have the following code:
this._timer = window.setInterval(() => {
this._frame.contentWindow.postMessage(this._client_id + " " + this._session_state, this._frame_origin);
}, this._interval);
The first time the interval fires, there appear to be no problems. The iFrame's contentWindow exists (as expected) and the postMessage method is called without issue. Two seconds later, when the interval fires again, this._frame.contentWindow is undefined - so my best guess is the iFrame is dying somehow. Again, this may not be an issue with oidc-client, but I'm looking for any helpful guidance on what could cause this iFrame to die (perhaps internally it could be dying on a script?) such as a missing necessary config value for oidc-client.
For oidc-client to work with silent renew, you need to have your aurelia-app on an element that is not the body, so you can place elements within the body yet outside of your aurelia-app.
This allows you to put the IFrame outside of the aurelia-app, which prevents the Aurelia bootstrapper from eating it and lets oidc-client function independently of Aurelia.
EDIT
Based on your comment, and a little memory refreshing on my part, I rephrase/clarify:
The session checker and the silent renew functions work independently of each other. You can silent renew before the session checker has started with a manual call. You can also start the session checker without doing any silent renew. They are just convenient to use together, but that's their only relationship.
I'm assuming you use the hybrid flow and have the standard session checker implementation with an RP and OP iframe, where the OP iframe is in a check_session.html page and the RP iframe is somewhere in your aurelia app. In one of my projects I have the RP iframe in the index.html, outside of the aurelia-app element so it works independently of aurelia. But I guess it doesn't necessarily have to be there.
The session checker starts when you set the src property of the RP iframe to the location of your check_session.html with the session_state, check_session_iframe and client_id after the hash.
The check_session.html page will respond to that by starting the periodic polling and post a message back to the window of your aurelia app if the state has changed.
From your aurelia app, you listen to that message and do the signinSilent() call if it indicates a changed state. And from the silent_renew.html page, you respond to that with signinSilentCallback()
All that being in place, it really doesn't matter when you start the session checker. Tuck it away in a feature somewhere and load that feature last.
The only two things you need to worry about during the startup of your application is:
Check for window.hash starting with #code and call signinRedirectCallback(code) if it does
If it does not, just call signinSilent() right away (that leaves you with the least amount of things to check)
And then after either of those have been done, do getUser() and check if it's null or if the expired property === true. If either of those is the case, do the signinRedirect(). If not, your user is authenticated and you can let the aurelia app do it's thing and start the session checker etc.
I would definitely not put the initial authentication checks on your index.html within the aurelia-app. Because if aurelia happens to finish loading before the oidc checks are done, the process will fail. You also probably want to store the user object (and UserManager) in some cache/service/other type of singleton class so you can easily interact with oidc from your aurelia application.

Categories

Resources