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/. */
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");
18 * Registers an event `listener` that is called every time events of
19 * specified `type` is emitted on the given event `target`.
21 * @param {Object} target
22 * Event target object.
23 * @param {String} type
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
31 * A function that removes the listener when called.
33 static on(target, type, listener, { signal } = {}) {
34 if (typeof listener !== "function" && !isEventHandler(listener)) {
35 throw new Error(BAD_LISTENER);
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.
44 if (!(eventListeners in target)) {
45 target[eventListeners] = new Map();
48 const events = target[eventListeners];
50 if (events.has(type)) {
51 events.get(type).add(listener);
53 events.set(type, new Set([listener]));
56 const offFn = () => EventEmitter.off(target, type, listener);
59 signal.addEventListener("abort", offFn, { once: true });
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
70 * @param {Object} target
71 * The event target object.
72 * @param {String} [type]
74 * @param {Function|Object} [listener]
75 * The listener that processes the event.
77 static off(target, type, listener) {
78 const length = arguments.length;
79 const events = target[eventListeners];
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) {
95 // If the listeners list contains the listener given, we just remove it.
96 if (listenersForType.has(listener)) {
97 listenersForType.delete(listener);
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()) {
106 onceOriginalListener in value &&
107 value[onceOriginalListener] === listener
109 listenersForType.delete(value);
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)) {
120 } else if (length === 1) {
121 // With only the `target` given, we're removing all the listeners from the object.
126 static clearEvents(target) {
127 const events = target[eventListeners];
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.
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
149 * The promise resolved once the event `type` is emitted.
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);
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);
168 // Otherwise we'll just call it
169 rv = listener.call(target, first, ...rest);
173 // We resolve the promise once the listener is called.
176 // Listeners may return a promise, so pass it along
180 newListener[onceOriginalListener] = listener;
181 EventEmitter.on(target, type, newListener, options);
185 static emit(target, type, ...rest) {
186 EventEmitter._emit(target, type, false, rest);
189 static emitAsync(target, type, ...rest) {
190 return EventEmitter._emit(target, type, true, rest);
194 * Emit an event of a given `type` on a given `target` object.
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;
209 static _emit(target, type, async, args) {
210 if (loggingEnabled) {
211 logEvent(type, args);
214 const targetEventListeners = target[eventListeners];
215 if (!targetEventListeners) {
219 const listeners = targetEventListeners.get(type);
220 if (!listeners?.size) {
224 const promises = async ? [] : null;
226 // Creating a temporary Set with the original listeners, to avoiding side effects
228 for (const listener of new Set(listeners)) {
229 // If the object was destroyed during event emission, stop emitting.
230 if (!(eventListeners in target)) {
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)) {
239 if (isEventHandler(listener)) {
240 promise = listener[handler](type, ...args);
242 promise = listener.apply(target, args);
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") {
249 `Listener for event '${type}' did not return a promise.`
252 promises.push(promise);
256 // Prevent a bad listener from interfering with the others.
258 const msg = ex + ": " + ex.stack;
265 return Promise.all(promises);
272 * Returns a number of event listeners registered for the given event `type`
273 * on the given event `target`.
275 * @param {Object} target
276 * Event target object.
277 * @param {String} type
280 * The number of event listeners.
282 static count(target, type) {
283 if (eventListeners in target) {
284 const listenersForType = target[eventListeners].get(type);
286 if (listenersForType) {
287 return listenersForType.size;
295 * Decorate an object with event emitter functionality; basically using the
296 * class' prototype as mixin.
298 * @param Object target
299 * The object to decorate.
301 * The object given, mixed.
303 static decorate(target) {
304 const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
305 delete descriptors.constructor;
306 return Object.defineProperties(target, descriptors);
309 static get handler() {
314 return EventEmitter.on(this, ...args);
318 EventEmitter.off(this, ...args);
322 EventEmitter.clearEvents(this);
326 return EventEmitter.once(this, ...args);
330 EventEmitter.emit(this, ...args);
334 return EventEmitter.emitAsync(this, ...args);
337 emitForTests(...args) {
339 EventEmitter.emit(this, ...args);
344 return EventEmitter.count(this, ...args);
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;
358 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false);
361 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
364 Services.prefs.addObserver("devtools.dump.emit", observer);
366 // Also listen for Loader unload to unregister the pref observer and
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");
374 Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
377 function serialize(target) {
381 if (typeof target === "undefined") {
385 if (target === null) {
390 if (typeof target === "string" || typeof target === "number") {
391 return truncate(target, MAXLEN);
395 if (target.nodeName) {
396 let out = target.nodeName;
399 out += "#" + target.id;
401 if (target.className) {
402 out += "." + target.className;
409 if (Array.isArray(target)) {
410 return truncate(target.toSource(), MAXLEN);
414 if (typeof target === "function") {
415 return `function ${target.name ? target.name : "anonymous"}()`;
419 if (target?.constructor?.name === "Window") {
420 return `window (${target.location.origin})`;
424 if (typeof target === "object") {
427 const entries = Object.entries(target);
428 for (let i = 0; i < Math.min(10, entries.length); i++) {
429 const [name, value] = entries[i];
435 out += `${name}: ${truncate(value, MAXLEN)}`;
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) {
454 // We need this try / catch to prevent any dead object errors.
456 argsOut = `${args.map(serialize).join(", ")}`;
458 // Object is dead so the toolbox is most likely shutting down,
462 const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
464 if (args.length > 0) {
465 dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
467 dump(`EMITTING: emit(${type}) from ${path}\n`);