tor-browser

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

LinksCache.sys.mjs (4733B)


      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 // This should be slightly less than SYSTEM_TICK_INTERVAL as timer
      6 // comparisons are too exact while the async/await functionality will make the
      7 // last recorded time a little bit later. This causes the comparasion to skip
      8 // updates.
      9 // It should be 10% less than SYSTEM_TICK to update at least once every 5 mins.
     10 // https://github.com/mozilla/activity-stream/pull/3695#discussion_r144678214
     11 const EXPIRATION_TIME = 4.5 * 60 * 1000; // 4.5 minutes
     12 
     13 /**
     14 * Cache link results from a provided object property and refresh after some
     15 * amount of time has passed. Allows for migrating data from previously cached
     16 * links to the new links with the same url.
     17 */
     18 export class LinksCache {
     19  /**
     20   * Create a links cache for a given object property.
     21   *
     22   * @param {object} linkObject Object containing the link property
     23   * @param {string} linkProperty Name of property on object to access
     24   * @param {Array} properties Optional properties list to migrate to new links.
     25   * @param {function} shouldRefresh Optional callback receiving the old and new
     26   *                                 options to refresh even when not expired.
     27   */
     28  constructor(
     29    linkObject,
     30    linkProperty,
     31    properties = [],
     32    shouldRefresh = () => {}
     33  ) {
     34    this.clear();
     35 
     36    // Allow getting links from both methods and array properties
     37    this.linkGetter = options => {
     38      const ret = linkObject[linkProperty];
     39      return typeof ret === "function" ? ret.call(linkObject, options) : ret;
     40    };
     41 
     42    // Always migrate the shared cache data in addition to any custom properties
     43    this.migrateProperties = ["__sharedCache", ...properties];
     44    this.shouldRefresh = shouldRefresh;
     45  }
     46 
     47  /**
     48   * Clear the cached data.
     49   */
     50  clear() {
     51    this.cache = Promise.resolve([]);
     52    this.lastOptions = {};
     53    this.expire();
     54  }
     55 
     56  /**
     57   * Force the next request to update the cache.
     58   */
     59  expire() {
     60    delete this.lastUpdate;
     61  }
     62 
     63  /**
     64   * Request data and update the cache if necessary.
     65   *
     66   * @param {object} options Optional data to pass to the underlying method.
     67   * @returns {promise(array)} Links array with objects that can be modified.
     68   */
     69  async request(options = {}) {
     70    // Update the cache if the data has been expired
     71    const now = Date.now();
     72    if (
     73      this.lastUpdate === undefined ||
     74      now > this.lastUpdate + EXPIRATION_TIME ||
     75      // Allow custom rules around refreshing based on options
     76      this.shouldRefresh(this.lastOptions, options)
     77    ) {
     78      // Update request state early so concurrent requests can refer to it
     79      this.lastOptions = options;
     80      this.lastUpdate = now;
     81 
     82      // Save a promise before awaits, so other requests wait for correct data
     83      // eslint-disable-next-line no-async-promise-executor
     84      this.cache = new Promise(async (resolve, reject) => {
     85        try {
     86          // Allow fast lookup of old links by url that might need to migrate
     87          const toMigrate = new Map();
     88          for (const oldLink of await this.cache) {
     89            if (oldLink) {
     90              toMigrate.set(oldLink.url, oldLink);
     91            }
     92          }
     93 
     94          // Update the cache with migrated links without modifying source objects
     95          resolve(
     96            (await this.linkGetter(options)).map(link => {
     97              // Keep original array hole positions
     98              if (!link) {
     99                return link;
    100              }
    101 
    102              // Migrate data to the new link copy if we have an old link
    103              const newLink = Object.assign({}, link);
    104              const oldLink = toMigrate.get(newLink.url);
    105              if (oldLink) {
    106                for (const property of this.migrateProperties) {
    107                  const oldValue = oldLink[property];
    108                  if (oldValue !== undefined) {
    109                    newLink[property] = oldValue;
    110                  }
    111                }
    112              } else {
    113                // Share data among link copies and new links from future requests
    114                newLink.__sharedCache = {};
    115              }
    116              // Provide a helper to update the cached link
    117              newLink.__sharedCache.updateLink = (property, value) => {
    118                newLink[property] = value;
    119              };
    120 
    121              return newLink;
    122            })
    123          );
    124        } catch (error) {
    125          reject(error);
    126        }
    127      });
    128    }
    129 
    130    // Provide a shallow copy of the cached link objects for callers to modify
    131    return (await this.cache).map(link => link && Object.assign({}, link));
    132  }
    133 }