Keep pushing it, with the W3C Push API

You are all familiar with this experience — a little bubble pops up on your phone without warning, containing a nagging message along the lines of “your insipidly cute little monsters are rested, and want to go and fight more battles!”, or “You’ve got unanswered friend requests from people you don’t know. Hurry up and reply!”

Push messages are definitely not a new concept, having been a popular feature of mobile platforms for years. It is only recently however that Push has come to the web platform. This article takes you through the basics, and outlines the current status of Push.

Note: MDN includes full reference docs for the Push API as well as a detailed tutorial, Using the Push API.

Browser support

The Push API is currently at Working Draft stage. (The latest editor’s draft is here.) Most of the API is supported in recent versions of Chrome (42+) and Firefox (42+), but only in pre-release channels, (e.g. Nightly, Developer Edition, and Beta), except for PushMessageData, which is currently supported only in Firefox Nightly (Firefox 44+, again not in release channels).

Push requires service workers to run, which are also mostly supported in the latest versions of Chrome and Firefox. Since service workers require https to work for security purposes, Push also requires https.

It is worth noting that Push is often used along with communication APIs like Web Notifications and Channel Messaging for communicating the results of Push messages being sent. These both have fairly widespread support across modern browsers.

The process of push

In this section we will go through a typical process by which an app utilizing push messages could be set up.

Note: You can find a demo showing the Push API in use on Github. It will be useful for you to get this running so you can follow along as you read the sections below.

Requesting permission

The first thing you should do is request permission for web notifications, or anything else you are using that requires permissions. For example:

Notification.requestPermission();

Registering a service worker and subscribing to push

The next thing is to register a service worker to control the page, using ServiceWorkerContainer.register(). This returns a promise that resolves with a ServiceWorkerRegistration object:

navigator.serviceWorker.register('sw.js').then(function(reg) {
  // do something with reg object
});

Once we have this registration object, we can start registering for Push. Often you will send the ServiceWorkerRegistration object to some kind of subscription function.

At any point we can check that the service worker is active and ready to start doing things using the ServiceWorkerContainer.ready property, which returns a promise that resolves with the aforementioned ServiceWorkerRegistration object. Once we have this, we can access the PushManager using the ServiceWorkerRegistration.pushManager property. Then we can subscribe to the push service using PushManager.subscribe(), which returns a promise that resolves with a PushSubscription object.

