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