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 }