const constants = require("./constants") /** * @template T */ class TtlCache { /** * @param {number} ttl time to keep each resource in milliseconds */ constructor(ttl) { this.ttl = ttl /** @type {Map} */ this.cache = new Map() this.sweepInterval = setInterval(() => { this.clean() }, constants.caching.cache_sweep_interval) this.sweepInterval.unref() } clean() { for (const key of this.cache.keys()) { this.cleanKey(key) } } cleanKey(key) { const value = this.cache.get(key) if (value && Date.now() > value.time + this.ttl) this.cache.delete(key) } /** * @param {string} key */ has(key) { this.cleanKey(key) return this.hasWithoutClean(key) } hasWithoutClean(key) { return this.cache.has(key) } hasNotPromise(key) { const has = this.has(key) if (!has) return false const value = this.get(key) if (value instanceof Promise || (value.constructor && value.constructor.name === "Promise")) return false return true } /** * @param {string} key */ get(key) { this.cleanKey(key) return this.getWithoutClean(key) } getWithoutClean(key) { const value = this.cache.get(key) if (value) return value.data else return null } /** * Returns null if doesn't exist * @param {string} key * @param {number} factor factor to divide the result by. use 60*1000 to get the ttl in minutes. */ getTtl(key, factor = 1) { if (this.has(key)) { return Math.max(Math.ceil((this.cache.get(key).time + this.ttl - Date.now()) / factor), 0) } else { return null } } /** * @param {string} key * @param {any} data */ set(key, data) { this.cache.set(key, {data, time: Date.now()}) } /** * @param {string} key */ refresh(key) { this.cache.get(key).time = Date.now() } } /** * @extends TtlCache * @template T */ class RequestCache extends TtlCache { /** * @param {number} ttl time to keep each resource in milliseconds */ constructor(ttl) { super(ttl) } /** * @param {string} key * @param {() => Promise} callback * @returns {Promise} */ getOrFetch(key, callback) { this.cleanKey(key) if (this.cache.has(key)) return Promise.resolve(this.get(key)) else { const pending = callback().then(result => { this.set(key, result) return result }) this.set(key, pending) return pending } } /** * @param {string} key * @param {() => Promise} callback * @returns {Promise} */ getOrFetchPromise(key, callback) { return this.getOrFetch(key, callback).then(result => { this.cache.delete(key) return result }) } } /** * @template T */ class UserRequestCache extends TtlCache { constructor(ttl) { super(ttl) /** @type {Map} */ this.cache } /** * @param {string} key * @param {boolean} isReel * @param {any} [data] */ set(key, isReel, data) { const existing = this.cache.get(key) // Preserve html failure status if now requesting as reel const htmlFailed = isReel && existing && existing.htmlFailed this.cache.set(key, {data, isReel, isFailedPromise: false, htmlFailed, time: Date.now()}) } /** * @param {string} key * @param {boolean} isHtmlPreferred * @param {boolean} willFetchReel * @param {() => Promise} callback * @returns {Promise} */ getOrFetch(key, willFetchReel, isHtmlPreferred, callback) { this.cleanKey(key) if (this.cache.has(key)) { const existing = this.cache.get(key) if (!existing.isFailedPromise) { // if the existing entry contains usable data if (!existing.isReel) { // hurrah, the best we could get! return Promise.resolve(existing.data) } // we don't have HTML, only reel if (!isHtmlPreferred) { // well that's cool, we only wanted reel anyway return Promise.resolve(existing.data) } else { // (isHtmlPreferred ~= true): we'd _like_ some HTML, but we don't have it currently. if HTML is blocked then using reel is smart if (existing.htmlFailed) { // HTML is in fact blocked, so we will have to settle for reel. fortunately we already have reel! return Promise.resolve(existing.data) } } } else { // (existing.isFailedPromise ~= true): the existing entry is a failed request if (existing.htmlFailed && !willFetchReel) { // it's no use! the HTML attempt will fail again: abandon it. return Promise.resolve(existing.data) // this is actually a promise rejection } } } const pending = callback().then(result => { if (this.getWithoutClean(key) === pending) { // if nothing has replaced the current cache in the meantime this.set(key, willFetchReel, result) } return result }).catch(error => { this.cache.get(key).htmlFailed = true this.cache.get(key).isFailedPromise = true throw error }) this.set(key, willFetchReel, pending) return pending } } module.exports.TtlCache = TtlCache module.exports.RequestCache = RequestCache module.exports.UserRequestCache = UserRequestCache