Bug 1883449 - [wpt-sync] Update web-platform-tests to bb67daef8e6a384ead5da4c991c12f8...
[gecko.git] / devtools / shared / event-emitter.js
blob2ba4ae927b3af4744dd245c16b1b4b93af78b53e
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/. */
5 "use strict";
7 const BAD_LISTENER =
8   "The event listener must be a function, or an object that has " +
9   "`EventEmitter.handler` Symbol.";
11 const eventListeners = Symbol("EventEmitter/listeners");
12 const onceOriginalListener = Symbol("EventEmitter/once-original-listener");
13 const handler = Symbol("EventEmitter/event-handler");
14 loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
16 class EventEmitter {
17   /**
18    * Registers an event `listener` that is called every time events of
19    * specified `type` is emitted on the given event `target`.
20    *
21    * @param {Object} target
22    *    Event target object.
23    * @param {String} type
24    *    The type of event.
25    * @param {Function|Object} listener
26    *    The listener that processes the event.
27    * @param {Object} options
28    * @param {AbortSignal} options.signal
29    *     The listener will be removed when linked AbortController’s abort() method is called
30    * @returns {Function}
31    *    A function that removes the listener when called.
32    */
33   static on(target, type, listener, { signal } = {}) {
34     if (typeof listener !== "function" && !isEventHandler(listener)) {
35       throw new Error(BAD_LISTENER);
36     }
38     if (signal?.aborted === true) {
39       // The signal is already aborted so don't setup the listener.
40       // We return an empty function as it's the expected returned value.
41       return () => {};
42     }
44     if (!(eventListeners in target)) {
45       target[eventListeners] = new Map();
46     }
48     const events = target[eventListeners];
50     if (events.has(type)) {
51       events.get(type).add(listener);
52     } else {
53       events.set(type, new Set([listener]));
54     }
56     const offFn = () => EventEmitter.off(target, type, listener);
58     if (signal) {
59       signal.addEventListener("abort", offFn, { once: true });
60     }
62     return offFn;
63   }
65   /**
66    * Removes an event `listener` for the given event `type` on the given event
67    * `target`. If no `listener` is passed removes all listeners of the given
68    * `type`. If `type` is not passed removes all the listeners of the given
69    * event `target`.
70    * @param {Object} target
71    *    The event target object.
72    * @param {String} [type]
73    *    The type of event.
74    * @param {Function|Object} [listener]
75    *    The listener that processes the event.
76    */
77   static off(target, type, listener) {
78     const length = arguments.length;
79     const events = target[eventListeners];
81     if (!events) {
82       return;
83     }
85     if (length >= 3) {
86       // Trying to remove from the `target` the `listener` specified for the
87       // event's `type` given.
88       const listenersForType = events.get(type);
90       // If we don't have listeners for the event's type, we bail out.
91       if (!listenersForType) {
92         return;
93       }
95       // If the listeners list contains the listener given, we just remove it.
96       if (listenersForType.has(listener)) {
97         listenersForType.delete(listener);
98       } else {
99         // If it's not present, there is still the possibility that the listener
100         // have been added using `once`, since the method wraps the original listener
101         // in another function.
102         // So we iterate all the listeners to check if any of them is a wrapper to
103         // the `listener` given.
104         for (const value of listenersForType.values()) {
105           if (
106             onceOriginalListener in value &&
107             value[onceOriginalListener] === listener
108           ) {
109             listenersForType.delete(value);
110             break;
111           }
112         }
113       }
114     } else if (length === 2) {
115       // No listener was given, it means we're removing all the listeners from
116       // the given event's `type`.
117       if (events.has(type)) {
118         events.delete(type);
119       }
120     } else if (length === 1) {
121       // With only the `target` given, we're removing all the listeners from the object.
122       events.clear();
123     }
124   }
126   static clearEvents(target) {
127     const events = target[eventListeners];
128     if (!events) {
129       return;
130     }
131     events.clear();
132   }
134   /**
135    * Registers an event `listener` that is called only the next time an event
136    * of the specified `type` is emitted on the given event `target`.
137    * It returns a Promise resolved once the specified event `type` is emitted.
138    *
139    * @param {Object} target
140    *    Event target object.
141    * @param {String} type
142    *    The type of the event.
143    * @param {Function|Object} [listener]
144    *    The listener that processes the event.
145    * @param {Object} options
146    * @param {AbortSignal} options.signal
147    *     The listener will be removed when linked AbortController’s abort() method is called
148    * @return {Promise}
149    *    The promise resolved once the event `type` is emitted.
150    */
151   static once(target, type, listener, options) {
152     return new Promise(resolve => {
153       // This is the actual listener that will be added to the target's listener, it wraps
154       // the call to the original `listener` given.
155       const newListener = (first, ...rest) => {
156         // To prevent side effects we're removing the listener upfront.
157         EventEmitter.off(target, type, newListener);
159         let rv;
160         if (listener) {
161           if (isEventHandler(listener)) {
162             // if the `listener` given is actually an object that handles the events
163             // using `EventEmitter.handler`, we want to call that function, passing also
164             // the event's type as first argument, and the `listener` (the object) as
165             // contextual object.
166             rv = listener[handler](type, first, ...rest);
167           } else {
168             // Otherwise we'll just call it
169             rv = listener.call(target, first, ...rest);
170           }
171         }
173         // We resolve the promise once the listener is called.
174         resolve(first);
176         // Listeners may return a promise, so pass it along
177         return rv;
178       };
180       newListener[onceOriginalListener] = listener;
181       EventEmitter.on(target, type, newListener, options);
182     });
183   }
185   static emit(target, type, ...rest) {
186     EventEmitter._emit(target, type, false, rest);
187   }
189   static emitAsync(target, type, ...rest) {
190     return EventEmitter._emit(target, type, true, rest);
191   }
193   /**
194    * Emit an event of a given `type` on a given `target` object.
195    *
196    * @param {Object} target
197    *    Event target object.
198    * @param {String} type
199    *    The type of the event.
200    * @param {Boolean} async
201    *    If true, this function will wait for each listener completion.
202    *    Each listener has to return a promise, which will be awaited for.
203    * @param {Array} args
204    *    The arguments to pass to each listener function.
205    * @return {Promise|undefined}
206    *    If `async` argument is true, returns the promise resolved once all listeners have resolved.
207    *    Otherwise, this function returns undefined;
208    */
209   static _emit(target, type, async, args) {
210     if (loggingEnabled) {
211       logEvent(type, args);
212     }
214     const targetEventListeners = target[eventListeners];
215     if (!targetEventListeners) {
216       return undefined;
217     }
219     const listeners = targetEventListeners.get(type);
220     if (!listeners?.size) {
221       return undefined;
222     }
224     const promises = async ? [] : null;
226     // Creating a temporary Set with the original listeners, to avoiding side effects
227     // in emit.
228     for (const listener of new Set(listeners)) {
229       // If the object was destroyed during event emission, stop emitting.
230       if (!(eventListeners in target)) {
231         break;
232       }
234       // If listeners were removed during emission, make sure the
235       // event handler we're going to fire wasn't removed.
236       if (listeners && listeners.has(listener)) {
237         try {
238           let promise;
239           if (isEventHandler(listener)) {
240             promise = listener[handler](type, ...args);
241           } else {
242             promise = listener.apply(target, args);
243           }
244           if (async) {
245             // Assert the name instead of `constructor != Promise` in order
246             // to avoid cross compartment issues where Promise can be multiple.
247             if (!promise || promise.constructor.name != "Promise") {
248               console.warn(
249                 `Listener for event '${type}' did not return a promise.`
250               );
251             } else {
252               promises.push(promise);
253             }
254           }
255         } catch (ex) {
256           // Prevent a bad listener from interfering with the others.
257           console.error(ex);
258           const msg = ex + ": " + ex.stack;
259           dump(msg + "\n");
260         }
261       }
262     }
264     if (async) {
265       return Promise.all(promises);
266     }
268     return undefined;
269   }
271   /**
272    * Returns a number of event listeners registered for the given event `type`
273    * on the given event `target`.
274    *
275    * @param {Object} target
276    *    Event target object.
277    * @param {String} type
278    *    The type of event.
279    * @return {Number}
280    *    The number of event listeners.
281    */
282   static count(target, type) {
283     if (eventListeners in target) {
284       const listenersForType = target[eventListeners].get(type);
286       if (listenersForType) {
287         return listenersForType.size;
288       }
289     }
291     return 0;
292   }
294   /**
295    * Decorate an object with event emitter functionality; basically using the
296    * class' prototype as mixin.
297    *
298    * @param Object target
299    *    The object to decorate.
300    * @return Object
301    *    The object given, mixed.
302    */
303   static decorate(target) {
304     const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
305     delete descriptors.constructor;
306     return Object.defineProperties(target, descriptors);
307   }
309   static get handler() {
310     return handler;
311   }
313   on(...args) {
314     return EventEmitter.on(this, ...args);
315   }
317   off(...args) {
318     EventEmitter.off(this, ...args);
319   }
321   clearEvents() {
322     EventEmitter.clearEvents(this);
323   }
325   once(...args) {
326     return EventEmitter.once(this, ...args);
327   }
329   emit(...args) {
330     EventEmitter.emit(this, ...args);
331   }
333   emitAsync(...args) {
334     return EventEmitter.emitAsync(this, ...args);
335   }
337   emitForTests(...args) {
338     if (flags.testing) {
339       EventEmitter.emit(this, ...args);
340     }
341   }
343   count(...args) {
344     return EventEmitter.count(this, ...args);
345   }
348 module.exports = EventEmitter;
350 const isEventHandler = listener =>
351   listener && handler in listener && typeof listener[handler] === "function";
353 const {
354   getNthPathExcluding,
355 } = require("resource://devtools/shared/platform/stack.js");
356 let loggingEnabled = false;
358 if (!isWorker) {
359   loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false);
360   const observer = {
361     observe: () => {
362       loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
363     },
364   };
365   Services.prefs.addObserver("devtools.dump.emit", observer);
367   // Also listen for Loader unload to unregister the pref observer and
368   // prevent leaking
369   const unloadObserver = function (subject) {
370     if (subject.wrappedJSObject == require("@loader/unload")) {
371       Services.prefs.removeObserver("devtools.dump.emit", observer);
372       Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
373     }
374   };
375   Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
378 function serialize(target) {
379   const MAXLEN = 60;
381   // Undefined
382   if (typeof target === "undefined") {
383     return "undefined";
384   }
386   if (target === null) {
387     return "null";
388   }
390   // Number / String
391   if (typeof target === "string" || typeof target === "number") {
392     return truncate(target, MAXLEN);
393   }
395   // HTML Node
396   if (target.nodeName) {
397     let out = target.nodeName;
399     if (target.id) {
400       out += "#" + target.id;
401     }
402     if (target.className) {
403       out += "." + target.className;
404     }
406     return out;
407   }
409   // Array
410   if (Array.isArray(target)) {
411     return truncate(target.toSource(), MAXLEN);
412   }
414   // Function
415   if (typeof target === "function") {
416     return `function ${target.name ? target.name : "anonymous"}()`;
417   }
419   // Window
420   if (target?.constructor?.name === "Window") {
421     return `window (${target.location.origin})`;
422   }
424   // Object
425   if (typeof target === "object") {
426     let out = "{";
428     const entries = Object.entries(target);
429     for (let i = 0; i < Math.min(10, entries.length); i++) {
430       const [name, value] = entries[i];
432       if (i > 0) {
433         out += ", ";
434       }
436       out += `${name}: ${truncate(value, MAXLEN)}`;
437     }
439     return out + "}";
440   }
442   // Other
443   return truncate(target.toSource(), MAXLEN);
446 function truncate(value, maxLen) {
447   // We don't use value.toString() because it can throw.
448   const str = String(value);
449   return str.length > maxLen ? str.substring(0, maxLen) + "..." : str;
452 function logEvent(type, args) {
453   let argsOut = "";
455   // We need this try / catch to prevent any dead object errors.
456   try {
457     argsOut = `${args.map(serialize).join(", ")}`;
458   } catch (e) {
459     // Object is dead so the toolbox is most likely shutting down,
460     // do nothing.
461   }
463   const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
465   if (args.length) {
466     dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
467   } else {
468     dump(`EMITTING: emit(${type}) from ${path}\n`);
469   }