How to conditionally cache bust an API in workbox - service-worker

Details about my goal.
I have workbox webpack plugin configured to cache an API for a duration of 30 seconds. I would like to force cache bust it conditionally when a different API request is triggered.
Example, below config caches feature-flags. I am trying to cache bust it when the page sends a request to "updateTests".
workbox configuration to cache feature-flag
Workbox configuration updated to clear feature-flags
Cache clear makes it work
Things I have tried
Deleting IndexedDB manually does a fresh fetch of feature-flags

Just to make sure I understand:
You have API calls that include the feature-flags in their URL, and you want all of those calls to be served cache-first out of the cache named api, with a 30 seconds max lifetime.
If at any point the browser makes a request for a URL that contains updateFlags, that should serve as kind of a "kill switch" that automatically clears out the contents of the api cache, ensuring that the next feature-flags request will always go against the network.
Assuming that's an accurate summary, you could add a new runtimeCaching route to your configuration that does the following:
runtimeCaching: [{
// existing route
}, {
urlPattern: new RegExp('updateFlags'),
handler: async ({request, event}) => {
// Deleting the 'api' cache will ensure that the next API
// request goes against the network.
event.waitUntil(caches.delete('api'));
// Assuming that you want the actual request for the URL
// containing updateFlags to be made against the server:
return fetch(request);
},
}]

Related

Does a service worker allow one to simply use long expiration headers on static assets?

Say I have a service worker that populates the cache with the following working code when its activated:
async function install() {
console.debug("SW: Installing ...");
const cache = await caches.open(CACHE_VERSION);
await cache.addAll(CACHE_ASSETS);
console.log("SW: Installed");
}
async function handleInstall(event) {
event.waitUntil(install());
}
self.addEventListener("install", handleInstall);
When performs cache.addAll(), will the browser use its own internal cache, or will it always download the content from the site? This is important, because it one creates a new service worker release, and there are new static assets, the old version maybe be cached by the service worker.
If not then I guess one still has to do named hashed/versioned static assets. Something I was hoping service workers would make none applicable.
cache.addAll()'s behavior is described in the service worker specification, but here's a more concise summary:
For each item in the parameter array, if it's a string and not a Request, construct a new Request using that string as input.
Perform fetch() on each request and get a response.
As long as the response has an ok status, call cache.put() to add the response to the cache, using the request as the key.
To answer your question, the most relevant step is 1., as that determines what kind of Request is passed to fetch(). If you just pass in a string, then there are a lot of defaults that will be used when implicitly constructing the Request. If you want more control over what's fetch()ed, then you should explicitly create a Request yourself and pass that to cache.addAll() instead of passing in strings.
For instance, this is how you'd explicitly set the cache mode on all the requests to 'reload', which always skip the browser's normal HTTP cache and go against the network for a response:
// Define your list of URLs somewhere...
const URLS = ['/one.css', '/two.js', '/three.js', '...'];
// Later...
const requests = URLS.map((url) => new Request(url, {cache: 'reload'}));
await cache.addAll(requests);

Workbox redirect the clients page when resource is not cached and offline

Usually whenever I read a blog post about PWA's, the tutorial seems to just precache every single asset. But this seems to go against the app shell pattern a bit, which as I understand is: Cache the bare necessities (only the app shell), and runtime cache as you go. (Please correct me if I understood this incorrectly)
Imagine I have this single page application, it's a simple index.html with a web component: <my-app>. That <my-app> component sets up some routes which looks a little bit like this, I'm using Vaadin router and web components, but I imagine the problem would be the same using React with React Router or something similar.
router.setRoutes([
{
path: '/',
component: 'app-main', // statically loaded
},
{
path: '/posts',
component: 'app-posts',
action: () => { import('./app-posts.js');} // dynamically loaded
},
/* many, many, many more routes */
{
path: '/offline', // redirect here when a resource is not cached and failed to get from network
component: 'app-offline', // also statically loaded
}
]);
My app may have many many routes, and may get very large. I don't want to precache all those resources straight away, but only cache the stuff I absolutely need, so in this case: my index.html, my-app.js, app-main.js, and app-offline.js. I want to cache app-posts.js at runtime, when it's requested.
Setting up runtime caching is simple enough, but my problem arises when my user visits one of the potentially many many routes that is not cached yet (because maybe the user hasn't visited that route before, so the js file may not have loaded/cached yet), and the user has no internet connection.
What I want to happen, in that case (when a route is not cached yet and there is no network), is for the user to be redirected to the /offline route, which is handled by my client side router. I could easily do something like: import('./app-posts.js').catch(() => /* redirect user to /offline */), but I'm wondering if there is a way to achieve this from workbox itself.
So in a nutshell:
When a js file hasn't been cached yet, and the user has no network, and so the request for the file fails: let workbox redirect the page to the /offline route.
Option 1 (not always useful):
As far as I can see and according to this answer, you cannot open a new window or change the URL of the browser from within the service worker. However you can open a new window only if the clients.openWindow() function is called from within the notificationclick event.
Option 2 (hardest):
You could use the WindowClient.navigate method within the activate event of the service worker however is a bit trickier as you still need to check if the file requested exists in the cache or not.
Option 3 (easiest & hackiest):
Otherwise, you could respond with a new Request object to the offline page:
const cacheOnly = new workbox.strategies.CacheOnly();
const networkFirst = new workbox.strategies.NetworkFirst();
workbox.routing.registerRoute(
/\/posts.|\/articles/,
async args => {
const offlineRequest = new Request('/offline.html');
try {
const response = await networkFirst.handle(args);
return response || await cacheOnly.handle({request: offlineRequest});
} catch (error) {
return await cacheOnly.handle({request: offlineRequest})
}
}
);
and then rewrite the URL of the browser in your offline.html file:
<head>
<script>
window.history.replaceState({}, 'You are offline', '/offline');
</script>
</head>
The above logic in Option 3 will respond to the requested URL by using the network first. If the network is not available will fallback to the cache and even if the request is not found in the cache, will fetch the offline.html file instead. Once the offline.html file is parsed, the browser URL will be replaced to /offline.

What files should I cache in service-worker install event?

I followed instructions from the Google PWA tutorial here to make my own app with offline functionality. When I ran Lighthouse check on my localhost:3000, I got a report that said everything is setup fine.
Note that I had only cached my index file and svg image assets only.
self.addEventListener('install', event => {
event.waitUntil(
caches
.open('word-cloud-v1')
.then(cache => {
return cache.addAll([
'/',
'/index.html',
'./images/paper-plane.svg',
'./images/idea.svg',
'./images/desk-lamp.svg',
'./images/stopwatch.svg',
'./images/pie-chart.svg',
])
})
)
})
But when I go offline and try to run my app, I get errors that some files have not loaded.
So I go back and add some more files to cache. Note that this file is not created by me.
.then(cache => {
return cache.addAll([
'/',
'/index.html',
'./static/js/bundle.js',
'./images/paper-plane.svg',
'./images/idea.svg',
'./images/desk-lamp.svg',
'./images/stopwatch.svg',
'./images/pie-chart.svg',
])
})
Although the offline feature works fine now, I'm also seeing a bunch of other randomly generated files that are created in the build folder that I have not explicitly cached yet. So what are the files I should cache inside a service worker so they show up in offline mode?
TLDR; what files should we cache apart from /, /index.html and images so we can have offline functionality?
You can cache anything that you would like to cache. Cache data can be of two types.
1) Static data - You can cache all your html/css/js/images which are not dynamic. When you plan to cache static data, it would be easy to wild card the files like below instead of choosing individual files. Below block (angular ng service worker. Your helper class might look different) will cache everything inside "assets" folder.
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**"
]
}
}
2) Dynamic data - You can cache dynamic data like API JSON response in IndexDB.

