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

Fetch Event

Once your Service Worker is registered and active it runs its own event queue and you can write event handlers that will be called as necessary.

The key Service Worker event is fetch which is generated whenever a page requests a resource which is within the Service Worker’s scope or by a request from a page within that scope.

For example, if we add to the Service Worker:

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

and place a button on the page that registers the Service Worker with the event handler:

button1.addEventListener("click", 
             async function (event) { 
             var response = await fetch('myFile.txt');
             console.log(await response.text());
           });

Now if the page is loaded and the button clicked you will see the request url in the console followed by the text stored in the file. If the fetch event handler simply returns then the browser’s default fetch is used. In other words, any resources in the scope of the Service Worker that it doesn’t want to modify are handled in the usual way.

If it does want to interfere in the resource then the respondWith method of the FetchEvent object can be used to return a custom response. Usually the custom responses are automatically generated either by being accessed via a Cache object or being downloaded from a different request. It is possible, however, to construct responses from scratch and it is worth seeing how to do it. First we need a suitable response object:

var init = {"status": 200, "statusText": "Generated"};
var myResponse = new Response("myText", init);

The Response constructor takes the body as the first parameter and in this case it is simple Unicode text.

Once we have the Response object it can be sent back to the requesting thread using respondWith which accepts a Promise as its only parameter:

event.respondWith(
         new Promise(
                     function (resolve, reject) {
                        resolve(myResponse);
                    }));

If you want to avoid the explicit use of a Promise you can use the idiom introduced in Chapter 8:

event.respondWith(async function(){
return myResponse;}());

The async modifier converts the function into an AsyncFunction which wraps its return value in a Promise.

In fact the simplest thing to do is pass a Response object which will be treated as if it was a Promise by respondWith:

event.respondWith(myResponse);                   

If you try this out within the fetch event handler you will find that the second time the page is loaded it is replaced by “myText” as this is now the response for all URLs in the Service Worker’s scope.

To make things a little more easy to follow we need to restrict the URLs that the custom response is returned for:

addEventListener("fetch", function (event) {
  console.log(event.request.url);
  if (!event.request.url.includes("myFile.txt"))
        return;
  var init = {"status": 200, "statusText": "Generated"};
  var myResponse = new Response("myText", init);
  event.respondWith(myResponse);                  
});

Notice that now the URL is tested and the custom Response is only returned if it contains “myFile.txt”. Now if you click the button you will see the “myText” in the console rather than the contents of the file.

There is a subtle point that you need to keep in mind when using respondWith. If the fetch event handler doesn’t call respondWith, the browser will fetch the resource in the usual way. However, if the event handler awaits for some function to complete i.e. is asynchronous before calling respondWith, the thread is freed and the browser decides that the event handler isn’t going to call respondWith and so fetches the resource.

What this means is that if you are going to handle the fetch you need to call respondWith before you release the thread with an await.

The simplest way to deal with this problem is to always supply an async function to respondWith that deals with the fetch. That is:

addEventListener("fetch", function (event) {
       event.respondWith(doFetch(event));
});

where doFetch is an async function that contains whatever code is needed to complete the fetch.

coverasync

Setting Up The Cache – waitUntil

Now you can begin to see how powerful the idea of the Service Worker is. In many ways it is the “real” entity of the app that you are creating as it persists even when the user isn’t browsing your website. In most cases what you want the Service Worker to do is to cache resources and retrieve them when pages within the scope request them. As you already know how the Cache object works, this is fairly easy apart from the problem of initializing the cache.

This is where the status events come into the story.

There are two status events:

  • Install - fires when the Service Worker is ready to be initialized.

  • Activate – fires when the Service Worker takes over from any earlier versions and is ready to do work. This is where you can clean up anything from previous versions.

Notice that the state events are triggered when the Service Worker’s state changes:

Installing → Install event → Waiting → Activate event → Active.

It is also important to be clear that these events only fire once when the Service Working is being registered or when it is being updated. That is, they only occur once for each version of the Service Worker.

So to set up a Cache object all we need to do is create an Install handler:

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");
    await myCache.add(request);
});

This handler creates and loads myCache with myFile.txt when the Service Worker is installed.

There is a small problem with this event handler that might well go undetected for some time. Consider for a moment what would happen if the Cache loading involved a great many and perhaps large files – not just one small file as in this test case. There is no problem with keeping the thread too busy, however, as we are using asynchronous functions and awaiting them. While the files are being downloaded the thread is free.



Last Updated ( Monday, 07 September 2020 )