author | Phil Ringnalda <philringnalda@gmail.com> |
Thu, 26 Jan 2017 19:13:11 -0800 | |
changeset 331299 | 0a15564434ca982325802dbfdbc92e28ebccd1e4 |
parent 331298 | c99f1f911660b4652132e394924401d525a77947 |
child 331300 | 4be138bdb7bf895d5d50f2884ff86c1a631c7a34 |
push id | 31265 |
push user | cbook@mozilla.com |
push date | Fri, 27 Jan 2017 09:41:20 +0000 |
treeherder | mozilla-central@dad46f412588 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
bugs | 1203330 |
milestone | 54.0a1 |
backs out | 2d42350d209a711e5378c1312ba4b173215f95eb 3a12c51c3eca5f1e5d563b0167a8723c5b24c537 31fac390e15db69d51aab3ea76764871a1a2a5ea |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/components/extensions/ext-bookmarks.js +++ b/browser/components/extensions/ext-bookmarks.js @@ -315,60 +315,56 @@ extensions.registerSchemaAPI("bookmarks" .catch(error => Promise.reject({message: error.message})); } catch (e) { return Promise.reject({message: `Invalid bookmark: ${JSON.stringify(info)}`}); } }, onCreated: new SingletonEventManager(context, "bookmarks.onCreated", fire => { let listener = (event, bookmark) => { - // Bug 1333889: make this asynchronous - fire.sync(bookmark.id, bookmark); + context.runSafe(fire, bookmark.id, bookmark); }; observer.on("created", listener); incrementListeners(); return () => { observer.off("created", listener); decrementListeners(); }; }).api(), onRemoved: new SingletonEventManager(context, "bookmarks.onRemoved", fire => { let listener = (event, data) => { - // Bug 1333889: make this asynchronous - fire.sync(data.guid, data.info); + context.runSafe(fire, data.guid, data.info); }; observer.on("removed", listener); incrementListeners(); return () => { observer.off("removed", listener); decrementListeners(); }; }).api(), onChanged: new SingletonEventManager(context, "bookmarks.onChanged", fire => { let listener = (event, data) => { - // Bug 1333889: make this asynchronous - fire.sync(data.guid, data.info); + context.runSafe(fire, data.guid, data.info); }; observer.on("changed", listener); incrementListeners(); return () => { observer.off("changed", listener); decrementListeners(); }; }).api(), onMoved: new SingletonEventManager(context, "bookmarks.onMoved", fire => { let listener = (event, data) => { - // Bug 1333889: make this asynchronous - fire.sync(data.guid, data.info); + context.runSafe(fire, data.guid, data.info); }; observer.on("moved", listener); incrementListeners(); return () => { observer.off("moved", listener); decrementListeners(); };
--- a/browser/components/extensions/ext-browserAction.js +++ b/browser/components/extensions/ext-browserAction.js @@ -13,17 +13,17 @@ XPCOMUtils.defineLazyServiceGetter(this, "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); Cu.import("resource://devtools/shared/event-emitter.js"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); Cu.import("resource://gre/modules/Task.jsm"); var { - SingletonEventManager, + EventManager, IconDetails, } = ExtensionUtils; const POPUP_PRELOAD_TIMEOUT_MS = 200; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; function isAncestorOrSelf(target, node) { @@ -441,20 +441,20 @@ extensions.on("shutdown", (type, extensi } }); /* eslint-enable mozilla/balanced-listeners */ extensions.registerSchemaAPI("browserAction", "addon_parent", context => { let {extension} = context; return { browserAction: { - onClicked: new SingletonEventManager(context, "browserAction.onClicked", fire => { + onClicked: new EventManager(context, "browserAction.onClicked", fire => { let listener = () => { let tab = TabManager.activeTab; - fire.async(TabManager.convert(extension, tab)); + fire(TabManager.convert(extension, tab)); }; BrowserAction.for(extension).on("click", listener); return () => { BrowserAction.for(extension).off("click", listener); }; }).api(), enable: function(tabId) {
--- a/browser/components/extensions/ext-c-omnibox.js +++ b/browser/components/extensions/ext-c-omnibox.js @@ -1,24 +1,25 @@ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { + runSafeSyncWithoutClone, SingletonEventManager, } = ExtensionUtils; extensions.registerSchemaAPI("omnibox", "addon_child", context => { return { omnibox: { onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => { let listener = (text, id) => { - fire.asyncWithoutClone(text, suggestions => { + runSafeSyncWithoutClone(fire, text, suggestions => { // TODO: Switch to using callParentFunctionNoReturn once bug 1314903 is fixed. context.childManager.callParentAsyncFunction("omnibox_internal.addSuggestions", [ id, suggestions, ]); }); }; context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
--- a/browser/components/extensions/ext-commands.js +++ b/browser/components/extensions/ext-commands.js @@ -1,17 +1,17 @@ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; Cu.import("resource://devtools/shared/event-emitter.js"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { - SingletonEventManager, + EventManager, PlatformInfo, } = ExtensionUtils; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; // WeakMap[Extension -> CommandList] var commandsMap = new WeakMap(); @@ -240,19 +240,19 @@ extensions.registerSchemaAPI("commands", return Promise.resolve(Array.from(commands, ([name, command]) => { return ({ name, description: command.description, shortcut: command.shortcut, }); })); }, - onCommand: new SingletonEventManager(context, "commands.onCommand", fire => { + onCommand: new EventManager(context, "commands.onCommand", fire => { let listener = (eventName, commandName) => { - fire.async(commandName); + fire(commandName); }; commandsMap.get(extension).on("command", listener); return () => { commandsMap.get(extension).off("command", listener); }; }).api(), }, };
--- a/browser/components/extensions/ext-contextMenus.js +++ b/browser/components/extensions/ext-contextMenus.js @@ -3,19 +3,19 @@ "use strict"; Cu.import("resource://gre/modules/ExtensionUtils.jsm"); Cu.import("resource://gre/modules/MatchPattern.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); var { + EventManager, ExtensionError, IconDetails, - SingletonEventManager, } = ExtensionUtils; const ACTION_MENU_TOP_LEVEL_LIMIT = 6; // Map[Extension -> Map[ID -> MenuItem]] // Note: we want to enumerate all the menu items so // this cannot be a weak map. var gContextMenuMap = new Map(); @@ -620,19 +620,19 @@ extensions.registerSchemaAPI("contextMen removeAll: function() { let root = gRootItems.get(extension); if (root) { root.remove(); } }, - onClicked: new SingletonEventManager(context, "contextMenus.onClicked", fire => { + onClicked: new EventManager(context, "contextMenus.onClicked", fire => { let listener = (event, info, tab) => { - fire.async(info, tab); + fire(info, tab); }; extension.on("webext-contextmenu-menuitem-click", listener); return () => { extension.off("webext-contextmenu-menuitem-click", listener); }; }).api(), },
--- a/browser/components/extensions/ext-history.js +++ b/browser/components/extensions/ext-history.js @@ -217,28 +217,28 @@ extensions.registerSchemaAPI("history", historyQuery.uri = NetUtil.newURI(url); let queryResult = PlacesUtils.history.executeQuery(historyQuery, options).root; let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem); return Promise.resolve(results); }, onVisited: new SingletonEventManager(context, "history.onVisited", fire => { let listener = (event, data) => { - fire.sync(data); + context.runSafe(fire, data); }; getObserver().on("visited", listener); return () => { getObserver().off("visited", listener); }; }).api(), onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => { let listener = (event, data) => { - fire.sync(data); + context.runSafe(fire, data); }; getObserver().on("visitRemoved", listener); return () => { getObserver().off("visitRemoved", listener); }; }).api(), },
--- a/browser/components/extensions/ext-omnibox.js +++ b/browser/components/extensions/ext-omnibox.js @@ -45,37 +45,37 @@ extensions.registerSchemaAPI("omnibox", ExtensionSearchHandler.setDefaultSuggestion(keyword, suggestion); } catch (e) { return Promise.reject(e.message); } }, onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => { let listener = (eventName) => { - fire.sync(); + fire(); }; extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener); return () => { extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener); }; }).api(), onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => { let listener = (eventName) => { - fire.sync(); + fire(); }; extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener); return () => { extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener); }; }).api(), onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => { let listener = (eventName, text, disposition) => { - fire.sync(text, disposition); + fire(text, disposition); }; extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener); return () => { extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener); }; }).api(), }, @@ -87,17 +87,17 @@ extensions.registerSchemaAPI("omnibox", } 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 SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => { let listener = (eventName, text, id) => { - fire.sync(text, id); + fire(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 @@ -1,16 +1,16 @@ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { - SingletonEventManager, + EventManager, IconDetails, } = ExtensionUtils; // WeakMap[Extension -> PageAction] var pageActionMap = new WeakMap(); // Handles URL bar icons, including the |page_action| manifest entry // and associated API. @@ -239,19 +239,19 @@ PageAction.for = extension => { }; global.pageActionFor = PageAction.for; extensions.registerSchemaAPI("pageAction", "addon_parent", context => { let {extension} = context; return { pageAction: { - onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => { + onClicked: new EventManager(context, "pageAction.onClicked", fire => { let listener = (evt, tab) => { - fire.async(TabManager.convert(extension, tab)); + fire(TabManager.convert(extension, tab)); }; let pageAction = PageAction.for(extension); pageAction.on("click", listener); return () => { pageAction.off("click", listener); }; }).api(),
--- a/browser/components/extensions/ext-sessions.js +++ b/browser/components/extensions/ext-sessions.js @@ -89,17 +89,17 @@ extensions.registerSchemaAPI("sessions", closedId = recentlyClosedTabs[0].closedId; session = SessionStore.undoCloseById(closedId); } return createSession(session, extension, closedId); }, onChanged: new SingletonEventManager(context, "sessions.onChanged", fire => { let observer = () => { - fire.async(); + context.runSafe(fire); }; Services.obs.addObserver(observer, SS_ON_CLOSED_OBJECTS_CHANGED, false); 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 @@ -13,17 +13,17 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", "resource://gre/modules/PromiseUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { - SingletonEventManager, + EventManager, ignoreEvent, } = ExtensionUtils; // This function is pretty tightly tied to Extension.jsm. // Its job is to fill in the |tab| property of the sender. function getSender(extension, target, sender) { let tabId; if ("tabId" in sender) { @@ -278,22 +278,22 @@ extensions.on("startup", () => { extensions.registerSchemaAPI("tabs", "addon_parent", context => { let {extension} = context; let self = { tabs: { onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => { let tab = event.originalTarget; let tabId = TabManager.getId(tab); let windowId = WindowManager.getId(tab.ownerGlobal); - fire.async({tabId, windowId}); + fire({tabId, windowId}); }).api(), - onCreated: new SingletonEventManager(context, "tabs.onCreated", fire => { + onCreated: new EventManager(context, "tabs.onCreated", fire => { let listener = (eventName, event) => { - fire.async(TabManager.convert(extension, event.tab)); + fire(TabManager.convert(extension, event.tab)); }; tabListener.on("tab-created", listener); return () => { tabListener.off("tab-created", listener); }; }).api(), @@ -302,55 +302,55 @@ extensions.registerSchemaAPI("tabs", "ad * 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 WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => { let tab = event.originalTarget; let tabIds = [TabManager.getId(tab)]; let windowId = WindowManager.getId(tab.ownerGlobal); - fire.async({tabIds, windowId}); + fire({tabIds, windowId}); }).api(), - onAttached: new SingletonEventManager(context, "tabs.onAttached", fire => { + onAttached: new EventManager(context, "tabs.onAttached", fire => { let listener = (eventName, event) => { - fire.async(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition}); + fire(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition}); }; tabListener.on("tab-attached", listener); return () => { tabListener.off("tab-attached", listener); }; }).api(), - onDetached: new SingletonEventManager(context, "tabs.onDetached", fire => { + onDetached: new EventManager(context, "tabs.onDetached", fire => { let listener = (eventName, event) => { - fire.async(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition}); + fire(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition}); }; tabListener.on("tab-detached", listener); return () => { tabListener.off("tab-detached", listener); }; }).api(), - onRemoved: new SingletonEventManager(context, "tabs.onRemoved", fire => { + onRemoved: new EventManager(context, "tabs.onRemoved", fire => { let listener = (eventName, event) => { - fire.async(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing}); + fire(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing}); }; tabListener.on("tab-removed", listener); return () => { tabListener.off("tab-removed", listener); }; }).api(), onReplaced: ignoreEvent(context, "tabs.onReplaced"), - onMoved: new SingletonEventManager(context, "tabs.onMoved", fire => { + 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. @@ -368,32 +368,32 @@ extensions.registerSchemaAPI("tabs", "ad let moveListener = event => { let tab = event.originalTarget; if (ignoreNextMove.has(tab)) { ignoreNextMove.delete(tab); return; } - fire.async(TabManager.getId(tab), { + fire(TabManager.getId(tab), { windowId: WindowManager.getId(tab.ownerGlobal), fromIndex: event.detail, toIndex: tab._tPos, }); }; AllWindowEvents.addListener("TabMove", moveListener); AllWindowEvents.addListener("TabOpen", openListener); return () => { AllWindowEvents.removeListener("TabMove", moveListener); AllWindowEvents.removeListener("TabOpen", openListener); }; }).api(), - onUpdated: new SingletonEventManager(context, "tabs.onUpdated", fire => { + onUpdated: new EventManager(context, "tabs.onUpdated", 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; @@ -405,17 +405,17 @@ extensions.registerSchemaAPI("tabs", "ad let fireForBrowser = (browser, changed) => { let [needed, changeInfo] = sanitize(extension, changed); if (needed) { let gBrowser = browser.ownerGlobal.gBrowser; let tabElem = gBrowser.getTabForBrowser(browser); let tab = TabManager.convert(extension, tabElem); - fire.async(tab.id, changeInfo, tab); + fire(tab.id, changeInfo, tab); } }; let listener = event => { let needed = []; if (event.type == "TabAttrModified") { let changed = event.detail.changed; if (changed.includes("image")) { @@ -442,17 +442,17 @@ extensions.registerSchemaAPI("tabs", "ad if (needed.length) { let tab = TabManager.convert(extension, event.originalTarget); let changeInfo = {}; for (let prop of needed) { changeInfo[prop] = tab[prop]; } - fire.async(tab.id, changeInfo, tab); + fire(tab.id, changeInfo, tab); } }; let progressListener = { onStateChange(browser, webProgress, request, stateFlags, statusCode) { if (!webProgress.isTopLevel) { return; } @@ -1020,17 +1020,17 @@ extensions.registerSchemaAPI("tabs", "ad let currentSettings = this._getZoomSettings(tab.id); if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) { return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`); } return Promise.resolve(); }, - onZoomChange: new SingletonEventManager(context, "tabs.onZoomChange", fire => { + onZoomChange: new EventManager(context, "tabs.onZoomChange", 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] @@ -1070,17 +1070,17 @@ extensions.registerSchemaAPI("tabs", "ad let oldZoomFactor = zoomLevels.get(browser); let newZoomFactor = getZoomLevel(browser); if (oldZoomFactor != newZoomFactor) { zoomLevels.set(browser, newZoomFactor); let tabId = TabManager.getId(tab); - fire.async({ + fire({ tabId, oldZoomFactor, newZoomFactor, zoomSettings: self.tabs._getZoomSettings(tabId), }); } };
--- a/browser/components/extensions/ext-utils.js +++ b/browser/components/extensions/ext-utils.js @@ -23,18 +23,18 @@ Cu.import("resource://gre/modules/Extens Cu.import("resource://gre/modules/AppConstants.jsm"); const POPUP_LOAD_TIMEOUT_MS = 200; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; var { DefaultWeakMap, + EventManager, promiseEvent, - SingletonEventManager, } = ExtensionUtils; // This file provides some useful code for the |tabs| and |windows| // modules. All of the code is installed on |global|, which is a scope // shared among the different ext-*.js scripts. global.makeWidgetId = id => { id = id.toLowerCase(); @@ -1276,21 +1276,21 @@ global.AllWindowEvents = { this.addWindowListener(window, eventType, listener); } } }, }; AllWindowEvents.openListener = AllWindowEvents.openListener.bind(AllWindowEvents); -// Subclass of SingletonEventManager where we just need to call +// Subclass of EventManager where we just need to call // add/removeEventListener on each XUL window. -global.WindowEventManager = class extends SingletonEventManager { - constructor(context, name, event, listener) { - super(context, name, fire => { - let listener2 = (...args) => listener(fire, ...args); - AllWindowEvents.addListener(event, listener2); - return () => { - AllWindowEvents.removeListener(event, listener2); - }; - }); - } +global.WindowEventManager = function(context, name, event, listener) { + EventManager.call(this, context, name, fire => { + let listener2 = (...args) => listener(fire, ...args); + AllWindowEvents.addListener(event, listener2); + return () => { + AllWindowEvents.removeListener(event, listener2); + }; + }); }; + +WindowEventManager.prototype = Object.create(EventManager.prototype);
--- a/browser/components/extensions/ext-windows.js +++ b/browser/components/extensions/ext-windows.js @@ -7,50 +7,50 @@ XPCOMUtils.defineLazyServiceGetter(this, "nsIAboutNewTabService"); XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { - SingletonEventManager, + EventManager, promiseObserved, } = ExtensionUtils; function onXULFrameLoaderCreated({target}) { target.messageManager.sendAsyncMessage("AllowScriptsToClose", {}); } extensions.registerSchemaAPI("windows", "addon_parent", context => { let {extension} = context; return { windows: { onCreated: new WindowEventManager(context, "windows.onCreated", "domwindowopened", (fire, window) => { - fire.async(WindowManager.convert(extension, window)); + fire(WindowManager.convert(extension, window)); }).api(), onRemoved: new WindowEventManager(context, "windows.onRemoved", "domwindowclosed", (fire, window) => { - fire.async(WindowManager.getId(window)); + fire(WindowManager.getId(window)); }).api(), - onFocusChanged: new SingletonEventManager(context, "windows.onFocusChanged", fire => { + onFocusChanged: new EventManager(context, "windows.onFocusChanged", 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 ? WindowManager.getId(window) : WindowManager.WINDOW_ID_NONE; if (windowId !== lastOnFocusChangedWindowId) { - fire.async(windowId); + fire(windowId); lastOnFocusChangedWindowId = windowId; } }); }; AllWindowEvents.addListener("focus", listener); AllWindowEvents.addListener("blur", listener); return () => { AllWindowEvents.removeListener("focus", listener);
--- a/mobile/android/components/extensions/ext-pageAction.js +++ b/mobile/android/components/extensions/ext-pageAction.js @@ -127,17 +127,17 @@ extensions.on("shutdown", (type, extensi /* eslint-enable mozilla/balanced-listeners */ extensions.registerSchemaAPI("pageAction", "addon_parent", context => { let {extension} = context; return { pageAction: { onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => { let listener = (event) => { - fire.async(); + fire(); }; pageActionMap.get(extension).on("click", listener); return () => { pageActionMap.get(extension).off("click", listener); }; }).api(), show(tabId) {
--- a/toolkit/components/extensions/ExtensionChild.jsm +++ b/toolkit/components/extensions/ExtensionChild.jsm @@ -36,16 +36,17 @@ XPCOMUtils.defineLazyModuleGetter(this, const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon"; const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools"; Cu.import("resource://gre/modules/ExtensionCommon.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); const { DefaultMap, + EventManager, SingletonEventManager, SpreadArgs, defineLazyGetter, getInnerWindowID, getMessageManager, getUniqueId, injectAPI, promiseEvent, @@ -113,27 +114,27 @@ class Port { disconnect: () => { this.disconnect(); }, postMessage: json => { this.postMessage(json); }, - onDisconnect: new SingletonEventManager(this.context, "Port.onDisconnect", fire => { + onDisconnect: new EventManager(this.context, "Port.onDisconnect", fire => { return this.registerOnDisconnect(error => { portError = error && this.context.normalizeError(error); - fire.asyncWithoutClone(portObj); + fire.withoutClone(portObj); }); }).api(), - onMessage: new SingletonEventManager(this.context, "Port.onMessage", fire => { + onMessage: new EventManager(this.context, "Port.onMessage", fire => { return this.registerOnMessage(msg => { msg = Cu.cloneInto(msg, this.context.cloneScope); - fire.asyncWithoutClone(msg, portObj); + fire.withoutClone(msg, portObj); }); }).api(), get error() { return portError; }, }; @@ -324,17 +325,17 @@ class Messenger { } sendNativeMessage(messageManager, msg, recipient, responseCallback) { msg = NativeApp.encodeMessage(this.context, msg); return this.sendMessage(messageManager, msg, recipient, responseCallback); } onMessage(name) { - return new SingletonEventManager(this.context, name, fire => { + return new SingletonEventManager(this.context, name, callback => { let listener = { messageFilterPermissive: this.optionalFilter, messageFilterStrict: this.filter, filterMessage: (sender, recipient) => { // Ignore the message if it was sent by this Messenger. return sender.contextId !== this.context.contextId; }, @@ -354,17 +355,17 @@ class Messenger { }); message = Cu.cloneInto(message, 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); + let result = callback(message, sender, sendResponse); if (result instanceof this.context.cloneScope.Promise) { return result; } else if (result === true) { return promise; } return response; }, }; @@ -406,17 +407,17 @@ 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) { - return new SingletonEventManager(this.context, name, fire => { + return new SingletonEventManager(this.context, name, callback => { let listener = { messageFilterPermissive: this.optionalFilter, messageFilterStrict: this.filter, filterMessage: (sender, recipient) => { // Ignore the port if it was created by this Messenger. return sender.contextId !== this.context.contextId; }, @@ -425,17 +426,17 @@ class Messenger { 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()); + this.context.runSafeWithoutClone(callback, port.api()); return true; }, }; MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener); return () => { MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener); };
--- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -575,104 +575,157 @@ LocaleData.prototype = { // Return the browser locale, but convert it to a Chrome-style // locale code. return Locale.getLocale().replace(/-/g, "_"); }, }; // This is a generic class for managing event listeners. Example usage: // -// new SingletonEventManager(context, "api.subAPI", fire => { +// new EventManager(context, "api.subAPI", fire => { // let listener = (...) => { // // Fire any listeners registered with addListener. -// fire.async(arg1, arg2); +// fire(arg1, arg2); // }; // // Register the listener. // SomehowRegisterListener(listener); // return () => { // // Return a way to unregister the listener. // SomehowUnregisterListener(listener); // }; // }).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 +// to register the listener. |register| is only called once, even if +// multiple listeners are registered. |register| should return an // unregister function that will unregister the listener. +function EventManager(context, name, register) { + this.context = context; + this.name = name; + this.register = register; + this.unregister = null; + this.callbacks = new Set(); +} + +EventManager.prototype = { + addListener(callback) { + if (typeof(callback) != "function") { + dump(`Expected function\n${Error().stack}`); + return; + } + if (this.context.unloaded) { + dump(`Cannot add listener to ${this.name} after context unloaded`); + return; + } + + if (!this.callbacks.size) { + this.context.callOnClose(this); + + let fireFunc = this.fire.bind(this); + let fireWithoutClone = this.fireWithoutClone.bind(this); + fireFunc.withoutClone = fireWithoutClone; + this.unregister = this.register(fireFunc); + } + this.callbacks.add(callback); + }, + + removeListener(callback) { + if (!this.callbacks.size) { + return; + } + + this.callbacks.delete(callback); + if (this.callbacks.size == 0) { + this.unregister(); + this.unregister = null; + + this.context.forgetOnClose(this); + } + }, + + hasListener(callback) { + return this.callbacks.has(callback); + }, + + fire(...args) { + this._fireCommon("runSafe", args); + }, + + fireWithoutClone(...args) { + this._fireCommon("runSafeWithoutClone", args); + }, + + _fireCommon(runSafeMethod, args) { + for (let callback of this.callbacks) { + Promise.resolve(callback).then(callback => { + if (this.context.unloaded) { + dump(`${this.name} event fired after context unloaded.\n`); + } else if (!this.context.active) { + dump(`${this.name} event fired while context is inactive.\n`); + } else if (this.callbacks.has(callback)) { + this.context[runSafeMethod](callback, ...args); + } + }); + } + }, + + close() { + if (this.callbacks.size) { + this.unregister(); + } + this.callbacks.clear(); + this.register = null; + this.unregister = null; + }, + + api() { + return { + addListener: callback => this.addListener(callback), + removeListener: callback => this.removeListener(callback), + hasListener: callback => this.hasListener(callback), + }; + }, +}; + +// Similar to EventManager, but it doesn't try to consolidate event +// notifications. Each addListener call causes us to register once. It +// allows extra arguments to be passed to addListener. function SingletonEventManager(context, name, register) { this.context = context; this.name = name; this.register = register; this.unregister = new Map(); } SingletonEventManager.prototype = { addListener(callback, ...args) { - if (this.unregister.has(callback)) { - return; - } - - let shouldFire = () => { + let wrappedCallback = (...args) => { if (this.context.unloaded) { dump(`${this.name} event fired after context unloaded.\n`); - } else if (!this.context.active) { - dump(`${this.name} event fired while context is inactive.\n`); } else if (this.unregister.has(callback)) { - return true; + return callback(...args); } - return false; }; - let fire = { - sync: (...args) => { - if (shouldFire()) { - return this.context.runSafe(callback, ...args); - } - }, - async: (...args) => { - return Promise.resolve().then(() => { - if (shouldFire()) { - return this.context.runSafe(callback, ...args); - } - }); - }, - raw: (...args) => { - if (!shouldFire()) { - throw new Error("Called raw() on unloaded/inactive context"); - } - return callback(...args); - }, - asyncWithoutClone: (...args) => { - return Promise.resolve().then(() => { - if (shouldFire()) { - return this.context.runSafeWithoutClone(callback, ...args); - } - }); - }, - }; - - - let unregister = this.register(fire, ...args); + let unregister = this.register(wrappedCallback, ...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); unregister(); - if (this.unregister.size == 0) { - this.context.forgetOnClose(this); - } }, hasListener(callback) { return this.unregister.has(callback); }, close() { for (let unregister of this.unregister.values()) { @@ -1145,16 +1198,17 @@ this.ExtensionUtils = { runSafe, runSafeSync, runSafeSyncWithoutClone, runSafeWithoutClone, stylesheetMap, DefaultMap, DefaultWeakMap, EventEmitter, + EventManager, ExtensionError, IconDetails, LocaleData, MessageManagerProxy, PlatformInfo, SingletonEventManager, SpreadArgs, };
--- a/toolkit/components/extensions/ext-alarms.js +++ b/toolkit/components/extensions/ext-alarms.js @@ -1,15 +1,15 @@ "use strict"; var {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { - SingletonEventManager, + EventManager, } = ExtensionUtils; // WeakMap[Extension -> Map[name -> Alarm]] var alarmsMap = new WeakMap(); // WeakMap[Extension -> Set[callback]] var alarmCallbacksMap = new WeakMap(); @@ -135,19 +135,19 @@ extensions.registerSchemaAPI("alarms", " let cleared = false; for (let alarm of alarmsMap.get(extension).values()) { alarm.clear(); cleared = true; } return Promise.resolve(cleared); }, - onAlarm: new SingletonEventManager(context, "alarms.onAlarm", fire => { + onAlarm: new EventManager(context, "alarms.onAlarm", fire => { let callback = alarm => { - fire.sync(alarm.data); + fire(alarm.data); }; alarmCallbacksMap.get(extension).add(callback); return () => { alarmCallbacksMap.get(extension).delete(callback); }; }).api(), },
--- a/toolkit/components/extensions/ext-c-test.js +++ b/toolkit/components/extensions/ext-c-test.js @@ -166,17 +166,17 @@ function makeTestAPI(context) { assertTrue(errorMatches(error, expectedError, context), `Function threw, expecting error to match ${toSource(expectedError)}` + `got ${errorMessage}${msg}`); } }, onMessage: new SingletonEventManager(context, "test.onMessage", fire => { let handler = (event, ...args) => { - fire.async(...args); + context.runSafe(fire, ...args); }; extension.on("test-harness-message", handler); return () => { extension.off("test-harness-message", handler); }; }).api(), },
--- a/toolkit/components/extensions/ext-cookies.js +++ b/toolkit/components/extensions/ext-cookies.js @@ -4,17 +4,17 @@ const {interfaces: Ci, utils: Cu} = Comp Cu.import("resource://gre/modules/ExtensionUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", "resource://gre/modules/ContextualIdentityService.jsm"); var { - SingletonEventManager, + EventManager, } = ExtensionUtils; var DEFAULT_STORE = "firefox-default"; var PRIVATE_STORE = "firefox-private"; var CONTAINER_STORE = "firefox-container-"; global.getCookieStoreIdForTab = function(data, tab) { if (data.incognito) { @@ -433,23 +433,23 @@ extensions.registerSchemaAPI("cookies", let result = []; for (let key in data) { result.push({id: key, tabIds: data[key], incognito: key == PRIVATE_STORE}); } return Promise.resolve(result); }, - onChanged: new SingletonEventManager(context, "cookies.onChanged", fire => { + onChanged: new EventManager(context, "cookies.onChanged", fire => { let observer = (subject, topic, data) => { let notify = (removed, cookie, cause) => { cookie.QueryInterface(Ci.nsICookie2); if (extension.whiteListedHosts.matchesCookie(cookie)) { - fire.async({removed, cookie: convert({cookie, isPrivate: topic == "private-cookie-changed"}), cause}); + fire({removed, cookie: convert({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;
--- a/toolkit/components/extensions/ext-downloads.js +++ b/toolkit/components/extensions/ext-downloads.js @@ -16,16 +16,17 @@ XPCOMUtils.defineLazyModuleGetter(this, "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", "resource://devtools/shared/event-emitter.js"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); const { ignoreEvent, normalizeTime, + runSafeSync, SingletonEventManager, PlatformInfo, } = ExtensionUtils; const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito", "danger", "mime", "startTime", "endTime", "estimatedEndTime", "state", "paused", "canResume", "error", @@ -745,47 +746,47 @@ extensions.registerSchemaAPI("downloads" changes[fld] = { previous: noundef(item.prechange[fld]), current: noundef(item[fld]), }; } }); if (Object.keys(changes).length > 0) { changes.id = item.id; - fire.async(changes); + runSafeSync(context, fire, changes); } }; let registerPromise = DownloadMap.getDownloadList().then(() => { DownloadMap.on("change", handler); }); return () => { registerPromise.then(() => { DownloadMap.off("change", handler); }); }; }).api(), onCreated: new SingletonEventManager(context, "downloads.onCreated", fire => { const handler = (what, item) => { - fire.async(item.serialize()); + runSafeSync(context, fire, item.serialize()); }; let registerPromise = DownloadMap.getDownloadList().then(() => { DownloadMap.on("create", handler); }); return () => { registerPromise.then(() => { DownloadMap.off("create", handler); }); }; }).api(), onErased: new SingletonEventManager(context, "downloads.onErased", fire => { const handler = (what, item) => { - fire.async(item.id); + runSafeSync(context, fire, item.id); }; let registerPromise = DownloadMap.getDownloadList().then(() => { DownloadMap.on("erase", handler); }); return () => { registerPromise.then(() => { DownloadMap.off("erase", handler); });
--- a/toolkit/components/extensions/ext-idle.js +++ b/toolkit/components/extensions/ext-idle.js @@ -76,17 +76,17 @@ extensions.registerSchemaAPI("idle", "ad } return Promise.resolve("idle"); }, setDetectionInterval: function(detectionIntervalInSeconds) { setDetectionInterval(extension, context, detectionIntervalInSeconds); }, onStateChanged: new SingletonEventManager(context, "idle.onStateChanged", fire => { let listener = (event, data) => { - fire.sync(data); + context.runSafe(fire, data); }; getObserver(extension, context).on("stateChanged", listener); return () => { getObserver(extension, context).off("stateChanged", listener); }; }).api(), },
--- a/toolkit/components/extensions/ext-notifications.js +++ b/toolkit/components/extensions/ext-notifications.js @@ -3,17 +3,17 @@ var {classes: Cc, interfaces: Ci, utils: Cu} = Components; Cu.import("resource://gre/modules/ExtensionUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", "resource://devtools/shared/event-emitter.js"); var { - SingletonEventManager, + EventManager, ignoreEvent, } = ExtensionUtils; // WeakMap[Extension -> Map[id -> Notification]] var notificationsMap = new WeakMap(); // Manages a notification popup (notifications API) created by the extension. function Notification(extension, id, options) { @@ -126,31 +126,31 @@ extensions.registerSchemaAPI("notificati getAll: function() { let result = {}; notificationsMap.get(extension).forEach((value, key) => { result[key] = value.options; }); return Promise.resolve(result); }, - onClosed: new SingletonEventManager(context, "notifications.onClosed", fire => { + onClosed: new EventManager(context, "notifications.onClosed", fire => { let listener = (event, notificationId) => { // FIXME: Support the byUser argument. - fire.async(notificationId, true); + fire(notificationId, true); }; notificationsMap.get(extension).on("closed", listener); return () => { notificationsMap.get(extension).off("closed", listener); }; }).api(), - onClicked: new SingletonEventManager(context, "notifications.onClicked", fire => { + onClicked: new EventManager(context, "notifications.onClicked", fire => { let listener = (event, notificationId) => { - fire.async(notificationId, true); + fire(notificationId, true); }; notificationsMap.get(extension).on("clicked", listener); return () => { notificationsMap.get(extension).off("clicked", listener); }; }).api(),
--- a/toolkit/components/extensions/ext-runtime.js +++ b/toolkit/components/extensions/ext-runtime.js @@ -23,55 +23,55 @@ extensions.registerSchemaAPI("runtime", runtime: { onStartup: new SingletonEventManager(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.async(); + fire(); } }; extension.on("startup", listener); return () => { extension.off("startup", listener); }; }).api(), onInstalled: new SingletonEventManager(context, "runtime.onInstalled", fire => { let listener = () => { switch (extension.startupReason) { case "APP_STARTUP": if (Extension.browserUpdated) { - fire.async({reason: "browser_update"}); + fire({reason: "browser_update"}); } break; case "ADDON_INSTALL": - fire.async({reason: "install"}); + fire({reason: "install"}); break; case "ADDON_UPGRADE": - fire.async({reason: "update"}); + fire({reason: "update"}); break; } }; extension.on("startup", listener); return () => { extension.off("startup", listener); }; }).api(), onUpdateAvailable: new SingletonEventManager(context, "runtime.onUpdateAvailable", fire => { let instanceID = extension.addonData.instanceID; AddonManager.addUpgradeListener(instanceID, upgrade => { extension.upgrade = upgrade; let details = { version: upgrade.version, }; - fire.async(details); + context.runSafe(fire, details); }); return () => { AddonManager.removeUpgradeListener(instanceID); }; }).api(), reload: () => { if (extension.upgrade) {
--- a/toolkit/components/extensions/ext-storage.js +++ b/toolkit/components/extensions/ext-storage.js @@ -6,18 +6,18 @@ XPCOMUtils.defineLazyModuleGetter(this, "resource://gre/modules/ExtensionStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorageSync", "resource://gre/modules/ExtensionStorageSync.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { + EventManager, ExtensionError, - SingletonEventManager, } = ExtensionUtils; function enforceNoTemporaryAddon(extensionId) { const EXCEPTION_MESSAGE = "The storage API will not work with a temporary addon ID. " + "Please add an explicit addon ID to your manifest. " + "For more information see https://bugzil.la/1323228."; if (AddonManagerPrivate.isTemporaryInstallID(extensionId)) { @@ -58,22 +58,22 @@ function storageApiFactory(context) { return ExtensionStorageSync.remove(extension, keys, context); }, clear: function() { enforceNoTemporaryAddon(extension.id); return ExtensionStorageSync.clear(extension, context); }, }, - onChanged: new SingletonEventManager(context, "storage.onChanged", fire => { + onChanged: new EventManager(context, "storage.onChanged", fire => { let listenerLocal = changes => { - fire.async(changes, "local"); + fire(changes, "local"); }; let listenerSync = changes => { - fire.async(changes, "sync"); + fire(changes, "sync"); }; ExtensionStorage.addOnChangedListener(extension.id, listenerLocal); ExtensionStorageSync.addOnChangedListener(extension, listenerSync, context); return () => { ExtensionStorage.removeOnChangedListener(extension.id, listenerLocal); ExtensionStorageSync.removeOnChangedListener(extension, listenerSync); };
--- a/toolkit/components/extensions/ext-webNavigation.js +++ b/toolkit/components/extensions/ext-webNavigation.js @@ -93,17 +93,17 @@ function fillTransitionProperties(eventN dst.transitionType = transitionType; dst.transitionQualifiers = transitionQualifiers; } } // Similar to WebRequestEventManager but for WebNavigation. function WebNavigationEventManager(context, eventName) { let name = `webNavigation.${eventName}`; - let register = (fire, urlFilters) => { + let register = (callback, 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; } @@ -122,17 +122,17 @@ function WebNavigationEventManager(conte // Fills in tabId typically. extensions.emit("fill-browser-data", data.browser, data2); if (data2.tabId < 0) { return; } fillTransitionProperties(eventName, data, data2); - fire.async(data2); + context.runSafe(callback, data2); }; WebNavigation[eventName].addListener(listener, filters); return () => { WebNavigation[eventName].removeListener(listener); }; };
--- a/toolkit/components/extensions/ext-webRequest.js +++ b/toolkit/components/extensions/ext-webRequest.js @@ -15,17 +15,17 @@ var { SingletonEventManager, } = ExtensionUtils; // EventManager-like class specifically for WebRequest. Inherits from // SingletonEventManager. Takes care of converting |details| parameter // when invoking listeners. function WebRequestEventManager(context, eventName) { let name = `webRequest.${eventName}`; - let register = (fire, filter, info) => { + let register = (callback, filter, info) => { let listener = data => { // Prevent listening in on requests originating from system principal to // prevent tinkering with OCSP, app and addon updates, etc. if (data.isSystemPrincipal) { return; } let browserData = {}; extensions.emit("fill-browser-data", data.browser, browserData); @@ -60,17 +60,17 @@ function WebRequestEventManager(context, let optional = ["requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl", "requestBody"]; for (let opt of optional) { if (opt in data) { data2[opt] = data[opt]; } } - return fire.sync(data2); + return context.runSafe(callback, data2); }; let filter2 = {}; filter2.urls = new MatchPattern(filter.urls); if (filter.types) { filter2.types = filter.types; } if (filter.tabId) {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_alarms.js @@ -47,23 +47,20 @@ add_task(function* test_alarm_fires() { background: `(${backgroundScript})()`, manifest: { permissions: ["alarms"], }, }); yield extension.startup(); yield extension.awaitFinish("alarm-fires"); - - // Defer unloading the extension so the asynchronous event listener - // reply finishes. - yield new Promise(resolve => setTimeout(resolve, 0)); yield extension.unload(); }); + add_task(function* test_alarm_fires_with_when() { function backgroundScript() { let ALARM_NAME = "test_ext_alarms"; let timer; browser.alarms.onAlarm.addListener(alarm => { browser.test.assertEq(ALARM_NAME, alarm.name, "alarm has the expected name"); clearTimeout(timer); @@ -84,23 +81,20 @@ add_task(function* test_alarm_fires_with background: `(${backgroundScript})()`, manifest: { permissions: ["alarms"], }, }); yield extension.startup(); yield extension.awaitFinish("alarm-when"); - - // Defer unloading the extension so the asynchronous event listener - // reply finishes. - yield new Promise(resolve => setTimeout(resolve, 0)); yield extension.unload(); }); + add_task(function* test_alarm_clear_non_matching_name() { async function backgroundScript() { let ALARM_NAME = "test_ext_alarms"; browser.alarms.create(ALARM_NAME, {when: Date.now() + 2000}); let wasCleared = await browser.alarms.clear(ALARM_NAME + "1"); browser.test.assertFalse(wasCleared, "alarm was not cleared"); @@ -117,16 +111,17 @@ add_task(function* test_alarm_clear_non_ }, }); yield extension.startup(); yield extension.awaitFinish("alarm-clear"); yield extension.unload(); }); + add_task(function* test_alarm_get_and_clear_single_argument() { async function backgroundScript() { browser.alarms.create({when: Date.now() + 2000}); let alarm = await browser.alarms.get(); browser.test.assertEq("", alarm.name, "expected alarm returned"); let wasCleared = await browser.alarms.clear();
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts.js @@ -7,16 +7,17 @@ Cu.import("resource://gre/modules/Timer. Cu.import("resource://gre/modules/ExtensionCommon.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); var { BaseContext, } = ExtensionCommon; var { + EventManager, SingletonEventManager, } = ExtensionUtils; class StubContext extends BaseContext { constructor() { let fakeExtension = {id: "test@web.extension"}; super("testEnv", fakeExtension); this.sandbox = Cu.Sandbox(global); @@ -59,54 +60,70 @@ add_task(function* test_post_unload_prom // any micro-tasks that get enqueued by the resolution handlers above. yield new Promise(resolve => setTimeout(resolve, 0)); }); add_task(function* test_post_unload_listeners() { let context = new StubContext(); + let fireEvent; + let onEvent = new EventManager(context, "onEvent", fire => { + fireEvent = fire; + return () => {}; + }); + let fireSingleton; - let onSingleton = new SingletonEventManager(context, "onSingleton", fire => { + let onSingleton = new SingletonEventManager(context, "onSingleton", callback => { fireSingleton = () => { - fire.async(); + Promise.resolve().then(callback); }; return () => {}; }); let fail = event => { ok(false, `Unexpected event: ${event}`); }; - // Check that event listeners isn't called after it has been removed. + // Check that event listeners aren't called after they've been removed. + onEvent.addListener(fail); onSingleton.addListener(fail); - let promise = new Promise(resolve => onSingleton.addListener(resolve)); + let promises = [ + new Promise(resolve => onEvent.addListener(resolve)), + new Promise(resolve => onSingleton.addListener(resolve)), + ]; + fireEvent("onEvent"); fireSingleton("onSingleton"); - // The `fireSingleton` call ia dispatched asynchronously, so it won't - // have fired by this point. The `fail` listener that we remove now - // should not be called, even though the event has already been + // Both `fireEvent` calls are dispatched asynchronously, so they won't + // have fired by this point. The `fail` listeners that we remove now + // should not be called, even though the events have already been // enqueued. + onEvent.removeListener(fail); onSingleton.removeListener(fail); - // Wait for the remaining listener to be called, which should always - // happen after the `fail` listener would normally be called. - yield promise; + // Wait for the remaining listeners to be called, which should always + // happen after the `fail` listeners would normally be called. + yield Promise.all(promises); - // Check that the event listener isn't called after the context has + // Check that event listeners aren't called after the context has // unloaded. + onEvent.addListener(fail); onSingleton.addListener(fail); - // The `fire` callback always dispatches events + // The EventManager `fire` callback always dispatches events // asynchronously, so we need to test that any pending event callbacks // aren't fired after the context unloads. We also need to test that // any `fire` calls that happen *after* the context is unloaded also // do not trigger callbacks. + fireEvent("onEvent"); + Promise.resolve("onEvent").then(fireEvent); + fireSingleton("onSingleton"); Promise.resolve("onSingleton").then(fireSingleton); context.unload(); // The `setTimeout` ensures that we return to the event loop after // promise resolution, which means we're guaranteed to return after // any micro-tasks that get enqueued by the resolution handlers above.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_idle.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_idle.js @@ -140,19 +140,16 @@ add_task(function* testSetDetectionInter }); idleService._reset(); yield extension.startup(); yield extension.awaitMessage("listenerAdded"); idleService._fireObservers("idle"); yield extension.awaitMessage("listenerFired"); checkActivity({expectedAdd: [99], expectedRemove: [], expectedFires: ["idle"]}); - // Defer unloading the extension so the asynchronous event listener - // reply finishes. - yield new Promise(resolve => setTimeout(resolve, 0)); yield extension.unload(); }); add_task(function* testSetDetectionIntervalAfterAddingListener() { function background() { browser.idle.onStateChanged.addListener(newState => { browser.test.assertEq("idle", newState, "listener fired with the expected state"); browser.test.sendMessage("listenerFired"); @@ -169,20 +166,16 @@ add_task(function* testSetDetectionInter }); idleService._reset(); yield extension.startup(); yield extension.awaitMessage("detectionIntervalSet"); idleService._fireObservers("idle"); yield extension.awaitMessage("listenerFired"); checkActivity({expectedAdd: [60, 99], expectedRemove: [60], expectedFires: ["idle"]}); - - // Defer unloading the extension so the asynchronous event listener - // reply finishes. - yield new Promise(resolve => setTimeout(resolve, 0)); yield extension.unload(); }); add_task(function* testOnlyAddingListener() { function background() { browser.idle.onStateChanged.addListener(newState => { browser.test.assertEq("active", newState, "listener fired with the expected state"); browser.test.sendMessage("listenerFired"); @@ -200,14 +193,10 @@ add_task(function* testOnlyAddingListene idleService._reset(); yield extension.startup(); yield extension.awaitMessage("listenerAdded"); idleService._fireObservers("active"); yield extension.awaitMessage("listenerFired"); // check that "idle-daily" topic does not cause a listener to fire idleService._fireObservers("idle-daily"); checkActivity({expectedAdd: [60], expectedRemove: [], expectedFires: ["active", "idle-daily"]}); - - // Defer unloading the extension so the asynchronous event listener - // reply finishes. - yield new Promise(resolve => setTimeout(resolve, 0)); yield extension.unload(); });