If you have worked with Service Workers, you may have run into some issues with previous Service Workers still being in control of a document, even though the file itself has been updated. The reason for this is to do with some nuances in the lifecycle of the Service Worker; it may be installed and valid, but not yet actually in control of the document.

A Service Worker can be in one of the following six states - parsed, installing, installed, activating, activated, and redundant.

Service Worker States

Parsed

When we first attempt to register a Service Worker, the user agent parses the script and obtains the entry point. If parsing is successful (and some other requirements, e.g. HTTPS, are met), we will have access to the Service Worker registration object. This contains information about the state of the Service Worker as well as it’s scope.

/* In main.js */
if ('serviceWorker' in navigator) {  
    navigator.serviceWorker.register('./sw.js')
    .then(function(registration) {
        console.log("Service Worker Registered", registration);
    })
    .catch(function(err) {
        console.log("Service Worker Failed to Register", err);
    })
}

Successful registration of the Service Worker doesn’t mean that it has been installed yet or is active, just that the script has been successfully parsed, it is on the same origin as the document, and the origin is HTTPS. Once this is complete, the Service Worker moves on to the next state.

Installing

Once the Service Worker script has been parsed, the user agent attempts to install it and it moves to the installing state. In the Service Worker registration object, we can check for this state in the installing child object.

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.installing) {
        // Service Worker is Installing
    }
})

During the installing state, the install event in the Service Worker script is carried out. In a typical install event, we cache the static files for the document.

/* In sw.js */
self.addEventListener('install', function(event) {  
  event.waitUntil(
    caches.open(currentCacheName).then(function(cache) {
      return cache.addAll(arrayOfFilesToCache);
    })
  );
});

If there is an event.waitUntil() method in the event, the installing event will not be successful until the Promise within it is resolved. If the Promise is rejected, the install event fails and the Service Worker becomes redundant.

/* In sw.js */
self.addEventListener('install', function(event) {  
  event.waitUntil(
   return Promise.reject(); // Failure
  );
});
Install Event will fail

Installed / Waiting

If the installation is successful, the Service Worker moves to the installed (also called waiting) state. In this state it is a valid, but not yet active, worker. It is not yet in control of the document, but rather is waiting to take control from the current worker.

In the Service Worker registration object, we can check for this state in the waiting child object.

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.waiting) {
        // Service Worker is Waiting
    }
})

This can be a good time to notify the app user that they can update to a new version, or automatically update for them.

Activating

The activating state will be triggered for a waiting Service Worker in one of the following scenarios -

  • If there is no current active worker already
  • If the self.skipWaiting() method is called in the Service Worker script
  • If the user has navigated away from the page, thereby releasing the previous active worker
  • If a specified period of time has passed, thereby releasing the previous active worker

During the activating state, the activate event in the Service Worker script is carried out. In a typical activate event, we clear files from old caches.

/* In sw.js */
self.addEventListener('activate', function(event) {  
  event.waitUntil(
    // Get all the cache names
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        // Get all the items that are stored under a different cache name than the current one
        cacheNames.filter(function(cacheName) {
          return cacheName != currentCacheName;
        }).map(function(cacheName) {
          // Delete the items
          return caches.delete(cacheName);
        })
      ); // end Promise.all()
    }) // end caches.keys()
  ); // end event.waitUntil()
});

Similarly to the install event, if there is an event.waitUntil() method in the activate event, activation will not be successful until the Promise is resolved. If the Promise is rejected, the activate event fails and the Service Worker becomes redundant.

Activated

If activation is successful, the Service Worker moves to the active state. In this state, it is an active worker in full control of the document. In the Service Worker registration object, we can check for this state in the active child object.

/* In main.js */
navigator.serviceWorker.register('./sw.js').then(function(registration) {  
    if (registration.active) {
        // Service Worker is Active
    }
})

When a Service Worker is active, it can now handle the functional events - fetch, and message.

/* In sw.js */

self.addEventListener('fetch', function(event) {  
  // Do stuff with fetch events
});

self.addEventListener('message', function(event) {  
  // Do stuff with postMessages received from document
});

Redundant

A Service Worker can become redundant for one of the following reasons -

  • If the installing event failed
  • If the activating event failed
  • If a new Service Worker replaces it as the active service worker

If the Service Worker is redundant for the first two reasons, we can see it (and related information) in our Dev Tools -

Service Worker Redundant in DevTools

If there was a previously active Service Worker, it maintains control of the document.