bug 1378459 part 1 event manager refactor draft
authorAndrew Swan <aswan@mozilla.com>
Wed, 14 Mar 2018 14:52:44 -0700
changeset 769734 df6aa6dfcc55c46599fcc50bb8d73c28789b192c
parent 769595 c8dbb4ed05f38f40ef3607a6e36545bd95b8a287
child 769735 dfe1c274fe8bcf1ad9a4f0c1836072615a11787e
push id103213
push useraswan@mozilla.com
push dateTue, 20 Mar 2018 01:22:00 +0000
bugs1378459
milestone61.0a1
bug 1378459 part 1 event manager refactor MozReview-Commit-ID: 72QDfiwRm5j
browser/components/extensions/ext-bookmarks.js
browser/components/extensions/ext-browser.js
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-c-devtools-network.js
browser/components/extensions/ext-c-devtools-panels.js
browser/components/extensions/ext-c-menus.js
browser/components/extensions/ext-c-omnibox.js
browser/components/extensions/ext-commands.js
browser/components/extensions/ext-devtools-network.js
browser/components/extensions/ext-devtools-panels.js
browser/components/extensions/ext-geckoProfiler.js
browser/components/extensions/ext-history.js
browser/components/extensions/ext-menus.js
browser/components/extensions/ext-omnibox.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-sessions.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-windows.js
mobile/android/components/extensions/ext-browserAction.js
mobile/android/components/extensions/ext-pageAction.js
mobile/android/components/extensions/ext-tabs.js
mobile/android/components/extensions/ext-utils.js
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ext-alarms.js
toolkit/components/extensions/ext-c-storage.js
toolkit/components/extensions/ext-c-test.js
toolkit/components/extensions/ext-contextualIdentities.js
toolkit/components/extensions/ext-cookies.js
toolkit/components/extensions/ext-downloads.js
toolkit/components/extensions/ext-idle.js
toolkit/components/extensions/ext-management.js
toolkit/components/extensions/ext-notifications.js
toolkit/components/extensions/ext-proxy.js
toolkit/components/extensions/ext-runtime.js
toolkit/components/extensions/ext-storage.js
toolkit/components/extensions/ext-theme.js
toolkit/components/extensions/ext-toolkit.js
toolkit/components/extensions/ext-webNavigation.js
toolkit/components/extensions/ext-webRequest.js
toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
--- a/browser/components/extensions/ext-bookmarks.js
+++ b/browser/components/extensions/ext-bookmarks.js
@@ -332,63 +332,79 @@ this.bookmarks = class extends Extension
           try {
             return PlacesUtils.bookmarks.remove(info)
               .catch(error => Promise.reject({message: error.message}));
           } catch (e) {
             return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`});
           }
         },
 
-        onCreated: new EventManager(context, "bookmarks.onCreated", fire => {
-          let listener = (event, bookmark) => {
-            fire.sync(bookmark.id, bookmark);
-          };
+        onCreated: new EventManager({
+          context,
+          name: "bookmarks.onCreated",
+          register: fire => {
+            let listener = (event, bookmark) => {
+              fire.sync(bookmark.id, bookmark);
+            };
 
-          observer.on("created", listener);
-          incrementListeners();
-          return () => {
-            observer.off("created", listener);
-            decrementListeners();
-          };
+            observer.on("created", listener);
+            incrementListeners();
+            return () => {
+              observer.off("created", listener);
+              decrementListeners();
+            };
+          },
         }).api(),
 
-        onRemoved: new EventManager(context, "bookmarks.onRemoved", fire => {
-          let listener = (event, data) => {
-            fire.sync(data.guid, data.info);
-          };
+        onRemoved: new EventManager({
+          context,
+          name: "bookmarks.onRemoved",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.sync(data.guid, data.info);
+            };
 
-          observer.on("removed", listener);
-          incrementListeners();
-          return () => {
-            observer.off("removed", listener);
-            decrementListeners();
-          };
+            observer.on("removed", listener);
+            incrementListeners();
+            return () => {
+              observer.off("removed", listener);
+              decrementListeners();
+            };
+          },
         }).api(),
 
-        onChanged: new EventManager(context, "bookmarks.onChanged", fire => {
-          let listener = (event, data) => {
-            fire.sync(data.guid, data.info);
-          };
+        onChanged: new EventManager({
+          context,
+          name: "bookmarks.onChanged",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.sync(data.guid, data.info);
+            };
 
-          observer.on("changed", listener);
-          incrementListeners();
-          return () => {
-            observer.off("changed", listener);
-            decrementListeners();
-          };
+            observer.on("changed", listener);
+            incrementListeners();
+            return () => {
+              observer.off("changed", listener);
+              decrementListeners();
+            };
+          },
         }).api(),
 
-        onMoved: new EventManager(context, "bookmarks.onMoved", fire => {
-          let listener = (event, data) => {
-            fire.sync(data.guid, data.info);
-          };
+        onMoved: new EventManager({
+          context,
+          name: "bookmarks.onMoved",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.sync(data.guid, data.info);
+            };
 
-          observer.on("moved", listener);
-          incrementListeners();
-          return () => {
-            observer.off("moved", listener);
-            decrementListeners();
-          };
+            observer.on("moved", listener);
+            incrementListeners();
+            return () => {
+              observer.off("moved", listener);
+              decrementListeners();
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -170,44 +170,16 @@ class WindowTracker extends WindowTracke
    *        Will return the topmost "normal" (i.e., not popup) window.
    *        @readonly
    */
   get topNormalWindow() {
     return RecentWindow.getMostRecentBrowserWindow({allowPopups: false});
   }
 }
 
-/**
- * An event manager API provider which listens for a DOM event in any browser
- * window, and calls the given listener function whenever an event is received.
- * That listener function receives a `fire` object, which it can use to dispatch
- * events to the extension, and a DOM event object.
- *
- * @param {BaseContext} context
- *        The extension context which the event manager belongs to.
- * @param {string} name
- *        The API name of the event manager, e.g.,"runtime.onMessage".
- * @param {string} event
- *        The name of the DOM event to listen for.
- * @param {function} listener
- *        The listener function to call when a DOM event is received.
- */
-global.WindowEventManager = class extends EventManager {
-  constructor(context, name, event, listener) {
-    super(context, name, fire => {
-      let listener2 = listener.bind(null, fire);
-
-      windowTracker.addListener(event, listener2);
-      return () => {
-        windowTracker.removeListener(event, listener2);
-      };
-    });
-  }
-};
-
 class TabTracker extends TabTrackerBase {
   constructor() {
     super();
 
     this._tabs = new WeakMap();
     this._browsers = new WeakMap();
     this._tabIds = new Map();
     this._nextId = 1;
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -585,25 +585,30 @@ this.browserAction = class extends Exten
       if (tabId !== null) {
         return tabTracker.getTab(tabId);
       }
       return null;
     }
 
     return {
       browserAction: {
-        onClicked: new InputEventManager(context, "browserAction.onClicked", fire => {
-          let listener = (event, browser) => {
-            context.withPendingBrowser(browser, () =>
-              fire.sync(tabManager.convert(tabTracker.activeTab)));
-          };
-          browserAction.on("click", listener);
-          return () => {
-            browserAction.off("click", listener);
-          };
+        onClicked: new EventManager({
+          context,
+          name: "browserAction.onClicked",
+          inputHandling: true,
+          register: fire => {
+            let listener = (event, browser) => {
+              context.withPendingBrowser(browser, () =>
+                fire.sync(tabManager.convert(tabTracker.activeTab)));
+            };
+            browserAction.on("click", listener);
+            return () => {
+              browserAction.off("click", listener);
+            };
+          },
         }).api(),
 
         enable: function(tabId) {
           let tab = getTab(tabId);
           browserAction.setProperty(tab, "enabled", true);
         },
 
         disable: function(tabId) {
--- a/browser/components/extensions/ext-c-devtools-network.js
+++ b/browser/components/extensions/ext-c-devtools-network.js
@@ -31,29 +31,33 @@ class ChildNetworkResponseLoader {
   }
 }
 
 this.devtools_network = class extends ExtensionAPI {
   getAPI(context) {
     return {
       devtools: {
         network: {
-          onRequestFinished: new EventManager(context, "devtools.network.onRequestFinished", fire => {
-            let onFinished = (data) => {
-              const loader = new ChildNetworkResponseLoader(context, data.requestId);
-              const harEntry = {...data.harEntry, ...loader.api()};
-              const result = Cu.cloneInto(harEntry, context.cloneScope, {
-                cloneFunctions: true,
-              });
-              fire.asyncWithoutClone(result);
-            };
+          onRequestFinished: new EventManager({
+            context,
+            name: "devtools.network.onRequestFinished",
+            register: fire => {
+              let onFinished = (data) => {
+                const loader = new ChildNetworkResponseLoader(context, data.requestId);
+                const harEntry = {...data.harEntry, ...loader.api()};
+                const result = Cu.cloneInto(harEntry, context.cloneScope, {
+                  cloneFunctions: true,
+                });
+                fire.asyncWithoutClone(result);
+              };
 
-            let parent = context.childManager.getParentEvent("devtools.network.onRequestFinished");
-            parent.addListener(onFinished);
-            return () => {
-              parent.removeListener(onFinished);
-            };
+              let parent = context.childManager.getParentEvent("devtools.network.onRequestFinished");
+              parent.addListener(onFinished);
+              return () => {
+                parent.removeListener(onFinished);
+              };
+            },
           }).api(),
         },
       },
     };
   }
 };
--- a/browser/components/extensions/ext-c-devtools-panels.js
+++ b/browser/components/extensions/ext-c-devtools-panels.js
@@ -92,37 +92,43 @@ class ChildDevToolsPanel extends Extensi
   }
 
   onParentPanelHidden() {
     this.emit("hidden");
   }
 
   api() {
     return {
-      onShown: new EventManager(
-        this.context, "devtoolsPanel.onShown", fire => {
+      onShown: new EventManager({
+        context: this.context,
+        name: "devtoolsPanel.onShown",
+        register: fire => {
           const listener = (eventName, panelContentWindow) => {
             fire.asyncWithoutClone(panelContentWindow);
           };
           this.on("shown", listener);
           return () => {
             this.off("shown", listener);
           };
-        }).api(),
+        },
+      }).api(),
 
-      onHidden: new EventManager(
-        this.context, "devtoolsPanel.onHidden", fire => {
+      onHidden: new EventManager({
+        context: this.context,
+        name: "devtoolsPanel.onHidden",
+        register: fire => {
           const listener = () => {
             fire.async();
           };
           this.on("hidden", listener);
           return () => {
             this.off("hidden", listener);
           };
-        }).api(),
+        },
+      }).api(),
 
       // TODO(rpl): onSearch event and createStatusBarButton method
     };
   }
 
   close() {
     this.mm.removeMessageListener("Extension:DevToolsPanelShown", this);
     this.mm.removeMessageListener("Extension:DevToolsPanelHidden", this);
@@ -187,37 +193,43 @@ class ChildDevToolsInspectorSidebar exte
   onParentSidebarHidden() {
     this.emit("hidden");
   }
 
   api() {
     const {context, id} = this;
 
     return {
-      onShown: new EventManager(
-        context, "devtoolsInspectorSidebar.onShown", fire => {
+      onShown: new EventManager({
+        context,
+        name: "devtoolsInspectorSidebar.onShown",
+        register: fire => {
           const listener = (eventName, panelContentWindow) => {
             fire.asyncWithoutClone(panelContentWindow);
           };
           this.on("shown", listener);
           return () => {
             this.off("shown", listener);
           };
-        }).api(),
+        },
+      }).api(),
 
-      onHidden: new EventManager(
-        context, "devtoolsInspectorSidebar.onHidden", fire => {
+      onHidden: new EventManager({
+        context,
+        name: "devtoolsInspectorSidebar.onHidden",
+        register: fire => {
           const listener = () => {
             fire.async();
           };
           this.on("hidden", listener);
           return () => {
             this.off("hidden", listener);
           };
-        }).api(),
+        },
+      }).api(),
 
       setObject(jsonObject, rootTitle) {
         return context.cloneScope.Promise.resolve().then(() => {
           return context.childManager.callParentAsyncFunction(
             "devtools.panels.elements.Sidebar.setObject",
             [id, jsonObject, rootTitle]
           );
         });
@@ -277,23 +289,26 @@ this.devtools_panels = class extends Ext
                                                     context.cloneScope,
                                                     {cloneFunctions: true});
               return devtoolsPanelAPI;
             });
           },
           get themeName() {
             return themeChangeObserver.themeName;
           },
-          onThemeChanged: new EventManager(
-            context, "devtools.panels.onThemeChanged", fire => {
+          onThemeChanged: new EventManager({
+            context,
+            name: "devtools.panels.onThemeChanged",
+            register: fire => {
               const listener = (eventName, themeName) => {
                 fire.async(themeName);
               };
               themeChangeObserver.on("themeChanged", listener);
               return () => {
                 themeChangeObserver.off("themeChanged", listener);
               };
-            }).api(),
+            },
+          }).api(),
         },
       },
     };
   }
 };
--- a/browser/components/extensions/ext-c-menus.js
+++ b/browser/components/extensions/ext-c-menus.js
@@ -157,27 +157,31 @@ this.menusInternal = class extends Exten
         },
 
         removeAll() {
           onClickedProp.deleteAllListenersFromExtension();
 
           return context.childManager.callParentAsyncFunction("menusInternal.removeAll", []);
         },
 
-        onClicked: new EventManager(context, "menus.onClicked", fire => {
-          let listener = (info, tab) => {
-            withHandlingUserInput(context.contentWindow,
-                                  () => fire.sync(info, tab));
-          };
+        onClicked: new EventManager({
+          context,
+          name: "menus.onClicked",
+          register: fire => {
+            let listener = (info, tab) => {
+              withHandlingUserInput(context.contentWindow,
+                                    () => fire.sync(info, tab));
+            };
 
-          let event = context.childManager.getParentEvent("menusInternal.onClicked");
-          event.addListener(listener);
-          return () => {
-            event.removeListener(listener);
-          };
+            let event = context.childManager.getParentEvent("menusInternal.onClicked");
+            event.addListener(listener);
+            return () => {
+              event.removeListener(listener);
+            };
+          },
         }).api(),
       },
     };
 
     const result = {};
     if (context.extension.hasPermission("menus")) {
       result.menus = api.menus;
     }
--- a/browser/components/extensions/ext-c-omnibox.js
+++ b/browser/components/extensions/ext-c-omnibox.js
@@ -4,26 +4,30 @@
 
 // The ext-* files are imported into the same scopes.
 /* import-globals-from ../../../toolkit/components/extensions/ext-c-toolkit.js */
 
 this.omnibox = class extends ExtensionAPI {
   getAPI(context) {
     return {
       omnibox: {
-        onInputChanged: new EventManager(context, "omnibox.onInputChanged", fire => {
-          let listener = (text, id) => {
-            fire.asyncWithoutClone(text, suggestions => {
-              context.childManager.callParentFunctionNoReturn("omnibox.addSuggestions", [
-                id,
-                suggestions,
-              ]);
-            });
-          };
-          context.childManager.getParentEvent("omnibox.onInputChanged").addListener(listener);
-          return () => {
-            context.childManager.getParentEvent("omnibox.onInputChanged").removeListener(listener);
-          };
+        onInputChanged: new EventManager({
+          context,
+          name: "omnibox.onInputChanged",
+          register: fire => {
+            let listener = (text, id) => {
+              fire.asyncWithoutClone(text, suggestions => {
+                context.childManager.callParentFunctionNoReturn("omnibox.addSuggestions", [
+                  id,
+                  suggestions,
+                ]);
+              });
+            };
+            context.childManager.getParentEvent("omnibox.onInputChanged").addListener(listener);
+            return () => {
+              context.childManager.getParentEvent("omnibox.onInputChanged").removeListener(listener);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-commands.js
+++ b/browser/components/extensions/ext-commands.js
@@ -359,21 +359,25 @@ this.commands = class extends ExtensionA
             "commands", name, extension.id);
 
           if (storedCommand && storedCommand.value) {
             commands.set(name, {...manifestCommands.get(name)});
             ExtensionSettingsStore.removeSetting(extension.id, "commands", name);
             this.registerKeys(commands);
           }
         },
-        onCommand: new EventManager(context, "commands.onCommand", fire => {
-          let listener = (eventName, commandName) => {
-            fire.async(commandName);
-          };
-          this.on("command", listener);
-          return () => {
-            this.off("command", listener);
-          };
+        onCommand: new EventManager({
+          context,
+          name: "commands.onCommand",
+          register: fire => {
+            let listener = (eventName, commandName) => {
+              fire.async(commandName);
+            };
+            this.on("command", listener);
+            return () => {
+              this.off("command", listener);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-devtools-network.js
+++ b/browser/components/extensions/ext-devtools-network.js
@@ -14,42 +14,47 @@ this.devtools_network = class extends Ex
     return {
       devtools: {
         network: {
           onNavigated: new EventManager(context, "devtools.onNavigated", fire => {
             let listener = data => {
               fire.async(data.url);
             };
 
-            let targetPromise = getDevToolsTargetForContext(context);
-            targetPromise.then(target => {
-              target.on("navigate", listener);
-            });
-            return () => {
+              let targetPromise = getDevToolsTargetForContext(context);
               targetPromise.then(target => {
-                target.off("navigate", listener);
+                target.on("navigate", listener);
               });
-            };
+              return () => {
+                targetPromise.then(target => {
+                  target.off("navigate", listener);
+                });
+              };
+            },
           }).api(),
 
           getHAR: function() {
             return context.devToolsToolbox.getHARFromNetMonitor();
           },
 
-          onRequestFinished: new EventManager(context, "devtools.network.onRequestFinished", fire => {
-            const listener = (data) => {
-              fire.async(data);
-            };
+          onRequestFinished: new EventManager({
+            context,
+            name: "devtools.network.onRequestFinished",
+            register: fire => {
+              const listener = (data) => {
+                fire.async(data);
+              };
 
-            const toolbox = context.devToolsToolbox;
-            toolbox.addRequestFinishedListener(listener);
+              const toolbox = context.devToolsToolbox;
+              toolbox.addRequestFinishedListener(listener);
 
-            return () => {
-              toolbox.removeRequestFinishedListener(listener);
-            };
+              return () => {
+                toolbox.removeRequestFinishedListener(listener);
+              };
+            },
           }).api(),
 
           // The following method is used internally to allow the request API
           // piece that is running in the child process to ask the parent process
           // to fetch response content from the back-end.
           Request: {
             async getContent(requestId) {
               return context.devToolsToolbox.fetchResponseContent(requestId)
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -524,26 +524,29 @@ this.devtools_panels = class extends Ext
     function newBasePanelId() {
       return `${context.extension.id}-${context.contextId}-${nextPanelId++}`;
     }
 
     return {
       devtools: {
         panels: {
           elements: {
-            onSelectionChanged: new EventManager(
-              context, "devtools.panels.elements.onSelectionChanged", fire => {
+            onSelectionChanged: new EventManager({
+              context,
+              name: "devtools.panels.elements.onSelectionChanged",
+              register: fire => {
                 const listener = (eventName) => {
                   fire.async();
                 };
                 toolboxSelectionObserver.on("selectionChanged", listener);
                 return () => {
                   toolboxSelectionObserver.off("selectionChanged", listener);
                 };
-              }).api(),
+              },
+            }).api(),
             createSidebarPane(title) {
               const id = `devtools-inspector-sidebar-${makeWidgetId(newBasePanelId())}`;
 
               const parentSidebar = new ParentDevToolsInspectorSidebar(context, {title, id});
               sidebarsById.set(id, parentSidebar);
 
               context.callOnClose({
                 close() {
--- a/browser/components/extensions/ext-geckoProfiler.js
+++ b/browser/components/extensions/ext-geckoProfiler.js
@@ -334,18 +334,22 @@ this.geckoProfiler = class extends Exten
               // known system library.
               // "nm" will fail if `nm` is not available.
             }
           }
 
           throw new Error(`Ran out of options to get symbols from library ${debugName} ${breakpadId}.`);
         },
 
-        onRunning: new EventManager(context, "geckoProfiler.onRunning", fire => {
-          isRunningObserver.addObserver(fire.async);
-          return () => {
-            isRunningObserver.removeObserver(fire.async);
-          };
+        onRunning: new EventManager({
+          context,
+          name: "geckoProfiler.onRunning",
+          register: fire => {
+            isRunningObserver.addObserver(fire.async);
+            return () => {
+              isRunningObserver.removeObserver(fire.async);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -218,44 +218,56 @@ this.history = class extends ExtensionAP
 
           let historyQuery = PlacesUtils.history.getNewQuery();
           historyQuery.uri = Services.io.newURI(url);
           let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root;
           let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
           return Promise.resolve(results);
         },
 
-        onVisited: new EventManager(context, "history.onVisited", fire => {
-          let listener = (event, data) => {
-            fire.sync(data);
-          };
+        onVisited: new EventManager({
+          context,
+          name: "history.onVisited",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.sync(data);
+            };
 
-          getHistoryObserver().on("visited", listener);
-          return () => {
-            getHistoryObserver().off("visited", listener);
-          };
+            getHistoryObserver().on("visited", listener);
+            return () => {
+              getHistoryObserver().off("visited", listener);
+            };
+          },
         }).api(),
 
-        onVisitRemoved: new EventManager(context, "history.onVisitRemoved", fire => {
-          let listener = (event, data) => {
-            fire.sync(data);
-          };
+        onVisitRemoved: new EventManager({
+          context,
+          name: "history.onVisitRemoved",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.sync(data);
+            };
 
-          getHistoryObserver().on("visitRemoved", listener);
-          return () => {
-            getHistoryObserver().off("visitRemoved", listener);
-          };
+            getHistoryObserver().on("visitRemoved", listener);
+            return () => {
+              getHistoryObserver().off("visitRemoved", listener);
+            };
+          },
         }).api(),
 
-        onTitleChanged: new EventManager(context, "history.onTitleChanged", fire => {
-          let listener = (event, data) => {
-            fire.sync(data);
-          };
+        onTitleChanged: new EventManager({
+          context,
+          name: "history.onTitleChanged",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.sync(data);
+            };
 
-          getHistoryObserver().on("titleChanged", listener);
-          return () => {
-            getHistoryObserver().off("titleChanged", listener);
-          };
+            getHistoryObserver().on("titleChanged", listener);
+            return () => {
+              getHistoryObserver().off("titleChanged", listener);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-menus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -812,50 +812,58 @@ this.menusInternal = class extends Exten
   getAPI(context) {
     let {extension} = context;
 
     const menus = {
       refresh() {
         gMenuBuilder.rebuildMenu(extension);
       },
 
-      onShown: new EventManager(context, "menus.onShown", fire => {
-        let listener = (event, menuIds, contextData) => {
-          let info = {
-            menuIds,
-            contexts: Array.from(getMenuContexts(contextData)),
-          };
+      onShown: new EventManager({
+        context,
+        name: "menus.onShown",
+        register: fire => {
+          let listener = (event, menuIds, contextData) => {
+            let info = {
+              menuIds,
+              contexts: Array.from(getMenuContexts(contextData)),
+            };
 
-          // The menus.onShown event is fired before the user has consciously
-          // interacted with an extension, so we require permissions before
-          // exposing sensitive contextual data.
-          let includeSensitiveData =
+            // The menus.onShown event is fired before the user has consciously
+            // interacted with an extension, so we require permissions before
+            // exposing sensitive contextual data.
+            let includeSensitiveData =
             extension.tabManager.hasActiveTabPermission(contextData.tab) ||
             extension.whiteListedHosts.matches(contextData.inFrame ? contextData.frameUrl : contextData.pageUrl);
 
-          addMenuEventInfo(info, contextData, includeSensitiveData);
+            addMenuEventInfo(info, contextData, includeSensitiveData);
 
-          let tab = extension.tabManager.convert(contextData.tab);
-          fire.sync(info, tab);
-        };
-        gOnShownSubscribers.add(extension);
-        extension.on("webext-menu-shown", listener);
-        return () => {
-          gOnShownSubscribers.delete(extension);
-          extension.off("webext-menu-shown", listener);
-        };
+            let tab = extension.tabManager.convert(contextData.tab);
+            fire.sync(info, tab);
+          };
+          gOnShownSubscribers.add(extension);
+          extension.on("webext-menu-shown", listener);
+          return () => {
+            gOnShownSubscribers.delete(extension);
+            extension.off("webext-menu-shown", listener);
+          };
+        },
       }).api(),
-      onHidden: new EventManager(context, "menus.onHidden", fire => {
-        let listener = () => {
-          fire.sync();
-        };
-        extension.on("webext-menu-hidden", listener);
-        return () => {
-          extension.off("webext-menu-hidden", listener);
-        };
+      onHidden: new EventManager({
+        context,
+        name: "menus.onHidden",
+        register: fire => {
+          let listener = () => {
+            fire.sync();
+          };
+          extension.on("webext-menu-hidden", listener);
+          return () => {
+            extension.off("webext-menu-hidden", listener);
+          };
+        },
       }).api(),
     };
 
     return {
       contextMenus: menus,
       menus,
       menusInternal: {
         create: function(createProperties) {
@@ -882,24 +890,28 @@ this.menusInternal = class extends Exten
 
         removeAll: function() {
           let root = gRootItems.get(extension);
           if (root) {
             root.remove();
           }
         },
 
-        onClicked: new EventManager(context, "menusInternal.onClicked", fire => {
-          let listener = (event, info, tab) => {
-            let {linkedBrowser} = tab || tabTracker.activeTab;
-            context.withPendingBrowser(linkedBrowser,
-                                       () => fire.sync(info, tab));
-          };
+        onClicked: new EventManager({
+          context,
+          name: "menusInternal.onClicked",
+          register: fire => {
+            let listener = (event, info, tab) => {
+              let {linkedBrowser} = tab || tabTracker.activeTab;
+              context.withPendingBrowser(linkedBrowser,
+                                         () => fire.sync(info, tab));
+            };
 
-          extension.on("webext-menu-menuitem-click", listener);
-          return () => {
-            extension.off("webext-menu-menuitem-click", listener);
-          };
+            extension.on("webext-menu-menuitem-click", listener);
+            return () => {
+              extension.off("webext-menu-menuitem-click", listener);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-omnibox.js
+++ b/browser/components/extensions/ext-omnibox.js
@@ -35,61 +35,77 @@ this.omnibox = class extends ExtensionAP
           try {
             // This will throw if the keyword failed to register.
             ExtensionSearchHandler.setDefaultSuggestion(this.keyword, suggestion);
           } catch (e) {
             return Promise.reject(e.message);
           }
         },
 
-        onInputStarted: new EventManager(context, "omnibox.onInputStarted", fire => {
-          let listener = (eventName) => {
-            fire.sync();
-          };
-          extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
-          return () => {
-            extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
-          };
+        onInputStarted: new EventManager({
+          context,
+          name: "omnibox.onInputStarted",
+          register: fire => {
+            let listener = (eventName) => {
+              fire.sync();
+            };
+            extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+            return () => {
+              extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+            };
+          },
         }).api(),
 
-        onInputCancelled: new EventManager(context, "omnibox.onInputCancelled", fire => {
-          let listener = (eventName) => {
-            fire.sync();
-          };
-          extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
-          return () => {
-            extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
-          };
+        onInputCancelled: new EventManager({
+          context,
+          name: "omnibox.onInputCancelled",
+          register: fire => {
+            let listener = (eventName) => {
+              fire.sync();
+            };
+            extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+            return () => {
+              extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+            };
+          },
         }).api(),
 
-        onInputEntered: new EventManager(context, "omnibox.onInputEntered", fire => {
-          let listener = (eventName, text, disposition) => {
-            fire.sync(text, disposition);
-          };
-          extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
-          return () => {
-            extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
-          };
+        onInputEntered: new EventManager({
+          context,
+          name: "omnibox.onInputEntered",
+          register: fire => {
+            let listener = (eventName, text, disposition) => {
+              fire.sync(text, disposition);
+            };
+            extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+            return () => {
+              extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+            };
+          },
         }).api(),
 
         // Internal APIs.
         addSuggestions: (id, suggestions) => {
           try {
             ExtensionSearchHandler.addSuggestions(this.keyword, id, suggestions);
           } catch (e) {
             // Silently fail because the extension developer can not know for sure if the user
             // has already invalidated the callback when asynchronously providing suggestions.
           }
         },
 
-        onInputChanged: new EventManager(context, "omnibox.onInputChanged", fire => {
-          let listener = (eventName, text, id) => {
-            fire.sync(text, id);
-          };
-          extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
-          return () => {
-            extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
-          };
+        onInputChanged: new EventManager({
+          context,
+          name: "omnibox.onInputChanged",
+          register: fire => {
+            let listener = (eventName, text, id) => {
+              fire.sync(text, id);
+            };
+            extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+            return () => {
+              extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -320,26 +320,31 @@ this.pageAction = class extends Extensio
   getAPI(context) {
     let {extension} = context;
 
     const {tabManager} = extension;
     const pageAction = this;
 
     return {
       pageAction: {
-        onClicked: new InputEventManager(context, "pageAction.onClicked", fire => {
-          let listener = (evt, tab) => {
-            context.withPendingBrowser(tab.linkedBrowser, () =>
-              fire.sync(tabManager.convert(tab)));
-          };
+        onClicked: new EventManager({
+          context,
+          name: "pageAction.onClicked",
+          inputHandling: true,
+          register: fire => {
+            let listener = (evt, tab) => {
+              context.withPendingBrowser(tab.linkedBrowser, () =>
+                fire.sync(tabManager.convert(tab)));
+            };
 
-          pageAction.on("click", listener);
-          return () => {
-            pageAction.off("click", listener);
-          };
+            pageAction.on("click", listener);
+            return () => {
+              pageAction.off("click", listener);
+            };
+          },
         }).api(),
 
         show(tabId) {
           let tab = tabTracker.getTab(tabId);
           pageAction.setProperty(tab, "show", true);
         },
 
         hide(tabId) {
--- a/browser/components/extensions/ext-sessions.js
+++ b/browser/components/extensions/ext-sessions.js
@@ -202,22 +202,26 @@ this.sessions = class extends ExtensionA
 
         removeWindowValue(windowId, key) {
           let {win, encodedKey} =
             getWindowParams(extension.id, key, windowId, context);
 
           SessionStore.deleteWindowValue(win, encodedKey);
         },
 
-        onChanged: new EventManager(context, "sessions.onChanged", fire => {
-          let observer = () => {
-            fire.async();
-          };
+        onChanged: new EventManager({
+          context,
+          name: "sessions.onChanged",
+          register: fire => {
+            let observer = () => {
+              fire.async();
+            };
 
-          Services.obs.addObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED);
-          return () => {
-            Services.obs.removeObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED);
-          };
+            Services.obs.addObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED);
+            return () => {
+              Services.obs.removeObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -333,133 +333,165 @@ this.tabs = class extends ExtensionAPI {
 
       await tabListener.awaitTabReady(tab.nativeTab);
 
       return tab;
     }
 
     let self = {
       tabs: {
-        onActivated: new EventManager(context, "tabs.onActivated", fire => {
-          let listener = (eventName, event) => {
-            fire.async(event);
-          };
+        onActivated: new EventManager({
+          context,
+          name: "tabs.onActivated",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async(event);
+            };
 
-          tabTracker.on("tab-activated", listener);
-          return () => {
-            tabTracker.off("tab-activated", listener);
-          };
+            tabTracker.on("tab-activated", listener);
+            return () => {
+              tabTracker.off("tab-activated", listener);
+            };
+          },
         }).api(),
 
-        onCreated: new EventManager(context, "tabs.onCreated", fire => {
-          let listener = (eventName, event) => {
-            fire.async(tabManager.convert(event.nativeTab, event.currentTab));
-          };
+        onCreated: new EventManager({
+          context,
+          name: "tabs.onCreated",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async(tabManager.convert(event.nativeTab, event.currentTab));
+            };
 
-          tabTracker.on("tab-created", listener);
-          return () => {
-            tabTracker.off("tab-created", listener);
-          };
+            tabTracker.on("tab-created", listener);
+            return () => {
+              tabTracker.off("tab-created", listener);
+            };
+          },
         }).api(),
 
         /**
          * Since multiple tabs currently can't be highlighted, onHighlighted
          * essentially acts an alias for self.tabs.onActivated but returns
          * the tabId in an array to match the API.
          * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
         */
-        onHighlighted: new EventManager(context, "tabs.onHighlighted", fire => {
-          let listener = (eventName, event) => {
-            fire.async({tabIds: [event.tabId], windowId: event.windowId});
-          };
+        onHighlighted: new EventManager({
+          context,
+          name: "tabs.onHighlighted",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async({tabIds: [event.tabId], windowId: event.windowId});
+            };
 
-          tabTracker.on("tab-activated", listener);
-          return () => {
-            tabTracker.off("tab-activated", listener);
-          };
+            tabTracker.on("tab-activated", listener);
+            return () => {
+              tabTracker.off("tab-activated", listener);
+            };
+          },
         }).api(),
 
-        onAttached: new EventManager(context, "tabs.onAttached", fire => {
-          let listener = (eventName, event) => {
-            fire.async(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
-          };
+        onAttached: new EventManager({
+          context,
+          name: "tabs.onAttached",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
+            };
 
-          tabTracker.on("tab-attached", listener);
-          return () => {
-            tabTracker.off("tab-attached", listener);
-          };
+            tabTracker.on("tab-attached", listener);
+            return () => {
+              tabTracker.off("tab-attached", listener);
+            };
+          },
         }).api(),
 
-        onDetached: new EventManager(context, "tabs.onDetached", fire => {
-          let listener = (eventName, event) => {
-            fire.async(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
-          };
+        onDetached: new EventManager({
+          context,
+          name: "tabs.onDetached",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
+            };
 
-          tabTracker.on("tab-detached", listener);
-          return () => {
-            tabTracker.off("tab-detached", listener);
-          };
-        }).api(),
-
-        onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
-          let listener = (eventName, event) => {
-            fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
-          };
-
-          tabTracker.on("tab-removed", listener);
-          return () => {
-            tabTracker.off("tab-removed", listener);
-          };
+            tabTracker.on("tab-detached", listener);
+            return () => {
+              tabTracker.off("tab-detached", listener);
+            };
+          },
         }).api(),
 
-        onReplaced: new EventManager(context, "tabs.onReplaced", fire => {
-          return () => {};
+        onRemoved: new EventManager({
+          context,
+          name: "tabs.onRemoved",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
+            };
+
+            tabTracker.on("tab-removed", listener);
+            return () => {
+              tabTracker.off("tab-removed", listener);
+            };
+          },
+        }).api(),
+
+        onReplaced: new EventManager({
+          context,
+          name: "tabs.onReplaced",
+          register: fire => {
+            return () => {};
+          },
         }).api(),
 
-        onMoved: new EventManager(context, "tabs.onMoved", fire => {
-          // There are certain circumstances where we need to ignore a move event.
-          //
-          // Namely, the first time the tab is moved after it's created, we need
-          // to report the final position as the initial position in the tab's
-          // onAttached or onCreated event. This is because most tabs are inserted
-          // in a temporary location and then moved after the TabOpen event fires,
-          // which generates a TabOpen event followed by a TabMove event, which
-          // does not match the contract of our API.
-          let ignoreNextMove = new WeakSet();
+        onMoved: new EventManager({
+          context,
+          name: "tabs.onMoved",
+          register: fire => {
+            // There are certain circumstances where we need to ignore a move event.
+            //
+            // Namely, the first time the tab is moved after it's created, we need
+            // to report the final position as the initial position in the tab's
+            // onAttached or onCreated event. This is because most tabs are inserted
+            // in a temporary location and then moved after the TabOpen event fires,
+            // which generates a TabOpen event followed by a TabMove event, which
+            // does not match the contract of our API.
+            let ignoreNextMove = new WeakSet();
 
-          let openListener = event => {
-            ignoreNextMove.add(event.target);
-            // Remove the tab from the set on the next tick, since it will already
-            // have been moved by then.
-            Promise.resolve().then(() => {
-              ignoreNextMove.delete(event.target);
-            });
-          };
+            let openListener = event => {
+              ignoreNextMove.add(event.target);
+              // Remove the tab from the set on the next tick, since it will already
+              // have been moved by then.
+              Promise.resolve().then(() => {
+                ignoreNextMove.delete(event.target);
+              });
+            };
 
-          let moveListener = event => {
-            let nativeTab = event.originalTarget;
+            let moveListener = event => {
+              let nativeTab = event.originalTarget;
 
-            if (ignoreNextMove.has(nativeTab)) {
-              ignoreNextMove.delete(nativeTab);
-              return;
-            }
+              if (ignoreNextMove.has(nativeTab)) {
+                ignoreNextMove.delete(nativeTab);
+                return;
+              }
 
-            fire.async(tabTracker.getId(nativeTab), {
-              windowId: windowTracker.getId(nativeTab.ownerGlobal),
-              fromIndex: event.detail,
-              toIndex: nativeTab._tPos,
-            });
-          };
+              fire.async(tabTracker.getId(nativeTab), {
+                windowId: windowTracker.getId(nativeTab.ownerGlobal),
+                fromIndex: event.detail,
+                toIndex: nativeTab._tPos,
+              });
+            };
 
-          windowTracker.addListener("TabMove", moveListener);
-          windowTracker.addListener("TabOpen", openListener);
-          return () => {
-            windowTracker.removeListener("TabMove", moveListener);
-            windowTracker.removeListener("TabOpen", openListener);
-          };
+            windowTracker.addListener("TabMove", moveListener);
+            windowTracker.addListener("TabOpen", openListener);
+            return () => {
+              windowTracker.removeListener("TabMove", moveListener);
+              windowTracker.removeListener("TabOpen", openListener);
+            };
+          },
         }).api(),
 
         onUpdated: new TabsUpdateFilterEventManager(context, "tabs.onUpdated").api(),
 
         create(createProperties) {
           return new Promise((resolve, reject) => {
             let window = createProperties.windowId !== null ?
               windowTracker.getWindow(createProperties.windowId, context) :
@@ -891,85 +923,89 @@ this.tabs = class extends ExtensionAPI {
           let currentSettings = this._getZoomSettings(tabTracker.getId(nativeTab));
 
           if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
             return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
           }
           return Promise.resolve();
         },
 
-        onZoomChange: new EventManager(context, "tabs.onZoomChange", fire => {
-          let getZoomLevel = browser => {
-            let {ZoomManager} = browser.ownerGlobal;
+        onZoomChange: new EventManager({
+          context,
+          name: "tabs.onZoomChange",
+          register: fire => {
+            let getZoomLevel = browser => {
+              let {ZoomManager} = browser.ownerGlobal;
 
-            return ZoomManager.getZoomForBrowser(browser);
-          };
-
-          // Stores the last known zoom level for each tab's browser.
-          // WeakMap[<browser> -> number]
-          let zoomLevels = new WeakMap();
+              return ZoomManager.getZoomForBrowser(browser);
+            };
 
-          // Store the zoom level for all existing tabs.
-          for (let window of windowTracker.browserWindows()) {
-            for (let nativeTab of window.gBrowser.tabs) {
-              let browser = nativeTab.linkedBrowser;
-              zoomLevels.set(browser, getZoomLevel(browser));
-            }
-          }
+            // Stores the last known zoom level for each tab's browser.
+            // WeakMap[<browser> -> number]
+            let zoomLevels = new WeakMap();
 
-          let tabCreated = (eventName, event) => {
-            let browser = event.nativeTab.linkedBrowser;
-            zoomLevels.set(browser, getZoomLevel(browser));
-          };
-
-
-          let zoomListener = event => {
-            let browser = event.originalTarget;
-
-            // For non-remote browsers, this event is dispatched on the document
-            // rather than on the <browser>.
-            if (browser instanceof Ci.nsIDOMDocument) {
-              browser = browser.docShell.chromeEventHandler;
+            // Store the zoom level for all existing tabs.
+            for (let window of windowTracker.browserWindows()) {
+              for (let nativeTab of window.gBrowser.tabs) {
+                let browser = nativeTab.linkedBrowser;
+                zoomLevels.set(browser, getZoomLevel(browser));
+              }
             }
 
-            let {gBrowser} = browser.ownerGlobal;
-            let nativeTab = gBrowser.getTabForBrowser(browser);
-            if (!nativeTab) {
-              // We only care about zoom events in the top-level browser of a tab.
-              return;
-            }
+            let tabCreated = (eventName, event) => {
+              let browser = event.nativeTab.linkedBrowser;
+              zoomLevels.set(browser, getZoomLevel(browser));
+            };
+
+
+            let zoomListener = event => {
+              let browser = event.originalTarget;
 
-            let oldZoomFactor = zoomLevels.get(browser);
-            let newZoomFactor = getZoomLevel(browser);
+              // For non-remote browsers, this event is dispatched on the document
+              // rather than on the <browser>.
+              if (browser instanceof Ci.nsIDOMDocument) {
+                browser = browser.docShell.chromeEventHandler;
+              }
 
-            if (oldZoomFactor != newZoomFactor) {
-              zoomLevels.set(browser, newZoomFactor);
+              let {gBrowser} = browser.ownerGlobal;
+              let nativeTab = gBrowser.getTabForBrowser(browser);
+              if (!nativeTab) {
+                // We only care about zoom events in the top-level browser of a tab.
+                return;
+              }
+
+              let oldZoomFactor = zoomLevels.get(browser);
+              let newZoomFactor = getZoomLevel(browser);
 
-              let tabId = tabTracker.getId(nativeTab);
-              fire.async({
-                tabId,
-                oldZoomFactor,
-                newZoomFactor,
-                zoomSettings: self.tabs._getZoomSettings(tabId),
-              });
-            }
-          };
+              if (oldZoomFactor != newZoomFactor) {
+                zoomLevels.set(browser, newZoomFactor);
+
+                let tabId = tabTracker.getId(nativeTab);
+                fire.async({
+                  tabId,
+                  oldZoomFactor,
+                  newZoomFactor,
+                  zoomSettings: self.tabs._getZoomSettings(tabId),
+                });
+              }
+            };
 
-          tabTracker.on("tab-attached", tabCreated);
-          tabTracker.on("tab-created", tabCreated);
+            tabTracker.on("tab-attached", tabCreated);
+            tabTracker.on("tab-created", tabCreated);
 
-          windowTracker.addListener("FullZoomChange", zoomListener);
-          windowTracker.addListener("TextZoomChange", zoomListener);
-          return () => {
-            tabTracker.off("tab-attached", tabCreated);
-            tabTracker.off("tab-created", tabCreated);
+            windowTracker.addListener("FullZoomChange", zoomListener);
+            windowTracker.addListener("TextZoomChange", zoomListener);
+            return () => {
+              tabTracker.off("tab-attached", tabCreated);
+              tabTracker.off("tab-created", tabCreated);
 
-            windowTracker.removeListener("FullZoomChange", zoomListener);
-            windowTracker.removeListener("TextZoomChange", zoomListener);
-          };
+              windowTracker.removeListener("FullZoomChange", zoomListener);
+              windowTracker.removeListener("TextZoomChange", zoomListener);
+            };
+          },
         }).api(),
 
         print() {
           let activeTab = getTabOrActive(null);
           let {PrintUtils} = activeTab.ownerGlobal;
 
           PrintUtils.printWindow(activeTab.linkedBrowser.outerWindowID, activeTab.linkedBrowser);
         },
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -14,56 +14,88 @@ ChromeUtils.defineModuleGetter(this, "Pr
 var {
   promiseObserved,
 } = ExtensionUtils;
 
 const onXULFrameLoaderCreated = ({target}) => {
   target.messageManager.sendAsyncMessage("AllowScriptsToClose", {});
 };
 
+/**
+ * An event manager API provider which listens for a DOM event in any browser
+ * window, and calls the given listener function whenever an event is received.
+ * That listener function receives a `fire` object, which it can use to dispatch
+ * events to the extension, and a DOM event object.
+ *
+ * @param {BaseContext} context
+ *        The extension context which the event manager belongs to.
+ * @param {string} name
+ *        The API name of the event manager, e.g.,"runtime.onMessage".
+ * @param {string} event
+ *        The name of the DOM event to listen for.
+ * @param {function} listener
+ *        The listener function to call when a DOM event is received.
+ *
+ * @returns {object} An injectable api for the new event.
+ */
+function WindowEventManager(context, name, event, listener) {
+  let register = fire => {
+    let listener2 = listener.bind(null, fire);
+
+    windowTracker.addListener(event, listener2);
+    return () => {
+      windowTracker.removeListener(event, listener2);
+    };
+  };
+
+  return new EventManager({context, name, register}).api();
+}
+
 this.windows = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
 
     const {windowManager} = extension;
 
     return {
       windows: {
-        onCreated:
-        new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
+        onCreated: WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => {
           fire.async(windowManager.convert(window));
-        }).api(),
+        }),
 
-        onRemoved:
-        new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
+        onRemoved: WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => {
           fire.async(windowTracker.getId(window));
-        }).api(),
+        }),
 
-        onFocusChanged: new EventManager(context, "windows.onFocusChanged", fire => {
-          // Keep track of the last windowId used to fire an onFocusChanged event
-          let lastOnFocusChangedWindowId;
+        onFocusChanged: new EventManager({
+          context,
+          name: "windows.onFocusChanged",
+          register: fire => {
+            // Keep track of the last windowId used to fire an onFocusChanged event
+            let lastOnFocusChangedWindowId;
 
-          let listener = event => {
-            // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
-            // event when switching focus between two Firefox windows.
-            Promise.resolve().then(() => {
-              let window = Services.focus.activeWindow;
-              let windowId = window ? windowTracker.getId(window) : Window.WINDOW_ID_NONE;
-              if (windowId !== lastOnFocusChangedWindowId) {
-                fire.async(windowId);
-                lastOnFocusChangedWindowId = windowId;
-              }
-            });
-          };
-          windowTracker.addListener("focus", listener);
-          windowTracker.addListener("blur", listener);
-          return () => {
-            windowTracker.removeListener("focus", listener);
-            windowTracker.removeListener("blur", listener);
-          };
+            let listener = event => {
+              // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE
+              // event when switching focus between two Firefox windows.
+              Promise.resolve().then(() => {
+                let window = Services.focus.activeWindow;
+                let windowId = window ? windowTracker.getId(window) : Window.WINDOW_ID_NONE;
+                if (windowId !== lastOnFocusChangedWindowId) {
+                  fire.async(windowId);
+                  lastOnFocusChangedWindowId = windowId;
+                }
+              });
+            };
+            windowTracker.addListener("focus", listener);
+            windowTracker.addListener("blur", listener);
+            return () => {
+              windowTracker.removeListener("focus", listener);
+              windowTracker.removeListener("blur", listener);
+            };
+          },
         }).api(),
 
         get: function(windowId, getInfo) {
           let window = windowTracker.getWindow(windowId, context);
           if (!window) {
             return Promise.reject({message: `Invalid window ID: ${windowId}`});
           }
           return Promise.resolve(windowManager.convert(window, getInfo));
--- a/mobile/android/components/extensions/ext-browserAction.js
+++ b/mobile/android/components/extensions/ext-browserAction.js
@@ -151,24 +151,28 @@ this.browserAction = class extends Exten
       if (tabId !== null) {
         return tabTracker.getTab(tabId);
       }
       return null;
     }
 
     return {
       browserAction: {
-        onClicked: new EventManager(context, "browserAction.onClicked", fire => {
-          let listener = (event, tab) => {
-            fire.async(tabManager.convert(tab));
-          };
-          browserActionMap.get(extension).on("click", listener);
-          return () => {
-            browserActionMap.get(extension).off("click", listener);
-          };
+        onClicked: new EventManager({
+          context,
+          name: "browserAction.onClicked",
+          register: fire => {
+            let listener = (event, tab) => {
+              fire.async(tabManager.convert(tab));
+            };
+            browserActionMap.get(extension).on("click", listener);
+            return () => {
+              browserActionMap.get(extension).off("click", listener);
+            };
+          },
         }).api(),
 
         setTitle: function(details) {
           let {tabId, title} = details;
           let tab = getTab(tabId);
           browserActionMap.get(extension).setProperty(tab, "name", title);
         },
 
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -227,24 +227,28 @@ this.pageAction = class extends Extensio
   getAPI(context) {
     const {extension} = context;
     const {tabManager} = extension;
 
     pageActionMap.get(extension).setContext(context);
 
     return {
       pageAction: {
-        onClicked: new EventManager(context, "pageAction.onClicked", fire => {
-          let listener = (event, tab) => {
-            fire.async(tabManager.convert(tab));
-          };
-          pageActionMap.get(extension).on("click", listener);
-          return () => {
-            pageActionMap.get(extension).off("click", listener);
-          };
+        onClicked: new EventManager({
+          context,
+          name: "pageAction.onClicked",
+          register: fire => {
+            let listener = (event, tab) => {
+              fire.async(tabManager.convert(tab));
+            };
+            pageActionMap.get(extension).on("click", listener);
+            return () => {
+              pageActionMap.get(extension).off("click", listener);
+            };
+          },
         }).api(),
 
         show(tabId) {
           let tab = tabTracker.getTab(tabId);
           return pageActionMap.get(extension).setProperty(tab, "show", true);
         },
 
         hide(tabId) {
--- a/mobile/android/components/extensions/ext-tabs.js
+++ b/mobile/android/components/extensions/ext-tabs.js
@@ -98,147 +98,163 @@ this.tabs = class extends ExtensionAPI {
 
       await tabListener.awaitTabReady(tab.nativeTab);
 
       return tab;
     }
 
     let self = {
       tabs: {
-        onActivated: new GlobalEventManager(context, "tabs.onActivated", "Tab:Selected", (fire, data) => {
+        onActivated: makeGlobalEvent(context, "tabs.onActivated", "Tab:Selected", (fire, data) => {
           let tab = tabManager.get(data.id);
 
           fire.async({tabId: tab.id, windowId: tab.windowId});
         }).api(),
 
         onCreated: new EventManager(context, "tabs.onCreated", fire => {
           let listener = (eventName, event) => {
             fire.async(tabManager.convert(event.nativeTab));
           };
 
           tabTracker.on("tab-created", listener);
           return () => {
             tabTracker.off("tab-created", listener);
           };
-        }).api(),
+        }),
 
         /**
          * Since multiple tabs currently can't be highlighted, onHighlighted
          * essentially acts an alias for self.tabs.onActivated but returns
          * the tabId in an array to match the API.
          * @see  https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
         */
-        onHighlighted: new GlobalEventManager(context, "tabs.onHighlighted", "Tab:Selected", (fire, data) => {
+        onHighlighted: makeGlobalEvent(context, "tabs.onHighlighted", "Tab:Selected", (fire, data) => {
           let tab = tabManager.get(data.id);
 
           fire.async({tabIds: [tab.id], windowId: tab.windowId});
+        }),
+
+        onAttached: new EventManager({
+          context,
+          name: "tabs.onAttached",
+          register: fire => { return () => {}; },
         }).api(),
 
-        onAttached: new EventManager(context, "tabs.onAttached", fire => {
-          return () => {};
-        }).api(),
-
-        onDetached: new EventManager(context, "tabs.onDetached", fire => {
-          return () => {};
+        onDetached: new EventManager({
+          context,
+          name: "tabs.onDetached",
+          register: fire => { return () => {}; },
         }).api(),
 
-        onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
-          let listener = (eventName, event) => {
-            fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
-          };
+        onRemoved: new EventManager({
+          context,
+          name: "tabs.onRemoved",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
+            };
 
-          tabTracker.on("tab-removed", listener);
-          return () => {
-            tabTracker.off("tab-removed", listener);
-          };
+            tabTracker.on("tab-removed", listener);
+            return () => {
+              tabTracker.off("tab-removed", listener);
+            };
+          },
         }).api(),
 
-        onReplaced: new EventManager(context, "tabs.onReplaced", fire => {
-          return () => {};
+        onReplaced: new EventManager({
+          context,
+          name: "tabs.onReplaced",
+          register: fire => { return () => {}; },
         }).api(),
 
-        onMoved: new EventManager(context, "tabs.onMoved", fire => {
-          return () => {};
+        onMoved: new EventManager({
+          context,
+          name: "tabs.onMoved",
+          register: fire => { return () => {}; },
         }).api(),
 
-        onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
-          const restricted = ["url", "favIconUrl", "title"];
+        onUpdated: new EventManager({
+          context,
+          name: "tabs.onUpdated",
+          register: fire => {
+            const restricted = ["url", "favIconUrl", "title"];
 
-          function sanitize(extension, changeInfo) {
-            let result = {};
-            let nonempty = false;
-            for (let prop in changeInfo) {
-              if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
-                nonempty = true;
-                result[prop] = changeInfo[prop];
+            function sanitize(extension, changeInfo) {
+              let result = {};
+              let nonempty = false;
+              for (let prop in changeInfo) {
+                if (extension.hasPermission("tabs") || !restricted.includes(prop)) {
+                  nonempty = true;
+                  result[prop] = changeInfo[prop];
+                }
               }
+              return [nonempty, result];
             }
-            return [nonempty, result];
-          }
 
-          let fireForTab = (tab, changed) => {
-            let [needed, changeInfo] = sanitize(extension, changed);
-            if (needed) {
-              fire.async(tab.id, changeInfo, tab.convert());
-            }
-          };
+            let fireForTab = (tab, changed) => {
+              let [needed, changeInfo] = sanitize(extension, changed);
+              if (needed) {
+                fire.async(tab.id, changeInfo, tab.convert());
+              }
+            };
 
-          let listener = event => {
-            let needed = [];
-            let nativeTab;
-            switch (event.type) {
-              case "DOMTitleChanged": {
-                let {BrowserApp} = getBrowserWindow(event.target.ownerGlobal);
+            let listener = event => {
+              let needed = [];
+              let nativeTab;
+              switch (event.type) {
+                case "DOMTitleChanged": {
+                  let {BrowserApp} = getBrowserWindow(event.target.ownerGlobal);
 
-                nativeTab = BrowserApp.getTabForWindow(event.target.ownerGlobal);
-                needed.push("title");
-                break;
+                  nativeTab = BrowserApp.getTabForWindow(event.target.ownerGlobal);
+                  needed.push("title");
+                  break;
+                }
+
+                case "DOMAudioPlaybackStarted":
+                case "DOMAudioPlaybackStopped": {
+                  let {BrowserApp} = event.target.ownerGlobal;
+                  nativeTab = BrowserApp.getTabForBrowser(event.originalTarget);
+                  needed.push("audible");
+                  break;
+                }
               }
 
-              case "DOMAudioPlaybackStarted":
-              case "DOMAudioPlaybackStopped": {
-                let {BrowserApp} = event.target.ownerGlobal;
-                nativeTab = BrowserApp.getTabForBrowser(event.originalTarget);
-                needed.push("audible");
-                break;
+              if (!nativeTab) {
+                return;
               }
-            }
-
-            if (!nativeTab) {
-              return;
-            }
 
-            let tab = tabManager.getWrapper(nativeTab);
-            let changeInfo = {};
-            for (let prop of needed) {
-              changeInfo[prop] = tab[prop];
-            }
-
-            fireForTab(tab, changeInfo);
-          };
-
-          let statusListener = ({browser, status, url}) => {
-            let {BrowserApp} = browser.ownerGlobal;
-            let nativeTab = BrowserApp.getTabForBrowser(browser);
-            if (nativeTab) {
-              let changed = {status};
-              if (url) {
-                changed.url = url;
+              let tab = tabManager.getWrapper(nativeTab);
+              let changeInfo = {};
+              for (let prop of needed) {
+                changeInfo[prop] = tab[prop];
               }
 
-              fireForTab(tabManager.wrapTab(nativeTab), changed);
-            }
-          };
+              fireForTab(tab, changeInfo);
+            };
+
+            let statusListener = ({browser, status, url}) => {
+              let {BrowserApp} = browser.ownerGlobal;
+              let nativeTab = BrowserApp.getTabForBrowser(browser);
+              if (nativeTab) {
+                let changed = {status};
+                if (url) {
+                  changed.url = url;
+                }
 
-          windowTracker.addListener("status", statusListener);
-          windowTracker.addListener("DOMTitleChanged", listener);
-          return () => {
-            windowTracker.removeListener("status", statusListener);
-            windowTracker.removeListener("DOMTitleChanged", listener);
-          };
+                fireForTab(tabManager.wrapTab(nativeTab), changed);
+              }
+            };
+
+            windowTracker.addListener("status", statusListener);
+            windowTracker.addListener("DOMTitleChanged", listener);
+            return () => {
+              windowTracker.removeListener("status", statusListener);
+              windowTracker.removeListener("DOMTitleChanged", listener);
+            };
+          },
         }).api(),
 
         async create(createProperties) {
           let window = createProperties.windowId !== null ?
             windowTracker.getWindow(createProperties.windowId, context) :
             windowTracker.topWindow;
 
           let {BrowserApp} = window;
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -182,75 +182,51 @@ class WindowTracker extends WindowTracke
     if (wrapper) {
       wrapper.destroy();
       listeners.delete(listener);
     }
   }
 }
 
 /**
- * An event manager API provider which listens for an event in the Android
- * global EventDispatcher, and calls the given listener function whenever an event
- * is received. That listener function receives a `fire` object, which it can
- * use to dispatch events to the extension, and an object detailing the
- * EventDispatcher event that was received.
+ * Helper to create an event manager which listens for an event in the Android
+ * global EventDispatcher, and calls the given listener function whenever the
+ * event is received. That listener function receives a `fire` object,
+ * which it can use to dispatch events to the extension, and an object
+ * detailing the EventDispatcher event that was received.
  *
  * @param {BaseContext} context
  *        The extension context which the event manager belongs to.
  * @param {string} name
  *        The API name of the event manager, e.g.,"runtime.onMessage".
  * @param {string} event
  *        The name of the EventDispatcher event to listen for.
  * @param {function} listener
  *        The listener function to call when an EventDispatcher event is
  *        recieved.
+ *
+ * @returns {object} An injectable api for the new event.
  */
-global.GlobalEventManager = class extends EventManager {
-  constructor(context, name, event, listener) {
-    super(context, name, fire => {
+global.makeGlobalEvent = function makeGlobalEvent(context, name, event, listener) {
+  return new EventManager({
+    context,
+    name,
+    register: fire => {
       let listener2 = {
         onEvent(event, data, callback) {
           listener(fire, data);
         },
       };
 
       GlobalEventDispatcher.registerListener(listener2, [event]);
       return () => {
         GlobalEventDispatcher.unregisterListener(listener2, [event]);
       };
-    });
-  }
-};
-
-/**
- * An event manager API provider which listens for a DOM event in any browser
- * window, and calls the given listener function whenever an event is received.
- * That listener function receives a `fire` object, which it can use to dispatch
- * events to the extension, and a DOM event object.
- *
- * @param {BaseContext} context
- *        The extension context which the event manager belongs to.
- * @param {string} name
- *        The API name of the event manager, e.g.,"runtime.onMessage".
- * @param {string} event
- *        The name of the DOM event to listen for.
- * @param {function} listener
- *        The listener function to call when a DOM event is received.
- */
-global.WindowEventManager = class extends EventManager {
-  constructor(context, name, event, listener) {
-    super(context, name, fire => {
-      let listener2 = listener.bind(null, fire);
-
-      windowTracker.addListener(event, listener2);
-      return () => {
-        windowTracker.removeListener(event, listener2);
-      };
-    });
-  }
+    },
+  }).api();
 };
 
 class TabTracker extends TabTrackerBase {
   constructor() {
     super();
 
     // Keep track of the extension popup tab.
     this._extensionPopupTabWeak = null;
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -174,29 +174,37 @@ class Port {
       disconnect: () => {
         this.disconnect();
       },
 
       postMessage: json => {
         this.postMessage(json);
       },
 
-      onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => {
-        return this.registerOnDisconnect(holder => {
-          let error = holder && holder.deserialize(this.context.cloneScope);
-          portError = error && this.context.normalizeError(error);
-          fire.asyncWithoutClone(portObj);
-        });
+      onDisconnect: new EventManager({
+        context: this.context,
+        name: "Port.onDisconnect",
+        register: fire => {
+          return this.registerOnDisconnect(holder => {
+            let error = holder && holder.deserialize(this.context.cloneScope);
+            portError = error && this.context.normalizeError(error);
+            fire.asyncWithoutClone(portObj);
+          });
+        },
       }).api(),
 
-      onMessage: new EventManager(this.context, "Port.onMessage", fire => {
-        return this.registerOnMessage(holder => {
-          let msg = holder.deserialize(this.context.cloneScope);
-          fire.asyncWithoutClone(msg, portObj);
-        });
+      onMessage: new EventManager({
+        context: this.context,
+        name: "Port.onMessage",
+        register: fire => {
+          return this.registerOnMessage(holder => {
+            let msg = holder.deserialize(this.context.cloneScope);
+            fire.asyncWithoutClone(msg, portObj);
+          });
+        },
       }).api(),
 
       get error() {
         return portError;
       },
     };
 
     if (this.sender) {
@@ -398,73 +406,77 @@ class Messenger {
   }
 
   sendNativeMessage(messageManager, msg, recipient, responseCallback) {
     msg = NativeApp.encodeMessage(this.context, msg);
     return this.sendMessage(messageManager, msg, recipient, responseCallback);
   }
 
   _onMessage(name, filter) {
-    return new EventManager(this.context, name, fire => {
-      const caller = this.context.getCaller();
+    return new EventManager({
+      context: this.context,
+      name,
+      register: fire => {
+        const caller = this.context.getCaller();
 
-      let listener = {
-        messageFilterPermissive: this.optionalFilter,
-        messageFilterStrict: this.filter,
+        let listener = {
+          messageFilterPermissive: this.optionalFilter,
+          messageFilterStrict: this.filter,
 
-        filterMessage: (sender, recipient) => {
-          // Exclude messages coming from content scripts for the devtools extension contexts
-          // (See Bug 1383310).
-          if (this.excludeContentScriptSender && sender.envType === "content_child") {
-            return false;
-          }
+          filterMessage: (sender, recipient) => {
+            // Exclude messages coming from content scripts for the devtools extension contexts
+            // (See Bug 1383310).
+            if (this.excludeContentScriptSender && sender.envType === "content_child") {
+              return false;
+            }
 
-          // Ignore the message if it was sent by this Messenger.
-          return (sender.contextId !== this.context.contextId &&
-                  filter(sender, recipient));
-        },
+            // Ignore the message if it was sent by this Messenger.
+            return (sender.contextId !== this.context.contextId &&
+                    filter(sender, recipient));
+          },
 
-        receiveMessage: ({target, data: holder, sender, recipient, channelId}) => {
-          if (!this.context.active) {
-            return;
-          }
+          receiveMessage: ({target, data: holder, sender, recipient, channelId}) => {
+            if (!this.context.active) {
+              return;
+            }
 
-          let sendResponse;
-          let response = undefined;
-          let promise = new Promise(resolve => {
-            sendResponse = value => {
-              resolve(value);
-              response = promise;
-            };
-          });
+            let sendResponse;
+            let response = undefined;
+            let promise = new Promise(resolve => {
+              sendResponse = value => {
+                resolve(value);
+                response = promise;
+              };
+            });
 
-          let message = holder.deserialize(this.context.cloneScope);
-          holder = null;
+            let message = holder.deserialize(this.context.cloneScope);
+            holder = null;
 
-          sender = Cu.cloneInto(sender, this.context.cloneScope);
-          sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
+            sender = Cu.cloneInto(sender, this.context.cloneScope);
+            sendResponse = Cu.exportFunction(sendResponse, this.context.cloneScope);
 
-          // Note: We intentionally do not use runSafe here so that any
-          // errors are propagated to the message sender.
-          let result = fire.raw(message, sender, sendResponse);
-          message = null;
+            // Note: We intentionally do not use runSafe here so that any
+            // errors are propagated to the message sender.
+            let result = fire.raw(message, sender, sendResponse);
+            message = null;
 
-          if (result instanceof this.context.cloneScope.Promise) {
-            return StrongPromise.wrap(result, channelId, caller);
-          } else if (result === true) {
-            return StrongPromise.wrap(promise, channelId, caller);
-          }
-          return response;
-        },
-      };
+            if (result instanceof this.context.cloneScope.Promise) {
+              return StrongPromise.wrap(result, channelId, caller);
+            } else if (result === true) {
+              return StrongPromise.wrap(promise, channelId, caller);
+            }
+            return response;
+          },
+        };
 
-      MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
-      return () => {
-        MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
-      };
+        MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
+        return () => {
+          MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
+        };
+      },
     }).api();
   }
 
   onMessage(name) {
     return this._onMessage(name, sender => sender.id === this.sender.id);
   }
 
   onMessageExternal(name) {
@@ -501,51 +513,55 @@ class Messenger {
     let portId = getUniqueId();
 
     let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
 
     return this._connect(messageManager, port, recipient);
   }
 
   _onConnect(name, filter) {
-    return new EventManager(this.context, name, fire => {
-      let listener = {
-        messageFilterPermissive: this.optionalFilter,
-        messageFilterStrict: this.filter,
+    return new EventManager({
+      context: this.context,
+      name,
+      register: fire => {
+        let listener = {
+          messageFilterPermissive: this.optionalFilter,
+          messageFilterStrict: this.filter,
 
-        filterMessage: (sender, recipient) => {
-          // Exclude messages coming from content scripts for the devtools extension contexts
-          // (See Bug 1383310).
-          if (this.excludeContentScriptSender && sender.envType === "content_child") {
-            return false;
-          }
+          filterMessage: (sender, recipient) => {
+            // Exclude messages coming from content scripts for the devtools extension contexts
+            // (See Bug 1383310).
+            if (this.excludeContentScriptSender && sender.envType === "content_child") {
+              return false;
+            }
 
-          // Ignore the port if it was created by this Messenger.
-          return (sender.contextId !== this.context.contextId &&
-                  filter(sender, recipient));
-        },
+            // Ignore the port if it was created by this Messenger.
+            return (sender.contextId !== this.context.contextId &&
+                    filter(sender, recipient));
+          },
 
-        receiveMessage: ({target, data: message, sender}) => {
-          let {name, portId} = message;
-          let mm = getMessageManager(target);
-          let recipient = Object.assign({}, sender);
-          if (recipient.tab) {
-            recipient.tabId = recipient.tab.id;
-            delete recipient.tab;
-          }
-          let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
-          fire.asyncWithoutClone(port.api());
-          return true;
-        },
-      };
+          receiveMessage: ({target, data: message, sender}) => {
+            let {name, portId} = message;
+            let mm = getMessageManager(target);
+            let recipient = Object.assign({}, sender);
+            if (recipient.tab) {
+              recipient.tabId = recipient.tab.id;
+              delete recipient.tab;
+            }
+            let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
+            fire.asyncWithoutClone(port.api());
+            return true;
+          },
+        };
 
-      MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
-      return () => {
-        MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
-      };
+        MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
+        return () => {
+          MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
+        };
+      },
     }).api();
   }
 
   onConnect(name) {
     return this._onConnect(name, sender => sender.id === this.sender.id);
   }
 
   onConnectExternal(name) {
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1752,34 +1752,39 @@ defineLazyGetter(LocaleData.prototype, "
  * }).api()
  *
  * The result is an object with addListener, removeListener, and
  * hasListener methods. `context` is an add-on scope (either an
  * ExtensionContext in the chrome process or ExtensionContext in a
  * content process). `name` is for debugging. `register` is a function
  * to register the listener. `register` should return an
  * unregister function that will unregister the listener.
- * @constructor
- *
- * @param {BaseContext} context
- *        An object representing the extension instance using this event.
- * @param {string} name
- *        A name used only for debugging.
- * @param {functon} register
- *        A function called whenever a new listener is added.
  */
-function EventManager(context, name, register) {
-  this.context = context;
-  this.name = name;
-  this.register = register;
-  this.unregister = new Map();
-  this.inputHandling = false;
-}
+class EventManager {
+  /*
+   * @param {object} params
+   *        Parameters that control this EventManager.
+   * @param {BaseContext} params.context
+   *        An object representing the extension instance using this event.
+   * @param {string} params.name
+   *        A name used only for debugging.
+   * @param {functon} params.register
+   *        A function called whenever a new listener is added.
+   * @param {boolean} [params.inputHandling=false]
+   *        If true, the "handling user input" flag is set while handlers
+   *        for this event are executing.
+   */
+  constructor({context, name, register, inputHandling = false}) {
+    this.context = context;
+    this.name = name;
+    this.register = register;
+    this.unregister = new Map();
+    this.inputHandling = inputHandling;
+  }
 
-EventManager.prototype = {
   addListener(callback, ...args) {
     if (this.unregister.has(callback)) {
       return;
     }
 
     let shouldFire = () => {
       if (this.context.unloaded) {
         dump(`${this.name} event fired after context unloaded.\n`);
@@ -1814,63 +1819,62 @@ EventManager.prototype = {
         return Promise.resolve().then(() => {
           if (shouldFire()) {
             return this.context.applySafeWithoutClone(callback, args);
           }
         });
       },
     };
 
-
     let unregister = this.register(fire, ...args);
     this.unregister.set(callback, unregister);
     this.context.callOnClose(this);
-  },
+  }
 
   removeListener(callback) {
     if (!this.unregister.has(callback)) {
       return;
     }
 
     let unregister = this.unregister.get(callback);
     this.unregister.delete(callback);
     try {
       unregister();
     } catch (e) {
       Cu.reportError(e);
     }
     if (this.unregister.size == 0) {
       this.context.forgetOnClose(this);
     }
-  },
+  }
 
   hasListener(callback) {
     return this.unregister.has(callback);
-  },
+  }
 
   revoke() {
     for (let callback of this.unregister.keys()) {
       this.removeListener(callback);
     }
-  },
+  }
 
   close() {
     this.revoke();
-  },
+  }
 
   api() {
     return {
       addListener: (...args) => this.addListener(...args),
       removeListener: (...args) => this.removeListener(...args),
       hasListener: (...args) => this.hasListener(...args),
       setUserInput: this.inputHandling,
       [Schemas.REVOKE]: () => this.revoke(),
     };
-  },
-};
+  }
+}
 
 // Simple API for event listeners where events never fire.
 function ignoreEvent(context, name) {
   return {
     addListener: function(callback) {
       let id = context.extension.id;
       let frame = Components.stack.caller;
       let msg = `In add-on ${id}, attempting to use listener "${name}", which is unimplemented.`;
--- a/toolkit/components/extensions/ext-alarms.js
+++ b/toolkit/components/extensions/ext-alarms.js
@@ -120,22 +120,26 @@ this.alarms = class extends ExtensionAPI
           let cleared = false;
           for (let alarm of self.alarms.values()) {
             alarm.clear();
             cleared = true;
           }
           return Promise.resolve(cleared);
         },
 
-        onAlarm: new EventManager(context, "alarms.onAlarm", fire => {
-          let callback = alarm => {
-            fire.sync(alarm.data);
-          };
+        onAlarm: new EventManager({
+          context,
+          name: "alarms.onAlarm",
+          register: fire => {
+            let callback = alarm => {
+              fire.sync(alarm.data);
+            };
 
-          self.callbacks.add(callback);
-          return () => {
-            self.callbacks.delete(callback);
-          };
+            self.callbacks.add(callback);
+            return () => {
+              self.callbacks.delete(callback);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-c-storage.js
+++ b/toolkit/components/extensions/ext-c-storage.js
@@ -145,27 +145,31 @@ this.storage = class extends ExtensionAP
           remove(keys) {
             return Promise.reject({message: "storage.managed is read-only"});
           },
           clear() {
             return Promise.reject({message: "storage.managed is read-only"});
           },
         },
 
-        onChanged: new EventManager(context, "storage.onChanged", fire => {
-          let onChanged = (data, area) => {
-            let changes = new context.cloneScope.Object();
-            for (let [key, value] of Object.entries(data)) {
-              changes[key] = deserialize(value);
-            }
-            fire.raw(changes, area);
-          };
+        onChanged: new EventManager({
+          context,
+          name: "storage.onChanged",
+          register: fire => {
+            let onChanged = (data, area) => {
+              let changes = new context.cloneScope.Object();
+              for (let [key, value] of Object.entries(data)) {
+                changes[key] = deserialize(value);
+              }
+              fire.raw(changes, area);
+            };
 
-          let parent = context.childManager.getParentEvent("storage.onChanged");
-          parent.addListener(onChanged);
-          return () => {
-            parent.removeListener(onChanged);
-          };
+            let parent = context.childManager.getParentEvent("storage.onChanged");
+            parent.addListener(onChanged);
+            return () => {
+              parent.removeListener(onChanged);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-c-test.js
+++ b/toolkit/components/extensions/ext-c-test.js
@@ -175,22 +175,28 @@ this.test = class extends ExtensionAPI {
             let errorMessage = toSource(error && error.message);
 
             assertTrue(errorMatches(error, expectedError, context),
                        `Function threw, expecting error to match ${toSource(expectedError)}` +
                        `got ${errorMessage}${msg}`);
           }
         },
 
-        onMessage: new TestEventManager(context, "test.onMessage", fire => {
-          let handler = (event, ...args) => {
-            fire.async(...args);
-          };
+        onMessage: new TestEventManager({
+          context,
+          name: "test.onMessage",
+          register: fire => {
+            let handler = (event, ...args) => {
+              dump(`in handler\n`);
+              fire.async(...args);
+            };
 
-          extension.on("test-harness-message", handler);
-          return () => {
-            extension.off("test-harness-message", handler);
-          };
+            dump(`in register, listening in ${extension.id}\n`);
+            extension.on("test-harness-message", handler);
+            return () => {
+              extension.off("test-harness-message", handler);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-contextualIdentities.js
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -222,56 +222,68 @@ this.contextualIdentities = class extend
 
           if (!ContextualIdentityService.remove(identity.userContextId)) {
             throw new ExtensionError(`Contextual identity failed to remove: ${cookieStoreId}`);
           }
 
           return convertedIdentity;
         },
 
-        onCreated: new EventManager(context, "contextualIdentities.onCreated", fire => {
-          let observer = (subject, topic) => {
-            let convertedIdentity = convertIdentityFromObserver(subject);
-            if (convertedIdentity) {
-              fire.async({contextualIdentity: convertedIdentity});
-            }
-          };
+        onCreated: new EventManager({
+          context,
+          name: "contextualIdentities.onCreated",
+          register: fire => {
+            let observer = (subject, topic) => {
+              let convertedIdentity = convertIdentityFromObserver(subject);
+              if (convertedIdentity) {
+                fire.async({contextualIdentity: convertedIdentity});
+              }
+            };
 
-          Services.obs.addObserver(observer, "contextual-identity-created");
-          return () => {
-            Services.obs.removeObserver(observer, "contextual-identity-created");
-          };
+            Services.obs.addObserver(observer, "contextual-identity-created");
+            return () => {
+              Services.obs.removeObserver(observer, "contextual-identity-created");
+            };
+          },
         }).api(),
 
-        onUpdated: new EventManager(context, "contextualIdentities.onUpdated", fire => {
-          let observer = (subject, topic) => {
-            let convertedIdentity = convertIdentityFromObserver(subject);
-            if (convertedIdentity) {
-              fire.async({contextualIdentity: convertedIdentity});
-            }
-          };
+        onUpdated: new EventManager({
+          context,
+          name: "contextualIdentities.onUpdated",
+          register: fire => {
+            let observer = (subject, topic) => {
+              let convertedIdentity = convertIdentityFromObserver(subject);
+              if (convertedIdentity) {
+                fire.async({contextualIdentity: convertedIdentity});
+              }
+            };
 
-          Services.obs.addObserver(observer, "contextual-identity-updated");
-          return () => {
-            Services.obs.removeObserver(observer, "contextual-identity-updated");
-          };
+            Services.obs.addObserver(observer, "contextual-identity-updated");
+            return () => {
+              Services.obs.removeObserver(observer, "contextual-identity-updated");
+            };
+          },
         }).api(),
 
-        onRemoved: new EventManager(context, "contextualIdentities.onRemoved", fire => {
-          let observer = (subject, topic) => {
-            let convertedIdentity = convertIdentityFromObserver(subject);
-            if (convertedIdentity) {
-              fire.async({contextualIdentity: convertedIdentity});
-            }
-          };
+        onRemoved: new EventManager({
+          context,
+          name: "contextualIdentities.onRemoved",
+          register: fire => {
+            let observer = (subject, topic) => {
+              let convertedIdentity = convertIdentityFromObserver(subject);
+              if (convertedIdentity) {
+                fire.async({contextualIdentity: convertedIdentity});
+              }
+            };
 
-          Services.obs.addObserver(observer, "contextual-identity-deleted");
-          return () => {
-            Services.obs.removeObserver(observer, "contextual-identity-deleted");
-          };
+            Services.obs.addObserver(observer, "contextual-identity-deleted");
+            return () => {
+              Services.obs.removeObserver(observer, "contextual-identity-deleted");
+            };
+          },
         }).api(),
 
       },
     };
 
     return self;
   }
 };
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -410,57 +410,61 @@ this.cookies = class extends ExtensionAP
 
           let result = [];
           for (let key in data) {
             result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE});
           }
           return Promise.resolve(result);
         },
 
-        onChanged: new EventManager(context, "cookies.onChanged", fire => {
-          let observer = (subject, topic, data) => {
-            let notify = (removed, cookie, cause) => {
-              cookie.QueryInterface(Ci.nsICookie2);
+        onChanged: new EventManager({
+          context,
+          name: "cookies.onChanged",
+          register: fire => {
+            let observer = (subject, topic, data) => {
+              let notify = (removed, cookie, cause) => {
+                cookie.QueryInterface(Ci.nsICookie2);
+
+                if (extension.whiteListedHosts.matchesCookie(cookie)) {
+                  fire.async({removed, cookie: convertCookie({cookie, isPrivate: topic == "private-cookie-changed"}), cause});
+                }
+              };
 
-              if (extension.whiteListedHosts.matchesCookie(cookie)) {
-                fire.async({removed, cookie: convertCookie({cookie, isPrivate: topic == "private-cookie-changed"}), cause});
+              // We do our best effort here to map the incompatible states.
+              switch (data) {
+                case "deleted":
+                  notify(true, subject, "explicit");
+                  break;
+                case "added":
+                  notify(false, subject, "explicit");
+                  break;
+                case "changed":
+                  notify(true, subject, "overwrite");
+                  notify(false, subject, "explicit");
+                  break;
+                case "batch-deleted":
+                  subject.QueryInterface(Ci.nsIArray);
+                  for (let i = 0; i < subject.length; i++) {
+                    let cookie = subject.queryElementAt(i, Ci.nsICookie2);
+                    if (!cookie.isSession && cookie.expiry * 1000 <= Date.now()) {
+                      notify(true, cookie, "expired");
+                    } else {
+                      notify(true, cookie, "evicted");
+                    }
+                  }
+                  break;
               }
             };
 
-            // We do our best effort here to map the incompatible states.
-            switch (data) {
-              case "deleted":
-                notify(true, subject, "explicit");
-                break;
-              case "added":
-                notify(false, subject, "explicit");
-                break;
-              case "changed":
-                notify(true, subject, "overwrite");
-                notify(false, subject, "explicit");
-                break;
-              case "batch-deleted":
-                subject.QueryInterface(Ci.nsIArray);
-                for (let i = 0; i < subject.length; i++) {
-                  let cookie = subject.queryElementAt(i, Ci.nsICookie2);
-                  if (!cookie.isSession && cookie.expiry * 1000 <= Date.now()) {
-                    notify(true, cookie, "expired");
-                  } else {
-                    notify(true, cookie, "evicted");
-                  }
-                }
-                break;
-            }
-          };
-
-          Services.obs.addObserver(observer, "cookie-changed");
-          Services.obs.addObserver(observer, "private-cookie-changed");
-          return () => {
-            Services.obs.removeObserver(observer, "cookie-changed");
-            Services.obs.removeObserver(observer, "private-cookie-changed");
-          };
+            Services.obs.addObserver(observer, "cookie-changed");
+            Services.obs.addObserver(observer, "private-cookie-changed");
+            return () => {
+              Services.obs.removeObserver(observer, "cookie-changed");
+              Services.obs.removeObserver(observer, "private-cookie-changed");
+            };
+          },
         }).api(),
       },
     };
 
     return self;
   }
 };
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -757,69 +757,81 @@ this.downloads = class extends Extension
         // i.e.:
         // setShelfEnabled(enabled) {
         //   if (!extension.hasPermission("downloads.shelf")) {
         //     throw new context.cloneScope.Error("Permission denied because 'downloads.shelf' permission is missing.");
         //   }
         //   ...
         // }
 
-        onChanged: new EventManager(context, "downloads.onChanged", fire => {
-          const handler = (what, item) => {
-            let changes = {};
-            const noundef = val => (val === undefined) ? null : val;
-            DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
-              if (item[fld] != item.prechange[fld]) {
-                changes[fld] = {
-                  previous: noundef(item.prechange[fld]),
-                  current: noundef(item[fld]),
-                };
+        onChanged: new EventManager({
+          context,
+          name: "downloads.onChanged",
+          register: fire => {
+            const handler = (what, item) => {
+              let changes = {};
+              const noundef = val => (val === undefined) ? null : val;
+              DOWNLOAD_ITEM_CHANGE_FIELDS.forEach(fld => {
+                if (item[fld] != item.prechange[fld]) {
+                  changes[fld] = {
+                    previous: noundef(item.prechange[fld]),
+                    current: noundef(item[fld]),
+                  };
+                }
+              });
+              if (Object.keys(changes).length > 0) {
+                changes.id = item.id;
+                fire.async(changes);
               }
+            };
+
+            let registerPromise = DownloadMap.getDownloadList().then(() => {
+              DownloadMap.on("change", handler);
             });
-            if (Object.keys(changes).length > 0) {
-              changes.id = item.id;
-              fire.async(changes);
-            }
-          };
-
-          let registerPromise = DownloadMap.getDownloadList().then(() => {
-            DownloadMap.on("change", handler);
-          });
-          return () => {
-            registerPromise.then(() => {
-              DownloadMap.off("change", handler);
-            });
-          };
+            return () => {
+              registerPromise.then(() => {
+                DownloadMap.off("change", handler);
+              });
+            };
+          },
         }).api(),
 
-        onCreated: new EventManager(context, "downloads.onCreated", fire => {
-          const handler = (what, item) => {
-            fire.async(item.serialize());
-          };
-          let registerPromise = DownloadMap.getDownloadList().then(() => {
-            DownloadMap.on("create", handler);
-          });
-          return () => {
-            registerPromise.then(() => {
-              DownloadMap.off("create", handler);
+        onCreated: new EventManager({
+          context,
+          name: "downloads.onCreated",
+          register: fire => {
+            const handler = (what, item) => {
+              fire.async(item.serialize());
+            };
+            let registerPromise = DownloadMap.getDownloadList().then(() => {
+              DownloadMap.on("create", handler);
             });
-          };
+            return () => {
+              registerPromise.then(() => {
+                DownloadMap.off("create", handler);
+              });
+            };
+          },
         }).api(),
 
-        onErased: new EventManager(context, "downloads.onErased", fire => {
-          const handler = (what, item) => {
-            fire.async(item.id);
-          };
-          let registerPromise = DownloadMap.getDownloadList().then(() => {
-            DownloadMap.on("erase", handler);
-          });
-          return () => {
-            registerPromise.then(() => {
-              DownloadMap.off("erase", handler);
+        onErased: new EventManager({
+          context,
+          name: "downloads.onErased",
+          register: fire => {
+            const handler = (what, item) => {
+              fire.async(item.id);
+            };
+            let registerPromise = DownloadMap.getDownloadList().then(() => {
+              DownloadMap.on("erase", handler);
             });
-          };
+            return () => {
+              registerPromise.then(() => {
+                DownloadMap.off("erase", handler);
+              });
+            };
+          },
         }).api(),
 
         onDeterminingFilename: ignoreEvent(context, "downloads.onDeterminingFilename"),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-idle.js
+++ b/toolkit/components/extensions/ext-idle.js
@@ -68,22 +68,26 @@ this.idle = class extends ExtensionAPI {
           if (idleService.idleTime < detectionIntervalInSeconds * 1000) {
             return Promise.resolve("active");
           }
           return Promise.resolve("idle");
         },
         setDetectionInterval: function(detectionIntervalInSeconds) {
           setDetectionInterval(extension, context, detectionIntervalInSeconds);
         },
-        onStateChanged: new EventManager(context, "idle.onStateChanged", fire => {
-          let listener = (event, data) => {
-            fire.sync(data);
-          };
+        onStateChanged: new EventManager({
+          context,
+          name: "idle.onStateChanged",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.sync(data);
+            };
 
-          getIdleObserver(extension, context).on("stateChanged", listener);
-          return () => {
-            getIdleObserver(extension, context).off("stateChanged", listener);
-          };
+            getIdleObserver(extension, context).on("stateChanged", listener);
+            return () => {
+              getIdleObserver(extension, context).off("stateChanged", listener);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-management.js
+++ b/toolkit/components/extensions/ext-management.js
@@ -228,56 +228,72 @@ this.management = class extends Extensio
             throw new ExtensionError("setEnabled applies only to theme addons");
           }
           if (addon.isSystem) {
             throw new ExtensionError("setEnabled cannot be used with a system addon");
           }
           addon.userDisabled = !enabled;
         },
 
-        onDisabled: new EventManager(context, "management.onDisabled", fire => {
-          let listener = (event, data) => {
-            fire.async(data);
-          };
+        onDisabled: new EventManager({
+          context,
+          name: "management.onDisabled",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.async(data);
+            };
 
-          getManagementListener(extension, context).on("onDisabled", listener);
-          return () => {
-            getManagementListener(extension, context).off("onDisabled", listener);
-          };
+            getManagementListener(extension, context).on("onDisabled", listener);
+            return () => {
+              getManagementListener(extension, context).off("onDisabled", listener);
+            };
+          },
         }).api(),
 
-        onEnabled: new EventManager(context, "management.onEnabled", fire => {
-          let listener = (event, data) => {
-            fire.async(data);
-          };
+        onEnabled: new EventManager({
+          context,
+          name: "management.onEnabled",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.async(data);
+            };
 
-          getManagementListener(extension, context).on("onEnabled", listener);
-          return () => {
-            getManagementListener(extension, context).off("onEnabled", listener);
-          };
+            getManagementListener(extension, context).on("onEnabled", listener);
+            return () => {
+              getManagementListener(extension, context).off("onEnabled", listener);
+            };
+          },
         }).api(),
 
-        onInstalled: new EventManager(context, "management.onInstalled", fire => {
-          let listener = (event, data) => {
-            fire.async(data);
-          };
+        onInstalled: new EventManager({
+          context,
+          name: "management.onInstalled",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.async(data);
+            };
 
-          getManagementListener(extension, context).on("onInstalled", listener);
-          return () => {
-            getManagementListener(extension, context).off("onInstalled", listener);
-          };
+            getManagementListener(extension, context).on("onInstalled", listener);
+            return () => {
+              getManagementListener(extension, context).off("onInstalled", listener);
+            };
+          },
         }).api(),
 
-        onUninstalled: new EventManager(context, "management.onUninstalled", fire => {
-          let listener = (event, data) => {
-            fire.async(data);
-          };
+        onUninstalled: new EventManager({
+          context,
+          name: "management.onUninstalled",
+          register: fire => {
+            let listener = (event, data) => {
+              fire.async(data);
+            };
 
-          getManagementListener(extension, context).on("onUninstalled", listener);
-          return () => {
-            getManagementListener(extension, context).off("onUninstalled", listener);
-          };
+            getManagementListener(extension, context).on("onUninstalled", listener);
+            return () => {
+              getManagementListener(extension, context).off("onUninstalled", listener);
+            };
+          },
         }).api(),
 
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-notifications.js
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -115,48 +115,60 @@ this.notifications = class extends Exten
         getAll: function() {
           let result = {};
           notificationsMap.forEach((value, key) => {
             result[key] = value.options;
           });
           return Promise.resolve(result);
         },
 
-        onClosed: new EventManager(context, "notifications.onClosed", fire => {
-          let listener = (event, notificationId) => {
-            // TODO Bug 1413188, Support the byUser argument.
-            fire.async(notificationId, true);
-          };
+        onClosed: new EventManager({
+          context,
+          name: "notifications.onClosed",
+          register: fire => {
+            let listener = (event, notificationId) => {
+              // TODO Bug 1413188, Support the byUser argument.
+              fire.async(notificationId, true);
+            };
 
-          notificationsMap.on("closed", listener);
-          return () => {
-            notificationsMap.off("closed", listener);
-          };
+            notificationsMap.on("closed", listener);
+            return () => {
+              notificationsMap.off("closed", listener);
+            };
+          },
         }).api(),
 
-        onClicked: new EventManager(context, "notifications.onClicked", fire => {
-          let listener = (event, notificationId) => {
-            fire.async(notificationId, true);
-          };
+        onClicked: new EventManager({
+          context,
+          name: "notifications.onClicked",
+          register: fire => {
+            let listener = (event, notificationId) => {
+              fire.async(notificationId, true);
+            };
 
-          notificationsMap.on("clicked", listener);
-          return () => {
-            notificationsMap.off("clicked", listener);
-          };
+            notificationsMap.on("clicked", listener);
+            return () => {
+              notificationsMap.off("clicked", listener);
+            };
+          },
         }).api(),
 
-        onShown: new EventManager(context, "notifications.onShown", fire => {
-          let listener = (event, notificationId) => {
-            fire.async(notificationId, true);
-          };
+        onShown: new EventManager({
+          context,
+          name: "notifications.onShown",
+          register: fire => {
+            let listener = (event, notificationId) => {
+              fire.async(notificationId, true);
+            };
 
-          notificationsMap.on("shown", listener);
-          return () => {
-            notificationsMap.off("shown", listener);
-          };
+            notificationsMap.on("shown", listener);
+            return () => {
+              notificationsMap.off("shown", listener);
+            };
+          },
         }).api(),
 
         // TODO Bug 1190681, implement button support.
         onButtonClicked: ignoreEvent(context, "notifications.onButtonClicked"),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-proxy.js
+++ b/toolkit/components/extensions/ext-proxy.js
@@ -42,17 +42,17 @@ class ProxyFilterEventManager extends Ev
       }
 
       let proxyFilter = new ProxyChannelFilter(context, listener, filter, extraInfoSpec);
       return () => {
         proxyFilter.destroy();
       };
     };
 
-    super(context, name, register);
+    super({context, name, register});
   }
 }
 
 this.proxy = class extends ExtensionAPI {
   onShutdown() {
     let {extension} = this;
 
     let proxyScriptContext = proxyScriptContextMap.get(extension);
@@ -60,24 +60,28 @@ this.proxy = class extends ExtensionAPI 
       proxyScriptContext.unload();
       proxyScriptContextMap.delete(extension);
     }
   }
 
   getAPI(context) {
     let {extension} = context;
 
-    let onError = new EventManager(context, "proxy.onError", fire => {
-      let listener = (name, error) => {
-        fire.async(error);
-      };
-      extension.on("proxy-error", listener);
-      return () => {
-        extension.off("proxy-error", listener);
-      };
+    let onError = new EventManager({
+      context,
+      name: "proxy.onError",
+      register: fire => {
+        let listener = (name, error) => {
+          fire.async(error);
+        };
+        extension.on("proxy-error", listener);
+        return () => {
+          extension.off("proxy-error", listener);
+        };
+      },
     }).api();
 
     return {
       proxy: {
         register(url) {
           this.unregister();
 
           let proxyScriptContext = new ProxyScriptContext(extension, url);
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -14,74 +14,86 @@ ChromeUtils.defineModuleGetter(this, "Se
 ChromeUtils.defineModuleGetter(this, "DevToolsShim",
                                "chrome://devtools-shim/content/DevToolsShim.jsm");
 
 this.runtime = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
     return {
       runtime: {
-        onStartup: new EventManager(context, "runtime.onStartup", fire => {
-          if (context.incognito) {
-            // This event should not fire if we are operating in a private profile.
-            return () => {};
-          }
-          let listener = () => {
-            if (extension.startupReason === "APP_STARTUP") {
-              fire.sync();
+        onStartup: new EventManager({
+          context,
+          name: "runtime.onStartup",
+          register: fire => {
+            if (context.incognito) {
+              // This event should not fire if we are operating in a private profile.
+              return () => {};
             }
-          };
-          extension.on("startup", listener);
-          return () => {
-            extension.off("startup", listener);
-          };
+            let listener = () => {
+              if (extension.startupReason === "APP_STARTUP") {
+                fire.sync();
+              }
+            };
+            extension.on("startup", listener);
+            return () => {
+              extension.off("startup", listener);
+            };
+          },
         }).api(),
 
-        onInstalled: new EventManager(context, "runtime.onInstalled", fire => {
-          let temporary = !!extension.addonData.temporarilyInstalled;
+        onInstalled: new EventManager({
+          context,
+          name: "runtime.onInstalled",
+          register: fire => {
+            let temporary = !!extension.addonData.temporarilyInstalled;
 
-          let listener = () => {
-            switch (extension.startupReason) {
-              case "APP_STARTUP":
-                if (AddonManagerPrivate.browserUpdated) {
-                  fire.sync({reason: "browser_update", temporary});
-                }
-                break;
-              case "ADDON_INSTALL":
-                fire.sync({reason: "install", temporary});
-                break;
-              case "ADDON_UPGRADE":
-                fire.sync({
-                  reason: "update",
-                  previousVersion: extension.addonData.oldVersion,
-                  temporary,
-                });
-                break;
-            }
-          };
-          extension.on("startup", listener);
-          return () => {
-            extension.off("startup", listener);
-          };
+            let listener = () => {
+              switch (extension.startupReason) {
+                case "APP_STARTUP":
+                  if (AddonManagerPrivate.browserUpdated) {
+                    fire.sync({reason: "browser_update", temporary});
+                  }
+                  break;
+                case "ADDON_INSTALL":
+                  fire.sync({reason: "install", temporary});
+                  break;
+                case "ADDON_UPGRADE":
+                  fire.sync({
+                    reason: "update",
+                    previousVersion: extension.addonData.oldVersion,
+                    temporary,
+                  });
+                  break;
+              }
+            };
+            extension.on("startup", listener);
+            return () => {
+              extension.off("startup", listener);
+            };
+          },
         }).api(),
 
-        onUpdateAvailable: new EventManager(context, "runtime.onUpdateAvailable", fire => {
-          let instanceID = extension.addonData.instanceID;
-          AddonManager.addUpgradeListener(instanceID, upgrade => {
-            extension.upgrade = upgrade;
-            let details = {
-              version: upgrade.version,
+        onUpdateAvailable: new EventManager({
+          context,
+          name: "runtime.onUpdateAvailable",
+          register: fire => {
+            let instanceID = extension.addonData.instanceID;
+            AddonManager.addUpgradeListener(instanceID, upgrade => {
+              extension.upgrade = upgrade;
+              let details = {
+                version: upgrade.version,
+              };
+              fire.sync(details);
+            });
+            return () => {
+              AddonManager.removeUpgradeListener(instanceID).catch(e => {
+                // This can happen if we try this after shutdown is complete.
+              });
             };
-            fire.sync(details);
-          });
-          return () => {
-            AddonManager.removeUpgradeListener(instanceID).catch(e => {
-              // This can happen if we try this after shutdown is complete.
-            });
-          };
+          },
         }).api(),
 
         reload: () => {
           if (extension.upgrade) {
             // If there is a pending update, install it now.
             extension.upgrade.install();
           } else {
             // Otherwise, reload the current extension.
--- a/toolkit/components/extensions/ext-storage.js
+++ b/toolkit/components/extensions/ext-storage.js
@@ -87,27 +87,31 @@ this.storage = class extends ExtensionAP
             let data = await lookup;
             if (!data) {
               return Promise.reject({message: "Managed storage manifest not found"});
             }
             return ExtensionStorage._filterProperties(data, keys);
           },
         },
 
-        onChanged: new EventManager(context, "storage.onChanged", fire => {
-          let listenerLocal = changes => {
-            fire.raw(changes, "local");
-          };
-          let listenerSync = changes => {
-            fire.async(changes, "sync");
-          };
+        onChanged: new EventManager({
+          context,
+          name: "storage.onChanged",
+          register: fire => {
+            let listenerLocal = changes => {
+              fire.raw(changes, "local");
+            };
+            let listenerSync = changes => {
+              fire.async(changes, "sync");
+            };
 
-          ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
-          extensionStorageSync.addOnChangedListener(extension, listenerSync, context);
-          return () => {
-            ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
-            extensionStorageSync.removeOnChangedListener(extension, listenerSync);
-          };
+            ExtensionStorage.addOnChangedListener(extension.id, listenerLocal);
+            extensionStorageSync.addOnChangedListener(extension, listenerSync, context);
+            return () => {
+              ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal);
+              extensionStorageSync.removeOnChangedListener(extension, listenerSync);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-theme.js
+++ b/toolkit/components/extensions/ext-theme.js
@@ -401,26 +401,30 @@ this.theme = class extends ExtensionAPI 
 
           if (!defaultTheme && !windowOverrides.has(windowId)) {
             // If no theme has been initialized, nothing to do.
             return;
           }
 
           Theme.unload(windowId);
         },
-        onUpdated: new EventManager(context, "theme.onUpdated", fire => {
-          let callback = (event, theme, windowId) => {
-            if (windowId) {
-              fire.async({theme, windowId});
-            } else {
-              fire.async({theme});
-            }
-          };
+        onUpdated: new EventManager({
+          context,
+          name: "theme.onUpdated",
+          register: fire => {
+            let callback = (event, theme, windowId) => {
+              if (windowId) {
+                fire.async({theme, windowId});
+              } else {
+                fire.async({theme});
+              }
+            };
 
-          onUpdatedEmitter.on("theme-updated", callback);
-          return () => {
-            onUpdatedEmitter.off("theme-updated", callback);
-          };
+            onUpdatedEmitter.on("theme-updated", callback);
+            return () => {
+              onUpdatedEmitter.off("theme-updated", callback);
+            };
+          },
         }).api(),
       },
     };
   }
 };
--- a/toolkit/components/extensions/ext-toolkit.js
+++ b/toolkit/components/extensions/ext-toolkit.js
@@ -16,22 +16,16 @@ ChromeUtils.defineModuleGetter(this, "Co
                                "resource://gre/modules/ContextualIdentityService.jsm");
 
 Cu.importGlobalProperties(["URL"]);
 
 ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
 
 global.EventEmitter = ExtensionUtils.EventEmitter;
 global.EventManager = ExtensionCommon.EventManager;
-global.InputEventManager = class extends EventManager {
-  constructor(...args) {
-    super(...args);
-    this.inputHandling = true;
-  }
-};
 
 /* globals DEFAULT_STORE, PRIVATE_STORE, CONTAINER_STORE */
 
 global.DEFAULT_STORE = "firefox-default";
 global.PRIVATE_STORE = "firefox-private";
 global.CONTAINER_STORE = "firefox-container-";
 
 global.getCookieStoreIdForTab = function(data, tab) {
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -88,89 +88,93 @@ const fillTransitionProperties = (eventN
 
     // Fill the transition properties in the webNavigation event object.
     dst.transitionType = transitionType;
     dst.transitionQualifiers = transitionQualifiers;
   }
 };
 
 // Similar to WebRequestEventManager but for WebNavigation.
-function WebNavigationEventManager(context, eventName) {
-  let name = `webNavigation.${eventName}`;
-  let register = (fire, urlFilters) => {
-    // Don't create a MatchURLFilters instance if the listener does not include any filter.
-    let filters = urlFilters ? new MatchURLFilters(urlFilters.url) : null;
+class WebNavigationEventManager extends EventManager {
+  constructor(context, eventName) {
+    let name = `webNavigation.${eventName}`;
+    let register = (fire, urlFilters) => {
+      // Don't create a MatchURLFilters instance if the listener does not include any filter.
+      let filters = urlFilters ? new MatchURLFilters(urlFilters.url) : null;
+
+      let listener = data => {
+        if (!data.browser) {
+          return;
+        }
+
+        let data2 = {
+          url: data.url,
+          timeStamp: Date.now(),
+        };
+
+        if (eventName == "onErrorOccurred") {
+          data2.error = data.error;
+        }
 
-    let listener = data => {
-      if (!data.browser) {
-        return;
-      }
+        if (data.frameId != undefined) {
+          data2.frameId = data.frameId;
+          data2.parentFrameId = data.parentFrameId;
+        }
+
+        if (data.sourceFrameId != undefined) {
+          data2.sourceFrameId = data.sourceFrameId;
+        }
 
-      let data2 = {
-        url: data.url,
-        timeStamp: Date.now(),
+        // Fills in tabId typically.
+        Object.assign(data2, tabTracker.getBrowserData(data.browser));
+        if (data2.tabId < 0) {
+          return;
+        }
+
+        if (data.sourceTabBrowser) {
+          data2.sourceTabId = tabTracker.getBrowserData(data.sourceTabBrowser).tabId;
+        }
+
+        fillTransitionProperties(eventName, data, data2);
+
+        fire.async(data2);
       };
 
-      if (eventName == "onErrorOccurred") {
-        data2.error = data.error;
-      }
-
-      if (data.frameId != undefined) {
-        data2.frameId = data.frameId;
-        data2.parentFrameId = data.parentFrameId;
-      }
-
-      if (data.sourceFrameId != undefined) {
-        data2.sourceFrameId = data.sourceFrameId;
-      }
-
-      // Fills in tabId typically.
-      Object.assign(data2, tabTracker.getBrowserData(data.browser));
-      if (data2.tabId < 0) {
-        return;
-      }
-
-      if (data.sourceTabBrowser) {
-        data2.sourceTabId = tabTracker.getBrowserData(data.sourceTabBrowser).tabId;
-      }
-
-      fillTransitionProperties(eventName, data, data2);
-
-      fire.async(data2);
+      WebNavigation[eventName].addListener(listener, filters);
+      return () => {
+        WebNavigation[eventName].removeListener(listener);
+      };
     };
 
-    WebNavigation[eventName].addListener(listener, filters);
-    return () => {
-      WebNavigation[eventName].removeListener(listener);
-    };
-  };
-
-  return EventManager.call(this, context, name, register);
+    super({context, name, register});
+  }
 }
 
-WebNavigationEventManager.prototype = Object.create(EventManager.prototype);
-
 const convertGetFrameResult = (tabId, data) => {
   return {
     errorOccurred: data.errorOccurred,
     url: data.url,
     tabId,
     frameId: data.frameId,
     parentFrameId: data.parentFrameId,
   };
 };
 
 this.webNavigation = class extends ExtensionAPI {
   getAPI(context) {
     let {tabManager} = context.extension;
 
     return {
       webNavigation: {
-        onTabReplaced: new EventManager(context, "webNavigation.onTabReplaced", fire => {
-          return () => {};
+        onTabReplaced: new EventManager({
+          context,
+          name: "webNavigation.onTabReplaced",
+          register: fire => {
+            return () => {};
+          },
         }).api(),
         onBeforeNavigate: new WebNavigationEventManager(context, "onBeforeNavigate").api(),
         onCommitted: new WebNavigationEventManager(context, "onCommitted").api(),
         onDOMContentLoaded: new WebNavigationEventManager(context, "onDOMContentLoaded").api(),
         onCompleted: new WebNavigationEventManager(context, "onCompleted").api(),
         onErrorOccurred: new WebNavigationEventManager(context, "onErrorOccurred").api(),
         onReferenceFragmentUpdated: new WebNavigationEventManager(context, "onReferenceFragmentUpdated").api(),
         onHistoryStateUpdated: new WebNavigationEventManager(context, "onHistoryStateUpdated").api(),
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -79,33 +79,31 @@ function WebRequestEventManager(context,
     WebRequest[eventName].addListener(
       listener, filter2, info2,
       listenerDetails);
     return () => {
       WebRequest[eventName].removeListener(listener);
     };
   };
 
-  return EventManager.call(this, context, name, register);
+  return new EventManager({context, name, register}).api();
 }
 
-WebRequestEventManager.prototype = Object.create(EventManager.prototype);
-
 this.webRequest = class extends ExtensionAPI {
   getAPI(context) {
     return {
       webRequest: {
-        onBeforeRequest: new WebRequestEventManager(context, "onBeforeRequest").api(),
-        onBeforeSendHeaders: new WebRequestEventManager(context, "onBeforeSendHeaders").api(),
-        onSendHeaders: new WebRequestEventManager(context, "onSendHeaders").api(),
-        onHeadersReceived: new WebRequestEventManager(context, "onHeadersReceived").api(),
-        onAuthRequired: new WebRequestEventManager(context, "onAuthRequired").api(),
-        onBeforeRedirect: new WebRequestEventManager(context, "onBeforeRedirect").api(),
-        onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
-        onErrorOccurred: new WebRequestEventManager(context, "onErrorOccurred").api(),
-        onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
+        onBeforeRequest: WebRequestEventManager(context, "onBeforeRequest"),
+        onBeforeSendHeaders: WebRequestEventManager(context, "onBeforeSendHeaders"),
+        onSendHeaders: WebRequestEventManager(context, "onSendHeaders"),
+        onHeadersReceived: WebRequestEventManager(context, "onHeadersReceived"),
+        onAuthRequired: WebRequestEventManager(context, "onAuthRequired"),
+        onBeforeRedirect: WebRequestEventManager(context, "onBeforeRedirect"),
+        onResponseStarted: WebRequestEventManager(context, "onResponseStarted"),
+        onErrorOccurred: WebRequestEventManager(context, "onErrorOccurred"),
+        onCompleted: WebRequestEventManager(context, "onCompleted"),
         handlerBehaviorChanged: function() {
           // TODO: Flush all caches.
         },
       },
     };
   }
 };
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js
@@ -60,21 +60,25 @@ add_task(async function test_post_unload
   await new Promise(resolve => setTimeout(resolve, 0));
 });
 
 
 add_task(async function test_post_unload_listeners() {
   let context = new StubContext();
 
   let fire;
-  let manager = new EventManager(context, "EventManager", _fire => {
-    fire = () => {
-      _fire.async();
-    };
-    return () => {};
+  let manager = new EventManager({
+    context,
+    name: "EventManager",
+    register: _fire => {
+      fire = () => {
+        _fire.async();
+      };
+      return () => {};
+    },
   });
 
   let fail = event => {
     ok(false, `Unexpected event: ${event}`);
   };
 
   // Check that event listeners isn't called after it has been removed.
   manager.addListener(fail);