-
Notifications
You must be signed in to change notification settings - Fork 312
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 #993
Comments
The more I think about it, the more I feel that activating the service worker when refreshing the last tab should be the automatic default, delaying the navigation automatically if necessary. If somebody wants the existing "refreshing doesn't update" behavior, they should have to opt into it. |
My idea around the self.addEventListener('navigation', event => {
event.waitUntil(p);
}); …which would pause the navigation (delaying the fetch) until p resolved. This would allow developers to do something like: self.addEventListener('navigation', event => {
if (registration.waiting) {
event.waitUntil(
clients.matchAll().then(clients => {
if (clients.length < 2) {
return registration.waiting.skipWaiting();
}
})
);
}
}); …the idea that the navigation would be delayed until the waiting worker activates, so the navigation request would go through the new worker. However, it's not clear to my how this would play with the concurrent request proposal. |
Then what happens if the response doesn't replace the current client? |
My imagination is poor, but I don't see how this can happen. The waiting service worker activates by default when you close the last tab, even if you immediately click a bookmark to re-open the site. In my view, that's exactly what refreshing is: it's a navigation away, followed immediately by a navigation back to the current page. |
This isn't really accurate. The browser starts loading the next page, but doesn't tear down the current page until that new request completes. So when the navigation FetchEvent fires the page is still being shown to the user.
I don't think you will have a waiting worker at this point. Both chrome and firefox wait to perform the update check until after the navigation load event. I guess you could manually call Given that |
@wanderview I think I haven't been clear enough in my explanation, so I've provided a clearer statement of the bug in the initial description, now with a code sample, steps to reproduce, expected behavior, and actual behavior. Specifically, the desired behavior is what AppCache does, so I know it must be possible.
I know; I've read all about it. But it works perfectly in AppCache. That's the behavior I want.
I think this is a misunderstanding about what I want. As I explained above, in AppCache, when the manifest updates, the first refresh gets stale content, as expected/desired. The AppCache re-installs itself in the background, so the second post-install refresh "just works." I agree that there's no good way to make the first refresh update the service worker, but subsequent refreshes should just work. The refreshed page's navigation should simply be paused while the initial page unloads and the new service worker activates. I could imagine a simple API like: Except, not really, because that should just be the default. If anything, if anybody wants to opt into this buggy behavior, they should be calling an API like, And, to be clear, the proposal to build an in-app update UI is really not a good one. It's actually pretty tricky for a page to know with confidence how many in-app tabs are still open, especially since there's no way to add a lock/semaphore on this. If that turns out to be the only long-term way to refresh a page atomically, then this is a big embarrassment for service workers. Back in 2012, @jakearchibald wrote that infamous article, http://alistapart.com/article/application-cache-is-a-douchebag As a "game" app, I guess I won the AppCache lottery or something, because AppCache was pretty straightforward. I had to set This bug is douchier than anything AppCache ever did to me. |
Appcache allows two versions of the app to run concurrently, assigning caches to documents. So one tab can be running off v2 while another is running on v1. Is this the model you want?
This is why we introduced the client IDs https://jakearchibald.com/2016/service-worker-meeting-notes/#fetch-event-clients, so you can track clients and associate caches with them (via indexeddb or something).
The clients API can do this pretty easily.
This thread is getting pretty long, but I ask that you don't throw around words like "embarrassment", especially as you don't seem so sure about how appcache worked. What's your favoured solution here? Assigning caches to a particular client, or providing a hook into the refresh button so you can call |
I finally found time to do more research on this. You're quite right that I didn't know that AppCache assigns caches to documents, and so allows each tab to run its own cache. re: assigning caches to particular clients (as AppCache does), I can see how the current API would allow for that, but, as you may have guessed, I really don't want to do that; I was wrong to think AppCache did it correctly. I want all tabs to share the same cache, until the last tab is refreshed, at which point I want the new cache to immediately set in before the refresh loads. Since I posted this issue last year, there's been other discussion around a "navigation" event, to which @wanderview replied:
I, a fool, didn't yet know that The idea is: on In my case, I know that all my SWs ever do is to check the cache and fallback to the network, so I can just open the newest cache (instead of the oldest, which is the default behavior for I've demonstrated this in https://github.com/dfabulich/service-worker-refresh-bug/tree/master/possible-workaround Here's the key section: self.addEventListener('fetch', function(event) {
console.log("[ServiceWorker] fetch", CACHE_NAME, event.request.url.replace(/^.*\//, ""));
event.respondWith(clients.matchAll().then(function(clients) {
if (clients.length < 2 && event.request.mode === "navigate" && registration.waiting) {
console.log("[ServiceWorker]", CACHE_NAME, 'sending skipWaiting');
registration.waiting.postMessage("skipWaiting");
var lastKey;
return caches.keys().then(function(keyList) {
lastKey = keyList[keyList.length-1];
return caches.open(lastKey);
}).then(function(cache) {
return cache.match(event.request);
}).then(function(cached) {
var response = cached || fetch(event.request);
console.log("[ServiceWorker] response", CACHE_NAME, "from", lastKey, event.request.url.replace(/^.*\//, ""), response);
return response;
})
} else {
return caches.match(event.request).then(function(cached) {
var response = cached || fetch(event.request);
console.log("[ServiceWorker] response", CACHE_NAME, event.request.url.replace(/^.*\//, ""), response);
return response;
});
}
}));
}); If there's just one client, the request is a navigation, and there's a SW waiting, tell it to skipWaiting, and return index.html from the newest cache (the last cache key, instead of the first); otherwise, do the normal thing. I tested this in Chrome 62 and it seemed to do what I want: with just one open tab, refreshing the tab twice (once to pick up the new SW and then again to activate it) updated the page, but with two open tabs, refreshing any number of times continued to use the old cache. Unfortunately, the code didn't work at all in Firefox, because Can y'all confirm that my code is actually correct? Is there a cleverer or more standard way to handle this case? Do I just need to file a bug on Firefox? |
Thrashing around for a way to make this work on Firefox, this trick seems to work, but it seems too clever to be the right thing:
|
This thread is too much full of my own confusions. I'm closing this issue and filing #1238 to replace it. |
When a service worker updates, it doesn't take control of the page right way; it goes into a "waiting" state, waiting to be activated.
Surprisingly, the updated service worker doesn't even take control of the tab after refreshing the page. Google explains:
https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle
This behavior makes sense in the case of multiple tabs. My app is a bundle that needs to be updated atomically; we can't mix and match part of the old bundle and part of the new bundle. (In a native app, atomicity is automatic and guaranteed.)
But in the case of a single tab, which I'll call the "last open tab" of the app, this behavior is not what I want. I want refreshing the last open tab to update my service worker.
(It's hard to imagine that anybody actually wants the old service worker to continue running when the last open tab is refreshed. Google's "navigation overlap" argument sounds to me like a good excuse for an unfortunate bug.)
I'm developing an "app" (a game). My app is normally only used in a single tab. In production, I want my users to be able to use the latest code just by refreshing the page, but that won't work: the old service worker will remain in control across refreshes.
I don't want to have to tell users, "to receive updates, be sure to close or navigate away from my app." I want to tell them, "just refresh."
When I raised this on StackOverflow, @jakearchibald advised me to add an in-app update notification; clicking on it would post a message to the service worker, which would
skipWaiting
, and thenwindow.location.reload()
once the new service worker became active.This seems like a hack, so we discussed it on Twitter. https://twitter.com/dfabu/status/789242276358651904
EDIT: I now think the current behavior is just a bug (with a good excuse! but it's still a bug), so I'm going to clarify steps to reproduce, expected, actual.
Consider this repo. https://github.com/dfabulich/service-worker-refresh-bug
There are three directories in there:
appcache
,broken-refresh
, andnonatomic-skip-waiting
. Each directory contains anindex.html
file containing hard-coded content and a delay-loaded request for a script tag; the delay is to demonstrate the race condition.The
appcache
directory is the simplest, and most clearly demonstrates the behavior I expect and desire. (AppCache is actually really nice! Please stop calling AppCache mean names 😉 )Here's the
appcache/index.html
file:And here's the
appcache/script.js
file. It writes the JS value into the page, and says "MATCH" or "MISMATCH" depending on whether it matches the hardcoded value in the HTML.Here's the AppCache manifest.
To see the expected behavior, open
appcache/index.html
on a web server. The page will say this, as expected:Now, without closing the tab, change the number
1
to2
inindex.html
,script.js
, andmanifest.appcache
; then refresh the page.On the initial refresh, the page will still say "1", as expected:
This is expected behavior because we're loading from the cache by default. The console log will explain:
Now refresh the page, and you'll see the page has updated:
Since I'm developing an "app" (a game), this is the exact behavior I want to emulate in serviceworker. But this is impossible.
First, try
broken-refresh/index.html
. Instead of declaring an AppCache manifest, it registers a service worker.The
script.js
file is identical to theappcache
variation, so I won't repeat it. Thesw.js
service worker says:An easy peasy boilerplate service worker.
To reproduce
Open
broken-refresh/index.html
on a web server. The page will show the expected initial results:Now, without closing the tab, update the HTML, JS, and SW to replace "1" with "2" and refresh.
As we'd expect, the page initially shows "1" because the page is loaded from the cache, and the new service worker is installed and waiting.
Now, refresh the page again.
Expected
Refreshing the (only) tab again, now that the service worker is installed and waiting, should load activate the new service worker before loading the next page, just like it does in AppCache.
Actual
The new navigation starts before the old service worker can be unloaded, so the old values remain, no matter how many times you refresh the page.
The obvious thing to try next is to
skipWaiting
oninstall
, which we can demonstrate innonatomic-skip-waiting
directory.The
nonatomic-skip-waiting
directory is identical to thebroken-refresh
directory, except that we addself.skipWaiting()
to the first line of theinstall
event handler.If you open
nonatomic-skip-waiting/index.html
, of course the page displays the right values at first:But if you update the HTML, JS, and SW to say "2", the page displays a mismatch value.
The point of this diatribe is that it's impossible to reproduce the desired AppCache behavior.
The text was updated successfully, but these errors were encountered: