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
<!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.
}