Skip to content

CodeCoupler Application Basics Service Worker Loader πŸ”’

What is this?

Registering a service worker is easy: just use navigator.serviceWorker.register(serviceWorkerUrl). But here's where things get tricky.

Was the service worker loaded right? Are there new versions of the service worker? How do I update the service worker?

A service worker keeps running even if it's just using one tab. Also, you can't update a service worker at any time. It's important to keep an eye on the status of the new service worker.

Is it loading right now? Is it installing right now? Is it already activated? Is it waiting to be activated?

You can solve all of these problems quickly and easily using the ServiceWrokerLoader class.

Setup

Install Bundle:

npm i @codecoupler-pro/ui-bundle

Define Aliases, Externals and Assets:

{
  module: "@codecoupler-pro/app-service-worker",
  global: ["codecoupler", "app", "serviceWorker"]
},
{
  module: "@codecoupler-pro/ui-bundle",
  global: ["codecoupler", "ui", "bundle"],
  entries: [
    "dist/codecoupler-pro-ui-bundle.js"
  ]
}

Basic Usage

To load a service worker from the location /sw.js:

import { ServiceWorkerLoader } from "@codecoupler-pro/app-service-worker";
let swLoader = new ServiceWorkerLoader();

To load a service worker from specific location:

1
2
3
4
import { ServiceWorkerLoader } from "@codecoupler-pro/app-service-worker";
let swLoader = new ServiceWorkerLoader({
  serviceWorkerUrl: "/any-other-path.js"
});

Handling Registration, Error and Updates via promises:

1
2
3
4
import { ServiceWorkerLoader } from "@codecoupler-pro/app-service-worker";
let swLoader = new ServiceWorkerLoader();
swLoader.promise.then(callbackRegistered).catch(callbackRegistrationFailed);
swLoader.updateFound.then(callbackUpdateFound);

Handling Registration, Error and Updates via constructor arguments:

1
2
3
4
5
6
import { ServiceWorkerLoader } from "@codecoupler-pro/app-service-worker";
new ServiceWorkerLoader({
  callbackRegistered,
  callbackRegistrationFailed,
  callbackUpdateFound
});

Handle Service Worker Registration

Registration errors won't lead to any exceptions. You'll have to handle the case yourself.

You can define callbacks during the initialization process. These will be called after the shared worker registration is successful or after it fails.

1
2
3
4
swLoader = new ServiceWorkerLoader({
  callbackRegistered,
  callbackRegistrationFailed
});

Or use a promise result after initialization to get informed about the registration process. If the registration was successful, the promise will be resolved. If the registration fails, the promise will be rejected:

swLoader = new ServiceWorkerLoader();
swLoader.promise.then(callbackRegistered).catch(callbackRegistrationFailed);

The success callback and the success promise receives the instance as first argument. Via the instance you have access to the ServiceWorkerRegistration object.

function callbackRegistered(swLoader) {
  // We know now that the service worker is loaded!

  // We can search for updates manually:
  swLoader.registration.update().then((e) => {
    if (e.waiting || e.installing) {
      // Update Found
    } else {
      // No Update Found
    }
  });

  // Or initialize a message channel to receive messages from the service worker:
  navigator.serviceWorker.addEventListener("message", (event) => {
    if (event.data.ccVersion) alert(event.data.ccVersion);
  });

  // Or send messages to the service worker:
  swLoader.registration.active.postMessage({
    codecoupler: "cc-get-version"
  });
}

The error callback and the failed promise will receive an error object:

1
2
3
4
5
6
export function callbackRegistrationFailed(reason) {
  alert(
    "Error! Reason: " +
    JSON.stringify(reason, Object.getOwnPropertyNames(reason));
  );
}

Handling Service Worker Updates

Set up a callback to let you know when there's a new service worker update available. You're responsible for activating the new versionβ€”all you have to do is post a message to your service worker.

Since updating the service worker reloads all tabs that use it, it's a good idea to let the user know first when an update is ready. The user can decide when to install the update and reload pages.

You can set a callback during the initialization process to find out when the update is ready.

swLoader = new ServiceWorkerLoader({ callbackUpdateFound });

Or you can use a promise result after initialization. The promise will never be rejected, only resolved:

swLoader = new ServiceWorkerLoader();
swLoader.updateFound.then(callbackUpdateFound);

The callback and the promise receives the instance as first argument.

Whenever you (or the user) think it's the right time to update the service worker (and reload all tabs using them) you have to post a message like this one:

swLoader.registration.waiting.postMessage({ codecoupler: "cc-skip-waiting" });

Then, when it gets the message, the service worker has to call the function self.skipWaiting() when it receives the message. Here's an example of something you can include in your service worker to handle the message { codecoupler: "cc-skip-waiting" }:

1
2
3
4
5
6
7
8
self.addEventListener("message", (messageEvent) => {
  switch (messageEvent.data?.codecoupler) {
    case "cc-skip-waiting": {
      self.skipWaiting();
      break;
    }
  }
});

This will then trigger a reload in all open tabs and windows that use the old service worker version.

It's a good idea to install two fallbacks:

The first thing to do is to check if the service worker is still waiting, so you don't end up with multiple reloads.

The second one is to make sure the reload happens. Usually, the command above should cause a reload in all tabs using the service worker. But if there's a service worker that doesn't understand this message, this reload won't happen. So, you should try to unregister all service workers after a timeout using the helper method forceUpdate.

Also, it's sometimes a good idea to add a note to the URL. That way, when you reload, you'll know that an update has happened.

Here's an example of what a stable update routine could look like:

function callbackUpdateFound(swLoader) {
  // Provide an "Update Now" Button
  updateNowButton.addEventListener("click", () => {
    // Check if the service worker is already waiting
    if (!swLoader.registration.waiting) return;
    // Add a hash to the address to recognize that an
    // update has taken place after reloading.
    window.history.replaceState({}, document.title, "#sw-update");
    // Start the Update
    swLoader.registration.waiting.postMessage({
      codecoupler: "cc-skip-waiting"
    });
    // Fallback Forced Update
    setTimeout(async () => {
      await swLoader.forceUpdate("#sw-update-forced");
    }, 2000);
  });
}

Reference

Constructor Options

The constructor expects an object with the following properties:

  • serviceWorkerUrl: Path to service worker source file (Default: "/sw.js").
  • callbackRegistered: Callback after successfull service worker registration.
  • callbackRegistrationFailed: Callback after failed service worker registration.
  • callbackUpdateFound: Callback after service worker update available.

Static Members

async forceUpdate(replaceState)

This'll unregister the service worker in all the tabs that use it, and then the page will reload. You can use the argument replaceState to change the current URL. If you use just a hash like #forced-update the same page will reload, but you can use that info for more processing.

Instance Members

promise

Promise which resolves to the instance as soon as the service worker is loaded. It'll never be rejected.

registration

ServiceWorkerRegistration object of the current registered service worker.