pwa - Service worker does not successfully serve the manifest's start_url

I am trying to add PWA functionality to an existing website that is hosted on Azure and uses Cloudflare CDN.
I have run the lighthouse testing tool on the site and it passes everything in the PWA section (e.g. service worker installed, served over https, manifest installed etc.) except:
"Service worker does not successfully serve the manifest's start_url."
My manifest.json has '/' as the start URL and "/" as the scope.
The '/' is actually default.aspx which I have cached as well.
My service worker caches '/', e.g.
var cacheShellFiles = [
'/',
'/manifest.json',
'/index.html',
'/scripts/app.js',
'/styles/inline.css'
...
]
// install - cache the app shell
self.addEventListener('install', function (event) {
console.log('From SW install: ', event);
// calling skipWatiing() means the sw will skip the waiting state and immediately
// activate even if other tabs open that use the previous sw
self.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME_SHELL)
.then(function (cache) {
console.log('Cache opened');
return cache.addAll(cacheShellFiles);
})
);
});
When I view the Cache Storage files in dev tools however, the Content-Length of the / and the .css and .js files is 0:
Image of Chrome Developer tools showing cache storage with Content-Length=0
Is the Content-Length = 0 the reason that it is saying it can't serve the manifest's start URL ?
This is an issue with your service worker's scope (different from the scope option in manifest.json).
Your start_url is set to /, but most likely your service worker file is served from a deeper path, e.g. /some-path/service-worker.js. In this case, your service worker's scope is /some-path/, therefore it will not be able to handle requests to paths outside of it, such as the root path /.
To fix this, you need to make sure that your service worker's scope covers your start_url. I can think of two ways to do this:
In your case, serve the service worker file directly from the root path, e.g. /service-worker.js.
Use the Service-Worker-Allowed response header, which overrides the service worker's scope, so that it wouldn't matter from which path the service worker file is served from.
Choose the one that is more appropriate to your setup.

Workbox is caching only time stamps to indexDb, how to intercept with json data in indexDb?

Below route defines to store json data as MyCachedData in cache storage, and IndexDb only stores the url and timestamp.
workboxSW.router.registerRoute('/MyApi(.*)',
workboxSW.strategies.staleWhileRevalidate({
cacheName: 'MyCachedData',
cacheExpiration: {
maxEntries: 50
},
cacheableResponse: {statuses: [0, 200]}
})
);
Is it possible to store the json data in the index db only and how can you define it to intercept (add, update, delete) using Workbox?
No, Workbox relies on the Cache Storage API to store the bodies of responses. (As you've observed, it uses IndexedDB for some out-of-band housekeeping info, like timestamps, used for cache expiration.)
If an approach that uses the Cache Storage API isn't appropriate for your use case (it would be good to hear why not?), then I'd recommend just updating IndexedDB directly, perhaps via a wrapper library like idb-keyval.
You can write a custom function that performs a fetch and stores the information in indexedDB, but this would be seperate from anything Workbox does outside of making sure you only get the API requests.
This is not tested, but something like:
workboxSW.router.registerRoute(
'/MyApi(.*)',
(event) => {
// TODO:
// 1. Check if entry if in indexedDB
// 1a. If it is then return new Response('<JSON Data from IndexedDB>');
// 1b. If not call fetch(event.request)
// Then parse fetch response, save to indexeddb
// Then return the response.
}
);

Resources