JavaScript Async - Service Workers
Written by Ian Elliot   
Monday, 07 September 2020
Article Index
JavaScript Async - Service Workers
Fetch Event
Awaiting Cache
Managing Service Workers

The problem is that when we await a Cache add in the event handler we free the thread and it continues where it left off. In this case it is installing the Service Worker and when we await in the event handler it continues to install the Service Worker. The problem is that it might well finish installing the Service Worker before the Cache has been completely loaded.

To avoid this sort of problem we have the new extendableEvent and its waitUntil method. This tells the event dispatcher that when it gets the thread back it is not to complete the task in hand. Instead it is to wait until the Promise provided resolves. If the Promise resolves then the Service Worker is installed. If it rejects then the entire installation fails. This stops incomplete Service Workers from being used.

To protect our Cache loading we need to include all of the asynchronous function calls within waitUntil. For example:

addEventListener("install",
  function (event) {
       console.log("install");
       var url = new URL("http://localhost/myFile.txt");
       var request = new Request(url, {
              method: "GET"
            });
       event.waitUntil(
              async function () {
              var myCache = await caches.open("myCache");
              var response = await caches.match(request);
              if (response === undefined) 
                       await myCache.add(request);
              }());
        });

Notice that we pass waitUntil an async function that performs all of the Cache loading and execute it immediately. Given that waitUntil needs to be passed a Promise that resolves when the initialization is finished, you might be worried that we don’t actually return a Promise or even a value. We don’t need to because the Promise constructed by the async modifier resolves to undefined when the function completes.

The need to “hold” an event while releasing the thread is something that occurs in other parts of Service Worker and you can expect to see extendableEvents occurring in other places.

Finally, there is another subtle point to keep in mind about the lifetime of the cache. The cache persists beyond the life of a Service Worker version. What this means is that in this case the install event occurs when a new version of the Service Worker is being prepared and the event handler retrieves myCache and loads a new version of myFile.txt into it.

New Service Worker versions should either work with the existing Cache or they should create new versions of the Cache and delete the old one once they are active. It isn’t a good idea to delete or modify the Cache in the install event handler because any active previous version will still be using it. It is better to wait for the new Service Worker to become active and then delete or modify the original Cache.

For example, if you want the Service Worker to create and load the Cache only when it is registered for the first time you would use something like:

addEventListener("install",
    async function (event) {
       console.log("install");
       var url = new URL("http://localhost/myFile.txt");
       var request = new Request(url, {
               method: "GET"
            });
       var myCache = await caches.open("myCache");
       var response = await caches.match(request);
       if (response === undefined) {
             await myCache.add(request);
       }
   });

Notice that all we do is check to see if the entry is in the cache and only load it if it is.

Fetch

With the Cache object set up and loaded we can now define a fetch event handler:

async function doResponse(event) {
    var response = await caches.match(event.request); 
    if (response === undefined)
               return fetch(event.request);
    return response;
};

This function can be called within the fetch event handler:

addEventListener("fetch",
        function (event) {
         console.log(event.request.url);
         event.respondWith(doResponse(event));
         });

The reason for using a function in this way is because using an await within the event handler would free the thread and if this occurs before respondWith the browser will assume that you don’t want to handle the fetch. Any asynchronous work has to be done in the respondWith call. You can, of course, use an IIFE in place of a named function.

The fetch event has a waitUntil method, that is it is an extendableEvent, however, you usually don’t need to use this explicitly as respondWith automatically uses it to extend the event until the response is available.

Controlling The Service Worker

When you start to do more complicated things with Service Workers what you will find is that keeping track of which version is actually running is difficult. The fact that your new version will not be installed until all of the pages that are using the old version have been closed is one problem. A more subtle problem is that the new version will not be activated until the page that causes it to be installed has finished loading. This means you usually need a refresh of that page as well as navigating to another page first.

You can modify the way a new version of a Service Worker takes control. The ServiceWorkerGlobalScope method skipWaiting forces the Service Worker to take control i.e. to become active, without waiting for previous versions to stop. It returns a Promise that immediately resolves to undefined. This is usually put into the install event handler as the final instruction. For example:

addEventListener("install",
        function (event) {
            console.log("install");
             event.waitUntil(
                async function () {
           	 code that loads the cache etc
		 return skipWaiting();
                }
            }());
        });

 

It doesn’t stop the previous version from servicing pages that are already open, but any new pages will use the new version. It also causes the Service Worker to move to the active state and it fires the active event. If you want to take over the open pages you can use the Clients.claim method. This takes over all of the open pages at once. You have to be aware that the pages that you take over will have been created by the previous Service Worker. You can use Clients.claim anywhere that makes sense, but it is usual to put in the active event handler:

addEventListener('activate',
	function(event) {
   		return self.clients.claim();
});

It also triggers a controllerchange event which gives you a chance to clean up.

The clients object is worth looking up as it has methods that allow you to interact with the current clients directly.



Last Updated ( Monday, 07 September 2020 )