/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * This module provides wrappers around standard message managers to * simplify bidirectional communication. It currently allows a caller to * send a message to a single listener, and receive a reply. If there * are no matching listeners, or the message manager disconnects before * a reply is received, the caller is returned an error. * * The listener end may specify filters for the messages it wishes to * receive, and the sender end likewise may specify recipient tags to * match the filters. * * The message handler on the listener side may return its response * value directly, or may return a promise, the resolution or rejection * of which will be returned instead. The sender end likewise receives a * promise which resolves or rejects to the listener's response. * * * A basic setup works something like this: * * A content script adds a message listener to its global * ContentFrameMessageManager, with an appropriate set of filters: * * { * init(messageManager, window, extensionID) { * this.window = window; * * MessageChannel.addListener( * messageManager, "ContentScript:TouchContent", * this); * * this.messageFilterStrict = { * innerWindowID: getInnerWindowID(window), * extensionID: extensionID, * }; * * this.messageFilterPermissive = { * outerWindowID: getOuterWindowID(window), * }; * }, * * receiveMessage({ target, messageName, sender, recipient, data }) { * if (messageName == "ContentScript:TouchContent") { * return new Promise(resolve => { * this.touchWindow(data.touchWith, result => { * resolve({ touchResult: result }); * }); * }); * } * }, * }; * * A script in the parent process sends a message to the content process * via a tab message manager, including recipient tags to match its * filter, and an optional sender tag to identify itself: * * let data = { touchWith: "pencil" }; * let sender = { extensionID, contextID }; * let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID }; * * MessageChannel.sendMessage( * tab.linkedBrowser.messageManager, "ContentScript:TouchContent", * data, {recipient, sender} * ).then(result => { * alert(result.touchResult); * }); * * Since the lifetimes of message senders and receivers may not always * match, either side of the message channel may cancel pending * responses which match its sender or recipient tags. * * For the above client, this might be done from an * inner-window-destroyed observer, when its target scope is destroyed: * * observe(subject, topic, data) { * if (topic == "inner-window-destroyed") { * let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; * * MessageChannel.abortResponses({ innerWindowID }); * } * }, * * From the parent, it may be done when its context is being destroyed: * * onDestroy() { * MessageChannel.abortResponses({ * extensionID: this.extensionID, * contextID: this.contextID, * }); * }, * */ export let MessageChannel; import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.sys.mjs", }); function getMessageManager(target) { if (typeof target.sendAsyncMessage === "function") { return target; } return new lazy.MessageManagerProxy(target); } function matches(target, messageManager) { return target === messageManager || target.messageManager === messageManager; } const { DEBUG } = AppConstants; // Idle callback timeout for low-priority message dispatch. const LOW_PRIORITY_TIMEOUT_MS = 250; const MESSAGE_MESSAGES = "MessageChannel:Messages"; const MESSAGE_RESPONSE = "MessageChannel:Response"; // ESLint can't tell that these are referenced, so tell it that they're // exported to make it happy. /* exported _deferredResult, _makeDeferred */ var _deferredResult; var _makeDeferred = (resolve, reject) => { // We use arrow functions here and refer to the outer variables via // `this`, to avoid a lexical name lookup. Yes, it makes a difference. // No, I don't like it any more than you do. _deferredResult.resolve = resolve; _deferredResult.reject = reject; }; /** * Helper to create a new Promise without allocating any closures to * receive its resolution functions. * * I know what you're thinking: "This is crazy. There is no possible way * this can be necessary. Just use the ordinary Promise constructor the * way it was meant to be used, you lunatic." * * And, against all odds, it turns out that you're wrong. Creating * lambdas to receive promise resolution functions consistently turns * out to be one of the most expensive parts of message dispatch in this * code. * * So we do the stupid micro-optimization, and try to live with * ourselves for it. * * (See also bug 1404950.) * * @returns {object} */ let Deferred = () => { let res = {}; _deferredResult = res; res.promise = new Promise(_makeDeferred); _deferredResult = null; return res; }; /** * Handles the mapping and dispatching of messages to their registered * handlers. There is one broker per message manager and class of * messages. Each class of messages is mapped to one native message * name, e.g., "MessageChannel:Message", and is dispatched to handlers * based on an internal message name, e.g., "Extension:ExecuteScript". */ class FilteringMessageManager { /** * @param {string} messageName * The name of the native message this broker listens for. * @param {Function} callback * A function which is called for each message after it has been * mapped to its handler. The function receives two arguments: * * result: * An object containing either a `handler` or an `error` property. * If no error occurs, `handler` will be a matching handler that * was registered by `addHandler`. Otherwise, the `error` property * will contain an object describing the error. * * data: * An object describing the message, as defined in * `MessageChannel.addListener`. * @param {nsIMessageListenerManager} messageManager */ constructor(messageName, callback, messageManager) { this.messageName = messageName; this.callback = callback; this.messageManager = messageManager; this.messageManager.addMessageListener(this.messageName, this, true); this.handlers = new Map(); } /** * Receives a set of messages from our message manager, maps each to a * handler, and passes the results to our message callbacks. */ receiveMessage({ data, target }) { data.forEach(msg => { if (msg) { let handlers = Array.from( this.getHandlers(msg.messageName, msg.sender || null, msg.recipient) ); msg.target = target; this.callback(handlers, msg); } }); } /** * Iterates over all handlers for the given message name. If `recipient` * is provided, only iterates over handlers whose filters match it. * * @param {string|number} messageName * The message for which to return handlers. * @param {object} sender * The sender data on which to filter handlers. * @param {object} recipient * The recipient data on which to filter handlers. */ *getHandlers(messageName, sender, recipient) { let handlers = this.handlers.get(messageName) || new Set(); for (let handler of handlers) { if ( MessageChannel.matchesFilter( handler.messageFilterStrict || null, recipient ) && MessageChannel.matchesFilter( handler.messageFilterPermissive || null, recipient, false ) && (!handler.filterMessage || handler.filterMessage(sender, recipient)) ) { yield handler; } } } /** * Registers a handler for the given message. * * @param {string} messageName * The internal message name for which to register the handler. * @param {object} handler * An opaque handler object. The object may have a * `messageFilterStrict` and/or a `messageFilterPermissive` * property and/or a `filterMessage` method on which to filter messages. * * Final dispatching is handled by the message callback passed to * the constructor. */ addHandler(messageName, handler) { if (!this.handlers.has(messageName)) { this.handlers.set(messageName, new Set()); } this.handlers.get(messageName).add(handler); } /** * Unregisters a handler for the given message. * * @param {string} messageName * The internal message name for which to unregister the handler. * @param {object} handler * The handler object to unregister. */ removeHandler(messageName, handler) { if (this.handlers.has(messageName)) { this.handlers.get(messageName).delete(handler); } } } /** * A message dispatch and response manager that wrapse a single native * message manager. Handles dispatching messages through the manager * (optionally coalescing several low-priority messages and dispatching * them during an idle slice), and mapping their responses to the * appropriate response callbacks. * * Note that this is a simplified subclass of FilteringMessageManager * that only supports one handler per message, and does not support * filtering. */ class ResponseManager extends FilteringMessageManager { constructor(messageName, callback, messageManager) { super(messageName, callback, messageManager); this.idleMessages = []; this.idleScheduled = false; this.onIdle = this.onIdle.bind(this); } /** * Schedules a new idle callback to dispatch pending low-priority * messages, if one is not already scheduled. */ scheduleIdleCallback() { if (!this.idleScheduled) { ChromeUtils.idleDispatch(this.onIdle, { timeout: LOW_PRIORITY_TIMEOUT_MS, }); this.idleScheduled = true; } } /** * Called when the event queue is idle, and dispatches any pending * low-priority messages in a single chunk. * * @param {IdleDeadline} deadline */ onIdle(deadline) { this.idleScheduled = false; let messages = this.idleMessages; this.idleMessages = []; let msgs = messages.map(msg => msg.getMessage()); try { this.messageManager.sendAsyncMessage(MESSAGE_MESSAGES, msgs); } catch (e) { for (let msg of messages) { msg.reject(e); } } } /** * Sends a message through our wrapped message manager, or schedules * it for low-priority dispatch during an idle callback. * * @param {any} message * The message to send. * @param {object} [options] * Message dispatch options. * @param {boolean} [options.lowPriority = false] * If true, dispatches the message in a single chunk with other * low-priority messages the next time the event queue is idle. */ sendMessage(message, options = {}) { if (options.lowPriority) { this.idleMessages.push(message); this.scheduleIdleCallback(); } else { this.messageManager.sendAsyncMessage(MESSAGE_MESSAGES, [ message.getMessage(), ]); } } receiveMessage({ data, target }) { data.target = target; this.callback(this.handlers.get(data.messageName), data); } *getHandlers(messageName, sender, recipient) { let handler = this.handlers.get(messageName); if (handler) { yield handler; } } addHandler(messageName, handler) { if (DEBUG && this.handlers.has(messageName)) { throw new Error( `Handler already registered for response ID ${messageName}` ); } this.handlers.set(messageName, handler); } /** * Unregisters a handler for the given message. * * @param {string} messageName * The internal message name for which to unregister the handler. * @param {object} handler * The handler object to unregister. */ removeHandler(messageName, handler) { if (DEBUG && this.handlers.get(messageName) !== handler) { Cu.reportError( `Attempting to remove unexpected response handler for ${messageName}` ); } this.handlers.delete(messageName); } } /** * Manages mappings of message managers to their corresponding message * brokers. Brokers are lazily created for each message manager the * first time they are accessed. In the case of content frame message * managers, they are also automatically destroyed when the frame * unload event fires. */ class FilteringMessageManagerMap extends Map { // Unfortunately, we can't use a WeakMap for this, because message // managers do not support preserved wrappers. /** * @param {string} messageName * The native message name passed to `FilteringMessageManager` constructors. * @param {Function} callback * The message callback function passed to * `FilteringMessageManager` constructors. * @param {Function} [constructor = FilteringMessageManager] * The constructor for the message manager class that we're * mapping to. */ constructor(messageName, callback, constructor = FilteringMessageManager) { super(); this.messageName = messageName; this.callback = callback; this._constructor = constructor; } /** * Returns, and possibly creates, a message broker for the given * message manager. * * @param {nsIMessageListenerManager} target * The message manager for which to return a broker. * * @returns {FilteringMessageManager} */ get(target) { let broker = super.get(target); if (broker) { return broker; } broker = new this._constructor(this.messageName, this.callback, target); this.set(target, broker); // XXXbz if target is really known to be a MessageListenerManager, // do we need this isInstance check? if (EventTarget.isInstance(target)) { let onUnload = event => { target.removeEventListener("unload", onUnload); this.delete(target); }; target.addEventListener("unload", onUnload); } return broker; } } /** * Represents a message being sent through a MessageChannel, which may * or may not have been dispatched yet, and is pending a response. * * When a response has been received, or the message has been canceled, * this class is responsible for settling the response promise as * appropriate. * * @param {number} channelId * The unique ID for this message. * @param {any} message * The message contents. * @param {object} sender * An object describing the sender of the message, used by * `abortResponses` to determine whether the message should be * aborted. * @param {ResponseManager} broker * The response broker on which we're expected to receive a * reply. */ class PendingMessage { constructor(channelId, message, sender, broker) { this.channelId = channelId; this.message = message; this.sender = sender; this.broker = broker; this.deferred = Deferred(); MessageChannel.pendingResponses.add(this); } /** * Cleans up after this message once we've received or aborted a * response. */ cleanup() { if (this.broker) { this.broker.removeHandler(this.channelId, this); MessageChannel.pendingResponses.delete(this); this.message = null; this.broker = null; } } /** * Returns the promise which will resolve when we've received or * aborted a response to this message. */ get promise() { return this.deferred.promise; } /** * Resolves the message's response promise, and cleans up. * * @param {any} value */ resolve(value) { this.cleanup(); this.deferred.resolve(value); } /** * Rejects the message's response promise, and cleans up. * * @param {any} value */ reject(value) { this.cleanup(); this.deferred.reject(value); } get messageManager() { return this.broker.messageManager; } /** * Returns the contents of the message to be sent over a message * manager, and registers the response with our response broker. * * Returns null if the response has already been canceled, and the * message should not be sent. * * @returns {any} */ getMessage() { let msg = null; if (this.broker) { this.broker.addHandler(this.channelId, this); msg = this.message; this.message = null; } return msg; } } // Web workers has MessageChannel API, which is unrelated to this. // eslint-disable-next-line no-global-assign MessageChannel = { init() { Services.obs.addObserver(this, "message-manager-close"); Services.obs.addObserver(this, "message-manager-disconnect"); this.messageManagers = new FilteringMessageManagerMap( MESSAGE_MESSAGES, this._handleMessage.bind(this) ); this.responseManagers = new FilteringMessageManagerMap( MESSAGE_RESPONSE, this._handleResponse.bind(this), ResponseManager ); /** * @property {Set} pendingResponses * Contains a set of pending responses, either waiting to be * received or waiting to be sent. * * The response object must be a deferred promise with the following * properties: * * promise: * The promise object which resolves or rejects when the response * is no longer pending. * * reject: * A function which, when called, causes the `promise` object to be * rejected. * * sender: * A sender object, as passed to `sendMessage. * * messageManager: * The message manager the response will be sent or received on. * * When the promise resolves or rejects, it must be removed from the * list. * * These values are used to clear pending responses when execution * contexts are destroyed. */ this.pendingResponses = new Set(); /** * @property {LimitedSet} abortedResponses * Contains the message name of a limited number of aborted response * handlers, the responses for which will be ignored. */ this.abortedResponses = new ExtensionUtils.LimitedSet(30); }, RESULT_SUCCESS: 0, RESULT_DISCONNECTED: 1, RESULT_NO_HANDLER: 2, RESULT_MULTIPLE_HANDLERS: 3, RESULT_ERROR: 4, RESULT_NO_RESPONSE: 5, REASON_DISCONNECTED: { result: 1, // this.RESULT_DISCONNECTED message: "Message manager disconnected", }, /** * Specifies that only a single listener matching the specified * recipient tag may be listening for the given message, at the other * end of the target message manager. * * If no matching listeners exist, a RESULT_NO_HANDLER error will be * returned. If multiple matching listeners exist, a * RESULT_MULTIPLE_HANDLERS error will be returned. */ RESPONSE_SINGLE: 0, /** * If multiple message managers matching the specified recipient tag * are listening for a message, all listeners are notified, but only * the first response or error is returned. * * Only handlers which return a value other than `undefined` are * considered to have responded. Returning a Promise which evaluates * to `undefined` is interpreted as an explicit response. * * If no matching listeners exist, a RESULT_NO_HANDLER error will be * returned. If no listeners return a response, a RESULT_NO_RESPONSE * error will be returned. */ RESPONSE_FIRST: 1, /** * If multiple message managers matching the specified recipient tag * are listening for a message, all listeners are notified, and all * responses are returned as an array, once all listeners have * replied. */ RESPONSE_ALL: 2, /** * Fire-and-forget: The sender of this message does not expect a reply. */ RESPONSE_NONE: 3, /** * Initializes message handlers for the given message managers if needed. * * @param {Array} messageManagers */ setupMessageManagers(messageManagers) { for (let mm of messageManagers) { // This call initializes a FilteringMessageManager for |mm| if needed. // The FilteringMessageManager must be created to make sure that senders // of messages that expect a reply, such as MessageChannel:Message, do // actually receive a default reply even if there are no explicit message // handlers. this.messageManagers.get(mm); } }, /** * Returns true if the properties of the `data` object match those in * the `filter` object. Matching is done on a strict equality basis, * and the behavior varies depending on the value of the `strict` * parameter. * * @param {object?} filter * The filter object to match against. * @param {object} data * The data object being matched. * @param {boolean} [strict=true] * If true, all properties in the `filter` object have a * corresponding property in `data` with the same value. If * false, properties present in both objects must have the same * value. * @returns {boolean} True if the objects match. */ matchesFilter(filter, data, strict = true) { if (!filter) { return true; } if (strict) { return Object.keys(filter).every(key => { return key in data && data[key] === filter[key]; }); } return Object.keys(filter).every(key => { return !(key in data) || data[key] === filter[key]; }); }, /** * Adds a message listener to the given message manager. * * @param {nsIMessageListenerManager|Array} targets * The message managers on which to listen. * @param {string|number} messageName * The name of the message to listen for. * @param {MessageReceiver} handler * The handler to dispatch to. Must be an object with the following * properties: * * receiveMessage: * A method which is called for each message received by the * listener. The method takes one argument, an object, with the * following properties: * * messageName: * The internal message name, as passed to `sendMessage`. * * target: * The message manager which received this message. * * channelId: * The internal ID of the transaction, used to map responses to * the original sender. * * sender: * An object describing the sender, as passed to `sendMessage`. * * recipient: * An object describing the recipient, as passed to * `sendMessage`. * * data: * The contents of the message, as passed to `sendMessage`. * * The method may return any structured-clone-compatible * object, which will be returned as a response to the message * sender. It may also instead return a `Promise`, the * resolution or rejection value of which will likewise be * returned to the message sender. * * messageFilterStrict: * An object containing arbitrary properties on which to filter * received messages. Messages will only be dispatched to this * object if the `recipient` object passed to `sendMessage` * matches this filter, as determined by `matchesFilter` with * `strict=true`. * * messageFilterPermissive: * An object containing arbitrary properties on which to filter * received messages. Messages will only be dispatched to this * object if the `recipient` object passed to `sendMessage` * matches this filter, as determined by `matchesFilter` with * `strict=false`. * * filterMessage: * An optional function that prevents the handler from handling a * message by returning `false`. See `getHandlers` for the parameters. */ addListener(targets, messageName, handler) { if (!Array.isArray(targets)) { targets = [targets]; } for (let target of targets) { this.messageManagers.get(target).addHandler(messageName, handler); } }, /** * Removes a message listener from the given message manager. * * @param {nsIMessageListenerManager|Array} targets * The message managers on which to stop listening. * @param {string|number} messageName * The name of the message to stop listening for. * @param {MessageReceiver} handler * The handler to stop dispatching to. */ removeListener(targets, messageName, handler) { if (!Array.isArray(targets)) { targets = [targets]; } for (let target of targets) { if (this.messageManagers.has(target)) { this.messageManagers.get(target).removeHandler(messageName, handler); } } }, /** * Sends a message via the given message manager. Returns a promise which * resolves or rejects with the return value of the message receiver. * * The promise also rejects if there is no matching listener, or the other * side of the message manager disconnects before the response is received. * * @param {nsIMessageSender} target * The message manager on which to send the message. * @param {string} messageName * The name of the message to send, as passed to `addListener`. * @param {object} data * A structured-clone-compatible object to send to the message * recipient. * @param {object} [options] * An object containing any of the following properties: * @param {object} [options.recipient] * A structured-clone-compatible object to identify the message * recipient. The object must match the `messageFilterStrict` and * `messageFilterPermissive` filters defined by recipients in order * for the message to be received. * @param {object} [options.sender] * A structured-clone-compatible object to identify the message * sender. This object may also be used to avoid delivering the * message to the sender, and as a filter to prematurely * abort responses when the sender is being destroyed. * @see `abortResponses`. * @param {boolean} [options.lowPriority = false] * If true, treat this as a low-priority message, and attempt to * send it in the same chunk as other messages to the same target * the next time the event queue is idle. This option reduces * messaging overhead at the expense of adding some latency. * @param {integer} [options.responseType = RESPONSE_SINGLE] * Specifies the type of response expected. See the `RESPONSE_*` * contents for details. * @returns {Promise} */ sendMessage(target, messageName, data, options = {}) { let sender = options.sender || {}; let recipient = options.recipient || {}; let responseType = options.responseType || this.RESPONSE_SINGLE; let channelId = ExtensionUtils.getUniqueId(); let message = { messageName, channelId, sender, recipient, data, responseType, }; data = null; if (responseType == this.RESPONSE_NONE) { try { target.sendAsyncMessage(MESSAGE_MESSAGES, [message]); } catch (e) { // Caller is not expecting a reply, so dump the error to the console. Cu.reportError(e); return Promise.reject(e); } return Promise.resolve(); // Not expecting any reply. } let broker = this.responseManagers.get(target); let pending = new PendingMessage(channelId, message, recipient, broker); message = null; try { broker.sendMessage(pending, options); } catch (e) { pending.reject(e); } return pending.promise; }, _callHandlers(handlers, data) { let responseType = data.responseType; // At least one handler is required for all response types but // RESPONSE_ALL. if (!handlers.length && responseType != this.RESPONSE_ALL) { return Promise.reject({ result: MessageChannel.RESULT_NO_HANDLER, message: "No matching message handler", }); } if (responseType == this.RESPONSE_SINGLE) { if (handlers.length > 1) { return Promise.reject({ result: MessageChannel.RESULT_MULTIPLE_HANDLERS, message: `Multiple matching handlers for ${data.messageName}`, }); } // Note: We use `new Promise` rather than `Promise.resolve` here // so that errors from the handler are trapped and converted into // rejected promises. return new Promise(resolve => { resolve(handlers[0].receiveMessage(data)); }); } let responses = handlers.map((handler, i) => { try { return handler.receiveMessage(data, i + 1 == handlers.length); } catch (e) { return Promise.reject(e); } }); data = null; responses = responses.filter(response => response !== undefined); switch (responseType) { case this.RESPONSE_FIRST: if (!responses.length) { return Promise.reject({ result: MessageChannel.RESULT_NO_RESPONSE, message: "No handler returned a response", }); } return Promise.race(responses); case this.RESPONSE_ALL: return Promise.all(responses); } return Promise.reject({ message: "Invalid response type" }); }, /** * Handles dispatching message callbacks from the message brokers to their * appropriate `MessageReceivers`, and routing the responses back to the * original senders. * * Each handler object is a `MessageReceiver` object as passed to * `addListener`. * * @param {Array} handlers * @param {object} data * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target */ _handleMessage(handlers, data) { if (data.responseType == this.RESPONSE_NONE) { handlers.forEach(handler => { // The sender expects no reply, so dump any errors to the console. new Promise(resolve => { resolve(handler.receiveMessage(data)); }).catch(e => { Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e); }); }); data = null; // Note: Unhandled messages are silently dropped. return; } let target = getMessageManager(data.target); let deferred = { sender: data.sender, messageManager: target, channelId: data.channelId, respondingSide: true, }; let cleanup = () => { this.pendingResponses.delete(deferred); if (target.dispose) { target.dispose(); } }; this.pendingResponses.add(deferred); deferred.promise = new Promise((resolve, reject) => { deferred.reject = reject; this._callHandlers(handlers, data).then(resolve, reject); data = null; }) .then( value => { let response = { result: this.RESULT_SUCCESS, messageName: deferred.channelId, recipient: {}, value, }; if (target.isDisconnected) { // Target is disconnected. We can't send an error response, so // don't even try. return; } target.sendAsyncMessage(MESSAGE_RESPONSE, response); }, error => { if (target.isDisconnected) { // Target is disconnected. We can't send an error response, so // don't even try. if ( error.result !== this.RESULT_DISCONNECTED && error.result !== this.RESULT_NO_RESPONSE ) { Cu.reportError( Cu.getClassName(error, false) === "Object" ? error.message : error ); } return; } let response = { result: this.RESULT_ERROR, messageName: deferred.channelId, recipient: {}, error: {}, }; if (error && typeof error == "object") { if (error.result) { response.result = error.result; } // Error objects are not structured-clonable, so just copy // over the important properties. for (let key of [ "fileName", "filename", "lineNumber", "columnNumber", "message", "stack", "result", "mozWebExtLocation", ]) { if (key in error) { response.error[key] = error[key]; } } } target.sendAsyncMessage(MESSAGE_RESPONSE, response); } ) .then(cleanup, e => { cleanup(); Cu.reportError(e); }); }, /** * Handles message callbacks from the response brokers. * * @param {MessageHandler?} handler * A deferred object created by `sendMessage`, to be resolved * or rejected based on the contents of the response. * @param {object} data * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target */ _handleResponse(handler, data) { // If we have an error at this point, we have handler to report it to, // so just log it. if (!handler) { if (this.abortedResponses.has(data.messageName)) { this.abortedResponses.delete(data.messageName); Services.console.logStringMessage( `Ignoring response to aborted listener for ${data.messageName}` ); } else { Cu.reportError( `No matching message response handler for ${data.messageName}` ); } } else if (data.result === this.RESULT_SUCCESS) { handler.resolve(data.value); } else { handler.reject(data.error); } }, /** * Aborts pending message response for the specific channel. * * @param {string} channelId * A string for channelId of the response. * @param {object} reason * An object describing the reason the response was aborted. * Will be passed to the promise rejection handler of the aborted * response. */ abortChannel(channelId, reason) { for (let response of this.pendingResponses) { if (channelId === response.channelId && response.respondingSide) { this.pendingResponses.delete(response); response.reject(reason); } } }, /** * Aborts any pending message responses to senders matching the given * filter. * * @param {object} sender * The object on which to filter senders, as determined by * `matchesFilter`. * @param {object} [reason] * An optional object describing the reason the response was aborted. * Will be passed to the promise rejection handler of all aborted * responses. */ abortResponses(sender, reason = this.REASON_DISCONNECTED) { for (let response of this.pendingResponses) { if (this.matchesFilter(sender, response.sender)) { this.pendingResponses.delete(response); this.abortedResponses.add(response.channelId); response.reject(reason); } } }, /** * Aborts any pending message responses to the broker for the given * message manager. * * @param {nsIMessageListenerManager} target * The message manager for which to abort brokers. * @param {object} reason * An object describing the reason the responses were aborted. * Will be passed to the promise rejection handler of all aborted * responses. */ abortMessageManager(target, reason) { for (let response of this.pendingResponses) { if (matches(response.messageManager, target)) { this.abortedResponses.add(response.channelId); response.reject(reason); } } }, observe(subject, topic, data) { switch (topic) { case "message-manager-close": case "message-manager-disconnect": try { if (this.responseManagers.has(subject)) { this.abortMessageManager(subject, this.REASON_DISCONNECTED); } } finally { this.responseManagers.delete(subject); this.messageManagers.delete(subject); } break; } }, }; MessageChannel.init();