summaryrefslogtreecommitdiffstats
path: root/remote/shared/messagehandler/EventsDispatcher.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/shared/messagehandler/EventsDispatcher.sys.mjs')
-rw-r--r--remote/shared/messagehandler/EventsDispatcher.sys.mjs260
1 files changed, 260 insertions, 0 deletions
diff --git a/remote/shared/messagehandler/EventsDispatcher.sys.mjs b/remote/shared/messagehandler/EventsDispatcher.sys.mjs
new file mode 100644
index 0000000000..9620febcc1
--- /dev/null
+++ b/remote/shared/messagehandler/EventsDispatcher.sys.mjs
@@ -0,0 +1,260 @@
+/* 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/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ContextDescriptorType:
+ "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ SessionDataCategory:
+ "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
+ SessionDataMethod:
+ "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());
+
+/**
+ * Helper to listen to events which rely on SessionData.
+ * In order to support the EventsDispatcher, a module emitting events should
+ * subscribe and unsubscribe to those events based on SessionData updates
+ * and should use the "event" SessionData category.
+ */
+export class EventsDispatcher {
+ // The MessageHandler owning this EventsDispatcher.
+ #messageHandler;
+
+ /**
+ * @typedef {object} EventListenerInfo
+ * @property {ContextDescriptor} contextDescriptor
+ * The ContextDescriptor to which those callbacks are associated
+ * @property {Set<Function>} callbacks
+ * The callbacks to trigger when an event matching the ContextDescriptor
+ * is received.
+ */
+
+ // Map from event name to map of strings (context keys) to EventListenerInfo.
+ #listenersByEventName;
+
+ /**
+ * Create a new EventsDispatcher instance.
+ *
+ * @param {MessageHandler} messageHandler
+ * The MessageHandler owning this EventsDispatcher.
+ */
+ constructor(messageHandler) {
+ this.#messageHandler = messageHandler;
+
+ this.#listenersByEventName = new Map();
+ }
+
+ destroy() {
+ for (const event of this.#listenersByEventName.keys()) {
+ this.#messageHandler.off(event, this.#onMessageHandlerEvent);
+ }
+
+ this.#listenersByEventName = null;
+ }
+
+ /**
+ * Check for existing listeners for a given event name and a given context.
+ *
+ * @param {string} name
+ * Name of the event to check.
+ * @param {ContextInfo} contextInfo
+ * ContextInfo identifying the context to check.
+ *
+ * @returns {boolean}
+ * True if there is a registered listener matching the provided arguments.
+ */
+ hasListener(name, contextInfo) {
+ if (!this.#listenersByEventName.has(name)) {
+ return false;
+ }
+
+ const listeners = this.#listenersByEventName.get(name);
+ for (const { contextDescriptor } of listeners.values()) {
+ if (this.#matchesContext(contextInfo, contextDescriptor)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Stop listening for an event relying on SessionData and relayed by the
+ * message handler.
+ *
+ * @param {string} event
+ * Name of the event to unsubscribe from.
+ * @param {ContextDescriptor} contextDescriptor
+ * Context descriptor for this event.
+ * @param {Function} callback
+ * Event listener callback.
+ * @returns {Promise}
+ * Promise which resolves when the event fully unsubscribed, including
+ * propagating the necessary session data.
+ */
+ async off(event, contextDescriptor, callback) {
+ return this.update([{ event, contextDescriptor, callback, enable: false }]);
+ }
+
+ /**
+ * Listen for an event relying on SessionData and relayed by the message
+ * handler.
+ *
+ * @param {string} event
+ * Name of the event to subscribe to.
+ * @param {ContextDescriptor} contextDescriptor
+ * Context descriptor for this event.
+ * @param {Function} callback
+ * Event listener callback.
+ * @returns {Promise}
+ * Promise which resolves when the event fully subscribed to, including
+ * propagating the necessary session data.
+ */
+ async on(event, contextDescriptor, callback) {
+ return this.update([{ event, contextDescriptor, callback, enable: true }]);
+ }
+
+ /**
+ * An object that holds information about subscription/unsubscription
+ * of an event.
+ *
+ * @typedef Subscription
+ *
+ * @param {string} event
+ * Name of the event to subscribe/unsubscribe to.
+ * @param {ContextDescriptor} contextDescriptor
+ * Context descriptor for this event.
+ * @param {Function} callback
+ * Event listener callback.
+ * @param {boolean} enable
+ * True, if we need to subscribe to an event.
+ * Otherwise false.
+ */
+
+ /**
+ * Start or stop listening to a list of events relying on SessionData
+ * and relayed by the message handler.
+ *
+ * @param {Array<Subscription>} subscriptions
+ * The list of information to subscribe/unsubscribe to.
+ *
+ * @returns {Promise}
+ * Promise which resolves when the events fully subscribed/unsubscribed to,
+ * including propagating the necessary session data.
+ */
+ async update(subscriptions) {
+ const sessionDataItemUpdates = [];
+ subscriptions.forEach(({ event, contextDescriptor, callback, enable }) => {
+ if (enable) {
+ // Setup listeners.
+ if (!this.#listenersByEventName.has(event)) {
+ this.#listenersByEventName.set(event, new Map());
+ this.#messageHandler.on(event, this.#onMessageHandlerEvent);
+ }
+
+ const key = this.#getContextKey(contextDescriptor);
+ const listeners = this.#listenersByEventName.get(event);
+ if (listeners.has(key)) {
+ const { callbacks } = listeners.get(key);
+ callbacks.add(callback);
+ } else {
+ const callbacks = new Set([callback]);
+ listeners.set(key, { callbacks, contextDescriptor });
+
+ sessionDataItemUpdates.push({
+ ...this.#getSessionDataItem(event, contextDescriptor),
+ method: lazy.SessionDataMethod.Add,
+ });
+ }
+ } else {
+ // Remove listeners.
+ const listeners = this.#listenersByEventName.get(event);
+ if (!listeners) {
+ return;
+ }
+
+ const key = this.#getContextKey(contextDescriptor);
+ if (!listeners.has(key)) {
+ return;
+ }
+
+ const { callbacks } = listeners.get(key);
+ if (callbacks.has(callback)) {
+ callbacks.delete(callback);
+ if (callbacks.size === 0) {
+ listeners.delete(key);
+ if (listeners.size === 0) {
+ this.#messageHandler.off(event, this.#onMessageHandlerEvent);
+ this.#listenersByEventName.delete(event);
+ }
+
+ sessionDataItemUpdates.push({
+ ...this.#getSessionDataItem(event, contextDescriptor),
+ method: lazy.SessionDataMethod.Remove,
+ });
+ }
+ }
+ }
+ });
+
+ // Update all sessionData at once.
+ await this.#messageHandler.updateSessionData(sessionDataItemUpdates);
+ }
+
+ #getContextKey(contextDescriptor) {
+ const { id, type } = contextDescriptor;
+ return `${type}-${id}`;
+ }
+
+ #getSessionDataItem(event, contextDescriptor) {
+ const [moduleName] = event.split(".");
+ return {
+ moduleName,
+ category: lazy.SessionDataCategory.Event,
+ contextDescriptor,
+ values: [event],
+ };
+ }
+
+ #matchesContext(contextInfo, contextDescriptor) {
+ if (contextDescriptor.type === lazy.ContextDescriptorType.All) {
+ return true;
+ }
+
+ if (
+ contextDescriptor.type === lazy.ContextDescriptorType.TopBrowsingContext
+ ) {
+ const eventBrowsingContext = lazy.TabManager.getBrowsingContextById(
+ contextInfo.contextId
+ );
+ return eventBrowsingContext?.browserId === contextDescriptor.id;
+ }
+
+ return false;
+ }
+
+ #onMessageHandlerEvent = (name, event, contextInfo) => {
+ const listeners = this.#listenersByEventName.get(name);
+ for (const { callbacks, contextDescriptor } of listeners.values()) {
+ if (!this.#matchesContext(contextInfo, contextDescriptor)) {
+ continue;
+ }
+
+ for (const callback of callbacks) {
+ try {
+ callback(name, event);
+ } catch (e) {
+ lazy.logger.debug(
+ `Error while executing callback for ${name}: ${e.message}`
+ );
+ }
+ }
+ }
+ };
+}