Bug 1150683 - Implement nsIPushNotificationService. r=dougt
☠☠ backed out by aa01c3fd458c ☠ ☠
authorKit Cambridge <github@kitcambridge.be>
Mon, 13 Apr 2015 17:25:18 -0700
changeset 257866 66ca87f2a9442b5087388d8283b3b5f51edd481b
parent 257865 814fc7abbe183ab1962cf3fc81ece965325d4f57
child 257867 4389151f1348369d48e83487f430e938eba08df0
push id8007
push userraliiev@mozilla.com
push dateMon, 11 May 2015 19:23:16 +0000
treeherdermozilla-aurora@e2ce1aac996e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdougt
bugs1150683
milestone40.0a1
Bug 1150683 - Implement nsIPushNotificationService. r=dougt
b2g/installer/package-manifest.in
browser/installer/package-manifest.in
dom/push/Push.manifest
dom/push/PushNotificationService.js
dom/push/PushService.jsm
dom/push/PushServiceLauncher.js
dom/push/moz.build
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -625,17 +625,17 @@
 @RESPATH@/components/XULStore.js
 @RESPATH@/components/XULStore.manifest
 @RESPATH@/components/Webapps.js
 @RESPATH@/components/Webapps.manifest
 @RESPATH@/components/AppsService.js
 @RESPATH@/components/AppsService.manifest
 @RESPATH@/components/Push.js
 @RESPATH@/components/Push.manifest
-@RESPATH@/components/PushServiceLauncher.js
+@RESPATH@/components/PushNotificationService.js
 
 @RESPATH@/components/InterAppComm.manifest
 @RESPATH@/components/InterAppCommService.js
 @RESPATH@/components/InterAppConnection.js
 @RESPATH@/components/InterAppMessagePort.js
 
 @RESPATH@/components/nsDOMIdentity.js
 @RESPATH@/components/nsIDService.js
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -552,17 +552,17 @@
 @RESPATH@/components/PhoneNumberService.js
 @RESPATH@/components/PhoneNumberService.manifest
 @RESPATH@/components/NotificationStorage.js
 @RESPATH@/components/NotificationStorage.manifest
 @RESPATH@/components/AlarmsManager.js
 @RESPATH@/components/AlarmsManager.manifest
 @RESPATH@/components/Push.js
 @RESPATH@/components/Push.manifest
-@RESPATH@/components/PushServiceLauncher.js
+@RESPATH@/components/PushNotificationService.js
 
 @RESPATH@/components/SlowScriptDebug.manifest
 @RESPATH@/components/SlowScriptDebug.js
 
 #ifndef RELEASE_BUILD
 @RESPATH@/components/InterAppComm.manifest
 @RESPATH@/components/InterAppCommService.js
 @RESPATH@/components/InterAppConnection.js
--- a/dom/push/Push.manifest
+++ b/dom/push/Push.manifest
@@ -1,11 +1,14 @@
 # DOM API
 component {cde1d019-fad8-4044-b141-65fb4fb7a245} Push.js
 contract @mozilla.org/push/PushManager;1 {cde1d019-fad8-4044-b141-65fb4fb7a245}
 
 component {CA86B665-BEDA-4212-8D0F-5C9F65270B58} Push.js
 contract @mozilla.org/push/PushSubscription;1 {CA86B665-BEDA-4212-8D0F-5C9F65270B58}
 
