browser/extensions/activity-stream/lib/LinksCache.jsm
author Ed Lee <edilee@mozilla.com>
Fri, 20 Oct 2017 15:11:28 -0700
changeset 440942 d1f0c44b2d7935ffacef0cdde5d4078259377613
parent 438192 8095eff05ac94898f4c8bf28340533f5aa7eaf68
child 443416 def95432b0a17a1b29bef1e6aea9305d69572fb9
permissions -rw-r--r--
Bug 1410541 - Add prerendered locales, preloaded pings and bug fixes to Activity Stream. r=k88hudson MozReview-Commit-ID: 81WygivxBoG

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

this.EXPORTED_SYMBOLS = ["LinksCache"];

// This should be slightly less than SYSTEM_TICK_INTERVAL as timer
// comparisons are too exact while the async/await functionality will make the
// last recorded time a little bit later. This causes the comparasion to skip
// updates.
// It should be 10% less than SYSTEM_TICK to update at least once every 5 mins.
// https://github.com/mozilla/activity-stream/pull/3695#discussion_r144678214
const EXPIRATION_TIME = 4.5 * 60 * 1000; // 4.5 minutes

/**
 * Cache link results from a provided object property and refresh after some
 * amount of time has passed. Allows for migrating data from previously cached
 * links to the new links with the same url.
 */
this.LinksCache = class LinksCache {
  /**
   * Create a links cache for a given object property.
   *
   * @param {object} linkObject Object containing the link property
   * @param {string} linkProperty Name of property on object to access
   * @param {array} properties Optional properties list to migrate to new links.
   * @param {function} shouldRefresh Optional callback receiving the old and new
   *                                 options to refresh even when not expired.
   */
  constructor(linkObject, linkProperty, properties = [], shouldRefresh = () => {}) {
    this.clear();

    // Allow getting links from both methods and array properties
    this.linkGetter = options => {
      const ret = linkObject[linkProperty];
      return typeof ret === "function" ? ret.call(linkObject, options) : ret;
    };

    // Always migrate the shared cache data in addition to any custom properties
    this.migrateProperties = ["__sharedCache", ...properties];
    this.shouldRefresh = shouldRefresh;
  }

  /**
   * Clear the cached data.
   */
  clear() {
    this.cache = Promise.resolve([]);
    this.lastOptions = {};
    this.expire();
  }

  /**
   * Force the next request to update the cache.
   */
  expire() {
    delete this.lastUpdate;
  }

  /**
   * Request data and update the cache if necessary.
   *
   * @param {object} options Optional data to pass to the underlying method.
   * @returns {promise(array)} Links array with objects that can be modified.
   */
  async request(options = {}) {
    // Update the cache if the data has been expired
    const now = Date.now();
    if (this.lastUpdate === undefined ||
        now > this.lastUpdate + EXPIRATION_TIME ||
        // Allow custom rules around refreshing based on options
        this.shouldRefresh(this.lastOptions, options)) {
      // Update request state early so concurrent requests can refer to it
      this.lastOptions = options;
      this.lastUpdate = now;

      // Save a promise before awaits, so other requests wait for correct data
      this.cache = new Promise(async resolve => {
        // Allow fast lookup of old links by url that might need to migrate
        const toMigrate = new Map();
        for (const oldLink of await this.cache) {
          if (oldLink) {
            toMigrate.set(oldLink.url, oldLink);
          }
        }

        // Update the cache with migrated links without modifying source objects
        resolve((await this.linkGetter(options)).map(link => {
          // Keep original array hole positions
          if (!link) {
            return link;
          }

          // Migrate data to the new link copy if we have an old link
          const newLink = Object.assign({}, link);
          const oldLink = toMigrate.get(newLink.url);
          if (oldLink) {
            for (const property of this.migrateProperties) {
              const oldValue = oldLink[property];
              if (oldValue) {
                newLink[property] = oldValue;
              }
            }
          } else {
            // Share data among link copies and new links from future requests
            newLink.__sharedCache = {
              // Provide a helper to update the cached link
              updateLink(property, value) {
                newLink[property] = value;
              }
            };
          }
          return newLink;
        }));
      });
    }

    // Provide a shallow copy of the cached link objects for callers to modify
    return (await this.cache).map(link => link && Object.assign({}, link));
  }
};