tor-browser

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

root-resource-command.js (11191B)


      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 class RootResourceCommand {
     10  /**
     11   * This class helps retrieving existing and listening to "root" resources.
     12   *
     13   * This is a fork of ResourceCommand, but specific to context-less
     14   * resources which can be listened to right away when connecting to the RDP server.
     15   *
     16   * The main difference in term of implementation is that:
     17   * - we receive a root front as constructor argument (instead of `commands` object)
     18   * - we only listen for RDP events on the Root actor (instead of watcher and target actors)
     19   * - there is no legacy listener support
     20   * - there is no resource transformers
     21   * - there is a lot of logic around targets that is removed here.
     22   *
     23   * See ResourceCommand for comments and jsdoc.
     24   *
     25   * TODO Bug 1758530 - Investigate sharing code with ResourceCommand instead of forking.
     26   *
     27   * @param object commands
     28   *        The commands object with all interfaces defined from devtools/shared/commands/
     29   * @param object rootFront
     30   *        Front for the Root actor.
     31   */
     32  constructor({ commands, rootFront }) {
     33    this.rootFront = rootFront ? rootFront : commands.client.mainRoot;
     34 
     35    this._onResourceAvailable = this._onResourceAvailable.bind(this);
     36    this._onResourceDestroyed = this._onResourceDestroyed.bind(this);
     37    this._onResourceAvailableArray = this._onResourceAvailableArray.bind(this);
     38    this._onResourceDestroyedArray = this._onResourceDestroyedArray.bind(this);
     39 
     40    this._watchers = [];
     41 
     42    this._pendingWatchers = new Set();
     43 
     44    this._cache = [];
     45    this._listenedResources = new Set();
     46 
     47    this._processingExistingResources = new Set();
     48 
     49    this._notifyWatchers = this._notifyWatchers.bind(this);
     50    this._throttledNotifyWatchers = throttle(this._notifyWatchers, 100);
     51  }
     52 
     53  getAllResources(resourceType) {
     54    return this._cache.filter(r => r.resourceType === resourceType);
     55  }
     56 
     57  getResourceById(resourceType, resourceId) {
     58    return this._cache.find(
     59      r => r.resourceType === resourceType && r.resourceId === resourceId
     60    );
     61  }
     62 
     63  async watchResources(resources, options) {
     64    const {
     65      onAvailable,
     66      onUpdated,
     67      onDestroyed,
     68      ignoreExistingResources = false,
     69    } = options;
     70 
     71    if (typeof onAvailable !== "function") {
     72      throw new Error(
     73        "RootResourceCommand.watchResources expects an onAvailable function as argument"
     74      );
     75    }
     76 
     77    for (const type of resources) {
     78      if (!this._isValidResourceType(type)) {
     79        throw new Error(
     80          `RootResourceCommand.watchResources invoked with an unknown type: "${type}"`
     81        );
     82      }
     83    }
     84 
     85    const pendingWatcher = {
     86      resources,
     87      onAvailable,
     88    };
     89    this._pendingWatchers.add(pendingWatcher);
     90 
     91    if (!this._listenerRegistered) {
     92      this._listenerRegistered = true;
     93      this.rootFront.on(
     94        "resources-available-array",
     95        this._onResourceAvailableArray
     96      );
     97      this.rootFront.on(
     98        "resources-destroyed-array",
     99        this._onResourceDestroyedArray
    100      );
    101    }
    102 
    103    const promises = [];
    104    for (const resource of resources) {
    105      promises.push(this._startListening(resource));
    106    }
    107    await Promise.all(promises);
    108 
    109    this._notifyWatchers();
    110 
    111    this._pendingWatchers.delete(pendingWatcher);
    112 
    113    const watchedResources = pendingWatcher.resources;
    114 
    115    if (!watchedResources.length) {
    116      return;
    117    }
    118 
    119    this._watchers.push({
    120      resources: watchedResources,
    121      onAvailable,
    122      onUpdated,
    123      onDestroyed,
    124      pendingEvents: [],
    125    });
    126 
    127    if (!ignoreExistingResources) {
    128      await this._forwardExistingResources(watchedResources, onAvailable);
    129    }
    130  }
    131 
    132  unwatchResources(resources, options) {
    133    const { onAvailable } = options;
    134 
    135    if (typeof onAvailable !== "function") {
    136      throw new Error(
    137        "RootResourceCommand.unwatchResources expects an onAvailable function as argument"
    138      );
    139    }
    140 
    141    for (const type of resources) {
    142      if (!this._isValidResourceType(type)) {
    143        throw new Error(
    144          `RootResourceCommand.unwatchResources invoked with an unknown type: "${type}"`
    145        );
    146      }
    147    }
    148 
    149    const allWatchers = [...this._watchers, ...this._pendingWatchers];
    150    for (const watcherEntry of allWatchers) {
    151      if (watcherEntry.onAvailable == onAvailable) {
    152        watcherEntry.resources = watcherEntry.resources.filter(resourceType => {
    153          return !resources.includes(resourceType);
    154        });
    155      }
    156    }
    157    this._watchers = this._watchers.filter(entry => {
    158      return !!entry.resources.length;
    159    });
    160 
    161    for (const resource of resources) {
    162      const isResourceWatched = allWatchers.some(watcherEntry =>
    163        watcherEntry.resources.includes(resource)
    164      );
    165 
    166      if (!isResourceWatched && this._listenedResources.has(resource)) {
    167        this._stopListening(resource);
    168      }
    169    }
    170  }
    171 
    172  clearResources(resourceTypes) {
    173    if (!Array.isArray(resourceTypes)) {
    174      throw new Error("clearResources expects an array of resource types");
    175    }
    176    // Clear the cached resources of the type.
    177    this._cache = this._cache.filter(
    178      cachedResource => !resourceTypes.includes(cachedResource.resourceType)
    179    );
    180 
    181    if (resourceTypes.length) {
    182      this.rootFront.clearResources(resourceTypes);
    183    }
    184  }
    185 
    186  async waitForNextResource(
    187    resourceType,
    188    { ignoreExistingResources = false, predicate } = {}
    189  ) {
    190    predicate = predicate || (resource => !!resource);
    191 
    192    let resolve;
    193    const promise = new Promise(r => (resolve = r));
    194    const onAvailable = async resources => {
    195      const matchingResource = resources.find(resource => predicate(resource));
    196      if (matchingResource) {
    197        this.unwatchResources([resourceType], { onAvailable });
    198        resolve(matchingResource);
    199      }
    200    };
    201 
    202    await this.watchResources([resourceType], {
    203      ignoreExistingResources,
    204      onAvailable,
    205    });
    206    return { onResource: promise };
    207  }
    208 
    209  async _onResourceAvailableArray(array) {
    210    for (const [resourceType, resources] of array) {
    211      for (const resource of resources) {
    212        if (!("resourceType" in resource)) {
    213          resource.resourceType = resourceType;
    214        }
    215      }
    216      this._onResourceAvailable(resources);
    217    }
    218  }
    219 
    220  async _onResourceDestroyedArray(context, array) {
    221    for (const [resourceType, resources] of array) {
    222      for (const resource of resources) {
    223        if (!("resourceType" in resource)) {
    224          resource.resourceType = resourceType;
    225        }
    226      }
    227      this._onResourceDestroyed(resources);
    228    }
    229  }
    230 
    231  _onResourceAvailable(resources) {
    232    for (const resource of resources) {
    233      const { resourceType } = resource;
    234 
    235      resource.isAlreadyExistingResource =
    236        this._processingExistingResources.has(resourceType);
    237 
    238      this._queueResourceEvent("available", resourceType, resource);
    239 
    240      this._cache.push(resource);
    241    }
    242 
    243    this._throttledNotifyWatchers();
    244  }
    245 
    246  _onResourceDestroyed(resources) {
    247    for (const resource of resources) {
    248      const { resourceType, resourceId } = resource;
    249 
    250      let index = -1;
    251      if (resourceId) {
    252        index = this._cache.findIndex(
    253          cachedResource =>
    254            cachedResource.resourceType == resourceType &&
    255            cachedResource.resourceId == resourceId
    256        );
    257      } else {
    258        index = this._cache.indexOf(resource);
    259      }
    260      if (index >= 0) {
    261        this._cache.splice(index, 1);
    262      } else {
    263        console.warn(
    264          `Resource ${resourceId || ""} of ${resourceType} was not found.`
    265        );
    266      }
    267 
    268      this._queueResourceEvent("destroyed", resourceType, resource);
    269    }
    270    this._throttledNotifyWatchers();
    271  }
    272 
    273  _queueResourceEvent(callbackType, resourceType, update) {
    274    for (const { resources, pendingEvents } of this._watchers) {
    275      if (!resources.includes(resourceType)) {
    276        continue;
    277      }
    278      if (pendingEvents.length) {
    279        const lastEvent = pendingEvents[pendingEvents.length - 1];
    280        if (lastEvent.callbackType == callbackType) {
    281          lastEvent.updates.push(update);
    282          continue;
    283        }
    284      }
    285      pendingEvents.push({
    286        callbackType,
    287        updates: [update],
    288      });
    289    }
    290  }
    291 
    292  _notifyWatchers() {
    293    for (const watcherEntry of this._watchers) {
    294      const { onAvailable, onDestroyed, pendingEvents } = watcherEntry;
    295      watcherEntry.pendingEvents = [];
    296 
    297      for (const { callbackType, updates } of pendingEvents) {
    298        try {
    299          if (callbackType == "available") {
    300            onAvailable(updates, { areExistingResources: false });
    301          } else if (callbackType == "destroyed" && onDestroyed) {
    302            onDestroyed(updates);
    303          }
    304        } catch (e) {
    305          console.error(
    306            "Exception while calling a RootResourceCommand",
    307            callbackType,
    308            "callback",
    309            ":",
    310            e
    311          );
    312        }
    313      }
    314    }
    315  }
    316 
    317  _isValidResourceType(type) {
    318    return this.ALL_TYPES.includes(type);
    319  }
    320 
    321  async _startListening(resourceType) {
    322    if (this._listenedResources.has(resourceType)) {
    323      return;
    324    }
    325    this._listenedResources.add(resourceType);
    326 
    327    this._processingExistingResources.add(resourceType);
    328 
    329    // For now, if the server doesn't support the resource type
    330    // act as if we were listening, but do nothing.
    331    // Calling watchResources/unwatchResources will work fine,
    332    // but no resource will be notified.
    333    if (this.rootFront.traits.resources?.[resourceType]) {
    334      await this.rootFront.watchResources([resourceType]);
    335    } else {
    336      console.warn(
    337        `Ignored watchRequest, resourceType "${resourceType}" not found in rootFront.traits.resources`
    338      );
    339    }
    340    this._processingExistingResources.delete(resourceType);
    341  }
    342 
    343  async _forwardExistingResources(resourceTypes, onAvailable) {
    344    const existingResources = this._cache.filter(resource =>
    345      resourceTypes.includes(resource.resourceType)
    346    );
    347    if (existingResources.length) {
    348      await onAvailable(existingResources, { areExistingResources: true });
    349    }
    350  }
    351 
    352  _stopListening(resourceType) {
    353    if (!this._listenedResources.has(resourceType)) {
    354      throw new Error(
    355        `Stopped listening for resource '${resourceType}' that isn't being listened to`
    356      );
    357    }
    358    this._listenedResources.delete(resourceType);
    359 
    360    this._cache = this._cache.filter(
    361      cachedResource => cachedResource.resourceType !== resourceType
    362    );
    363 
    364    if (
    365      !this.rootFront.isDestroyed() &&
    366      this.rootFront.traits.resources?.[resourceType]
    367    ) {
    368      this.rootFront.unwatchResources([resourceType]);
    369    }
    370  }
    371 }
    372 
    373 RootResourceCommand.TYPES = RootResourceCommand.prototype.TYPES = {
    374  EXTENSIONS_BGSCRIPT_STATUS: "extensions-backgroundscript-status",
    375 };
    376 RootResourceCommand.ALL_TYPES = RootResourceCommand.prototype.ALL_TYPES =
    377  Object.values(RootResourceCommand.TYPES);
    378 module.exports = RootResourceCommand;