michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * EventEmitter. michael@0: */ michael@0: michael@0: this.EventEmitter = function EventEmitter() {}; michael@0: michael@0: if (typeof(require) === "function") { michael@0: module.exports = EventEmitter; michael@0: var {Cu, components} = require("chrome"); michael@0: } else { michael@0: var EXPORTED_SYMBOLS = ["EventEmitter"]; michael@0: var Cu = this["Components"].utils; michael@0: var components = Components; michael@0: } michael@0: michael@0: const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: const { Services } = Cu.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: /** michael@0: * Decorate an object with event emitter functionality. michael@0: * michael@0: * @param Object aObjectToDecorate michael@0: * Bind all public methods of EventEmitter to michael@0: * the aObjectToDecorate object. michael@0: */ michael@0: EventEmitter.decorate = function EventEmitter_decorate (aObjectToDecorate) { michael@0: let emitter = new EventEmitter(); michael@0: aObjectToDecorate.on = emitter.on.bind(emitter); michael@0: aObjectToDecorate.off = emitter.off.bind(emitter); michael@0: aObjectToDecorate.once = emitter.once.bind(emitter); michael@0: aObjectToDecorate.emit = emitter.emit.bind(emitter); michael@0: }; michael@0: michael@0: EventEmitter.prototype = { michael@0: /** michael@0: * Connect a listener. michael@0: * michael@0: * @param string aEvent michael@0: * The event name to which we're connecting. michael@0: * @param function aListener michael@0: * Called when the event is fired. michael@0: */ michael@0: on: function EventEmitter_on(aEvent, aListener) { michael@0: if (!this._eventEmitterListeners) michael@0: this._eventEmitterListeners = new Map(); michael@0: if (!this._eventEmitterListeners.has(aEvent)) { michael@0: this._eventEmitterListeners.set(aEvent, []); michael@0: } michael@0: this._eventEmitterListeners.get(aEvent).push(aListener); michael@0: }, michael@0: michael@0: /** michael@0: * Listen for the next time an event is fired. michael@0: * michael@0: * @param string aEvent michael@0: * The event name to which we're connecting. michael@0: * @param function aListener michael@0: * (Optional) Called when the event is fired. Will be called at most michael@0: * one time. michael@0: * @return promise michael@0: * A promise which is resolved when the event next happens. The michael@0: * resolution value of the promise is the first event argument. If michael@0: * you need access to second or subsequent event arguments (it's rare michael@0: * that this is needed) then use aListener michael@0: */ michael@0: once: function EventEmitter_once(aEvent, aListener) { michael@0: let deferred = promise.defer(); michael@0: michael@0: let handler = function(aEvent, aFirstArg) { michael@0: this.off(aEvent, handler); michael@0: if (aListener) { michael@0: aListener.apply(null, arguments); michael@0: } michael@0: deferred.resolve(aFirstArg); michael@0: }.bind(this); michael@0: michael@0: handler._originalListener = aListener; michael@0: this.on(aEvent, handler); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Remove a previously-registered event listener. Works for events michael@0: * registered with either on or once. michael@0: * michael@0: * @param string aEvent michael@0: * The event name whose listener we're disconnecting. michael@0: * @param function aListener michael@0: * The listener to remove. michael@0: */ michael@0: off: function EventEmitter_off(aEvent, aListener) { michael@0: if (!this._eventEmitterListeners) michael@0: return; michael@0: let listeners = this._eventEmitterListeners.get(aEvent); michael@0: if (listeners) { michael@0: this._eventEmitterListeners.set(aEvent, listeners.filter(l => { michael@0: return l !== aListener && l._originalListener !== aListener; michael@0: })); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Emit an event. All arguments to this method will michael@0: * be sent to listner functions. michael@0: */ michael@0: emit: function EventEmitter_emit(aEvent) { michael@0: this.logEvent(aEvent, arguments); michael@0: michael@0: if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent)) { michael@0: return; michael@0: } michael@0: michael@0: let originalListeners = this._eventEmitterListeners.get(aEvent); michael@0: for (let listener of this._eventEmitterListeners.get(aEvent)) { michael@0: // If the object was destroyed during event emission, stop michael@0: // emitting. michael@0: if (!this._eventEmitterListeners) { michael@0: break; michael@0: } michael@0: michael@0: // If listeners were removed during emission, make sure the michael@0: // event handler we're going to fire wasn't removed. michael@0: if (originalListeners === this._eventEmitterListeners.get(aEvent) || michael@0: this._eventEmitterListeners.get(aEvent).some(function(l) l === listener)) { michael@0: try { michael@0: listener.apply(null, arguments); michael@0: } michael@0: catch (ex) { michael@0: // Prevent a bad listener from interfering with the others. michael@0: let msg = ex + ": " + ex.stack; michael@0: Cu.reportError(msg); michael@0: dump(msg + "\n"); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: logEvent: function(aEvent, args) { michael@0: let logging = Services.prefs.getBoolPref("devtools.dump.emit"); michael@0: michael@0: if (logging) { michael@0: let caller = components.stack.caller.caller; michael@0: let func = caller.name; michael@0: let path = caller.filename.split(/ -> /)[1] + ":" + caller.lineNumber; michael@0: michael@0: let argOut = "("; michael@0: if (args.length === 1) { michael@0: argOut += aEvent; michael@0: } michael@0: michael@0: let out = "EMITTING: "; michael@0: michael@0: // We need this try / catch to prevent any dead object errors. michael@0: try { michael@0: for (let i = 1; i < args.length; i++) { michael@0: if (i === 1) { michael@0: argOut = "(" + aEvent + ", "; michael@0: } else { michael@0: argOut += ", "; michael@0: } michael@0: michael@0: let arg = args[i]; michael@0: argOut += arg; michael@0: michael@0: if (arg && arg.nodeName) { michael@0: argOut += " (" + arg.nodeName; michael@0: if (arg.id) { michael@0: argOut += "#" + arg.id; michael@0: } michael@0: if (arg.className) { michael@0: argOut += "." + arg.className; michael@0: } michael@0: argOut += ")"; michael@0: } michael@0: } michael@0: } catch(e) { michael@0: // Object is dead so the toolbox is most likely shutting down, michael@0: // do nothing. michael@0: } michael@0: michael@0: argOut += ")"; michael@0: out += "emit" + argOut + " from " + func + "() -> " + path + "\n"; michael@0: michael@0: dump(out); michael@0: } michael@0: }, michael@0: };