Bug 1150812 - Split PushService - make the separation more common for webSocket and http2. r=nsm, r=mt
authorDragana Damjanovic <dd.mozilla@gmail.com>
Wed, 03 Jun 2015 08:05:00 -0400
changeset 247023 4d17715232f56f8c44afc560c06f98883f25918c
parent 247022 d9707252997de48b577ba539c9027574c3bb37ba
child 247024 036d9abeb00c1de853fc4b04fc43c9c793038cb3
push id28848
push userryanvm@gmail.com
push dateWed, 03 Jun 2015 20:00:13 +0000
treeherdermozilla-central@0920f2325a6d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnsm, mt
bugs1150812
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1150812 - Split PushService - make the separation more common for webSocket and http2. r=nsm, r=mt
dom/push/PushDB.jsm
dom/push/PushService.jsm
dom/push/PushServiceWebSocket.jsm
--- a/dom/push/PushDB.jsm
+++ b/dom/push/PushDB.jsm
@@ -11,28 +11,33 @@ let gDebuggingEnabled = false;
 function debug(s) {
   if (gDebuggingEnabled) {
     dump("-*- PushDB.jsm: " + s + "\n");
   }
 }
 
 const Cu = Components.utils;
 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.importGlobalProperties(["indexedDB"]);
 
+const prefs = new Preferences("dom.push.");
+
 this.EXPORTED_SYMBOLS = ["PushDB"];
 
 this.PushDB = function PushDB(dbName, dbVersion, dbStoreName, schemaFunction) {
   debug("PushDB()");
   this._dbStoreName = dbStoreName;
   this._schemaFunction = schemaFunction;
 
   // set the indexeddb database
   this.initDBHelper(dbName, dbVersion,
                     [dbStoreName]);
+  gDebuggingEnabled = prefs.get("debug");
+  prefs.observe("debug", this);
 };
 
 this.PushDB.prototype = {
   __proto__: IndexedDBHelper.prototype,
 
   upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
     if (this._schemaFunction) {
       this._schemaFunction(aTransaction, aDb, aOldVersion, aNewVersion, this);
@@ -109,17 +114,17 @@ this.PushDB.prototype = {
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
           aTxn.result = undefined;
 
           let index = aStore.index("pushEndpoint");
           index.get(aPushEndpoint).onsuccess = function setTxnResult(aEvent) {
             aTxn.result = aEvent.target.result;
             debug("Fetch successful " + aEvent.target.result);
-          }
+          };
         },
         resolve,
         reject
       )
     );
   },
 
   getByChannelID: function(aChannelID) {
@@ -130,17 +135,17 @@ this.PushDB.prototype = {
         "readonly",
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
           aTxn.result = undefined;
 
           aStore.get(aChannelID).onsuccess = function setTxnResult(aEvent) {
             aTxn.result = aEvent.target.result;
             debug("Fetch successful " + aEvent.target.result);
-          }
+          };
         },
         resolve,
         reject
       )
     );
   },
 
 
@@ -153,17 +158,17 @@ this.PushDB.prototype = {
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
           aTxn.result = undefined;
 
           let index = aStore.index("scope");
           index.get(aScope).onsuccess = function setTxnResult(aEvent) {
             aTxn.result = aEvent.target.result;
             debug("Fetch successful " + aEvent.target.result);
-          }
+          };
         },
         resolve,
         reject
       )
     );
   },
 
   getAllChannelIDs: function() {
@@ -171,17 +176,17 @@ this.PushDB.prototype = {
 
     return new Promise((resolve, reject) =>
       this.newTxn(
         "readonly",
         this._dbStoreName,
         function txnCb(aTxn, aStore) {
           aStore.mozGetAll().onsuccess = function(event) {
             aTxn.result = event.target.result;
-          }
+          };
         },
         resolve,
         reject
       )
     );
   },
 
   drop: function() {
@@ -195,12 +200,13 @@ this.PushDB.prototype = {
           aStore.clear();
         },
         resolve,
         reject
       )
     );
   },
 
-  observeDebug: function(enabled) {
-    gDebuggingEnabled = enabled;
+  observe: function observe(aSubject, aTopic, aData) {
+    if ((aTopic == "nsPref:changed") && (aData == "dom.push.debug"))
+      gDebuggingEnabled = prefs.get("debug");
   }
 };
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -4,256 +4,559 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Don't modify this, instead set dom.push.debug.
 let gDebuggingEnabled = true;
 
 function debug(s) {
-  if (gDebuggingEnabled)
+  if (gDebuggingEnabled) {
     dump("-*- PushService.jsm: " + s + "\n");
+  }
 }
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 const {PushServiceWebSocket} = Cu.import("resource://gre/modules/PushServiceWebSocket.jsm");
 
+// Currently supported protocols: WebSocket.
+const CONNECTION_PROTOCOLS = [PushServiceWebSocket];
+
 XPCOMUtils.defineLazyModuleGetter(this, "AlarmService",
                                   "resource://gre/modules/AlarmService.jsm");
 
 this.EXPORTED_SYMBOLS = ["PushService"];
 
 const prefs = new Preferences("dom.push.");
 // Set debug first so that all debugging actually works.
 gDebuggingEnabled = prefs.get("debug");
 
-const kPUSHWSDB_DB_NAME = "pushapi";
-const kPUSHWSDB_DB_VERSION = 1; // Change this if the IndexedDB format changes
-const kPUSHWSDB_STORE_NAME = "pushapi";
-
 const kCHILD_PROCESS_MESSAGES = ["Push:Register", "Push:Unregister",
                                  "Push:Registration"];
 
