2020-03-18 08:14:12 +00:00
|
|
|
const constants = require("./constants")
|
|
|
|
|
2020-01-18 15:38:14 +00:00
|
|
|
/**
|
|
|
|
* @template T
|
|
|
|
*/
|
|
|
|
class TtlCache {
|
2020-01-12 12:50:21 +00:00
|
|
|
/**
|
2020-01-18 15:38:14 +00:00
|
|
|
* @param {number} ttl time to keep each resource in milliseconds
|
2020-01-12 12:50:21 +00:00
|
|
|
*/
|
|
|
|
constructor(ttl) {
|
|
|
|
this.ttl = ttl
|
2020-01-18 15:38:14 +00:00
|
|
|
/** @type {Map<string, {data: T, time: number}>} */
|
2020-01-12 12:50:21 +00:00
|
|
|
this.cache = new Map()
|
2020-03-01 03:49:16 +00:00
|
|
|
this.sweepInterval = setInterval(() => {
|
|
|
|
this.clean()
|
2020-03-18 08:14:12 +00:00
|
|
|
}, constants.caching.cache_sweep_interval)
|
2020-06-20 16:09:36 +00:00
|
|
|
this.sweepInterval.unref()
|
2020-01-12 12:50:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
clean() {
|
|
|
|
for (const key of this.cache.keys()) {
|
2020-01-26 14:56:59 +00:00
|
|
|
this.cleanKey(key)
|
2020-01-12 12:50:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-26 14:56:59 +00:00
|
|
|
cleanKey(key) {
|
|
|
|
const value = this.cache.get(key)
|
|
|
|
if (value && Date.now() > value.time + this.ttl) this.cache.delete(key)
|
|
|
|
}
|
|
|
|
|
2020-01-18 15:38:14 +00:00
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
*/
|
|
|
|
has(key) {
|
2020-01-26 14:56:59 +00:00
|
|
|
this.cleanKey(key)
|
|
|
|
return this.hasWithoutClean(key)
|
|
|
|
}
|
|
|
|
|
|
|
|
hasWithoutClean(key) {
|
2020-01-18 15:38:14 +00:00
|
|
|
return this.cache.has(key)
|
|
|
|
}
|
|
|
|
|
2020-02-02 13:24:14 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-01-12 12:50:21 +00:00
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
*/
|
|
|
|
get(key) {
|
2020-01-26 14:56:59 +00:00
|
|
|
this.cleanKey(key)
|
|
|
|
return this.getWithoutClean(key)
|
|
|
|
}
|
|
|
|
|
|
|
|
getWithoutClean(key) {
|
2020-01-18 15:38:14 +00:00
|
|
|
const value = this.cache.get(key)
|
|
|
|
if (value) return value.data
|
|
|
|
else return null
|
2020-01-12 12:50:21 +00:00
|
|
|
}
|
|
|
|
|
2020-01-18 15:38:14 +00:00
|
|
|
/**
|
2020-01-26 14:56:59 +00:00
|
|
|
* Returns null if doesn't exist
|
2020-01-18 15:38:14 +00:00
|
|
|
* @param {string} key
|
|
|
|
* @param {number} factor factor to divide the result by. use 60*1000 to get the ttl in minutes.
|
|
|
|
*/
|
2020-01-14 14:38:33 +00:00
|
|
|
getTtl(key, factor = 1) {
|
2020-01-26 14:56:59 +00:00
|
|
|
if (this.has(key)) {
|
2020-01-30 03:05:43 +00:00
|
|
|
return Math.max(Math.ceil((this.cache.get(key).time + this.ttl - Date.now()) / factor), 0)
|
2020-01-26 14:56:59 +00:00
|
|
|
} else {
|
|
|
|
return null
|
|
|
|
}
|
2020-01-14 14:38:33 +00:00
|
|
|
}
|
|
|
|
|
2020-01-12 12:50:21 +00:00
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
* @param {any} data
|
|
|
|
*/
|
|
|
|
set(key, data) {
|
|
|
|
this.cache.set(key, {data, time: Date.now()})
|
|
|
|
}
|
2020-01-26 14:56:59 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
*/
|
|
|
|
refresh(key) {
|
|
|
|
this.cache.get(key).time = Date.now()
|
|
|
|
}
|
2020-01-18 15:38:14 +00:00
|
|
|
}
|
|
|
|
|
2020-02-02 14:53:37 +00:00
|
|
|
/**
|
2020-07-29 03:32:04 +00:00
|
|
|
* @extends TtlCache<Promise<T>>
|
2020-02-02 14:53:37 +00:00
|
|
|
* @template T
|
|
|
|
*/
|
2020-01-18 15:38:14 +00:00
|
|
|
class RequestCache extends TtlCache {
|
|
|
|
/**
|
|
|
|
* @param {number} ttl time to keep each resource in milliseconds
|
|
|
|
*/
|
|
|
|
constructor(ttl) {
|
|
|
|
super(ttl)
|
|
|
|
}
|
2020-01-12 12:50:21 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
* @param {() => Promise<T>} callback
|
2020-07-22 12:58:21 +00:00
|
|
|
* @returns {Promise<{result: T, fromCache: boolean}>}
|
2020-01-12 12:50:21 +00:00
|
|
|
*/
|
|
|
|
getOrFetch(key, callback) {
|
2020-01-26 14:56:59 +00:00
|
|
|
this.cleanKey(key)
|
2020-07-29 03:32:04 +00:00
|
|
|
if (this.cache.has(key)) {
|
|
|
|
return this.getWithoutClean(key).then(result => ({result, fromCache: true}))
|
|
|
|
} else {
|
|
|
|
const pending = callback()
|
2020-01-12 12:50:21 +00:00
|
|
|
this.set(key, pending)
|
2020-07-29 03:32:04 +00:00
|
|
|
return pending.then(result => ({result, fromCache: false}))
|
2020-01-12 12:50:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
* @param {() => Promise<T>} callback
|
2020-07-22 12:58:21 +00:00
|
|
|
* @returns {Promise<{result: T, fromCache: boolean}>}
|
2020-01-12 12:50:21 +00:00
|
|
|
*/
|
|
|
|
getOrFetchPromise(key, callback) {
|
|
|
|
return this.getOrFetch(key, callback).then(result => {
|
|
|
|
this.cache.delete(key)
|
|
|
|
return result
|
2020-07-16 11:41:00 +00:00
|
|
|
}).catch(error => {
|
|
|
|
this.cache.delete(key)
|
|
|
|
throw error
|
2020-01-12 12:50:21 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-02 14:53:37 +00:00
|
|
|
/**
|
|
|
|
* @template T
|
|
|
|
*/
|
|
|
|
class UserRequestCache extends TtlCache {
|
|
|
|
constructor(ttl) {
|
|
|
|
super(ttl)
|
2020-07-07 10:08:19 +00:00
|
|
|
/** @type {Map<string, {data: T, isReel: boolean, isFailedPromise: boolean, htmlFailed: boolean, reelFailed: boolean, time: number}>} */
|
2020-02-02 14:53:37 +00:00
|
|
|
this.cache
|
2020-07-29 09:51:41 +00:00
|
|
|
/** @type {Map<string, string>} */
|
|
|
|
this.idCache = new Map()
|
2020-02-02 14:53:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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
|
2020-07-07 10:08:19 +00:00
|
|
|
this.cache.set(key, {data, isReel, isFailedPromise: false, htmlFailed, reelFailed: false, time: Date.now()})
|
2020-07-29 09:51:41 +00:00
|
|
|
if (data && data.data && data.data.id) this.idCache.set(data.data.id, key) // this if statement is bad
|
2020-02-02 14:53:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
* @param {boolean} isHtmlPreferred
|
|
|
|
* @param {boolean} willFetchReel
|
|
|
|
* @param {() => Promise<T>} callback
|
|
|
|
* @returns {Promise<T>}
|
|
|
|
*/
|
|
|
|
getOrFetch(key, willFetchReel, isHtmlPreferred, callback) {
|
|
|
|
this.cleanKey(key)
|
|
|
|
if (this.cache.has(key)) {
|
|
|
|
const existing = this.cache.get(key)
|
2020-02-14 10:28:13 +00:00
|
|
|
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
|
2020-07-07 10:08:19 +00:00
|
|
|
if (existing.reelFailed || (existing.htmlFailed && !willFetchReel)) { // it's no use! the attempt will fail again; don't try.
|
2020-02-14 10:28:13 +00:00
|
|
|
return Promise.resolve(existing.data) // this is actually a promise rejection
|
|
|
|
}
|
|
|
|
}
|
2020-02-02 14:53:37 +00:00
|
|
|
}
|
|
|
|
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 => {
|
2020-07-07 10:08:19 +00:00
|
|
|
if (willFetchReel) this.cache.get(key).reelFailed = true
|
|
|
|
else this.cache.get(key).htmlFailed = true
|
2020-02-02 14:53:37 +00:00
|
|
|
this.cache.get(key).isFailedPromise = true
|
|
|
|
throw error
|
|
|
|
})
|
|
|
|
this.set(key, willFetchReel, pending)
|
|
|
|
return pending
|
|
|
|
}
|
2020-07-29 09:51:41 +00:00
|
|
|
|
|
|
|
getByID(id) {
|
|
|
|
const key = this.idCache.get(id)
|
|
|
|
if (key == null) return null
|
|
|
|
const data = this.getWithoutClean(key)
|
|
|
|
if (data == null) return null
|
|
|
|
return data
|
|
|
|
}
|
2020-02-02 14:53:37 +00:00
|
|
|
}
|
|
|
|
|
2020-01-18 15:38:14 +00:00
|
|
|
module.exports.TtlCache = TtlCache
|
|
|
|
module.exports.RequestCache = RequestCache
|
2020-02-02 14:53:37 +00:00
|
|
|
module.exports.UserRequestCache = UserRequestCache
|