@kolodny/lru-ttl-cache
Version:
Fast LRU and TTL cache with upsert and promise option
1 lines • 15.8 kB
Source Map (JSON)
{"version":3,"sources":["index.ts"],"names":[],"mappings":"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","file":"index.d.ts","sourcesContent":["import MS from 'ms';\nimport Bytes from 'bytes';\n/**\n * LRU & TTL fast in-mermory cache\n * Promise supported\n * Upsert supported\n * Permanent items supported\n */\n\n/** Options */\nexport interface ConstOptions<K,V>{\n /** Max entries @default Infinity */\n max?: number\n /** Max bytes @default Infinity */\n maxBytes?: number|string\n /** Time to live @default Infinity */\n ttl?: number|string\n /** TTL check interval. @default 60s */\n ttlInterval?: number|string\n /** Upsert callback: enables to create missing elements */\n upsert?: ((key: K, additionalArgs?: any[])=> UpserResult<V> | Promise<UpserResult<V>>) | undefined\n}\n\n/** Upsert result */\nexport interface UpserResult<V>{\n value: V,\n bytes?: number,\n isPermanent?: boolean\n}\n\n/** NodeChain */\ninterface NodeChain{\n /** Previous element */\n _prev?: NodeChain,\n /** Next element */\n _next?: NodeChain\n}\n\n/** Node interface for LRU */\ninterface Node<K, V> extends NodeChain{\n value: V | Promise<V>\n key: K\n /** Object size */\n bytes: number\n /** Created timestamp */\n createdAt: number\n /** Last access */\n lastAccess: number\n /** Is permanent node */\n isPermanent: Boolean\n}\n\ntype NodeReadOnly<K,V>= Readonly<Node<K,V>>\n\n/** Main interface */\nexport default class LRU_TTL<K, V> implements NodeChain{\n private _map: Map<K, Node<K, V>>= new Map<K, Node<K, V>>();\n\t/** Max temp elements */\n private _max: number\n private _maxBytes: number // max bytes for temp entries\n /** TLL */\n private _ttl: number\n private _ttlInterval: number\n private _ttlP?: NodeJS.Timeout= undefined;\n private _upsert?: ConstOptions<K,V>[\"upsert\"]\n\n\t/** Temp elements count */\n\tprivate _tmpSize:number = 0;\n /** Total bytes inside the cache */\n private _totalBytes: number= 0\n /** Temp entries bytes */\n private _tmpBytes: number= 0\n /** Last used element */\n _next: NodeChain= this\n /** Least used element */\n _prev: NodeChain= this\n\n constructor(options?: ConstOptions<K, V>){\n // Set config\n if(options){\n // max entries\n this._max= options.max==null? Infinity : options.max;\n // max bytes\n var a: string|number|undefined= options.maxBytes;\n this._maxBytes= a==null? Infinity : typeof a=== 'number' ? a : Bytes.parse(a);\n // TTL\n a= options.ttl;\n this._ttl= a==null? Infinity : typeof a=== 'number' ? a : MS(a);\n // TTL interval\n a= options.ttlInterval;\n this._ttlInterval= a==null? Infinity : typeof a=== 'number' ? a : MS(a);\n this._upsert= options.upsert;\n } else {\n this._max= Infinity;\n this._maxBytes= Infinity;\n this._ttl= Infinity;\n this._ttlInterval= 60000;\n }\n\t\t// fix ttl interval\n\t\tif(this.ttlInterval > this.ttl)\n\t\t\tthis.ttlInterval= this.ttl;\n\t\t// init chain\n\t\tthis._prev= this._next= this;\n }\n\n /** Set max */\n get max(){return this._max}\n set max(max: number){\n this._max= max\n }\n\n get maxBytes(){ return this._maxBytes }\n set maxBytes(maxBytes: number|string){\n this._maxBytes= typeof maxBytes === 'number'? maxBytes : Bytes.parse(maxBytes);\n }\n\n get ttl(){ return this._ttl }\n set ttl(ttl: number|string){\n this._ttl= typeof ttl ==='number' ? ttl : MS(ttl);\n }\n\n get ttlInterval(){ return this._ttlInterval }\n set ttlInterval(ttlInterval: number|string){\n this._ttlInterval= typeof ttlInterval==='number' ? ttlInterval : MS(ttlInterval);\n\t\t// fix ttl interval\n\t\tif(this.ttlInterval > this.ttl)\n\t\t\tthis.ttlInterval= this.ttl;\n\t\t// reload cleaner\n if(this._ttlP){\n clearInterval(this._ttlP);\n this._ttlP= setInterval(this._ttlClean.bind(this), this._ttlInterval);\n }\n }\n\n get upsertCb(){ return this._upsert }\n set upsertCb(cb: ConstOptions<K,V>[\"upsert\"]){ this._upsert= cb; }\n\n /** Get total bytes */\n get bytes(){ return this._totalBytes }\n /** Temp entries bytes */\n get tmpBytes(){ return this._tmpBytes }\n\n /** Get cache size */\n get size(): number{ return this._map.size }\n\n\t/** Get temp elements count */\n\tget tmpSize(): number{ return this._tmpSize }\n\n /** Check if cache has key */\n has(key: K){ return this._map.has(key) }\n\n /** Set value */\n set(key: K, value: V|Promise<V>, bytes:number=0, isPermanent= false): this{\n var item;\n if(item= this._map.get(key)){\n if(item.value === value && item.bytes=== bytes && item.isPermanent=== isPermanent){\n item.lastAccess= Date.now();\n return this\n }\n this._delete(item);\n }\n this._set(key, value, bytes, isPermanent);\n return this;\n }\n\n /** Add permanent element to the cache (will persist until user removes it manually) */\n setPermanent(key: K, value: V, bytes:number=0): this{\n return this.set(key, value, bytes, true);\n }\n\n /** @private Insert new item */\n private _set(key: K, value: V|Promise<V>, bytes:number, isPermanent:boolean): Node<K, V>{\n var now= Date.now();\n var ele: Node<K, V>={\n key, value, bytes,\n createdAt: now,\n lastAccess: now,\n isPermanent: isPermanent,\n _prev: undefined,\n _next: undefined\n }\n // add to map\n this._map.set(key, ele);\n // Flags\n this._totalBytes+= bytes;\n // add to chain\n if(!isPermanent){\n var p= this._next;\n\t\t\tp._prev= ele;\n\t\t\tele._next= p;\n ele._prev= this;\n this._next= ele;\n // Flags\n\t\t\tthis._tmpSize++; // inc tmp counter\n this._tmpBytes+= bytes;\n // remove last permanent element\n if(this._tmpSize > this._max)\n this._delete(this._prev as Node<K,V>) // Remove least used element\n // remove until maxBytes\n while(this._tmpBytes > this._maxBytes && this._prev!= this){\n this._delete(this._prev as Node<K,V>);\n }\n // Run TTL\n if(!this._ttlP)\n this._ttlP= setInterval(this._ttlClean.bind(this), this._ttlInterval)\n }\n return ele;\n }\n\n /** Get element from the cache */\n get(key: K, upsert?: boolean, additionalUpsertCbArgs?: any[]):V|Promise<V>|undefined{\n var ele: Node<K, V>|undefined;\n var p: NodeChain;\n var p2: NodeChain;\n if(ele= this._map.get(key)){\n ele.lastAccess= Date.now();\n if(!ele.isPermanent && ele._prev!== this){\n // Remove from chain\n p= ele._next!;\n p2= ele._prev!;\n p2!._next= p\n p._prev= p2\n // bring forword\n p= this._next\n\t\t\t\tp._prev= ele\n ele._next= p;\n ele._prev= this;\n this._next= ele\n }\n return ele.value;\n } else if (upsert){\n if(typeof this._upsert !== 'function')\n throw new Error('Missing upsert callback!');\n var upsertResult= this._upsert(key, additionalUpsertCbArgs);\n if(upsertResult instanceof Promise){\n ele= this._set(key, upsertResult.then(({value})=> value), 0, true);\n return upsertResult.then((r: UpserResult<V>)=>{\n // Check object not modified\n if(ele=== this._map.get(key)){\n this._delete(ele!);\n this._set(key, r.value, r.bytes||0, !!r.isPermanent);\n }\n return r.value;\n });\n }\n else {\n this._set(key, upsertResult.value, upsertResult.bytes||0, !!upsertResult.isPermanent);\n return upsertResult.value;\n }\n }\n else return undefined;\n }\n /** Get element from the cache without changing it's timeout and LRU */\n peek(key: K): V|Promise<V>|undefined{\n return this._map.get(key)?.value;\n }\n\n /** Get and remove Least used element */\n pop(): V | Promise<V> |undefined{\n if(this._prev!==this){\n var oldest= this._prev as Node<K, V>;\n this._delete(oldest);\n return oldest.value;\n }\n return undefined;\n }\n \n /** Get least recently used item */\n getLRU(){\n return this._prev !== this ? (this._prev as Node<K,V>).value : undefined\n }\n\n /** Upsert element in the cache */\n upsert(\n\t\t/** Cache key */\n\t\tkey: K,\n\t\t/** Additional args for upsert callback */\n\t\t... args: any[]\n\t): V|Promise<V>{\n return this.get(key, true, args)!;\n }\n\n /** Delete element from the cache */\n delete(key: K): this{\n var el;\n if(el= this._map.get(key))\n this._delete(el);\n return this;\n }\n\n /** @private remove element from cache */\n private _delete(ele: Node<K, V>){\n // remove from map\n this._map.delete(ele.key);\n // remove from chain\n var bytes= ele.bytes;\n if(!ele.isPermanent){\n var p = ele._next!;\n\t\t\tvar p2= ele._prev!;\n p._prev= p2;\n p2._next= p;\n // adjust cache bytes\n this._tmpBytes-= bytes;\n\t\t\tthis._tmpSize--;\n }\n // Adjust total bytes\n this._totalBytes-= bytes;\n }\n\n /** Clear all the cache excluding permanent items */\n clearTemp(){\n var el= this._prev as NodeChain;\n var map= this._map;\n // @ts-ignore\n while(el !== this){\n map.delete((el as Node<K, V>).key);\n el= el._next!;\n }\n this._next= this._prev= this;\n this._totalBytes-= this._tmpBytes;\n this._tmpBytes= 0;\n\t\tthis._tmpSize= 0;\n }\n\n /** Clear all items in the cache including permanent items */\n clearAll(){\n this._next= this._prev= this;\n this._map.clear();\n this._tmpBytes= this._tmpSize= this._totalBytes= 0;\n }\n\n /** Get entries */\n *entries():IterableIterator<[K, V|Promise<V>]>{\n var it= this._map.entries();\n var p= it.next();\n var v;\n while(!p.done){\n v= p.value;\n yield [v[0], v[1].value];\n p= it.next();\n }\n }\n\n /** Get all keys */\n keys(): IterableIterator<K>{ return this._map.keys() }\n\n /** Values */\n *values():IterableIterator<V|Promise<V>>{\n var it= this._map.values();\n var p= it.next()\n while(!p.done){\n yield p.value.value;\n }\n }\n\n /** ForEach */\n forEach(cb: (value: V|Promise<V>, key: K) => void, thisArg: any){\n var it= this._map.values();\n var p= it.next()\n if(arguments.length===1) thisArg= this;\n while(!p.done){\n var e= p.value;\n cb.call(thisArg, e.value, e.key);\n }\n }\n\n /** TTL CLean */\n _ttlClean(){\n // Find last node to keep\n var expires= Date.now() - this._ttl;\n var p= this._prev as Node<K,V>;\n var bytes= 0;\n var map= this._map;\n while((p as NodeChain)!== this && p.lastAccess < expires){\n bytes+= p.bytes;\n map.delete(p.key);\n p= p._prev as Node<K,V>;\n }\n // Remove other nodes\n if((p as NodeChain)===this){\n // Remove all nodes\n this._prev= this._next= this\n this._totalBytes-= bytes;\n if(this._totalBytes<0) this._totalBytes= 0\n this._tmpBytes= this._tmpSize= 0;\n // Break ttl\n clearInterval(this._ttlP!);\n this._ttlP= undefined;\n } else {\n this._prev= p;\n p._next= this;\n }\n }\n\n /** For(of) */\n *[Symbol.iterator](){\n var it= this._map.values();\n var v= it.next();\n while(!v.done){\n var entry= v.value\n yield [entry.key, entry.value];\n }\n }\n\n\t/** Get element metadata */\n\tgetMetadata(key: K): NodeReadOnly<K,V>|undefined{\n\t\treturn this._map.get(key);\n\t}\n}"]}