forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1176 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1176 lines
		
	
	
	
		
			36 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- 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/. */
 | |
| "use strict";
 | |
| 
 | |
| /**
 | |
|  * 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,
 | |
|  *    });
 | |
|  *  },
 | |
|  *
 | |
|  */
 | |
| 
 | |
| const EXPORTED_SYMBOLS = ["MessageChannel"];
 | |
| let MessageChannel;
 | |
| 
 | |
| const { AppConstants } = ChromeUtils.import(
 | |
|   "resource://gre/modules/AppConstants.jsm"
 | |
| );
 | |
| const { ExtensionUtils } = ChromeUtils.import(
 | |
|   "resource://gre/modules/ExtensionUtils.jsm"
 | |
| );
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineModuleGetter(
 | |
|   lazy,
 | |
|   "MessageManagerProxy",
 | |
|   "resource://gre/modules/MessageManagerProxy.jsm"
 | |
| );
 | |
| 
 | |
| 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<Deferred>} 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<string>} 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<nsIMessageListenerManager>} 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<nsIMessageListenerManager>} 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<nsIMessageListenerManager>} 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<MessageHandler>} 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();
 | 
