Gitlab@Informatics

Skip to content
Snippets Groups Projects
Select Git revision
  • 0fb1de11afae4abc279cc6c935959643414bb3b2
  • main default protected
  • revert-a98119d8
3 results

index.js

Blame
  • index.js 33.14 KiB
    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
      }
    }
    
    module.exports = LRUCache