Bug 1887594 - Fix saveTabToExistingCollectionFromMainMenuTest UI test r=aaronmt
[gecko.git] / toolkit / modules / EventEmitter.sys.mjs
blob90900e22e5bad60f92badc085952ca0c9edd22ed
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 export function EventEmitter() {}
7 let loggingEnabled = Services.prefs.getBoolPref("toolkit.dump.emit");
8 Services.prefs.addObserver("toolkit.dump.emit", {
9   observe: () => {
10     loggingEnabled = Services.prefs.getBoolPref("toolkit.dump.emit");
11   },
12 });
14 /**
15  * Decorate an object with event emitter functionality.
16  *
17  * @param Object objectToDecorate
18  *        Bind all public methods of EventEmitter to
19  *        the objectToDecorate object.
20  */
21 EventEmitter.decorate = function (objectToDecorate) {
22   let emitter = new EventEmitter();
23   objectToDecorate.on = emitter.on.bind(emitter);
24   objectToDecorate.off = emitter.off.bind(emitter);
25   objectToDecorate.once = emitter.once.bind(emitter);
26   objectToDecorate.emit = emitter.emit.bind(emitter);
29 function describeNthCaller(n) {
30   let caller = Components.stack;
31   // Do one extra iteration to skip this function.
32   while (n >= 0) {
33     --n;
34     caller = caller.caller;
35   }
37   let func = caller.name;
38   let file = caller.filename;
39   if (file.includes(" -> ")) {
40     file = caller.filename.split(/ -> /)[1];
41   }
42   let path = file + ":" + caller.lineNumber;
44   return func + "() -> " + path;
47 EventEmitter.prototype = {
48   /**
49    * Connect a listener.
50    *
51    * @param string event
52    *        The event name to which we're connecting.
53    * @param function listener
54    *        Called when the event is fired.
55    */
56   on(event, listener) {
57     if (!this._eventEmitterListeners) {
58       this._eventEmitterListeners = new Map();
59     }
60     if (!this._eventEmitterListeners.has(event)) {
61       this._eventEmitterListeners.set(event, []);
62     }
63     this._eventEmitterListeners.get(event).push(listener);
64   },
66   /**
67    * Listen for the next time an event is fired.
68    *
69    * @param string event
70    *        The event name to which we're connecting.
71    * @param function listener
72    *        (Optional) Called when the event is fired. Will be called at most
73    *        one time.
74    * @return promise
75    *        A promise which is resolved when the event next happens. The
76    *        resolution value of the promise is the first event argument. If
77    *        you need access to second or subsequent event arguments (it's rare
78    *        that this is needed) then use listener
79    */
80   once(event, listener) {
81     return new Promise(resolve => {
82       let handler = (_, first, ...rest) => {
83         this.off(event, handler);
84         if (listener) {
85           listener(event, first, ...rest);
86         }
87         resolve(first);
88       };
90       handler._originalListener = listener;
91       this.on(event, handler);
92     });
93   },
95   /**
96    * Remove a previously-registered event listener.  Works for events
97    * registered with either on or once.
98    *
99    * @param string event
100    *        The event name whose listener we're disconnecting.
101    * @param function listener
102    *        The listener to remove.
103    */
104   off(event, listener) {
105     if (!this._eventEmitterListeners) {
106       return;
107     }
108     let listeners = this._eventEmitterListeners.get(event);
109     if (listeners) {
110       this._eventEmitterListeners.set(
111         event,
112         listeners.filter(l => {
113           return l !== listener && l._originalListener !== listener;
114         })
115       );
116     }
117   },
119   /**
120    * Emit an event.  All arguments to this method will
121    * be sent to listener functions.
122    */
123   emit(event) {
124     this.logEvent(event, arguments);
126     if (
127       !this._eventEmitterListeners ||
128       !this._eventEmitterListeners.has(event)
129     ) {
130       return;
131     }
133     let originalListeners = this._eventEmitterListeners.get(event);
134     for (let listener of this._eventEmitterListeners.get(event)) {
135       // If the object was destroyed during event emission, stop
136       // emitting.
137       if (!this._eventEmitterListeners) {
138         break;
139       }
141       // If listeners were removed during emission, make sure the
142       // event handler we're going to fire wasn't removed.
143       if (
144         originalListeners === this._eventEmitterListeners.get(event) ||
145         this._eventEmitterListeners.get(event).some(l => l === listener)
146       ) {
147         try {
148           listener.apply(null, arguments);
149         } catch (ex) {
150           console.error(ex);
151         }
152       }
153     }
154   },
156   logEvent(event, args) {
157     if (!loggingEnabled) {
158       return;
159     }
161     let description = describeNthCaller(2);
163     let argOut = "(";
164     if (args.length === 1) {
165       argOut += event;
166     }
168     let out = "EMITTING: ";
170     // We need this try / catch to prevent any dead object errors.
171     try {
172       for (let i = 1; i < args.length; i++) {
173         if (i === 1) {
174           argOut = "(" + event + ", ";
175         } else {
176           argOut += ", ";
177         }
179         let arg = args[i];
180         argOut += arg;
182         if (arg && arg.nodeName) {
183           argOut += " (" + arg.nodeName;
184           if (arg.id) {
185             argOut += "#" + arg.id;
186           }
187           if (arg.className) {
188             argOut += "." + arg.className;
189           }
190           argOut += ")";
191         }
192       }
193     } catch (e) {
194       // Object is dead so the toolbox is most likely shutting down,
195       // do nothing.
196     }
198     argOut += ")";
199     out += "emit" + argOut + " from " + description + "\n";
201     dump(out);
202   },