Bug 1558298: Part 7 - Convert SpecialPowersAPI and sub-classes to ES6 classes. r=nika
authorKris Maglione <maglione.k@gmail.com>
Mon, 10 Jun 2019 15:15:37 -0700
changeset 540912 0e91fc9541c294b8c3133aae81f45d0bcab8155c
parent 540911 edd1cc6badf788f950ee68168aed0a14aa191411
child 540913 4ccecdba1c3430a7e605329425020e43b2e9bc39
push id11533
push userarchaeopteryx@coole-files.de
push dateMon, 08 Jul 2019 18:18:03 +0000
treeherdermozilla-beta@f4452e031aed [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnika
bugs1558298
milestone69.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 1558298: Part 7 - Convert SpecialPowersAPI and sub-classes to ES6 classes. r=nika Differential Revision: https://phabricator.services.mozilla.com/D34596
testing/mochitest/tests/SimpleTest/ChromePowers.js
testing/specialpowers/content/SpecialPowers.jsm
testing/specialpowers/content/SpecialPowersAPI.jsm
--- a/testing/mochitest/tests/SimpleTest/ChromePowers.js
+++ b/testing/mochitest/tests/SimpleTest/ChromePowers.js
@@ -1,110 +1,109 @@
 /* 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/. */
 
 ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm", this);
 
-function ChromePowers(window) {
-  this.window = Cu.getWeakReference(window);
+class ChromePowers extends SpecialPowersAPI {
+  constructor(window) {
+    super();
 
-  this.chromeWindow = window;
+    this.window = Cu.getWeakReference(window);
 
-  this.DOMWindowUtils = bindDOMWindowUtils(window);
+    this.chromeWindow = window;
 
-  this.spObserver = new SpecialPowersObserverAPI();
-  this.spObserver._sendReply = this._sendReply.bind(this);
-  this.listeners = new Map();
-}
+    this.DOMWindowUtils = bindDOMWindowUtils(window);
 
-ChromePowers.prototype = new SpecialPowersAPI();
+    this.spObserver = new SpecialPowersObserverAPI();
+    this.spObserver._sendReply = this._sendReply.bind(this);
+    this.listeners = new Map();
+  }
 
-ChromePowers.prototype.toString = function() { return "[ChromePowers]"; };
-ChromePowers.prototype.sanityCheck = function() { return "foo"; };
-
-// This gets filled in in the constructor.
-ChromePowers.prototype.DOMWindowUtils = undefined;
+  toString() { return "[ChromePowers]"; }
+  sanityCheck() { return "foo"; }
 
-ChromePowers.prototype._sendReply = function(aOrigMsg, aType, aMsg) {
-  var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
-  if (!this.listeners.has(aType)) {
-    throw new Error(`No listener for ${aType}`);
+  _sendReply(aOrigMsg, aType, aMsg) {
+    var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
+    if (!this.listeners.has(aType)) {
+      throw new Error(`No listener for ${aType}`);
+    }
+    this.listeners.get(aType)(msg);
   }
-  this.listeners.get(aType)(msg);
-};
 
-ChromePowers.prototype._sendSyncMessage = function(aType, aMsg) {
-  var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
-  return [this._receiveMessage(msg)];
-};
+  _sendSyncMessage(aType, aMsg) {
+    var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
+    return [this._receiveMessage(msg)];
+  }
 
-ChromePowers.prototype._sendAsyncMessage = function(aType, aMsg) {
-  var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
-  this._receiveMessage(msg);
-};
+  _sendAsyncMessage(aType, aMsg) {
+    var msg = {'name':aType, 'json': aMsg, 'data': aMsg};
+    this._receiveMessage(msg);
+  }
 
-ChromePowers.prototype._addMessageListener = function(aType, aCallback) {
-  if (this.listeners.has(aType)) {
-    throw new Error(`unable to handle multiple listeners for ${aType}`);
+  _addMessageListener(aType, aCallback) {
+    if (this.listeners.has(aType)) {
+      throw new Error(`unable to handle multiple listeners for ${aType}`);
+    }
+    this.listeners.set(aType, aCallback);
   }
-  this.listeners.set(aType, aCallback);
-};
-ChromePowers.prototype._removeMessageListener = function(aType, aCallback) {
-  this.listeners.delete(aType);
-};
+  _removeMessageListener(aType, aCallback) {
+    this.listeners.delete(aType);
+  }
 
-ChromePowers.prototype.registerProcessCrashObservers = function() {
-  this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
-};
+  registerProcessCrashObservers() {
+    this._sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
+  }
 
-ChromePowers.prototype.unregisterProcessCrashObservers = function() {
-  this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
-};
+  unregisterProcessCrashObservers() {
+    this._sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
+  }
 
-ChromePowers.prototype._receiveMessage = function(aMessage) {
-  switch (aMessage.name) {
-    case "SpecialPowers.Quit":
-      let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
-      appStartup.quit(Ci.nsIAppStartup.eForceQuit);
-      break;
-    case "SPProcessCrashService":
-      if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") {
-        // Hack out register/unregister specifically for browser-chrome leaks
+  _receiveMessage(aMessage) {
+    switch (aMessage.name) {
+      case "SpecialPowers.Quit":
+        let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+        appStartup.quit(Ci.nsIAppStartup.eForceQuit);
         break;
-      } else if (aMessage.type == "crash-observed") {
-        for (let e of msg.dumpIDs) {
-          this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
+      case "SPProcessCrashService":
+        if (aMessage.json.op == "register-observer" || aMessage.json.op == "unregister-observer") {
+          // Hack out register/unregister specifically for browser-chrome leaks
+          break;
+        } else if (aMessage.type == "crash-observed") {
+          for (let e of msg.dumpIDs) {
+            this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
+          }
         }
-      }
-    default:
-      // All calls go here, because we need to handle SPProcessCrashService calls as well
-      return this.spObserver._receiveMessageAPI(aMessage);
+      default:
+        // All calls go here, because we need to handle SPProcessCrashService calls as well
+        return this.spObserver._receiveMessageAPI(aMessage);
+    }
+    return undefined;		// Avoid warning.
   }
-  return undefined;		// Avoid warning.
-};
 
-ChromePowers.prototype.quit = function() {
-  // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers.
-  // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
-  // allows us to use the ChromePowers object which we defined below.
-  SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {});
-};
+  quit() {
+    // We come in here as SpecialPowers.quit, but SpecialPowers is really ChromePowers.
+    // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
+    // allows us to use the ChromePowers object which we defined below.
+    SpecialPowers._sendSyncMessage("SpecialPowers.Quit", {});
+  }
 
-ChromePowers.prototype.focus = function(aWindow) {
-  // We come in here as SpecialPowers.focus, but SpecialPowers is really ChromePowers.
-  // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
-  // allows us to use the ChromePowers object which we defined below.
-  if (aWindow)
-    aWindow.focus();
-};
+  focus(aWindow) {
+    // We come in here as SpecialPowers.focus, but SpecialPowers is really ChromePowers.
+    // For some reason this.<func> resolves to TestRunner, so using SpecialPowers
+    // allows us to use the ChromePowers object which we defined below.
+    if (aWindow)
+      aWindow.focus();
+  }
 
-ChromePowers.prototype.executeAfterFlushingMessageQueue = function(aCallback) {
-  aCallback();
-};
+  executeAfterFlushingMessageQueue(aCallback) {
+    aCallback();
+  }
+}
 
 if (window.parent.SpecialPowers && !window.SpecialPowers) {
   window.SpecialPowers = window.parent.SpecialPowers;
 } else {
   const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
   Services.scriptloader.loadSubScript("resource://specialpowers/SpecialPowersObserverAPI.js", this);
 
   window.SpecialPowers = new ChromePowers(window);
--- a/testing/specialpowers/content/SpecialPowers.jsm
+++ b/testing/specialpowers/content/SpecialPowers.jsm
@@ -10,296 +10,293 @@ var {Services} = ChromeUtils.import("res
 Services.scriptloader.loadSubScript("resource://specialpowers/MozillaLogger.js", this);
 
 var EXPORTED_SYMBOLS = ["SpecialPowers", "attachSpecialPowersToWindow"];
 
 const {bindDOMWindowUtils, SpecialPowersAPI} = ChromeUtils.import("resource://specialpowers/SpecialPowersAPI.jsm");
 
 Cu.forcePermissiveCOWs();
 
-function SpecialPowers(window, mm) {
-  this.mm = mm;
+class SpecialPowers extends SpecialPowersAPI {
+  constructor(window, mm) {
+    super();
+
+    this.mm = mm;
 
-  this.window = Cu.getWeakReference(window);
-  this._windowID = window.windowUtils.currentInnerWindowID;
-  this._encounteredCrashDumpFiles = [];
-  this._unexpectedCrashDumpFiles = { };
-  this._crashDumpDir = null;
-  this._serviceWorkerRegistered = false;
-  this._serviceWorkerCleanUpRequests = new Map();
-  this.DOMWindowUtils = bindDOMWindowUtils(window);
-  Object.defineProperty(this, "Components", {
-      configurable: true, enumerable: true, value: this.getFullComponents(),
-  });
-  this._pongHandlers = [];
-  this._messageListener = this._messageReceived.bind(this);
-  this._grandChildFrameMM = null;
-  this._createFilesOnError = null;
-  this._createFilesOnSuccess = null;
-  this.SP_SYNC_MESSAGES = ["SPChromeScriptMessage",
-                           "SPLoadChromeScript",
-                           "SPImportInMainProcess",
-                           "SPObserverService",
-                           "SPPermissionManager",
-                           "SPPrefService",
-                           "SPProcessCrashService",
-                           "SPSetTestPluginEnabledState",
-                           "SPCleanUpSTSData",
-                           "SPCheckServiceWorkers"];
+    this.window = Cu.getWeakReference(window);
+    this._windowID = window.windowUtils.currentInnerWindowID;
+    this._encounteredCrashDumpFiles = [];
+    this._unexpectedCrashDumpFiles = { };
+    this._crashDumpDir = null;
+    this._serviceWorkerRegistered = false;
+    this._serviceWorkerCleanUpRequests = new Map();
+    this.DOMWindowUtils = bindDOMWindowUtils(window);
+    Object.defineProperty(this, "Components", {
+        configurable: true, enumerable: true, value: this.getFullComponents(),
+    });
+    this._pongHandlers = [];
+    this._messageListener = this._messageReceived.bind(this);
+    this._grandChildFrameMM = null;
+    this._createFilesOnError = null;
+    this._createFilesOnSuccess = null;
+    this.SP_SYNC_MESSAGES = ["SPChromeScriptMessage",
+                             "SPLoadChromeScript",
+                             "SPImportInMainProcess",
+                             "SPObserverService",
+                             "SPPermissionManager",
+                             "SPPrefService",
+                             "SPProcessCrashService",
+                             "SPSetTestPluginEnabledState",
+                             "SPCleanUpSTSData",
+                             "SPCheckServiceWorkers"];
 
-  this.SP_ASYNC_MESSAGES = ["SpecialPowers.Focus",
-                            "SpecialPowers.Quit",
-                            "SpecialPowers.CreateFiles",
-                            "SpecialPowers.RemoveFiles",
-                            "SPPingService",
-                            "SPLoadExtension",
-                            "SPProcessCrashManagerWait",
-                            "SPStartupExtension",
-                            "SPUnloadExtension",
-                            "SPExtensionMessage",
-                            "SPRequestDumpCoverageCounters",
-                            "SPRequestResetCoverageCounters",
-                            "SPRemoveAllServiceWorkers",
-                            "SPRemoveServiceWorkerDataForExampleDomain"];
-  mm.addMessageListener("SPPingService", this._messageListener);
-  mm.addMessageListener("SPServiceWorkerRegistered", this._messageListener);
-  mm.addMessageListener("SpecialPowers.FilesCreated", this._messageListener);
-  mm.addMessageListener("SpecialPowers.FilesError", this._messageListener);
-  mm.addMessageListener("SPServiceWorkerCleanupComplete", this._messageListener);
-  let self = this;
-  Services.obs.addObserver(function onInnerWindowDestroyed(subject, topic, data) {
-    var id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
-    if (self._windowID === id) {
-      Services.obs.removeObserver(onInnerWindowDestroyed, "inner-window-destroyed");
-      try {
-        mm.removeMessageListener("SPPingService", self._messageListener);
-        mm.removeMessageListener("SpecialPowers.FilesCreated", self._messageListener);
-        mm.removeMessageListener("SpecialPowers.FilesError", self._messageListener);
-        mm.removeMessageListener("SPServiceWorkerCleanupComplete", self._messageListener);
-      } catch (e) {
-        // Ignore the exception which the message manager has been destroyed.
-        if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE) {
-          throw e;
-        }
-      }
-    }
-  }, "inner-window-destroyed");
-}
-
-SpecialPowers.prototype = new SpecialPowersAPI();
-
-SpecialPowers.prototype.toString = function() { return "[SpecialPowers]"; };
-SpecialPowers.prototype.sanityCheck = function() { return "foo"; };
-
-// This gets filled in in the constructor.
-SpecialPowers.prototype.DOMWindowUtils = undefined;
-SpecialPowers.prototype.Components = undefined;
-SpecialPowers.prototype.IsInNestedFrame = false;
-
-SpecialPowers.prototype._sendSyncMessage = function(msgname, msg) {
-  if (!this.SP_SYNC_MESSAGES.includes(msgname)) {
-    dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
-  }
-  let result = this.mm.sendSyncMessage(msgname, msg);
-  return Cu.cloneInto(result, this);
-};
-
-SpecialPowers.prototype._sendAsyncMessage = function(msgname, msg) {
-  if (!this.SP_ASYNC_MESSAGES.includes(msgname)) {
-    dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
-  }
-  this.mm.sendAsyncMessage(msgname, msg);
-};
-
-SpecialPowers.prototype._addMessageListener = function(msgname, listener) {
-  this.mm.addMessageListener(msgname, listener);
-  this.mm.sendAsyncMessage("SPPAddNestedMessageListener", { name: msgname });
-};
-
-SpecialPowers.prototype._removeMessageListener = function(msgname, listener) {
-  this.mm.removeMessageListener(msgname, listener);
-};
-
-SpecialPowers.prototype.registerProcessCrashObservers = function() {
-  this.mm.addMessageListener("SPProcessCrashService", this._messageListener);
-  this.mm.sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
-};
-
-SpecialPowers.prototype.unregisterProcessCrashObservers = function() {
-  this.mm.removeMessageListener("SPProcessCrashService", this._messageListener);
-  this.mm.sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
-};
-
-SpecialPowers.prototype._messageReceived = function(aMessage) {
-  switch (aMessage.name) {
-    case "SPProcessCrashService":
-      if (aMessage.json.type == "crash-observed") {
-        for (let e of aMessage.json.dumpIDs) {
-          this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
-        }
-      }
-      break;
-
-    case "SPPingService":
-      if (aMessage.json.op == "pong") {
-        var handler = this._pongHandlers.shift();
-        if (handler) {
-          handler();
-        }
-        if (this._grandChildFrameMM) {
-          this._grandChildFrameMM.sendAsyncMessage("SPPingService", { op: "pong" });
+    this.SP_ASYNC_MESSAGES = ["SpecialPowers.Focus",
+                              "SpecialPowers.Quit",
+                              "SpecialPowers.CreateFiles",
+                              "SpecialPowers.RemoveFiles",
+                              "SPPingService",
+                              "SPLoadExtension",
+                              "SPProcessCrashManagerWait",
+                              "SPStartupExtension",
+                              "SPUnloadExtension",
+                              "SPExtensionMessage",
+                              "SPRequestDumpCoverageCounters",
+                              "SPRequestResetCoverageCounters",
+                              "SPRemoveAllServiceWorkers",
+                              "SPRemoveServiceWorkerDataForExampleDomain"];
+    mm.addMessageListener("SPPingService", this._messageListener);
+    mm.addMessageListener("SPServiceWorkerRegistered", this._messageListener);
+    mm.addMessageListener("SpecialPowers.FilesCreated", this._messageListener);
+    mm.addMessageListener("SpecialPowers.FilesError", this._messageListener);
+    mm.addMessageListener("SPServiceWorkerCleanupComplete", this._messageListener);
+    let self = this;
+    Services.obs.addObserver(function onInnerWindowDestroyed(subject, topic, data) {
+      var id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+      if (self._windowID === id) {
+        Services.obs.removeObserver(onInnerWindowDestroyed, "inner-window-destroyed");
+        try {
+          mm.removeMessageListener("SPPingService", self._messageListener);
+          mm.removeMessageListener("SpecialPowers.FilesCreated", self._messageListener);
+          mm.removeMessageListener("SpecialPowers.FilesError", self._messageListener);
+          mm.removeMessageListener("SPServiceWorkerCleanupComplete", self._messageListener);
+        } catch (e) {
+          // Ignore the exception which the message manager has been destroyed.
+          if (e.result != Cr.NS_ERROR_ILLEGAL_VALUE) {
+            throw e;
+          }
         }
       }
-      break;
+    }, "inner-window-destroyed");
+  }
 
-    case "SPServiceWorkerRegistered":
-      this._serviceWorkerRegistered = aMessage.data.registered;
-      break;
+  toString() { return "[SpecialPowers]"; }
+  sanityCheck() { return "foo"; }
 
-    case "SpecialPowers.FilesCreated":
-      var createdHandler = this._createFilesOnSuccess;
-      this._createFilesOnSuccess = null;
-      this._createFilesOnError = null;
-      if (createdHandler) {
-        createdHandler(Cu.cloneInto(aMessage.data, this.mm.content));
-      }
-      break;
+  _sendSyncMessage(msgname, msg) {
+    if (!this.SP_SYNC_MESSAGES.includes(msgname)) {
+      dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
+    }
+    let result = this.mm.sendSyncMessage(msgname, msg);
+    return Cu.cloneInto(result, this);
+  }
 
-    case "SpecialPowers.FilesError":
-      var errorHandler = this._createFilesOnError;
-      this._createFilesOnSuccess = null;
-      this._createFilesOnError = null;
-      if (errorHandler) {
-        errorHandler(aMessage.data);
-      }
-      break;
+  _sendAsyncMessage(msgname, msg) {
+    if (!this.SP_ASYNC_MESSAGES.includes(msgname)) {
+      dump("TEST-INFO | specialpowers.js |  Unexpected SP message: " + msgname + "\n");
+    }
+    this.mm.sendAsyncMessage(msgname, msg);
+  }
+
+  _addMessageListener(msgname, listener) {
+    this.mm.addMessageListener(msgname, listener);
+    this.mm.sendAsyncMessage("SPPAddNestedMessageListener", { name: msgname });
+  }
 
-    case "SPServiceWorkerCleanupComplete": {
-      let id = aMessage.data.id;
-      // It's possible for us to receive requests for other SpecialPowers
-      // instances, ignore them.
-      if (this._serviceWorkerCleanUpRequests.has(id)) {
-        let resolve = this._serviceWorkerCleanUpRequests.get(id);
-        this._serviceWorkerCleanUpRequests.delete(id);
-        resolve();
-      }
-      break;
-    }
+  _removeMessageListener(msgname, listener) {
+    this.mm.removeMessageListener(msgname, listener);
+  }
+
+  registerProcessCrashObservers() {
+    this.mm.addMessageListener("SPProcessCrashService", this._messageListener);
+    this.mm.sendSyncMessage("SPProcessCrashService", { op: "register-observer" });
+  }
+
+  unregisterProcessCrashObservers() {
+    this.mm.removeMessageListener("SPProcessCrashService", this._messageListener);
+    this.mm.sendSyncMessage("SPProcessCrashService", { op: "unregister-observer" });
   }
 
-  return true;
-};
+  _messageReceived(aMessage) {
+    switch (aMessage.name) {
+      case "SPProcessCrashService":
+        if (aMessage.json.type == "crash-observed") {
+          for (let e of aMessage.json.dumpIDs) {
+            this._encounteredCrashDumpFiles.push(e.id + "." + e.extension);
+          }
+        }
+        break;
 
-SpecialPowers.prototype.quit = function() {
-  this.mm.sendAsyncMessage("SpecialPowers.Quit", {});
-};
+      case "SPPingService":
+        if (aMessage.json.op == "pong") {
+          var handler = this._pongHandlers.shift();
+          if (handler) {
+            handler();
+          }
+          if (this._grandChildFrameMM) {
+            this._grandChildFrameMM.sendAsyncMessage("SPPingService", { op: "pong" });
+          }
+        }
+        break;
+
+      case "SPServiceWorkerRegistered":
+        this._serviceWorkerRegistered = aMessage.data.registered;
+        break;
 
-// fileRequests is an array of file requests. Each file request is an object.
-// A request must have a field |name|, which gives the base of the name of the
-// file to be created in the profile directory. If the request has a |data| field
-// then that data will be written to the file.
-SpecialPowers.prototype.createFiles = function(fileRequests, onCreation, onError) {
-  if (this._createFilesOnSuccess || this._createFilesOnError) {
-    onError("Already waiting for SpecialPowers.createFiles() to finish.");
-    return;
+      case "SpecialPowers.FilesCreated":
+        var createdHandler = this._createFilesOnSuccess;
+        this._createFilesOnSuccess = null;
+        this._createFilesOnError = null;
+        if (createdHandler) {
+          createdHandler(Cu.cloneInto(aMessage.data, this.mm.content));
+        }
+        break;
+
+      case "SpecialPowers.FilesError":
+        var errorHandler = this._createFilesOnError;
+        this._createFilesOnSuccess = null;
+        this._createFilesOnError = null;
+        if (errorHandler) {
+          errorHandler(aMessage.data);
+        }
+        break;
+
+      case "SPServiceWorkerCleanupComplete": {
+        let id = aMessage.data.id;
+        // It's possible for us to receive requests for other SpecialPowers
+        // instances, ignore them.
+        if (this._serviceWorkerCleanUpRequests.has(id)) {
+          let resolve = this._serviceWorkerCleanUpRequests.get(id);
+          this._serviceWorkerCleanUpRequests.delete(id);
+          resolve();
+        }
+        break;
+      }
+    }
+
+    return true;
+  }
+
+  quit() {
+    this.mm.sendAsyncMessage("SpecialPowers.Quit", {});
   }
 
-  this._createFilesOnSuccess = onCreation;
-  this._createFilesOnError = onError;
-  this.mm.sendAsyncMessage("SpecialPowers.CreateFiles", fileRequests);
-};
-
-// Remove the files that were created using |SpecialPowers.createFiles()|.
-// This will be automatically called by |SimpleTest.finish()|.
-SpecialPowers.prototype.removeFiles = function() {
-  this.mm.sendAsyncMessage("SpecialPowers.RemoveFiles", {});
-};
-
-SpecialPowers.prototype.executeAfterFlushingMessageQueue = function(aCallback) {
-  this._pongHandlers.push(aCallback);
-  this.mm.sendAsyncMessage("SPPingService", { op: "ping" });
-};
-
-SpecialPowers.prototype.nestedFrameSetup = function() {
-  let self = this;
-  Services.obs.addObserver(function onRemoteBrowserShown(subject, topic, data) {
-    let frameLoader = subject;
-    // get a ref to the app <iframe>
-    let frame = frameLoader.ownerElement;
-    let frameId = frame.getAttribute("id");
-    if (frameId === "nested-parent-frame") {
-      Services.obs.removeObserver(onRemoteBrowserShown, "remote-browser-shown");
-
-      let mm = frame.frameLoader.messageManager;
-      self._grandChildFrameMM = mm;
-
-      self.SP_SYNC_MESSAGES.forEach(function(msgname) {
-        mm.addMessageListener(msgname, function(msg) {
-          return self._sendSyncMessage(msgname, msg.data)[0];
-        });
-      });
-      self.SP_ASYNC_MESSAGES.forEach(function(msgname) {
-        mm.addMessageListener(msgname, function(msg) {
-          self._sendAsyncMessage(msgname, msg.data);
-        });
-      });
-      mm.addMessageListener("SPPAddNestedMessageListener", function(msg) {
-        self._addMessageListener(msg.json.name, function(aMsg) {
-          mm.sendAsyncMessage(aMsg.name, aMsg.data);
-          });
-      });
-
-      mm.loadFrameScript("resource://specialpowers/specialpowersFrameScript.js", false);
-
-      let frameScript = "SpecialPowers.prototype.IsInNestedFrame=true;";
-      mm.loadFrameScript("data:," + frameScript, false);
-    }
-  }, "remote-browser-shown");
-};
-
-SpecialPowers.prototype.registeredServiceWorkers = function() {
-  // For the time being, if parent_intercept is false, we can assume that
-  // ServiceWorkers registered by the current test are all known to the SWM in
-  // this process.
-  if (!Services.prefs.getBoolPref("dom.serviceWorkers.parent_intercept", false)) {
-    let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
-                .getService(Ci.nsIServiceWorkerManager);
-    let regs = swm.getAllRegistrations();
-
-    // XXX This is shared with SpecialPowersObserverAPI.js
-    let workers = new Array(regs.length);
-    for (let i = 0; i < workers.length; ++i) {
-      let { scope, scriptSpec } = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
-      workers[i] = { scope, scriptSpec };
+  // fileRequests is an array of file requests. Each file request is an object.
+  // A request must have a field |name|, which gives the base of the name of the
+  // file to be created in the profile directory. If the request has a |data| field
+  // then that data will be written to the file.
+  createFiles(fileRequests, onCreation, onError) {
+    if (this._createFilesOnSuccess || this._createFilesOnError) {
+      onError("Already waiting for SpecialPowers.createFiles() to finish.");
+      return;
     }
 
-    return workers;
+    this._createFilesOnSuccess = onCreation;
+    this._createFilesOnError = onError;
+    this.mm.sendAsyncMessage("SpecialPowers.CreateFiles", fileRequests);
+  }
+
+  // Remove the files that were created using |SpecialPowers.createFiles()|.
+  // This will be automatically called by |SimpleTest.finish()|.
+  removeFiles() {
+    this.mm.sendAsyncMessage("SpecialPowers.RemoveFiles", {});
+  }
+
+  executeAfterFlushingMessageQueue(aCallback) {
+    this._pongHandlers.push(aCallback);
+    this.mm.sendAsyncMessage("SPPingService", { op: "ping" });
   }
 
-  // Please see the comment in SpecialPowersObserver.jsm above
-  // this._serviceWorkerListener's assignment for what this returns.
-  if (this._serviceWorkerRegistered) {
-    // This test registered at least one service worker. Send a synchronous
-    // call to the parent to make sure that it called unregister on all of its
-    // service workers.
-    let { workers } = this._sendSyncMessage("SPCheckServiceWorkers")[0];
-    return workers;
+  nestedFrameSetup() {
+    let self = this;
+    Services.obs.addObserver(function onRemoteBrowserShown(subject, topic, data) {
+      let frameLoader = subject;
+      // get a ref to the app <iframe>
+      let frame = frameLoader.ownerElement;
+      let frameId = frame.getAttribute("id");
+      if (frameId === "nested-parent-frame") {
+        Services.obs.removeObserver(onRemoteBrowserShown, "remote-browser-shown");
+
+        let mm = frame.frameLoader.messageManager;
+        self._grandChildFrameMM = mm;
+
+        self.SP_SYNC_MESSAGES.forEach(function(msgname) {
+          mm.addMessageListener(msgname, function(msg) {
+            return self._sendSyncMessage(msgname, msg.data)[0];
+          });
+        });
+        self.SP_ASYNC_MESSAGES.forEach(function(msgname) {
+          mm.addMessageListener(msgname, function(msg) {
+            self._sendAsyncMessage(msgname, msg.data);
+          });
+        });
+        mm.addMessageListener("SPPAddNestedMessageListener", function(msg) {
+          self._addMessageListener(msg.json.name, function(aMsg) {
+            mm.sendAsyncMessage(aMsg.name, aMsg.data);
+            });
+        });
+
+        mm.loadFrameScript("resource://specialpowers/specialpowersFrameScript.js", false);
+
+        let frameScript = "SpecialPowers.prototype.IsInNestedFrame=true;";
+        mm.loadFrameScript("data:," + frameScript, false);
+      }
+    }, "remote-browser-shown");
   }
 
-  return [];
-};
+  registeredServiceWorkers() {
+    // For the time being, if parent_intercept is false, we can assume that
+    // ServiceWorkers registered by the current test are all known to the SWM in
+    // this process.
+    if (!Services.prefs.getBoolPref("dom.serviceWorkers.parent_intercept", false)) {
+      let swm = Cc["@mozilla.org/serviceworkers/manager;1"]
+                  .getService(Ci.nsIServiceWorkerManager);
+      let regs = swm.getAllRegistrations();
+
+      // XXX This is shared with SpecialPowersObserverAPI.js
+      let workers = new Array(regs.length);
+      for (let i = 0; i < workers.length; ++i) {
+        let { scope, scriptSpec } = regs.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo);
+        workers[i] = { scope, scriptSpec };
+      }
+
+      return workers;
+    }
 
-SpecialPowers.prototype._removeServiceWorkerData = function(messageName) {
-  return new Promise(resolve => {
-    let id = Cc["@mozilla.org/uuid-generator;1"]
-               .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
-    this._serviceWorkerCleanUpRequests.set(id, resolve);
-    this._sendAsyncMessage(messageName, { id });
-  });
-};
+    // Please see the comment in SpecialPowersObserver.jsm above
+    // this._serviceWorkerListener's assignment for what this returns.
+    if (this._serviceWorkerRegistered) {
+      // This test registered at least one service worker. Send a synchronous
+      // call to the parent to make sure that it called unregister on all of its
+      // service workers.
+      let { workers } = this._sendSyncMessage("SPCheckServiceWorkers")[0];
+      return workers;
+    }
+
+    return [];
+  }
+
+  _removeServiceWorkerData(messageName) {
+    return new Promise(resolve => {
+      let id = Cc["@mozilla.org/uuid-generator;1"]
+                 .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+      this._serviceWorkerCleanUpRequests.set(id, resolve);
+      this._sendAsyncMessage(messageName, { id });
+    });
+  }
+}
 
 // Attach our API to the window.
 function attachSpecialPowersToWindow(aWindow, mm) {
   try {
     if ((aWindow !== null) &&
         (aWindow !== undefined) &&
         (aWindow.wrappedJSObject) &&
         !(aWindow.wrappedJSObject.SpecialPowers)) {
--- a/testing/specialpowers/content/SpecialPowersAPI.jsm
+++ b/testing/specialpowers/content/SpecialPowersAPI.jsm
@@ -36,31 +36,16 @@ ChromeUtils.defineModuleGetter(this, "Pe
 // Allow stuff from this scope to be accessed from non-privileged scopes. This
 // would crash if used outside of automation.
 Cu.forcePermissiveCOWs();
 
 function bindDOMWindowUtils(aWindow) {
   return aWindow && WrapPrivileged.wrap(aWindow.windowUtils);
 }
 
-function SpecialPowersAPI() {
-  this._consoleListeners = [];
-  this._encounteredCrashDumpFiles = [];
-  this._unexpectedCrashDumpFiles = { };
-  this._crashDumpDir = null;
-  this._mfl = null;
-  this._prefEnvUndoStack = [];
-  this._pendingPrefs = [];
-  this._applyingPrefs = false;
-  this._permissionsUndoStack = [];
-  this._pendingPermissions = [];
-  this._applyingPermissions = false;
-  this._observingPermissions = false;
-}
-
 
 // SPConsoleListener reflects nsIConsoleMessage objects into JS in a
 // tidy, XPCOM-hiding way.  Messages that are nsIScriptError objects
 // have their properties exposed in detail.  It also auto-unregisters
 // itself when it receives a "sentinel" message.
 function SPConsoleListener(callback) {
   this.callback = callback;
 }
@@ -123,17 +108,35 @@ SPConsoleListener.prototype = {
       Services.console.unregisterListener(this);
     }
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.nsIConsoleListener,
                                           Ci.nsIObserver]),
 };
 
-SpecialPowersAPI.prototype = {
+class SpecialPowersAPI {
+  constructor() {
+    this._consoleListeners = [];
+    this._encounteredCrashDumpFiles = [];
+    this._unexpectedCrashDumpFiles = { };
+    this._crashDumpDir = null;
+    this._mfl = null;
+    this._applyingPrefs = false;
+    this._applyingPermissions = false;
+    this._observingPermissions = false;
+    this._asyncObservers = new WeakMap();
+    this._xpcomabi = null;
+    this._os = null;
+    this._pu = null;
+
+
+    this._nextExtensionID = 0;
+    this._extensionListeners = null;
+  }
 
   /*
    * Privileged object wrapping API
    *
    * Usage:
    *   var wrapper = SpecialPowers.wrap(obj);
    *   wrapper.privilegedMethod(); wrapper.privilegedProperty;
    *   obj === SpecialPowers.unwrap(wrapper);
@@ -153,71 +156,71 @@ SpecialPowersAPI.prototype = {
    *
    *  - The wrapper cannot see expando properties on unprivileged DOM objects.
    *    That is to say, the wrapper uses Xray delegation.
    *
    *  - The wrapper sometimes guesses certain ES5 attributes for returned
    *    properties. This is explained in a comment in the wrapper code above,
    *    and shouldn't be a problem.
    */
-  wrap(obj) { return WrapPrivileged.wrap(obj); },
-  unwrap(obj) { return WrapPrivileged.unwrap(obj); },
-  isWrapper(val) { return WrapPrivileged.isWrapper(val); },
+  wrap(obj) { return WrapPrivileged.wrap(obj); }
+  unwrap(obj) { return WrapPrivileged.unwrap(obj); }
+  isWrapper(val) { return WrapPrivileged.isWrapper(val); }
 
   /*
    * When content needs to pass a callback or a callback object to an API
    * accessed over SpecialPowers, that API may sometimes receive arguments for
    * whom it is forbidden to create a wrapper in content scopes. As such, we
    * need a layer to wrap the values in SpecialPowers wrappers before they ever
    * reach content.
    */
-  wrapCallback(func) { return WrapPrivileged.wrapCallback(func); },
-  wrapCallbackObject(obj) { return WrapPrivileged.wrapCallbackObject(obj); },
+  wrapCallback(func) { return WrapPrivileged.wrapCallback(func); }
+  wrapCallbackObject(obj) { return WrapPrivileged.wrapCallbackObject(obj); }
 
   /*
    * Used for assigning a property to a SpecialPowers wrapper, without unwrapping
    * the value that is assigned.
    */
   setWrapped(obj, prop, val) {
     if (!WrapPrivileged.isWrapper(obj))
       throw new Error("You only need to use this for SpecialPowers wrapped objects");
 
     obj = WrapPrivileged.unwrap(obj);
     return Reflect.set(obj, prop, val);
-  },
+  }
 
   /*
    * Create blank privileged objects to use as out-params for privileged functions.
    */
   createBlankObject() {
     return {};
-  },
+  }
 
   /*
    * Because SpecialPowers wrappers don't preserve identity, comparing with ==
    * can be hazardous. Sometimes we can just unwrap to compare, but sometimes
    * wrapping the underlying object into a content scope is forbidden. This
    * function strips any wrappers if they exist and compare the underlying
    * values.
    */
   compare(a, b) {
     return WrapPrivileged.unwrap(a) === WrapPrivileged.unwrap(b);
-  },
+  }
 
   get MockFilePicker() {
     return MockFilePicker;
-  },
+  }
 
   get MockColorPicker() {
     return MockColorPicker;
-  },
+  }
 
   get MockPermissionPrompt() {
     return MockPermissionPrompt;
-  },
+  }
 
   /*
    * Load a privileged script that runs same-process. This is different from
    * |loadChromeScript|, which will run in the parent process in e10s mode.
    */
   loadPrivilegedScript(aFunction) {
     var str = "(" + aFunction.toString() + ")();";
     let gGlobalObject = Cu.getGlobalForObject(this);
@@ -229,17 +232,17 @@ SpecialPowersAPI.prototype = {
       let blob = new Blob([str], {type: "application/javascript"});
       let blobUrl = URL.createObjectURL(blob);
       Services.scriptloader.loadSubScript(blobUrl, sb);
     } catch (e) {
       throw WrapPrivileged.wrap(e);
     }
 
     return mc.port2;
-  },
+  }
 
   loadChromeScript(urlOrFunction, sandboxOptions) {
     // Create a unique id for this chrome script
     let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                           .getService(Ci.nsIUUIDGenerator);
     let id = uuidGenerator.generateUUID().toString();
 
     // Tells chrome code to evaluate this chrome script
@@ -352,63 +355,63 @@ SpecialPowersAPI.prototype = {
         }
       } else {
         // When we are running only a single mochitest, there is no test runner
         dump(msg + "\n");
       }
     };
 
     return this.wrap(chromeScript);
-  },
+  }
 
   importInMainProcess(importString) {
     var message = this._sendSyncMessage("SPImportInMainProcess", importString)[0];
     if (message.hadError) {
       throw new Error("SpecialPowers.importInMainProcess failed with error " + message.errorMessage);
     }
-  },
+  }
 
   get Services() {
     return WrapPrivileged.wrap(Services);
-  },
+  }
 
   /*
    * A getter for the privileged Components object we have.
    */
   getFullComponents() {
     return Components;
-  },
+  }
 
   /*
    * Convenient shortcuts to the standard Components abbreviations.
    */
