Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a way to skipWaiting when the last tab refreshes #1238

Open
dfabulich opened this issue Nov 30, 2017 · 4 comments
Open

Provide a way to skipWaiting when the last tab refreshes #1238

dfabulich opened this issue Nov 30, 2017 · 4 comments

Comments

@dfabulich
Copy link

dfabulich commented Nov 30, 2017

When you have a single ("last/only") tab/client controlled by a Service Worker v1, and a new v2 Service Worker installs, the v2 SW goes into a "waiting" state. The v2 SW activates when the last client closes.

Surprisingly, the v2 Service Worker doesn't activate when the last client refreshes/navigates to a new page, because the navigation request initiates while the last/only tab is still open, so the v1 Service Worker handles it; I think many developers don't realize that navigation requests start that early.

If we wanted to mitigate the surprise, the "obvious" thing to do would be to always delay the navigation request when there's a v2 Service Worker waiting, activate the v2 SW, and let v2 control the navigation request instead.

But there's a problem. There are some HTTP responses that can cause the browser to cancel the navigation. (I know of two such cases: 204 No Content responses have null bodies and prevent navigation, and Content-Disposition: attachment responses force the browser to just initiate a download without navigating.) That would leave a tab that was loaded in v1 under the control of the v2 Service Worker, which may be unsafe.

What I'd like is a way of expressing to the Service Worker lifecycle that I know it's safe for the v2 Service Worker to activate when the last/only client navigates.

Jake suggested a navigation event listener, like this:

self.addEventListener('navigation', event => {
  if (registration.waiting) {
    event.waitUntil(
      clients.matchAll().then(clients => {
        if (clients.length < 2) {
          return registration.waiting.skipWaiting();
        }
      })
    );
  }
});

The navigation would be delayed until the waiting worker activates, so the navigation request would go through the new worker.

That'd be really nice.

@dfabulich
Copy link
Author

dfabulich commented Nov 30, 2017

In the meantime, I recommend developers use code like this during the fetch event in navigate mode, returning a blank Response with a Refresh: 0 header.

addEventListener('fetch', event => {
  event.respondWith((async () => {
    if (event.request.mode === "navigate" &&
      event.request.method === "GET" &&
      registration.waiting &&
      (await clients.matchAll()).length < 2
    ) {
      registration.waiting.postMessage('skipWaiting');
      return new Response("", {headers: {"Refresh": "0"}});
    }
    return await caches.match(event.request) ||
      fetch(event.request);
  })());
});

When doing an ordinary navigation while a v2 Service Worker is waiting, the last/only v1 tab will briefly show a blank page, then auto-refresh into a v2 tab. If the page response happens to use Content-Disposition or 204 No Content, the tab will turn blank and stay blank.

@garygreen
Copy link

@dfabulich old thread/comment so things must of changed since you made that suggestion - but that code you posted completely freezes my browser and goes into endless loop when the service worker code is updated and you refresh the page.

@dfabulich
Copy link
Author

dfabulich commented Aug 6, 2019

I don't think the code is buggy. Try this sample. https://github.com/dfabulich/service-worker-refresh-sample use the refresh-last-tab branch.

I think you've fallen into a common service worker trap. If you reload on controllerchange events and you've opened Chrome Dev Tools and checked the "Update on reload" box on the Application tab, it starts an infinite refresh loop.

Don't do this in the page:

navigator.serviceWorker.addEventListener(`controllerchange`, function (event) {
  window.location.reload();
}

do this instead

var refreshing;
navigator.serviceWorker.addEventListener(`controllerchange`, function (event) {
  if (refreshing) return;
  refreshing = true;
  window.location.reload();
}

@signorpipo
Copy link

In 2024 this still seems to be an issue, I tried your solution, which works, but has a bug: if you open a new tab with the same link, while a tab is already opened, the service worker catches the navigate event and still think it has only 1 client, since the second one is loading, and therefore skip waiting, even though the first client has not reloaded.

This leads to a situation when the first client was loaded using the old sw but now is controlled by the new one, making this solution risky =/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants