mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			470 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			470 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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/. */
 | 
						||
 | 
						||
"use strict";
 | 
						||
 | 
						||
const BAD_LISTENER =
 | 
						||
  "The event listener must be a function, or an object that has " +
 | 
						||
  "`EventEmitter.handler` Symbol.";
 | 
						||
 | 
						||
const eventListeners = Symbol("EventEmitter/listeners");
 | 
						||
const onceOriginalListener = Symbol("EventEmitter/once-original-listener");
 | 
						||
const handler = Symbol("EventEmitter/event-handler");
 | 
						||
loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js");
 | 
						||
 | 
						||
class EventEmitter {
 | 
						||
  /**
 | 
						||
   * Registers an event `listener` that is called every time events of
 | 
						||
   * specified `type` is emitted on the given event `target`.
 | 
						||
   *
 | 
						||
   * @param {Object} target
 | 
						||
   *    Event target object.
 | 
						||
   * @param {String} type
 | 
						||
   *    The type of event.
 | 
						||
   * @param {Function|Object} listener
 | 
						||
   *    The listener that processes the event.
 | 
						||
   * @param {Object} options
 | 
						||
   * @param {AbortSignal} options.signal
 | 
						||
   *     The listener will be removed when linked AbortController’s abort() method is called
 | 
						||
   * @returns {Function}
 | 
						||
   *    A function that removes the listener when called.
 | 
						||
   */
 | 
						||
  static on(target, type, listener, { signal } = {}) {
 | 
						||
    if (typeof listener !== "function" && !isEventHandler(listener)) {
 | 
						||
      throw new Error(BAD_LISTENER);
 | 
						||
    }
 | 
						||
 | 
						||
    if (signal?.aborted === true) {
 | 
						||
      // The signal is already aborted so don't setup the listener.
 | 
						||
      // We return an empty function as it's the expected returned value.
 | 
						||
      return () => {};
 | 
						||
    }
 | 
						||
 | 
						||
    if (!(eventListeners in target)) {
 | 
						||
      target[eventListeners] = new Map();
 | 
						||
    }
 | 
						||
 | 
						||
    const events = target[eventListeners];
 | 
						||
 | 
						||
    if (events.has(type)) {
 | 
						||
      events.get(type).add(listener);
 | 
						||
    } else {
 | 
						||
      events.set(type, new Set([listener]));
 | 
						||
    }
 | 
						||
 | 
						||
    const offFn = () => EventEmitter.off(target, type, listener);
 | 
						||
 | 
						||
    if (signal) {
 | 
						||
      signal.addEventListener("abort", offFn, { once: true });
 | 
						||
    }
 | 
						||
 | 
						||
    return offFn;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Removes an event `listener` for the given event `type` on the given event
 | 
						||
   * `target`. If no `listener` is passed removes all listeners of the given
 | 
						||
   * `type`. If `type` is not passed removes all the listeners of the given
 | 
						||
   * event `target`.
 | 
						||
   * @param {Object} target
 | 
						||
   *    The event target object.
 | 
						||
   * @param {String} [type]
 | 
						||
   *    The type of event.
 | 
						||
   * @param {Function|Object} [listener]
 | 
						||
   *    The listener that processes the event.
 | 
						||
   */
 | 
						||
  static off(target, type, listener) {
 | 
						||
    const length = arguments.length;
 | 
						||
    const events = target[eventListeners];
 | 
						||
 | 
						||
    if (!events) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    if (length >= 3) {
 | 
						||
      // Trying to remove from the `target` the `listener` specified for the
 | 
						||
      // event's `type` given.
 | 
						||
      const listenersForType = events.get(type);
 | 
						||
 | 
						||
      // If we don't have listeners for the event's type, we bail out.
 | 
						||
      if (!listenersForType) {
 | 
						||
        return;
 | 
						||
      }
 | 
						||
 | 
						||
      // If the listeners list contains the listener given, we just remove it.
 | 
						||
      if (listenersForType.has(listener)) {
 | 
						||
        listenersForType.delete(listener);
 | 
						||
      } else {
 | 
						||
        // If it's not present, there is still the possibility that the listener
 | 
						||
        // have been added using `once`, since the method wraps the original listener
 | 
						||
        // in another function.
 | 
						||
        // So we iterate all the listeners to check if any of them is a wrapper to
 | 
						||
        // the `listener` given.
 | 
						||
        for (const value of listenersForType.values()) {
 | 
						||
          if (
 | 
						||
            onceOriginalListener in value &&
 | 
						||
            value[onceOriginalListener] === listener
 | 
						||
          ) {
 | 
						||
            listenersForType.delete(value);
 | 
						||
            break;
 | 
						||
          }
 | 
						||
        }
 | 
						||
      }
 | 
						||
    } else if (length === 2) {
 | 
						||
      // No listener was given, it means we're removing all the listeners from
 | 
						||
      // the given event's `type`.
 | 
						||
      if (events.has(type)) {
 | 
						||
        events.delete(type);
 | 
						||
      }
 | 
						||
    } else if (length === 1) {
 | 
						||
      // With only the `target` given, we're removing all the listeners from the object.
 | 
						||
      events.clear();
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  static clearEvents(target) {
 | 
						||
    const events = target[eventListeners];
 | 
						||
    if (!events) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    events.clear();
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Registers an event `listener` that is called only the next time an event
 | 
						||
   * of the specified `type` is emitted on the given event `target`.
 | 
						||
   * It returns a Promise resolved once the specified event `type` is emitted.
 | 
						||
   *
 | 
						||
   * @param {Object} target
 | 
						||
   *    Event target object.
 | 
						||
   * @param {String} type
 | 
						||
   *    The type of the event.
 | 
						||
   * @param {Function|Object} [listener]
 | 
						||
   *    The listener that processes the event.
 | 
						||
   * @param {Object} options
 | 
						||
   * @param {AbortSignal} options.signal
 | 
						||
   *     The listener will be removed when linked AbortController’s abort() method is called
 | 
						||
   * @return {Promise}
 | 
						||
   *    The promise resolved once the event `type` is emitted.
 | 
						||
   */
 | 
						||
  static once(target, type, listener, options) {
 | 
						||
    return new Promise(resolve => {
 | 
						||
      // This is the actual listener that will be added to the target's listener, it wraps
 | 
						||
      // the call to the original `listener` given.
 | 
						||
      const newListener = (first, ...rest) => {
 | 
						||
        // To prevent side effects we're removing the listener upfront.
 | 
						||
        EventEmitter.off(target, type, newListener);
 | 
						||
 | 
						||
        let rv;
 | 
						||
        if (listener) {
 | 
						||
          if (isEventHandler(listener)) {
 | 
						||
            // if the `listener` given is actually an object that handles the events
 | 
						||
            // using `EventEmitter.handler`, we want to call that function, passing also
 | 
						||
            // the event's type as first argument, and the `listener` (the object) as
 | 
						||
            // contextual object.
 | 
						||
            rv = listener[handler](type, first, ...rest);
 | 
						||
          } else {
 | 
						||
            // Otherwise we'll just call it
 | 
						||
            rv = listener.call(target, first, ...rest);
 | 
						||
          }
 | 
						||
        }
 | 
						||
 | 
						||
        // We resolve the promise once the listener is called.
 | 
						||
        resolve(first);
 | 
						||
 | 
						||
        // Listeners may return a promise, so pass it along
 | 
						||
        return rv;
 | 
						||
      };
 | 
						||
 | 
						||
      newListener[onceOriginalListener] = listener;
 | 
						||
      EventEmitter.on(target, type, newListener, options);
 | 
						||
    });
 | 
						||
  }
 | 
						||
 | 
						||
  static emit(target, type, ...rest) {
 | 
						||
    EventEmitter._emit(target, type, false, rest);
 | 
						||
  }
 | 
						||
 | 
						||
  static emitAsync(target, type, ...rest) {
 | 
						||
    return EventEmitter._emit(target, type, true, rest);
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Emit an event of a given `type` on a given `target` object.
 | 
						||
   *
 | 
						||
   * @param {Object} target
 | 
						||
   *    Event target object.
 | 
						||
   * @param {String} type
 | 
						||
   *    The type of the event.
 | 
						||
   * @param {Boolean} async
 | 
						||
   *    If true, this function will wait for each listener completion.
 | 
						||
   *    Each listener has to return a promise, which will be awaited for.
 | 
						||
   * @param {Array} args
 | 
						||
   *    The arguments to pass to each listener function.
 | 
						||
   * @return {Promise|undefined}
 | 
						||
   *    If `async` argument is true, returns the promise resolved once all listeners have resolved.
 | 
						||
   *    Otherwise, this function returns undefined;
 | 
						||
   */
 | 
						||
  static _emit(target, type, async, args) {
 | 
						||
    if (loggingEnabled) {
 | 
						||
      logEvent(type, args);
 | 
						||
    }
 | 
						||
 | 
						||
    const targetEventListeners = target[eventListeners];
 | 
						||
    if (!targetEventListeners) {
 | 
						||
      return undefined;
 | 
						||
    }
 | 
						||
 | 
						||
    const listeners = targetEventListeners.get(type);
 | 
						||
    if (!listeners?.size) {
 | 
						||
      return undefined;
 | 
						||
    }
 | 
						||
 | 
						||
    const promises = async ? [] : null;
 | 
						||
 | 
						||
    // Creating a temporary Set with the original listeners, to avoiding side effects
 | 
						||
    // in emit.
 | 
						||
    for (const listener of new Set(listeners)) {
 | 
						||
      // If the object was destroyed during event emission, stop emitting.
 | 
						||
      if (!(eventListeners in target)) {
 | 
						||
        break;
 | 
						||
      }
 | 
						||
 | 
						||
      // If listeners were removed during emission, make sure the
 | 
						||
      // event handler we're going to fire wasn't removed.
 | 
						||
      if (listeners && listeners.has(listener)) {
 | 
						||
        try {
 | 
						||
          let promise;
 | 
						||
          if (isEventHandler(listener)) {
 | 
						||
            promise = listener[handler](type, ...args);
 | 
						||
          } else {
 | 
						||
            promise = listener.apply(target, args);
 | 
						||
          }
 | 
						||
          if (async) {
 | 
						||
            // Assert the name instead of `constructor != Promise` in order
 | 
						||
            // to avoid cross compartment issues where Promise can be multiple.
 | 
						||
            if (!promise || promise.constructor.name != "Promise") {
 | 
						||
              console.warn(
 | 
						||
                `Listener for event '${type}' did not return a promise.`
 | 
						||
              );
 | 
						||
            } else {
 | 
						||
              promises.push(promise);
 | 
						||
            }
 | 
						||
          }
 | 
						||
        } catch (ex) {
 | 
						||
          // Prevent a bad listener from interfering with the others.
 | 
						||
          console.error(ex);
 | 
						||
          const msg = ex + ": " + ex.stack;
 | 
						||
          dump(msg + "\n");
 | 
						||
        }
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    if (async) {
 | 
						||
      return Promise.all(promises);
 | 
						||
    }
 | 
						||
 | 
						||
    return undefined;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Returns a number of event listeners registered for the given event `type`
 | 
						||
   * on the given event `target`.
 | 
						||
   *
 | 
						||
   * @param {Object} target
 | 
						||
   *    Event target object.
 | 
						||
   * @param {String} type
 | 
						||
   *    The type of event.
 | 
						||
   * @return {Number}
 | 
						||
   *    The number of event listeners.
 | 
						||
   */
 | 
						||
  static count(target, type) {
 | 
						||
    if (eventListeners in target) {
 | 
						||
      const listenersForType = target[eventListeners].get(type);
 | 
						||
 | 
						||
      if (listenersForType) {
 | 
						||
        return listenersForType.size;
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    return 0;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Decorate an object with event emitter functionality; basically using the
 | 
						||
   * class' prototype as mixin.
 | 
						||
   *
 | 
						||
   * @param Object target
 | 
						||
   *    The object to decorate.
 | 
						||
   * @return Object
 | 
						||
   *    The object given, mixed.
 | 
						||
   */
 | 
						||
  static decorate(target) {
 | 
						||
    const descriptors = Object.getOwnPropertyDescriptors(this.prototype);
 | 
						||
    delete descriptors.constructor;
 | 
						||
    return Object.defineProperties(target, descriptors);
 | 
						||
  }
 | 
						||
 | 
						||
  static get handler() {
 | 
						||
    return handler;
 | 
						||
  }
 | 
						||
 | 
						||
  on(...args) {
 | 
						||
    return EventEmitter.on(this, ...args);
 | 
						||
  }
 | 
						||
 | 
						||
  off(...args) {
 | 
						||
    EventEmitter.off(this, ...args);
 | 
						||
  }
 | 
						||
 | 
						||
  clearEvents() {
 | 
						||
    EventEmitter.clearEvents(this);
 | 
						||
  }
 | 
						||
 | 
						||
  once(...args) {
 | 
						||
    return EventEmitter.once(this, ...args);
 | 
						||
  }
 | 
						||
 | 
						||
  emit(...args) {
 | 
						||
    EventEmitter.emit(this, ...args);
 | 
						||
  }
 | 
						||
 | 
						||
  emitAsync(...args) {
 | 
						||
    return EventEmitter.emitAsync(this, ...args);
 | 
						||
  }
 | 
						||
 | 
						||
  emitForTests(...args) {
 | 
						||
    if (flags.testing) {
 | 
						||
      EventEmitter.emit(this, ...args);
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  count(...args) {
 | 
						||
    return EventEmitter.count(this, ...args);
 | 
						||
  }
 | 
						||
}
 | 
						||
 | 
						||
module.exports = EventEmitter;
 | 
						||
 | 
						||
const isEventHandler = listener =>
 | 
						||
  listener && handler in listener && typeof listener[handler] === "function";
 | 
						||
 | 
						||
const {
 | 
						||
  getNthPathExcluding,
 | 
						||
} = require("resource://devtools/shared/platform/stack.js");
 | 
						||
let loggingEnabled = false;
 | 
						||
 | 
						||
if (!isWorker) {
 | 
						||
  loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit", false);
 | 
						||
  const observer = {
 | 
						||
    observe: () => {
 | 
						||
      loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
 | 
						||
    },
 | 
						||
  };
 | 
						||
  Services.prefs.addObserver("devtools.dump.emit", observer);
 | 
						||
 | 
						||
  // Also listen for Loader unload to unregister the pref observer and
 | 
						||
  // prevent leaking
 | 
						||
  const unloadObserver = function(subject) {
 | 
						||
    if (subject.wrappedJSObject == require("@loader/unload")) {
 | 
						||
      Services.prefs.removeObserver("devtools.dump.emit", observer);
 | 
						||
      Services.obs.removeObserver(unloadObserver, "devtools:loader:destroy");
 | 
						||
    }
 | 
						||
  };
 | 
						||
  Services.obs.addObserver(unloadObserver, "devtools:loader:destroy");
 | 
						||
}
 | 
						||
 | 
						||
function serialize(target) {
 | 
						||
  const MAXLEN = 60;
 | 
						||
 | 
						||
  // Undefined
 | 
						||
  if (typeof target === "undefined") {
 | 
						||
    return "undefined";
 | 
						||
  }
 | 
						||
 | 
						||
  if (target === null) {
 | 
						||
    return "null";
 | 
						||
  }
 | 
						||
 | 
						||
  // Number / String
 | 
						||
  if (typeof target === "string" || typeof target === "number") {
 | 
						||
    return truncate(target, MAXLEN);
 | 
						||
  }
 | 
						||
 | 
						||
  // HTML Node
 | 
						||
  if (target.nodeName) {
 | 
						||
    let out = target.nodeName;
 | 
						||
 | 
						||
    if (target.id) {
 | 
						||
      out += "#" + target.id;
 | 
						||
    }
 | 
						||
    if (target.className) {
 | 
						||
      out += "." + target.className;
 | 
						||
    }
 | 
						||
 | 
						||
    return out;
 | 
						||
  }
 | 
						||
 | 
						||
  // Array
 | 
						||
  if (Array.isArray(target)) {
 | 
						||
    return truncate(target.toSource(), MAXLEN);
 | 
						||
  }
 | 
						||
 | 
						||
  // Function
 | 
						||
  if (typeof target === "function") {
 | 
						||
    return `function ${target.name ? target.name : "anonymous"}()`;
 | 
						||
  }
 | 
						||
 | 
						||
  // Window
 | 
						||
  if (target?.constructor?.name === "Window") {
 | 
						||
    return `window (${target.location.origin})`;
 | 
						||
  }
 | 
						||
 | 
						||
  // Object
 | 
						||
  if (typeof target === "object") {
 | 
						||
    let out = "{";
 | 
						||
 | 
						||
    const entries = Object.entries(target);
 | 
						||
    for (let i = 0; i < Math.min(10, entries.length); i++) {
 | 
						||
      const [name, value] = entries[i];
 | 
						||
 | 
						||
      if (i > 0) {
 | 
						||
        out += ", ";
 | 
						||
      }
 | 
						||
 | 
						||
      out += `${name}: ${truncate(value, MAXLEN)}`;
 | 
						||
    }
 | 
						||
 | 
						||
    return out + "}";
 | 
						||
  }
 | 
						||
 | 
						||
  // Other
 | 
						||
  return truncate(target.toSource(), MAXLEN);
 | 
						||
}
 | 
						||
 | 
						||
function truncate(value, maxLen) {
 | 
						||
  // We don't use value.toString() because it can throw.
 | 
						||
  const str = String(value);
 | 
						||
  return str.length > maxLen ? str.substring(0, maxLen) + "..." : str;
 | 
						||
}
 | 
						||
 | 
						||
function logEvent(type, args) {
 | 
						||
  let argsOut = "";
 | 
						||
 | 
						||
  // We need this try / catch to prevent any dead object errors.
 | 
						||
  try {
 | 
						||
    argsOut = `${args.map(serialize).join(", ")}`;
 | 
						||
  } catch (e) {
 | 
						||
    // Object is dead so the toolbox is most likely shutting down,
 | 
						||
    // do nothing.
 | 
						||
  }
 | 
						||
 | 
						||
  const path = getNthPathExcluding(0, "devtools/shared/event-emitter.js");
 | 
						||
 | 
						||
  if (args.length) {
 | 
						||
    dump(`EMITTING: emit(${type}, ${argsOut}) from ${path}\n`);
 | 
						||
  } else {
 | 
						||
    dump(`EMITTING: emit(${type}) from ${path}\n`);
 | 
						||
  }
 | 
						||
}
 |