summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/LinksCache.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/lib/LinksCache.jsm')
-rw-r--r--browser/components/newtab/lib/LinksCache.jsm136
1 files changed, 136 insertions, 0 deletions
diff --git a/browser/components/newtab/lib/LinksCache.jsm b/browser/components/newtab/lib/LinksCache.jsm
new file mode 100644
index 0000000000..c0a73cc18e
--- /dev/null
+++ b/browser/components/newtab/lib/LinksCache.jsm
@@ -0,0 +1,136 @@
+/* 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";
+
+const 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.
+ */
+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
+ // eslint-disable-next-line no-async-promise-executor
+ this.cache = new Promise(async (resolve, reject) => {
+ try {
+ // 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 !== undefined) {
+ 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
+ newLink.__sharedCache.updateLink = (property, value) => {
+ newLink[property] = value;
+ };
+
+ return newLink;
+ })
+ );
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ // Provide a shallow copy of the cached link objects for callers to modify
+ return (await this.cache).map(link => link && Object.assign({}, link));
+ }
+}