Skip to content

Instantly share code, notes, and snippets.

@slightlyoff
Last active June 7, 2018 07:15
Show Gist options
  • Save slightlyoff/8927885 to your computer and use it in GitHub Desktop.
Save slightlyoff/8927885 to your computer and use it in GitHub Desktop.
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.
}
@slightlyoff
Copy link
Author

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
Copy link

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
Copy link
Author

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
Copy link

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