javascriptservice-workersw-toolbox

Refresh page after load on cache-first Service Worker


I'm currently considering adding service workers to a Web app I'm building.

This app is, essentially, a collection manager. You can CRUD items of various types and they are usually tightly linked together (e.g. A hasMany B hasMany C).

sw-toolbox offers a toolbox.fastest handler which goes to the cache and then to the network (in 99% of the cases, cache will be faster), updating the cache in the background. What I'm wondering is how you can be notified that there's a new version of the page available. My intent is to show the cached version and, then, if the network fetch got a newer version, to suggest to the user to refresh the page in order to see the latest edits. I saw something in a YouTube video a while ago but the presenter gives no clue of how to deal with this.

Is that possible? Is there some event handler or promise that I could bind to the request so that I know when the newer version is retrieved? I would then post a message to the page to show a notification.

If not, I know I can use toolbox.networkFirst along with a reasonable timeout to make the pages available even on Lie-Fi, but it's not as good.


Solution

  • I just stumbled accross the Mozilla Service Worker Cookbook, which includes more or less what I wanted: https://serviceworke.rs/strategy-cache-update-and-refresh.html

    Here are the relevant parts (not my code: copied here for convenience).

    Fetch methods for the worker

    // On fetch, use cache but update the entry with the latest contents from the server.
    self.addEventListener('fetch', function(evt) {
      console.log('The service worker is serving the asset.');
      // You can use respondWith() to answer ASAP…
      evt.respondWith(fromCache(evt.request));
      // ...and waitUntil() to prevent the worker to be killed until the cache is updated.
      evt.waitUntil(
        update(evt.request)
        // Finally, send a message to the client to inform it about the resource is up to date.
        .then(refresh)
      );
    });
    
    // Open the cache where the assets were stored and search for the requested resource. Notice that in case of no matching, the promise still resolves but it does with undefined as value.
    function fromCache(request) {
      return caches.open(CACHE).then(function (cache) {
        return cache.match(request);
      });
    }
    
    // Update consists in opening the cache, performing a network request and storing the new response data.
    function update(request) {
      return caches.open(CACHE).then(function (cache) {
        return fetch(request).then(function (response) {
          return cache.put(request, response.clone()).then(function () {
            return response;
          });
        });
      });
    }
    
    // Sends a message to the clients.
    function refresh(response) {
      return self.clients.matchAll().then(function (clients) {
        clients.forEach(function (client) {
        // Encode which resource has been updated. By including the ETag the client can check if the content has changed.
          var message = {
            type: 'refresh',
            url: response.url,
            // Notice not all servers return the ETag header. If this is not provided you should use other cache headers or rely on your own means to check if the content has changed.
            eTag: response.headers.get('ETag')
          };
    
          // Tell the client about the update.
          client.postMessage(JSON.stringify(message));
        });
      });
    }
    

    Handling of the "resource was updated" message

    navigator.serviceWorker.onmessage = function (evt) {
      var message = JSON.parse(evt.data);
    
      var isRefresh = message.type === 'refresh';
      var isAsset = message.url.includes('asset');
      var lastETag = localStorage.currentETag;
    
      // ETag header usually contains the hash of the resource so it is a very effective way of check for fresh content.
      var isNew =  lastETag !== message.eTag;
    
      if (isRefresh && isAsset && isNew) {
        // Escape the first time (when there is no ETag yet)
        if (lastETag) {
          // Inform the user about the update.
          notice.hidden = false;
        }
        //For teaching purposes, although this information is in the offline cache and it could be retrieved from the service worker, keeping track of the header in the localStorage keeps the implementation simple.
        localStorage.currentETag = message.eTag;
      }
    };