event-emitter.js (11184B)
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 "use strict"; 6 7 const BAD_LISTENER = "The event listener must be a function."; 8 9 const eventListeners = Symbol("EventEmitter/listeners"); 10 const onceResolvers = Symbol("EventEmitter/once-resolvers"); 11 loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js"); 12 13 class EventEmitter { 14 /** 15 * Decorate an object with event emitter functionality; basically using the 16 * class' prototype as mixin. 17 * 18 * @param Object target 19 * The object to decorate. 20 * @return Object 21 * The object given, mixed. 22 */ 23 static decorate(target) { 24 const descriptors = Object.getOwnPropertyDescriptors(this.prototype); 25 delete descriptors.constructor; 26 return Object.defineProperties(target, descriptors); 27 } 28 29 /** 30 * Registers an event `listener` that is called every time events of 31 * specified `type` is emitted on this instance. 32 * 33 * @param {string} type 34 * The type of event. 35 * @param {Function} listener 36 * The listener that processes the event. 37 * @param {object} options 38 * @param {AbortSignal} options.signal 39 * The listener will be removed when linked AbortController’s abort() method is called 40 * @returns {Function} 41 * A function that removes the listener when called. 42 */ 43 on(type, listener, { signal } = {}) { 44 if (typeof listener !== "function") { 45 throw new Error(BAD_LISTENER); 46 } 47 48 if (signal?.aborted === true) { 49 // The signal is already aborted so don't setup the listener. 50 // We return an empty function as it's the expected returned value. 51 return () => {}; 52 } 53 54 if (!(eventListeners in this)) { 55 this[eventListeners] = new Map(); 56 } 57 58 const events = this[eventListeners]; 59 60 if (events.has(type)) { 61 events.get(type).add(listener); 62 } else { 63 events.set(type, new Set([listener])); 64 } 65 66 const offFn = () => this.off(type, listener); 67 68 if (signal) { 69 signal.addEventListener("abort", offFn, { once: true }); 70 } 71 72 return offFn; 73 } 74 75 /** 76 * Removes an event `listener` for the given event `type` on this instance 77 * If no `listener` is passed removes all listeners of the given 78 * `type`. If `type` is not passed removes all the listeners of this instance. 79 * 80 * @param {string} [type] 81 * The type of event. 82 * @param {Function} [listener] 83 * The listener that processes the event. 84 */ 85 off(type, listener) { 86 const length = arguments.length; 87 const events = this[eventListeners]; 88 89 if (!events) { 90 return; 91 } 92 93 if (length >= 2) { 94 // Trying to remove from `this` the `listener` specified for the event's `type` given. 95 const listenersForType = events.get(type); 96 97 // If we don't have listeners for the event's type, we bail out. 98 if (!listenersForType) { 99 return; 100 } 101 102 // If the listeners list contains the listener given, we just remove it. 103 if (listenersForType.has(listener)) { 104 listenersForType.delete(listener); 105 delete listener[onceResolvers]; 106 } 107 } else if (length === 1) { 108 // No listener was given, it means we're removing all the listeners from 109 // the given event's `type`. 110 if (events.has(type)) { 111 events.delete(type); 112 } 113 } else if (length === 0) { 114 // With no parameter passed, we're removing all the listeners from this. 115 events.clear(); 116 } 117 } 118 119 clearEvents() { 120 const events = this[eventListeners]; 121 if (!events) { 122 return; 123 } 124 events.clear(); 125 } 126 127 /** 128 * Registers an event `listener` that is called only the next time an event 129 * of the specified `type` is emitted on this instance. 130 * It returns a Promise resolved once the specified event `type` is emitted. 131 * 132 * @param {string} type 133 * The type of the event. 134 * @param {Function} [listener] 135 * The listener that processes the event. 136 * @param {object} options 137 * @param {AbortSignal} options.signal 138 * The listener will be removed when linked AbortController’s abort() method is called 139 * @return {Promise} 140 * The promise resolved once the event `type` is emitted. 141 */ 142 once(type, listener = function () {}, options) { 143 const { promise, resolve } = Promise.withResolvers(); 144 if (!listener[onceResolvers]) { 145 listener[onceResolvers] = []; 146 } 147 listener[onceResolvers].push(resolve); 148 this.on(type, listener, options); 149 return promise; 150 } 151 152 emit(type, ...rest) { 153 this._emit(type, false, rest); 154 } 155 156 emitAsync(type, ...rest) { 157 return this._emit(type, true, rest); 158 } 159 160 emitForTests(type, ...rest) { 161 if (flags.testing) { 162 this.emit(type, ...rest); 163 } 164 } 165 166 /** 167 * Emit an event of a given `type` on this instance. 168 * 169 * @param {string} type 170 * The type of the event. 171 * @param {boolean} async 172 * If true, this function will wait for each listener completion. 173 * Each listener has to return a promise, which will be awaited for. 174 * @param {Array} args 175 * The arguments to pass to each listener function. 176 * @return {Promise|undefined} 177 * If `async` argument is true, returns the promise resolved once all listeners have resolved. 178 * Otherwise, this function returns undefined; 179 */ 180 _emit(type, async, args) { 181 if (loggingEnabled) { 182 logEvent(type, args); 183 } 184 185 const targetEventListeners = this[eventListeners]; 186 if (!targetEventListeners) { 187 return undefined; 188 } 189 190 const listeners = targetEventListeners.get(type); 191 if (!listeners?.size) { 192 return undefined; 193 } 194 195 const promises = async ? [] : null; 196 197 // Creating a temporary Set with the original listeners, to avoiding side effects 198 // in emit. 199 for (const listener of new Set(listeners)) { 200 // If the object was destroyed during event emission, stop emitting. 201 if (!(eventListeners in this)) { 202 break; 203 } 204 205 // If listeners were removed during emission, make sure the 206 // event handler we're going to fire wasn't removed. 207 if (listeners && listeners.has(listener)) { 208 try { 209 // If this was a one-off listener (add via `EventEmitter#once`), unregister the 210 // listener right away, before firing the listener, to prevent re-entry in case 211 // the listener fires the same event again. 212 const resolvers = listener[onceResolvers]; 213 if (resolvers) { 214 this.off(type, listener); 215 } 216 const promise = listener.apply(this, args); 217 // Resolve the promise returned by `EventEmitter#once` only after having called 218 // the listener. 219 if (resolvers) { 220 for (const resolver of resolvers) { 221 // Resolve with the first argument fired on the listened event 222 // (`EventEmitter#once` listeners don't have access to all the other arguments). 223 resolver(args[0]); 224 } 225 } 226 if (async) { 227 // Assert the name instead of `constructor != Promise` in order 228 // to avoid cross compartment issues where Promise can be multiple. 229 if (!promise || promise.constructor.name != "Promise") { 230 console.warn( 231 `Listener for event '${type}' did not return a promise.` 232 ); 233 } else { 234 promises.push(promise); 235 } 236 } 237 } catch (ex) { 238 // Prevent a bad listener from interfering with the others. 239 console.error(ex); 240 const msg = ex + ": " + ex.stack; 241 dump(msg + "\n"); 242 } 243 } 244 } 245 246 if (async) { 247 return Promise.all(promises); 248 } 249 250 return undefined; 251 } 252 253 /** 254 * Returns a number of event listeners registered for the given event `type` on this instance. 255 * 256 * @param {string} type 257 * The type of event. 258 * @return {number} 259 * The number of event listeners. 260 */ 261 count(type) { 262 if (eventListeners in this) { 263 const listenersForType = this[eventListeners].get(type); 264 265 if (listenersForType) { 266 return listenersForType.size; 267 } 268 } 269 270 return 0; 271 } 272 } 273 274 module.exports = EventEmitter; 275 276 const { 277 getNthPathExcluding, 278 } = require("resource://devtools/shared/platform/stack.js"); 279 let loggingEnabled = false; 280 281 if (!isWorker) { 282 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false); 283 const observer = { 284 observe: () => { 285 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit"); 286 }, 287 }; 288 Services.prefs.addObserver("devtools.dump.emit", observer); 289 290 // Also listen for Loader unload to unregister the pref observer and 291 // prevent leaking 292 const unloadObserver = function (subject) { 293 if (subject.wrappedJSObject == require("@loader/unload")) { 294 Services.prefs.removeObserver("devtools.dump.emit", observer); 295 Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy"); 296 } 297 }; 298 Services.obs.addObserver(unloadObserver, "devtools:loader:destroy"); 299 } 300 301 function serialize(target) { 302 const MAXLEN = 60; 303 304 // Undefined 305 if (typeof target === "undefined") { 306 return "undefined"; 307 } 308 309 if (target === null) { 310 return "null"; 311 } 312 313 // Number / String 314 if (typeof target === "string" || typeof target === "number") { 315 return truncate(target, MAXLEN); 316 } 317 318 // HTML Node 319 if (target.nodeName) { 320 let out = target.nodeName; 321 322 if (target.id) { 323 out += "#" + target.id; 324 } 325 if (target.className) { 326 out += "." + target.className; 327 } 328 329 return out; 330 } 331 332 // Array 333 if (Array.isArray(target)) { 334 return truncate(target.toSource(), MAXLEN); 335 } 336 337 // Function 338 if (typeof target === "function") { 339 return `function ${target.name ? target.name : "anonymous"}()`; 340 } 341 342 // Window 343 if (target?.constructor?.name === "Window") { 344 return `window (${target.location.origin})`; 345 } 346 347 // Object 348 if (typeof target === "object") { 349 let out = "{"; 350 351 const entries = Object.entries(target); 352 for (let i = 0; i < Math.min(10, entries.length); i++) { 353 const [name, value] = entries[i]; 354 355 if (i > 0) { 356 out += ", "; 357 } 358 359 out += `${name}: ${truncate(value, MAXLEN)}`; 360 } 361 362 return out + "}"; 363 } 364 365 // Other 366 return truncate(target.toSource(), MAXLEN); 367 } 368 369 function truncate(value, maxLen) { 370 // We don't use value.toString() because it can throw. 371 const str = String(value); 372 return str.length > maxLen ? str.substring(0, maxLen) + "..." : str; 373 } 374 375 function logEvent(type, args) { 376 let argsOut = ""; 377 378 // We need this try / catch to prevent any dead object errors. 379 try { 380 argsOut = `${args.map(serialize).join(", ")}`; 381 } catch (e) { 382 // Object is dead so the toolbox is most likely shutting down, 383 // do nothing. 384 } 385 386 const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js"); 387 388 if (args.length) { 389 dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`); 390 } else { 391 dump(`EMITTING: emit(${type}) from ${path}\n`); 392 } 393 }