Skip to content

Instantly share code, notes, and snippets.

@slightlyoff
Created September 5, 2013 05:53
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slightlyoff/7fae65a908ac318f69a3 to your computer and use it in GitHub Desktop.
Save slightlyoff/7fae65a908ac318f69a3 to your computer and use it in GitHub Desktop.
Service Workers

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?

@nikhilm
Copy link

nikhilm commented Sep 19, 2013

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment