secret

Service Workers

  • Download Gist
controller_event_registration.md
Markdown

Service Workers: Navigation Controllers, Minus Navigations, Plus New Shiny

It's possible to imagine there being a "service layer" that installed web apps are wired into. The service events are driven by the host environment and, for offline, the events are defined per the NC explainer. If we generalize the concept and imagine a "ServiceWorker" type, we see that it needs a lot of what's already in the NC design:

  • Less API surface area than documents provide.
  • Non-blocking behavior
  • Extensibility to cover new events in the future
  • A runtime contract that allows the host environment to start and kill these handlers at will, reserving un-handled events for re-dispatch later
  • The ability to install and upgrade across versions

If we were to contemplate simply extending the set of events sent to NC's directly, we can quickly envision running into a few issues:

  • Since any NC might possibly handle any/all event types, it's not possible to skip booting up the controller to see if it'll register an event handler. That's pretty nasty for memory and CPU usage.
  • Spamming NC's with non-navigation events stretches the mental model for developers trying to provide offline app support and not much else.

But there is at least some overlap: we can imagine a quota/eviction-pressure event that Service Workers could choose to listen in on. In most cases, if there's a Navigation Controller in play, it's probably the right script to handle dealing with/prioritizing resources pre and post eviction.

This is an exploration of how we might integrate services with NC registration & runtime APIs to enable Service Workers.

Runtime

The simplest thing at runtime is for everything about a NC to be preserved and for the controller script to simply begin to handle more events. For instance, here's handling an alarm:

// http://example.com/controller.js

// this.onfetch = function(e){ ... };
// this.oninstall = function(e){ ... };
// this.onactivate = function(e){ ... };

this.onalarm = function(e) {
  if (e.name == "frobulate") {
    // Inform all of the live views (if any) of the event
    this.windows.forEach(function(w) {
      w.port.postMessage({ frobulate: true });
    });
    // And store it off in the database somewhere.
    // Hand-wavey API based on:
    //    https://github.com/slightlyoff/async-local-storage
    storage.set("frobulated", { "when": Date.now() });
  }
  // ...
};

This event might be send to the Worker/Controller when the Alarm permission is granted to an application, likely this way:

<!DOCTYPE html>
<!-- http://example.com/app.html -->
<html>
  <head>
    <script>
      // Attempt to register a Navigation Controller
      navigator.controller.register("/*", "controller.js");

      // Ask for the Alarm permission and, if granted, schedule an event to be
      // sent to the controller:
      navigator.requestAlarmPermission().then(function(alarms) {
        // Cribbed from the Chrome Alarms API:
        //    http://developer.chrome.com/extensions/alarms.html
        var a = new alarms.Alarm({
          period: 30 // every half hour
        });
        alarms.set("name", a); // Roughly the ES6 Map API
      });
    </script>
  </head>
</html>

Other services that can be reasonably described in this way include:

  • Push notification delivery
  • Background data synchronization (e.g., content updates, unread counts, etc.)
  • Quota and storage events, such as pre/post-eviction.
  • Intermittent location updates
  • Tab & Window creation/removal notification
  • Inter-application messaging

In short, most things which apps might want access to on a delayed basis and which it might fall to the system to provide/dispatch.

Registration

The above example envisions overloading the Navigation Controller for all of these tasks. That's one option, perhaps the simplest, but potentially also the worst for performance.

Several options exist for providing ways to enumerate the events each service script can handle.

<!DOCTYPE html>
<!-- http://example.com/app.html -->
<html>
  <head>
    <script>
      // NC++ style: the controller only receives Navigation Controller-defined
      // events by default, but can request additional events be dispatched to
      // it using an optional list specified at registration time:
      navigator.controller.register("/*",
                                    "controller.js",
                                    ["alarm", "quota", "location"]);
    </script>
  </head>
</html>

The obvious down-side with this version is that it's not straightforward to extend the list of events without mirroring the event-enumeration logic in oninstall for the controller. Without that doubling-up, we see that it wouldn't be possible to easily expand/contract the application's functionality when new versions of the controller are installed. Here's a variant that relies only on what controller.js claims it will handles:

<!DOCTYPE html>
<!-- http://example.com/app.html -->
<html>
  <head>
    <script>
      // Back to the simple registration
      navigator.controller.register("/*", "controller.js");
    </script>
  </head>
</html>
// http://example.com/controller.js

this.oninstall = function(e){
  e.listenFor("alarm", "beforeevicted", "evicted", "location");
  // ...
};

this.onalarm = function(e) {
  // ...
};

// this.onfetch = function(e){ ... };
// this.onactivate = function(e){ ... };

We can imagine generalizing the above further. What if we don't think of the Navigation Controller as it's own "thing", and instead simply treat navigations and fetches as events which Service Workers can opt into handling?

app.html and controller.js (renamed services.js) might look like:

<!DOCTYPE html>
<!-- http://example.com/app.html -->
<html>
  <head>
    <script>
      navigator.services.register("/*", "services.js");
    </script>
  </head>
</html>
// http://example.com/services.js

this.oninstall = function(e){
  e.listenFor("fetch", "alarm");
  // ...
};
// this.onactivate = function(e){ ... };