-  get Cc() { return WrapPrivileged.wrap(this.getFullComponents().classes); },
-  get Ci() { return WrapPrivileged.wrap(this.getFullComponents().interfaces); },
-  get Cu() { return WrapPrivileged.wrap(this.getFullComponents().utils); },
-  get Cr() { return WrapPrivileged.wrap(this.getFullComponents().results); },
+  get Cc() { return WrapPrivileged.wrap(this.getFullComponents().classes); }
+  get Ci() { return WrapPrivileged.wrap(this.getFullComponents().interfaces); }
+  get Cu() { return WrapPrivileged.wrap(this.getFullComponents().utils); }
+  get Cr() { return WrapPrivileged.wrap(this.getFullComponents().results); }
 
   getDOMWindowUtils(aWindow) {
     if (aWindow == this.window.get() && this.DOMWindowUtils != null)
       return this.DOMWindowUtils;
 
     return bindDOMWindowUtils(aWindow);
-  },
+  }
 
   /*
    * A method to get a DOMParser that can't parse XUL.
    */
   getNoXULDOMParser() {
     // If we create it with a system subject principal (so it gets a
     // nullprincipal), it won't be able to parse XUL by default.
     return WrapPrivileged.wrap(new DOMParser());
-  },
+  }
 
-  get InspectorUtils() { return WrapPrivileged.wrap(InspectorUtils); },
+  get InspectorUtils() { return WrapPrivileged.wrap(InspectorUtils); }
 
