dler and will run in the parent process. This can be
 *   useful if some code has to be executed in the parent process, even though
 *   the final destination is a WINDOW_GLOBAL.
 * - And so on, as more MessageHandler types get added, more combinations will
 *   follow based on the same pattern:
 *   - {contextName}/{ModuleName}.sys.mjs
 *   - or {destinationType}-in-{currentType}/{ModuleName}.sys.mjs
 *
 * All those implementations are optional. If a module cannot be found, based on
 * the logic detailed above, the MessageHandler will assume that the command
 * should simply be forwarded to the next layer of the network.
 */
export class ModuleCache {
  #messageHandler;
  #messageHandlerType;
  #modules;
  #protocol;

  /**
   * @param {MessageHandler} messageHandler
   *     The MessageHandler instance which owns this ModuleCache instance.
   */
  constructor(messageHandler) {
    this.#messageHandler = messageHandler;
    this.#messageHandlerType = messageHandler.constructor.type;

    // Map of absolute module paths to module instances.
    this.#modules = new Map();

    // Use the module class from the WebDriverBiDi ModuleRegistry if we
    // are not using test modules.
    this.#protocol = Services.prefs.getBoolPref(
      "remote.messagehandler.modulecache.useBrowserTestRoot",
      false
    )
      ? protocols.test
      : protocols.bidi;
  }

  /**
   * Destroy all instantiated modules.
   */
  destroy() {
    this.#modules.forEach(module => module?.destroy());
  }

  /**
   * Retrieve all module classes matching the provided module name to reach the
   * provided destination, from the current context.
   *
   * This corresponds to the path a command can take to reach its destination.
   * A command's method must be implemented in one of the classes returned by
   * getAllModuleClasses in order to be successfully handled.
   *
   * @param {string} moduleName
   *     The name of the module.
   * @param {Destination} destination
   *     The destination.
   * @returns {Array<class<Module>|null>}
   *     An array of Module classes.
   */
  getAllModuleClasses(moduleName, destination) {
    const classes = [];

    let currentType = destination.type;
    while (currentType) {
      classes.push(
        this.#getModuleClass(moduleName, currentType, destination.type)
      );
      currentType = lazy.MessageHandlerParentMap.get(currentType);
    }

    return classes.filter(cls => !!cls);
  }

  /**
   * Get a module instance corresponding to the provided moduleName and
   * destination. If no existing module can be found in the cache, ModuleCache
   * will attempt to import the module file and create a new instance, which
   * will then be cached and returned for subsequent calls.
   *
   * @param {string} moduleName
   *     The name of the module which should implement the command.
   * @param {CommandDestination} destination
   *     The destination of the command for which we need to instantiate a
   *     module. See MessageHandler.sys.mjs for the CommandDestination typedef.
   * @returns {object=}
   *     A module instance corresponding to the provided moduleName and
   *     destination, or null if it could not be instantiated.
   */
  getModuleInstance(moduleName, destination) {
    const key = `${moduleName}-${destination.type}`;

    if (this.#modules.has(key)) {
      // If there is already a cached instance (potentially null) for the
      // module name + destination type pair, return it.
      return this.#modules.get(key);
    }

    const ModuleClass = this.#getModuleClass(
      moduleName,
      this.#messageHandlerType,
      destination.type
    );

    let module = null;
    if (ModuleClass) {
      module = new ModuleClass(this.#messageHandler);
      module.moduleName = moduleName;
    }

    this.#modules.set(key, module);
    return module;
  }

  /**
   * Check if the given module exists for the destination.
   *
   * @param {string} moduleName
   *     The name of the module.
   * @param {Destination} destination
   *     The destination.
   * @returns {boolean}
   *     True if the module exists.
   */
  hasModuleClass(moduleName, destination) {
    const classes = this.getAllModuleClasses(moduleName, destination);
    return !!classes.length;
  }

  toString() {
    return `[object ${this.constructor.name} ${this.#messageHandler.name}]`;
  }

  /**
   * Retrieve the module class matching the provided module name and folder.
   *
   * @param {string} moduleName
   *     The name of the module to get the class for.
   * @param {string} originType
   *     The MessageHandler type from where the command comes.
   * @param {string} destinationType
   *     The MessageHandler type where the command should go to.
   * @returns {Class=}
   *     The class corresponding to the module name and folder, null if no match
   *     was found.
   * @throws {Error}
   *     If the provided module folder is unexpected.
   */
  #getModuleClass = function (moduleName, originType, destinationType) {
    if (
      destinationType === lazy.RootMessageHandler.type &&
      originType !== destinationType
    ) {
      // If we are trying to reach the root layer from a lower layer, no module
      // class should attempt to handle the command in the current layer and
      // the command should be forwarded unconditionally.
      return null;
    }

    const moduleFolder = this.#getModuleFolder(originType, destinationType);
    if (!this.#protocol.modules[moduleFolder]) {
      throw new Error(
        `Invalid module folder "${moduleFolder}", expected one of "${Object.keys(
          this.#protocol.modules
        )}"`
      );
    }

    let modulePath = this.#protocol.modules[moduleFolder];
    if (moduleName.includes(":")) {
      const parts = moduleName.split(":");
      moduleName = parts[1];
      modulePath = modulePath[parts[0]];
    }

    const moduleClass = modulePath?.[moduleName] ?? null;

    // Module hit/miss logs generate a lot of spam. Only log if verbose is true.
    //
    // Note: Due to https://bugzilla.mozilla.org/show_bug.cgi?id=1828395
    // verbose is currently always false if the log level is trace.
    // If those logs are needed before the bug is fixed, temporarily remove the
    // condition.
    if (lazy.Log.verbose) {
      if (moduleClass) {
        lazy.logger.trace(
          `Module ${moduleFolder}/${moduleName}.sys.mjs found for ${destinationType}`
        );
      } else {
        lazy.logger.trace(
          `Module ${moduleFolder}/${moduleName}.sys.mjs not found for ${destinationType}`
        );
      }
    }

    return moduleClass;
  };

  #getModuleFolder(originType, destinationType) {
    const originPath = lazy.getMessageHandlerClass(originType).modulePath;
    if (originType === destinationType) {
      // If the command is targeting the current type, the module is expected to
      // be in eg "windowglobal/${moduleName}.sys.mjs".
      return originPath;
    }

    // If the command is targeting another type, the module is expected to
    // be in a composed folder eg "windowglobal-in-root/${moduleName}.sys.mjs".
    const destinationPath =
      lazy.getMessageHandlerClass(destinationType).modulePath;
    return `${destinationPath}-in-${originPath}`;
  }
}
PK