EventsDispatcher.sys.mjs (8002B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 Log: "chrome://remote/content/shared/Log.sys.mjs", 9 SessionDataCategory: 10 "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs", 11 SessionDataMethod: 12 "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs", 13 }); 14 15 ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); 16 17 /** 18 * Helper to listen to events which rely on SessionData. 19 * In order to support the EventsDispatcher, a module emitting events should 20 * subscribe and unsubscribe to those events based on SessionData updates 21 * and should use the "event" SessionData category. 22 */ 23 export class EventsDispatcher { 24 // The MessageHandler owning this EventsDispatcher. 25 #messageHandler; 26 27 /** 28 * @typedef {object} EventListenerInfo 29 * @property {ContextDescriptor} contextDescriptor 30 * The ContextDescriptor to which those callbacks are associated 31 * @property {Set<Function>} callbacks 32 * The callbacks to trigger when an event matching the ContextDescriptor 33 * is received. 34 */ 35 36 // Map from event name to map of strings (context keys) to EventListenerInfo. 37 #listenersByEventName; 38 39 /** 40 * Create a new EventsDispatcher instance. 41 * 42 * @param {MessageHandler} messageHandler 43 * The MessageHandler owning this EventsDispatcher. 44 */ 45 constructor(messageHandler) { 46 this.#messageHandler = messageHandler; 47 48 this.#listenersByEventName = new Map(); 49 } 50 51 destroy() { 52 for (const event of this.#listenersByEventName.keys()) { 53 this.#messageHandler.off(event, this.#onMessageHandlerEvent); 54 } 55 56 this.#listenersByEventName = null; 57 } 58 59 /** 60 * Check for existing listeners for a given event name and a given context. 61 * 62 * @param {string} name 63 * Name of the event to check. 64 * @param {ContextInfo} contextInfo 65 * ContextInfo identifying the context to check. 66 * 67 * @returns {boolean} 68 * True if there is a registered listener matching the provided arguments. 69 */ 70 hasListener(name, contextInfo) { 71 if (!this.#listenersByEventName.has(name)) { 72 return false; 73 } 74 75 const listeners = this.#listenersByEventName.get(name); 76 for (const { contextDescriptor } of listeners.values()) { 77 if (this.#matchesContext(contextInfo, contextDescriptor)) { 78 return true; 79 } 80 } 81 return false; 82 } 83 84 /** 85 * Stop listening for an event relying on SessionData and relayed by the 86 * message handler. 87 * 88 * @param {string} event 89 * Name of the event to unsubscribe from. 90 * @param {ContextDescriptor} contextDescriptor 91 * Context descriptor for this event. 92 * @param {Function} callback 93 * Event listener callback. 94 * @returns {Promise} 95 * Promise which resolves when the event fully unsubscribed, including 96 * propagating the necessary session data. 97 */ 98 async off(event, contextDescriptor, callback) { 99 return this.update([{ event, contextDescriptor, callback, enable: false }]); 100 } 101 102 /** 103 * Listen for an event relying on SessionData and relayed by the message 104 * handler. 105 * 106 * @param {string} event 107 * Name of the event to subscribe to. 108 * @param {ContextDescriptor} contextDescriptor 109 * Context descriptor for this event. 110 * @param {Function} callback 111 * Event listener callback. 112 * @returns {Promise} 113 * Promise which resolves when the event fully subscribed to, including 114 * propagating the necessary session data. 115 */ 116 async on(event, contextDescriptor, callback) { 117 return this.update([{ event, contextDescriptor, callback, enable: true }]); 118 } 119 120 /** 121 * An object that holds information about subscription/unsubscription 122 * of an event. 123 * 124 * @typedef Subscription 125 * 126 * @param {string} event 127 * Name of the event to subscribe/unsubscribe to. 128 * @param {ContextDescriptor} contextDescriptor 129 * Context descriptor for this event. 130 * @param {Function} callback 131 * Event listener callback. 132 * @param {boolean} enable 133 * True, if we need to subscribe to an event. 134 * Otherwise false. 135 */ 136 137 /** 138 * Start or stop listening to a list of events relying on SessionData 139 * and relayed by the message handler. 140 * 141 * @param {Array<Subscription>} subscriptions 142 * The list of information to subscribe/unsubscribe to. 143 * 144 * @returns {Promise} 145 * Promise which resolves when the events fully subscribed/unsubscribed to, 146 * including propagating the necessary session data. 147 */ 148 async update(subscriptions) { 149 const sessionDataItemUpdates = []; 150 subscriptions.forEach(subscription => { 151 // Skip invalid subscriptions 152 if (subscription === null) { 153 return; 154 } 155 156 const { event, contextDescriptor, callback, enable } = subscription; 157 if (enable) { 158 // Setup listeners. 159 if (!this.#listenersByEventName.has(event)) { 160 this.#listenersByEventName.set(event, new Map()); 161 this.#messageHandler.on(event, this.#onMessageHandlerEvent); 162 } 163 164 const key = this.#getContextKey(contextDescriptor); 165 const listeners = this.#listenersByEventName.get(event); 166 if (listeners.has(key)) { 167 const { callbacks } = listeners.get(key); 168 callbacks.add(callback); 169 } else { 170 const callbacks = new Set([callback]); 171 listeners.set(key, { callbacks, contextDescriptor }); 172 173 sessionDataItemUpdates.push({ 174 ...this.#getSessionDataItem(event, contextDescriptor), 175 method: lazy.SessionDataMethod.Add, 176 }); 177 } 178 } else { 179 // Remove listeners. 180 const listeners = this.#listenersByEventName.get(event); 181 if (!listeners) { 182 return; 183 } 184 185 const key = this.#getContextKey(contextDescriptor); 186 if (!listeners.has(key)) { 187 return; 188 } 189 190 const { callbacks } = listeners.get(key); 191 if (callbacks.has(callback)) { 192 callbacks.delete(callback); 193 if (callbacks.size === 0) { 194 listeners.delete(key); 195 if (listeners.size === 0) { 196 this.#messageHandler.off(event, this.#onMessageHandlerEvent); 197 this.#listenersByEventName.delete(event); 198 } 199 200 sessionDataItemUpdates.push({ 201 ...this.#getSessionDataItem(event, contextDescriptor), 202 method: lazy.SessionDataMethod.Remove, 203 }); 204 } 205 } 206 } 207 }); 208 209 // Update all sessionData at once. 210 await this.#messageHandler.updateSessionData(sessionDataItemUpdates); 211 } 212 213 #getContextKey(contextDescriptor) { 214 const { id, type } = contextDescriptor; 215 return `${type}-${id}`; 216 } 217 218 #getSessionDataItem(event, contextDescriptor) { 219 const [moduleName] = event.split("."); 220 return { 221 moduleName, 222 category: lazy.SessionDataCategory.Event, 223 contextDescriptor, 224 values: [event], 225 }; 226 } 227 228 #matchesContext(contextInfo, contextDescriptor) { 229 const eventBrowsingContext = BrowsingContext.get(contextInfo.contextId); 230 return this.#messageHandler.contextMatchesDescriptor( 231 eventBrowsingContext, 232 contextDescriptor 233 ); 234 } 235 236 #onMessageHandlerEvent = (name, event, contextInfo) => { 237 const listeners = this.#listenersByEventName.get(name); 238 for (const { callbacks, contextDescriptor } of listeners.values()) { 239 if (!this.#matchesContext(contextInfo, contextDescriptor)) { 240 continue; 241 } 242 243 for (const callback of callbacks) { 244 try { 245 callback(name, event); 246 } catch (e) { 247 lazy.logger.debug( 248 `Error while executing callback for ${name}: ${e.message}` 249 ); 250 } 251 } 252 } 253 }; 254 }