-var upgradeSchemaWS = function(aTransaction, aDb, aOldVersion, aNewVersion, aDbInstance) {
-  debug("upgradeSchemaWS()");
-
-  let objectStore = aDb.createObjectStore(aDbInstance._dbStoreName,
-                                          { keyPath: "channelID" });
-
-  // index to fetch records based on endpoints. used by unregister
-  objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
-
-  // index to fetch records per scope, so we can identify endpoints
-  // associated with an app.
-  objectStore.createIndex("scope", "scope", { unique: true });
-};
+const PUSH_SERVICE_UNINIT = 0;
+const PUSH_SERVICE_INIT = 1; // No serverURI
+const PUSH_SERVICE_ACTIVATING = 2;//activating db
+const PUSH_SERVICE_CONNECTION_DISABLE = 3;
+const PUSH_SERVICE_ACTIVE_OFFLINE = 4;
+const PUSH_SERVICE_RUNNING = 5;
 
 /**
- * The implementation of the SimplePush system. This runs in the B2G parent
- * process and is started on boot. It uses WebSockets to communicate with the
- * server and PushDB (IndexedDB) for persistence.
+ * State is change only in couple of functions:
+ *   init - change state to PUSH_SERVICE_INIT if state was PUSH_SERVICE_UNINIT
+ *   changeServerURL - change state to PUSH_SERVICE_ACTIVATING if serverURL
+ *                     present or PUSH_SERVICE_INIT if not present.
+ *   changeStateConnectionEnabledEvent - it is call on pref change or during
+ *                                       the service activation and it can
+ *                                       change state to
+ *                                       PUSH_SERVICE_CONNECTION_DISABLE
+ *   changeStateOfflineEvent - it is called when offline state changes or during
+ *                             the service activation and it change state to
+ *                             PUSH_SERVICE_ACTIVE_OFFLINE or
+ *                             PUSH_SERVICE_RUNNING.
+ *   uninit - change state to PUSH_SERVICE_UNINIT.
+ **/
+
+// This is for starting and stopping service.
+const STARTING_SERVICE_EVENT = 0;
+const CHANGING_SERVICE_EVENT = 1;
+const STOPPING_SERVICE_EVENT = 2;
+const UNINIT_EVENT = 3;
+
+/**
+ * The implementation of the push system. It uses WebSockets
+ * (PushServiceWebSocket) to communicate with the server and PushDB (IndexedDB)
+ * for persistence.
  */
 this.PushService = {
   _service: null,
+  _state: PUSH_SERVICE_UNINIT,
+  _db: null,
+  _options: null,
+
+  // When serverURI changes (this is used for testing), db is cleaned up and a
+  // a new db is started. This events must be sequential.
+  _serverURIProcessQueue: null,
+  _serverURIProcessEnqueue: function(op) {
+    if (!this._serverURIProcessQueue) {
+      this._serverURIProcessQueue = Promise.resolve();
+    }
+
+    this._serverURIProcessQueue = this._serverURIProcessQueue
+                                    .then(op)
+                                    .catch(_ => {});
+  },
+
+  // Pending request. If a worker try to register for the same scope again, do
+  // not send a new registration request. Therefore we need queue of pending
+  // register requests. This is the list of scopes which pending registration.
+  _pendingRegisterRequest: {},
+  _notifyActivated: null,
+  _activated: null,
+  _checkActivated: function() {
+    if (this._state < PUSH_SERVICE_ACTIVATING) {
+      return Promise.reject({state: 0, error: "Service not active"});
+    } else if (this._state > PUSH_SERVICE_ACTIVATING) {
+      return Promise.resolve();
+    } else {
+      return (this._activated) ? this._activated :
+                                 this._activated = new Promise((res, rej) =>
+                                   this._notifyActivated = {resolve: res,
+                                                            reject: rej});
+    }
+  },
+
+  _setState: function(aNewState) {
+    debug("new state: " + aNewState + " old state: " + this._state);
+
+    if (this._state == aNewState) {
+      return;
+    }
+
+    if (this._state == PUSH_SERVICE_ACTIVATING) {
+      // It is not important what is the new state as soon as we leave
+      // PUSH_SERVICE_ACTIVATING
+      this._state = aNewState;
+      if (this._notifyActivated) {
+        if (aNewState < PUSH_SERVICE_ACTIVATING) {
+          this._notifyActivated.reject({state: 0, error: "Service not active"});
+        } else {
+          this._notifyActivated.resolve();
+        }
+      }
+      this._notifyActivated = null;
+      this._activated = null;
+    }
+    this._state = aNewState;
+  },
+
+  _changeStateOfflineEvent: function(offline, calledFromConnEnabledEvent) {
+    debug("changeStateOfflineEvent: " + offline);
+
+    if (this._state < PUSH_SERVICE_ACTIVE_OFFLINE &&
+        this._state != PUSH_SERVICE_ACTIVATING &&
+        !calledFromConnEnabledEvent) {
+      return;
+    }
+
+    if (offline) {
+      if (this._state == PUSH_SERVICE_RUNNING) {
+        this._service.disconnect();
+      }
+      this._setState(PUSH_SERVICE_ACTIVE_OFFLINE);
+    } else {
+      if (this._state == PUSH_SERVICE_RUNNING) {
+        // PushService was not in the offline state, but got notification to
+        // go online (a offline notification has not been sent).
+        // Disconnect first.
+        this._service.disconnect();
+      }
+      this._db.getAllChannelIDs()
+        .then(channelIDs => {
+          if (channelIDs.length > 0) {
+            // if there are request waiting
+            this._service.connect(channelIDs);
+          }
+        });
+      this._setState(PUSH_SERVICE_RUNNING);
+    }
+  },
+
+  _changeStateConnectionEnabledEvent: function(enabled) {
+    debug("changeStateConnectionEnabledEvent: " + enabled);
+
+    if (this._state < PUSH_SERVICE_CONNECTION_DISABLE &&
+        this._state != PUSH_SERVICE_ACTIVATING) {
+      return;
+    }
+
+    if (enabled) {
+      this._changeStateOfflineEvent(Services.io.offline, true);
+    } else {
+      if (this._state == PUSH_SERVICE_RUNNING) {
+        this._service.disconnect();
+      }
+      this._setState(PUSH_SERVICE_CONNECTION_DISABLE);
+    }
+  },
 
   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.
+       * 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. */
-        this._service.observeNetworkChange(aSubject, aTopic, aData);
+        this._changeStateOfflineEvent(aData === "offline", false);
         break;
+
       case "nsPref:changed":
         if (aData == "dom.push.serverURL") {
           debug("dom.push.serverURL changed! websocket. new value " +
                 prefs.get("serverURL"));
-          this._service.shutdownService();
+          this._serverURIProcessEnqueue(_ =>
+            this._changeServerURL(prefs.get("serverURL"),
+                                  CHANGING_SERVICE_EVENT)
+          );
+
         } else if (aData == "dom.push.connection.enabled") {
-          this._service.observePushConnectionPref(prefs.get("connection.enabled"));
+          this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"));
+
         } else if (aData == "dom.push.debug") {
           gDebuggingEnabled = prefs.get("debug");
-          this._service.observeDebug(gDebuggingEnabled);
-          this._db.observeDebug(gDebuggingEnabled);
         }
         break;
-      case "timer-callback":
-        this._service.observeTimer(aSubject, aTopic, aData);
-        break;
+
       case "webapps-clear-data":
         debug("webapps-clear-data");
 
-        let data = aSubject.QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
+        let data = aSubject
+                     .QueryInterface(Ci.mozIApplicationClearPrivateDataParams);
         if (!data) {
-          debug("webapps-clear-data: Failed to get information about application");
+          debug("webapps-clear-data: Failed to get information about " +
+                "application");
           return;
         }
 
-        // TODO 1149274.  We should support site permissions as well as a way to go from manifest
-        // url to 'all scopes registered for push in this app'
+        // TODO 1149274.  We should support site permissions as well as a way to
+        // go from manifest url to 'all scopes registered for push in this app'
         let appsService = Cc["@mozilla.org/AppsService;1"]
                             .getService(Ci.nsIAppsService);
         let scope = appsService.getScopeByLocalId(data.appId);
         if (!scope) {
           debug("webapps-clear-data: No scope found for " + data.appId);
           return;
         }
 
         this._db.getByScope(scope)
           .then(record =>
-            this._db.delete(records.channelID)
-              .then(_ =>
-                this._service.unregister(records),
-                err => {
-                  debug("webapps-clear-data: " + scope +
-                        " Could not delete entry " + records.channelID);
-
-                this._service.unregister(records)
-                throw "Database error";
-              })
-          , _ => {
+            Promise.all([
+              this._db.delete(record.channelID),
+              this._sendRequest("unregister", record)
+            ])
+          ).catch(_ => {
             debug("webapps-clear-data: Error in getByScope(" + scope + ")");
           });
 
         break;
     }
   },
 
-  // utility function used to add/remove observers in init() and shutdown()
+  // utility function used to add/remove observers in startObservers() and
+  // stopObservers()
   getNetworkStateChangeEventName: function() {
     try {
       Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager);
       return "network-active-changed";
     } catch (e) {
       return "network:offline-status-changed";
     }
   },
 
