0

I have a PWA built with Aurelia and compiled with Webpack, using the Workbox Plugin that generates the sw.js service worker file. I'm trying to make the "New version available" user notification so that the user can activate the new version when clicking on a link within the app.

I am successfully downloading and installing the new version in the background, and even detecting that a new version is ready. However, when I try to call the skipWaiting() method to force refresh of the page with the new version, it fails, because apparently I don't have the right scope or object.

The main problem is probably that I can't edit the actual sw.js because it is automatically generated. The examples all suggest the use of self.skipWaiting();, but I don't know how to access that object.

webpack.config.js

new WorkboxPlugin({
  globDirectory: './dist',
  globPatterns: ['**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
  swDest: './dist/sw.js',
  clientsClaim: true,
  skipWaiting: false, // because I want to notify the user and wait for response
}),

index.ejs

<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => {
        // make the registration available globally, for access within app
        window.myServiceWorkerReg = reg;
        // Check for update on loading the app (is this necessary?)
        return reg.update();
      })
      .catch(console.error);
  }
</script>

app.js

activate() {
  // listener for service worker update
  this.swReg = window.myServiceWorkerReg;
  console.warn('[app.js] ACTIVATE.', this.swReg);

  this.swReg.addEventListener('updatefound', () => {
    // updated service worker found in reg.installing!
    console.warn('[app.js] UPDATE FOUND.', this.swReg);

    const newWorker = this.swReg.installing;
    newWorker.addEventListener('statechange', () => {
      // has the service worker state changed?
      console.warn('[app.js]  STATE HAS CHANGED.', newWorker, newWorker.state);

      if (newWorker.state === 'installed') {
        // New service worker ready.

        // Notify user; callback for user request to load new app
        myUserMessage({ clickToActivate: () => {
          // reload fresh copy (do not cache)
          console.warn('[app.js] Post Action: skipWaiting.');
          // this.swReg.postMessage({ action: 'skipWaiting' });

          // THIS IS THE LINE THAT FAILS
          this.swReg.skipWaiting();
        }});
      }
    });
  });
}

Everything works fine except the last line (this.swReg.skipWaiting();). Has anyone else used webpack+workbox plugin and gotten the skipWaiting to happen as a result of user interaction?

LStarky
  • 2,740
  • 1
  • 17
  • 47

2 Answers2

3

I finally got it to work. One problem was that I was using an older version of workbox-webpack-plugin. The current version (4.2) includes a listener in the service worker that can trigger self.skipWaiting() when a message is posted to the worker like this:

newWorker.postMessage({ type: 'SKIP_WAITING' });

But you have to ensure that the config has skipWaiting: false; and that you are using the latest version.

These instructions are pretty good:

https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin

https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users

However, I tweaked things to work well between my App and the service worker instantiation in the index.ejs file.

webpack.config.js

new GenerateSW({
  globPatterns: ['dist/**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
  swDest: 'sw.js',
  clientsClaim: true,
  skipWaiting: false,
})),

index.ejs

<script>
  if ('serviceWorker' in navigator) {
    // register the service worker
    navigator.serviceWorker.register('/sw.js')
      .then(reg => {
        window.myWorkerReg = reg;
        // Check for update on loading the app (is this necessary?)
        return reg.update();
      })
      .catch(console.error);
    // The event listener that is fired when the service worker updates
    navigator.serviceWorker.addEventListener('controllerchange', function () {
      // when the service worker controller is changed, reload the page
      if (window.swRefreshing) return;
      window.location.reload();
      window.swRefreshing = true;
    });

  }
</script>

app.js

activate() {
  // listener for service worker update
  this.swReg = window.myWorkerReg;
  if (this.swReg) {
    // if there is already a new service worker ready to install, prompt user
    if (this.swReg.waiting) {
      this.promptUpdateServiceWorker(this.swReg.waiting);
    }
    // add listener to detect when a new service worker is downloaded
    this.swReg.addEventListener('updatefound', () => {
      // updated service worker is being installed
      const newWorker = this.swReg.installing;
      // add listener to detect when installation is finished
      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed') {
          // New service worker ready to activate; prompt user
          this.promptUpdateServiceWorker(newWorker);
        }
      });
    });
  }
}

// listener for buildVersion
buildVersionChanged(buildVersion) {
  // through proprietary code, we've detected a new version could be downloaded now
  window.myWorkerReg.update();
}

// New service worker ready.  Show the notification
promptUpdateServiceWorker(newWorker) {
  // actual code for UI prompt will vary; this is pseudocode
  uiPrompt('New_version_ready').then((response) => {
    if (response.approved) {
      // reload fresh copy (do not cache)
      newWorker.postMessage({ type: 'SKIP_WAITING' });
    }
  });
}
LStarky
  • 2,740
  • 1
  • 17
  • 47
1

You cannot call it on the page (app.js). You call self.skipWaiting on the Service Worker script (service-worker.js).

https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/skipWaiting

pate
  • 4,937
  • 1
  • 19
  • 25
  • Very true. That was one of the misunderstandings I had. Fortunately, this particular package (workbox-webpack-plugin) creates a listener to allow you to trigger the skipWaiting by posting a message to the new worker that is waiting to be activated. I added a complete description of my solution as the accepted answer to the question. – LStarky Apr 20 '19 at 13:58