Friday, 1 February 2019

Handling Version Updates in a Blazor PWA

Last time we saw how to enable some basic offline first / progressive web app behaviours for our Blazor application. Now, let's enable update notifications so the users know when we publish a new version.

Service Worker Updates


It's really simple to notify clients of an update, but there are a couple of quirks that you should be aware of first.

The browser will identify that you have an update by comparing a hash of your Service Worker file each time it is fetched from the server. When a change is identified in the file, the update process begins.

If the user has visited your site before, they will have an existing Service Worker running in the browser, which cannot be overwritten while it is in use, so your new file gets staged, waiting for activation. 

At this point, the new Service Worker can (and should) populate a new cache of files required to load and run your application.

Activation of the new Service Worker will happen when the page is reloaded by the user.

How will the user know there is an update? 
You tell them. 
However you want to notify them, it should be a user choice to reload when they are ready.

When the page reloads, the old Service Worker is retired and the new one is activated (well, ok - it may be - more on this later).

It's at this point that the user will see the new release of your amazing Blazor application.

Our New Service Worker


The only change we want to make to our Service Worker from the previous post is a command that tells the browser to go ahead and update when the user reloads the page.

Earlier, I mentioned that the page reload will retire the old worker and activate the new one - that's not the whole story, but if we modify our install handler we can make that happen.

In our blazorServiceWorker,js file, we need to modify the event listener for install : 

self.addEventListener('install', event => {
  self.skipWaiting();
  event.waitUntil(
    caches.open(staticCacheName).then(function (cache) {
      return cache.addAll(filesToCache);
    })
  );
});

The new line self.skipWaiting(); tells the browser that we want it to activate our new Service Worker as soon as the old one is retired. 

Without this line, the user has to close all browser tabs that are connected to your site before the update can happen - not a nice flow!

Registering For Update Notifications


We need to know when there is an update, so we need to add some code to the Service Worker registration that we can hook into.

This code comes in various flavours (yes, you are reading a British blog), I got this flavour from "zwacky's" post on medium.

It took me a while to understand - and I highly recommend taking that time and making that effort - but understanding the code is vital for when you have to debug why your application won't udpate.

So, in blazorSWRegister.js we replace the original code with this:

window.updateAvailable = new Promise(function (resolve, reject) {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/blazorServiceWorker.js')
      .then(function (registration) {
        console.log('Registration successful, scope is:', registration.scope);
        registration.onupdatefound = () => {
          const installingWorker = registration.installing;
          installingWorker.onstatechange = () => {
            switch (installingWorker.state) {
              case 'installed':
                if (navigator.serviceWorker.controller) {
                  resolve(true);
                } else {
                  resolve(false);
                }
                break;
              default:
            }
          };
        };
      })
      .catch(error =>
        console.log('Service worker registration failed, error:', error));
  }
});

The change we are making is to wrap the Service Worker registration in an async method (Promise) which is added to the window object, so that we can create a hook from JavaScript to Blazor.

After registering the Service Worker, it attaches an event handler for updatefound, which allows us to detect when the Service Worker has been installed.

When we call it, we pass a DotNetReference that will be the entry point to our Blazor application and allow us to notify the end user.

JavaScript Interop


This is new for this project - we need to be able to subscribe to the Service Worker registration Promise created above from our Blazor application, and we do this through JSInterop.

We'll put this in a new file called blazorFuncs.js

window.blazorFuncs = {
  registerClient: function (caller) {
    window['updateAvailable']
      .then(isAvailable => {
        if (isAvailable) {
          caller.invokeMethodAsync("onupdateavailable").then(r => console.log(r));
        }
      });
  }
};

Here, we are creating a method we can call from Blazor, with a parameter that will be a Blazor component with a JSInvokable callback function - identified as onupdateavailable.

The registerClient function subscribes to the Promise we just created - and when that Promise resolves, we invoke the JSInvokable instance method onupdateavailable of our Blazor component caller.

Blazor Needs To Know


So, we've got a situation coded up where the browser will detect a new Service Worker, and we have a mechanism for receiving notifications in Blazor, but we haven't actually hooked that up.

I have decided to hook this up in my MainLayout because I want to display something to the user whichever page they are on.

In the C# code for the MainLayout view, we need to add an OnInitAsync override, where we call registerClient, and pass it a DotNetReference to the MainLayout.

protected override async Task OnInitAsync()
{
  await JSRuntime
    .Current
    .InvokeAsync<object>(
      "blazorFuncs.registerClient", 
      new DotNetObjectRef(this)
    );
}

We also need to create the JSInvokable method that will receive notifications when the Service Worker is being updated.

[JSInvokable("onupdateavailable")]
public async Task<string> AppUpdate()
{
  Console.WriteLine("New version available");
  updateReady = true;
  StateHasChanged();
  return await Task.FromResult("Alerted client");
}

For simplicity, I am simply going to display a message in the top bar to inform the user.

This is controlled by a boolean defined on the MainLayout 

bool updateReady;

And some markup in the Razor section of the view

<div class="main">
  <div class="top-row px-4">
    <p class="text-muted mb-0">Version 0.1</p>
    @if (updateReady)
    {
      <p class="text-info mb-0">
      There is an update available. Reload when ready.
      </p>
    }
    <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
  </div>
 
  <div class="content px-4">
    @Body
  </div>
</div>

Recap


In this post, I have shown how we can add update notifications to the Offline First PWA we built previously.

To make the client app update, we need to remember that the browser will only detect an update when the Service Worker file changes. This change detection is based on a hash of the file contents, so any change is enough to trigger the update flow.

It is recommended that you always modify the name of your cache - in this example, that is defined in the constant staticCacheName. This will cause a flush and re-load of the cache and ensure the browser has picked up all your code changes before the user receives the update.

Happy coding!

No comments:

Post a Comment