-  get PromiseDebugging() { return WrapPrivileged.wrap(PromiseDebugging); },
+  get PromiseDebugging() { return WrapPrivileged.wrap(PromiseDebugging); }
 
   waitForCrashes(aExpectingProcessCrash) {
     return new Promise((resolve, reject) => {
       if (!aExpectingProcessCrash) {
         resolve();
       }
 
       var crashIds = this._encounteredCrashDumpFiles.filter((filename) => {
@@ -423,76 +426,76 @@ SpecialPowersAPI.prototype = {
         resolve();
       }
 
       this._addMessageListener("SPProcessCrashManagerWait", messageListener);
       this._sendAsyncMessage("SPProcessCrashManagerWait", {
         crashIds,
       });
     });
-  },
+  }
 
   removeExpectedCrashDumpFiles(aExpectingProcessCrash) {
     var success = true;
     if (aExpectingProcessCrash) {
       var message = {
         op: "delete-crash-dump-files",
         filenames: this._encounteredCrashDumpFiles,
       };
       if (!this._sendSyncMessage("SPProcessCrashService", message)[0]) {
         success = false;
       }
     }
     this._encounteredCrashDumpFiles.length = 0;
     return success;
-  },
+  }
 
   findUnexpectedCrashDumpFiles() {
     var self = this;
     var message = {
       op: "find-crash-dump-files",
       crashDumpFilesToIgnore: this._unexpectedCrashDumpFiles,
     };
     var crashDumpFiles = this._sendSyncMessage("SPProcessCrashService", message)[0];
     crashDumpFiles.forEach(function(aFilename) {
       self._unexpectedCrashDumpFiles[aFilename] = true;
     });
     return crashDumpFiles;
-  },
+  }
 
   removePendingCrashDumpFiles() {
     var message = {
       op: "delete-pending-crash-dump-files",
     };
     var removed = this._sendSyncMessage("SPProcessCrashService", message)[0];
     return removed;
-  },
+  }
 
   _setTimeout(callback) {
     // for mochitest-browser
     if (typeof this.chromeWindow != "undefined")
       this.chromeWindow.setTimeout(callback, 0);
     // for mochitest-plain
     else
       this.mm.content.setTimeout(callback, 0);
-  },
+  }
 
   _delayCallbackTwice(callback) {
      let delayedCallback = () => {
        let delayAgain = (aCallback) => {
          // Using this._setTimeout doesn't work here
          // It causes failures in mochtests that use
          // multiple pushPrefEnv calls
          // For chrome/browser-chrome mochitests
          this._setTimeout(aCallback);
        };
        delayAgain(delayAgain.bind(this, callback));
      };
      return delayedCallback;
-  },
+  }
 
   /* apply permissions to the system and when the test case is finished (SimpleTest.finish())
      we will revert the permission back to the original.
 
      inPermissions is an array of objects where each object has a type, action, context, ex:
      [{'type': 'SystemXHR', 'allow': 1, 'context': document},
       {'type': 'SystemXHR', 'allow': Ci.nsIPermissionManager.PROMPT_ACTION, 'context': document}]
 
@@ -590,17 +593,17 @@ SpecialPowersAPI.prototype = {
       }
       this._permissionsUndoStack.push(cleanupPermissions);
       this._pendingPermissions.push([pendingPermissions,
                                      this._delayCallbackTwice(callback)]);
       this._applyPermissions();
     } else {
       this._setTimeout(callback);
     }
-  },
+  }
 
   /*
    * This function should be used when specialpowers is in content process but
    * it want to get the notification from chrome space.
    *
    * This function will call Services.obs.addObserver in SpecialPowersObserver
    * (that is in chrome process) and forward the data received to SpecialPowers
    * via messageManager.
@@ -615,107 +618,52 @@ SpecialPowersAPI.prototype = {
    * ChromePowers.js
    */
   registerObservers(topic) {
     var msg = {
       "op": "add",
       "observerTopic": topic,
     };
     this._sendSyncMessage("SPObserverService", msg);
-  },
+  }
 
   permChangedProxy(aMessage) {
     let permission = aMessage.json.permission;
     let aData = aMessage.json.aData;
     this._permissionObserver.observe(permission, aData);
-  },
-
-  permissionObserverProxy: {
-    // 'this' in permChangedObserverProxy is the permChangedObserverProxy
-    // object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersAPI'
-    // object to call the member function in SpecialPowersAPI.
-    _specialPowersAPI: null,
-    observe(aSubject, aTopic, aData) {
-      if (aTopic == "perm-changed") {
-        var permission = aSubject.QueryInterface(Ci.nsIPermission);
-        this._specialPowersAPI._permissionObserver.observe(permission, aData);
-      }
-    },
-  },
+  }
 
   popPermissions(callback) {
     if (this._permissionsUndoStack.length > 0) {
       // See pushPermissions comment regarding delay.
       let cb = callback ? this._delayCallbackTwice(callback) : null;
       /* Each pop from the stack will yield an object {op/type/permission/value/url/appid/isInIsolatedMozBrowserElement} or null */
       this._pendingPermissions.push([this._permissionsUndoStack.pop(), cb]);
       this._applyPermissions();
     } else {
       if (this._observingPermissions) {
         this._observingPermissions = false;
         this._removeMessageListener("specialpowers-perm-changed", this.permChangedProxy.bind(this));
       }
       this._setTimeout(callback);
     }
-  },
+  }
 
   flushPermissions(callback) {
     while (this._permissionsUndoStack.length > 1)
       this.popPermissions(null);
 
     this.popPermissions(callback);
-  },
+  }
 
 
   setTestPluginEnabledState(newEnabledState, pluginName) {
     return this._sendSyncMessage("SPSetTestPluginEnabledState",
                                  { newEnabledState, pluginName })[0];
-  },
-
-
-  _permissionObserver: {
-    _self: null,
-    _lastPermission: {},
-    _callBack: null,
-    _nextCallback: null,
-    _obsDataMap: {
-      "deleted": "remove",
-      "added": "add",
-    },
-    observe(permission, aData) {
-      if (this._self._applyingPermissions) {
-        if (permission.type == this._lastPermission.type) {
-          this._self._setTimeout(this._callback);
-          this._self._setTimeout(this._nextCallback);
-          this._callback = null;
-          this._nextCallback = null;
-        }
-      } else {
-        var found = false;
-        for (var i = 0; !found && i < this._self._permissionsUndoStack.length; i++) {
-          var undos = this._self._permissionsUndoStack[i];
-          for (var j = 0; j < undos.length; j++) {
-            var undo = undos[j];
-            if (undo.op == this._obsDataMap[aData] &&
-                undo.type == permission.type) {
-              // Remove this undo item if it has been done by others(not
-              // specialpowers itself.)
-              undos.splice(j, 1);
-              found = true;
-              break;
-            }
-          }
-          if (!undos.length) {
-            // Remove the empty row in permissionsUndoStack
-            this._self._permissionsUndoStack.splice(i, 1);
-          }
-        }
-      }
-    },
-  },
+  }
 
   /*
     Iterate through one atomic set of permissions actions and perform allow/deny as appropriate.
     All actions performed must modify the relevant permission.
   */
   _applyPermissions() {
     if (this._applyingPermissions || this._pendingPermissions.length <= 0) {
       return;
@@ -737,29 +685,29 @@ SpecialPowersAPI.prototype = {
         // Now apply any permissions that may have been queued while we were applying
         self._applyPermissions();
     };
 
     for (var idx in pendingActions) {
       var perm = pendingActions[idx];
       this._sendSyncMessage("SPPermissionManager", perm)[0];
     }
-  },
+  }
 
   /**
    * Helper to resolve a promise by calling the resolve function and call an
    * optional callback.
    */
   _resolveAndCallOptionalCallback(resolveFn, callback = null) {
     resolveFn();
 
     if (callback) {
       callback();
     }
-  },
+  }
 
   /**
    * Take in a list of pref changes to make, then invokes |callback| and resolves
    * the returned Promise once those changes have taken effect.  When the test
    * finishes, these changes are reverted.
    *
    * |inPrefs| must be an object with up to two properties: "set" and "clear".
    * pushPrefEnv will set prefs as indicated in |inPrefs.set| and will unset
@@ -866,58 +814,58 @@ SpecialPowersAPI.prototype = {
         this._prefEnvUndoStack.push(cleanupActions);
         this._pendingPrefs.push([pendingActions,
                                  this._delayCallbackTwice(done)]);
         this._applyPrefs();
       } else {
         this._setTimeout(done);
       }
     });
-  },
+  }
 
   popPrefEnv(callback = null) {
     return new Promise(resolve => {
       let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback);
       if (this._prefEnvUndoStack.length > 0) {
         // See pushPrefEnv comment regarding delay.
         let cb = this._delayCallbackTwice(done);
         /* Each pop will have a valid block of preferences */
         this._pendingPrefs.push([this._prefEnvUndoStack.pop(), cb]);
         this._applyPrefs();
       } else {
         this._setTimeout(done);
       }
     });
