Bug 1870926 [wpt PR 43734] - Remove experimental ::details-summary pseudo-element...
[gecko.git] / toolkit / components / extensions / MessageChannel.sys.mjs
blobbb2445129cd4d42de5ea9e66dccc375efcb840ea
1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /**
8  * This module provides wrappers around standard message managers to
9  * simplify bidirectional communication. It currently allows a caller to
10  * send a message to a single listener, and receive a reply. If there
11  * are no matching listeners, or the message manager disconnects before
12  * a reply is received, the caller is returned an error.
13  *
14  * The listener end may specify filters for the messages it wishes to
15  * receive, and the sender end likewise may specify recipient tags to
16  * match the filters.
17  *
18  * The message handler on the listener side may return its response
19  * value directly, or may return a promise, the resolution or rejection
20  * of which will be returned instead. The sender end likewise receives a
21  * promise which resolves or rejects to the listener's response.
22  *
23  *
24  * A basic setup works something like this:
25  *
26  * A content script adds a message listener to its global
27  * ContentFrameMessageManager, with an appropriate set of filters:
28  *
29  *  {
30  *    init(messageManager, window, extensionID) {
31  *      this.window = window;
32  *
33  *      MessageChannel.addListener(
34  *        messageManager, "ContentScript:TouchContent",
35  *        this);
36  *
37  *      this.messageFilterStrict = {
38  *        innerWindowID: getInnerWindowID(window),
39  *        extensionID: extensionID,
40  *      };
41  *
42  *      this.messageFilterPermissive = {
43  *        outerWindowID: getOuterWindowID(window),
44  *      };
45  *    },
46  *
47  *    receiveMessage({ target, messageName, sender, recipient, data }) {
48  *      if (messageName == "ContentScript:TouchContent") {
49  *        return new Promise(resolve => {
50  *          this.touchWindow(data.touchWith, result => {
51  *            resolve({ touchResult: result });
52  *          });
53  *        });
54  *      }
55  *    },
56  *  };
57  *
58  * A script in the parent process sends a message to the content process
59  * via a tab message manager, including recipient tags to match its
60  * filter, and an optional sender tag to identify itself:
61  *
62  *  let data = { touchWith: "pencil" };
63  *  let sender = { extensionID, contextID };
64  *  let recipient = { innerWindowID: tab.linkedBrowser.innerWindowID, extensionID };
65  *
66  *  MessageChannel.sendMessage(
67  *    tab.linkedBrowser.messageManager, "ContentScript:TouchContent",
68  *    data, {recipient, sender}
69  *  ).then(result => {
70  *    alert(result.touchResult);
71  *  });
72  *
73  * Since the lifetimes of message senders and receivers may not always
74  * match, either side of the message channel may cancel pending
75  * responses which match its sender or recipient tags.
76  *
77  * For the above client, this might be done from an
78  * inner-window-destroyed observer, when its target scope is destroyed:
79  *
80  *  observe(subject, topic, data) {
81  *    if (topic == "inner-window-destroyed") {
82  *      let innerWindowID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
83  *
84  *      MessageChannel.abortResponses({ innerWindowID });
85  *    }
86  *  },
87  *
88  * From the parent, it may be done when its context is being destroyed:
89  *
90  *  onDestroy() {
91  *    MessageChannel.abortResponses({
92  *      extensionID: this.extensionID,
93  *      contextID: this.contextID,
94  *    });
95  *  },
96  *
97  */
99 export let MessageChannel;
101 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
103 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
105 const lazy = {};
107 ChromeUtils.defineESModuleGetters(lazy, {
108   MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.sys.mjs",
111 function getMessageManager(target) {
112   if (typeof target.sendAsyncMessage === "function") {
113     return target;
114   }
115   return new lazy.MessageManagerProxy(target);
118 function matches(target, messageManager) {
119   return target === messageManager || target.messageManager === messageManager;
122 const { DEBUG } = AppConstants;
124 // Idle callback timeout for low-priority message dispatch.
125 const LOW_PRIORITY_TIMEOUT_MS = 250;
127 const MESSAGE_MESSAGES = "MessageChannel:Messages";
128 const MESSAGE_RESPONSE = "MessageChannel:Response";
130 var _deferredResult;
131 var _makeDeferred = (resolve, reject) => {
132   // We use arrow functions here and refer to the outer variables via
133   // `this`, to avoid a lexical name lookup. Yes, it makes a difference.
134   // No, I don't like it any more than you do.
135   _deferredResult.resolve = resolve;
136   _deferredResult.reject = reject;
140  * Helper to create a new Promise without allocating any closures to
141  * receive its resolution functions.
143  * I know what you're thinking: "This is crazy. There is no possible way
144  * this can be necessary. Just use the ordinary Promise constructor the
145  * way it was meant to be used, you lunatic."
147  * And, against all odds, it turns out that you're wrong. Creating
148  * lambdas to receive promise resolution functions consistently turns
149  * out to be one of the most expensive parts of message dispatch in this
150  * code.
152  * So we do the stupid micro-optimization, and try to live with
153  * ourselves for it.
155  * (See also bug 1404950.)
157  * @returns {object}
158  */
159 let Deferred = () => {
160   let res = {};
161   _deferredResult = res;
162   res.promise = new Promise(_makeDeferred);
163   _deferredResult = null;
164   return res;
168  * Handles the mapping and dispatching of messages to their registered
169  * handlers. There is one broker per message manager and class of
170  * messages. Each class of messages is mapped to one native message
171  * name, e.g., "MessageChannel:Message", and is dispatched to handlers
172  * based on an internal message name, e.g., "Extension:ExecuteScript".
173  */
174 class FilteringMessageManager {
175   /**
176    * @param {string} messageName
177    *     The name of the native message this broker listens for.
178    * @param {Function} callback
179    *     A function which is called for each message after it has been
180    *     mapped to its handler. The function receives two arguments:
181    *
182    *       result:
183    *         An object containing either a `handler` or an `error` property.
184    *         If no error occurs, `handler` will be a matching handler that
185    *         was registered by `addHandler`. Otherwise, the `error` property
186    *         will contain an object describing the error.
187    *
188    *        data:
189    *          An object describing the message, as defined in
190    *          `MessageChannel.addListener`.
191    * @param {nsIMessageListenerManager} messageManager
192    */
193   constructor(messageName, callback, messageManager) {
194     this.messageName = messageName;
195     this.callback = callback;
196     this.messageManager = messageManager;
198     this.messageManager.addMessageListener(this.messageName, this, true);
200     this.handlers = new Map();
201   }
203   /**
204    * Receives a set of messages from our message manager, maps each to a
205    * handler, and passes the results to our message callbacks.
206    */
207   receiveMessage({ data, target }) {
208     data.forEach(msg => {
209       if (msg) {
210         let handlers = Array.from(
211           this.getHandlers(msg.messageName, msg.sender || null, msg.recipient)
212         );
214         msg.target = target;
215         this.callback(handlers, msg);
216       }
217     });
218   }
220   /**
221    * Iterates over all handlers for the given message name. If `recipient`
222    * is provided, only iterates over handlers whose filters match it.
223    *
224    * @param {string|number} messageName
225    *     The message for which to return handlers.
226    * @param {object} sender
227    *     The sender data on which to filter handlers.
228    * @param {object} recipient
229    *     The recipient data on which to filter handlers.
230    */
231   *getHandlers(messageName, sender, recipient) {
232     let handlers = this.handlers.get(messageName) || new Set();
233     for (let handler of handlers) {
234       if (
235         MessageChannel.matchesFilter(
236           handler.messageFilterStrict || null,
237           recipient
238         ) &&
239         MessageChannel.matchesFilter(
240           handler.messageFilterPermissive || null,
241           recipient,
242           false
243         ) &&
244         (!handler.filterMessage || handler.filterMessage(sender, recipient))
245       ) {
246         yield handler;
247       }
248     }
249   }
251   /**
252    * Registers a handler for the given message.
253    *
254    * @param {string} messageName
255    *     The internal message name for which to register the handler.
256    * @param {object} handler
257    *     An opaque handler object. The object may have a
258    *     `messageFilterStrict` and/or a `messageFilterPermissive`
259    *     property and/or a `filterMessage` method on which to filter messages.
260    *
261    *     Final dispatching is handled by the message callback passed to
262    *     the constructor.
263    */
264   addHandler(messageName, handler) {
265     if (!this.handlers.has(messageName)) {
266       this.handlers.set(messageName, new Set());
267     }
269     this.handlers.get(messageName).add(handler);
270   }
272   /**
273    * Unregisters a handler for the given message.
274    *
275    * @param {string} messageName
276    *     The internal message name for which to unregister the handler.
277    * @param {object} handler
278    *     The handler object to unregister.
279    */
280   removeHandler(messageName, handler) {
281     if (this.handlers.has(messageName)) {
282       this.handlers.get(messageName).delete(handler);
283     }
284   }
288  * A message dispatch and response manager that wrapse a single native
289  * message manager. Handles dispatching messages through the manager
290  * (optionally coalescing several low-priority messages and dispatching
291  * them during an idle slice), and mapping their responses to the
292  * appropriate response callbacks.
294  * Note that this is a simplified subclass of FilteringMessageManager
295  * that only supports one handler per message, and does not support
296  * filtering.
297  */
298 class ResponseManager extends FilteringMessageManager {
299   constructor(messageName, callback, messageManager) {
300     super(messageName, callback, messageManager);
302     this.idleMessages = [];
303     this.idleScheduled = false;
304     this.onIdle = this.onIdle.bind(this);
305   }
307   /**
308    * Schedules a new idle callback to dispatch pending low-priority
309    * messages, if one is not already scheduled.
310    */
311   scheduleIdleCallback() {
312     if (!this.idleScheduled) {
313       ChromeUtils.idleDispatch(this.onIdle, {
314         timeout: LOW_PRIORITY_TIMEOUT_MS,
315       });
316       this.idleScheduled = true;
317     }
318   }
320   /**
321    * Called when the event queue is idle, and dispatches any pending
322    * low-priority messages in a single chunk.
323    *
324    * @param {IdleDeadline} deadline
325    */
326   onIdle(deadline) {
327     this.idleScheduled = false;
329     let messages = this.idleMessages;
330     this.idleMessages = [];
332     let msgs = messages.map(msg => msg.getMessage());
333     try {
334       this.messageManager.sendAsyncMessage(MESSAGE_MESSAGES, msgs);
335     } catch (e) {
336       for (let msg of messages) {
337         msg.reject(e);
338       }
339     }
340   }
342   /**
343    * Sends a message through our wrapped message manager, or schedules
344    * it for low-priority dispatch during an idle callback.
345    *
346    * @param {any} message
347    *        The message to send.
348    * @param {object} [options]
349    *        Message dispatch options.
350    * @param {boolean} [options.lowPriority = false]
351    *        If true, dispatches the message in a single chunk with other
352    *        low-priority messages the next time the event queue is idle.
353    */
354   sendMessage(message, options = {}) {
355     if (options.lowPriority) {
356       this.idleMessages.push(message);
357       this.scheduleIdleCallback();
358     } else {
359       this.messageManager.sendAsyncMessage(MESSAGE_MESSAGES, [
360         message.getMessage(),
361       ]);
362     }
363   }
365   receiveMessage({ data, target }) {
366     data.target = target;
368     this.callback(this.handlers.get(data.messageName), data);
369   }
371   *getHandlers(messageName, sender, recipient) {
372     let handler = this.handlers.get(messageName);
373     if (handler) {
374       yield handler;
375     }
376   }
378   addHandler(messageName, handler) {
379     if (DEBUG && this.handlers.has(messageName)) {
380       throw new Error(
381         `Handler already registered for response ID ${messageName}`
382       );
383     }
384     this.handlers.set(messageName, handler);
385   }
387   /**
388    * Unregisters a handler for the given message.
389    *
390    * @param {string} messageName
391    *     The internal message name for which to unregister the handler.
392    * @param {object} handler
393    *     The handler object to unregister.
394    */
395   removeHandler(messageName, handler) {
396     if (DEBUG && this.handlers.get(messageName) !== handler) {
397       Cu.reportError(
398         `Attempting to remove unexpected response handler for ${messageName}`
399       );
400     }
401     this.handlers.delete(messageName);
402   }
406  * Manages mappings of message managers to their corresponding message
407  * brokers. Brokers are lazily created for each message manager the
408  * first time they are accessed. In the case of content frame message
409  * managers, they are also automatically destroyed when the frame
410  * unload event fires.
411  */
412 class FilteringMessageManagerMap extends Map {
413   // Unfortunately, we can't use a WeakMap for this, because message
414   // managers do not support preserved wrappers.
416   /**
417    * @param {string} messageName
418    *     The native message name passed to `FilteringMessageManager` constructors.
419    * @param {Function} callback
420    *     The message callback function passed to
421    *     `FilteringMessageManager` constructors.
422    * @param {Function} [constructor = FilteringMessageManager]
423    *     The constructor for the message manager class that we're
424    *     mapping to.
425    */
426   constructor(messageName, callback, constructor = FilteringMessageManager) {
427     super();
429     this.messageName = messageName;
430     this.callback = callback;
431     this._constructor = constructor;
432   }
434   /**
435    * Returns, and possibly creates, a message broker for the given
436    * message manager.
437    *
438    * @param {nsIMessageListenerManager} target
439    *     The message manager for which to return a broker.
440    *
441    * @returns {FilteringMessageManager}
442    */
443   get(target) {
444     let broker = super.get(target);
445     if (broker) {
446       return broker;
447     }
449     broker = new this._constructor(this.messageName, this.callback, target);
450     this.set(target, broker);
452     // XXXbz if target is really known to be a MessageListenerManager,
453     // do we need this isInstance check?
454     if (EventTarget.isInstance(target)) {
455       let onUnload = event => {
456         target.removeEventListener("unload", onUnload);
457         this.delete(target);
458       };
459       target.addEventListener("unload", onUnload);
460     }
462     return broker;
463   }
467  * Represents a message being sent through a MessageChannel, which may
468  * or may not have been dispatched yet, and is pending a response.
470  * When a response has been received, or the message has been canceled,
471  * this class is responsible for settling the response promise as
472  * appropriate.
474  * @param {number} channelId
475  *        The unique ID for this message.
476  * @param {any} message
477  *        The message contents.
478  * @param {object} sender
479  *        An object describing the sender of the message, used by
480  *        `abortResponses` to determine whether the message should be
481  *        aborted.
482  * @param {ResponseManager} broker
483  *        The response broker on which we're expected to receive a
484  *        reply.
485  */
486 class PendingMessage {
487   constructor(channelId, message, sender, broker) {
488     this.channelId = channelId;
489     this.message = message;
490     this.sender = sender;
491     this.broker = broker;
492     this.deferred = Deferred();
494     MessageChannel.pendingResponses.add(this);
495   }
497   /**
498    * Cleans up after this message once we've received or aborted a
499    * response.
500    */
501   cleanup() {
502     if (this.broker) {
503       this.broker.removeHandler(this.channelId, this);
504       MessageChannel.pendingResponses.delete(this);
506       this.message = null;
507       this.broker = null;
508     }
509   }
511   /**
512    * Returns the promise which will resolve when we've received or
513    * aborted a response to this message.
514    */
515   get promise() {
516     return this.deferred.promise;
517   }
519   /**
520    * Resolves the message's response promise, and cleans up.
521    *
522    * @param {any} value
523    */
524   resolve(value) {
525     this.cleanup();
526     this.deferred.resolve(value);
527   }
529   /**
530    * Rejects the message's response promise, and cleans up.
531    *
532    * @param {any} value
533    */
534   reject(value) {
535     this.cleanup();
536     this.deferred.reject(value);
537   }
539   get messageManager() {
540     return this.broker.messageManager;
541   }
543   /**
544    * Returns the contents of the message to be sent over a message
545    * manager, and registers the response with our response broker.
546    *
547    * Returns null if the response has already been canceled, and the
548    * message should not be sent.
549    *
550    * @returns {any}
551    */
552   getMessage() {
553     let msg = null;
554     if (this.broker) {
555       this.broker.addHandler(this.channelId, this);
556       msg = this.message;
557       this.message = null;
558     }
559     return msg;
560   }
563 // Web workers has MessageChannel API, which is unrelated to this.
564 // eslint-disable-next-line no-global-assign
565 MessageChannel = {
566   init() {
567     Services.obs.addObserver(this, "message-manager-close");
568     Services.obs.addObserver(this, "message-manager-disconnect");
570     this.messageManagers = new FilteringMessageManagerMap(
571       MESSAGE_MESSAGES,
572       this._handleMessage.bind(this)
573     );
575     this.responseManagers = new FilteringMessageManagerMap(
576       MESSAGE_RESPONSE,
577       this._handleResponse.bind(this),
578       ResponseManager
579     );
581     /**
582      * @property {Set<Deferred>} pendingResponses
583      * Contains a set of pending responses, either waiting to be
584      * received or waiting to be sent.
585      *
586      * The response object must be a deferred promise with the following
587      * properties:
588      *
589      *  promise:
590      *    The promise object which resolves or rejects when the response
591      *    is no longer pending.
592      *
593      *  reject:
594      *    A function which, when called, causes the `promise` object to be
595      *    rejected.
596      *
597      *  sender:
598      *    A sender object, as passed to `sendMessage.
599      *
600      *  messageManager:
601      *    The message manager the response will be sent or received on.
602      *
603      * When the promise resolves or rejects, it must be removed from the
604      * list.
605      *
606      * These values are used to clear pending responses when execution
607      * contexts are destroyed.
608      */
609     this.pendingResponses = new Set();
611     /**
612      * @property {LimitedSet<string>} abortedResponses
613      * Contains the message name of a limited number of aborted response
614      * handlers, the responses for which will be ignored.
615      */
616     this.abortedResponses = new ExtensionUtils.LimitedSet(30);
617   },
619   RESULT_SUCCESS: 0,
620   RESULT_DISCONNECTED: 1,
621   RESULT_NO_HANDLER: 2,
622   RESULT_MULTIPLE_HANDLERS: 3,
623   RESULT_ERROR: 4,
624   RESULT_NO_RESPONSE: 5,
626   REASON_DISCONNECTED: {
627     result: 1, // this.RESULT_DISCONNECTED
628     message: "Message manager disconnected",
629   },
631   /**
632    * Specifies that only a single listener matching the specified
633    * recipient tag may be listening for the given message, at the other
634    * end of the target message manager.
635    *
636    * If no matching listeners exist, a RESULT_NO_HANDLER error will be
637    * returned. If multiple matching listeners exist, a
638    * RESULT_MULTIPLE_HANDLERS error will be returned.
639    */
640   RESPONSE_SINGLE: 0,
642   /**
643    * If multiple message managers matching the specified recipient tag
644    * are listening for a message, all listeners are notified, but only
645    * the first response or error is returned.
646    *
647    * Only handlers which return a value other than `undefined` are
648    * considered to have responded. Returning a Promise which evaluates
649    * to `undefined` is interpreted as an explicit response.
650    *
651    * If no matching listeners exist, a RESULT_NO_HANDLER error will be
652    * returned. If no listeners return a response, a RESULT_NO_RESPONSE
653    * error will be returned.
654    */
655   RESPONSE_FIRST: 1,
657   /**
658    * If multiple message managers matching the specified recipient tag
659    * are listening for a message, all listeners are notified, and all
660    * responses are returned as an array, once all listeners have
661    * replied.
662    */
663   RESPONSE_ALL: 2,
665   /**
666    * Fire-and-forget: The sender of this message does not expect a reply.
667    */
668   RESPONSE_NONE: 3,
670   /**
671    * Initializes message handlers for the given message managers if needed.
672    *
673    * @param {Array<nsIMessageListenerManager>} messageManagers
674    */
675   setupMessageManagers(messageManagers) {
676     for (let mm of messageManagers) {
677       // This call initializes a FilteringMessageManager for |mm| if needed.
678       // The FilteringMessageManager must be created to make sure that senders
679       // of messages that expect a reply, such as MessageChannel:Message, do
680       // actually receive a default reply even if there are no explicit message
681       // handlers.
682       this.messageManagers.get(mm);
683     }
684   },
686   /**
687    * Returns true if the properties of the `data` object match those in
688    * the `filter` object. Matching is done on a strict equality basis,
689    * and the behavior varies depending on the value of the `strict`
690    * parameter.
691    *
692    * @param {object?} filter
693    *    The filter object to match against.
694    * @param {object} data
695    *    The data object being matched.
696    * @param {boolean} [strict=true]
697    *    If true, all properties in the `filter` object have a
698    *    corresponding property in `data` with the same value. If
699    *    false, properties present in both objects must have the same
700    *    value.
701    * @returns {boolean} True if the objects match.
702    */
703   matchesFilter(filter, data, strict = true) {
704     if (!filter) {
705       return true;
706     }
707     if (strict) {
708       return Object.keys(filter).every(key => {
709         return key in data && data[key] === filter[key];
710       });
711     }
712     return Object.keys(filter).every(key => {
713       return !(key in data) || data[key] === filter[key];
714     });
715   },
717   /**
718    * Adds a message listener to the given message manager.
719    *
720    * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
721    *    The message managers on which to listen.
722    * @param {string|number} messageName
723    *    The name of the message to listen for.
724    * @param {MessageReceiver} handler
725    *    The handler to dispatch to. Must be an object with the following
726    *    properties:
727    *
728    *      receiveMessage:
729    *        A method which is called for each message received by the
730    *        listener. The method takes one argument, an object, with the
731    *        following properties:
732    *
733    *          messageName:
734    *            The internal message name, as passed to `sendMessage`.
735    *
736    *          target:
737    *            The message manager which received this message.
738    *
739    *          channelId:
740    *            The internal ID of the transaction, used to map responses to
741    *            the original sender.
742    *
743    *          sender:
744    *            An object describing the sender, as passed to `sendMessage`.
745    *
746    *          recipient:
747    *            An object describing the recipient, as passed to
748    *            `sendMessage`.
749    *
750    *          data:
751    *            The contents of the message, as passed to `sendMessage`.
752    *
753    *        The method may return any structured-clone-compatible
754    *        object, which will be returned as a response to the message
755    *        sender. It may also instead return a `Promise`, the
756    *        resolution or rejection value of which will likewise be
757    *        returned to the message sender.
758    *
759    *      messageFilterStrict:
760    *        An object containing arbitrary properties on which to filter
761    *        received messages. Messages will only be dispatched to this
762    *        object if the `recipient` object passed to `sendMessage`
763    *        matches this filter, as determined by `matchesFilter` with
764    *        `strict=true`.
765    *
766    *      messageFilterPermissive:
767    *        An object containing arbitrary properties on which to filter
768    *        received messages. Messages will only be dispatched to this
769    *        object if the `recipient` object passed to `sendMessage`
770    *        matches this filter, as determined by `matchesFilter` with
771    *        `strict=false`.
772    *
773    *      filterMessage:
774    *        An optional function that prevents the handler from handling a
775    *        message by returning `false`. See `getHandlers` for the parameters.
776    */
777   addListener(targets, messageName, handler) {
778     if (!Array.isArray(targets)) {
779       targets = [targets];
780     }
781     for (let target of targets) {
782       this.messageManagers.get(target).addHandler(messageName, handler);
783     }
784   },
786   /**
787    * Removes a message listener from the given message manager.
788    *
789    * @param {nsIMessageListenerManager|Array<nsIMessageListenerManager>} targets
790    *    The message managers on which to stop listening.
791    * @param {string|number} messageName
792    *    The name of the message to stop listening for.
793    * @param {MessageReceiver} handler
794    *    The handler to stop dispatching to.
795    */
796   removeListener(targets, messageName, handler) {
797     if (!Array.isArray(targets)) {
798       targets = [targets];
799     }
800     for (let target of targets) {
801       if (this.messageManagers.has(target)) {
802         this.messageManagers.get(target).removeHandler(messageName, handler);
803       }
804     }
805   },
807   /**
808    * Sends a message via the given message manager. Returns a promise which
809    * resolves or rejects with the return value of the message receiver.
810    *
811    * The promise also rejects if there is no matching listener, or the other
812    * side of the message manager disconnects before the response is received.
813    *
814    * @param {nsIMessageSender} target
815    *    The message manager on which to send the message.
816    * @param {string} messageName
817    *    The name of the message to send, as passed to `addListener`.
818    * @param {object} data
819    *    A structured-clone-compatible object to send to the message
820    *    recipient.
821    * @param {object} [options]
822    *    An object containing any of the following properties:
823    * @param {object} [options.recipient]
824    *    A structured-clone-compatible object to identify the message
825    *    recipient. The object must match the `messageFilterStrict` and
826    *    `messageFilterPermissive` filters defined by recipients in order
827    *    for the message to be received.
828    * @param {object} [options.sender]
829    *    A structured-clone-compatible object to identify the message
830    *    sender. This object may also be used to avoid delivering the
831    *    message to the sender, and as a filter to prematurely
832    *    abort responses when the sender is being destroyed.
833    *    @see `abortResponses`.
834    * @param {boolean} [options.lowPriority = false]
835    *    If true, treat this as a low-priority message, and attempt to
836    *    send it in the same chunk as other messages to the same target
837    *    the next time the event queue is idle. This option reduces
838    *    messaging overhead at the expense of adding some latency.
839    * @param {integer} [options.responseType = RESPONSE_SINGLE]
840    *    Specifies the type of response expected. See the `RESPONSE_*`
841    *    contents for details.
842    * @returns {Promise}
843    */
844   sendMessage(target, messageName, data, options = {}) {
845     let sender = options.sender || {};
846     let recipient = options.recipient || {};
847     let responseType = options.responseType || this.RESPONSE_SINGLE;
849     let channelId = ExtensionUtils.getUniqueId();
850     let message = {
851       messageName,
852       channelId,
853       sender,
854       recipient,
855       data,
856       responseType,
857     };
858     data = null;
860     if (responseType == this.RESPONSE_NONE) {
861       try {
862         target.sendAsyncMessage(MESSAGE_MESSAGES, [message]);
863       } catch (e) {
864         // Caller is not expecting a reply, so dump the error to the console.
865         Cu.reportError(e);
866         return Promise.reject(e);
867       }
868       return Promise.resolve(); // Not expecting any reply.
869     }
871     let broker = this.responseManagers.get(target);
872     let pending = new PendingMessage(channelId, message, recipient, broker);
873     message = null;
874     try {
875       broker.sendMessage(pending, options);
876     } catch (e) {
877       pending.reject(e);
878     }
879     return pending.promise;
880   },
882   _callHandlers(handlers, data) {
883     let responseType = data.responseType;
885     // At least one handler is required for all response types but
886     // RESPONSE_ALL.
887     if (!handlers.length && responseType != this.RESPONSE_ALL) {
888       return Promise.reject({
889         result: MessageChannel.RESULT_NO_HANDLER,
890         message: "No matching message handler",
891       });
892     }
894     if (responseType == this.RESPONSE_SINGLE) {
895       if (handlers.length > 1) {
896         return Promise.reject({
897           result: MessageChannel.RESULT_MULTIPLE_HANDLERS,
898           message: `Multiple matching handlers for ${data.messageName}`,
899         });
900       }
902       // Note: We use `new Promise` rather than `Promise.resolve` here
903       // so that errors from the handler are trapped and converted into
904       // rejected promises.
905       return new Promise(resolve => {
906         resolve(handlers[0].receiveMessage(data));
907       });
908     }
910     let responses = handlers.map((handler, i) => {
911       try {
912         return handler.receiveMessage(data, i + 1 == handlers.length);
913       } catch (e) {
914         return Promise.reject(e);
915       }
916     });
917     data = null;
918     responses = responses.filter(response => response !== undefined);
920     switch (responseType) {
921       case this.RESPONSE_FIRST:
922         if (!responses.length) {
923           return Promise.reject({
924             result: MessageChannel.RESULT_NO_RESPONSE,
925             message: "No handler returned a response",
926           });
927         }
929         return Promise.race(responses);
931       case this.RESPONSE_ALL:
932         return Promise.all(responses);
933     }
934     return Promise.reject({ message: "Invalid response type" });
935   },
937   /**
938    * Handles dispatching message callbacks from the message brokers to their
939    * appropriate `MessageReceivers`, and routing the responses back to the
940    * original senders.
941    *
942    * Each handler object is a `MessageReceiver` object as passed to
943    * `addListener`.
944    *
945    * @param {Array<MessageHandler>} handlers
946    * @param {object} data
947    * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
948    */
949   _handleMessage(handlers, data) {
950     if (data.responseType == this.RESPONSE_NONE) {
951       handlers.forEach(handler => {
952         // The sender expects no reply, so dump any errors to the console.
953         new Promise(resolve => {
954           resolve(handler.receiveMessage(data));
955         }).catch(e => {
956           Cu.reportError(e.stack ? `${e}\n${e.stack}` : e.message || e);
957         });
958       });
959       data = null;
960       // Note: Unhandled messages are silently dropped.
961       return;
962     }
964     let target = getMessageManager(data.target);
966     let deferred = {
967       sender: data.sender,
968       messageManager: target,
969       channelId: data.channelId,
970       respondingSide: true,
971     };
973     let cleanup = () => {
974       this.pendingResponses.delete(deferred);
975       if (target.dispose) {
976         target.dispose();
977       }
978     };
979     this.pendingResponses.add(deferred);
981     deferred.promise = new Promise((resolve, reject) => {
982       deferred.reject = reject;
984       this._callHandlers(handlers, data).then(resolve, reject);
985       data = null;
986     })
987       .then(
988         value => {
989           let response = {
990             result: this.RESULT_SUCCESS,
991             messageName: deferred.channelId,
992             recipient: {},
993             value,
994           };
996           if (target.isDisconnected) {
997             // Target is disconnected. We can't send an error response, so
998             // don't even try.
999             return;
1000           }
1001           target.sendAsyncMessage(MESSAGE_RESPONSE, response);
1002         },
1003         error => {
1004           if (target.isDisconnected) {
1005             // Target is disconnected. We can't send an error response, so
1006             // don't even try.
1007             if (
1008               error.result !== this.RESULT_DISCONNECTED &&
1009               error.result !== this.RESULT_NO_RESPONSE
1010             ) {
1011               Cu.reportError(
1012                 Cu.getClassName(error, false) === "Object"
1013                   ? error.message
1014                   : error
1015               );
1016             }
1017             return;
1018           }
1020           let response = {
1021             result: this.RESULT_ERROR,
1022             messageName: deferred.channelId,
1023             recipient: {},
1024             error: {},
1025           };
1027           if (error && typeof error == "object") {
1028             if (error.result) {
1029               response.result = error.result;
1030             }
1031             // Error objects are not structured-clonable, so just copy
1032             // over the important properties.
1033             for (let key of [
1034               "fileName",
1035               "filename",
1036               "lineNumber",
1037               "columnNumber",
1038               "message",
1039               "stack",
1040               "result",
1041               "mozWebExtLocation",
1042             ]) {
1043               if (key in error) {
1044                 response.error[key] = error[key];
1045               }
1046             }
1047           }
1049           target.sendAsyncMessage(MESSAGE_RESPONSE, response);
1050         }
1051       )
1052       .then(cleanup, e => {
1053         cleanup();
1054         Cu.reportError(e);
1055       });
1056   },
1058   /**
1059    * Handles message callbacks from the response brokers.
1060    *
1061    * @param {MessageHandler?} handler
1062    *        A deferred object created by `sendMessage`, to be resolved
1063    *        or rejected based on the contents of the response.
1064    * @param {object} data
1065    * @param {nsIMessageSender|{messageManager:nsIMessageSender}} data.target
1066    */
1067   _handleResponse(handler, data) {
1068     // If we have an error at this point, we have handler to report it to,
1069     // so just log it.
1070     if (!handler) {
1071       if (this.abortedResponses.has(data.messageName)) {
1072         this.abortedResponses.delete(data.messageName);
1073         Services.console.logStringMessage(
1074           `Ignoring response to aborted listener for ${data.messageName}`
1075         );
1076       } else {
1077         Cu.reportError(
1078           `No matching message response handler for ${data.messageName}`
1079         );
1080       }
1081     } else if (data.result === this.RESULT_SUCCESS) {
1082       handler.resolve(data.value);
1083     } else {
1084       handler.reject(data.error);
1085     }
1086   },
1088   /**
1089    * Aborts pending message response for the specific channel.
1090    *
1091    * @param {string} channelId
1092    *    A string for channelId of the response.
1093    * @param {object} reason
1094    *    An object describing the reason the response was aborted.
1095    *    Will be passed to the promise rejection handler of the aborted
1096    *    response.
1097    */
1098   abortChannel(channelId, reason) {
1099     for (let response of this.pendingResponses) {
1100       if (channelId === response.channelId && response.respondingSide) {
1101         this.pendingResponses.delete(response);
1102         response.reject(reason);
1103       }
1104     }
1105   },
1107   /**
1108    * Aborts any pending message responses to senders matching the given
1109    * filter.
1110    *
1111    * @param {object} sender
1112    *    The object on which to filter senders, as determined by
1113    *    `matchesFilter`.
1114    * @param {object} [reason]
1115    *    An optional object describing the reason the response was aborted.
1116    *    Will be passed to the promise rejection handler of all aborted
1117    *    responses.
1118    */
1119   abortResponses(sender, reason = this.REASON_DISCONNECTED) {
1120     for (let response of this.pendingResponses) {
1121       if (this.matchesFilter(sender, response.sender)) {
1122         this.pendingResponses.delete(response);
1123         this.abortedResponses.add(response.channelId);
1124         response.reject(reason);
1125       }
1126     }
1127   },
1129   /**
1130    * Aborts any pending message responses to the broker for the given
1131    * message manager.
1132    *
1133    * @param {nsIMessageListenerManager} target
1134    *    The message manager for which to abort brokers.
1135    * @param {object} reason
1136    *    An object describing the reason the responses were aborted.
1137    *    Will be passed to the promise rejection handler of all aborted
1138    *    responses.
1139    */
1140   abortMessageManager(target, reason) {
1141     for (let response of this.pendingResponses) {
1142       if (matches(response.messageManager, target)) {
1143         this.abortedResponses.add(response.channelId);
1144         response.reject(reason);
1145       }
1146     }
1147   },
1149   observe(subject, topic, data) {
1150     switch (topic) {
1151       case "message-manager-close":
1152       case "message-manager-disconnect":
1153         try {
1154           if (this.responseManagers.has(subject)) {
1155             this.abortMessageManager(subject, this.REASON_DISCONNECTED);
1156           }
1157         } finally {
1158           this.responseManagers.delete(subject);
1159           this.messageManagers.delete(subject);
1160         }
1161         break;
1162     }
1163   },
1166 MessageChannel.init();