navigator.serviceWorker.ready.then(function(reg) {
  reg.pushManager.getSubscription()
  .then(function(subscription) {
    // do something with your PushSubscription object
  });
}

To unsubscribe, the code is similar, but you must use the PushSubscription.unsubscribe() method to unsubscribe from push:

navigator.serviceWorker.ready.then(function(reg) {
  reg.pushManager.getSubscription()
  .then(function(subscription) {
    subscription.unsubscribe().then(function(successful) {
      // unsubscribe was successful
    })
  });
}

Push servers, and sending push messages

To send push messages, you need a server component. This can be written in any server-side language you like, as long as it can handle secure requests/responses and data encryption. (Push messages require https, and data sent by push needs to be encrypted.)

Note: Support for PushMessageData and encryption is currently Firefox-only, with the encryption process still being worked out. (Encryption and getKey() don’t yet appear in the spec.)

Once we have our PushSubscription object, we need to grab two pieces of information that are used for sending push messages:

  • PushSubscription.endpoint: This is a unique URL pointing at the push server that handles sending the push messages (each browser will have its own push server). For example: https://updates.push.services.mozilla.com/push/gAAAAABWJ-VZaQ9DhwvjZJHEHlZCzNJBPTPAcucU9mprtyzisSow75qHbY5lrjglEXE7G6SIfWvz-QSwhBcjpRjx2PAnKCAHd-5XHh1RFXa1ngqq_2-I0-PZoEqigI7E3ISO5zE1tNy29_Iyiu06m0tc_2nfKyuEcjwDPLyOC8c3IvawhBUUzMM=. This is used by your server to send the push message — the request hits the push server, and the random string on the end makes sure the push message is sent to the service worker associated with that particular push subscription.
  • PushSubscription.getKey('p256dh'): This method generates a public client key, one of the components used to encrypt the data. These details should then be sent to the server so it can send push messages when required. (You could use Fetch or XMLHttpRequest to do this.)

On the server-side, you should store the endpoint and any other required details so they are available when a push message needs to be sent to a push subscriber (use a database or whatever you like). In a production app, make sure these details are hidden, so malicious parties can’t steal endpoints and spam subscribers with push messages. Anyone with the endpoint can send a push message, as long as the subscription stays alive.

To send a push message without data, you need to send it to the endpoint URL with a method of POST. To send it with data in Firefox, you need to encrypt it, which involves the client public key. This is a pretty complex procedure (read Message Encryption for Web Push for more details). As time goes on, libraries will be written to do this kind of thing for you; Marco Castelluccio’s NodeJS web-push library is a good current option for NodeJS.

The service worker, and responding to push messages

Over in your service worker, you need to set up an onpush handler to respond to push messages being received.

self.addEventListener('push', function(event) {
  var obj = event.data.json();
  // do something with JSON
});

Note that the event object is of type PushEvent; its data property contains a PushMessageData object, which contains the data sent over the push message. This object has methods available for returning the message payload as a blob, array buffer, JSON object, or plain text string (we are converting it to JSON above). Once you have the payload, you can do what you want with it.

Sending a channel message

If you want respond to the push event by sending a channel message back to the main context, you first need to open a message channel between the main context and the service worker. In the main context, you can do something like this:

navigator.serviceWorker.ready.then(function(reg) {
  var channel = new MessageChannel();
  channel.port1.onmessage = function(e) {
    handleChannelMessage(e.data);
  }

  mySW = reg.active;
  mySW.postMessage('hello', [channel.port2]);
});

First we create a new MessageChannel object using a constructor, then set up an onmessage handler to handle messages coming across the channel to the main context.

Then, as before, we get a reference to our ServiceWorkerRegistration object. We then use its active property to return a ServiceWorker object. We can use the ServiceWorker object’s postMessage() method to post a message to the service worker context, along with port2 of the message channel.

Over in the service worker, we grab a reference to port2 using the following:

var port;

self.onmessage = function(e) {
  port = e.ports[0];
}

Once this link is established, data can be sent back to the main context using:

port.postMessage('my message');

Firing a notification

If you want to respond by firing a system notification, you can do this using ServiceWorkerRegistration.showNotification:

function fireNotification(obj, event) {
  var title = 'Subscription change';
  var body = obj.name + ' has ' + obj.action + 'd.';
  var icon = 'push-icon.png';
  var tag = 'push';

  event.waitUntil(self.registration.showNotification(title, {
    body: body,
    icon: icon,
    tag: tag
  }));
}

Note that here we have run this inside an ExtendableEvent.waitUntil method — this extends the lifetime of the event until after the notification has been fired, so we can make sure everything has happened as we intended.

Handling premature subscription expiration

Sometimes push subscriptions expire prematurely, without unsubscribe() being called. This can happen when the server gets overloaded, or if you are offline for a long time.  This is highly server-dependent, so the exact behavior is difficult to predict. In any case, you can handle this problem using the onsubscriptionchange handler, which will be invoked only in this specific case.

self.addEventListener('subscriptionchange', function() {
  // do something, usually resubscribe to push and
  // send the new subscription details back to the
  // server via XHR or Fetch
});

Chrome support for Push

Chrome has good support for Push as well, but with a few differences from Firefox. For a start, it doesn’t yet support sending PushMessageData in push messages; it also relies on the Google Cloud Messaging service. Read Extra steps for Chrome support for full details.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Tagged on: , ,

2 thoughts on “Keep pushing it, with the W3C Push API

  1. voracity

    Oh, I suppose this feature _would_ need a centralised server. There’s no decentralised alternative on the horizon (for an IPv6 world)?

    VA:F [1.9.22_1171]
    Rating: 0.0/5 (0 votes cast)
    VA:F [1.9.22_1171]
    Rating: 0 (from 0 votes)
  2. PhistucK

    Just a quick note and sort of corrections –
    Chrome (42) supports the Push API by default (your phrasing suggests that it supports it only in early release channels).
    Service Workers are indeed supported by default in Chrome (40), but they are disabled by default in Firefox (even in the latest stable version).

    VA:F [1.9.22_1171]
    Rating: 0.0/5 (0 votes cast)
    VA:F [1.9.22_1171]
    Rating: 0 (from 0 votes)

Leave a Reply