123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227 |
- 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
|