-  },
+  }
 
   flushPrefEnv(callback = null) {
     while (this._prefEnvUndoStack.length > 1)
       this.popPrefEnv(null);
 
     return new Promise(resolve => {
       let done = this._resolveAndCallOptionalCallback.bind(this, resolve, callback);
       this.popPrefEnv(done);
     });
-  },
+  }
 
   _isPrefActionNeeded(prefAction) {
     if (prefAction.action === "clear") {
       return Services.prefs.prefHasUserValue(prefAction.name);
     } else if (prefAction.action === "set") {
       try {
         let currentValue  = this._getPref(prefAction.name, prefAction.type, {});
         return currentValue != prefAction.value;
       } catch (e) {
         // If the preference is not defined yet, setting the value will have an effect.
         return true;
       }
     }
     // Only "clear" and "set" actions are supported.
     return false;
-  },
+  }
 
   /*
     Iterate through one atomic set of pref actions and perform sets/clears as appropriate.
     All actions performed must modify the relevant pref.
   */
   _applyPrefs() {
     if (this._applyingPrefs || this._pendingPrefs.length <= 0) {
       return;
@@ -961,279 +909,267 @@ SpecialPowersAPI.prototype = {
     for (var idx in pendingActions) {
       var pref = pendingActions[idx];
       if (pref.action == "set") {
         this._setPref(pref.name, pref.type, pref.value, pref.Iid);
       } else if (pref.action == "clear") {
         this.clearUserPref(pref.name);
       }
     }
-  },
-
-  _proxiedObservers: {
-    "specialpowers-http-notify-request": function(aMessage) {
-      let uri = aMessage.json.uri;
-      Services.obs.notifyObservers(null, "specialpowers-http-notify-request", uri);
-    },
-
-    "specialpowers-service-worker-shutdown": function(aMessage) {
-      Services.obs.notifyObservers(null, "specialpowers-service-worker-shutdown");
-    },
-  },
+  }
 
   _addObserverProxy(notification) {
     if (notification in this._proxiedObservers) {
       this._addMessageListener(notification, this._proxiedObservers[notification]);
     }
-  },
+  }
   _removeObserverProxy(notification) {
     if (notification in this._proxiedObservers) {
       this._removeMessageListener(notification, this._proxiedObservers[notification]);
     }
-  },
+  }
 
   addObserver(obs, notification, weak) {
     this._addObserverProxy(notification);
     obs = Cu.waiveXrays(obs);
     if (typeof obs == "object" && obs.observe.name != "SpecialPowersCallbackWrapper")
       obs.observe = WrapPrivileged.wrapCallback(obs.observe);
     Services.obs.addObserver(obs, notification, weak);
-  },
+  }
   removeObserver(obs, notification) {
     this._removeObserverProxy(notification);
     Services.obs.removeObserver(Cu.waiveXrays(obs), notification);
-  },
+  }
   notifyObservers(subject, topic, data) {
     Services.obs.notifyObservers(subject, topic, data);
-  },
+  }
 
   /**
    * An async observer is useful if you're listening for a
    * notification that normally is only used by C++ code or chrome
    * code (so it runs in the SystemGroup), but we need to know about
    * it for a test (which runs as web content). If we used
    * addObserver, we would assert when trying to enter web content
    * from a runnabled labeled by the SystemGroup. An async observer
    * avoids this problem.
    */
