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