diff options
Diffstat (limited to 'toolkit/modules/EventEmitter.sys.mjs')
-rw-r--r-- | toolkit/modules/EventEmitter.sys.mjs | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/toolkit/modules/EventEmitter.sys.mjs b/toolkit/modules/EventEmitter.sys.mjs new file mode 100644 index 0000000000..5cc5964829 --- /dev/null +++ b/toolkit/modules/EventEmitter.sys.mjs @@ -0,0 +1,214 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + console: "resource://gre/modules/Console.sys.mjs", +}); + +export function EventEmitter() {} + +let loggingEnabled = Services.prefs.getBoolPref("toolkit.dump.emit"); +Services.prefs.addObserver("toolkit.dump.emit", { + observe: () => { + loggingEnabled = Services.prefs.getBoolPref("toolkit.dump.emit"); + }, +}); + +/** + * Decorate an object with event emitter functionality. + * + * @param Object objectToDecorate + * Bind all public methods of EventEmitter to + * the objectToDecorate object. + */ +EventEmitter.decorate = function(objectToDecorate) { + let emitter = new EventEmitter(); + objectToDecorate.on = emitter.on.bind(emitter); + objectToDecorate.off = emitter.off.bind(emitter); + objectToDecorate.once = emitter.once.bind(emitter); + objectToDecorate.emit = emitter.emit.bind(emitter); +}; + +function describeNthCaller(n) { + let caller = Components.stack; + // Do one extra iteration to skip this function. + while (n >= 0) { + --n; + caller = caller.caller; + } + + let func = caller.name; + let file = caller.filename; + if (file.includes(" -> ")) { + file = caller.filename.split(/ -> /)[1]; + } + let path = file + ":" + caller.lineNumber; + + return func + "() -> " + path; +} + +EventEmitter.prototype = { + /** + * Connect a listener. + * + * @param string event + * The event name to which we're connecting. + * @param function listener + * Called when the event is fired. + */ + on(event, listener) { + if (!this._eventEmitterListeners) { + this._eventEmitterListeners = new Map(); + } + if (!this._eventEmitterListeners.has(event)) { + this._eventEmitterListeners.set(event, []); + } + this._eventEmitterListeners.get(event).push(listener); + }, + + /** + * Listen for the next time an event is fired. + * + * @param string event + * The event name to which we're connecting. + * @param function listener + * (Optional) Called when the event is fired. Will be called at most + * one time. + * @return promise + * A promise which is resolved when the event next happens. The + * resolution value of the promise is the first event argument. If + * you need access to second or subsequent event arguments (it's rare + * that this is needed) then use listener + */ + once(event, listener) { + return new Promise(resolve => { + let handler = (_, first, ...rest) => { + this.off(event, handler); + if (listener) { + listener(event, first, ...rest); + } + resolve(first); + }; + + handler._originalListener = listener; + this.on(event, handler); + }); + }, + + /** + * Remove a previously-registered event listener. Works for events + * registered with either on or once. + * + * @param string event + * The event name whose listener we're disconnecting. + * @param function listener + * The listener to remove. + */ + off(event, listener) { + if (!this._eventEmitterListeners) { + return; + } + let listeners = this._eventEmitterListeners.get(event); + if (listeners) { + this._eventEmitterListeners.set( + event, + listeners.filter(l => { + return l !== listener && l._originalListener !== listener; + }) + ); + } + }, + + /** + * Emit an event. All arguments to this method will + * be sent to listener functions. + */ + emit(event) { + this.logEvent(event, arguments); + + if ( + !this._eventEmitterListeners || + !this._eventEmitterListeners.has(event) + ) { + return; + } + + let originalListeners = this._eventEmitterListeners.get(event); + for (let listener of this._eventEmitterListeners.get(event)) { + // If the object was destroyed during event emission, stop + // emitting. + if (!this._eventEmitterListeners) { + break; + } + + // If listeners were removed during emission, make sure the + // event handler we're going to fire wasn't removed. + if ( + originalListeners === this._eventEmitterListeners.get(event) || + this._eventEmitterListeners.get(event).some(l => l === listener) + ) { + try { + listener.apply(null, arguments); + } catch (ex) { + // Prevent a bad listener from interfering with the others. + let msg = ex + ": " + ex.stack; + lazy.console.error(msg); + if (loggingEnabled) { + dump(msg + "\n"); + } + } + } + } + }, + + logEvent(event, args) { + if (!loggingEnabled) { + return; + } + + let description = describeNthCaller(2); + + let argOut = "("; + if (args.length === 1) { + argOut += event; + } + + let out = "EMITTING: "; + + // We need this try / catch to prevent any dead object errors. + try { + for (let i = 1; i < args.length; i++) { + if (i === 1) { + argOut = "(" + event + ", "; + } else { + argOut += ", "; + } + + let arg = args[i]; + argOut += arg; + + if (arg && arg.nodeName) { + argOut += " (" + arg.nodeName; + if (arg.id) { + argOut += "#" + arg.id; + } + if (arg.className) { + argOut += "." + arg.className; + } + argOut += ")"; + } + } + } catch (e) { + // Object is dead so the toolbox is most likely shutting down, + // do nothing. + } + + argOut += ")"; + out += "emit" + argOut + " from " + description + "\n"; + + dump(out); + }, +}; |