-  _asyncObservers: new WeakMap(),
   addAsyncObserver(obs, notification, weak) {
     obs = Cu.waiveXrays(obs);
     if (typeof obs == "object" && obs.observe.name != "SpecialPowersCallbackWrapper") {
       obs.observe = WrapPrivileged.wrapCallback(obs.observe);
     }
     let asyncObs = (...args) => {
       Services.tm.dispatchToMainThread(() => {
         if (typeof obs == "function") {
           obs(...args);
         } else {
           obs.observe.call(undefined, ...args);
         }
       });
     };
     this._asyncObservers.set(obs, asyncObs);
     Services.obs.addObserver(asyncObs, notification, weak);
-  },
+  }
   removeAsyncObserver(obs, notification) {
     let asyncObs = this._asyncObservers.get(Cu.waiveXrays(obs));
     Services.obs.removeObserver(asyncObs, notification);
-  },
+  }
 
   can_QI(obj) {
     return obj.QueryInterface !== undefined;
-  },
+  }
   do_QueryInterface(obj, iface) {
     return obj.QueryInterface(Ci[iface]);
-  },
+  }
 
   call_Instanceof(obj1, obj2) {
      obj1 = WrapPrivileged.unwrap(obj1);
      obj2 = WrapPrivileged.unwrap(obj2);
      return obj1 instanceof obj2;
-  },
+  }
 
   // Returns a privileged getter from an object. GetOwnPropertyDescriptor does
   // not work here because xray wrappers don't properly implement it.
   //
   // This terribleness is used by dom/base/test/test_object.html because
   // <object> and <embed> tags will spawn plugins if their prototype is touched,
   // so we need to get and cache the getter of |hasRunningPlugin| if we want to
   // call it without paradoxically spawning the plugin.
   do_lookupGetter(obj, name) {
     return Object.prototype.__lookupGetter__.call(obj, name);
-  },
+  }
 
   // Mimic the get*Pref API
   getBoolPref(prefName, defaultValue) {
     return this._getPref(prefName, "BOOL", { defaultValue });
-  },
+  }
   getIntPref(prefName, defaultValue) {
     return this._getPref(prefName, "INT", { defaultValue });
-  },
+  }
   getCharPref(prefName, defaultValue) {
     return this._getPref(prefName, "CHAR", { defaultValue });
-  },
+  }
   getComplexValue(prefName, iid) {
     return this._getPref(prefName, "COMPLEX", { iid });
-  },
+  }
 
   // Mimic the set*Pref API
   setBoolPref(prefName, value) {
     return this._setPref(prefName, "BOOL", value);
-  },
+  }
   setIntPref(prefName, value) {
     return this._setPref(prefName, "INT", value);
-  },
+  }
   setCharPref(prefName, value) {
     return this._setPref(prefName, "CHAR", value);
-  },
+  }
   setComplexValue(prefName, iid, value) {
     return this._setPref(prefName, "COMPLEX", value, iid);
-  },
+  }
 
   // Mimic the clearUserPref API
   clearUserPref(prefName) {
     let msg = {
       op: "clear",
       prefName,
       prefType: "",
     };
     this._sendSyncMessage("SPPrefService", msg);
-  },
+  }
 
   // Private pref functions to communicate to chrome
   _getPref(prefName, prefType, { defaultValue, iid }) {
     let msg = {
       op: "get",
       prefName,
       prefType,
       iid, // Only used with complex prefs
       defaultValue, // Optional default value
     };
     let val = this._sendSyncMessage("SPPrefService", msg);
     if (val == null || val[0] == null) {
       throw new Error(`Error getting pref '${prefName}'`);
     }
     return val[0];
-  },
+  }
   _setPref(prefName, prefType, prefValue, iid) {
     let msg = {
       op: "set",
       prefName,
       prefType,
       iid, // Only used with complex prefs
       prefValue,
     };
     return this._sendSyncMessage("SPPrefService", msg)[0];
-  },
+  }
 
   _getDocShell(window) {
     return window.docShell;
-  },
+  }
   _getMUDV(window) {
     return this._getDocShell(window).contentViewer;
-  },
+  }
   // XXX: these APIs really ought to be removed, they're not e10s-safe.
   // (also they're pretty Firefox-specific)
   _getTopChromeWindow(window) {
     return window.docShell.rootTreeItem.domWindow
                  .QueryInterface(Ci.nsIDOMChromeWindow);
-  },
+  }
   _getAutoCompletePopup(window) {
     return this._getTopChromeWindow(window).document
                                            .getElementById("PopupAutoComplete");
-  },
+  }
   addAutoCompletePopupEventListener(window, eventname, listener) {
     this._getAutoCompletePopup(window).addEventListener(eventname,
                                                         listener);
-  },
+  }
   removeAutoCompletePopupEventListener(window, eventname, listener) {
     this._getAutoCompletePopup(window).removeEventListener(eventname,
                                                            listener);
-  },
+  }
   get formHistory() {
     let tmp = {};
     ChromeUtils.import("resource://gre/modules/FormHistory.jsm", tmp);
     return WrapPrivileged.wrap(tmp.FormHistory);
-  },
+  }
   getFormFillController(window) {
     return Cc["@mozilla.org/satchel/form-fill-controller;1"]
              .getService(Ci.nsIFormFillController);
-  },
+  }
   attachFormFillControllerTo(window) {
     this.getFormFillController()
         .attachPopupElementToBrowser(this._getDocShell(window),
                                      this._getAutoCompletePopup(window));
-  },
+  }
   detachFormFillControllerFrom(window) {
     this.getFormFillController().detachFromBrowser(this._getDocShell(window));
-  },
+  }
   isBackButtonEnabled(window) {
     return !this._getTopChromeWindow(window).document
                                       .getElementById("Browser:Back")
                                       .hasAttribute("disabled");
-  },
+  }
   // XXX end of problematic APIs
 
   addChromeEventListener(type, listener, capture, allowUntrusted) {
     this.mm.addEventListener(type, listener, capture, allowUntrusted);
-  },
+  }
   removeChromeEventListener(type, listener, capture) {
     this.mm.removeEventListener(type, listener, capture);
-  },
+  }
 
   // Note: each call to registerConsoleListener MUST be paired with a
   // call to postConsoleSentinel; when the callback receives the
   // sentinel it will unregister itself (_after_ calling the
   // callback).  SimpleTest.expectConsoleMessages does this for you.
   // If you register more than one console listener, a call to
   // postConsoleSentinel will zap all of them.
   registerConsoleListener(callback) {
     let listener = new SPConsoleListener(callback);
     Services.console.registerListener(listener);
 
     // listen for dom/console events as well
     Services.obs.addObserver(listener, "console-api-log-event");
-  },
+  }
   postConsoleSentinel() {
     Services.console.logStringMessage("SENTINEL");
-  },
+  }
   resetConsole() {
     Services.console.reset();
-  },
+  }
 
   getFullZoom(window) {
     return this._getMUDV(window).fullZoom;
-  },
+  }
   getDeviceFullZoom(window) {
     return this._getMUDV(window).deviceFullZoom;
-  },
+  }
   setFullZoom(window, zoom) {
     this._getMUDV(window).fullZoom = zoom;
-  },
+  }
   getTextZoom(window) {
     return this._getMUDV(window).textZoom;
-  },
+  }
   setTextZoom(window, zoom) {
     this._getMUDV(window).textZoom = zoom;
-  },
+  }
 
   getOverrideDPPX(window) {
     return this._getMUDV(window).overrideDPPX;
-  },
+  }
   setOverrideDPPX(window, dppx) {
     this._getMUDV(window).overrideDPPX = dppx;
-  },
+  }
 
   emulateMedium(window, mediaType) {
     this._getMUDV(window).emulateMedium(mediaType);
-  },
+  }
   stopEmulatingMedium(window) {
     this._getMUDV(window).stopEmulatingMedium();
-  },
+  }
 
   snapshotWindowWithOptions(win, rect, bgcolor, options) {
     var el = this.window.get().document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
     if (rect === undefined) {
       rect = { top: win.scrollY, left: win.scrollX,
                width: win.innerWidth, height: win.innerHeight };
     }
     if (bgcolor === undefined) {
@@ -1252,50 +1188,50 @@ SpecialPowersAPI.prototype = {
       flags |= options[option] && ctx[option];
     }
 
     ctx.drawWindow(win,
                    rect.left, rect.top, rect.width, rect.height,
                    bgcolor,
                    flags);
     return el;
-  },
+  }
 
   snapshotWindow(win, withCaret, rect, bgcolor) {
     return this.snapshotWindowWithOptions(win, rect, bgcolor,
                                           { DRAWWINDOW_DRAW_CARET: withCaret });
-  },
+  }
 
   snapshotRect(win, rect, bgcolor) {
     return this.snapshotWindowWithOptions(win, rect, bgcolor);
-  },
+  }
 
   gc() {
     this.DOMWindowUtils.garbageCollect();
-  },
+  }
 
   forceGC() {
     Cu.forceGC();
-  },
+  }
 
   forceShrinkingGC() {
     Cu.forceShrinkingGC();
-  },
+  }
 
   forceCC() {
     Cu.forceCC();
-  },
+  }
 
   finishCC() {
     Cu.finishCC();
-  },
+  }
 
   ccSlice(budget) {
     Cu.ccSlice(budget);
-  },
+  }
 
   // Due to various dependencies between JS objects and C++ objects, an ordinary
   // forceGC doesn't necessarily clear all unused objects, thus the GC and CC
   // needs to run several times and when no other JS is running.
   // The current number of iterations has been determined according to massive
   // cross platform testing.
   exactGC(callback) {
     let count = 0;
@@ -1307,165 +1243,161 @@ SpecialPowersAPI.prototype = {
           Cu.schedulePreciseGC(genGCCallback(cb));
         } else if (cb) {
           cb();
         }
       };
     }
 
     Cu.schedulePreciseGC(genGCCallback(callback));
