Bug 1668452 require an exact match of Window, rate, and device when selecting a Media...
[gecko.git] / devtools / shared / event-emitter.js
blobe7d27fa26b69ede14a59d5ab78f1111a1f2f1e72
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", "devtools/shared/flags");
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 Services = require("Services");
354 const { getNthPathExcluding } = require("devtools/shared/platform/stack");
355 let loggingEnabled = false;
357 if (!isWorker) {
358   loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false);
359   const observer = {
360     observe: () => {
361       loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
362     },
363   };
364   Services.prefs.addObserver("devtools.dump.emit", observer);
366   // Also listen for Loader unload to unregister the pref observer and
367   // prevent leaking
368   const unloadObserver = function(subject) {
369     if (subject.wrappedJSObject == require("@loader/unload")) {
370       Services.prefs.removeObserver("devtools.dump.emit", observer);
371       Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
372     }
373   };
374   Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
377 function serialize(target) {
378   const MAXLEN = 60;
380   // Undefined
381   if (typeof target === "undefined") {
382     return "undefined";
383   }
385   if (target === null) {
386     return "null";
387   }
389   // Number / String
390   if (typeof target === "string" || typeof target === "number") {
391     return truncate(target, MAXLEN);
392   }
394   // HTML Node
395   if (target.nodeName) {
396     let out = target.nodeName;
398     if (target.id) {
399       out += "#" + target.id;
400     }
401     if (target.className) {
402       out += "." + target.className;
403     }
405     return out;
406   }
408   // Array
409   if (Array.isArray(target)) {
410     return truncate(target.toSource(), MAXLEN);
411   }
413   // Function
414   if (typeof target === "function") {
415     return `function ${target.name ? target.name : "anonymous"}()`;
416   }
418   // Window
419   if (target?.constructor?.name === "Window") {
420     return `window (${target.location.origin})`;
421   }
423   // Object
424   if (typeof target === "object") {
425     let out = "{";
427     const entries = Object.entries(target);
428     for (let i = 0; i < Math.min(10, entries.length); i++) {
429       const [name, value] = entries[i];
431       if (i > 0) {
432         out += ", ";
433       }
435       out += `${name}: ${truncate(value, MAXLEN)}`;
436     }
438     return out + "}";
439   }
441   // Other
442   return truncate(target.toSource(), MAXLEN);
445 function truncate(value, maxLen) {
446   // We don't use value.toString() because it can throw.
447   const str = String(value);
448   return str.length > maxLen ? str.substring(0, maxLen) + "..." : str;
451 function logEvent(type, args) {
452   let argsOut = "";
454   // We need this try / catch to prevent any dead object errors.
455   try {
456     argsOut = `${args.map(serialize).join(", ")}`;
457   } catch (e) {
458     // Object is dead so the toolbox is most likely shutting down,
459     // do nothing.
460   }
462   const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
464   if (args.length > 0) {
465     dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
466   } else {
467     dump(`EMITTING: emit(${type}) from ${path}\n`);
468   }