this.onalarm = function(e) {
  // ...
};

this.onfetch = function(e){
  // ...
};

// Never called!
this.onbeforeevicted = function(e) {
  // The installation didn't claim this service script was
  // interested in "onbeforeevicted" events, so they're never dispatched here,
  // even if the app comes under eviction pressure.
};

This model seems cleanest, modulo the "chaches" and "windows" objects being populated in the Navigation Controller execution context. Modulo those warts, treating these as shared workers with the Navigation Controller's upgrade-dance design and the ability to opt into specific events appears to meet all of the "nice to have" and "must have" design criteria.

Other designs that include registration of multiple scripts (one per event type!) were considered, but given that each script then needs to be upgraded -- perhaps in tandem with other scripts (what if one fails and the others don't?) -- those alternatives were rejected.

Thoughts? Alternatives?

A few arguments I don't understand:

1) if register() took a list of events, why would you need to re-list them in the SW's oninstall event? The risks I see is that the developer would listen for an event they'd never receive, or get events they never registered for, but that doesn't seem like a problem at all to me.

2) I don't see why multiple scripts (near the end of this) is bad - I mean why not let one script handle 'alarm' and 'location' and another script handle 'fetch'?

navigator.controller.register("/", "uievents.js", ["alarm", "location"])
navigator.controller.register("/
", "fetches.js", ["fetch"])

If a developer chooses to do this, it's their prerogative..

Thoughts? Alternatives?

For the navcontroller and it's onfetch event, registration by urlnamespace makes perfect sense. For these other event types, not so much. If we try to generalize 'service worker' and cast fetch as one class of events a 'service worker' can handle, i think we might end up with a different signature on the registration method.

navigator.controllers.register(scriptUrl, [eventstypes]);
// controllers has an 's' in it since it's not representative of any particular instance,

But that's missing the urlnamespace needed for the onfetch eventtype. If instead of strings, the array of [eventtypes] contained dictionary objects, we'd have a place to specify additional registration info per eventType.

navigator.controllers.register(scriptUrl,
    [{eventType:"fetch", urlnamespace:"/*"},
      "alarm",  // plain old string works for a simple eventType name
      {eventType:"push",
       /* other members that would allow the user agent to listen for
          and route server pushed events to this registered thing */}

If we recast along these lines we might want to change the signature of the attribute used to get the navcontroller associated with the current document.

navigator.fetchController

This model seems cleanest, modulo the "chaches" and "windows" objects being populated
in the Navigation Controller execution context.

If we separate the 'windows discovery' feature from nav controller, then it's not a wart. Any worker, service worker, or page make discover windows at their pleasure. Is there any reason to not do that?

I think a major open question here (maybe out of scope) is the implied permissions model.

It's interesting, for instance, that in the above examples the alarm permission is requested before dispatching an alarm event, rather than at registration. It's both event-specific and the timing is possibly out of sync with any user interaction with this event.

For other event types, and particularly if the same app needs multiple event type permissions, might this not get messy quickly? E.g. various background apps requesting permissions at seemingly random times as they need to post some event type?

Further, I worry that we've never solved the permissions overload problem in Chrome's UI. We've mostly buried them behind page actions or stuck them in infobars, and we've tried hard to avoid adding new instances of each. Service workers usher in a host of new ones.

It's interesting, for instance, that in the above examples the alarm permission is requested before dispatching an alarm event, rather than at registration. It's both event-specific and the timing is possibly out of sync with any user interaction with this event.

The permission is requested in a 'front end' page that is being viewed by the user in a tab. So it would be a standard infobar. Of course, ideally some system to have the page request all permissions in one go is great, and passing those to registration is tempting, but not all events may be handled by the service worker.

What about extending the idea of the SharedWorker to this type as well, and having "new ServiceWorker" basically do an install of the given url? Where SharedWorker has a one-per-origin kind of singleton property, ServiceWorker would extend that to a one-per-origin-and-persistent property. The idea would also be to inherit the kind of SharedWorker origin/CORS evaluation so that i.e. CDN-hosted sites can access the service worker and be controlled thereby.

Would this give the right API usage? You could then imagine registering the SW for events using syntax specific to the various event types. Even the examples given so far illustrate there's a variety of those. If they end up in some kind of manifest, they need a shared serialization, but for the API they can be unique.

So

sw = new ServiceWorker("my url");
navigator.registerPushListener(sw, $push arguments);
navigator.registerAlarmListener(sw);
navigator.registerNavigationListener(sw, "/*");

This would also more explicitly extend the SharedWorker permissions model: "there is a hidden thing doing work on behalf of this page" captures a lot of what users need to know to understand service workers and how to assign blame to them and control them. It also sets a consistent expectation for app developers: if the user ain't on your page, your script ain't running.

wiltzius: my sense is that a goal of service workers is that they be able to run inside the sandbox such that there's either no, or very minimal, permissions infrastructure needed specifically for them. The UA maintains control of when and where to invoke them, so the permissions stay with the capabilities requested: "Can this page set alarms" and such. There may be another permissions context where things like alarms come along for the ride, but that'd be orthogonal to service workers.

There should also be a capability for a web app to declare service workers in its manifest 1

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.