-  },
+  }
 
   nondeterministicGetWeakMapKeys(m) {
     return ChromeUtils.nondeterministicGetWeakMapKeys(m);
-  },
+  }
 
   getMemoryReports() {
     try {
       Cc["@mozilla.org/memory-reporter-manager;1"]
         .getService(Ci.nsIMemoryReporterManager)
         .getReports(() => {}, null, () => {}, null, false);
     } catch (e) { }
-  },
+  }
 
   setGCZeal(zeal) {
     Cu.setGCZeal(zeal);
-  },
+  }
 
   isMainProcess() {
     try {
       return Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
     } catch (e) { }
     return true;
-  },
-
-  _xpcomabi: null,
+  }
 
   get XPCOMABI() {
     if (this._xpcomabi != null)
       return this._xpcomabi;
 
     var xulRuntime = Services.appinfo.QueryInterface(Ci.nsIXULRuntime);
 
     this._xpcomabi = xulRuntime.XPCOMABI;
     return this._xpcomabi;
-  },
+  }
 
   // The optional aWin parameter allows the caller to specify a given window in
   // whose scope the runnable should be dispatched. If aFun throws, the
   // exception will be reported to aWin.
   executeSoon(aFun, aWin) {
     // Create the runnable in the scope of aWin to avoid running into COWs.
     var runnable = {};
     if (aWin)
         runnable = Cu.createObjectIn(aWin);
     runnable.run = aFun;
     Cu.dispatch(runnable, aWin);
-  },
-
-  _os: null,
+  }
 
   get OS() {
     if (this._os != null)
       return this._os;
 
     this._os = Services.appinfo.OS;
     return this._os;
-  },
+  }
 
   addSystemEventListener(target, type, listener, useCapture) {
     Services.els.addSystemEventListener(target, type, listener, useCapture);
-  },
+  }
   removeSystemEventListener(target, type, listener, useCapture) {
     Services.els.removeSystemEventListener(target, type, listener, useCapture);
-  },
+  }
 
   // helper method to check if the event is consumed by either default group's
   // event listener or system group's event listener.
   defaultPreventedInAnyGroup(event) {
     // FYI: Event.defaultPrevented returns false in content context if the
     //      event is consumed only by system group's event listeners.
     return event.defaultPrevented;
-  },
+  }
 
   getDOMRequestService() {
     var serv = Services.DOMRequest;
     var res = {};
     var props = ["createRequest", "createCursor", "fireError", "fireSuccess",
                  "fireDone", "fireDetailedError"];
     for (var i in props) {
       let prop = props[i];
       res[prop] = function() { return serv[prop].apply(serv, arguments); };
     }
     return res;
-  },
+  }
 
   setLogFile(path) {
     this._mfl = new MozillaFileLogger(path);
-  },
+  }
 
   log(data) {
     this._mfl.log(data);
-  },
+  }
 
   closeLogFile() {
     this._mfl.close();
-  },
+  }
 
   addCategoryEntry(category, entry, value, persists, replace) {
     Services.catMan
       .addCategoryEntry(category, entry, value, persists, replace);
-  },
+  }
 
   deleteCategoryEntry(category, entry, persists) {
     Services.catMan
       .deleteCategoryEntry(category, entry, persists);
-  },
+  }
   openDialog(win, args) {
     return win.openDialog.apply(win, args);
-  },
+  }
   // This is a blocking call which creates and spins a native event loop
   spinEventLoop(win) {
     // simply do a sync XHR back to our windows location.
     var syncXHR = new win.XMLHttpRequest();
     syncXHR.open("GET", win.location, false);
     syncXHR.send();
-  },
+  }
 
   // :jdm gets credit for this.  ex: getPrivilegedProps(window, 'location.href');
   getPrivilegedProps(obj, props) {
     var parts = props.split(".");
     for (var i = 0; i < parts.length; i++) {
       var p = parts[i];
       if (obj[p] != undefined) {
         obj = obj[p];
       } else {
         return null;
       }
     }
     return obj;
-  },
+  }
 
   getFocusedElementForWindow(targetWindow, aDeep) {
     var outParam = {};
     Services.focus.getFocusedElementForWindow(targetWindow, aDeep, outParam);
     return outParam.value;
-  },
+  }
 
   get focusManager() {
     return Services.focus;
-  },
+  }
 
   activeWindow() {
     return Services.focus.activeWindow;
-  },
+  }
 
   focusedWindow() {
     return Services.focus.focusedWindow;
-  },
+  }
 
   focus(aWindow) {
     // This is called inside TestRunner._makeIframe without aWindow, because of assertions in oop mochitests
     // With aWindow, it is called in SimpleTest.waitForFocus to allow popup window opener focus switching
     if (aWindow)
       aWindow.focus();
     var mm = this.mm;
     if (aWindow) {
@@ -1474,17 +1406,17 @@ SpecialPowersAPI.prototype = {
         mm = windowMM;
       }
       /*
        * Otherwise (e.g. XUL chrome windows from mochitest-chrome which won't
        * have a message manager) just stick with "global".
        */
     }
     mm.sendAsyncMessage("SpecialPowers.Focus", {});
-  },
+  }
 
   getClipboardData(flavor, whichClipboard) {
     if (whichClipboard === undefined)
       whichClipboard = Services.clipboard.kGlobalClipboard;
 
     var xferable = Cc["@mozilla.org/widget/transferable;1"].
                    createInstance(Ci.nsITransferable);
     xferable.init(this._getDocShell(this.chromeWindow || this.mm.content.window)
@@ -1495,27 +1427,27 @@ SpecialPowersAPI.prototype = {
     try {
       xferable.getTransferData(flavor, data);
     } catch (e) {}
     data = data.value || null;
     if (data == null)
       return "";
 
     return data.QueryInterface(Ci.nsISupportsString).data;
-  },
+  }
 
   clipboardCopyString(str) {
     Cc["@mozilla.org/widget/clipboardhelper;1"].
       getService(Ci.nsIClipboardHelper).
       copyString(str);
-  },
+  }
 
   supportsSelectionClipboard() {
     return Services.clipboard.supportsSelectionClipboard();
-  },
+  }
 
   swapFactoryRegistration(cid, contractID, newFactory) {
     newFactory = Cu.waiveXrays(newFactory);
 
     var componentRegistrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
 
     var currentCID = componentRegistrar.contractIDToCID(contractID);
     var currentFactory = Components.manager.getClassObject(Cc[contractID],
@@ -1529,45 +1461,45 @@ SpecialPowersAPI.prototype = {
     }
 
     // Restore the original factory.
     componentRegistrar.registerFactory(cid,
                                        "",
                                        contractID,
                                        newFactory);
     return {"originalCID": currentCID};
-  },
+  }
 
   _getElement(aWindow, id) {
     return ((typeof(id) == "string") ?
         aWindow.document.getElementById(id) : id);
-  },
+  }
 
   dispatchEvent(aWindow, target, event) {
     var el = this._getElement(aWindow, target);
     return el.dispatchEvent(event);
-  },
+  }
 
   get isDebugBuild() {
     delete SpecialPowersAPI.prototype.isDebugBuild;
 
     var debug = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
     return SpecialPowersAPI.prototype.isDebugBuild = debug.isDebugBuild;
-  },
+  }
   assertionCount() {
     var debugsvc = Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2);
     return debugsvc.assertionCount;