+  _findService: function(serverURI) {
+    var uri;
+    var service;
+    if (serverURI) {
+      for (let connProtocol of CONNECTION_PROTOCOLS) {
+        uri = connProtocol.checkServerURI(serverURI);
+        if (uri) {
+          service = connProtocol;
+          break;
+        }
+      }
+    }
+    return [service, uri];
+  },
+
+  _changeServerURL: function(serverURI, event) {
+    debug("changeServerURL");
+
+    switch(event) {
+      case UNINIT_EVENT:
+        return this._stopService(event);
+
+      case STARTING_SERVICE_EVENT:
+      {
+        let [service, uri] = this._findService(serverURI);
+        if (!service) {
+          this._setState(PUSH_SERVICE_INIT);
+          return Promise.resolve();
+        }
+        return this._startService(service, uri, event)
+          .then(_ =>
+            this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"))
+          );
+      }
+      case CHANGING_SERVICE_EVENT:
+        let [service, uri] = this._findService(serverURI);
+        if (service) {
+          if (this._state == PUSH_SERVICE_INIT) {
+            this._setState(PUSH_SERVICE_ACTIVATING);
+            // The service has not been running - start it.
+            return this._startService(service, uri, STARTING_SERVICE_EVENT)
+              .then(_ =>
+                this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"))
+              );
+
+          } else {
+            this._setState(PUSH_SERVICE_ACTIVATING);
+            // If we already had running service - stop service, start the new
+            // one and check connection.enabled and offline state(offline state
+            // check is called in changeStateConnectionEnabledEvent function)
+            return this._stopService(CHANGING_SERVICE_EVENT)
+              .then(_ =>
+                 this._startService(service, uri, CHANGING_SERVICE_EVENT)
+              )
+              .then(_ =>
+                this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"))
+              );
+
+          }
+        } else {
+          if (this._state == PUSH_SERVICE_INIT) {
+            return Promise.resolve();
+
+          } else {
+            // The new serverUri is empty or misconfigured - stop service.
+            this._setState(PUSH_SERVICE_INIT);
+            return this._stopService(STOPPING_SERVICE_EVENT);
+          }
+        }
+    }
+  },
+
+  /**
+   * PushService initialization is divided into 4 parts:
+   * init() - start listening for xpcom-shutdown and serverURL changes.
+   *          state is change to PUSH_SERVICE_INIT
+   * startService() - if serverURL is present this function is called. It starts
+   *                  listening for broadcasted messages, starts db and
+   *                  PushService connection (WebSocket).
+   *                  state is change to PUSH_SERVICE_ACTIVATING.
+   * startObservers() - start other observers.
+   * changeStateConnectionEnabledEvent  - checks prefs and offline state.
+   *                                      It changes state to:
+   *                                        PUSH_SERVICE_RUNNING,
+   *                                        PUSH_SERVICE_ACTIVE_OFFLINE or
+   *                                        PUSH_SERVICE_CONNECTION_DISABLE.
+   */
   init: function(options = {}) {
     debug("init()");
-    if (this._started) {
+
+    if (this._state > PUSH_SERVICE_UNINIT) {
       return;
     }
 
+    this._setState(PUSH_SERVICE_ACTIVATING);
+
     var globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
-               .getService(Ci.nsIFrameScriptLoader);
+                     .getService(Ci.nsIFrameScriptLoader);
+
+    globalMM.loadFrameScript("chrome://global/content/PushServiceChildPreload.js",
+                             true);
 
-    globalMM.loadFrameScript("chrome://global/content/PushServiceChildPreload.js", true);
+    // Debugging
+    prefs.observe("debug", this);
+
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+
+    if (options.serverURI) {
+      // this is use for xpcshell test.
 
-    let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
-                 .getService(Ci.nsIMessageBroadcaster);
+      var uri;
+      var service;
+      for (let connProtocol of CONNECTION_PROTOCOLS) {
+        uri = connProtocol.checkServerURI(options.serverURI);
+        if (uri) {
+          service = connProtocol;
+          break;
+        }
+      }
+      if (!service) {
+        this._setState(PUSH_SERVICE_INIT);
+        return;
+      }
 
-    kCHILD_PROCESS_MESSAGES.forEach(function addMessage(msgName) {
-        ppmm.addMessageListener(msgName, this);
-    }.bind(this));
+      // Start service.
+      this._startService(service, uri, false, options);
+      // Before completing the activation check prefs. This will first check
+      // connection.enabled pref and then check offline state.
+      this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"));
+
+    } else {
+      // This is only used for testing. Different tests require connecting to
+      // slightly different URLs.
+      prefs.observe("serverURL", this);
+
+      this._serverURIProcessEnqueue(_ =>
+        this._changeServerURL(prefs.get("serverURL"), STARTING_SERVICE_EVENT));
+    }
+  },
+
+  _startObservers: function() {
+    debug("startObservers");
+
+    if (this._state != PUSH_SERVICE_ACTIVATING) {
+      return;
+    }
 
     this._alarmID = null;
 
-    Services.obs.addObserver(this, "xpcom-shutdown", false);
     Services.obs.addObserver(this, "webapps-clear-data", false);
 
     // On B2G the NetworkManager interface fires a network-active-changed
     // event.
     //
     // The "active network" is based on priority - i.e. Wi-Fi has higher
     // priority than data. The PushService should just use the preferred
     // network, and not care about all interface changes.
     // network-active-changed is not fired when the network goes offline, but
     // socket connections time out. The check for Services.io.offline in
-    // _beginWSSetup() prevents unnecessary retries.  When the network comes
-    // back online, network-active-changed is fired.
+    // PushServiceWebSocket._beginWSSetup() prevents unnecessary retries.  When
+    // the network comes back online, network-active-changed is fired.
     //
     // On non-B2G platforms, the offline-status-changed event is used to know
     // when to (dis)connect. It may not fire if the underlying OS changes
     // networks; in such a case we rely on timeout.
     //
     // On B2G both events fire, one after the other, when the network goes
     // online, so we explicitly check for the presence of NetworkManager and
     // don't add an observer for offline-status-changed on B2G.
     this._networkStateChangeEventName = this.getNetworkStateChangeEventName();
     Services.obs.addObserver(this, this._networkStateChangeEventName, false);
 
-    // This is only used for testing. Different tests require connecting to
-    // slightly different URLs.
-    prefs.observe("serverURL", this);
     // Used to monitor if the user wishes to disable Push.
     prefs.observe("connection.enabled", this);
-    // Debugging
-    prefs.observe("debug", this);
+  },
+
+  _startService: function(service, serverURI, event, options = {}) {
+    debug("startService");
+
+    if (this._state != PUSH_SERVICE_ACTIVATING) {
+      return;
+    }
+
+    if (event != CHANGING_SERVICE_EVENT) {
+      // if only serverURL is changed we can keep listening for broadcast
+      // messages and queue them.
+      let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+                   .getService(Ci.nsIMessageBroadcaster);
+
+      kCHILD_PROCESS_MESSAGES.forEach(msgName =>
+        ppmm.addMessageListener(msgName, this)
+      );
+    }
+
+    this._service = service;
+
     this._db = options.db;
     if (!this._db) {
-      this._db = new PushDB(kPUSHWSDB_DB_NAME, kPUSHWSDB_DB_VERSION, kPUSHWSDB_STORE_NAME, upgradeSchemaWS);
+      this._db = this._service.newPushDB();
     }
-    this._service = PushServiceWebSocket;
-    this._service.init(options, this);
-    this._service.startListeningIfChannelsPresent();
-    this._started = true;
+
+    this._service.init(options, this, serverURI);
+    this._startObservers();
+    return Promise.resolve();
   },
 
-  uninit: function() {
-    if (!this._started)
+  /**
+   * PushService uninitialization is divided into 3 parts:
+   * stopObservers() - stot observers started in startObservers.
+   * stopService() - It stops listening for broadcasted messages, stops db and
+   *                 PushService connection (WebSocket).
+   *                 state is changed to PUSH_SERVICE_INIT.
+   * uninit() - stop listening for xpcom-shutdown and serverURL changes.
+   *            state is change to PUSH_SERVICE_UNINIT
+   */
+  _stopService: function(event) {
+    debug("stopService");
+
+    if (this._state < PUSH_SERVICE_ACTIVATING) {
       return;
+    }
 
-    debug("uninit()");
+    this.stopAlarm();
+    this._stopObservers();
+
+    if (event != CHANGING_SERVICE_EVENT) {
+      let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"]
+                   .getService(Ci.nsIMessageBroadcaster);
+
+      kCHILD_PROCESS_MESSAGES.forEach(
+        msgName => ppmm.removeMessageListener(msgName, this)
+      );
+    }
+    this._service.disconnect();
+    this._service.uninit();
+
+    if (!this._db) {
+      return Promise.resolve();
+    }
+    if (event == UNINIT_EVENT) {
+      // If it is uninitialized just close db.
+      this._db.close();
+      this._db = null;
+      return Promise.resolve();
+    }
+
+    return this.dropRegistrations()
+       .then(_ => {
+         this._db.close();
+         this._db = null;
+       }, err => {
+         this._db.close();
+         this._db = null;
+       });
+  },
+
+  _stopObservers: function() {
+    debug("stopObservers()");
+
+    if (this._state < PUSH_SERVICE_ACTIVATING) {
+      return;
+    }
 
     prefs.ignore("debug", this);
     prefs.ignore("connection.enabled", this);
-    prefs.ignore("serverURL", this);
+
     Services.obs.removeObserver(this, this._networkStateChangeEventName);
     Services.obs.removeObserver(this, "webapps-clear-data", false);
-    Services.obs.removeObserver(this, "xpcom-shutdown", false);
-
-    // At this point, profile-change-net-teardown has already fired, so the
-    // WebSocket has been closed with NS_ERROR_ABORT (if it was up) and will
-    // try to reconnect. Stop the timer.
-    this.stopAlarm();
-
-    if (this._db) {
-      this._db.close();
-      this._db = null;
-    }
-
-    this._service.uninit();
-
-    this._started = false;
-    debug("shutdown complete!");
   },
 
-  getServerURI: function() {
-    let serverURL = prefs.get("serverURL");
-    if (!serverURL) {
-      debug("No dom.push.serverURL found!");
+  uninit: function() {
+    debug("uninit()");
+
+    if (this._state == PUSH_SERVICE_UNINIT) {
       return;
     }
 
-    let uri;
-    try {
-      uri = Services.io.newURI(serverURL, null, null);
-    } catch(e) {
-      debug("Error creating valid URI from dom.push.serverURL (" +
-            serverURL + ")");
-      return;
-    }
-    return uri;
+    this._setState(PUSH_SERVICE_UNINIT);
+
+    prefs.ignore("serverURL", this);
+    Services.obs.removeObserver(this, "xpcom-shutdown", false);
+
+    this._serverURIProcessEnqueue(_ =>
+            this._changeServerURL("", UNINIT_EVENT));
+    debug("shutdown complete!");
   },
 
   /** |delay| should be in milliseconds. */
   setAlarm: function(delay) {
     // Bug 909270: Since calls to AlarmService.add() are async, calls must be
     // 'queued' to ensure only one alarm is ever active.
     if (this._settingAlarm) {
         // onSuccess will handle the set. Overwriting the variable enforces the
@@ -267,50 +570,59 @@ this.PushService = {
     this.stopAlarm();
 
     this._settingAlarm = true;
     AlarmService.add(
       {
         date: new Date(Date.now() + delay),
         ignoreTimezone: true
       },
-      this._service.onAlarmFired.bind(this._service),
-      function onSuccess(alarmID) {
+      () => {
+        if (this._service) {
+          this._service.onAlarmFired();
+        }
+      }, (alarmID) => {
         this._alarmID = alarmID;
         debug("Set alarm " + delay + " in the future " + this._alarmID);
         this._settingAlarm = false;
 
         if (this._waitingForAlarmSet) {
           this._waitingForAlarmSet = false;
           this.setAlarm(this._queuedAlarmDelay);
         }
-      }.bind(this)
-    )
+      }
+    );
   },
 
   stopAlarm: function() {
     if (this._alarmID !== null) {
       debug("Stopped existing alarm " + this._alarmID);
       AlarmService.remove(this._alarmID);
       this._alarmID = null;
     }
   },
 
+  dropRegistrations: function() {
+    return this._notifyAllAppsRegister()
+      .then(_ => this._db.drop());
+  },
+
   // Fires a push-register system message to all applications that have
   // registration.
-  notifyAllAppsRegister: function() {
+  _notifyAllAppsRegister: function() {
     debug("notifyAllAppsRegister()");
     // records are objects describing the registration as stored in IndexedDB.
     return this._db.getAllChannelIDs()
       .then(records => {
         let scopes = new Set();
         for (let record of records) {
           scopes.add(record.scope);
         }
-        let globalMM = Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager);
+        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
           );
 
@@ -319,165 +631,144 @@ this.PushService = {
             scope: scope
           };
 
           globalMM.broadcastAsyncMessage('pushsubscriptionchange', data);
         }
       });
   },
 
-  notifyApp: function(aPushRecord) {
+  receivedPushMessage: function(aPushRecord, message) {
+    this._updatePushRecord(aPushRecord)
+      .then(_ => this._notifyApp(aPushRecord, message));
+  },
+
+  _notifyApp: function(aPushRecord, message) {
     if (!aPushRecord || !aPushRecord.scope) {
-      debug("notifyApp() something is undefined.  Dropping notification: "
-        + JSON.stringify(aPushRecord) );
+      debug("notifyApp() something is undefined.  Dropping notification: " +
+        JSON.stringify(aPushRecord) );
       return;
     }
 
     debug("notifyApp() " + aPushRecord.scope);
     let scopeURI = Services.io.newURI(aPushRecord.scope, null, null);
     // Notify XPCOM observers.
     let notification = Cc["@mozilla.org/push/ObserverNotification;1"]
                          .createInstance(Ci.nsIPushObserverNotification);
     notification.pushEndpoint = aPushRecord.pushEndpoint;
     notification.version = aPushRecord.version;
-    notification.data = "";
+    notification.data = message;
     notification.lastPush = aPushRecord.lastPush;
     notification.pushCount = aPushRecord.pushCount;
 
     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.")
+    if (Services.perms.testExactPermission(scopeURI, "push") !=
+        Ci.nsIPermissionManager.ALLOW_ACTION) {
+      debug("Does not have permission for push.");
       return;
     }
 
     // TODO data.
     let data = {
-      payload: "Short as life is, we make it still shorter by the careless waste of time.",
+      payload: message,
       originAttributes: {}, // TODO bug 1166350
       scope: aPushRecord.scope
     };
 
     let globalMM = Cc['@mozilla.org/globalmessagemanager;1']
                  .getService(Ci.nsIMessageListenerManager);
     globalMM.broadcastAsyncMessage('push', data);
   },
 
-  updatePushRecord: function(aPushRecord) {
+  _updatePushRecord: function(aPushRecord) {
     debug("updatePushRecord()");
     return this._db.put(aPushRecord);
   },
 
   getByChannelID: function(aChannelID) {
-    return this._db.getByChannelID(aChannelID)
+    return this._db.getByChannelID(aChannelID);
   },
 
   getAllChannelIDs: function() {
-    return this._db.getAllChannelIDs()
+    return this._db.getAllChannelIDs();
   },
 
-  dropRegistration: function() {
-    return this._db.drop();
+  _sendRequest(action, aRecord) {
+    if (this._state == PUSH_SERVICE_CONNECTION_DISABLE) {
+      return Promise.reject({state: 0, error: "Service not active"});
+    } else if (this._state == PUSH_SERVICE_ACTIVE_OFFLINE) {
+      return Promise.reject({state: 0, error: "NetworkError"});
+    }
+    return this._service.request(action, aRecord);
   },
 
-    /**
+  /**
    * 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) {
+  _registerWithServer: function(aPageRecord) {
     debug("registerWithServer()");
 
-    return this._service.register(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.
-    return uuidGenerator.generateUUID().toString().slice(1, -1);
+    return this._sendRequest("register", aPageRecord)
+      .then(pushRecord => this._onRegisterSuccess(pushRecord),
+            err => this._onRegisterError(err))
+      .then(pushRecord => {
+        if (this._pendingRegisterRequest[aPageRecord.scope]) {
+          delete this._pendingRegisterRequest[aPageRecord.scope];
+        }
+        return pushRecord;
+      }, err => {
+        if (this._pendingRegisterRequest[aPageRecord.scope]) {
+          delete this._pendingRegisterRequest[aPageRecord.scope];
+        }
+        throw err;
+     });
   },
 
   _register: function(aPageRecord) {
-    return this._db.getByScope(aPageRecord.scope)
+    return this._checkActivated()
+      .then(_ => this._db.getByScope(aPageRecord.scope))
       .then(pushRecord => {
-        if (pushRecord == null) {
-          let channelID = this._generateID();
-          return this._registerWithServer(channelID, aPageRecord);
+        if (pushRecord === undefined) {
+          if (this._pendingRegisterRequest[aPageRecord.scope]) {
+            return this._pendingRegisterRequest[aPageRecord.scope];
+          }
+          return this._pendingRegisterRequest[aPageRecord.scope] = this._registerWithServer(aPageRecord);
         }
         return pushRecord;
-      },
-      error => {
+      }, error => {
         debug("getByScope failed");
-        throw "Database error";
-      }
-    );
+        throw error;
+      });
   },
 
   /**
    * Exceptions thrown in _onRegisterSuccess are caught by the promise obtained
-   * from _sendRequest, causing the promise to be rejected instead.
+   * from _service.request, causing the promise to be rejected instead.
    */
-  _onRegisterSuccess: function(aPageRecord, generatedChannelID, data) {
+  _onRegisterSuccess: function(aRecord) {
     debug("_onRegisterSuccess()");
 
-    if (typeof data.channelID !== "string") {
-      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);
-      throw "Server sent 200 status code but different channelID";
-    }
-
-    try {
-      Services.io.newURI(data.pushEndpoint, null, null);
-    }
-    catch (e) {
-      debug("Invalid pushEndpoint " + data.pushEndpoint);
-      throw "Invalid pushEndpoint " + data.pushEndpoint;
-    }
-
-    let record = {
-      channelID: data.channelID,
-      pushEndpoint: data.pushEndpoint,
-      pageURL: aPageRecord.pageURL,
-      scope: aPageRecord.scope,
-      pushCount: 0,
-      lastPush: 0,
-      version: null
-    };
-
-    debug("scope in _onRegisterSuccess: " + aPageRecord.scope)
-
-    return this.updatePushRecord(record)
-      .then(
-        function() {
-          return record;
-        },
-        function(error) {
-          // Unable to save.
-          this._service.unregister(record);
-          throw error;
-        }.bind(this)
-      );
+    return this._updatePushRecord(aRecord)
+      .then(_ => aRecord, error => {
+        // Unable to save.
+        this._sendRequest("unregister", aRecord);
+        throw error;
+      });
   },
 
   /**
    * Exceptions thrown in _onRegisterError are caught by the promise obtained
-   * from _sendRequest, causing the promise to be rejected instead.
+   * from _service.request, causing the promise to be rejected instead.
    */
   _onRegisterError: function(reply) {
     debug("_onRegisterError()");
     if (!reply.error) {
       debug("Called without valid error message!");
       throw "Registration error";
     }
     throw reply.error;
@@ -488,38 +779,37 @@ this.PushService = {
 
     if (kCHILD_PROCESS_MESSAGES.indexOf(aMessage.name) == -1) {
       debug("Invalid message from child " + aMessage.name);
       return;
     }
 
     let mm = aMessage.target.QueryInterface(Ci.nsIMessageSender);
     let json = aMessage.data;
+
     this[aMessage.name.slice("Push:".length).toLowerCase()](json, mm);
   },
 
   register: function(aPageRecord, aMessageManager) {
     debug("register(): " + JSON.stringify(aPageRecord));
 
-    this._register(aPageRecord).then(
-      function(aPageRecord, aMessageManager, pushRecord) {
+    this._register(aPageRecord)
+      .then(pushRecord => {
         let message = {
           requestID: aPageRecord.requestID,
           pushEndpoint: pushRecord.pushEndpoint
         };
         aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
-      }.bind(this, aPageRecord, aMessageManager),
-      function(error) {
+      }, error => {
         let message = {
           requestID: aPageRecord.requestID,
           error
         };
         aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
-      }
-    );
+      });
   },
 
   /**
    * Called on message from the child process.
    *
    * Why is the record being deleted from the local database before the server
    * is told?
    *
@@ -540,94 +830,90 @@ this.PushService = {
    * 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) {
     debug("unregisterWithServer()");
 
     if (!aPageRecord.scope) {
-      return Promise.reject("NotFoundError");
+      return Promise.reject({state: 0, error: "NotFoundError"});
     }
 
-    return this._db.getByScope(aPageRecord.scope)
+    return this._checkActivated()
+      .then(_ => this._db.getByScope(aPageRecord.scope))
       .then(record => {
         // If the endpoint didn't exist, let's just fail.
         if (record === undefined) {
           throw "NotFoundError";
         }
 
-        this._db.delete(record.channelID)
-          .then(_ =>
-            // Let's be nice to the server and try to inform it, but we don't care
-            // about the reply.
-            this._service.unregister(record)
-          );
+        // Let's be nice to the server and try to inform it, but we don't care
+        // about the reply.
+        this._sendRequest("unregister", record);
+        this._db.delete(record.channelID);
       });
   },
 
   unregister: function(aPageRecord, aMessageManager) {
     debug("unregister() " + JSON.stringify(aPageRecord));
 
-    this._unregister(aPageRecord).then(
-      () => {
+    this._unregister(aPageRecord)
+      .then(_ =>
         aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
           requestID: aPageRecord.requestID,
           pushEndpoint: aPageRecord.pushEndpoint
-        });
-      },
-      error => {
+        }), error =>
         aMessageManager.sendAsyncMessage("PushService:Unregister:KO", {
           requestID: aPageRecord.requestID,
           error
-        });
-      }
-    );
+        })
+      );
   },
 
   _clearAll: function _clearAll() {
-    return this._db.clearAll();
+    return this._checkActivated()
+      .then(_ => this._db.clearAll())
+      .catch(_ => {
+        return Promise.resolve();
+      });
   },
 
   /**
    * Called on message from the child process
    */
   _registration: function(aPageRecord) {
+    debug("_registration()");
     if (!aPageRecord.scope) {
-      return Promise.reject("Database error");
+      return Promise.reject({state: 0, error: "Database error"});
     }
 
-    return this._db.getByScope(aPageRecord.scope)
-        .then(pushRecord => {
-          let registration = null;
-          if (pushRecord) {
-            registration = {
-              pushEndpoint: pushRecord.pushEndpoint,
-              version: pushRecord.version,
-              lastPush: pushRecord.lastPush,
-              pushCount: pushRecord.pushCount
-            };
-          }
-          return registration;
-        }, _ => {
-          throw "Database error";
-        });
+    return this._checkActivated()
+      .then(_ => this._db.getByScope(aPageRecord.scope))
+      .then(pushRecord => {
+        if (!pushRecord) {
+          return null;
+        }
+        return {
+          pushEndpoint: pushRecord.pushEndpoint,
+          version: pushRecord.version,
+          lastPush: pushRecord.lastPush,
+          pushCount: pushRecord.pushCount
+        };
+      });
   },
 
   registration: function(aPageRecord, aMessageManager) {
     debug("registration()");
 
-    return this._registration(aPageRecord).then(
-      registration => {
+    return this._registration(aPageRecord)
+      .then(registration =>
         aMessageManager.sendAsyncMessage("PushService:Registration:OK", {
           requestID: aPageRecord.requestID,
           registration
-        });
-      },
-      error => {
+        }), error =>
         aMessageManager.sendAsyncMessage("PushService:Registration:KO", {
           requestID: aPageRecord.requestID,
           error
-        });
-      }
-    );
+        })
+      );
   }
 };
--- a/dom/push/PushServiceWebSocket.jsm
+++ b/dom/push/PushServiceWebSocket.jsm
@@ -5,16 +5,17 @@
 
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
@@ -22,17 +23,22 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsIDNSService");
 
 #ifdef MOZ_B2G
 XPCOMUtils.defineLazyServiceGetter(this, "gPowerManagerService",
                                    "@mozilla.org/power/powermanagerservice;1",
                                    "nsIPowerManagerService");
 #endif
 
-var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
+var threadManager = Cc["@mozilla.org/thread-manager;1"]
+                      .getService(Ci.nsIThreadManager);
+
+const kPUSHWSDB_DB_NAME = "pushapi";
+const kPUSHWSDB_DB_VERSION = 1; // Change this if the IndexedDB format changes
+const kPUSHWSDB_STORE_NAME = "pushapi";
 
 const kUDP_WAKEUP_WS_STATUS_CODE = 4774;  // WebSocket Close status code sent
                                           // by server to signal that it can
                                           // wake client up using UDP.
 
 const kWS_MAX_WENTDOWN = 2;
 
 // 1 minute is the least allowed ping interval
@@ -41,62 +47,70 @@ const kWS_MIN_PING_INTERVAL = 60000;
 const prefs = new Preferences("dom.push.");
 
 this.EXPORTED_SYMBOLS = ["PushServiceWebSocket"];
 
 // Don't modify this, instead set dom.push.debug.
 let gDebuggingEnabled = true;
 
 function debug(s) {
-  if (gDebuggingEnabled)
+  if (gDebuggingEnabled) {
     dump("-*- PushServiceWebSocket.jsm: " + s + "\n");
+  }
 }
 
+// Set debug first so that all debugging actually works.
+gDebuggingEnabled = prefs.get("debug");
+
 /**
  * A proxy between the PushService and the WebSocket. The listener is used so
  * that the PushService can silence messages from the WebSocket by setting
  * PushWebSocketListener._pushService to null. This is required because
  * a WebSocket can continue to send messages or errors after it has been
  * closed but the PushService may not be interested in these. It's easier to
  * stop listening than to have checks at specific points.
  */
-this.PushWebSocketListener = function(pushService) {
+var PushWebSocketListener = function(pushService) {
   this._pushService = pushService;
-}
+};
 
-this.PushWebSocketListener.prototype = {
+PushWebSocketListener.prototype = {
   onStart: function(context) {
-    if (!this._pushService)
+    if (!this._pushService) {
         return;
+    }
     this._pushService._wsOnStart(context);
   },
 
   onStop: function(context, statusCode) {
-    if (!this._pushService)
+    if (!this._pushService) {
         return;
+    }
     this._pushService._wsOnStop(context, statusCode);
   },
 
   onAcknowledge: function(context, size) {
     // EMPTY
   },
 
   onBinaryMessageAvailable: function(context, message) {
     // EMPTY
   },
 
   onMessageAvailable: function(context, message) {
-    if (!this._pushService)
+    if (!this._pushService) {
         return;
+    }
     this._pushService._wsOnMessageAvailable(context, message);
   },
 
   onServerClose: function(context, aStatusCode, aReason) {
-    if (!this._pushService)
+    if (!this._pushService) {
         return;
+    }
     this._pushService._wsOnServerClose(context, aStatusCode, aReason);
   }
 };
 
 // websocket states
 // websocket is off
 const STATE_SHUT_DOWN = 0;
 // Websocket has been opened on client side, waiting for successful open.
@@ -104,104 +118,125 @@ const STATE_SHUT_DOWN = 0;
 const STATE_WAITING_FOR_WS_START = 1;
 // Websocket opened, hello sent, waiting for server reply (_handleHelloReply).
 const STATE_WAITING_FOR_HELLO = 2;
 // Websocket operational, handshake completed, begin protocol messaging.
 const STATE_READY = 3;
 
 this.PushServiceWebSocket = {
   _mainPushService: null,
+  _serverURI: null,
 
-  observeNetworkChange: function(aSubject, aTopic, aData) {
-    // 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()
-      this._udpServer = null;
-    }
+  upgradeSchema: function(aTransaction,
+                          aDb,
+                          aOldVersion,
+                          aNewVersion,
+                          aDbInstance) {
+    debug("upgradeSchemaWS()");
 
-    this._shutdownWS();
+    let objectStore = aDb.createObjectStore(aDbInstance._dbStoreName,
+                                            { keyPath: "channelID" });
 
-    // Try to connect if network-active-changed or the offline-status
-    // changed to online.
-    if (aTopic === "network-active-changed" || aData === "online") {
-      this.startListeningIfChannelsPresent();
-    }
+    // index to fetch records based on endpoints. used by unregister
+    objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
+
+    // index to fetch records per scope, so we can identify endpoints
+    // associated with an app.
+    objectStore.createIndex("scope", "scope", { unique: true });
   },
 
-  observePushConnectionPref: function(enabled) {
-    if (enabled) {
-      this.startListeningIfChannelsPresent();
-    } else {
-      this._shutdownWS();
-    }
+  newPushDB: function() {
+    return new PushDB(kPUSHWSDB_DB_NAME,
+                      kPUSHWSDB_DB_VERSION,
+                      kPUSHWSDB_STORE_NAME,
+                      this.upgradeSchema);
   },
 
-  observeDebug: function(enabled) {
-    gDebuggingEnabled = enabled;
-  },
-  shutdownService: function() {
+  disconnect: function() {
     this._shutdownWS();
   },
 
-  observeTimer: function(aSubject, aTopic, aData) {
-    if (aSubject == this._requestTimeoutTimer) {
-      if (Object.keys(this._pendingRequests).length == 0) {
-        this._requestTimeoutTimer.cancel();
-      }
+  observe: function(aSubject, aTopic, aData) {
+
+    switch (aTopic) {
+      case "nsPref:changed":
+        if (aData == "dom.push.debug") {
+          gDebuggingEnabled = prefs.get("debug");
+        }
+        break;
+    case "timer-callback":
+      if (aSubject == this._requestTimeoutTimer) {
+        if (Object.keys(this._pendingRequests).length === 0) {
+          this._requestTimeoutTimer.cancel();
+        }
 
-      // Set to true if at least one request timed out.
-      let requestTimedOut = false;
-      for (let channelID in this._pendingRequests) {
-        let duration = Date.now() - this._pendingRequests[channelID].ctime;
-        // If any of the registration requests time out, all the ones after it
-        // 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"});
+        // Set to true if at least one request timed out.
+        let requestTimedOut = false;
+        for (let channelID in this._pendingRequests) {
+          let duration = Date.now() - this._pendingRequests[channelID].ctime;
+          // If any of the registration requests time out, all the ones after it
+          // 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]
+              .reject({status: 0, error: "TimeoutError"});
 
-          delete this._pendingRequests[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);
-            }
+            delete this._pendingRequests[channelID];
           }
         }
+
+        // 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();
+        }
       }
+      break;
+    }
+  },
 
-      // 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();
-      }
+  checkServerURI: function(serverURL) {
+    if (!serverURL) {
+      debug("No dom.push.serverURL found!");
+      return;
     }
+
+    let uri;
+    try {
+      uri = Services.io.newURI(serverURL, null, null);
+    } catch(e) {
+      debug("Error creating valid URI from dom.push.serverURL (" +
+            serverURL + ")");
+      return null;
+    }
+
+    if (uri.scheme !== "wss") {
+      debug("Unsupported websocket scheme " + uri.scheme);
+      return null;
+    }
+    return uri;
   },
 
   get _UAID() {
     return prefs.get("userAgentID");
   },
 
   set _UAID(newID) {
     if (typeof(newID) !== "string") {
       debug("Got invalid, non-string UAID " + newID +
             ". Not updating userAgentID");
       return;
     }
     debug("New _UAID: " + newID);
     prefs.set("userAgentID", newID);
   },
 
-  // keeps requests buffered if the websocket disconnects or is not connected
-  _requestQueue: [],
   _ws: null,
   _pendingRequests: {},
   _currentState: STATE_SHUT_DOWN,
   _requestTimeout: 0,
   _requestTimeoutTimer: null,
   _retryFailCount: 0,
 
   /**
@@ -266,20 +301,21 @@ this.PushServiceWebSocket = {
       debug("No WebSocket initialized. Cannot send a message.");
       return;
     }
     msg = JSON.stringify(msg);
     debug("Sending message: " + msg);
     this._ws.sendMsg(msg);
   },
 
-  init: function(options, mainPushService) {
+  init: function(options, mainPushService, serverURI) {
     debug("init()");
 
     this._mainPushService = mainPushService;
+    this._serverURI = serverURI;
 
     // Override the default WebSocket factory function. The returned object
     // must be null or satisfy the nsIWebSocketChannel interface. Used by
     // the tests to provide a mock WebSocket implementation.
     if (options.makeWebSocket) {
       this._makeWebSocket = options.makeWebSocket;
     }
 
@@ -293,33 +329,46 @@ this.PushServiceWebSocket = {
     this._networkInfo = options.networkInfo;
     if (!this._networkInfo) {
       this._networkInfo = PushNetworkInfo;
     }
 
     this._requestTimeout = prefs.get("requestTimeout");
     this._adaptiveEnabled = prefs.get('adaptive.enabled');
     this._upperLimit = prefs.get('adaptive.upperLimit');
+    gDebuggingEnabled = prefs.get("debug");
+    prefs.observe("debug", this);
   },
 
   _shutdownWS: function() {
     debug("shutdownWS()");
     this._currentState = STATE_SHUT_DOWN;
     this._willBeWokenUpByUDP = false;
 
-    if (this._wsListener)
+    if (this._wsListener) {
       this._wsListener._pushService = null;
+    }
     try {
         this._ws.close(0, null);
     } catch (e) {}
     this._ws = null;
 
     this._waitingForPong = false;
-    this._mainPushService.stopAlarm();
+    if (this._mainPushService) {
+      this._mainPushService.stopAlarm();
+    } else {
+      dump("This should not happend");
+    }
+
     this._cancelPendingRequests();
+
+    if (this._notifyRequestQueue) {
+      this._notifyRequestQueue();
+      this._notifyRequestQueue = null;
+    }
   },
 
   uninit: function() {
 
     if (this._udpServer) {
       this._udpServer.close();
       this._udpServer = null;
     }
@@ -361,17 +410,21 @@ this.PushServiceWebSocket = {
     // Calculate new timeout, but cap it to pingInterval.
     let retryTimeout = prefs.get("retryBaseInterval") *
                        Math.pow(2, this._retryFailCount);
     retryTimeout = Math.min(retryTimeout, prefs.get("pingInterval"));
 
     this._retryFailCount++;
 
     debug("Retry in " + retryTimeout + " Try number " + this._retryFailCount);
-    this._mainPushService.setAlarm(retryTimeout);
+    if (this._mainPushService) {
+      this._mainPushService.setAlarm(retryTimeout);
+    } else {
+      dump("This should not happend");
+    }
   },
 
   /**
    * We need to calculate a new ping based on:
    *  1) Latest good ping
    *  2) A safe gap between 1) and the calculated new ping (which is
    *  by default, 1 minute)
    *
@@ -385,17 +438,18 @@ this.PushServiceWebSocket = {
    *  1) We might lose the connection, so we need to reconnect again (wasting
    *  resources)
    *  2) We use a lot of network signaling just for pinging.
    *
    * This algorithm tries to search the best value between a disconnection and a
    * valid ping, to ensure better battery life and network resources usage.
    *
    * The value is saved in dom.push.pingInterval
-   * @param wsWentDown [Boolean] if the WebSocket was closed or it is still alive
+   * @param wsWentDown [Boolean] if the WebSocket was closed or it is still
+   * alive
    *
    */
   _calculateAdaptivePing: function(wsWentDown) {
     debug('_calculateAdaptivePing()');
     if (!this._adaptiveEnabled) {
       debug('Adaptive ping is disabled');
       return;
     }
@@ -408,17 +462,18 @@ this.PushServiceWebSocket = {
     }
 
     if (!wsWentDown) {
       debug('Setting websocket down counter to 0');
       this._wsWentDownCounter = 0;
     }
 
     if (!this._recalculatePing && !wsWentDown) {
-      debug('We do not need to recalculate the ping now, based on previous data');
+      debug('We do not need to recalculate the ping now, based on previous ' +
+            'data');
       return;
     }
 
     // Save actual state of the network
     let ns = this._networkInfo.getNetworkInformation();
 
     if (ns.ip) {
       // mobile
@@ -493,17 +548,18 @@ this.PushServiceWebSocket = {
          return;
        }
 
        // Latest ping was invalid, we need to lower the limit to limit / 2
        nextPingInterval = Math.floor(lastTriedPingInterval / 2);
 
       // If the new ping interval is close to the last good one, we are near
       // optimum, so stop calculating.
-      if (nextPingInterval - this._lastGoodPingInterval < prefs.get('adaptive.gap')) {
+      if (nextPingInterval - this._lastGoodPingInterval <
+          prefs.get('adaptive.gap')) {
         debug('We have reached the gap, we have finished the calculation');
         debug('nextPingInterval=' + nextPingInterval);
         debug('lastGoodPing=' + this._lastGoodPingInterval);
         nextPingInterval = this._lastGoodPingInterval;
         this._recalculatePing = false;
       } else {
         debug('We need to calculate next time');
         this._recalculatePing = true;
@@ -512,65 +568,54 @@ this.PushServiceWebSocket = {
     } else {
       debug('The WebSocket is still up');
       this._lastGoodPingInterval = lastTriedPingInterval;
       nextPingInterval = Math.floor(lastTriedPingInterval * 1.5);
     }
 
     // Check if we have reached the upper limit
     if (this._upperLimit < nextPingInterval) {
-      debug('Next ping will be bigger than the configured upper limit, capping interval');
+      debug('Next ping will be bigger than the configured upper limit, ' +
+            'capping interval');
       this._recalculatePing = false;
       this._lastGoodPingInterval = lastTriedPingInterval;
       nextPingInterval = lastTriedPingInterval;
     }
 
     debug('Setting the pingInterval to ' + nextPingInterval);
     prefs.set('pingInterval', nextPingInterval);
 
     this._save(ns, nextPingInterval);
   },
 
   _save: function(ns, nextPingInterval){
     //Save values for our current network
     if (ns.ip) {
       prefs.set('pingInterval.mobile', nextPingInterval);
-      prefs.set('adaptive.lastGoodPingInterval.mobile', this._lastGoodPingInterval);
+      prefs.set('adaptive.lastGoodPingInterval.mobile',
+                this._lastGoodPingInterval);
     } else {
       prefs.set('pingInterval.wifi', nextPingInterval);
-      prefs.set('adaptive.lastGoodPingInterval.wifi', this._lastGoodPingInterval);
+      prefs.set('adaptive.lastGoodPingInterval.wifi',
+                this._lastGoodPingInterval);
     }
   },
 
   _makeWebSocket: function(uri) {
     if (!prefs.get("connection.enabled")) {
       debug("_makeWebSocket: connection.enabled is not set to true. Aborting.");
       return null;
     }
     if (Services.io.offline) {
       debug("Network is offline.");
       return null;
     }
-    let socket;
-    if (uri.scheme === "wss") {
-      socket = Cc["@mozilla.org/network/protocol;1?name=wss"]
-                 .createInstance(Ci.nsIWebSocketChannel);
-    }
-    else if (uri.scheme === "ws") {
-      if (uri.host != "localhost") {
-        debug("Push over an insecure connection (ws://) is not allowed!");
-        return null;
-      }
-      socket = Cc["@mozilla.org/network/protocol;1?name=ws"]
-                 .createInstance(Ci.nsIWebSocketChannel);
-    }
-    else {
-      debug("Unsupported websocket scheme " + uri.scheme);
-      return null;
-    }
+    let socket = Cc["@mozilla.org/network/protocol;1?name=wss"]
+                   .createInstance(Ci.nsIWebSocketChannel);
+
     socket.initLoadInfo(null, // aLoadingNode
                         Services.scriptSecurityManager.getSystemPrincipal(),
                         null, // aTriggeringPrincipal
                         Ci.nsILoadInfo.SEC_NORMAL,
                         Ci.nsIContentPolicy.TYPE_WEBSOCKET);
 
     return socket;
   },
@@ -579,57 +624,53 @@ this.PushServiceWebSocket = {
     debug("beginWSSetup()");
     if (this._currentState != STATE_SHUT_DOWN) {
       debug("_beginWSSetup: Not in shutdown state! Current state " +
             this._currentState);
       return;
     }
 
     // Stop any pending reconnects scheduled for the near future.
-    this._mainPushService.stopAlarm();
+    if (this._mainPushService) {
+      this._mainPushService.stopAlarm();
+    }
 
-    let uri = this._mainPushService.getServerURI();
+    let uri = this._serverURI;
     if (!uri) {
       return;
     }
     let socket = this._makeWebSocket(uri);
     if (!socket) {
       return;
     }
     this._ws = socket.QueryInterface(Ci.nsIWebSocketChannel);
 
     debug("serverURL: " + uri.spec);
     this._wsListener = new PushWebSocketListener(this);
     this._ws.protocol = "push-notification";
 
     try {
-      // Grab a wakelock before we open the socket to ensure we don't go to sleep
-      // before connection the is opened.
+      // Grab a wakelock before we open the socket to ensure we don't go to
+      // sleep before connection the is opened.
       this._ws.asyncOpen(uri, uri.spec, this._wsListener, null);
       this._acquireWakeLock();
       this._currentState = STATE_WAITING_FOR_WS_START;
     } catch(e) {
       debug("Error opening websocket. asyncOpen failed!");
       this._shutdownWS();
       this._reconnectAfterBackoff();
     }
   },
 
-  startListeningIfChannelsPresent: function() {
+  connect: function(channelIDs) {
+    debug("connect");
     // Check to see if we need to do anything.
-    if (this._requestQueue.length > 0) {
+    if (channelIDs.length > 0) {
       this._beginWSSetup();
-      return;
     }
-    this._mainPushService.getAllChannelIDs()
-      .then(channelIDs => {
-        if (channelIDs.length > 0) {
-          this._beginWSSetup();
-        }
-      });
   },
 
   /**
    * There is only one alarm active at any time. This alarm has 3 intervals
    * corresponding to 3 tasks.
    *
    * 1) Reconnect on ping timeout.
    *    If we haven't received any messages from the server by the time this
@@ -671,17 +712,17 @@ this.PushServiceWebSocket = {
       try {
         this._wsSendMessage({});
       } catch (e) {
       }
 
       this._waitingForPong = true;
       this._mainPushService.setAlarm(prefs.get("requestTimeout"));
     }
-    else if (this._alarmID !== null) {
+    else if (this._mainPushService && this._mainPushService._alarmID !== null) {
       debug("reconnect alarm fired.");
       // Reconnect after back-off.
       // The check for a non-null _alarmID prevents a situation where the alarm
       // fires, but _shutdownWS() is called from another code-path (e.g.
       // network state change) and we don't want to reconnect.
       //
       // It also handles the case where _beginWSSetup() is called from another
       // code-path.
@@ -699,17 +740,18 @@ this.PushServiceWebSocket = {
 #ifdef MOZ_B2G
     // Disable the wake lock on non-B2G platforms to work around bug 1154492.
     if (!this._socketWakeLock) {
       debug("Acquiring Socket Wakelock");
       this._socketWakeLock = gPowerManagerService.newWakeLock("cpu");
     }
     if (!this._socketWakeLockTimer) {
       debug("Creating Socket WakeLock Timer");
-      this._socketWakeLockTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this._socketWakeLockTimer = Cc["@mozilla.org/timer;1"]
+                                    .createInstance(Ci.nsITimer);
     }
 
     debug("Setting Socket WakeLock Timer");
     this._socketWakeLockTimer
       .initWithCallback(this._releaseWakeLock.bind(this),
                         // Allow the same time for socket setup as we do for
                         // requests after the setup. Fudge it a bit since
                         // timers can be a little off and we don't want to go
@@ -762,57 +804,80 @@ this.PushServiceWebSocket = {
             reply.uaid);
       this._shutdownWS();
       return;
     }
 
     function finishHandshake() {
       this._UAID = reply.uaid;
       this._currentState = STATE_READY;
-      this._processNextRequestInQueue();
+      if (this._notifyRequestQueue) {
+        this._notifyRequestQueue();
+        this._notifyRequestQueue = null;
+      }
     }
 
     // By this point we've got a UAID from the server that we are ready to
     // accept.
     //
     // If we already had a valid UAID before, we have to ask apps to
     // re-register.
     if (this._UAID && this._UAID != reply.uaid) {
       debug("got new UAID: all re-register");
 
-      this._mainPushService.notifyAllAppsRegister()
-          .then(_ => this._mainPushService.dropRegistrations())
+      this._mainPushService.dropRegistrations()
           .then(finishHandshake.bind(this));
 
       return;
     }
 
     // otherwise we are good to go
     finishHandshake.bind(this)();
   },
 
   /**
    * Protocol handler invoked by server message.
    */
   _handleRegisterReply: function(reply) {
     debug("handleRegisterReply()");
     if (typeof reply.channelID !== "string" ||
-        typeof this._pendingRequests[reply.channelID] !== "object")
+        typeof this._pendingRequests[reply.channelID] !== "object") {
       return;
+    }
 
     let tmp = this._pendingRequests[reply.channelID];
     delete this._pendingRequests[reply.channelID];
-    if (Object.keys(this._pendingRequests).length == 0 &&
-        this._requestTimeoutTimer)
+    if (Object.keys(this._pendingRequests).length === 0 &&
+        this._requestTimeoutTimer) {
       this._requestTimeoutTimer.cancel();
+    }
 
     if (reply.status == 200) {
-      tmp.deferred.resolve(reply);
+      try {
+        Services.io.newURI(reply.pushEndpoint, null, null);
+      }
+      catch (e) {
+        debug("Invalid pushEndpoint " + reply.pushEndpoint);
+        tmp.reject({state: 0, error: "Invalid pushEndpoint " +
+                                     reply.pushEndpoint});
+        return;
+      }
+
+      let record = {
+        channelID: reply.channelID,
+        pushEndpoint: reply.pushEndpoint,
+        pageURL: tmp.record.pageURL,
+        scope: tmp.record.scope,
+        pushCount: 0,
+        lastPush: 0,
+        version: null
+      };
+      tmp.resolve(record);
     } else {
-      tmp.deferred.reject(reply);
+      tmp.reject(reply);
     }
   },
 
   /**
    * Protocol handler invoked by server message.
    */
   _handleNotificationReply: function(reply) {
     debug("handleNotificationReply()");
@@ -848,193 +913,188 @@ this.PushServiceWebSocket = {
         this._sendAck(update.channelID, version);
       }
     }
   },
 
   // FIXME(nsm): batch acks for efficiency reasons.
   _sendAck: function(channelID, version) {
     debug("sendAck()");
-    this._send('ack', {
-      updates: [{channelID: channelID, version: version}]
-    });
+    var data = {messageType: 'ack',
+                updates: [{channelID: channelID,
+                           version: version}]
+               };
+    this._queueRequest(data);
   },
 
-  /*
-   * 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({error: "Received non-string channelID"});
-    }
+  _generateID: function() {
+    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
+                          .getService(Ci.nsIUUIDGenerator);
+    // generateUUID() gives a UUID surrounded by {...}, slice them off.
+    return uuidGenerator.generateUUID().toString().slice(1, -1);
+  },
 
-    if (Object.keys(this._pendingRequests).length == 0) {
+  request: function(action, record) {
+    debug("request() " + action);
+
+    if (Object.keys(this._pendingRequests).length === 0) {
       // start the timer since we now have at least one request
-      if (!this._requestTimeoutTimer)
+      if (!this._requestTimeoutTimer) {
         this._requestTimeoutTimer = Cc["@mozilla.org/timer;1"]
                                       .createInstance(Ci.nsITimer);
+      }
       this._requestTimeoutTimer.init(this,
                                      this._requestTimeout,
                                      Ci.nsITimer.TYPE_REPEATING_SLACK);
     }
 
-    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};
+    if (action == "register") {
+      record.channelID = this._generateID();
     }
-    request.ctime = Date.now();
-    this._send(action, data);
-    return deferred.promise;
+    var data = {channelID: record.channelID,
+                messageType: action};
+
+    return new Promise((resolve, reject) => {
+      this._pendingRequests[data.channelID] = {record: record,
+                                               resolve: resolve,
+                                               reject: reject,
+                                               ctime: Date.now()
+                                              };
+      this._queueRequest(data);
+    });
   },
 
-  _send: function(action, data) {
-    debug("send()");
-    this._requestQueue.push([action, data]);
-    debug("Queued " + action);
-    this._processNextRequestInQueue();
+  _queueStart: Promise.resolve(),
+  _notifyRequestQueue: null,
+  _queue: null,
+  _enqueue: function(op, errop) {
+    debug("enqueue");
+    if (!this._queue) {
+      this._queue = this._queueStart;
+    }
+    this._queue = this._queue
+                    .then(op)
+                    .catch(_ => {});
   },
 
-  _processNextRequestInQueue: function() {
-    debug("_processNextRequestInQueue()");
+  _send(data) {
+    if (this._currentState == STATE_READY) {
+      if (data.messageType == "ack") {
+        this._wsSendMessage(data);
+      } else {
+        // check if request has not been canelled
+        if (typeof this._pendingRequests[data.channelID] == "object") {
+          this._wsSendMessage(data);
+        }
+      }
+    }
+  },
 
-    if (this._requestQueue.length == 0) {
-      debug("Request queue empty");
-      return;
+  _queueRequest(data) {
+    if (this._currentState != STATE_READY) {
+      if (!this._notifyRequestQueue) {
+        this._enqueue(_ => {
+          return new Promise((resolve, reject) => {
+                               this._notifyRequestQueue = resolve;
+                             });
+        });
+      }
+
     }
 
-    if (this._currentState != STATE_READY) {
-      if (!this._ws) {
-        // This will end up calling processNextRequestInQueue().
-        this._beginWSSetup();
+    this._enqueue(_ => this._send(data));
+    if (!this._ws) {
+      // This will end up calling notifyRequestQueue().
+      this._beginWSSetup();
+      // If beginWSSetup does not succeed to make ws, notifyRequestQueue will
+      // not be call.
+      if (!this._ws && this._notifyRequestQueue) {
+        this._notifyRequestQueue();
+        this._notifyRequestQueue = null;
       }
-      else {
-        // We have a socket open so we are just waiting for hello to finish.
-        // That will call processNextRequestInQueue().
-      }
-      return;
     }
-
-    let [action, data] = this._requestQueue.shift();
-    data.messageType = action;
-    if (!this._ws) {
-      // If our websocket is not ready and our state is STATE_READY we may as
-      // well give up all assumptions about the world and start from scratch
-      // again.  Discard the message itself, let the timeout notify error to
-      // the app.
-      debug("This should never happen!");
-      this._shutdownWS();
-    }
-
-    this._wsSendMessage(data);
-    // Process the next one as soon as possible.
-    setTimeout(this._processNextRequestInQueue.bind(this), 0);
   },
 
   _receivedUpdate: function(aChannelID, aLatestVersion) {
     debug("Updating: " + aChannelID + " -> " + aLatestVersion);
 
     let compareRecordVersionAndNotify = function(aPushRecord) {
       debug("compareRecordVersionAndNotify()");
       if (!aPushRecord) {
         debug("No record for channel ID " + aChannelID);
         return;
       }
 
-      if (aPushRecord.version == null ||
+      if (aPushRecord.version === null ||
           aPushRecord.version < aLatestVersion) {
         debug("Version changed, notifying app and updating DB");
         aPushRecord.version = aLatestVersion;
         aPushRecord.pushCount = aPushRecord.pushCount + 1;
         aPushRecord.lastPush = new Date().getTime();
-        this._mainPushService.notifyApp(aPushRecord);
-        this._mainPushService.updatePushRecord(aPushRecord)
-          .then(
-            null,
-            function(e) {
-              debug("Error updating push record");
-            }
-          );
+        this._mainPushService.receivedPushMessage(aPushRecord,
+                                                  "Short as life is, we make " +
+                                                  "it still shorter by the " +
+                                                  "careless waste of time.");
       }
       else {
         debug("No significant version change: " + aLatestVersion);
       }
-    }
+    };
 
     let recoverNoSuchChannelID = function(aChannelIDFromServer) {
       debug("Could not get channelID " + aChannelIDFromServer + " from DB");
-    }
+    };
 
     this._mainPushService.getByChannelID(aChannelID)
       .then(compareRecordVersionAndNotify.bind(this),
             err => recoverNoSuchChannelID(err));
   },
 
-  register: function(channelID) {
-    return this._sendRequest("register", {channelID: channelID})
-  },
-
-  unregister: function(aRecord) {
-    // courtesy, but don't establish a connection
-    // just for it
-    if (this._ws) {
-      debug("Had a connection, so telling the server");
-      this._send("unregister", {channelID: aRecord.channelID});
-    }
-  },
-
   // begin Push protocol handshake
   _wsOnStart: function(context) {
     debug("wsOnStart()");
     this._releaseWakeLock();
 
     if (this._currentState != STATE_WAITING_FOR_WS_START) {
       debug("NOT in STATE_WAITING_FOR_WS_START. Current state " +
             this._currentState + ". Skipping");
       return;
     }
 
     // Since we've had a successful connection reset the retry fail count.
     this._retryFailCount = 0;
 
     let data = {
       messageType: "hello",
-    }
+    };
 
-    if (this._UAID)
-      data["uaid"] = this._UAID;
+    if (this._UAID) {
+      data.uaid = this._UAID;
+    }
 
     function sendHelloMessage(ids) {
       // On success, ids is an array, on error its not.
-      data["channelIDs"] = ids.map ?
+      data.channelIDs = ids.map ?
                            ids.map(function(el) { return el.channelID; }) : [];
       this._wsSendMessage(data);
       this._currentState = STATE_WAITING_FOR_HELLO;
     }
 
     this._networkInfo.getNetworkState((networkState) => {
       if (networkState.ip) {
         // Opening an available UDP port.
         this._listenForUDPWakeup();
 
         // Host-port is apparently a thing.
-        data["wakeup_hostport"] = {
+        data.wakeup_hostport = {
           ip: networkState.ip,
           port: this._udpServer && this._udpServer.port
         };
 
-        data["mobilenetwork"] = {
+        data.mobilenetwork = {
           mcc: networkState.mcc,
           mnc: networkState.mnc,
           netid: networkState.netid
         };
       }
 
       this._mainPushService.getAllChannelIDs()
         .then(sendHelloMessage.bind(this),
@@ -1066,17 +1126,17 @@ this.PushServiceWebSocket = {
     this._shutdownWS();
   },
 
   _wsOnMessageAvailable: function(context, message) {
     debug("wsOnMessageAvailable() " + message);
 
     this._waitingForPong = false;
 
-    let reply = undefined;
+    let reply;
     try {
       reply = JSON.parse(message);
     } catch(e) {
       debug("Parsing JSON failed. text : " + message);
       return;
     }
 
     // If we are not waiting for a hello message, reset the retry fail count
@@ -1152,17 +1212,17 @@ this.PushServiceWebSocket = {
 
   /**
    * 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: "AbortError"});
+      request.reject({status: 0, error: "AbortError"});
     }
   },
 
   _makeUDPSocket: function() {
     return Cc["@mozilla.org/network/udp-socket;1"]
              .createInstance(Ci.nsIUDPSocket);
   },
 
@@ -1224,19 +1284,22 @@ let PushNetworkInfo = {
     debug("getNetworkInformation()");
 
     try {
       if (!prefs.get("udp.wakeupEnabled")) {
         debug("UDP support disabled, we do not send any carrier info");
         throw new Error("UDP disabled");
       }
 
-      let nm = Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager);
-      if (nm.active && nm.active.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) {
-        let iccService = Cc["@mozilla.org/icc/iccservice;1"].getService(Ci.nsIIccService);
+      let nm = Cc["@mozilla.org/network/manager;1"]
+                 .getService(Ci.nsINetworkManager);
+      if (nm.active &&
+          nm.active.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) {
+        let iccService = Cc["@mozilla.org/icc/iccservice;1"]
+                           .getService(Ci.nsIIccService);
         // TODO: Bug 927721 - PushService for multi-sim
         // In Multi-sim, there is more than one client in iccService. Each
         // client represents a icc handle. To maintain backward compatibility
         // with single sim, we always use client 0 for now. Adding support
         // for multiple sim will be addressed in bug 927721, if needed.
         let clientId = 0;
         let icc = iccService.getIccByServiceId(clientId);
         let iccInfo = icc && icc.iccInfo;
@@ -1246,17 +1309,17 @@ let PushNetworkInfo = {
           let ips = {};
           let prefixLengths = {};
           nm.active.getAddresses(ips, prefixLengths);
 
           return {
             mcc: iccInfo.mcc,
             mnc: iccInfo.mnc,
             ip:  ips.value[0]
-          }
+          };
         }
       }
     } catch (e) {
       debug("Error recovering mobile network information: " + e);
     }
 
     debug("Running on wifi");
     return {