Bug 1467571 [wpt PR 11385] - Make manifest's parsers quicker, a=testonly
[gecko.git] / devtools / shared / event-emitter.js
blobbea47f3de0fe0a6de100284164908f1cf840312c
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 = "The event listener must be a function, or an object that has " +
8                      "`EventEmitter.handler` Symbol.";
10 const eventListeners = Symbol("EventEmitter/listeners");
11 const onceOriginalListener = Symbol("EventEmitter/once-original-listener");
12 const handler = Symbol("EventEmitter/event-handler");
14 class EventEmitter {
15   constructor() {
16     this[eventListeners] = new Map();
17   }
19   /**
20    * Registers an event `listener` that is called every time events of
21    * specified `type` is emitted on the given event `target`.
22    *
23    * @param {Object} target
24    *    Event target object.
25    * @param {String} type
26    *    The type of event.
27    * @param {Function|Object} listener
28    *    The listener that processes the event.
29    */
30   static on(target, type, listener) {
31     if (typeof listener !== "function" && !isEventHandler(listener)) {
32       throw new Error(BAD_LISTENER);
33     }
35     if (!(eventListeners in target)) {
36       target[eventListeners] = new Map();
37     }
39     const events = target[eventListeners];
41     if (events.has(type)) {
42       events.get(type).add(listener);
43     } else {
44       events.set(type, new Set([listener]));
45     }
46   }
48   /**
49    * Removes an event `listener` for the given event `type` on the given event
50    * `target`. If no `listener` is passed removes all listeners of the given
51    * `type`. If `type` is not passed removes all the listeners of the given
52    * event `target`.
53    * @param {Object} target
54    *    The event target object.
55    * @param {String} [type]
56    *    The type of event.
57    * @param {Function|Object} [listener]
58    *    The listener that processes the event.
59    */
60   static off(target, type, listener) {
61     const length = arguments.length;
62     const events = target[eventListeners];
64     if (!events) {
65       return;
66     }
68     if (length === 3) {
69       // Trying to remove from the `target` the `listener` specified for the
70       // event's `type` given.
71       const listenersForType = events.get(type);
73       // If we don't have listeners for the event's type, we bail out.
74       if (!listenersForType) {
75         return;
76       }
78       // If the listeners list contains the listener given, we just remove it.
79       if (listenersForType.has(listener)) {
80         listenersForType.delete(listener);
81       } else {
82         // If it's not present, there is still the possibility that the listener
83         // have been added using `once`, since the method wraps the original listener
84         // in another function.
85         // So we iterate all the listeners to check if any of them is a wrapper to
86         // the `listener` given.
87         for (const value of listenersForType.values()) {
88           if (onceOriginalListener in value && value[onceOriginalListener] === listener) {
89             listenersForType.delete(value);
90             break;
91           }
92         }
93       }
94     } else if (length === 2) {
95       // No listener was given, it means we're removing all the listeners from
96       // the given event's `type`.
97       if (events.has(type)) {
98         events.delete(type);
99       }
100     } else if (length === 1) {
101       // With only the `target` given, we're removing all the isteners from the object.
102       events.clear();
103     }
104   }
106   /**
107    * Registers an event `listener` that is called only the next time an event
108    * of the specified `type` is emitted on the given event `target`.
109    * It returns a promised resolved once the specified event `type` is emitted.
110    *
111    * @param {Object} target
112    *    Event target object.
113    * @param {String} type
114    *    The type of the event.
115    * @param {Function|Object} [listener]
116    *    The listener that processes the event.
117    * @return {Promise}
118    *    The promise resolved once the event `type` is emitted.
119    */
120   static once(target, type, listener) {
121     return new Promise(resolve => {
122       // This is the actual listener that will be added to the target's listener, it wraps
123       // the call to the original `listener` given.
124       const newListener = (first, ...rest) => {
125         // To prevent side effects we're removing the listener upfront.
126         EventEmitter.off(target, type, newListener);
128         if (listener) {
129           if (isEventHandler(listener)) {
130             // if the `listener` given is actually an object that handles the events
131             // using `EventEmitter.handler`, we want to call that function, passing also
132             // the event's type as first argument, and the `listener` (the object) as
133             // contextual object.
134             listener[handler](type, first, ...rest);
135           } else {
136             // Otherwise we'll just call it
137             listener.call(target, first, ...rest);
138           }
139         }
141         // We resolve the promise once the listener is called.
142         resolve(first);
143       };
145       newListener[onceOriginalListener] = listener;
146       EventEmitter.on(target, type, newListener);
147     });
148   }
150   static emit(target, type, ...rest) {
151     logEvent(type, rest);
153     if (!(eventListeners in target)) {
154       return;
155     }
157     if (target[eventListeners].has(type)) {
158       // Creating a temporary Set with the original listeners, to avoiding side effects
159       // in emit.
160       const listenersForType = new Set(target[eventListeners].get(type));
162       for (const listener of listenersForType) {
163         // If the object was destroyed during event emission, stop emitting.
164         if (!(eventListeners in target)) {
165           break;
166         }
168         const events = target[eventListeners];
169         const listeners = events.get(type);
171         // If listeners were removed during emission, make sure the
172         // event handler we're going to fire wasn't removed.
173         if (listeners && listeners.has(listener)) {
174           try {
175             if (isEventHandler(listener)) {
176               listener[handler](type, ...rest);
177             } else {
178               listener.call(target, ...rest);
179             }
180           } catch (ex) {
181             // Prevent a bad listener from interfering with the others.
182             const msg = ex + ": " + ex.stack;
183             console.error(msg);
184             dump(msg + "\n");
185           }
186         }
187       }
188     }
190     // Backward compatibility with the SDK event-emitter: support wildcard listeners that
191     // will be called for any event. The arguments passed to the listener are the event
192     // type followed by the actual arguments.
193     // !!! This API will be removed by Bug 1391261.
194     const hasWildcardListeners = target[eventListeners].has("*");
195     if (type !== "*" && hasWildcardListeners) {
196       EventEmitter.emit(target, "*", type, ...rest);
197     }
198   }
200   /**
201    * Returns a number of event listeners registered for the given event `type`
202    * on the given event `target`.
203    *
204    * @param {Object} target
205    *    Event target object.
206    * @param {String} type
207    *    The type of event.
208    * @return {Number}
209    *    The number of event listeners.
210    */
211   static count(target, type) {
212     if (eventListeners in target) {
213       const listenersForType = target[eventListeners].get(type);
215       if (listenersForType) {
216         return listenersForType.size;
217       }
218     }
220     return 0;
221   }
223   /**
224    * Decorate an object with event emitter functionality; basically using the
225    * class' prototype as mixin.
226    *
227    * @param Object target
228    *    The object to decorate.
229    * @return Object
230    *    The object given, mixed.
231    */
232   static decorate(target) {
233     const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
234     delete descriptors.constructor;
235     return Object.defineProperties(target, descriptors);
236   }
238   static get handler() {
239     return handler;
240   }
242   on(...args) {
243     EventEmitter.on(this, ...args);
244   }
246   off(...args) {
247     EventEmitter.off(this, ...args);
248   }
250   once(...args) {
251     return EventEmitter.once(this, ...args);
252   }
254   emit(...args) {
255     EventEmitter.emit(this, ...args);
256   }
259 module.exports = EventEmitter;
261 const isEventHandler = (listener) =>
262   listener && handler in listener && typeof listener[handler] === "function";
264 const Services = require("Services");
265 const { getNthPathExcluding } = require("devtools/shared/platform/stack");
266 let loggingEnabled = false;
268 if (!isWorker) {
269   loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
270   Services.prefs.addObserver("devtools.dump.emit", {
271     observe: () => {
272       loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
273     }
274   });
277 function serialize(target) {
278   const MAXLEN = 60;
280   // Undefined
281   if (typeof target === "undefined") {
282     return "undefined";
283   }
285   if (target === null) {
286     return "null";
287   }
289   // Number / String
290   if (typeof target === "string" ||
291       typeof target === "number") {
292     return truncate(target, MAXLEN);
293   }
295   // HTML Node
296   if (target.nodeName) {
297     let out = target.nodeName;
299     if (target.id) {
300       out += "#" + target.id;
301     }
302     if (target.className) {
303       out += "." + target.className;
304     }
306     return out;
307   }
309   // Array
310   if (Array.isArray(target)) {
311     return truncate(target.toSource(), MAXLEN);
312   }
314   // Function
315   if (typeof target === "function") {
316     return `function ${target.name ? target.name : "anonymous"}()`;
317   }
319   // Window
320   if (target.constructor &&
321       target.constructor.name &&
322       target.constructor.name === "Window") {
323     return `window (${target.location.origin})`;
324   }
326   // Object
327   if (typeof target === "object") {
328     let out = "{";
330     const entries = Object.entries(target);
331     for (let i = 0; i < Math.min(10, entries.length); i++) {
332       const [name, value] = entries[i];
334       if (i > 0) {
335         out += ", ";
336       }
338       out += `${name}: ${truncate(value, MAXLEN)}`;
339     }
341     return out + "}";
342   }
344   // Other
345   return truncate(target.toSource(), MAXLEN);
348 function truncate(value, maxLen) {
349   // We don't use value.toString() because it can throw.
350   const str = String(value);
351   return str.length > maxLen ? str.substring(0, maxLen) + "..." : str;
354 function logEvent(type, args) {
355   if (!loggingEnabled) {
356     return;
357   }
359   let argsOut = "";
361   // We need this try / catch to prevent any dead object errors.
362   try {
363     argsOut = `${args.map(serialize).join(", ")}`;
364   } catch (e) {
365     // Object is dead so the toolbox is most likely shutting down,
366     // do nothing.
367   }
369   const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
371   if (args.length > 0) {
372     dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
373   } else {
374     dump(`EMITTING: emit(${type}) from ${path}\n`);
375   }