Search code examples
javascriptworkbox

How to ensure Workbox cache update for updates that never activate


I've been working on converting a web app we have to a PWA and I am using Google's Workbox (v3.6.1) for precaching resources. For the most part it has been working well, but there appears to be a specific scenario that is causing the cached files to fall out of sync.

I am using the basic precacheAndRoute() functionality to setup the files for precaching.

workbox.precaching.precacheAndRoute([]); //populated at build time via workbox-cli

On first install, and most updates, the files are getting precached as expected. However, if a Service Worker instance is currently waiting and a new update gets installed, all of the pending files in the temp cache are getting deleted and don't get installed with the newest version.

It seems that the install step of workbox.precaching updated the IndexDB that contains all of the file versions when it adds the files to the temp cache. So the next Service Worker version believes that the prior latest version of all files are currently cached, even though they are still only in the temp cache. Then the new install removes everything from the temp cache before inserting it's own files. Thus the pending cached files for the previous waiting instance are lost forever.

I had the idea that on install of a new version, I could force the temp cache to sync to the permanent cache (by using a PrecacheController and the activate() function) before allowing the new instance to precache, but I have some concerns about updating the permanent cache while the user is actively using the app.

I'm looking for either confirmation that my idea here is an appropriate solution, or any other suggestions on how to handle this scenario.


Solution

  • Workbox recently released version 4.0.0 and this issue appears to have been fixed with that upgrade. I'll leave the below answer up as it may still be useful for anyone who cannot upgrade to 4.0.0 at this time.


    I got this more or less working using the PrecacheController, like I mentioned in the question. One annoying piece to this is that I had to implement the fetch and activate listeners myself since I am no longer using the standard workbox.precaching. If anyone has any other ideas, feel free to post other options.

    const precacheController = new workbox.precaching.PrecacheController();
    precacheController.addToCacheList([]); //populated at build-time with workbox-cli
    
    self.addEventListener('fetch', (event) => {
        var url = event.request.url.endsWith('/') ? event.request.url + 'index.html' : event.request.url;
        event.respondWith(new Promise(function (resolve) {
                if (precacheController.getCachedUrls().indexOf(url) > -1) {
                    resolve(caches.open(workbox.core.cacheNames.precache)
                        .then((cache) => {
                            return cache.match(url);
                        })
                        .then((cachedResponse) => {
                            return cachedResponse || fetch(url);
                        }));
                } else {
                    resolve(fetch(event.request));
                }
            }));
    });
    
    self.addEventListener('activate', (event) => {
        event.waitUntil(precacheController.activate());
        self.clients.claim();
    });
    
    self.addEventListener('install', function (event) {
        var timeoutId = null;
        event.waitUntil((new Promise(function (resolve, reject) {
                    if (self.registration.waiting) {
                        var channel = new MessageChannel();
                        channel.port1.onmessage = function (event) {
                            resolve();
                        };
    
                        //tell the current 'waiting' instance to cleanup its temp cache
                        self.registration.waiting.postMessage({
                            action: 'cleanupCache'
                        }, [channel.port2]);
                    } else {
                        resolve();
                    }
                }))
            .finally(function () {
                //once temp cache is cleaned up from any 'waiting' instance, begin my install
                return precacheController.install();
            }));
    });
    
    self.addEventListener('message', function (event) {
        if (event.data.action === 'cleanupCache') {
        //move files from temp cache to permanent cache
            precacheController.activate().finally(function () {
                if (event.ports[0]) {
                    event.ports[0].postMessage('cleanupComplete');
                }
            });
        }
    });