3

What am I trying to do?

I am creating a progressive web application (PWA) and in order to send upgrade the app correctly, I am working on a step where the user is notified and once the user says "upgrade", the serviceWorker calls skipWaiting()

What have I done so far?
I am following up a nice article https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 to achieve this

In order to remove the complexity, I am only testing sending of messages between serviceWorkers to see how skipWaiting works. I am using create-react-app (v"react": "^16.5.2",) which comes with workbox bundled as plugin.

My current registerServiceWorker.js looks like

// In production, we register a service worker to serve assets from local cache.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.

// To learn more about the benefits of this model, read 
// This link also includes instructions on opting out of this behavior.

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    // [::1] is the IPv6 localhost address.
    window.location.hostname === '[::1]' ||
    // 127.0.0.1/8 is considered localhost for IPv4.
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
    )
);

export default function register() {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (!isLocalhost) {
        // Is not local host. Just register service worker
        registerValidSW(swUrl);
      } else {
        // This is running on localhost. Lets check if a service worker still exists or not.
        checkValidServiceWorker(swUrl);
      }
    });

    window.addEventListener('message', messageEvent => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (messageEvent.data === 'skipWaiting') {
        console.log('skipWaiting');
        return navigator.serviceWorker.getRegistration(swUrl)
          .then(registration => registration.skipWaiting());
      }

      console.log(`message=${messageEvent.data}`);
    });

    let refreshingPage;
    navigator.serviceWorker.addEventListener('controllerchange', () => {
      console.log('refreshing page now');
      if (refreshingPage) return;
      refreshingPage = true;
      window.location.reload();
    });
  }
}

function registerValidSW(swUrl) {
  navigator.serviceWorker
    .register(swUrl)
    .then(registration => {
      registration.onupdatefound = () => {
        const installingWorker = registration.installing;
        installingWorker.onstatechange = () => {
          if (installingWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              // At this point, the old content will have been purged and
              // the fresh content will have been added to the cache.
              // It's the perfect time to display a "New content is
              // available; please refresh." message in your web app.
              console.log('>> New content is available; please refresh.');
              navigator.serviceWorker.controller.postMessage('skipWaiting');
            } else {
              // At this point, everything has been precached.
              // It's the perfect time to display a
              // "Content is cached for offline use." message.
              console.log('Content is cached for offline use.');
            }
          }
        };
      };
    })
    .catch(error => {
      console.error('Error during service worker registration:', error);
    });
}

function checkValidServiceWorker(swUrl) {
  // Check if the service worker can be found. If it can't reload the page.
  fetch(swUrl)
    .then(response => {
      // Ensure service worker exists, and that we really are getting a JS file.
      if (
        response.status === 404 ||
        response.headers.get('content-type').indexOf('javascript') === -1
      ) {
        // No service worker found. Probably a different app. Reload the page.
        navigator.serviceWorker.ready.then(registration => {
          registration.unregister().then(() => {
            window.location.reload();
          });
        });
      } else {
        // Service worker found. Proceed as normal.
        registerValidSW(swUrl);
      }
    })
    .catch(() => {
      console.log(
        'No internet connection found. App is running in offline mode.'
      );
    });
}

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready.then(registration => {
      registration.unregister();
    });
  }
}

As you see, when a new serviceWorker is installed, I am sending a postMessage as

console.log('>> New content is available; please refresh.');
navigator.serviceWorker.controller.postMessage('skipWaiting');

and I am expecting the message to be handled with the function

    window.addEventListener('message', messageEvent => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (messageEvent.data === 'skipWaiting') {
        console.log('skipWaiting');
        return navigator.serviceWorker.getRegistration(swUrl)
          .then(registration => registration.skipWaiting());
      }

      console.log(`message=${messageEvent.data}`);
    });

Then, I deploy my changes so that this serviceWorker is ready. Then, I make changes to my application (index.html) and now when I deploy, I see multiple messages being logged, but none with skipWaiting

message=
registerServiceWorker.js:53 message=!_{"h":"I0_1548194465894"}
registerServiceWorker.js:53 message=!_{"s":"/I0_1548194465894::_g_rpcReady","f":"I0_1548194465894","r":"I0_1548194465894","t":"32296067","c":1,"a":[null],"g":false}
registerServiceWorker.js:53 message=!_{"s":"__cb","f":"I0_1548194465894","r":"I0_1548194465894","t":"32296067","c":null,"a":[1,[null]],"g":false}
registerServiceWorker.js:53 message=!_{"s":"__cb","f":"I0_1548194465894","r":"I0_1548194465894","t":"32296067","c":null,"a":[2,[null]],"g":false}
registerServiceWorker.js:53 message=!_{"s":"/I0_1548194465894::_g_restyleMe","f":"I0_1548194465894","r":"I0_1548194465894","t":"32296067","c":2,"a":[{"setHideOnLeave":false}],"g":false}
registerServiceWorker.js:53 message=!_{"s":"__cb","f":"I0_1548194465894","r":"I0_1548194465894","t":"32296067","c":null,"a":[4,[null]],"g":false}
registerServiceWorker.js:53 message=!_{"s":"/I0_1548194465894::authEvent","f":"I0_1548194465894","r":"I0_1548194465894","t":"32296067","c":3,"a":[{"type":"authEvent","authEvent":{"type":"unknown","eventId":null,"urlResponse":null,"sessionId":null,"postBody":null,"tenantId":null,"error":{"code":"auth/no-auth-event","message":"An internal error has occurred."}}}],"g":false}
registerServiceWorker.js:53 message=!_{"s":"__cb","f":"I0_1548194465894","r":"I0_1548194465894","t":"32296067","c":null,"a":[3,[true]],"g":false}

What am I doing wrong here?

daydreamer
  • 87,243
  • 191
  • 450
  • 722
  • 1
    This code looks very confused. You've exhibited only one script here, `registerServiceWorker.js` that runs in the page, but service workers require at least two scripts, one running in the page, and one running in the service worker. I recommend learning this material by taking Google's Udacity course. https://www.udacity.com/course/offline-web-applications--ud899 – Dan Fabulich Jan 22 '19 at 22:25

1 Answers1

0

There a few things wrong

  1. navigator.serviceWorker.controller.postMessage('skipWaiting');

This is sending a message from window to worker, but you're waiting for the message on the window

  • controller -> is not the worker instance. postMessage() would send the message to that worker.
  • You don't want to post to that worker though - it's your current active worker, while you want to notify the newly installed worker that haven't taken over yet
  1. Even if you manage to run the code in the message event handler it won't do
      if (messageEvent.data === 'skipWaiting') {
        console.log('skipWaiting');
        return navigator.serviceWorker.getRegistration(swUrl)
          .then(registration => registration.skipWaiting());
      }

There's no registration.skipWaiting() method. We call skipWaiting inside the service-worker.js code

In your case you would add an event listener for message inside the worker code and intercept the "Skip Waiting" message there

worker code

self.addEventListener('message', (event) => {
  if (event.data == 'skipWaiting') self.skipWaiting();
});

window code (changes only)

Send a message to the now installed worker to skip waiting and take over

console.log('>> New content is available; please refresh.');
installingWorker.postMessage('skipWaiting');
kidroca
  • 3,480
  • 2
  • 27
  • 44