-  },
+  }
 
   /**
    * Get the message manager associated with an <iframe mozbrowser>.
    */
   getBrowserFrameMessageManager(aFrameElement) {
     return this.wrap(aFrameElement.frameLoader.messageManager);
-  },
+  }
 
   _getPrincipalFromArg(arg) {
     let principal;
     let secMan = Services.scriptSecurityManager;
 
     if (typeof(arg) == "string") {
       // It's an URL.
       let uri = Services.io.newURI(arg);
@@ -1578,17 +1510,17 @@ SpecialPowersAPI.prototype = {
       principal = WrapPrivileged.unwrap(arg).nodePrincipal;
     } else {
       let uri = Services.io.newURI(arg.url);
       let attrs = arg.originAttributes || {};
       principal = secMan.createCodebasePrincipal(uri, attrs);
     }
 
     return principal;
-  },
+  }
 
   addPermission(type, allow, arg, expireType, expireTime) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return; // nothing to do
     }
 
     let permission;
@@ -1604,131 +1536,128 @@ SpecialPowersAPI.prototype = {
       "type": type,
       "permission": permission,
       "principal": principal,
       "expireType": (typeof expireType === "number") ? expireType : 0,
       "expireTime": (typeof expireTime === "number") ? expireTime : 0,
     };
 
     this._sendSyncMessage("SPPermissionManager", msg);
-  },
+  }
 
   removePermission(type, arg) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return; // nothing to do
     }
 
     var msg = {
       "op": "remove",
       "type": type,
       "principal": principal,
     };
 
     this._sendSyncMessage("SPPermissionManager", msg);
-  },
+  }
 
   hasPermission(type, arg) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return true; // system principals have all permissions
     }
 
     var msg = {
       "op": "has",
       "type": type,
       "principal": principal,
     };
 
     return this._sendSyncMessage("SPPermissionManager", msg)[0];
