tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

resource-command.js (55426B)


      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/. */
      4 
      5 "use strict";
      6 
      7 const { throttle } = require("resource://devtools/shared/throttle.js");
      8 
      9 let gLastResourceId = 0;
     10 
     11 function cacheKey(resourceType, resourceId) {
     12  return `${resourceType}:${resourceId}`;
     13 }
     14 
     15 class ResourceCommand {
     16  #destroyed = false;
     17 
     18  /**
     19   * This class helps retrieving existing and listening to resources.
     20   * A resource is something that:
     21   *  - the target you are debugging exposes
     22   *  - can be created as early as the process/worker/page starts loading
     23   *  - can already exist, or will be created later on
     24   *  - doesn't require any user data to be fetched, only a type/category
     25   *
     26   * @param object commands
     27   *        The commands object with all interfaces defined from devtools/shared/commands/
     28   */
     29  constructor({ commands }) {
     30    this.targetCommand = commands.targetCommand;
     31 
     32    // Public attribute set by tests to disable throttling
     33    this.throttlingDisabled = false;
     34 
     35    this._onTargetAvailable = this._onTargetAvailable.bind(this);
     36    this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
     37 
     38    // Array of all the currently registered watchers, which contains object with attributes:
     39    // - {String} resources: list of all resource watched by this one watcher
     40    // - {Function} onAvailable: watcher's function to call when a new resource is available
     41    // - {Function} onUpdated: watcher's function to call when a resource has been updated
     42    // - {Function} onDestroyed: watcher's function to call when a resource is destroyed
     43    this._watchers = [];
     44 
     45    // Set of watchers currently going through watchResources, only used to handle
     46    // early calls to unwatchResources. Using a Set instead of an array for easier
     47    // delete operations.
     48    this._pendingWatchers = new Set();
     49 
     50    // Caches for all resources by the order that the resource was taken.
     51    this._cache = new Map();
     52    this._listenedResources = new Set();
     53 
     54    // WeakMap used to avoid starting a legacy listener twice for the same
     55    // target + resource-type pair. Legacy listener creation can be subject to
     56    // race conditions.
     57    // Maps a target front to an array of resource types.
     58    this._existingLegacyListeners = new WeakMap();
     59    this._processingExistingResources = new Set();
     60 
     61    // List of targetFront event listener unregistration functions keyed by target front.
     62    // These are called when unwatching resources, so if a consumer starts watching resources again,
     63    // we don't have listeners registered twice.
     64    this._offTargetFrontListeners = new Map();
     65 
     66    // Bug 1914386: We used to throttle the resource on client and should try to remove it entirely.
     67    const throttleDelay = 0;
     68    this._notifyWatchers = this._notifyWatchers.bind(this);
     69    this._throttledNotifyWatchers = throttle(
     70      this._notifyWatchers,
     71      throttleDelay
     72    );
     73  }
     74 
     75  destroy() {
     76    this.#destroyed = true;
     77  }
     78 
     79  get watcherFront() {
     80    return this.targetCommand.watcherFront;
     81  }
     82 
     83  addResourceToCache(resource) {
     84    const { resourceId, resourceType } = resource;
     85    if (TRANSIENT_RESOURCE_TYPES.includes(resourceType)) {
     86      return;
     87    }
     88    this._cache.set(cacheKey(resourceType, resourceId), resource);
     89  }
     90 
     91  /**
     92   * Clear all the resources related to specifed resource types.
     93   * Should also trigger clearing of the caches that exists on the related
     94   * serverside resource watchers.
     95   *
     96   * @param {Array:string} resourceTypes
     97   *                       A list of all the resource types whose
     98   *                       resources shouled be cleared.
     99   */
    100  async clearResources(resourceTypes) {
    101    if (!Array.isArray(resourceTypes)) {
    102      throw new Error("clearResources expects a list of resources types");
    103    }
    104    // Clear the cached resources of the type.
    105    for (const [key, resource] of this._cache) {
    106      if (resourceTypes.includes(resource.resourceType)) {
    107        // NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
    108        this._cache.delete(key);
    109      }
    110    }
    111 
    112    const resourcesToClear = resourceTypes.filter(resourceType =>
    113      this.hasResourceCommandSupport(resourceType)
    114    );
    115    if (resourcesToClear.length) {
    116      this.watcherFront.clearResources(resourcesToClear);
    117    }
    118  }
    119  /**
    120   * Return all specified resources cached in this watcher.
    121   *
    122   * @param {string} resourceType
    123   * @return {Array} resources cached in this watcher
    124   */
    125  getAllResources(resourceType) {
    126    const result = [];
    127    for (const resource of this._cache.values()) {
    128      if (resource.resourceType === resourceType) {
    129        result.push(resource);
    130      }
    131    }
    132    return result;
    133  }
    134 
    135  /**
    136   * Return the specified resource cached in this watcher.
    137   *
    138   * @param {string} resourceType
    139   * @param {string} resourceId
    140   * @return {object} resource cached in this watcher
    141   */
    142  getResourceById(resourceType, resourceId) {
    143    return this._cache.get(cacheKey(resourceType, resourceId));
    144  }
    145 
    146  /**
    147   * Request to start retrieving all already existing instances of given
    148   * type of resources and also start watching for the one to be created after.
    149   *
    150   * @param {Array:string} resources
    151   *        List of all resources which should be fetched and observed.
    152   * @param {object} options
    153   *        - {Function} onAvailable: This attribute is mandatory.
    154   *                                  Function which will be called with an array of resources
    155   *                                  each time resource(s) are created.
    156   *                                  A second dictionary argument with `areExistingResources` boolean
    157   *                                  attribute helps knowing if that's live resources, or some coming
    158   *                                  from ResourceCommand cache.
    159   *        - {Function} onUpdated:   This attribute is optional.
    160   *                                  Function which will be called with an array of updates resources
    161   *                                  each time resource(s) are updated.
    162   *                                  These resources were previously notified via onAvailable.
    163   *        - {Function} onDestroyed: This attribute is optional.
    164   *                                  Function which will be called with an array of deleted resources
    165   *                                  each time resource(s) are destroyed.
    166   *        - {boolean} ignoreExistingResources:
    167   *                                  This attribute is optional. Default value is false.
    168   *                                  If set to true, onAvailable won't be called with
    169   *                                  existing resources.
    170   */
    171  async watchResources(resources, options) {
    172    const {
    173      onAvailable,
    174      onUpdated,
    175      onDestroyed,
    176      ignoreExistingResources = false,
    177    } = options;
    178 
    179    if (typeof onAvailable !== "function") {
    180      throw new Error(
    181        "ResourceCommand.watchResources expects an onAvailable function as argument"
    182      );
    183    }
    184 
    185    for (const type of resources) {
    186      if (!this._isValidResourceType(type)) {
    187        throw new Error(
    188          `ResourceCommand.watchResources invoked with an unknown type: "${type}"`
    189        );
    190      }
    191    }
    192 
    193    // Copy the array in order to avoid the callsite to modify the list of watched resources by mutating the array.
    194    // You have to call (un)watchResources to update the list of resources being watched!
    195    resources = [...resources];
    196 
    197    // Pending watchers are used in unwatchResources to remove watchers which
    198    // are not fully registered yet. Store `onAvailable` which is the unique key
    199    // for a watcher, as well as the resources array, so that unwatchResources
    200    // can update the array if we stop watching a specific resource.
    201    const pendingWatcher = {
    202      resources,
    203      onAvailable,
    204    };
    205    this._pendingWatchers.add(pendingWatcher);
    206 
    207    // Bug 1675763: Watcher actor is not available in all situations yet.
    208    if (!this._listenerRegistered && this.watcherFront) {
    209      this._listenerRegistered = true;
    210      // Resources watched from the parent process will be emitted on the Watcher Actor.
    211      // So that we also have to listen for this event on it, in addition to all targets.
    212      this.watcherFront.on(
    213        "resources-available-array",
    214        this._onResourceAvailableArray.bind(this, {
    215          watcherFront: this.watcherFront,
    216        })
    217      );
    218      this.watcherFront.on(
    219        "resources-updated-array",
    220        this._onResourceUpdatedArray.bind(this, {
    221          watcherFront: this.watcherFront,
    222        })
    223      );
    224      this.watcherFront.on(
    225        "resources-destroyed-array",
    226        this._onResourceDestroyedArray.bind(this, {
    227          watcherFront: this.watcherFront,
    228        })
    229      );
    230    }
    231 
    232    const promises = [];
    233    for (const resource of resources) {
    234      promises.push(this._startListening(resource));
    235    }
    236    await Promise.all(promises);
    237 
    238    // The resource cache is immediately filled when receiving the sources, but they are
    239    // emitted with a delay due to throttling. Since the cache can contain resources that
    240    // will soon be emitted, we have to flush it before adding the new listeners.
    241    // Otherwise _forwardExistingResources might emit resources that will also be emitted by
    242    // the next `_notifyWatchers` call done when calling `_startListening`, which will pull the
    243    // "already existing" resources.
    244    this._notifyWatchers();
    245 
    246    // Update the _pendingWatchers set before adding the watcher to _watchers.
    247    this._pendingWatchers.delete(pendingWatcher);
    248 
    249    // If unwatchResources was called in the meantime, use pendingWatcher's
    250    // resources to get the updated list of watched resources.
    251    const watchedResources = pendingWatcher.resources;
    252 
    253    // If no resource needs to be watched anymore, do not add an empty watcher
    254    // to _watchers, and do not notify about cached resources.
    255    if (!watchedResources.length) {
    256      return;
    257    }
    258 
    259    // Register the watcher just after calling _startListening in order to avoid it being called
    260    // for already existing resources, which will optionally be notified via _forwardExistingResources
    261    this._watchers.push({
    262      resources: watchedResources,
    263      onAvailable,
    264      onUpdated,
    265      onDestroyed,
    266      pendingEvents: [],
    267    });
    268 
    269    if (!ignoreExistingResources) {
    270      await this._forwardExistingResources(watchedResources, onAvailable);
    271    }
    272  }
    273 
    274  /**
    275   * Stop watching for given type of resources.
    276   * See `watchResources` for the arguments as both methods receive the same.
    277   * Note that `onUpdated` and `onDestroyed` attributes of `options` aren't used here.
    278   * Only `onAvailable` attribute is looked up and we unregister all the other registered callbacks
    279   * when a matching available callback is found.
    280   */
    281  unwatchResources(resources, options) {
    282    const { onAvailable } = options;
    283 
    284    if (typeof onAvailable !== "function") {
    285      throw new Error(
    286        "ResourceCommand.unwatchResources expects an onAvailable function as argument"
    287      );
    288    }
    289 
    290    for (const type of resources) {
    291      if (!this._isValidResourceType(type)) {
    292        throw new Error(
    293          `ResourceCommand.unwatchResources invoked with an unknown type: "${type}"`
    294        );
    295      }
    296    }
    297 
    298    // Unregister the callbacks from the watchers registries.
    299    // Check _watchers for the fully initialized watchers, as well as
    300    // `_pendingWatchers` for new watchers still being created by `watchResources`
    301    const allWatchers = [...this._watchers, ...this._pendingWatchers];
    302    for (const watcherEntry of allWatchers) {
    303      // onAvailable is the only mandatory argument which ends up being used to match
    304      // the right watcher entry.
    305      if (watcherEntry.onAvailable == onAvailable) {
    306        // Remove all resources that we stop watching. We may still watch for some others.
    307        watcherEntry.resources = watcherEntry.resources.filter(resourceType => {
    308          return !resources.includes(resourceType);
    309        });
    310      }
    311    }
    312    this._watchers = this._watchers.filter(entry => {
    313      // Remove entries entirely if it isn't watching for any resource type
    314      return !!entry.resources.length;
    315    });
    316 
    317    // Stop listening to all resources for which we removed the last watcher
    318    for (const resource of resources) {
    319      const isResourceWatched = allWatchers.some(watcherEntry =>
    320        watcherEntry.resources.includes(resource)
    321      );
    322 
    323      // Also check in _listenedResources as we may call unwatchResources
    324      // for resources that we haven't started watching for.
    325      if (!isResourceWatched && this._listenedResources.has(resource)) {
    326        this._stopListening(resource);
    327      }
    328    }
    329 
    330    // Stop watching for targets if we removed the last listener.
    331    if (this._listenedResources.size == 0) {
    332      this._unwatchAllTargets();
    333    }
    334  }
    335 
    336  /**
    337   * Wait for a single resource of the provided resourceType.
    338   *
    339   * @param {string} resourceType
    340   *        One of ResourceCommand.TYPES, type of the expected resource.
    341   * @param {object} additional options
    342   *        - {Boolean} ignoreExistingResources: ignore existing resources or not.
    343   *        - {Function} predicate: if provided, will wait until a resource makes
    344   *          predicate(resource) return true.
    345   * @return {Promise<object>}
    346   *         Return a promise which resolves once we fully settle the resource listener.
    347   *         You should await for its resolution before doing the action which may fire
    348   *         your resource.
    349   *         This promise will expose an object with `onResource` attribute,
    350   *         itself being a promise, which will resolve once a matching resource is received.
    351   */
    352  async waitForNextResource(
    353    resourceType,
    354    { ignoreExistingResources = false, predicate } = {}
    355  ) {
    356    // If no predicate was provided, convert to boolean to avoid resolving for
    357    // empty `resources` arrays.
    358    predicate = predicate || (resource => !!resource);
    359 
    360    let resolve;
    361    const promise = new Promise(r => (resolve = r));
    362    const onAvailable = async resources => {
    363      const matchingResource = resources.find(resource => predicate(resource));
    364      if (matchingResource) {
    365        this.unwatchResources([resourceType], { onAvailable });
    366        resolve(matchingResource);
    367      }
    368    };
    369 
    370    await this.watchResources([resourceType], {
    371      ignoreExistingResources,
    372      onAvailable,
    373    });
    374    return { onResource: promise };
    375  }
    376 
    377  /**
    378   * Check if there are any watchers for the specified resource.
    379   *
    380   * @param {string} resourceType
    381   *         One of ResourceCommand.TYPES
    382   * @return {boolean}
    383   *         If the resources type is beibg watched.
    384   */
    385  isResourceWatched(resourceType) {
    386    return this._listenedResources.has(resourceType);
    387  }
    388 
    389  /**
    390   * Start watching for all already existing and future targets.
    391   *
    392   * We are using ALL_TYPES, but this won't force listening to all types.
    393   * It will only listen for types which are defined by `TargetCommand.startListening`.
    394   */
    395  async _watchAllTargets() {
    396    if (!this._watchTargetsPromise) {
    397      // If this is the very first listener registered, of all kind of resource types:
    398      // * we want to start observing targets via TargetCommand
    399      // * _onTargetAvailable will be called for each already existing targets and the next one to come
    400      this._watchTargetsPromise = this.targetCommand.watchTargets({
    401        types: this.targetCommand.ALL_TYPES,
    402        onAvailable: this._onTargetAvailable,
    403        onDestroyed: this._onTargetDestroyed,
    404      });
    405    }
    406    return this._watchTargetsPromise;
    407  }
    408 
    409  _unwatchAllTargets() {
    410    if (!this._watchTargetsPromise) {
    411      return;
    412    }
    413 
    414    for (const offList of this._offTargetFrontListeners.values()) {
    415      offList.forEach(off => off());
    416    }
    417    this._offTargetFrontListeners.clear();
    418 
    419    this._watchTargetsPromise = null;
    420    this.targetCommand.unwatchTargets({
    421      types: this.targetCommand.ALL_TYPES,
    422      onAvailable: this._onTargetAvailable,
    423      onDestroyed: this._onTargetDestroyed,
    424    });
    425  }
    426 
    427  /**
    428   * For a given resource type, start the legacy listeners for all already existing targets.
    429   * Do that only if we have to. If this resourceType requires legacy listeners.
    430   */
    431  async _startLegacyListenersForExistingTargets(resourceType) {
    432    // If we were already listening to targets, we want to start the legacy listeners
    433    // for all already existing targets.
    434    //
    435    // Only try instantiating the legacy listener, if this resource type:
    436    //   - has legacy listener implementation
    437    // (new resource types may not be supported by old runtime and just not be received without breaking anything)
    438    //   - isn't supported by the server, or, the target type requires the a legacy listener implementation.
    439    const shouldRunLegacyListeners =
    440      resourceType in LegacyListeners &&
    441      (!this.hasResourceCommandSupport(resourceType) ||
    442        this._shouldRunLegacyListenerEvenWithWatcherSupport(resourceType));
    443    if (shouldRunLegacyListeners) {
    444      const promises = [];
    445      const targets = this.targetCommand.getAllTargets(
    446        this.targetCommand.ALL_TYPES
    447      );
    448      for (const targetFront of targets) {
    449        // We disable warning in case we already registered the legacy listener for this target
    450        // as this code may race with the call from onTargetAvailable if we end up having multiple
    451        // calls to _startListening in parallel.
    452        promises.push(
    453          this._watchResourcesForTarget({
    454            targetFront,
    455            resourceType,
    456            disableWarning: true,
    457          })
    458        );
    459      }
    460      await Promise.all(promises);
    461    }
    462  }
    463 
    464  /**
    465   * Method called by the TargetCommand for each already existing or target which has just been created.
    466   *
    467   * @param {object} arg
    468   * @param {Front} arg.targetFront
    469   *        The Front of the target that is available.
    470   *        This Front inherits from TargetMixin and is typically
    471   *        composed of a WindowGlobalTargetFront or ContentProcessTargetFront.
    472   * @param {boolean} arg.isTargetSwitching
    473   *         true when the new target was created because of a target switching.
    474   */
    475  async _onTargetAvailable({ targetFront, isTargetSwitching }) {
    476    const resources = [];
    477    if (isTargetSwitching) {
    478      // WatcherActor currently only watches additional frame targets and
    479      // explicitely ignores top level one that may be created when navigating
    480      // to a new process.
    481      // In order to keep working resources that are being watched via the
    482      // Watcher actor, we have to unregister and re-register the resource
    483      // types. This will force calling `Resources.watchResources` on the new top
    484      // level target.
    485      for (const resourceType of Object.values(ResourceCommand.TYPES)) {
    486        // ...which has at least one listener...
    487        if (!this._listenedResources.has(resourceType)) {
    488          continue;
    489        }
    490 
    491        if (this._shouldRestartListenerOnTargetSwitching(resourceType)) {
    492          this._stopListening(resourceType, {
    493            bypassListenerCount: true,
    494          });
    495          resources.push(resourceType);
    496        }
    497      }
    498    }
    499 
    500    if (targetFront.isDestroyed()) {
    501      return;
    502    }
    503 
    504    // If we are target switching, we already stop & start listening to all the
    505    // currently monitored resources.
    506    if (!isTargetSwitching) {
    507      // For each resource type...
    508      for (const resourceType of Object.values(ResourceCommand.TYPES)) {
    509        // ...which has at least one listener...
    510        if (!this._listenedResources.has(resourceType)) {
    511          continue;
    512        }
    513        // ...request existing resource and new one to come from this one target
    514        // *but* only do that for backward compat, where we don't have the watcher API
    515        // (See bug 1626647)
    516        await this._watchResourcesForTarget({ targetFront, resourceType });
    517      }
    518    }
    519 
    520    // Compared to the TargetCommand and Watcher.watchTargets,
    521    // We do call Watcher.watchResources, but the events are fired on the target.
    522    // That's because the Watcher runs in the parent process/main thread, while resources
    523    // are available from the target's process/thread.
    524    const offResourceAvailableArray = targetFront.on(
    525      "resources-available-array",
    526      this._onResourceAvailableArray.bind(this, { targetFront })
    527    );
    528    const offResourceUpdatedArray = targetFront.on(
    529      "resources-updated-array",
    530      this._onResourceUpdatedArray.bind(this, { targetFront })
    531    );
    532    const offResourceDestroyedArray = targetFront.on(
    533      "resources-destroyed-array",
    534      this._onResourceDestroyedArray.bind(this, { targetFront })
    535    );
    536 
    537    const offList = this._offTargetFrontListeners.get(targetFront) || [];
    538    offList.push(
    539      offResourceAvailableArray,
    540      offResourceUpdatedArray,
    541      offResourceDestroyedArray
    542    );
    543 
    544    if (isTargetSwitching) {
    545      await Promise.all(
    546        resources.map(resourceType =>
    547          this._startListening(resourceType, {
    548            bypassListenerCount: true,
    549          })
    550        )
    551      );
    552    }
    553 
    554    // DOCUMENT_EVENT's will-navigate should replace target actor's will-navigate event,
    555    // but only for targets provided by the watcher actor.
    556    // Emit a fake DOCUMENT_EVENT's "will-navigate" out of target actor's will-navigate
    557    // until watcher actor is supported by all descriptors (bug 1675763).
    558    if (!this.targetCommand.hasTargetWatcherSupport()) {
    559      const offWillNavigate = targetFront.on(
    560        "will-navigate",
    561        ({ url, isFrameSwitching }) => {
    562          targetFront.emit("resource-available-form", [
    563            {
    564              resourceType: this.TYPES.DOCUMENT_EVENT,
    565              name: "will-navigate",
    566              time: Date.now(), // will-navigate was not passing any timestamp
    567              isFrameSwitching,
    568              newURI: url,
    569            },
    570          ]);
    571        }
    572      );
    573      offList.push(offWillNavigate);
    574    }
    575 
    576    this._offTargetFrontListeners.set(targetFront, offList);
    577  }
    578 
    579  _shouldRestartListenerOnTargetSwitching(resourceType) {
    580    // Note that we aren't using isServerTargetSwitchingEnabled, nor checking the
    581    // server side target switching preference as we may have server side targets
    582    // even when this is false/disabled.
    583    // This will happen for bfcache navigations, even with server side targets disabled.
    584    // `followWindowGlobalLifeCycle` will be false for the first top level target
    585    // and only become true when doing a bfcache navigation.
    586    // (only server side targets follow the WindowGlobal lifecycle)
    587    // When server side targets are enabled, this will always be true.
    588    const isServerSideTarget =
    589      this.targetCommand.targetFront.targetForm.followWindowGlobalLifeCycle;
    590    if (isServerSideTarget) {
    591      // For top-level targets created from the server, only restart legacy
    592      // listeners.
    593      return !this.hasResourceCommandSupport(resourceType);
    594    }
    595 
    596    // For top-level targets created from the client we should always restart
    597    // listeners.
    598    return true;
    599  }
    600 
    601  /**
    602   * Method called by the TargetCommand when a target has just been destroyed
    603   *
    604   * @param {object} arg
    605   * @param {Front} arg.targetFront
    606   *        The Front of the target that was destroyed
    607   * @param {boolean} arg.isModeSwitching
    608   *         true when this is called as the result of a change to the devtools.browsertoolbox.scope pref.
    609   */
    610  _onTargetDestroyed({ targetFront, isModeSwitching }) {
    611    // Clear the map of legacy listeners for this target.
    612    this._existingLegacyListeners.set(targetFront, []);
    613    this._offTargetFrontListeners.delete(targetFront);
    614 
    615    // Purge the cache from any resource related to the destroyed target.
    616    // Top level BrowsingContext target will be purge via DOCUMENT_EVENT will-navigate events.
    617    // If we were to clean resources from target-destroyed, we will clear resources
    618    // happening between will-navigate and target-destroyed. Typically the navigation request
    619    // At the moment, isModeSwitching can only be true when targetFront.isTopLevel isn't true,
    620    // so we don't need to add a specific check for isModeSwitching.
    621    if (!targetFront.isTopLevel || !targetFront.isBrowsingContext) {
    622      for (const [key, resource] of this._cache) {
    623        if (resource.targetFront === targetFront) {
    624          // NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
    625          this._cache.delete(key);
    626        }
    627      }
    628    }
    629 
    630    // Purge "available" pendingEvents for resources from the destroyed target when switching
    631    // mode as we want to ignore those.
    632    if (isModeSwitching) {
    633      for (const watcherEntry of this._watchers) {
    634        for (const pendingEvent of watcherEntry.pendingEvents) {
    635          if (pendingEvent.callbackType == "available") {
    636            pendingEvent.updates = pendingEvent.updates.filter(
    637              update => update.targetFront !== targetFront
    638            );
    639          }
    640        }
    641      }
    642    }
    643  }
    644 
    645  async _onResourceAvailableArray({ targetFront, watcherFront }, array) {
    646    let includesDocumentEventWillNavigate = false;
    647    let includesDocumentEventDomLoading = false;
    648    for (const [resourceType, resources] of array) {
    649      const isAlreadyExistingResource =
    650        this._processingExistingResources.has(resourceType);
    651      const transformer = ResourceTransformers[resourceType];
    652 
    653      for (let i = 0; i < resources.length; i++) {
    654        let resource = resources[i];
    655        if (!("resourceType" in resource)) {
    656          resource.resourceType = resourceType;
    657        }
    658 
    659        if (watcherFront) {
    660          try {
    661            targetFront = await this._getTargetForWatcherResource(resource);
    662          } catch (e) {
    663            if (this.#destroyed) {
    664              // If the resource-command was destroyed while waiting for
    665              // _getTargetForWatcherResource, swallow the error.
    666              return;
    667            }
    668            throw e;
    669          }
    670          // When we receive resources from the Watcher actor,
    671          // there is no guarantee that the target front is fully initialized.
    672          // The Target Front is initialized by the TargetCommand, by calling TargetFront.attachAndInitThread.
    673          // We have to wait for its completion as resources watchers are expecting it to be completed.
    674          //
    675          // But when navigating, we may receive resources packets for a destroyed target.
    676          // Or, in the context of the browser toolbox, they may not relate to any target.
    677          if (targetFront) {
    678            await targetFront.initialized;
    679          }
    680        }
    681 
    682        // Put the targetFront on the resource for easy retrieval.
    683        // (Resources from the legacy listeners may already have the attribute set)
    684        if (!resource.targetFront) {
    685          resource.targetFront = targetFront;
    686        }
    687 
    688        if (transformer) {
    689          resource = transformer({
    690            resource,
    691            targetCommand: this.targetCommand,
    692            targetFront,
    693            watcherFront: this.watcherFront,
    694          });
    695          resources[i] = resource;
    696        }
    697 
    698        // isAlreadyExistingResource indicates that the resources already existed before
    699        // the resource command started watching for this type of resource.
    700        resource.isAlreadyExistingResource = isAlreadyExistingResource;
    701 
    702        if (!resource.resourceId) {
    703          resource.resourceId = `auto:${++gLastResourceId}`;
    704        }
    705 
    706        // Only consider top level document, and ignore remote iframes top document
    707        let isWillNavigate = false;
    708        if (resourceType == DOCUMENT_EVENT) {
    709          isWillNavigate = resource.name === "will-navigate";
    710          const isBrowserToolbox =
    711            this.targetCommand.descriptorFront.isBrowserProcessDescriptor;
    712          if (
    713            isWillNavigate &&
    714            resource.targetFront.isTopLevel &&
    715            // When selecting a document in the Browser Toolbox iframe picker, we're getting
    716            // a will-navigate event. In such case, we don't want to clear the cache,
    717            // otherwise we'd miss some resources that we might already received (e.g. stylesheets)
    718            // See Bug 1981937
    719            (!isBrowserToolbox || !resource.isFrameSwitching)
    720          ) {
    721            includesDocumentEventWillNavigate = true;
    722            this._onWillNavigate(resource.targetFront);
    723          }
    724 
    725          if (
    726            resource.name === "dom-loading" &&
    727            resource.targetFront.isTopLevel
    728          ) {
    729            includesDocumentEventDomLoading = true;
    730          }
    731        }
    732 
    733        // Avoid storing will-navigate resource and consider it as a transcient resource.
    734        // We do that to prevent leaking this resource (and its target) on navigation.
    735        // We do clear the cache in _onWillNavigate, that we call a few lines before this.
    736        if (!isWillNavigate) {
    737          this.addResourceToCache(resource);
    738        }
    739      }
    740 
    741      this._queueResourceEvent("available", resourceType, resources);
    742    }
    743 
    744    // If we receive the DOCUMENT_EVENT for:
    745    // - will-navigate
    746    // - dom-loading + we're using the service worker legacy listener
    747    // then flush immediately the resources to notify about the navigation sooner than later.
    748    // (this is especially useful for tests, even if they should probably avoid depending on this...)
    749    if (
    750      includesDocumentEventWillNavigate ||
    751      (includesDocumentEventDomLoading &&
    752        !this.targetCommand.hasTargetWatcherSupport("service_worker")) ||
    753      this.throttlingDisabled
    754    ) {
    755      this._notifyWatchers();
    756    } else {
    757      this._throttledNotifyWatchers();
    758    }
    759  }
    760 
    761  async _onResourceUpdatedArray(context, array) {
    762    for (const [resourceType, resources] of array) {
    763      for (const resource of resources) {
    764        if (!("resourceType" in resource)) {
    765          resource.resourceType = resourceType;
    766        }
    767      }
    768      await this._onResourceUpdated(context, resources);
    769    }
    770  }
    771 
    772  async _onResourceDestroyedArray(context, array) {
    773    const resources = [];
    774    for (const [resourceType, resourceIds] of array) {
    775      for (const resourceId of resourceIds) {
    776        resources.push({ resourceType, resourceId });
    777      }
    778    }
    779    await this._onResourceDestroyed(context, resources);
    780  }
    781 
    782  /**
    783   * Called every time a resource is updated in the remote target.
    784   *
    785   * Method called either by:
    786   * - the backward compatibility code (LegacyListeners)
    787   * - target actors RDP events
    788   *
    789   * @param {object} source
    790   *        A dictionary object with only one of these two attributes:
    791   *        - targetFront: a Target Front, if the resource is watched from the
    792   *          target process or thread.
    793   *        - watcherFront: a Watcher Front, if the resource is watched from
    794   *          the parent process.
    795   * @param {Array<object>} updates
    796   *        Depending on the listener.
    797   *
    798   *        Among the element in the array, the following attributes are given special handling.
    799   *          - resourceType {String}:
    800   *            The type of resource to be updated.
    801   *          - resourceId {String}:
    802   *            The id of resource to be updated.
    803   *          - resourceUpdates {Object}:
    804   *            If resourceUpdates is in the element, a cached resource specified by resourceType
    805   *            and resourceId is updated by Object.assign(cachedResource, resourceUpdates).
    806   *          - nestedResourceUpdates {Object}:
    807   *            If `nestedResourceUpdates` is passed, update one nested attribute with a new value
    808   *            This allows updating one attribute of an object stored in a resource's attribute,
    809   *            as well as adding new elements to arrays.
    810   *            `path` is an array mentioning all nested attribute to walk through.
    811   *            `value` is the new nested attribute value to set.
    812   *
    813   *        And also, the element is passed to the listener as it is as “update” object.
    814   *        So if we don't want to update a cached resource but have information want to
    815   *        pass on to the listener, can pass it on using attributes other than the ones
    816   *        listed above.
    817   *        For example, if the element consists of like
    818   *        "{ resourceType:… resourceId:…, testValue: “test”, }”,
    819   *        the listener can receive the value as follows.
    820   *
    821   *        onResourceUpdate({ update }) {
    822   *          console.log(update.testValue); // “test” should be displayed
    823   *        }
    824   */
    825  async _onResourceUpdated({ targetFront, watcherFront }, updates) {
    826    for (const update of updates) {
    827      const {
    828        resourceType,
    829        resourceId,
    830        resourceUpdates,
    831        nestedResourceUpdates,
    832      } = update;
    833 
    834      if (!resourceId) {
    835        console.warn(`Expected resource ${resourceType} to have a resourceId`);
    836      }
    837 
    838      // See _onResourceAvailableArray()
    839      // We also need to wait for the related targetFront to be initialized
    840      // otherwise we would notify about the update *before* it's available
    841      // and the resource won't be in _cache.
    842      if (watcherFront) {
    843        try {
    844          targetFront = await this._getTargetForWatcherResource(update);
    845        } catch (e) {
    846          if (this.#destroyed) {
    847            // If the resource-command was destroyed while waiting for
    848            // _getTargetForWatcherResource, swallow the error.
    849            return;
    850          }
    851          throw e;
    852        }
    853 
    854        // When we receive the navigation request, the target front has already been
    855        // destroyed, but this is fine. The cached resource has the reference to
    856        // the (destroyed) target front and it is fully initialized.
    857        if (targetFront) {
    858          await targetFront.initialized;
    859        }
    860      }
    861 
    862      const existingResource = this._cache.get(
    863        cacheKey(resourceType, resourceId)
    864      );
    865      if (!existingResource) {
    866        continue;
    867      }
    868 
    869      if (resourceUpdates) {
    870        Object.assign(existingResource, resourceUpdates);
    871      }
    872 
    873      if (nestedResourceUpdates) {
    874        for (const { path, value } of nestedResourceUpdates) {
    875          let target = existingResource;
    876 
    877          for (let i = 0; i < path.length - 1; i++) {
    878            target = target[path[i]];
    879          }
    880 
    881          target[path[path.length - 1]] = value;
    882        }
    883      }
    884      this._queueResourceEvent("updated", resourceType, [
    885        {
    886          resource: existingResource,
    887          update,
    888        },
    889      ]);
    890    }
    891 
    892    this._throttledNotifyWatchers();
    893  }
    894 
    895  /**
    896   * Called every time a resource is destroyed in the remote target.
    897   *
    898   * @param {object} source
    899   *        A dictionary object with only one of these two attributes:
    900   *        - targetFront: a Target Front, if the resource is watched from the
    901   *          target process or thread.
    902   *        - watcherFront: a Watcher Front, if the resource is watched from
    903   *          the parent process.
    904   * @param {Array<json/Front>} resources
    905   *        Depending on the resource Type, it can be an Array composed of
    906   *        either JSON objects or Fronts, which describes the resource.
    907   */
    908  async _onResourceDestroyed({ targetFront }, resources) {
    909    for (const resource of resources) {
    910      const { resourceType, resourceId } = resource;
    911      this._cache.delete(cacheKey(resourceType, resourceId));
    912      if (!resource.targetFront) {
    913        resource.targetFront = targetFront;
    914      }
    915      this._queueResourceEvent("destroyed", resourceType, [resource]);
    916    }
    917    this._throttledNotifyWatchers();
    918  }
    919 
    920  _queueResourceEvent(callbackType, resourceType, updates) {
    921    for (const { resources, pendingEvents } of this._watchers) {
    922      // This watcher doesn't listen to this type of resource
    923      if (!resources.includes(resourceType)) {
    924        continue;
    925      }
    926      // Avoid trying to coalesce with last pending event as mutating `updates` may have side effects
    927      // with other watchers as this array is shared between all the watchers.
    928      pendingEvents.push({
    929        callbackType,
    930        updates,
    931      });
    932    }
    933  }
    934 
    935  /**
    936   * Flush the pending event and notify all the currently registered watchers
    937   * about all the available, updated and destroyed events that have been accumulated in
    938   * `_watchers`'s `pendingEvents` arrays.
    939   */
    940  _notifyWatchers() {
    941    for (const watcherEntry of this._watchers) {
    942      const { onAvailable, onUpdated, onDestroyed, pendingEvents } =
    943        watcherEntry;
    944      // Immediately clear the buffer in order to avoid possible races, where an event listener
    945      // would end up somehow adding a new throttled resource
    946      watcherEntry.pendingEvents = [];
    947 
    948      for (const { callbackType, updates } of pendingEvents) {
    949        try {
    950          if (callbackType == "available") {
    951            onAvailable(updates, { areExistingResources: false });
    952          } else if (callbackType == "updated" && onUpdated) {
    953            onUpdated(updates);
    954          } else if (callbackType == "destroyed" && onDestroyed) {
    955            onDestroyed(updates);
    956          }
    957        } catch (e) {
    958          console.error(
    959            "Exception while calling a ResourceCommand",
    960            callbackType,
    961            "callback",
    962            ":",
    963            e
    964          );
    965        }
    966      }
    967    }
    968  }
    969 
    970  // Compute the target front if the resource comes from the Watcher Actor.
    971  // (`targetFront` will be null as the watcher is in the parent process
    972  // and targets are in distinct processes)
    973  _getTargetForWatcherResource(resource) {
    974    const { browsingContextID, innerWindowId, resourceType } = resource;
    975 
    976    // Some privileged resources aren't related to any BrowsingContext
    977    // and so aren't bound to any Target Front.
    978    // Server watchers should pass an explicit "-1" value in order to prevent
    979    // silently ignoring an undefined browsingContextID attribute.
    980    if (browsingContextID == -1) {
    981      return this.targetCommand.targetFront;
    982    }
    983 
    984    if (innerWindowId && this.targetCommand.isServerTargetSwitchingEnabled()) {
    985      return this.watcherFront.getWindowGlobalTargetByInnerWindowId(
    986        innerWindowId
    987      );
    988    } else if (browsingContextID) {
    989      return this.watcherFront.getWindowGlobalTarget(browsingContextID);
    990    }
    991    console.error(
    992      `Resource of ${resourceType} is missing a browsingContextID or innerWindowId attribute`
    993    );
    994    return null;
    995  }
    996 
    997  _onWillNavigate() {
    998    // Special case for toolboxes debugging a document,
    999    // purge the cache entirely when we start navigating to a new document.
   1000    // Other toolboxes and additional target for remote iframes or content process
   1001    // will be purge from onTargetDestroyed.
   1002 
   1003    // NOTE: we could `clear` the cache here, but technically if anything is
   1004    // currently iterating over resources provided by getAllResources, that
   1005    // would interfere with their iteration. We just assign a new Map here to
   1006    // leave those iterators as is.
   1007    this._cache = new Map();
   1008  }
   1009 
   1010  /**
   1011   * Tells if the server supports listening to the given resource type
   1012   * via the watcher actor's watchResources method.
   1013   *
   1014   * @return {boolean} True, if the server supports this type.
   1015   */
   1016  hasResourceCommandSupport(resourceType) {
   1017    return this.watcherFront?.traits?.resources?.[resourceType];
   1018  }
   1019 
   1020  /**
   1021   * Tells if the server supports listening to the given resource type
   1022   * via the watcher actor's watchResources method, and that, for a specific
   1023   * target.
   1024   *
   1025   * @return {boolean} True, if the server supports this type.
   1026   */
   1027  _hasResourceCommandSupportForTarget(resourceType, targetFront) {
   1028    // First check if the watcher supports this target type.
   1029    // If it doesn't, no resource type can be listened via the Watcher actor for this target.
   1030    if (!this.targetCommand.hasTargetWatcherSupport(targetFront.targetType)) {
   1031      return false;
   1032    }
   1033 
   1034    return this.hasResourceCommandSupport(resourceType);
   1035  }
   1036 
   1037  _isValidResourceType(type) {
   1038    return this.ALL_TYPES.includes(type);
   1039  }
   1040 
   1041  /**
   1042   * Start listening for a given type of resource.
   1043   * For backward compatibility code, we register the legacy listeners on
   1044   * each individual target
   1045   *
   1046   * @param {string} resourceType
   1047   *        One string of ResourceCommand.TYPES, which designates the types of resources
   1048   *        to be listened.
   1049   * @param {object}
   1050   *        - {Boolean} bypassListenerCount
   1051   *          Pass true to avoid checking/updating the listenersCount map.
   1052   *          Exclusively used when target switching, to stop & start listening
   1053   *          to all resources.
   1054   */
   1055  async _startListening(resourceType, { bypassListenerCount = false } = {}) {
   1056    if (!bypassListenerCount) {
   1057      if (this._listenedResources.has(resourceType)) {
   1058        return;
   1059      }
   1060      this._listenedResources.add(resourceType);
   1061    }
   1062 
   1063    this._processingExistingResources.add(resourceType);
   1064 
   1065    // Ensuring enabling listening to targets.
   1066    // This will be a no-op expect for the very first call to `_startListening`,
   1067    // where it is going to call `onTargetAvailable` for all already existing targets,
   1068    // as well as for those who will be created later.
   1069    //
   1070    // Do this *before* calling WatcherActor.watchResources in order to register "resource-available"
   1071    // listeners on targets before these events start being emitted.
   1072    await this._watchAllTargets(resourceType);
   1073 
   1074    // When we are calling _startListening for the first time, _watchAllTargets
   1075    // will register legacylistener when it will call onTargetAvailable for all existing targets.
   1076    // But for any next calls to _startListening, _watchAllTargets will be a no-op,
   1077    // and nothing will start legacy listener for each already registered targets.
   1078    await this._startLegacyListenersForExistingTargets(resourceType);
   1079 
   1080    // If the server supports the Watcher API and the Watcher supports
   1081    // this resource type, use this API
   1082    if (this.hasResourceCommandSupport(resourceType)) {
   1083      await this.watcherFront.watchResources([resourceType]);
   1084    }
   1085    this._processingExistingResources.delete(resourceType);
   1086  }
   1087 
   1088  /**
   1089   * Return true if the resource should be watched via legacy listener,
   1090   * even when watcher supports this resource type.
   1091   *
   1092   * Bug 1678385: In order to support watching for JS Source resource
   1093   * for service workers and parent process workers, which aren't supported yet
   1094   * by the watcher actor, we do not bail out here and allow to execute
   1095   * the legacy listener for these targets.
   1096   * Once bug 1608848 is fixed, we can remove this and never trigger
   1097   * the legacy listeners codepath for these resource types.
   1098   *
   1099   * If this isn't fixed soon, we may add other resources we want to see
   1100   * being fetched from these targets.
   1101   */
   1102  _shouldRunLegacyListenerEvenWithWatcherSupport(resourceType) {
   1103    return WORKER_RESOURCE_TYPES.includes(resourceType);
   1104  }
   1105 
   1106  async _forwardExistingResources(resourceTypes, onAvailable) {
   1107    const existingResources = [];
   1108    for (const resource of this._cache.values()) {
   1109      if (resourceTypes.includes(resource.resourceType)) {
   1110        existingResources.push(resource);
   1111      }
   1112    }
   1113    if (existingResources.length) {
   1114      await onAvailable(existingResources, { areExistingResources: true });
   1115    }
   1116  }
   1117 
   1118  /**
   1119   * Call backward compatibility code from `LegacyListeners` in order to listen for a given
   1120   * type of resource from a given target.
   1121   */
   1122  async _watchResourcesForTarget({
   1123    targetFront,
   1124    resourceType,
   1125    disableWarning = false,
   1126  }) {
   1127    if (this._hasResourceCommandSupportForTarget(resourceType, targetFront)) {
   1128      // This resource / target pair should already be handled by the watcher,
   1129      // no need to start legacy listeners.
   1130      return;
   1131    }
   1132 
   1133    // All workers target types are still not supported by the watcher
   1134    // so that we have to spawn legacy listener for all their resources.
   1135    // But some resources are irrelevant to workers, like network events.
   1136    // And we removed the related legacy listener as they are no longer used.
   1137    if (
   1138      targetFront.targetType.endsWith("worker") &&
   1139      !WORKER_RESOURCE_TYPES.includes(resourceType)
   1140    ) {
   1141      return;
   1142    }
   1143 
   1144    if (targetFront.isDestroyed()) {
   1145      return;
   1146    }
   1147 
   1148    const onAvailableArray = this._onResourceAvailableArray.bind(this, {
   1149      targetFront,
   1150    });
   1151    const onUpdatedArray = this._onResourceUpdatedArray.bind(this, {
   1152      targetFront,
   1153    });
   1154    const onDestroyedArray = this._onResourceDestroyedArray.bind(this, {
   1155      targetFront,
   1156    });
   1157 
   1158    if (!(resourceType in LegacyListeners)) {
   1159      throw new Error(`Missing legacy listener for ${resourceType}`);
   1160    }
   1161 
   1162    const legacyListeners =
   1163      this._existingLegacyListeners.get(targetFront) || [];
   1164    if (legacyListeners.includes(resourceType)) {
   1165      if (!disableWarning) {
   1166        console.warn(
   1167          `Already started legacy listener for ${resourceType} on ${targetFront.actorID}`
   1168        );
   1169      }
   1170      return;
   1171    }
   1172    this._existingLegacyListeners.set(
   1173      targetFront,
   1174      legacyListeners.concat(resourceType)
   1175    );
   1176 
   1177    try {
   1178      await LegacyListeners[resourceType]({
   1179        targetCommand: this.targetCommand,
   1180        targetFront,
   1181        onAvailableArray,
   1182        onDestroyedArray,
   1183        onUpdatedArray,
   1184      });
   1185    } catch (e) {
   1186      // Swallow the error to avoid breaking calls to watchResources which will
   1187      // loop on all existing targets to create legacy listeners.
   1188      // If a legacy listener fails to handle a target for some reason, we
   1189      // should still try to process other targets as much as possible.
   1190      // See Bug 1687645.
   1191      console.error(
   1192        `Failed to start [${resourceType}] legacy listener for target ${targetFront.actorID}`,
   1193        e
   1194      );
   1195    }
   1196  }
   1197 
   1198  /**
   1199   * Reverse of _startListening. Stop listening for a given type of resource.
   1200   * For backward compatibility, we unregister from each individual target.
   1201   *
   1202   * See _startListening for parameters description.
   1203   */
   1204  _stopListening(resourceType, { bypassListenerCount = false } = {}) {
   1205    if (!bypassListenerCount) {
   1206      if (!this._listenedResources.has(resourceType)) {
   1207        throw new Error(
   1208          `Stopped listening for resource '${resourceType}' that isn't being listened to`
   1209        );
   1210      }
   1211      this._listenedResources.delete(resourceType);
   1212    }
   1213 
   1214    // Clear the cached resources of the type.
   1215    for (const [key, resource] of this._cache) {
   1216      if (resource.resourceType == resourceType) {
   1217        // NOTE: To anyone paranoid like me, yes it is okay to delete from a Map while iterating it.
   1218        this._cache.delete(key);
   1219      }
   1220    }
   1221 
   1222    // If the server supports the Watcher API and the Watcher supports
   1223    // this resource type, use this API
   1224    if (this.hasResourceCommandSupport(resourceType)) {
   1225      if (!this.watcherFront.isDestroyed()) {
   1226        this.watcherFront.unwatchResources([resourceType]);
   1227      }
   1228 
   1229      const shouldRunLegacyListeners =
   1230        this._shouldRunLegacyListenerEvenWithWatcherSupport(resourceType);
   1231      if (!shouldRunLegacyListeners) {
   1232        return;
   1233      }
   1234    }
   1235    // Otherwise, fallback on backward compat mode and use LegacyListeners.
   1236 
   1237    // If this was the last listener, we should stop watching these events from the actors
   1238    // and the actors should stop watching things from the platform
   1239    const targets = this.targetCommand.getAllTargets(
   1240      this.targetCommand.ALL_TYPES
   1241    );
   1242    for (const target of targets) {
   1243      this._unwatchResourcesForTarget(target, resourceType);
   1244    }
   1245  }
   1246 
   1247  /**
   1248   * Backward compatibility code, reverse of _watchResourcesForTarget.
   1249   */
   1250  _unwatchResourcesForTarget(targetFront, resourceType) {
   1251    if (this._hasResourceCommandSupportForTarget(resourceType, targetFront)) {
   1252      // This resource / target pair should already be handled by the watcher,
   1253      // no need to stop legacy listeners.
   1254    }
   1255    // Is there really a point in:
   1256    // - unregistering `onAvailable` RDP event callbacks from target-scoped actors?
   1257    // - calling `stopListeners()` as we are most likely closing the toolbox and destroying everything?
   1258    //
   1259    // It is important to keep this method synchronous and do as less as possible
   1260    // in the case of toolbox destroy.
   1261    //
   1262    // We are aware of one case where that might be useful.
   1263    // When a panel is disabled via the options panel, after it has been opened.
   1264    // Would that justify doing this? Is there another usecase?
   1265 
   1266    // XXX: This is most likely only needed to avoid growing the Map infinitely.
   1267    // Unless in the "disabled panel" use case mentioned in the comment above,
   1268    // we should not see the same target actorID again.
   1269    const listeners = this._existingLegacyListeners.get(targetFront);
   1270    if (listeners && listeners.includes(resourceType)) {
   1271      const remainingListeners = listeners.filter(l => l !== resourceType);
   1272      this._existingLegacyListeners.set(targetFront, remainingListeners);
   1273    }
   1274  }
   1275 }
   1276 
   1277 const DOCUMENT_EVENT = "document-event";
   1278 ResourceCommand.TYPES = ResourceCommand.prototype.TYPES = {
   1279  CONSOLE_MESSAGE: "console-message",
   1280  CSS_CHANGE: "css-change",
   1281  CSS_MESSAGE: "css-message",
   1282  CSS_REGISTERED_PROPERTIES: "css-registered-properties",
   1283  ERROR_MESSAGE: "error-message",
   1284  PLATFORM_MESSAGE: "platform-message",
   1285  DOCUMENT_EVENT,
   1286  ROOT_NODE: "root-node",
   1287  STYLESHEET: "stylesheet",
   1288  NETWORK_EVENT: "network-event",
   1289  WEBSOCKET: "websocket",
   1290  WEBTRANSPORT: "webtransport",
   1291  COOKIE: "cookies",
   1292  LOCAL_STORAGE: "local-storage",
   1293  SESSION_STORAGE: "session-storage",
   1294  CACHE_STORAGE: "Cache",
   1295  EXTENSION_STORAGE: "extension-storage",
   1296  INDEXED_DB: "indexed-db",
   1297  NETWORK_EVENT_STACKTRACE: "network-event-stacktrace",
   1298  REFLOW: "reflow",
   1299  SOURCE: "source",
   1300  THREAD_STATE: "thread-state",
   1301  JSTRACER_TRACE: "jstracer-trace",
   1302  JSTRACER_STATE: "jstracer-state",
   1303  SERVER_SENT_EVENT: "server-sent-event",
   1304  LAST_PRIVATE_CONTEXT_EXIT: "last-private-context-exit",
   1305 };
   1306 ResourceCommand.ALL_TYPES = ResourceCommand.prototype.ALL_TYPES = Object.values(
   1307  ResourceCommand.TYPES
   1308 );
   1309 module.exports = ResourceCommand;
   1310 
   1311 // This is the list of resource types supported by workers.
   1312 // We need such list to know when forcing to run the legacy listeners
   1313 // and when to avoid try to spawn some unsupported ones for workers.
   1314 const WORKER_RESOURCE_TYPES = [
   1315  ResourceCommand.TYPES.CONSOLE_MESSAGE,
   1316  ResourceCommand.TYPES.ERROR_MESSAGE,
   1317  ResourceCommand.TYPES.SOURCE,
   1318  ResourceCommand.TYPES.THREAD_STATE,
   1319 ];
   1320 
   1321 // List of resource types which aren't stored in the internal ResourceCommand cache.
   1322 // Only the first `watchResources()` call for a given resource type may receive already existing
   1323 // resources. All subsequent call to `watchResources()` for the same resource type will
   1324 // only receive future resource, and not the one already notified in the past.
   1325 // This is typically used for resources with very high throughput.
   1326 const TRANSIENT_RESOURCE_TYPES = [
   1327  ResourceCommand.TYPES.JSTRACER_TRACE,
   1328  ResourceCommand.TYPES.JSTRACER_STATE,
   1329 ];
   1330 
   1331 // Backward compat code for each type of resource.
   1332 // Each section added here should eventually be removed once the equivalent server
   1333 // code is implement in Firefox, in its release channel.
   1334 const LegacyListeners = {
   1335  async [ResourceCommand.TYPES.DOCUMENT_EVENT]({ targetFront, onAvailable }) {
   1336    // DocumentEventsListener of webconsole handles only top level document.
   1337    if (!targetFront.isTopLevel) {
   1338      return;
   1339    }
   1340 
   1341    const webConsoleFront = await targetFront.getFront("console");
   1342    webConsoleFront.on("documentEvent", event => {
   1343      event.resourceType = ResourceCommand.TYPES.DOCUMENT_EVENT;
   1344      onAvailable([event]);
   1345    });
   1346    await webConsoleFront.startListeners(["DocumentEvents"]);
   1347  },
   1348 };
   1349 loader.lazyRequireGetter(
   1350  LegacyListeners,
   1351  ResourceCommand.TYPES.CONSOLE_MESSAGE,
   1352  "resource://devtools/shared/commands/resource/legacy-listeners/console-messages.js"
   1353 );
   1354 loader.lazyRequireGetter(
   1355  LegacyListeners,
   1356  ResourceCommand.TYPES.CSS_MESSAGE,
   1357  "resource://devtools/shared/commands/resource/legacy-listeners/css-messages.js"
   1358 );
   1359 loader.lazyRequireGetter(
   1360  LegacyListeners,
   1361  ResourceCommand.TYPES.ERROR_MESSAGE,
   1362  "resource://devtools/shared/commands/resource/legacy-listeners/error-messages.js"
   1363 );
   1364 loader.lazyRequireGetter(
   1365  LegacyListeners,
   1366  ResourceCommand.TYPES.PLATFORM_MESSAGE,
   1367  "resource://devtools/shared/commands/resource/legacy-listeners/platform-messages.js"
   1368 );
   1369 loader.lazyRequireGetter(
   1370  LegacyListeners,
   1371  ResourceCommand.TYPES.ROOT_NODE,
   1372  "resource://devtools/shared/commands/resource/legacy-listeners/root-node.js"
   1373 );
   1374 
   1375 loader.lazyRequireGetter(
   1376  LegacyListeners,
   1377  ResourceCommand.TYPES.SOURCE,
   1378  "resource://devtools/shared/commands/resource/legacy-listeners/source.js"
   1379 );
   1380 loader.lazyRequireGetter(
   1381  LegacyListeners,
   1382  ResourceCommand.TYPES.THREAD_STATE,
   1383  "resource://devtools/shared/commands/resource/legacy-listeners/thread-states.js"
   1384 );
   1385 
   1386 loader.lazyRequireGetter(
   1387  LegacyListeners,
   1388  ResourceCommand.TYPES.REFLOW,
   1389  "resource://devtools/shared/commands/resource/legacy-listeners/reflow.js"
   1390 );
   1391 
   1392 // Optional transformers for each type of resource.
   1393 // Each module added here should be a function that will receive the resource, the target, …
   1394 // and perform some transformation on the resource before it will be emitted.
   1395 // This is a good place to handle backward compatibility and manual resource marshalling.
   1396 const ResourceTransformers = {};
   1397 
   1398 loader.lazyRequireGetter(
   1399  ResourceTransformers,
   1400  ResourceCommand.TYPES.CONSOLE_MESSAGE,
   1401  "resource://devtools/shared/commands/resource/transformers/console-messages.js"
   1402 );
   1403 loader.lazyRequireGetter(
   1404  ResourceTransformers,
   1405  ResourceCommand.TYPES.ERROR_MESSAGE,
   1406  "resource://devtools/shared/commands/resource/transformers/error-messages.js"
   1407 );
   1408 loader.lazyRequireGetter(
   1409  ResourceTransformers,
   1410  ResourceCommand.TYPES.CACHE_STORAGE,
   1411  "resource://devtools/shared/commands/resource/transformers/storage-cache.js"
   1412 );
   1413 loader.lazyRequireGetter(
   1414  ResourceTransformers,
   1415  ResourceCommand.TYPES.COOKIE,
   1416  "resource://devtools/shared/commands/resource/transformers/storage-cookie.js"
   1417 );
   1418 loader.lazyRequireGetter(
   1419  ResourceTransformers,
   1420  ResourceCommand.TYPES.EXTENSION_STORAGE,
   1421  "resource://devtools/shared/commands/resource/transformers/storage-extension.js"
   1422 );
   1423 loader.lazyRequireGetter(
   1424  ResourceTransformers,
   1425  ResourceCommand.TYPES.INDEXED_DB,
   1426  "resource://devtools/shared/commands/resource/transformers/storage-indexed-db.js"
   1427 );
   1428 loader.lazyRequireGetter(
   1429  ResourceTransformers,
   1430  ResourceCommand.TYPES.LOCAL_STORAGE,
   1431  "resource://devtools/shared/commands/resource/transformers/storage-local-storage.js"
   1432 );
   1433 loader.lazyRequireGetter(
   1434  ResourceTransformers,
   1435  ResourceCommand.TYPES.SESSION_STORAGE,
   1436  "resource://devtools/shared/commands/resource/transformers/storage-session-storage.js"
   1437 );
   1438 loader.lazyRequireGetter(
   1439  ResourceTransformers,
   1440  ResourceCommand.TYPES.NETWORK_EVENT,
   1441  "resource://devtools/shared/commands/resource/transformers/network-events.js"
   1442 );
   1443 loader.lazyRequireGetter(
   1444  ResourceTransformers,
   1445  ResourceCommand.TYPES.THREAD_STATE,
   1446  "resource://devtools/shared/commands/resource/transformers/thread-states.js"
   1447 );