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 this[eventListeners] = new Map();
22 * Registers an event `listener` that is called every time events of
23 * specified `type` is emitted on the given event `target`.
25 * @param {Object} target
26 * Event target object.
27 * @param {String} type
29 * @param {Function|Object} listener
30 * The listener that processes the event.
32 * A function that removes the listener when called.
34 static on(target, type, listener) {
35 if (typeof listener !== "function" && !isEventHandler(listener)) {
36 throw new Error(BAD_LISTENER);
39 if (!(eventListeners in target)) {
40 target[eventListeners] = new Map();
43 const events = target[eventListeners];
45 if (events.has(type)) {
46 events.get(type).add(listener);
48 events.set(type, new Set([listener]));
51 return () => EventEmitter.off(target, type, listener);
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
59 * @param {Object} target
60 * The event target object.
61 * @param {String} [type]
63 * @param {Function|Object} [listener]
64 * The listener that processes the event.
66 static off(target, type, listener) {
67 const length = arguments.length;
68 const events = target[eventListeners];
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) {
84 // If the listeners list contains the listener given, we just remove it.
85 if (listenersForType.has(listener)) {
86 listenersForType.delete(listener);
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()) {
95 onceOriginalListener in value &&
96 value[onceOriginalListener] === listener
98 listenersForType.delete(value);
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)) {
109 } else if (length === 1) {
110 // With only the `target` given, we're removing all the listeners from the object.
115 static clearEvents(target) {
116 const events = target[eventListeners];
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.
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.
135 * The promise resolved once the event `type` is emitted.
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);
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);
154 // Otherwise we'll just call it
155 rv = listener.call(target, first, ...rest);
159 // We resolve the promise once the listener is called.
162 // Listeners may return a promise, so pass it along
166 newListener[onceOriginalListener] = listener;
167 EventEmitter.on(target, type, newListener);
171 static emit(target, type, ...rest) {
172 EventEmitter._emit(target, type, false, ...rest);
175 static emitAsync(target, type, ...rest) {
176 return EventEmitter._emit(target, type, true, ...rest);
180 * Emit an event of a given `type` on a given `target` object.
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;
195 static _emit(target, type, async, ...rest) {
196 logEvent(type, rest);
198 if (!(eventListeners in target)) {
202 const promises = async ? [] : null;
204 if (target[eventListeners].has(type)) {
205 // Creating a temporary Set with the original listeners, to avoiding side effects
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)) {
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)) {
223 if (isEventHandler(listener)) {
224 promise = listener[handler](type, ...rest);
226 promise = listener.call(target, ...rest);
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") {
233 `Listener for event '${type}' did not return a promise.`
236 promises.push(promise);
240 // Prevent a bad listener from interfering with the others.
242 const msg = ex + ": " + ex.stack;
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);
259 return Promise.all(promises);
266 * Returns a number of event listeners registered for the given event `type`
267 * on the given event `target`.
269 * @param {Object} target
270 * Event target object.
271 * @param {String} type
274 * The number of event listeners.
276 static count(target, type) {
277 if (eventListeners in target) {
278 const listenersForType = target[eventListeners].get(type);
280 if (listenersForType) {
281 return listenersForType.size;
289 * Decorate an object with event emitter functionality; basically using the
290 * class' prototype as mixin.
292 * @param Object target
293 * The object to decorate.
295 * The object given, mixed.
297 static decorate(target) {
298 const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
299 delete descriptors.constructor;
300 return Object.defineProperties(target, descriptors);
303 static get handler() {
308 return EventEmitter.on(this, ...args);
312 EventEmitter.off(this, ...args);
316 EventEmitter.clearEvents(this);
320 return EventEmitter.once(this, ...args);
324 EventEmitter.emit(this, ...args);
328 return EventEmitter.emitAsync(this, ...args);
331 emitForTests(...args) {
333 EventEmitter.emit(this, ...args);
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;
348 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
351 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
354 Services.prefs.addObserver("devtools.dump.emit", observer);
356 // Also listen for Loader unload to unregister the pref observer and
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");
364 Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
367 function serialize(target) {
371 if (typeof target === "undefined") {
375 if (target === null) {
380 if (typeof target === "string" || typeof target === "number") {
381 return truncate(target, MAXLEN);
385 if (target.nodeName) {
386 let out = target.nodeName;
389 out += "#" + target.id;
391 if (target.className) {
392 out += "." + target.className;
399 if (Array.isArray(target)) {
400 return truncate(target.toSource(), MAXLEN);
404 if (typeof target === "function") {
405 return `function ${target.name ? target.name : "anonymous"}()`;
410 target.constructor &&
411 target.constructor.name &&
412 target.constructor.name === "Window"
414 return `window (${target.location.origin})`;
418 if (typeof target === "object") {
421 const entries = Object.entries(target);
422 for (let i = 0; i < Math.min(10, entries.length); i++) {
423 const [name, value] = entries[i];
429 out += `${name}: ${truncate(value, MAXLEN)}`;
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) {
452 // We need this try / catch to prevent any dead object errors.
454 argsOut = `${args.map(serialize).join(", ")}`;
456 // Object is dead so the toolbox is most likely shutting down,
460 const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
462 if (args.length > 0) {
463 dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
465 dump(`EMITTING: emit(${type}) from ${path}\n`);