Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Service Worker + Push API

Motivation

The current Push API Draft specifies a system that has no notion of push channel persistence. Further, it does not include push message payloads, a feature that many developers want.

This gist outlines an API which:

  • integrates with the Service Worker to enable delivery of push messages to applications which do not have visible tabs
  • enables a JSON-formatted body of content
  • guards access to registration for pushes on potential user consent

Example

<!DOCTYPE html>
<!-- https://example.com/index.html -->
<html>
  <head>
    <script>
      // Notes:
      //  - PushRegistration has been extended with a "channelName" property
      //  - PushMessage has been extended with "data" and "channelName" properties
      //  - The "version" property of PushMessage has been removed.

      var logError = console.error.bind(console);

      function toQueryString(obj) {
        var encode = encodeURIComponent;
        for (var x in obj) {
          components.push(encode(x) + "=" + encode(obj[x]));
        }
        return "?" + components.join("&");
      }

      function registerWithAppServer(registration) {
        // We've been handed channel name with a PushRegistration which
        // we send to the app server
        asyncXHR(
          "http://example.com/push/activate" + toQueryString(registration);
        );
      }

      // This is an ugly detail of the current SW design. See:
      //   https://github.com/slightlyoff/ServiceWorker/issues/174
      function swReady() {
        var sw = navigator.serviceWorker;
        return new Promise(function(resolve, reject) {
          if (!sw.active) {
            // Note that the promise returned from register() resolves as soon
            // as the script for the SW is downloaded and evaluates without issue.
            // At this point it is NOT considered installed and will not receive
            // push events. We will, however, allow the registration for pushes
            // to proceed at this point. Push registrations that succeed and generate
            // messages will see those messages queued until the SW is installed
            // and activated, perhaps at some time in the near future (e.g., once
            // resources for the application itself are downloaded).
            sw.register("/service_worker.js", { scope: "*" }).then(resolve, reject);
          } else {
            sw.ready().then(resolve, reject);
          }
        });
      }


      // Only try to register for a push channel when we have valid SW
      swReady().then(function(sw) {
        var gcmSenderId = "......";
        var apnSenderId = "......";
        var cn = "channel name";
        var push = navigator.push;

        // The push object is an async map that can be used to enumerate and
        // test for active channels.
        push.has(cn).catch(function() {
          // register() is async and can prompt the user. On success, the
          // channel name is added to the map.
          push.register(cn, {
            // the "sender" field is push-server-specific and is optional.
            // Some servers (e.g., GCM) will require that this field be
            // populated with a pre-arranged ID for the app. The system
            // selects between the supplied keys to decide which one to
            // offer the "local" push server for this registration.
            sender: {
              apn: apnSenderId,
              gcm: gcmSenderId,
              // ...
            }
          }).then(registerWithAppServer, logError);
        });
      });


    </script>
  </head>
</html>
// https://example.com/service_worker.js

this.oninstall = function(e) {
  // Note that push messages won't be delivered to this SW
  // until the install and activate steps are completed. This
  // May mean waiting until caches are populated, etc.
  e.waitUntil(...);
};
this.onactivate = function(e) { ... }

// Note that we no longer need navigator.hasPendingMessages() because the UA
// can queue push messages for the SW and deliver them as events whenever it
// deems necessary.
this.onpush = function(e) {
  // Log the channel name
  console.log(e.message.channelName);
  // Log the deserialized JSON data object
  console.log(e.message.data);

  // ...

  // From here the SW can write the data to IDB, send it to any open windows,
  // etc.
}
@twirl

This comment has been minimized.

Copy link

@twirl twirl commented Feb 11, 2014

  1. What ServiceWorker will get push messages if there are several workers matching different URL patterns?
  2. Why .has(nc).catch(...)? Using .has(cn).then(function (res) { if (res === false) ... }) seems more logical.
@slightlyoff

This comment has been minimized.

Copy link
Owner Author

@slightlyoff slightlyoff commented Feb 12, 2014

  1. that's a good question. One thought I had was that the push messages would be dispatched to the SW for the URL that registered them, in this case https://example.com/index.html. Dispatch could happen based on usual longest-prefix-with-globbing matches the way navigations are matched.
  2. Hrm...I need to think about that. Thanks for the suggestion!
@twirl

This comment has been minimized.

Copy link

@twirl twirl commented Feb 12, 2014

AFAIK, current push server implementations don't care about webapp url at all. In my opinion, the best way to handle this issue is to alter .registerServiceWorker signature like that:
sw.register("/service_worker.js", { scope: "*", handlePushMessages: true }); // last registration wins
Other alternatives which came to my mind:

  • alter .register signature (.register(cn, sw)) - raises the same problem of binding message stream to runtime-only entity
  • direct push messages to the root (/) of a domain - unobvious
@xogeny

This comment has been minimized.

Copy link

@xogeny xogeny commented Feb 12, 2014

Not sure if this is relevant, but you might find this project interesting. It's a server side messaging library that uses HAL+JSON. It has a topic taxonomy where you can subscribe at different levels. The API itself might be interesting.

@jungkees

This comment has been minimized.

Copy link

@jungkees jungkees commented Feb 13, 2014

For push message, it seems channel name should be the key to match the Service Worker to be spun up as the scope is for navigation matching. And as the active worker is the prerequisite for push registration, the UA can be informed the channel information either in SW registration time:

sw.register("/service_worker.js", { pushChannel: "twitter" });

or in push registration time:

swReady().then(function(sw) {
  push.register(sw, "twitter");
}

I think the latter is a bit more extensible option for other APIs:

// e.g. Task Scheduler API: http://www.w3.org/2012/sysapps/web-alarms/
var alarm = navigator.taskScheduler;

swReady().then(function(sw) {
  alarm.add(sw, Date.now() + (10 * 60000), { 
    message: "Alright. Let's go for lunch now!" 
  });
}
@jakearchibald

This comment has been minimized.

Copy link

@jakearchibald jakearchibald commented Feb 14, 2014

If push only works with a serivceworker, shouldn't registration also happen in the serviceworker? That does away with the weird swReady stuff.

@slightlyoff

This comment has been minimized.

Copy link
Owner Author

@slightlyoff slightlyoff commented Feb 14, 2014

Re: scope matching. I had thought that perhaps the URL of the registering page would be used for mapping messages on the channel to active SW's. It's more implicit, which isn't great, but will Just Work (TM) most of the time.

WDYT?

@tobie

This comment has been minimized.

Copy link

@tobie tobie commented Feb 17, 2014

FWIW, current Push Notification spec avoids sending data along with the event for privacy reasons. Push servers are generally going to be owned by third parties (e.g. the operator), so you don't want to send unencrypted content through them.

Sequence would be:

  1. App Server notifies push server of new event.
  2. Push Server notifies User Agent of new event.
  3. User Agent wakes up appropriate Service Worker.
  4. Service Worker makes XHR request to App Server to get data.
@slightlyoff

This comment has been minimized.

Copy link
Owner Author

@slightlyoff slightlyoff commented Feb 25, 2014

After last week's discussion with Bryan, we're going to remove the channel name. If we need it, it can go in the payload.

@pwFoo

This comment has been minimized.

Copy link

@pwFoo pwFoo commented Jun 7, 2018

I'm new with notification / push api and searching for a lib with closed window notification support for background running browsers or android devices.
Has your Code support for that feature.

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