| const perf = |
| typeof performance === 'object' && |
| performance && |
| typeof performance.now === 'function' |
| ? performance |
| : Date |
| |
| const hasAbortController = typeof AbortController === 'function' |
| |
| // minimal backwards-compatibility polyfill |
| // this doesn't have nearly all the checks and whatnot that |
| // actual AbortController/Signal has, but it's enough for |
| // our purposes, and if used properly, behaves the same. |
| const AC = hasAbortController |
| ? AbortController |
| : class AbortController { |
| constructor() { |
| this.signal = new AS() |
| } |
| abort(reason = new Error('This operation was aborted')) { |
| this.signal.reason = this.signal.reason || reason |
| this.signal.aborted = true |
| this.signal.dispatchEvent({ |
| type: 'abort', |
| target: this.signal, |
| }) |
| } |
| } |
| |
| const hasAbortSignal = typeof AbortSignal === 'function' |
| // Some polyfills put this on the AC class, not global |
| const hasACAbortSignal = typeof AC.AbortSignal === 'function' |
| const AS = hasAbortSignal |
| ? AbortSignal |
| : hasACAbortSignal |
| ? AC.AbortController |
| : class AbortSignal { |
| constructor() { |
| this.reason = undefined |
| this.aborted = false |
| this._listeners = [] |
| } |
| dispatchEvent(e) { |
| if (e.type === 'abort') { |
| this.aborted = true |
| this.onabort(e) |
| this._listeners.forEach(f => f(e), this) |
| } |
| } |
| onabort() {} |
| addEventListener(ev, fn) { |
| if (ev === 'abort') { |
| this._listeners.push(fn) |
| } |
| } |
| removeEventListener(ev, fn) { |
| if (ev === 'abort') { |
| this._listeners = this._listeners.filter(f => f !== fn) |
| } |
| } |
| } |
| |
| const warned = new Set() |
| const deprecatedOption = (opt, instead) => { |
| const code = `LRU_CACHE_OPTION_${opt}` |
| if (shouldWarn(code)) { |
| warn(code, `${opt} option`, `options.${instead}`, LRUCache) |
| } |
| } |
| const deprecatedMethod = (method, instead) => { |
| const code = `LRU_CACHE_METHOD_${method}` |
| if (shouldWarn(code)) { |
| const { prototype } = LRUCache |
| const { get } = Object.getOwnPropertyDescriptor(prototype, method) |
| warn(code, `${method} method`, `cache.${instead}()`, get) |
| } |
| } |
| const deprecatedProperty = (field, instead) => { |
| const code = `LRU_CACHE_PROPERTY_${field}` |
| if (shouldWarn(code)) { |
| const { prototype } = LRUCache |
| const { get } = Object.getOwnPropertyDescriptor(prototype, field) |
| warn(code, `${field} property`, `cache.${instead}`, get) |
| } |
| } |
| |
| const emitWarning = (...a) => { |
| typeof process === 'object' && |
| process && |
| typeof process.emitWarning === 'function' |
| ? process.emitWarning(...a) |
| : console.error(...a) |
| } |
| |
| const shouldWarn = code => !warned.has(code) |
| |
| const warn = (code, what, instead, fn) => { |
| warned.add(code) |
| const msg = `The ${what} is deprecated. Please use ${instead} instead.` |
| emitWarning(msg, 'DeprecationWarning', code, fn) |
| } |
| |
| const isPosInt = n => n && n === Math.floor(n) && n > 0 && isFinite(n) |
| |
| /* istanbul ignore next - This is a little bit ridiculous, tbh. |
| * The maximum array length is 2^32-1 or thereabouts on most JS impls. |
| * And well before that point, you're caching the entire world, I mean, |
| * that's ~32GB of just integers for the next/prev links, plus whatever |
| * else to hold that many keys and values. Just filling the memory with |
| * zeroes at init time is brutal when you get that big. |
| * But why not be complete? |
| * Maybe in the future, these limits will have expanded. */ |
| const getUintArray = max => |
| !isPosInt(max) |
| ? null |
| : max <= Math.pow(2, 8) |
| ? Uint8Array |
| : max <= Math.pow(2, 16) |
| ? Uint16Array |
| : max <= Math.pow(2, 32) |
| ? Uint32Array |
| : max <= Number.MAX_SAFE_INTEGER |
| ? ZeroArray |
| : null |
| |
| class ZeroArray extends Array { |
| constructor(size) { |
| super(size) |
| this.fill(0) |
| } |
| } |
| |
| class Stack { |
| constructor(max) { |
| if (max === 0) { |
| return [] |
| } |
| const UintArray = getUintArray(max) |
| this.heap = new UintArray(max) |
| this.length = 0 |
| } |
| push(n) { |
| this.heap[this.length++] = n |
| } |
| pop() { |
| return this.heap[--this.length] |
| } |
| } |
| |
| class LRUCache { |
| constructor(options = {}) { |
| const { |
| max = 0, |
| ttl, |
| ttlResolution = 1, |
| ttlAutopurge, |
| updateAgeOnGet, |
| updateAgeOnHas, |
| allowStale, |
| dispose, |
| disposeAfter, |
| noDisposeOnSet, |
| noUpdateTTL, |
| maxSize = 0, |
| maxEntrySize = 0, |
| sizeCalculation, |
| fetchMethod, |
| fetchContext, |
| noDeleteOnFetchRejection, |
| noDeleteOnStaleGet, |
| allowStaleOnFetchRejection, |
| allowStaleOnFetchAbort, |
| ignoreFetchAbort, |
| } = options |
| |
| // deprecated options, don't trigger a warning for getting them if |
| // the thing being passed in is another LRUCache we're copying. |
| const { length, maxAge, stale } = |
| options instanceof LRUCache ? {} : options |
| |
| if (max !== 0 && !isPosInt(max)) { |
| throw new TypeError('max option must be a nonnegative integer') |
| } |
| |
| const UintArray = max ? getUintArray(max) : Array |
| if (!UintArray) { |
| throw new Error('invalid max value: ' + max) |
| } |
| |
| this.max = max |
| this.maxSize = maxSize |
| this.maxEntrySize = maxEntrySize || this.maxSize |
| this.sizeCalculation = sizeCalculation || length |
| if (this.sizeCalculation) { |
| if (!this.maxSize && !this.maxEntrySize) { |
| throw new TypeError( |
| 'cannot set sizeCalculation without setting maxSize or maxEntrySize' |
| ) |
| } |
| if (typeof this.sizeCalculation !== 'function') { |
| throw new TypeError('sizeCalculation set to non-function') |
| } |
| } |
| |
| this.fetchMethod = fetchMethod || null |
| if (this.fetchMethod && typeof this.fetchMethod !== 'function') { |
| throw new TypeError( |
| 'fetchMethod must be a function if specified' |
| ) |
| } |
| |
| this.fetchContext = fetchContext |
| if (!this.fetchMethod && fetchContext !== undefined) { |
| throw new TypeError( |
| 'cannot set fetchContext without fetchMethod' |
| ) |
| } |
| |
| this.keyMap = new Map() |
| this.keyList = new Array(max).fill(null) |
| this.valList = new Array(max).fill(null) |
| this.next = new UintArray(max) |
| this.prev = new UintArray(max) |
| this.head = 0 |
| this.tail = 0 |
| this.free = new Stack(max) |
| this.initialFill = 1 |
| this.size = 0 |
| |
| if (typeof dispose === 'function') { |
| this.dispose = dispose |
| } |
| if (typeof disposeAfter === 'function') { |
| this.disposeAfter = disposeAfter |
| this.disposed = [] |
| } else { |
| this.disposeAfter = null |
| this.disposed = null |
| } |
| this.noDisposeOnSet = !!noDisposeOnSet |
| this.noUpdateTTL = !!noUpdateTTL |
| this.noDeleteOnFetchRejection = !!noDeleteOnFetchRejection |
| this.allowStaleOnFetchRejection = !!allowStaleOnFetchRejection |
| this.allowStaleOnFetchAbort = !!allowStaleOnFetchAbort |
| this.ignoreFetchAbort = !!ignoreFetchAbort |
| |
| // NB: maxEntrySize is set to maxSize if it's set |
| if (this.maxEntrySize !== 0) { |
| if (this.maxSize !== 0) { |
| if (!isPosInt(this.maxSize)) { |
| throw new TypeError( |
| 'maxSize must be a positive integer if specified' |
| ) |
| } |
| } |
| if (!isPosInt(this.maxEntrySize)) { |
| throw new TypeError( |
| 'maxEntrySize must be a positive integer if specified' |
| ) |
| } |
| this.initializeSizeTracking() |
| } |
| |
| this.allowStale = !!allowStale || !!stale |
| this.noDeleteOnStaleGet = !!noDeleteOnStaleGet |
| this.updateAgeOnGet = !!updateAgeOnGet |
| this.updateAgeOnHas = !!updateAgeOnHas |
| this.ttlResolution = |
| isPosInt(ttlResolution) || ttlResolution === 0 |
| ? ttlResolution |
| : 1 |
| this.ttlAutopurge = !!ttlAutopurge |
| this.ttl = ttl || maxAge || 0 |
| if (this.ttl) { |
| if (!isPosInt(this.ttl)) { |
| throw new TypeError( |
| 'ttl must be a positive integer if specified' |
| ) |
| } |
| this.initializeTTLTracking() |
| } |
| |
| // do not allow completely unbounded caches |
| if (this.max === 0 && this.ttl === 0 && this.maxSize === 0) { |
| throw new TypeError( |
| 'At least one of max, maxSize, or ttl is required' |
| ) |
| } |
| if (!this.ttlAutopurge && !this.max && !this.maxSize) { |
| const code = 'LRU_CACHE_UNBOUNDED' |
| if (shouldWarn(code)) { |
| warned.add(code) |
| const msg = |
| 'TTL caching without ttlAutopurge, max, or maxSize can ' + |
| 'result in unbounded memory consumption.' |
| emitWarning(msg, 'UnboundedCacheWarning', code, LRUCache) |
| } |
| } |
| |
| if (stale) { |
| deprecatedOption('stale', 'allowStale') |
| } |
| if (maxAge) { |
| deprecatedOption('maxAge', 'ttl') |
| } |
| if (length) { |
| deprecatedOption('length', 'sizeCalculation') |
| } |
| } |
| |
| getRemainingTTL(key) { |
| return this.has(key, { updateAgeOnHas: false }) ? Infinity : 0 |
| } |
| |
| initializeTTLTracking() { |
| this.ttls = new ZeroArray(this.max) |
| this.starts = new ZeroArray(this.max) |
| |
| this.setItemTTL = (index, ttl, start = perf.now()) => { |
| this.starts[index] = ttl !== 0 ? start : 0 |
| this.ttls[index] = ttl |
| if (ttl !== 0 && this.ttlAutopurge) { |
| const t = setTimeout(() => { |
| if (this.isStale(index)) { |
| this.delete(this.keyList[index]) |
| } |
| }, ttl + 1) |
| /* istanbul ignore else - unref() not supported on all platforms */ |
| if (t.unref) { |
| t.unref() |
| } |
| } |
| } |
| |
| this.updateItemAge = index => { |
| this.starts[index] = this.ttls[index] !== 0 ? perf.now() : 0 |
| } |
| |
| this.statusTTL = (status, index) => { |
| if (status) { |
| status.ttl = this.ttls[index] |
| status.start = this.starts[index] |
| status.now = cachedNow || getNow() |
| status.remainingTTL = status.now + status.ttl - status.start |
| } |
| } |
| |
| // debounce calls to perf.now() to 1s so we're not hitting |
| // that costly call repeatedly. |
| let cachedNow = 0 |
| const getNow = () => { |
| const n = perf.now() |
| if (this.ttlResolution > 0) { |
| cachedNow = n |
| const t = setTimeout( |
| () => (cachedNow = 0), |
| this.ttlResolution |
| ) |
| /* istanbul ignore else - not available on all platforms */ |
| if (t.unref) { |
| t.unref() |
| } |
| } |
| return n |
| } |
| |
| this.getRemainingTTL = key => { |
| const index = this.keyMap.get(key) |
| if (index === undefined) { |
| return 0 |
| } |
| return this.ttls[index] === 0 || this.starts[index] === 0 |
| ? Infinity |
| : this.starts[index] + |
| this.ttls[index] - |
| (cachedNow || getNow()) |
| } |
| |
| this.isStale = index => { |
| return ( |
| this.ttls[index] !== 0 && |
| this.starts[index] !== 0 && |
| (cachedNow || getNow()) - this.starts[index] > |
| this.ttls[index] |
| ) |
| } |
| } |
| updateItemAge(_index) {} |
| statusTTL(_status, _index) {} |
| setItemTTL(_index, _ttl, _start) {} |
| isStale(_index) { |
| return false |
| } |
| |
| initializeSizeTracking() { |
| this.calculatedSize = 0 |
| this.sizes = new ZeroArray(this.max) |
| this.removeItemSize = index => { |
| this.calculatedSize -= this.sizes[index] |
| this.sizes[index] = 0 |
| } |
| this.requireSize = (k, v, size, sizeCalculation) => { |
| // provisionally accept background fetches. |
| // actual value size will be checked when they return. |
| if (this.isBackgroundFetch(v)) { |
| return 0 |
| } |
| if (!isPosInt(size)) { |
| if (sizeCalculation) { |
| if (typeof sizeCalculation !== 'function') { |
| throw new TypeError('sizeCalculation must be a function') |
| } |
| size = sizeCalculation(v, k) |
| if (!isPosInt(size)) { |
| throw new TypeError( |
| 'sizeCalculation return invalid (expect positive integer)' |
| ) |
| } |
| } else { |
| throw new TypeError( |
| 'invalid size value (must be positive integer). ' + |
| 'When maxSize or maxEntrySize is used, sizeCalculation or size ' + |
| 'must be set.' |
| ) |
| } |
| } |
| return size |
| } |
| this.addItemSize = (index, size, status) => { |
| this.sizes[index] = size |
| if (this.maxSize) { |
| const maxSize = this.maxSize - this.sizes[index] |
| while (this.calculatedSize > maxSize) { |
| this.evict(true) |
| } |
| } |
| this.calculatedSize += this.sizes[index] |
| if (status) { |
| status.entrySize = size |
| status.totalCalculatedSize = this.calculatedSize |
| } |
| } |
| } |
| removeItemSize(_index) {} |
| addItemSize(_index, _size) {} |
| requireSize(_k, _v, size, sizeCalculation) { |
| if (size || sizeCalculation) { |
| throw new TypeError( |
| 'cannot set size without setting maxSize or maxEntrySize on cache' |
| ) |
| } |
| } |
| |
| *indexes({ allowStale = this.allowStale } = {}) { |
| if (this.size) { |
| for (let i = this.tail; true; ) { |
| if (!this.isValidIndex(i)) { |
| break |
| } |
| if (allowStale || !this.isStale(i)) { |
| yield i |
| } |
| if (i === this.head) { |
| break |
| } else { |
| i = this.prev[i] |
| } |
| } |
| } |
| } |
| |
| *rindexes({ allowStale = this.allowStale } = {}) { |
| if (this.size) { |
| for (let i = this.head; true; ) { |
| if (!this.isValidIndex(i)) { |
| break |
| } |
| if (allowStale || !this.isStale(i)) { |
| yield i |
| } |
| if (i === this.tail) { |
| break |
| } else { |
| i = this.next[i] |
| } |
| } |
| } |
| } |
| |
| isValidIndex(index) { |
| return ( |
| index !== undefined && |
| this.keyMap.get(this.keyList[index]) === index |
| ) |
| } |
| |
| *entries() { |
| for (const i of this.indexes()) { |
| if ( |
| this.valList[i] !== undefined && |
| this.keyList[i] !== undefined && |
| !this.isBackgroundFetch(this.valList[i]) |
| ) { |
| yield [this.keyList[i], this.valList[i]] |
| } |
| } |
| } |
| *rentries() { |
| for (const i of this.rindexes()) { |
| if ( |
| this.valList[i] !== undefined && |
| this.keyList[i] !== undefined && |
| !this.isBackgroundFetch(this.valList[i]) |
| ) { |
| yield [this.keyList[i], this.valList[i]] |
| } |
| } |
| } |
| |
| *keys() { |
| for (const i of this.indexes()) { |
| if ( |
| this.keyList[i] !== undefined && |
| !this.isBackgroundFetch(this.valList[i]) |
| ) { |
| yield this.keyList[i] |
| } |
| } |
| } |
| *rkeys() { |
| for (const i of this.rindexes()) { |
| if ( |
| this.keyList[i] !== undefined && |
| !this.isBackgroundFetch(this.valList[i]) |
| ) { |
| yield this.keyList[i] |
| } |
| } |
| } |
| |
| *values() { |
| for (const i of this.indexes()) { |
| if ( |
| this.valList[i] !== undefined && |
| !this.isBackgroundFetch(this.valList[i]) |
| ) { |
| yield this.valList[i] |
| } |
| } |
| } |
| *rvalues() { |
| for (const i of this.rindexes()) { |
| if ( |
| this.valList[i] !== undefined && |
| !this.isBackgroundFetch(this.valList[i]) |
| ) { |
| yield this.valList[i] |
| } |
| } |
| } |
| |
| [Symbol.iterator]() { |
| return this.entries() |
| } |
| |
| find(fn, getOptions) { |
| for (const i of this.indexes()) { |
| const v = this.valList[i] |
| const value = this.isBackgroundFetch(v) |
| ? v.__staleWhileFetching |
| : v |
| if (value === undefined) continue |
| if (fn(value, this.keyList[i], this)) { |
| return this.get(this.keyList[i], getOptions) |
| } |
| } |
| } |
| |
| forEach(fn, thisp = this) { |
| for (const i of this.indexes()) { |
| const v = this.valList[i] |
| const value = this.isBackgroundFetch(v) |
| ? v.__staleWhileFetching |
| : v |
| if (value === undefined) continue |
| fn.call(thisp, value, this.keyList[i], this) |
| } |
| } |
| |
| rforEach(fn, thisp = this) { |
| for (const i of this.rindexes()) { |
| const v = this.valList[i] |
| const value = this.isBackgroundFetch(v) |
| ? v.__staleWhileFetching |
| : v |
| if (value === undefined) continue |
| fn.call(thisp, value, this.keyList[i], this) |
| } |
| } |
| |
| get prune() { |
| deprecatedMethod('prune', 'purgeStale') |
| return this.purgeStale |
| } |
| |
| purgeStale() { |
| let deleted = false |
| for (const i of this.rindexes({ allowStale: true })) { |
| if (this.isStale(i)) { |
| this.delete(this.keyList[i]) |
| deleted = true |
| } |
| } |
| return deleted |
| } |
| |
| dump() { |
| const arr = [] |
| for (const i of this.indexes({ allowStale: true })) { |
| const key = this.keyList[i] |
| const v = this.valList[i] |
| const value = this.isBackgroundFetch(v) |
| ? v.__staleWhileFetching |
| : v |
| if (value === undefined) continue |
| const entry = { value } |
| if (this.ttls) { |
| entry.ttl = this.ttls[i] |
| // always dump the start relative to a portable timestamp |
| // it's ok for this to be a bit slow, it's a rare operation. |
| const age = perf.now() - this.starts[i] |
| entry.start = Math.floor(Date.now() - age) |
| } |
| if (this.sizes) { |
| entry.size = this.sizes[i] |
| } |
| arr.unshift([key, entry]) |
| } |
| return arr |
| } |
| |
| load(arr) { |
| this.clear() |
| for (const [key, entry] of arr) { |
| if (entry.start) { |
| // entry.start is a portable timestamp, but we may be using |
| // node's performance.now(), so calculate the offset. |
| // it's ok for this to be a bit slow, it's a rare operation. |
| const age = Date.now() - entry.start |
| entry.start = perf.now() - age |
| } |
| this.set(key, entry.value, entry) |
| } |
| } |
| |
| dispose(_v, _k, _reason) {} |
| |
| set( |
| k, |
| v, |
| { |
| ttl = this.ttl, |
| start, |
| noDisposeOnSet = this.noDisposeOnSet, |
| size = 0, |
| sizeCalculation = this.sizeCalculation, |
| noUpdateTTL = this.noUpdateTTL, |
| status, |
| } = {} |
| ) { |
| size = this.requireSize(k, v, size, sizeCalculation) |
| // if the item doesn't fit, don't do anything |
| // NB: maxEntrySize set to maxSize by default |
| if (this.maxEntrySize && size > this.maxEntrySize) { |
| if (status) { |
| status.set = 'miss' |
| status.maxEntrySizeExceeded = true |
| } |
| // have to delete, in case a background fetch is there already. |
| // in non-async cases, this is a no-op |
| this.delete(k) |
| return this |
| } |
| let index = this.size === 0 ? undefined : this.keyMap.get(k) |
| if (index === undefined) { |
| // addition |
| index = this.newIndex() |
| this.keyList[index] = k |
| this.valList[index] = v |
| this.keyMap.set(k, index) |
| this.next[this.tail] = index |
| this.prev[index] = this.tail |
| this.tail = index |
| this.size++ |
| this.addItemSize(index, size, status) |
| if (status) { |
| status.set = 'add' |
| } |
| noUpdateTTL = false |
| } else { |
| // update |
| this.moveToTail(index) |
| const oldVal = this.valList[index] |
| if (v !== oldVal) { |
| if (this.isBackgroundFetch(oldVal)) { |
| oldVal.__abortController.abort(new Error('replaced')) |
| } else { |
| if (!noDisposeOnSet) { |
| this.dispose(oldVal, k, 'set') |
| if (this.disposeAfter) { |
| this.disposed.push([oldVal, k, 'set']) |
| } |
| } |
| } |
| this.removeItemSize(index) |
| this.valList[index] = v |
| this.addItemSize(index, size, status) |
| if (status) { |
| status.set = 'replace' |
| const oldValue = |
| oldVal && this.isBackgroundFetch(oldVal) |
| ? oldVal.__staleWhileFetching |
| : oldVal |
| if (oldValue !== undefined) status.oldValue = oldValue |
| } |
| } else if (status) { |
| status.set = 'update' |
| } |
| } |
| if (ttl !== 0 && this.ttl === 0 && !this.ttls) { |
| this.initializeTTLTracking() |
| } |
| if (!noUpdateTTL) { |
| this.setItemTTL(index, ttl, start) |
| } |
| this.statusTTL(status, index) |
| if (this.disposeAfter) { |
| while (this.disposed.length) { |
| this.disposeAfter(...this.disposed.shift()) |
| } |
| } |
| return this |
| } |
| |
| newIndex() { |
| if (this.size === 0) { |
| return this.tail |
| } |
| if (this.size === this.max && this.max !== 0) { |
| return this.evict(false) |
| } |
| if (this.free.length !== 0) { |
| return this.free.pop() |
| } |
| // initial fill, just keep writing down the list |
| return this.initialFill++ |
| } |
| |
| pop() { |
| if (this.size) { |
| const val = this.valList[this.head] |
| this.evict(true) |
| return val |
| } |
| } |
| |
| evict(free) { |
| const head = this.head |
| const k = this.keyList[head] |
| const v = this.valList[head] |
| if (this.isBackgroundFetch(v)) { |
| v.__abortController.abort(new Error('evicted')) |
| } else { |
| this.dispose(v, k, 'evict') |
| if (this.disposeAfter) { |
| this.disposed.push([v, k, 'evict']) |
| } |
| } |
| this.removeItemSize(head) |
| // if we aren't about to use the index, then null these out |
| if (free) { |
| this.keyList[head] = null |
| this.valList[head] = null |
| this.free.push(head) |
| } |
| this.head = this.next[head] |
| this.keyMap.delete(k) |
| this.size-- |
| return head |
| } |
| |
| has(k, { updateAgeOnHas = this.updateAgeOnHas, status } = {}) { |
| const index = this.keyMap.get(k) |
| if (index !== undefined) { |
| if (!this.isStale(index)) { |
| if (updateAgeOnHas) { |
| this.updateItemAge(index) |
| } |
| if (status) status.has = 'hit' |
| this.statusTTL(status, index) |
| return true |
| } else if (status) { |
| status.has = 'stale' |
| this.statusTTL(status, index) |
| } |
| } else if (status) { |
| status.has = 'miss' |
| } |
| return false |
| } |
| |
| // like get(), but without any LRU updating or TTL expiration |
| peek(k, { allowStale = this.allowStale } = {}) { |
| const index = this.keyMap.get(k) |
| if (index !== undefined && (allowStale || !this.isStale(index))) { |
| const v = this.valList[index] |
| // either stale and allowed, or forcing a refresh of non-stale value |
| return this.isBackgroundFetch(v) ? v.__staleWhileFetching : v |
| } |
| } |
| |
| backgroundFetch(k, index, options, context) { |
| const v = index === undefined ? undefined : this.valList[index] |
| if (this.isBackgroundFetch(v)) { |
| return v |
| } |
| const ac = new AC() |
| if (options.signal) { |
| options.signal.addEventListener('abort', () => |
| ac.abort(options.signal.reason) |
| ) |
| } |
| const fetchOpts = { |
| signal: ac.signal, |
| options, |
| context, |
| } |
| const cb = (v, updateCache = false) => { |
| const { aborted } = ac.signal |
| const ignoreAbort = options.ignoreFetchAbort && v !== undefined |
| if (options.status) { |
| if (aborted && !updateCache) { |
| options.status.fetchAborted = true |
| options.status.fetchError = ac.signal.reason |
| if (ignoreAbort) options.status.fetchAbortIgnored = true |
| } else { |
| options.status.fetchResolved = true |
| } |
| } |
| if (aborted && !ignoreAbort && !updateCache) { |
| return fetchFail(ac.signal.reason) |
| } |
| // either we didn't abort, and are still here, or we did, and ignored |
| if (this.valList[index] === p) { |
| if (v === undefined) { |
| if (p.__staleWhileFetching) { |
| this.valList[index] = p.__staleWhileFetching |
| } else { |
| this.delete(k) |
| } |
| } else { |
| if (options.status) options.status.fetchUpdated = true |
| this.set(k, v, fetchOpts.options) |
| } |
| } |
| return v |
| } |
| const eb = er => { |
| if (options.status) { |
| options.status.fetchRejected = true |
| options.status.fetchError = er |
| } |
| return fetchFail(er) |
| } |
| const fetchFail = er => { |
| const { aborted } = ac.signal |
| const allowStaleAborted = |
| aborted && options.allowStaleOnFetchAbort |
| const allowStale = |
| allowStaleAborted || options.allowStaleOnFetchRejection |
| const noDelete = allowStale || options.noDeleteOnFetchRejection |
| if (this.valList[index] === p) { |
| // if we allow stale on fetch rejections, then we need to ensure that |
| // the stale value is not removed from the cache when the fetch fails. |
| const del = !noDelete || p.__staleWhileFetching === undefined |
| if (del) { |
| this.delete(k) |
| } else if (!allowStaleAborted) { |
| // still replace the *promise* with the stale value, |
| // since we are done with the promise at this point. |
| // leave it untouched if we're still waiting for an |
| // aborted background fetch that hasn't yet returned. |
| this.valList[index] = p.__staleWhileFetching |
| } |
| } |
| if (allowStale) { |
| if (options.status && p.__staleWhileFetching !== undefined) { |
| options.status.returnedStale = true |
| } |
| return p.__staleWhileFetching |
| } else if (p.__returned === p) { |
| throw er |
| } |
| } |
| const pcall = (res, rej) => { |
| this.fetchMethod(k, v, fetchOpts).then(v => res(v), rej) |
| // ignored, we go until we finish, regardless. |
| // defer check until we are actually aborting, |
| // so fetchMethod can override. |
| ac.signal.addEventListener('abort', () => { |
| if ( |
| !options.ignoreFetchAbort || |
| options.allowStaleOnFetchAbort |
| ) { |
| res() |
| // when it eventually resolves, update the cache. |
| if (options.allowStaleOnFetchAbort) { |
| res = v => cb(v, true) |
| } |
| } |
| }) |
| } |
| if (options.status) options.status.fetchDispatched = true |
| const p = new Promise(pcall).then(cb, eb) |
| p.__abortController = ac |
| p.__staleWhileFetching = v |
| p.__returned = null |
| if (index === undefined) { |
| // internal, don't expose status. |
| this.set(k, p, { ...fetchOpts.options, status: undefined }) |
| index = this.keyMap.get(k) |
| } else { |
| this.valList[index] = p |
| } |
| return p |
| } |
| |
| isBackgroundFetch(p) { |
| return ( |
| p && |
| typeof p === 'object' && |
| typeof p.then === 'function' && |
| Object.prototype.hasOwnProperty.call( |
| p, |
| '__staleWhileFetching' |
| ) && |
| Object.prototype.hasOwnProperty.call(p, '__returned') && |
| (p.__returned === p || p.__returned === null) |
| ) |
| } |
| |
| // this takes the union of get() and set() opts, because it does both |
| async fetch( |
| k, |
| { |
| // get options |
| allowStale = this.allowStale, |
| updateAgeOnGet = this.updateAgeOnGet, |
| noDeleteOnStaleGet = this.noDeleteOnStaleGet, |
| // set options |
| ttl = this.ttl, |
| noDisposeOnSet = this.noDisposeOnSet, |
| size = 0, |
| sizeCalculation = this.sizeCalculation, |
| noUpdateTTL = this.noUpdateTTL, |
| // fetch exclusive options |
| noDeleteOnFetchRejection = this.noDeleteOnFetchRejection, |
| allowStaleOnFetchRejection = this.allowStaleOnFetchRejection, |
| ignoreFetchAbort = this.ignoreFetchAbort, |
| allowStaleOnFetchAbort = this.allowStaleOnFetchAbort, |
| fetchContext = this.fetchContext, |
| forceRefresh = false, |
| status, |
| signal, |
| } = {} |
| ) { |
| if (!this.fetchMethod) { |
| if (status) status.fetch = 'get' |
| return this.get(k, { |
| allowStale, |
| updateAgeOnGet, |
| noDeleteOnStaleGet, |
| status, |
| }) |
| } |
| |
| const options = { |
| allowStale, |
| updateAgeOnGet, |
| noDeleteOnStaleGet, |
| ttl, |
| noDisposeOnSet, |
| size, |
| sizeCalculation, |
| noUpdateTTL, |
| noDeleteOnFetchRejection, |
| allowStaleOnFetchRejection, |
| allowStaleOnFetchAbort, |
| ignoreFetchAbort, |
| status, |
| signal, |
| } |
| |
| let index = this.keyMap.get(k) |
| if (index === undefined) { |
| if (status) status.fetch = 'miss' |
| const p = this.backgroundFetch(k, index, options, fetchContext) |
| return (p.__returned = p) |
| } else { |
| // in cache, maybe already fetching |
| const v = this.valList[index] |
| if (this.isBackgroundFetch(v)) { |
| const stale = |
| allowStale && v.__staleWhileFetching !== undefined |
| if (status) { |
| status.fetch = 'inflight' |
| if (stale) status.returnedStale = true |
| } |
| return stale ? v.__staleWhileFetching : (v.__returned = v) |
| } |
| |
| // if we force a refresh, that means do NOT serve the cached value, |
| // unless we are already in the process of refreshing the cache. |
| const isStale = this.isStale(index) |
| if (!forceRefresh && !isStale) { |
| if (status) status.fetch = 'hit' |
| this.moveToTail(index) |
| if (updateAgeOnGet) { |
| this.updateItemAge(index) |
| } |
| this.statusTTL(status, index) |
| return v |
| } |
| |
| // ok, it is stale or a forced refresh, and not already fetching. |
| // refresh the cache. |
| const p = this.backgroundFetch(k, index, options, fetchContext) |
| const hasStale = p.__staleWhileFetching !== undefined |
| const staleVal = hasStale && allowStale |
| if (status) { |
| status.fetch = hasStale && isStale ? 'stale' : 'refresh' |
| if (staleVal && isStale) status.returnedStale = true |
| } |
| return staleVal ? p.__staleWhileFetching : (p.__returned = p) |
| } |
| } |
| |
| get( |
| k, |
| { |
| allowStale = this.allowStale, |
| updateAgeOnGet = this.updateAgeOnGet, |
| noDeleteOnStaleGet = this.noDeleteOnStaleGet, |
| status, |
| } = {} |
| ) { |
| const index = this.keyMap.get(k) |
| if (index !== undefined) { |
| const value = this.valList[index] |
| const fetching = this.isBackgroundFetch(value) |
| this.statusTTL(status, index) |
| if (this.isStale(index)) { |
| if (status) status.get = 'stale' |
| // delete only if not an in-flight background fetch |
| if (!fetching) { |
| if (!noDeleteOnStaleGet) { |
| this.delete(k) |
| } |
| if (status) status.returnedStale = allowStale |
| return allowStale ? value : undefined |
| } else { |
| if (status) { |
| status.returnedStale = |
| allowStale && value.__staleWhileFetching !== undefined |
| } |
| return allowStale ? value.__staleWhileFetching : undefined |
| } |
| } else { |
| if (status) status.get = 'hit' |
| // if we're currently fetching it, we don't actually have it yet |
| // it's not stale, which means this isn't a staleWhileRefetching. |
| // If it's not stale, and fetching, AND has a __staleWhileFetching |
| // value, then that means the user fetched with {forceRefresh:true}, |
| // so it's safe to return that value. |
| if (fetching) { |
| return value.__staleWhileFetching |
| } |
| this.moveToTail(index) |
| if (updateAgeOnGet) { |
| this.updateItemAge(index) |
| } |
| return value |
| } |
| } else if (status) { |
| status.get = 'miss' |
| } |
| } |
| |
| connect(p, n) { |
| this.prev[n] = p |
| this.next[p] = n |
| } |
| |
| moveToTail(index) { |
| // if tail already, nothing to do |
| // if head, move head to next[index] |
| // else |
| // move next[prev[index]] to next[index] (head has no prev) |
| // move prev[next[index]] to prev[index] |
| // prev[index] = tail |
| // next[tail] = index |
| // tail = index |
| if (index !== this.tail) { |
| if (index === this.head) { |
| this.head = this.next[index] |
| } else { |
| this.connect(this.prev[index], this.next[index]) |
| } |
| this.connect(this.tail, index) |
| this.tail = index |
| } |
| } |
| |
| get del() { |
| deprecatedMethod('del', 'delete') |
| return this.delete |
| } |
| |
| delete(k) { |
| let deleted = false |
| if (this.size !== 0) { |
| const index = this.keyMap.get(k) |
| if (index !== undefined) { |
| deleted = true |
| if (this.size === 1) { |
| this.clear() |
| } else { |
| this.removeItemSize(index) |
| const v = this.valList[index] |
| if (this.isBackgroundFetch(v)) { |
| v.__abortController.abort(new Error('deleted')) |
| } else { |
| this.dispose(v, k, 'delete') |
| if (this.disposeAfter) { |
| this.disposed.push([v, k, 'delete']) |
| } |
| } |
| this.keyMap.delete(k) |
| this.keyList[index] = null |
| this.valList[index] = null |
| if (index === this.tail) { |
| this.tail = this.prev[index] |
| } else if (index === this.head) { |
| this.head = this.next[index] |
| } else { |
| this.next[this.prev[index]] = this.next[index] |
| this.prev[this.next[index]] = this.prev[index] |
| } |
| this.size-- |
| this.free.push(index) |
| } |
| } |
| } |
| if (this.disposed) { |
| while (this.disposed.length) { |
| this.disposeAfter(...this.disposed.shift()) |
| } |
| } |
| return deleted |
| } |
| |
| clear() { |
| for (const index of this.rindexes({ allowStale: true })) { |
| const v = this.valList[index] |
| if (this.isBackgroundFetch(v)) { |
| v.__abortController.abort(new Error('deleted')) |
| } else { |
| const k = this.keyList[index] |
| this.dispose(v, k, 'delete') |
| if (this.disposeAfter) { |
| this.disposed.push([v, k, 'delete']) |
| } |
| } |
| } |
| |
| this.keyMap.clear() |
| this.valList.fill(null) |
| this.keyList.fill(null) |
| if (this.ttls) { |
| this.ttls.fill(0) |
| this.starts.fill(0) |
| } |
| if (this.sizes) { |
| this.sizes.fill(0) |
| } |
| this.head = 0 |
| this.tail = 0 |
| this.initialFill = 1 |
| this.free.length = 0 |
| this.calculatedSize = 0 |
| this.size = 0 |
| if (this.disposed) { |
| while (this.disposed.length) { |
| this.disposeAfter(...this.disposed.shift()) |
| } |
| } |
| } |
| |
| get reset() { |
| deprecatedMethod('reset', 'clear') |
| return this.clear |
| } |
| |
| get length() { |
| deprecatedProperty('length', 'size') |
| return this.size |
| } |
| |
| static get AbortController() { |
| return AC |
| } |
| static get AbortSignal() { |
| return AS |
| } |
| } |
| |
| export default LRUCache |