Bug 1622408 [wpt PR 22244] - Restore the event delegate for a CSSTransition after...
[gecko.git] / devtools / shared / event-emitter.js
blobc28451ad2b14d78a1ba82116d351d56584641e20
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   constructor() {
18     this[eventListeners] = new Map();
19   }
21   /**
22    * Registers an event `listener` that is called every time events of
23    * specified `type` is emitted on the given event `target`.
24    *
25    * @param {Object} target
26    *    Event target object.
27    * @param {String} type
28    *    The type of event.
29    * @param {Function|Object} listener
30    *    The listener that processes the event.
31    * @returns {Function}
32    *    A function that removes the listener when called.
33    */
34   static on(target, type, listener) {
35     if (typeof listener !== "function" && !isEventHandler(listener)) {
36       throw new Error(BAD_LISTENER);
37     }
39     if (!(eventListeners in target)) {
40       target[eventListeners] = new Map();
41     }
43     const events = target[eventListeners];
45     if (events.has(type)) {
46       events.get(type).add(listener);
47     } else {
48       events.set(type, new Set([listener]));
49     }
51     return () => EventEmitter.off(target, type, listener);
52   }
54   /**
55    * Removes an event `listener` for the given event `type` on the given event
56    * `target`. If no `listener` is passed removes all listeners of the given
57    * `type`. If `type` is not passed removes all the listeners of the given
58    * event `target`.
59    * @param {Object} target
60    *    The event target object.
61    * @param {String} [type]
62    *    The type of event.
63    * @param {Function|Object} [listener]
64    *    The listener that processes the event.
65    */
66   static off(target, type, listener) {
67     const length = arguments.length;
68     const events = target[eventListeners];
70     if (!events) {
71       return;
72     }
74     if (length >= 3) {
75       // Trying to remove from the `target` the `listener` specified for the
76       // event's `type` given.
77       const listenersForType = events.get(type);
79       // If we don't have listeners for the event's type, we bail out.
80       if (!listenersForType) {
81         return;
82       }
84       // If the listeners list contains the listener given, we just remove it.
85       if (listenersForType.has(listener)) {
86         listenersForType.delete(listener);
87       } else {
88         // If it's not present, there is still the possibility that the listener
89         // have been added using `once`, since the method wraps the original listener
90         // in another function.
91         // So we iterate all the listeners to check if any of them is a wrapper to
92         // the `listener` given.
93         for (const value of listenersForType.values()) {
94           if (
95             onceOriginalListener in value &&
96             value[onceOriginalListener] === listener
97           ) {
98             listenersForType.delete(value);
99             break;
100           }
101         }
102       }
103     } else if (length === 2) {
104       // No listener was given, it means we're removing all the listeners from
105       // the given event's `type`.
106       if (events.has(type)) {
107         events.delete(type);
108       }
109     } else if (length === 1) {
110       // With only the `target` given, we're removing all the listeners from the object.
111       events.clear();
112     }
113   }
115   static clearEvents(target) {
116     const events = target[eventListeners];
117     if (!events) {
118       return;
119     }
120     events.clear();
121   }
123   /**
124    * Registers an event `listener` that is called only the next time an event
125    * of the specified `type` is emitted on the given event `target`.
126    * It returns a promised resolved once the specified event `type` is emitted.
127    *
128    * @param {Object} target
129    *    Event target object.
130    * @param {String} type
131    *    The type of the event.
132    * @param {Function|Object} [listener]
133    *    The listener that processes the event.
134    * @return {Promise}
135    *    The promise resolved once the event `type` is emitted.
136    */
137   static once(target, type, listener) {
138     return new Promise(resolve => {
139       // This is the actual listener that will be added to the target's listener, it wraps
140       // the call to the original `listener` given.
141       const newListener = (first, ...rest) => {
142         // To prevent side effects we're removing the listener upfront.
143         EventEmitter.off(target, type, newListener);
145         let rv;
146         if (listener) {
147           if (isEventHandler(listener)) {
148             // if the `listener` given is actually an object that handles the events
149             // using `EventEmitter.handler`, we want to call that function, passing also
150             // the event's type as first argument, and the `listener` (the object) as
151             // contextual object.
152             rv = listener[handler](type, first, ...rest);
153           } else {
154             // Otherwise we'll just call it
155             rv = listener.call(target, first, ...rest);
156           }
157         }
159         // We resolve the promise once the listener is called.
160         resolve(first);
162         // Listeners may return a promise, so pass it along
163         return rv;
164       };
166       newListener[onceOriginalListener] = listener;
167       EventEmitter.on(target, type, newListener);
168     });
169   }
171   static emit(target, type, ...rest) {
172     EventEmitter._emit(target, type, false, ...rest);
173   }
175   static emitAsync(target, type, ...rest) {
176     return EventEmitter._emit(target, type, true, ...rest);
177   }
179   /**
180    * Emit an event of a given `type` on a given `target` object.
181    *
182    * @param {Object} target
183    *    Event target object.
184    * @param {String} type
185    *    The type of the event.
186    * @param {Boolean} async
187    *    If true, this function will wait for each listener completion.
188    *    Each listener has to return a promise, which will be awaited for.
189    * @param {any} ...rest
190    *    The arguments to pass to each listener function.
191    * @return {Promise|undefined}
192    *    If `async` argument is true, returns the promise resolved once all listeners have resolved.
193    *    Otherwise, this function returns undefined;
194    */
195   static _emit(target, type, async, ...rest) {
196     logEvent(type, rest);
198     if (!(eventListeners in target)) {
199       return undefined;
200     }
202     const promises = async ? [] : null;
204     if (target[eventListeners].has(type)) {
205       // Creating a temporary Set with the original listeners, to avoiding side effects
206       // in emit.
207       const listenersForType = new Set(target[eventListeners].get(type));
209       const events = target[eventListeners];
210       const listeners = events.get(type);
212       for (const listener of listenersForType) {
213         // If the object was destroyed during event emission, stop emitting.
214         if (!(eventListeners in target)) {
215           break;
216         }
218         // If listeners were removed during emission, make sure the
219         // event handler we're going to fire wasn't removed.
220         if (listeners && listeners.has(listener)) {
221           try {
222             let promise;
223             if (isEventHandler(listener)) {
224               promise = listener[handler](type, ...rest);
225             } else {
226               promise = listener.call(target, ...rest);
227             }
228             if (async) {
229               // Assert the name instead of `constructor != Promise` in order
230               // to avoid cross compartment issues where Promise can be multiple.
231               if (!promise || promise.constructor.name != "Promise") {
232                 console.warn(
233                   `Listener for event '${type}' did not return a promise.`
234                 );
235               } else {
236                 promises.push(promise);
237               }
238             }
239           } catch (ex) {
240             // Prevent a bad listener from interfering with the others.
241             console.error(ex);
242             const msg = ex + ": " + ex.stack;
243             dump(msg + "\n");
244           }
245         }
246       }
247     }
249     // Backward compatibility with the SDK event-emitter: support wildcard listeners that
250     // will be called for any event. The arguments passed to the listener are the event
251     // type followed by the actual arguments.
252     // !!! This API will be removed by Bug 1391261.
253     const hasWildcardListeners = target[eventListeners].has("*");
254     if (type !== "*" && hasWildcardListeners) {
255       EventEmitter.emit(target, "*", type, ...rest);
256     }
258     if (async) {
259       return Promise.all(promises);
260     }
262     return undefined;
263   }
265   /**
266    * Returns a number of event listeners registered for the given event `type`
267    * on the given event `target`.
268    *
269    * @param {Object} target
270    *    Event target object.
271    * @param {String} type
272    *    The type of event.
273    * @return {Number}
274    *    The number of event listeners.
275    */
276   static count(target, type) {
277     if (eventListeners in target) {
278       const listenersForType = target[eventListeners].get(type);
280       if (listenersForType) {
281         return listenersForType.size;
282       }
283     }
285     return 0;
286   }
288   /**
289    * Decorate an object with event emitter functionality; basically using the
290    * class' prototype as mixin.
291    *
292    * @param Object target
293    *    The object to decorate.
294    * @return Object
295    *    The object given, mixed.
296    */
297   static decorate(target) {
298     const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
299     delete descriptors.constructor;
300     return Object.defineProperties(target, descriptors);
301   }
303   static get handler() {
304     return handler;
305   }
307   on(...args) {
308     return EventEmitter.on(this, ...args);
309   }
311   off(...args) {
312     EventEmitter.off(this, ...args);
313   }
315   clearEvents() {
316     EventEmitter.clearEvents(this);
317   }
319   once(...args) {
320     return EventEmitter.once(this, ...args);
321   }
323   emit(...args) {
324     EventEmitter.emit(this, ...args);
325   }
327   emitAsync(...args) {
328     return EventEmitter.emitAsync(this, ...args);
329   }
331   emitForTests(...args) {
332     if (flags.testing) {
333       EventEmitter.emit(this, ...args);
334     }
335   }
338 module.exports = EventEmitter;
340 const isEventHandler = listener =>
341   listener && handler in listener && typeof listener[handler] === "function";
343 const Services = require("Services");
344 const { getNthPathExcluding } = require("devtools/shared/platform/stack");
345 let loggingEnabled = false;
347 if (!isWorker) {
348   loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
349   const observer = {
350     observe: () => {
351       loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
352     },
353   };
354   Services.prefs.addObserver("devtools.dump.emit", observer);
356   // Also listen for Loader unload to unregister the pref observer and
357   // prevent leaking
358   const unloadObserver = function(subject) {
359     if (subject.wrappedJSObject == require("@loader/unload")) {
360       Services.prefs.removeObserver("devtools.dump.emit", observer);
361       Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
362     }
363   };
364   Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
367 function serialize(target) {
368   const MAXLEN = 60;
370   // Undefined
371   if (typeof target === "undefined") {
372     return "undefined";
373   }
375   if (target === null) {
376     return "null";
377   }
379   // Number / String
380   if (typeof target === "string" || typeof target === "number") {
381     return truncate(target, MAXLEN);
382   }
384   // HTML Node
385   if (target.nodeName) {
386     let out = target.nodeName;
388     if (target.id) {
389       out += "#" + target.id;
390     }
391     if (target.className) {
392       out += "." + target.className;
393     }
395     return out;
396   }
398   // Array
399   if (Array.isArray(target)) {
400     return truncate(target.toSource(), MAXLEN);
401   }
403   // Function
404   if (typeof target === "function") {
405     return `function ${target.name ? target.name : "anonymous"}()`;
406   }
408   // Window
409   if (
410     target.constructor &&
411     target.constructor.name &&
412     target.constructor.name === "Window"
413   ) {
414     return `window (${target.location.origin})`;
415   }
417   // Object
418   if (typeof target === "object") {
419     let out = "{";
421     const entries = Object.entries(target);
422     for (let i = 0; i < Math.min(10, entries.length); i++) {
423       const [name, value] = entries[i];
425       if (i > 0) {
426         out += ", ";
427       }
429       out += `${name}: ${truncate(value, MAXLEN)}`;
430     }
432     return out + "}";
433   }
435   // Other
436   return truncate(target.toSource(), MAXLEN);
439 function truncate(value, maxLen) {
440   // We don't use value.toString() because it can throw.
441   const str = String(value);
442   return str.length > maxLen ? str.substring(0, maxLen) + "..." : str;
445 function logEvent(type, args) {
446   if (!loggingEnabled) {
447     return;
448   }
450   let argsOut = "";
452   // We need this try / catch to prevent any dead object errors.
453   try {
454     argsOut = `${args.map(serialize).join(", ")}`;
455   } catch (e) {
456     // Object is dead so the toolbox is most likely shutting down,
457     // do nothing.
458   }
460   const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
462   if (args.length > 0) {
463     dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
464   } else {
465     dump(`EMITTING: emit(${type}) from ${path}\n`);
466   }