summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/MessageManagerProxy.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/MessageManagerProxy.sys.mjs')
-rw-r--r--toolkit/components/extensions/MessageManagerProxy.sys.mjs210
1 files changed, 210 insertions, 0 deletions
diff --git a/toolkit/components/extensions/MessageManagerProxy.sys.mjs b/toolkit/components/extensions/MessageManagerProxy.sys.mjs
new file mode 100644
index 0000000000..4df410e80f
--- /dev/null
+++ b/toolkit/components/extensions/MessageManagerProxy.sys.mjs
@@ -0,0 +1,210 @@
+/* -*- 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/. */
+
+import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
+
+const { DefaultMap } = ExtensionUtils;
+
+/**
+ * Acts as a proxy for a message manager or message manager owner, and
+ * tracks docShell swaps so that messages are always sent to the same
+ * receiver, even if it is moved to a different <browser>.
+ *
+ * @param {nsIMessageSender|Element} target
+ * The target message manager on which to send messages, or the
+ * <browser> element which owns it.
+ */
+export class MessageManagerProxy {
+ constructor(target) {
+ this.listeners = new DefaultMap(() => new Map());
+ this.closed = false;
+
+ if (target instanceof Ci.nsIMessageSender) {
+ this.messageManager = target;
+ } else {
+ this.addListeners(target);
+ }
+
+ Services.obs.addObserver(this, "message-manager-close");
+ }
+
+ /**
+ * Disposes of the proxy object, removes event listeners, and drops
+ * all references to the underlying message manager.
+ *
+ * Must be called before the last reference to the proxy is dropped,
+ * unless the underlying message manager or <browser> is also being
+ * destroyed.
+ */
+ dispose() {
+ if (this.eventTarget) {
+ this.removeListeners(this.eventTarget);
+ this.eventTarget = null;
+ }
+ this.messageManager = null;
+
+ Services.obs.removeObserver(this, "message-manager-close");
+ }
+
+ observe(subject, topic, data) {
+ if (topic === "message-manager-close") {
+ if (subject === this.messageManager) {
+ this.closed = true;
+ }
+ }
+ }
+
+ /**
+ * Returns true if the given target is the same as, or owns, the given
+ * message manager.
+ *
+ * @param {nsIMessageSender|MessageManagerProxy|Element} target
+ * The message manager, MessageManagerProxy, or <browser>
+ * element against which to match.
+ * @param {nsIMessageSender} messageManager
+ * The message manager against which to match `target`.
+ *
+ * @returns {boolean}
+ * True if `messageManager` is the same object as `target`, or
+ * `target` is a MessageManagerProxy or <browser> element that
+ * is tied to it.
+ */
+ static matches(target, messageManager) {
+ return (
+ target === messageManager || target.messageManager === messageManager
+ );
+ }
+
+ /**
+ * @property {nsIMessageSender|null} messageManager
+ * The message manager that is currently being proxied. This
+ * may change during the life of the proxy object, so should
+ * not be stored elsewhere.
+ */
+
+ /**
+ * Sends a message on the proxied message manager.
+ *
+ * @param {Array} args
+ * Arguments to be passed verbatim to the underlying
+ * sendAsyncMessage method.
+ * @returns {undefined}
+ */
+ sendAsyncMessage(...args) {
+ if (this.messageManager) {
+ return this.messageManager.sendAsyncMessage(...args);
+ }
+
+ Cu.reportError(
+ `Cannot send message: Other side disconnected: ${uneval(args)}`
+ );
+ }
+
+ get isDisconnected() {
+ return this.closed || !this.messageManager;
+ }
+
+ /**
+ * Adds a message listener to the current message manager, and
+ * transfers it to the new message manager after a docShell swap.
+ *
+ * @param {string} message
+ * The name of the message to listen for.
+ * @param {nsIMessageListener} listener
+ * The listener to add.
+ * @param {boolean} [listenWhenClosed = false]
+ * If true, the listener will receive messages which were sent
+ * after the remote side of the listener began closing.
+ */
+ addMessageListener(message, listener, listenWhenClosed = false) {
+ this.messageManager.addMessageListener(message, listener, listenWhenClosed);
+ this.listeners.get(message).set(listener, listenWhenClosed);
+ }
+
+ /**
+ * Adds a message listener from the current message manager.
+ *
+ * @param {string} message
+ * The name of the message to stop listening for.
+ * @param {nsIMessageListener} listener
+ * The listener to remove.
+ */
+ removeMessageListener(message, listener) {
+ this.messageManager.removeMessageListener(message, listener);
+
+ let listeners = this.listeners.get(message);
+ listeners.delete(listener);
+ if (!listeners.size) {
+ this.listeners.delete(message);
+ }
+ }
+
+ /**
+ * Iterates over all of the currently registered message listeners.
+ *
+ * @private
+ */
+ *iterListeners() {
+ for (let [message, listeners] of this.listeners) {
+ for (let [listener, listenWhenClosed] of listeners) {
+ yield { message, listener, listenWhenClosed };
+ }
+ }
+ }
+
+ /**
+ * Adds docShell swap listeners to the message manager owner.
+ *
+ * @param {Element} target
+ * The target element.
+ * @private
+ */
+ addListeners(target) {
+ target.addEventListener("SwapDocShells", this);
+
+ this.eventTarget = target;
+ this.messageManager = target.messageManager;
+
+ for (let { message, listener, listenWhenClosed } of this.iterListeners()) {
+ this.messageManager.addMessageListener(
+ message,
+ listener,
+ listenWhenClosed
+ );
+ }
+ }
+
+ /**
+ * Removes docShell swap listeners to the message manager owner.
+ *
+ * @param {Element} target
+ * The target element.
+ * @private
+ */
+ removeListeners(target) {
+ target.removeEventListener("SwapDocShells", this);
+
+ for (let { message, listener } of this.iterListeners()) {
+ this.messageManager.removeMessageListener(message, listener);
+ }
+ }
+
+ handleEvent(event) {
+ if (event.type == "SwapDocShells") {
+ this.removeListeners(this.eventTarget);
+ // The SwapDocShells event is dispatched for both browsers that are being
+ // swapped. To avoid double-swapping, register the event handler after
+ // both SwapDocShells events have fired.
+ this.eventTarget.addEventListener(
+ "EndSwapDocShells",
+ () => {
+ this.addListeners(event.detail);
+ },
+ { once: true }
+ );
+ }
+ }
+}