-# Component to initialize PushService on startup.
-component {4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d} PushServiceLauncher.js
-contract @mozilla.org/push/ServiceLauncher;1 {4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d}
-category app-startup PushServiceLauncher @mozilla.org/push/ServiceLauncher;1
+# XPCOM component; initializes the PushService on startup.
+component {32028e38-903b-4a64-a180-5857eb4cb3dd} PushNotificationService.js
+contract @mozilla.org/push/NotificationService;1 {32028e38-903b-4a64-a180-5857eb4cb3dd}
+category app-startup PushNotificationService @mozilla.org/push/NotificationService;1
+
+component {66a87970-6dc9-46e0-ac61-adb4a13791de} PushNotificationService.js
+contract @mozilla.org/push/ObserverNotification;1 {66a87970-6dc9-46e0-ac61-adb4a13791de}
new file mode 100644
--- /dev/null
+++ b/dom/push/PushNotificationService.js
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let isParent = Cc["@mozilla.org/xre/runtime;1"]
+                 .getService(Ci.nsIXULRuntime)
+                 .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+XPCOMUtils.defineLazyGetter(this, "PushService", function() {
+  // Lazily initialize the PushService on `final-ui-startup` or first use.
+  const {PushService} = Cu.import("resource://gre/modules/PushService.jsm", {});
+  if (isParent) {
+    PushService.init();
+  }
+  return PushService;
+});
+
+this.PushNotificationService = function PushNotificationService() {};
+
+PushNotificationService.prototype = {
+  classID: Components.ID("{32028e38-903b-4a64-a180-5857eb4cb3dd}"),
+
+  contractID: "@mozilla.org/push/NotificationService;1",
+
+  _xpcom_factory: XPCOMUtils.generateSingletonFactory(PushNotificationService),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsIPushNotificationService]),
+
+  _pushEnabled: function _pushEnabled() {
+    return Services.prefs.getBoolPref("dom.push.enabled");
+  },
+
+  register: function register(scope, pageURL) {
+    if (!this._pushEnabled()) {
+      return Promise.reject(Cr.NS_ERROR_NOT_AVAILABLE);
+    }
+    return PushService._register({scope, pageURL});
+  },
+
+  unregister: function unregister(scope) {
+    if (!this._pushEnabled()) {
+      return Promise.reject(Cr.NS_ERROR_NOT_AVAILABLE);
+    }
+    return PushService._unregister({scope});
+  },
+
+  registration: function registration(scope) {
+    if (!this._pushEnabled()) {
+      return Promise.reject(Cr.NS_ERROR_NOT_AVAILABLE);
+    }
+    return PushService._registration({scope});
+  },
+
+  observe: function observe(subject, topic, data) {
+    switch (topic) {
+      case "app-startup":
+        Services.obs.addObserver(this, "final-ui-startup", true);
+        break;
+      case "final-ui-startup":
+        Services.obs.removeObserver(this, "final-ui-startup");
+        if (!this._pushEnabled()) {
+          return;
+        }
+        if (isParent) {
+          PushService.init();
+        }
+        break;
+    }
+  }
+};
+
+this.PushObserverNotification = function PushObserverNotification() {};
+
+PushObserverNotification.prototype = {
+  classID: Components.ID("{66a87970-6dc9-46e0-ac61-adb4a13791de}"),
+
+  contractID: "@mozilla.org/push/ObserverNotification;1",
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushObserverNotification])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+  PushNotificationService,
+  PushObserverNotification
+]);
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -289,16 +289,17 @@ this.PushService = {
   observe: function observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       /*
        * We need to call uninit() on shutdown to clean up things that modules aren't very good
        * at automatically cleaning up, so we don't get shutdown leaks on browser shutdown.
        */
       case "xpcom-shutdown":
         this.uninit();
+        break;
       case "network-active-changed":         /* On B2G. */
       case "network:offline-status-changed": /* On desktop. */
         // In case of network-active-changed, always disconnect existing
         // connections. In case of offline-status changing from offline to
         // online, it is likely that these statements will be no-ops.
         if (this._udpServer) {
           this._udpServer.close();
           // Set to null since this is checked in _listenForUDPWakeup()
@@ -343,19 +344,22 @@ this.PushService = {
             // also made to fail, since we are going to be disconnecting the socket.
             if (requestTimedOut || duration > this._requestTimeout) {
               debug("Request timeout: Removing " + channelID);
               requestTimedOut = true;
               this._pendingRequests[channelID]
                 .deferred.reject({status: 0, error: "TimeoutError"});
 
               delete this._pendingRequests[channelID];
-              for (let i = this._requestQueue.length - 1; i >= 0; --i)
-                if (this._requestQueue[i].channelID == channelID)
+              for (let i = this._requestQueue.length - 1; i >= 0; --i) {
+                let [, data] = this._requestQueue[i];
+                if (data && data.channelID == channelID) {
                   this._requestQueue.splice(i, 1);
+                }
+              }
             }
           }
 
           // The most likely reason for a registration request timing out is
           // that the socket has disconnected. Best to reconnect.
           if (requestTimedOut) {
             this._shutdownWS();
             this._reconnectAfterBackoff();
@@ -488,16 +492,19 @@ this.PushService = {
     }
     msg = JSON.stringify(msg);
     debug("Sending message: " + msg);
     this._ws.sendMsg(msg);
   },
 
   init: function() {
     debug("init()");
+    if (this._started) {
+      return;
+    }
     if (!prefs.get("enabled"))
         return null;
 
     var globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
                .getService(Ci.nsIFrameScriptLoader);
 
     globalMM.loadFrameScript("chrome://global/content/PushServiceChildPreload.js", true);
 
@@ -561,16 +568,17 @@ this.PushService = {
       this._wsListener._pushService = null;
     try {
         this._ws.close(0, null);
     } catch (e) {}
     this._ws = null;
 
     this._waitingForPong = false;
     this._stopAlarm();
+    this._cancelPendingRequests();
   },
 
   uninit: function() {
     if (!this._started)
       return;
 
     debug("uninit()");
 
@@ -600,16 +608,17 @@ this.PushService = {
     // WebSocket has been closed with NS_ERROR_ABORT (if it was up) and will
     // try to reconnect. Stop the timer.
     this._stopAlarm();
 
     if (this._requestTimeoutTimer) {
       this._requestTimeoutTimer.cancel();
     }
 
+    this._started = false;
     debug("shutdown complete!");
   },
 
   /**
    * How retries work:  The goal is to ensure websocket is always up on
    * networks not supporting UDP. So the websocket should only be shutdown if
    * onServerClose indicates UDP wakeup.  If WS is closed due to socket error,
    * _reconnectAfterBackoff() is called.  The retry alarm is started and when
@@ -877,16 +886,20 @@ this.PushService = {
       debug("Error opening websocket. asyncOpen failed!");
       this._shutdownWS();
       this._reconnectAfterBackoff();
     }
   },
 
   _startListeningIfChannelsPresent: function() {
     // Check to see if we need to do anything.
+    if (this._requestQueue.length > 0) {
+      this._beginWSSetup();
+      return;
+    }
     this._db.getAllChannelIDs(function(channelIDs) {
       if (channelIDs.length > 0) {
         this._beginWSSetup();
       }
     }.bind(this));
   },
 
   /** |delay| should be in milliseconds. */
@@ -1160,33 +1173,41 @@ this.PushService = {
 
   /*
    * Must be used only by request/response style calls over the websocket.
    */
   _sendRequest: function(action, data) {
     debug("sendRequest() " + action);
     if (typeof data.channelID !== "string") {
       debug("Received non-string channelID");
-      return Promise.reject("Received non-string channelID");
+      return Promise.reject({error: "Received non-string channelID"});
     }
 
-    let deferred = Promise.defer();
-
     if (Object.keys(this._pendingRequests).length == 0) {
       // start the timer since we now have at least one request
       if (!this._requestTimeoutTimer)
         this._requestTimeoutTimer = Cc["@mozilla.org/timer;1"]
                                       .createInstance(Ci.nsITimer);
       this._requestTimeoutTimer.init(this,
                                      this._requestTimeout,
                                      Ci.nsITimer.TYPE_REPEATING_SLACK);
     }
 
-    this._pendingRequests[data.channelID] = { deferred: deferred,
-                                              ctime: Date.now() };
+    let deferred;
+    let request = this._pendingRequests[data.channelID];
+    if (request) {
+      // If a request is already pending for this channel ID, assume it's a
+      // retry. Use the existing deferred, but update the send time and re-send
+      // the request.
+      deferred = request.deferred;
+    } else {
+      deferred = Promise.defer();
+      request = this._pendingRequests[data.channelID] = {deferred};
+    }
+    request.ctime = Date.now();
 
     this._send(action, data);
     return deferred.promise;
   },
 
   _send: function(action, data) {
     debug("send()");
     this._requestQueue.push([action, data]);
@@ -1198,16 +1219,21 @@ this.PushService = {
     debug("_processNextRequestInQueue()");
 
     if (this._requestQueue.length == 0) {
       debug("Request queue empty");
       return;
     }
 
     if (this._currentState != STATE_READY) {
+      if (!this._started) {
+        // The component hasn't been initialized yet. Return early; init()
+        // will dequeue all pending requests.
+        return;
+      }
       if (!this._ws) {
         // This will end up calling processNextRequestInQueue().
         this._beginWSSetup();
       }
       else {
         // We have a socket open so we are just waiting for hello to finish.
         // That will call processNextRequestInQueue().
       }
@@ -1266,62 +1292,59 @@ this.PushService = {
                             compareRecordVersionAndNotify.bind(this),
                             recoverNoSuchChannelID.bind(this));
   },
 
   // Fires a push-register system message to all applications that have
   // registration.
   _notifyAllAppsRegister: function() {
     debug("notifyAllAppsRegister()");
-    let deferred = Promise.defer();
-
-    // records are objects describing the registration as stored in IndexedDB.
-    function wakeupRegisteredApps(records) {
-      // Pages to be notified.
-      // wakeupTable[scope] -> [ pageURL ]
-      let wakeupTable = {};
-      for (let i = 0; i < records.length; i++) {
-        let record = records[i];
-        if (!(record.scope in wakeupTable))
-          wakeupTable[record.scope] = [];
-
-        wakeupTable[record.scope].push(record.pageURL);
-      }
-
-      // TODO -- test needed.  E10s support needed.
-
-      let globalMM = Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager);
-      for (let scope in wakeupTable) {
-        wakeupTable[scope].forEach(function(pageURL) {
-          globalMM.broadcastAsyncMessage('pushsubscriptionchanged', aPushRecord.scope);
-        });
-      }
-      deferred.resolve();
-    }
-
-    this._db.getAllChannelIDs(wakeupRegisteredApps, deferred.reject);
-
-    return deferred.promise;
+    return new Promise((resolve, reject) => {
+      // records are objects describing the registration as stored in IndexedDB.
+      this._db.getAllChannelIDs(records => {
+        let scopes = new Set();
+        for (let record of records) {
+          scopes.add(record.scope);
+        }
+        let globalMM = Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager);
+        for (let scope of scopes) {
+          // Notify XPCOM observers.
+          Services.obs.notifyObservers(
+            null,
+            "push-subscription-change",
+            scope
+          );
+          // TODO -- test needed.  E10s support needed.
+          globalMM.broadcastAsyncMessage('pushsubscriptionchanged', scope);
+        }
+        resolve();
+      }, reject);
+    });
   },
 
   _notifyApp: function(aPushRecord) {
-    if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.scope) {
+    if (!aPushRecord || !aPushRecord.scope) {
       debug("notifyApp() something is undefined.  Dropping notification: "
         + JSON.stringify(aPushRecord) );
       return;
     }
 
-    debug("notifyApp() " + aPushRecord.pageURL +
-          "  " + aPushRecord.scope);
-    let pageURI = Services.io.newURI(aPushRecord.pageURL, null, null);
+    debug("notifyApp() " + aPushRecord.scope);
     let scopeURI = Services.io.newURI(aPushRecord.scope, null, null);
-    let message = {
-      pushEndpoint: aPushRecord.pushEndpoint,
-      version: aPushRecord.version
-    };
+    // Notify XPCOM observers.
+    let notification = Cc["@mozilla.org/push/ObserverNotification;1"]
+                         .createInstance(Ci.nsIPushObserverNotification);
+    notification.pushEndpoint = aPushRecord.pushEndpoint;
+    notification.version = aPushRecord.version;
+    notification.data = "";
+    Services.obs.notifyObservers(
+      notification,
+      "push-notification",
+      aPushRecord.scope
+    );
 
     // If permission has been revoked, trash the message.
     if(Services.perms.testExactPermission(scopeURI, "push") != Ci.nsIPermissionManager.ALLOW_ACTION) {
       debug("Does not have permission for push.")
       return;
     }
 
     // TODO data.
@@ -1360,123 +1383,132 @@ this.PushService = {
     let json = aMessage.data;
     this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm);
   },
 
   /**
    * Called on message from the child process. aPageRecord is an object sent by
    * navigator.push, identifying the sending page and other fields.
    */
+  _registerWithServer: function(channelID, aPageRecord) {
+    debug("registerWithServer()");
 
-  _registerWithServer: function(aPageRecord, aMessageManager) {
+    return this._sendRequest("register", {channelID: channelID})
+      .then(
+        this._onRegisterSuccess.bind(this, aPageRecord, channelID),
+        this._onRegisterError.bind(this)
+      );
+  },
+
+  _generateID: function() {
     let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                           .getService(Ci.nsIUUIDGenerator);
     // generateUUID() gives a UUID surrounded by {...}, slice them off.
-    let channelID = uuidGenerator.generateUUID().toString().slice(1, -1);
+    return uuidGenerator.generateUUID().toString().slice(1, -1);
+  },
+
+  _register: function(aPageRecord) {
+    let recordPromise = new Promise((resolve, reject) =>
+      this._db.getByScope(aPageRecord.scope, resolve, reject));
 
-    this._sendRequest("register", {channelID: channelID})
-      .then(
-        this._onRegisterSuccess.bind(this, aPageRecord, channelID),
-        this._onRegisterError.bind(this, aPageRecord, aMessageManager)
-      )
-      .then(
-        function(message) {
-          aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
-        },
-        function(message) {
-          aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
+    return recordPromise.then(
+      pushRecord => {
+        if (pushRecord == null) {
+          let channelID = this._generateID();
+          return this._registerWithServer(channelID, aPageRecord);
+        }
+        return pushRecord;
+      },
+      error => {
+        debug("getByScope failed");
+        throw "Database error";
       }
     );
   },
 
   register: function(aPageRecord, aMessageManager) {
     debug("register(): " + JSON.stringify(aPageRecord));
 
-    this._db.getByScope(aPageRecord.scope,
+    this._register(aPageRecord).then(
       function(aPageRecord, aMessageManager, pushRecord) {
-        if (pushRecord == null) {
-          this._registerWithServer(aPageRecord, aMessageManager);
-        }
-        else {
-          this._onRegistrationSuccess(aPageRecord, aMessageManager, pushRecord);
-        }
+        let message = {
+          requestID: aPageRecord.requestID,
+          pushEndpoint: pushRecord.pushEndpoint
+        };
+        aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
       }.bind(this, aPageRecord, aMessageManager),
-      function () {
-        debug("getByScope failed");
+      function(error) {
+        let message = {
+          requestID: aPageRecord.requestID,
+          error
+        };
+        aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
       }
     );
   },
 
   /**
    * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
    * from _sendRequest, causing the promise to be rejected instead.
    */
   _onRegisterSuccess: function(aPageRecord, generatedChannelID, data) {
     debug("_onRegisterSuccess()");
-    let deferred = Promise.defer();
-    let message = { requestID: aPageRecord.requestID };
 
     if (typeof data.channelID !== "string") {
-      debug("Invalid channelID " + message);
-      message["error"] = "Invalid channelID received";
-      throw message;
+      debug("Invalid channelID " + data.channelID);
+      throw "Invalid channelID received";
     }
     else if (data.channelID != generatedChannelID) {
       debug("Server replied with different channelID " + data.channelID +
             " than what UA generated " + generatedChannelID);
-      message["error"] = "Server sent 200 status code but different channelID";
-      throw message;
+      throw "Server sent 200 status code but different channelID";
     }
 
     try {
       Services.io.newURI(data.pushEndpoint, null, null);
     }
     catch (e) {
       debug("Invalid pushEndpoint " + data.pushEndpoint);
-      message["error"] = "Invalid pushEndpoint " + data.pushEndpoint;
-      throw message;
+      throw "Invalid pushEndpoint " + data.pushEndpoint;
     }
 
     let record = {
       channelID: data.channelID,
       pushEndpoint: data.pushEndpoint,
       pageURL: aPageRecord.pageURL,
       scope: aPageRecord.scope,
       version: null
     };
 
     debug("scope in _onRegisterSuccess: " + aPageRecord.scope)
 
-    this._updatePushRecord(record)
+    return this._updatePushRecord(record)
       .then(
         function() {
-          message["pushEndpoint"] = data.pushEndpoint;
-          deferred.resolve(message);
+          return record;
         },
         function(error) {
           // Unable to save.
           this._send("unregister", {channelID: record.channelID});
-          message["error"] = error;
-          deferred.reject(message);
+          throw error;
         }.bind(this)
       );
-
-    return deferred.promise;
   },
 
   /**
    * Exceptions thrown in _onRegisterError are caught by the promise obtained
    * from _sendRequest, causing the promise to be rejected instead.
    */
-  _onRegisterError: function(aPageRecord, aMessageManager, reply) {
+  _onRegisterError: function(reply) {
     debug("_onRegisterError()");
     if (!reply.error) {
       debug("Called without valid error message!");
+      throw "Registration error";
     }
-    throw { requestID: aPageRecord.requestID, error: reply.error };
+    throw reply.error;
   },
 
   /**
    * Called on message from the child process.
    *
    * Why is the record being deleted from the local database before the server
    * is told?
    *
@@ -1493,88 +1525,109 @@ this.PushService = {
    * channelID, but the response is lost because of network failure, the
    * application is never informed. In addition the application may retry the
    * unregister when it fails due to timeout at which point the server will say
    * it does not know of this unregistration.  We'll have to make the
    * registration/unregistration phases have retries and attempts to resend
    * messages from the server, and have the client acknowledge. On a server,
    * data is cheap, reliable notification is not.
    */
-  unregister: function(aPageRecord, aMessageManager) {
-    debug("unregister() " + JSON.stringify(aPageRecord));
+  _unregister: function(aPageRecord) {
+    debug("unregisterWithServer()");
 
+    let deferred = Promise.defer();
     let fail = function(error) {
       debug("unregister() fail() error " + error);
-      let message = {requestID: aPageRecord.requestID, error: error};
-      aMessageManager.sendAsyncMessage("PushService:Unregister:KO", message);
+      deferred.reject(error);
+    };
+
+    if (!aPageRecord.scope) {
+      fail("NotFoundError");
+      return deferred.promise;
     }
 
-    this._db.getByPushEndpoint(aPageRecord.pushEndpoint, function(record) {
+    this._db.getByScope(aPageRecord.scope, function(record) {
       // If the endpoint didn't exist, let's just fail.
       if (record === undefined) {
         fail("NotFoundError");
         return;
       }
 
-      // Non-owner tried to unregister, say success, but don't do anything.
-      if (record.scope !== aPageRecord.scope) {
-        aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
-          requestID: aPageRecord.requestID,
-          pushEndpoint: aPageRecord.pushEndpoint
-        });
-        return;
-      }
-
       this._db.delete(record.channelID, function() {
         // Let's be nice to the server and try to inform it, but we don't care
         // about the reply.
         this._send("unregister", {channelID: record.channelID});
+        deferred.resolve();
+      }.bind(this), fail);
+    }.bind(this), fail);
+
+    return deferred.promise;
+  },
+
+  unregister: function(aPageRecord, aMessageManager) {
+    debug("unregister() " + JSON.stringify(aPageRecord));
+
+    this._unregister(aPageRecord).then(
+      () => {
         aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
           requestID: aPageRecord.requestID,
           pushEndpoint: aPageRecord.pushEndpoint
         });
-      }.bind(this), fail);
-    }.bind(this), fail);
+      },
+      error => {
+        aMessageManager.sendAsyncMessage("PushService:Unregister:KO", {
+          requestID: aPageRecord.requestID,
+          error
+        });
+      }
+    );
   },
 
   /**
    * Called on message from the child process
    */
-  registration: function(aPageRecord, aMessageManager) {
-    debug("registration()");
-    this._db.getByScope(aPageRecord.scope,
-      this._onRegistrationSuccess.bind(this, aPageRecord, aMessageManager),
-      this._onRegistrationError.bind(this, aPageRecord, aMessageManager));
-  },
-
-  _onRegistrationSuccess: function(aPageRecord,
-                                   aMessageManager,
-                                   pushRecord) {
-
-
-    let registration = null;
-
-    if (pushRecord) {
-      registration = {
-        pushEndpoint: pushRecord.pushEndpoint,
-        version: pushRecord.version
-      };
-    }
-
-    aMessageManager.sendAsyncMessage("PushService:Registration:OK", {
-      requestID: aPageRecord.requestID,
-      registration: registration
+  _registration: function(aPageRecord) {
+    return new Promise((resolve, reject) => {
+      if (!aPageRecord.scope) {
+        reject("Database error");
+        return;
+      }
+      this._db.getByScope(aPageRecord.scope,
+        pushRecord => {
+          let registration = null;
+          if (pushRecord) {
+            registration = {
+              pushEndpoint: pushRecord.pushEndpoint,
+              version: pushRecord.version
+            };
+          }
+          resolve(registration);
+        },
+        () => reject("Database error")
+      );
     });
   },
 
-  _onRegistrationError: function(aPageRecord, aMessageManager) {
-    aMessageManager.sendAsyncMessage("PushService:Registration:KO", {
-      requestID: aPageRecord.requestID,
-      error: "Database error"
-    });
+  registration: function(aPageRecord, aMessageManager) {
+    debug("registration()");
+
+    return this._registration(aPageRecord).then(
+      registration => {
+        aMessageManager.sendAsyncMessage("PushService:Registration:OK", {
+          requestID: aPageRecord.requestID,
+          registration
+        });
+      },
+      error => {
+        aMessageManager.sendAsyncMessage("PushService:Registration:KO", {
+          requestID: aPageRecord.requestID,
+          error
+        });
+      }
+    );
   },
 
   // begin Push protocol handshake
   _wsOnStart: function(context) {
     debug("wsOnStart()");
     this._releaseWakeLock();
 
     if (this._currentState != STATE_WAITING_FOR_WS_START) {
@@ -1728,16 +1781,27 @@ this.PushService = {
     if (aStatusCode == kUDP_WAKEUP_WS_STATUS_CODE) {
       debug("Server closed with promise to wake up");
       this._willBeWokenUpByUDP = true;
       // TODO: there should be no pending requests
     }
   },
 
   /**
+   * Rejects all pending requests with errors.
+   */
+  _cancelPendingRequests: function() {
+    for (let channelID in this._pendingRequests) {
+      let request = this._pendingRequests[channelID];
+      delete this._pendingRequests[channelID];
+      request.deferred.reject({status: 0, error: "CancelledError"});
+    }
+  },
+
+  /**
    * This method should be called only if the device is on a mobile network!
    */
   _listenForUDPWakeup: function() {
     debug("listenForUDPWakeup()");
 
     if (this._udpServer) {
       debug("UDP Server already running");
       return;
@@ -1901,9 +1965,9 @@ this.PushService = {
     }
 
     debug("[_getMobileNetworkId:queryDNSForDomain] Getting mobile network ID");
 
     let netidAddress = "wakeup.mnc" + ("00" + networkInfo.mnc).slice(-3) +
       ".mcc" + ("00" + networkInfo.mcc).slice(-3) + ".3gppnetwork.org";
     queryDNSForDomain(netidAddress, callback);
   }
-}
+};
deleted file mode 100644
--- a/dom/push/PushServiceLauncher.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cu = Components.utils;
-const Cr = Components.results;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-function PushServiceLauncher() {
-};
-
-PushServiceLauncher.prototype = {
-  classID: Components.ID("{4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d}"),
-
-  contractID: "@mozilla.org/push/ServiceLauncher;1",
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-                                         Ci.nsISupportsWeakReference]),
-
-  observe: function observe(subject, topic, data) {
-    switch (topic) {
-      case "app-startup":
-        Services.obs.addObserver(this, "final-ui-startup", true);
-        break;
-      case "final-ui-startup":
-        Services.obs.removeObserver(this, "final-ui-startup");
-        if (!Services.prefs.getBoolPref("dom.push.enabled")) {
-          return;
-        }
-
-        let isParent = Cc["@mozilla.org/xre/runtime;1"]
-                       .getService(Ci.nsIXULRuntime)
-                       .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
-
-        if (isParent) {
-          Cu.import("resource://gre/modules/PushService.jsm");
-          PushService.init();
-        }
-        break;
-    }
-  }
-};
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PushServiceLauncher]);
--- a/dom/push/moz.build
+++ b/dom/push/moz.build
@@ -1,17 +1,17 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXTRA_COMPONENTS += [
     'Push.js',
     'Push.manifest',
-    'PushServiceLauncher.js',
+    'PushNotificationService.js',
 ]
 
 EXTRA_JS_MODULES += [
     'PushService.jsm',
 ]
 
 MOCHITEST_MANIFESTS += [
     'test/mochitest.ini',