import Yallist from 'https://deno.land/x/yallist@4.0.0-deno/mod.ts';
const MAX = Symbol('max')const LENGTH = Symbol('length')const LENGTH_CALCULATOR = Symbol('lengthCalculator')const ALLOW_STALE = Symbol('allowStale')const MAX_AGE = Symbol('maxAge')const DISPOSE = Symbol('dispose')const NO_DISPOSE_ON_SET = Symbol('noDisposeOnSet')const LRU_LIST = Symbol('lruList')const CACHE = Symbol('cache')const UPDATE_AGE_ON_GET = Symbol('updateAgeOnGet')
const naiveLength = () => 1;
export interface Options<K, V> { max?: number;
maxAge?: number;
length?(value: V, key?: K): number;
dispose?(key: K, value: V): void;
stale?: boolean;
noDisposeOnSet?: boolean;
updateAgeOnGet?: boolean;}
export default class LRUCache<K, V> {
private [LENGTH_CALCULATOR]: (v: V, k: K) => number; private [ALLOW_STALE]: any; private [MAX_AGE]: number; private [DISPOSE]: any; private [NO_DISPOSE_ON_SET]: any; private [UPDATE_AGE_ON_GET]: any; private [LENGTH]: any; private [LRU_LIST]: Yallist<LEntry>; private [MAX]: any; private [CACHE]: any;
constructor(options?: Options<K, V>); constructor(max: number); constructor(options?: number | Options<K, V>) { if (typeof options === 'number') options = { max: options }
if (!options) options = {}
if (options.max && (typeof options.max !== 'number' || options.max < 0)) throw new TypeError('max must be a non-negative number') const max = this[MAX] = options.max || Infinity
const lc = options.length || naiveLength this[LENGTH_CALCULATOR] = (typeof lc !== 'function') ? naiveLength : lc this[ALLOW_STALE] = options.stale || false if (options.maxAge && typeof options.maxAge !== 'number') throw new TypeError('maxAge must be a number') this[MAX_AGE] = options.maxAge || 0 this[DISPOSE] = options.dispose this[NO_DISPOSE_ON_SET] = options.noDisposeOnSet || false this[UPDATE_AGE_ON_GET] = options.updateAgeOnGet || false this.reset() }
set max(mL) { if (typeof mL !== 'number' || mL < 0) throw new TypeError('max must be a non-negative number')
this[MAX] = mL || Infinity trim(this) }
get max(): number { return this[MAX] }
set allowStale(allowStale: boolean) { this[ALLOW_STALE] = !!allowStale }
get allowStale(): boolean { return this[ALLOW_STALE] }
set maxAge(mA) { if (typeof mA !== 'number') throw new TypeError('maxAge must be a non-negative number')
this[MAX_AGE] = mA trim(this) }
get maxAge(): number { return this[MAX_AGE] }
set lengthCalculator(lC: (value: V, key: K) => number) { if (typeof lC !== 'function') lC = naiveLength
if (lC !== this[LENGTH_CALCULATOR]) { this[LENGTH_CALCULATOR] = lC this[LENGTH] = 0 this[LRU_LIST].forEach((hit: LEntry) => { hit.length = this[LENGTH_CALCULATOR](hit.value, hit.key) this[LENGTH] += hit.length }) } trim(this) } get lengthCalculator() { return this[LENGTH_CALCULATOR] }
get length() { return this[LENGTH] }
get itemCount() { return this[LRU_LIST].length }
rforEach<T = this>(callbackFn: (this: T, value: V, key: K, cache: this) => void, thisArg?: T): void; rforEach(fn: any, thisp: any) { thisp = thisp || this for (let walker = this[LRU_LIST].tail; walker !== null;) { const prev = walker.prev forEachStep(this, fn, walker, thisp) walker = prev } }
forEach<T = this>(callbackFn: (this: T, value: V, key: K, cache: this) => void, thisArg?: T): void; forEach(fn: any, thisp: any) { thisp = thisp || this for (let walker = this[LRU_LIST].head; walker !== null;) { const next = walker.next forEachStep(this, fn, walker, thisp) walker = next } }
keys(): K[] { return this[LRU_LIST].toArray().map((k: LEntry) => k.key) }
values(): V[] { return this[LRU_LIST].toArray().map((k: LEntry) => k.value) }
reset(): void { if (this[DISPOSE] && this[LRU_LIST] && this[LRU_LIST].length) { this[LRU_LIST].forEach((hit: LEntry) => this[DISPOSE](hit.key, hit.value)) }
this[CACHE] = new Map() this[LRU_LIST] = new Yallist() this[LENGTH] = 0 }
dump(): Array<Entry<K, V>> { return this[LRU_LIST].map((hit: LEntry) => isStale(this, hit) ? false : { k: hit.key, v: hit.value, e: hit.now + (hit.maxAge || 0) }).toArray().filter((h: any) => h) as any }
dumpLru() { return this[LRU_LIST] }
set(key: K, value: V, maxAge?: number): boolean { maxAge = maxAge || this[MAX_AGE]
if (maxAge && typeof maxAge !== 'number') throw new TypeError('maxAge must be a number')
const now = maxAge ? Date.now() : 0 const len = this[LENGTH_CALCULATOR](value, key)
if (this[CACHE].has(key)) { if (len > this[MAX]) { del(this, this[CACHE].get(key)) return false }
const node = this[CACHE].get(key) const item = node.value
if (this[DISPOSE]) { if (!this[NO_DISPOSE_ON_SET]) this[DISPOSE](key, item.value) }
item.now = now item.maxAge = maxAge item.value = value this[LENGTH] += len - item.length item.length = len this.get(key) trim(this) return true }
const hit = new LEntry(key, value, len, now, maxAge)
if (hit.length > this[MAX]) { if (this[DISPOSE]) this[DISPOSE](key, value)
return false }
this[LENGTH] += hit.length this[LRU_LIST].unshift(hit) this[CACHE].set(key, this[LRU_LIST].head) trim(this) return true }
has(key: K): boolean { if (!this[CACHE].has(key)) return false const hit = this[CACHE].get(key).value return !isStale(this, hit) }
get(key: K): V | undefined { return get(this, key, true) }
peek(key: K): V | undefined { return get(this, key, false) }
pop() { const node = this[LRU_LIST].tail if (!node) return null
del(this, node) return node.value }
del(key: K): void { del(this, this[CACHE].get(key)) }
load(arr: ReadonlyArray<Entry<K, V>>): void { this.reset()
const now = Date.now() for (let l = arr.length - 1; l >= 0; l--) { const hit = arr[l] const expiresAt = hit.e || 0 if (expiresAt === 0) this.set(hit.k, hit.v) else { const maxAge = expiresAt - now if (maxAge > 0) { this.set(hit.k, hit.v, maxAge) } } } }
prune(): void { this[CACHE].forEach((value: any, key: any) => get(this, key, false)) }}
const get = (self: any, key: any, doUse: any) => { const node = self[CACHE].get(key) if (node) { const hit = node.value if (isStale(self, hit)) { del(self, node) if (!self[ALLOW_STALE]) return undefined } else { if (doUse) { if (self[UPDATE_AGE_ON_GET]) node.value.now = Date.now() self[LRU_LIST].unshiftNode(node) } } return hit.value }}
const isStale = (self: any, hit: any) => { if (!hit || (!hit.maxAge && !self[MAX_AGE])) return false
const diff = Date.now() - hit.now return hit.maxAge ? diff > hit.maxAge : self[MAX_AGE] && (diff > self[MAX_AGE])}
const trim = (self: any) => { if (self[LENGTH] > self[MAX]) { for (let walker = self[LRU_LIST].tail; self[LENGTH] > self[MAX] && walker !== null;) { const prev = walker.prev del(self, walker) walker = prev } }}
const del = (self: any, node: any) => { if (node) { const hit = node.value if (self[DISPOSE]) self[DISPOSE](hit.key, hit.value)
self[LENGTH] -= hit.length self[CACHE].delete(hit.key) self[LRU_LIST].removeNode(node) }}
class LEntry { constructor(public key: any , public value: any , public length: any , public now: any , public maxAge: any) { this.maxAge = maxAge || 0 }}
const forEachStep = (self: any, fn: any, node: any, thisp: any) => { let hit = node.value if (isStale(self, hit)) { del(self, node) if (!self[ALLOW_STALE]) hit = undefined } if (hit) fn.call(thisp, hit.value, hit.key, self)}
export interface Entry<K, V> { k: K; v: V; e: number;}