-  },
+  }
 
   testPermission(type, value, arg) {
     let principal = this._getPrincipalFromArg(arg);
     if (principal.isSystemPrincipal) {
       return true; // system principals have all permissions
     }
 
     var msg = {
       "op": "test",
       "type": type,
       "value": value,
       "principal": principal,
     };
     return this._sendSyncMessage("SPPermissionManager", msg)[0];
-  },
+  }
 
   isContentWindowPrivate(win) {
     return PrivateBrowsingUtils.isContentWindowPrivate(win);
-  },
+  }
 
   notifyObserversInParentProcess(subject, topic, data) {
     if (subject) {
       throw new Error("Can't send subject to another process!");
     }
     if (this.isMainProcess()) {
       this.notifyObservers(subject, topic, data);
       return;
     }
     var msg = {
       "op": "notify",
       "observerTopic": topic,
       "observerData": data,
     };
     this._sendSyncMessage("SPObserverService", msg);
-  },
+  }
 
   removeAllServiceWorkerData() {
     return WrapPrivileged.wrap(this._removeServiceWorkerData("SPRemoveAllServiceWorkers"));
-  },
+  }
 
   removeServiceWorkerDataForExampleDomain() {
     return WrapPrivileged.wrap(this._removeServiceWorkerData("SPRemoveServiceWorkerDataForExampleDomain"));
-  },
+  }
 
   cleanUpSTSData(origin, flags) {
     return this._sendSyncMessage("SPCleanUpSTSData", {origin, flags: flags || 0});
-  },
+  }
 
   requestDumpCoverageCounters(cb) {
     // We want to avoid a roundtrip between child and parent.
     if (!PerTestCoverageUtils.enabled) {
       return Promise.resolve();
     }
 
     return new Promise(resolve => {
       let messageListener = _ => {
         this._removeMessageListener("SPRequestDumpCoverageCounters", messageListener);
         resolve();
       };
 
       this._addMessageListener("SPRequestDumpCoverageCounters", messageListener);
       this._sendAsyncMessage("SPRequestDumpCoverageCounters", {});
     });
-  },
+  }
 
   requestResetCoverageCounters(cb) {
     // We want to avoid a roundtrip between child and parent.
     if (!PerTestCoverageUtils.enabled) {
       return Promise.resolve();
     }
 
     return new Promise(resolve => {
       let messageListener = _ => {
         this._removeMessageListener("SPRequestResetCoverageCounters", messageListener);
         resolve();
       };
 
       this._addMessageListener("SPRequestResetCoverageCounters", messageListener);
       this._sendAsyncMessage("SPRequestResetCoverageCounters", {});
     });
-  },
-
-  _nextExtensionID: 0,
-  _extensionListeners: null,
+  }
 
   loadExtension(ext, handler) {
     if (this._extensionListeners == null) {
       this._extensionListeners = new Set();
 
       this._addMessageListener("SPExtensionMessage", msg => {
         for (let listener of this._extensionListeners) {
           try {
@@ -1803,30 +1732,30 @@ SpecialPowersAPI.prototype = {
         } else {
           dump(`Unexpected: ${msg.data.type}\n`);
         }
       }
     };
 
     this._extensionListeners.add(listener);
     return extension;
-  },
+  }
 
   invalidateExtensionStorageCache() {
     this.notifyObserversInParentProcess(null, "extension-invalidate-storage-cache", "");
-  },
+  }
 
   allowMedia(window, enable) {
     this._getDocShell(window).allowMedia = enable;
-  },
+  }
 
   createChromeCache(name, url) {
     let principal = this._getPrincipalFromArg(url);
     return WrapPrivileged.wrap(new this.mm.content.CacheStorage(name, principal));
-  },
+  }
 
   loadChannelAndReturnStatus(url, loadUsingSystemPrincipal) {
     const BinaryInputStream =
         Components.Constructor("@mozilla.org/binaryinputstream;1",
                                "nsIBinaryInputStream",
                                "setInputStream");
 
     return new Promise(function(resolve) {
@@ -1853,19 +1782,17 @@ SpecialPowersAPI.prototype = {
       let uri = NetUtil.newURI(url);
       let channel = NetUtil.newChannel({uri, loadUsingSystemPrincipal});
 
       channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
       channel.QueryInterface(Ci.nsIHttpChannelInternal);
       channel.documentURI = uri;
       channel.asyncOpen(listener);
     });
-  },
-
-  _pu: null,
+  }
 
   get ParserUtils() {
     if (this._pu != null)
       return this._pu;
 
     let pu = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
     // We need to create and return our own wrapper.
     this._pu = {
@@ -1878,49 +1805,49 @@ SpecialPowersAPI.prototype = {
       parseFragment(fragment, flags, isXML, baseURL, element) {
         let baseURI = baseURL ? NetUtil.newURI(baseURL) : null;
         return pu.parseFragment(WrapPrivileged.unwrap(fragment),
                                 flags, isXML, baseURI,
                                 WrapPrivileged.unwrap(element));
       },
     };
     return this._pu;
-  },
+  }
 
   createDOMWalker(node, showAnonymousContent) {
     node = WrapPrivileged.unwrap(node);
     let walker = Cc["@mozilla.org/inspector/deep-tree-walker;1"].
                  createInstance(Ci.inIDeepTreeWalker);
     walker.showAnonymousContent = showAnonymousContent;
     walker.init(node.ownerDocument, NodeFilter.SHOW_ALL);
     walker.currentNode = node;
     return {
       get firstChild() {
         return WrapPrivileged.wrap(walker.firstChild());
       },
       get lastChild() {
         return WrapPrivileged.wrap(walker.lastChild());
       },
     };
-  },
+  }
 
   observeMutationEvents(mo, node, nativeAnonymousChildList, subtree) {
     WrapPrivileged.unwrap(mo).observe(WrapPrivileged.unwrap(node),
                                       {nativeAnonymousChildList, subtree});
-  },
+  }
 
   doCommand(window, cmd) {
     return this._getDocShell(window).doCommand(cmd);
-  },
+  }
 
   setCommandNode(window, node) {
     return this._getDocShell(window).contentViewer
                .QueryInterface(Ci.nsIContentViewerEdit)
                .setCommandNode(node);
-  },
+  }
 
   /* Bug 1339006 Runnables of nsIURIClassifier.classify may be labeled by
    * SystemGroup, but some test cases may run as web content. That would assert
    * when trying to enter web content from a runnable labeled by the
    * SystemGroup. To avoid that, we run classify from SpecialPowers which is
    * chrome-privileged and allowed to run inside SystemGroup
    */
 
@@ -1935,17 +1862,17 @@ SpecialPowersAPI.prototype = {
         } else {
           callback.onClassifyComplete.call(undefined, ...args);
         }
       });
     };
 
     return classifierService.classify(WrapPrivileged.unwrap(principal), eventTarget,
                                       wrapCallback);
-  },
+  }
 
   // TODO: Bug 1353701 - Supports custom event target for labelling.
   doUrlClassifyLocal(uri, tables, callback) {
     let classifierService =
       Cc["@mozilla.org/url-classifier/dbservice;1"].getService(Ci.nsIURIClassifier);
 
     let wrapCallback = results => {
       Services.tm.dispatchToMainThread(() => {
@@ -1957,16 +1884,91 @@ SpecialPowersAPI.prototype = {
       });
     };
 
     let feature = classifierService.createFeatureWithTables("test", tables.split(","), []);
     return classifierService.asyncClassifyLocalWithFeatures(WrapPrivileged.unwrap(uri),
                                                             [feature],
                                                             Ci.nsIUrlClassifierFeature.blacklist,
                                                             wrapCallback);
+  }
+}
+
+SpecialPowersAPI.prototype._proxiedObservers = {
+  "specialpowers-http-notify-request": function(aMessage) {
+    let uri = aMessage.json.uri;
+    Services.obs.notifyObservers(null, "specialpowers-http-notify-request", uri);
   },
 
-  EARLY_BETA_OR_EARLIER: AppConstants.EARLY_BETA_OR_EARLIER,
+  "specialpowers-service-worker-shutdown": function(aMessage) {
+    Services.obs.notifyObservers(null, "specialpowers-service-worker-shutdown");
+  },
+};
+
+SpecialPowersAPI.prototype.permissionObserverProxy = {
+  // 'this' in permChangedObserverProxy is the permChangedObserverProxy
+  // object itself. The '_specialPowersAPI' will be set to the 'SpecialPowersAPI'
+  // object to call the member function in SpecialPowersAPI.
+  _specialPowersAPI: null,
+  observe(aSubject, aTopic, aData) {
+    if (aTopic == "perm-changed") {
+      var permission = aSubject.QueryInterface(Ci.nsIPermission);
+      this._specialPowersAPI._permissionObserver.observe(permission, aData);
+    }
+  },
+};
 
+SpecialPowersAPI.prototype._permissionObserver = {
+  _self: null,
+  _lastPermission: {},
+  _callBack: null,
+  _nextCallback: null,
+  _obsDataMap: {
+    "deleted": "remove",
+    "added": "add",
+  },
+  observe(permission, aData) {
+    if (this._self._applyingPermissions) {
+      if (permission.type == this._lastPermission.type) {
+        this._self._setTimeout(this._callback);
+        this._self._setTimeout(this._nextCallback);
+        this._callback = null;
+        this._nextCallback = null;
+      }
+    } else {
+      var found = false;
+      for (var i = 0; !found && i < this._self._permissionsUndoStack.length; i++) {
+        var undos = this._self._permissionsUndoStack[i];
+        for (var j = 0; j < undos.length; j++) {
+          var undo = undos[j];
+          if (undo.op == this._obsDataMap[aData] &&
+              undo.type == permission.type) {
+            // Remove this undo item if it has been done by others(not
+            // specialpowers itself.)
+            undos.splice(j, 1);
+            found = true;
+            break;
+          }
+        }
+        if (!undos.length) {
+          // Remove the empty row in permissionsUndoStack
+          this._self._permissionsUndoStack.splice(i, 1);
+        }
+      }
+    }
+  },
 };
 
+SpecialPowersAPI.prototype.EARLY_BETA_OR_EARLIER = AppConstants.EARLY_BETA_OR_EARLIER;
+
+// Due to an unfortunate accident of history, when this API was
+// subclassed using `Thing.prototype = new SpecialPowersAPI()`, existing
+// code depends on all SpecialPowers instances using the same arrays for
+// these.
+Object.assign(SpecialPowersAPI.prototype, {
+  _prefEnvUndoStack: [],
+  _pendingPrefs: [],
+  _permissionsUndoStack: [],
+  _pendingPermissions: [],
+});
+
 this.SpecialPowersAPI = SpecialPowersAPI;
 this.bindDOMWindowUtils = bindDOMWindowUtils;