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/. */
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");
16 this[eventListeners] = new Map();
20 * Registers an event `listener` that is called every time events of
21 * specified `type` is emitted on the given event `target`.
23 * @param {Object} target
24 * Event target object.
25 * @param {String} type
27 * @param {Function|Object} listener
28 * The listener that processes the event.
30 static on(target, type, listener) {
31 if (typeof listener !== "function" && !isEventHandler(listener)) {
32 throw new Error(BAD_LISTENER);
35 if (!(eventListeners in target)) {
36 target[eventListeners] = new Map();
39 const events = target[eventListeners];
41 if (events.has(type)) {
42 events.get(type).add(listener);
44 events.set(type, new Set([listener]));
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
53 * @param {Object} target
54 * The event target object.
55 * @param {String} [type]
57 * @param {Function|Object} [listener]
58 * The listener that processes the event.
60 static off(target, type, listener) {
61 const length = arguments.length;
62 const events = target[eventListeners];
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) {
78 // If the listeners list contains the listener given, we just remove it.
79 if (listenersForType.has(listener)) {
80 listenersForType.delete(listener);
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);
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)) {
100 } else if (length === 1) {
101 // With only the `target` given, we're removing all the isteners from the object.
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.
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.
118 * The promise resolved once the event `type` is emitted.
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);
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);
136 // Otherwise we'll just call it
137 listener.call(target, first, ...rest);
141 // We resolve the promise once the listener is called.
145 newListener[onceOriginalListener] = listener;
146 EventEmitter.on(target, type, newListener);
150 static emit(target, type, ...rest) {
151 logEvent(type, rest);
153 if (!(eventListeners in target)) {
157 if (target[eventListeners].has(type)) {
158 // Creating a temporary Set with the original listeners, to avoiding side effects
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)) {
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)) {
175 if (isEventHandler(listener)) {
176 listener[handler](type, ...rest);
178 listener.call(target, ...rest);
181 // Prevent a bad listener from interfering with the others.
182 const msg = ex + ": " + ex.stack;
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);
201 * Returns a number of event listeners registered for the given event `type`
202 * on the given event `target`.
204 * @param {Object} target
205 * Event target object.
206 * @param {String} type
209 * The number of event listeners.
211 static count(target, type) {
212 if (eventListeners in target) {
213 const listenersForType = target[eventListeners].get(type);
215 if (listenersForType) {
216 return listenersForType.size;
224 * Decorate an object with event emitter functionality; basically using the
225 * class' prototype as mixin.
227 * @param Object target
228 * The object to decorate.
230 * The object given, mixed.
232 static decorate(target) {
233 const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
234 delete descriptors.constructor;
235 return Object.defineProperties(target, descriptors);
238 static get handler() {
243 EventEmitter.on(this, ...args);
247 EventEmitter.off(this, ...args);
251 return EventEmitter.once(this, ...args);
255 EventEmitter.emit(this, ...args);
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;
269 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
270 Services.prefs.addObserver("devtools.dump.emit", {
272 loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
277 function serialize(target) {
281 if (typeof target === "undefined") {
285 if (target === null) {
290 if (typeof target === "string" ||
291 typeof target === "number") {
292 return truncate(target, MAXLEN);
296 if (target.nodeName) {
297 let out = target.nodeName;
300 out += "#" + target.id;
302 if (target.className) {
303 out += "." + target.className;
310 if (Array.isArray(target)) {
311 return truncate(target.toSource(), MAXLEN);
315 if (typeof target === "function") {
316 return `function ${target.name ? target.name : "anonymous"}()`;
320 if (target.constructor &&
321 target.constructor.name &&
322 target.constructor.name === "Window") {
323 return `window (${target.location.origin})`;
327 if (typeof target === "object") {
330 const entries = Object.entries(target);
331 for (let i = 0; i < Math.min(10, entries.length); i++) {
332 const [name, value] = entries[i];
338 out += `${name}: ${truncate(value, MAXLEN)}`;
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) {
361 // We need this try / catch to prevent any dead object errors.
363 argsOut = `${args.map(serialize).join(", ")}`;
365 // Object is dead so the toolbox is most likely shutting down,
369 const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
371 if (args.length > 0) {
372 dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
374 dump(`EMITTING: emit(${type}) from ${path}\n`);