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

[Proposal] ServiceWorkerContainer.installing(scope) promise #1364

Open
FluorescentHallucinogen opened this issue Oct 28, 2018 · 3 comments
Open

Comments

@FluorescentHallucinogen

Motivation

I'm developing a web component to observe the whole service workers lifecycle. Something like:

<sw-lifecycle-events
    scope="/"
    registration-state="{{registrationState}}"
    state="{{state}}"
    on-service-worker-installing="handleInstallingEvent"
    on-service-worker-updated="handleUpdatedEvent"
    on-service-worker-installed="handleInstalledEvent"
    on-service-worker-activating="handleActivatingEvent"
    on-service-worker-activated="handleActivatedEvent"
    on-service-worker-redundant="handleRedundantEvent">
</sw-lifecycle-events>

Valid values of registration-state property (attribute) are installing, waiting and active.

Valid values of state property (attribute) are installing, installed, activating, activated and redundant.

When the user inserts this custom HTML element into the page, it finds the service worker, updates its properties (attributes) and creates custom events. The user can listen to these events and e.g. show popups when the service worker has been installed for the first time ("Content is now available offline") or the new service worker has been installed, replacing the current service worker ("New or updated content is available"), etc.

The first idea that came to my mind was to use ready promise:

navigator.serviceWorker.ready.then(registration => {

The good thing about ready promise is it will never reject, and waits indefinitely until the ServiceWorkerRegistration associated with the current page has an active worker.

But this is a bad idea.

Problems

  • ready promise resolves to the registration when the state is activating or activated. This means it's not possible to get installing and installed states. So it's not possible to show popups when the service worker has been installed for the first time or the new service worker has been installed, replacing the current service worker.

  • ready doesn't have the scope parameter. What if the web app has multiple service workers and I need to get the service worker with a certain scope?

The next idea was to use getRegistration() method.

The good thing about getRegistration() promise is it has the scope parameter.

But it also has problems.

Problems

  • getRegistration() dosn't wait indefinitely (like ready). It can resolves to undefined (if getRegistration() is called before register()). That means I need to find the right moment to call getRegistration(). If I call it too soon, I will get undefined. If I call it too late, I may miss installing / installed / activating states.

The good practice is to use the window load event to register service worker (to keep the page load performant):

window.addEventListener('load', () => {
  navigator.serviceWorker.register('service-worker.js');

But it doesn't mean that everyone is doing it.

The first thing I did in my web component was to place getRegistration() inside connectedCallback lifecycle callback:

connectedCallback() {
  super.connectedCallback();
  navigator.serviceWorker.getRegistration(this.scope).then(registration => {

Here are the problems I encountered:

Problems

  • If the registered service worker doesn't use the window load event, there are 2 options:

    • connectedCallback (and getRegistration()) is called before register(), I will get undefined.
    • register() is called before connectedCallback (and getRegistration()), I may miss some of installing / installed / activating states.

The next idea was to use the window load event to call getRegistration() inside connectedCallback lifecycle callback (it should fix the case when getRegistration() is called before the window load event (and before register()):

connectedCallback() {
  super.connectedCallback();
  window.addEventListener('load', () => {
    navigator.serviceWorker.getRegistration(this.scope).then(registration => {

But this is also a bad idea:

Problems

  • Anyway, there is no guarantee that register() will be called before the getRegistration(). Both use the window load event.
  • If the user adds web component to the page dynamically, then connectedCallback will be called after window.onload. But the code inside connectedCallback waits the window load event. This means that the code inside connectedCallback will not execute.

There is also a case when the web app has a service worker (for precaching static content) [with scope /] and the second service worker (for receiving push notifications) [with the different scope] downloads from CDN and registers dynamically later (after the user gives permission to receive push notifications). This is how e.g. Firebase Notifications works (@gauntface knows).

The user of my web component may want to observe the whole lifecycle of this lazy-loaded service workers (starting from installing state). But it seems that at the moment it's impossible. If I'm wrong, please correct me.

Proposal

  • A new (similar to ready) promise navigator.serviceWorker.installing(scope) that will never reject, waits indefinitely and resolves to the registration when the state is installing. Also it should have the optional scope parameter.

  • In the future, it would be great to have a methods, based on observables instead of promises. The biggest difference between promises and observables is that an observable can receive multiple values over time while a promise only represents a single value (when an async operation completes or fails). I.e. a promise represents a single future event, an observable represents a stream of future events. It should solve the problem with multiple dynamically (at any time) registered service workers. E.g. an observable-based method that returns a (changing over time) array of registrations / service workers.

@FluorescentHallucinogen
Copy link
Author

@FluorescentHallucinogen
Copy link
Author

@jeffposnick @jakearchibald @dfabulich PTAL.

Please help fix (work around) the problems mentioned above in my web component.

@dfabulich
Copy link

I think I'd need to see more clarity in the "Motivation" section. At a minimum, I have these questions:

  1. Why do you want to observe the whole service worker lifecycle? In Provide a one-line way to listen for a waiting Service Worker #1222, separately filed as Provide an easier way to listen for waiting/activated/redundant Service Workers #1247, @gauntface suggested an updatestatechange event listener API for doing so, but I was skeptical, because I didn't really see the point. I still don't. It sounds like error tracking and/or performance tracking was all anybody came up with, but all of that stuff is best done inside the SW script itself, not in client-side window code.

  2. Why do you want to build this as a web component? As you've noticed, you've created a problem for yourself by having to wait for connectedCallback. But why do you want to use a WC here at all? Instead of trying to getRegistration() and hoping that you call it after someone else called register(), it seems like you should just attach this code to the register() promise handler.

But let's suppose you say "Very well, I now see that I didn't really want this to be a WC at all; I really just wanted to prolyfill updatestatechange. I'll use the registration provided to me by register(). Now please tell me how."

Per #1247 I think you can do what you want with code like this:

function listenForStateChanges(reg, callback) {
  if (reg.installing) reg.installing.addEventListener('statechange', callback);
  if (reg.waiting) reg.waiting.addEventListener('statechange', callback);
  reg.addEventListener('updatefound', function() {
    reg.installing.addEventListener('statechange', callback);
  });
}
navigator.serviceWorker.register('/sw.js').then(function(reg) {
  listenForStateChanges(reg);
});

But, again, if you're just using code like this for performance logging and/or error tracking, I recommend not doing this at all. Just put your tracking code in the SW script.

"But what if the SW script fails?" Feel free to track registration failures when you call register() client-side. But that's all you need to track client side. Track everything else in the SW.

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

2 participants