UNPKG

@kolodny/lru-ttl-cache

Version:

Fast LRU and TTL cache with upsert and promise option

269 lines (268 loc) 8.26 kB
import MS from 'ms'; import Bytes from 'bytes'; export default class LRU_TTL { _map = new Map(); _max; _maxBytes; _ttl; _ttlInterval; _ttlP = undefined; _upsert; _tmpSize = 0; _totalBytes = 0; _tmpBytes = 0; _next = this; _prev = this; constructor(options) { if (options) { this._max = options.max == null ? Infinity : options.max; var a = options.maxBytes; this._maxBytes = a == null ? Infinity : typeof a === 'number' ? a : Bytes.parse(a); a = options.ttl; this._ttl = a == null ? Infinity : typeof a === 'number' ? a : MS(a); a = options.ttlInterval; this._ttlInterval = a == null ? Infinity : typeof a === 'number' ? a : MS(a); this._upsert = options.upsert; } else { this._max = Infinity; this._maxBytes = Infinity; this._ttl = Infinity; this._ttlInterval = 60000; } if (this.ttlInterval > this.ttl) this.ttlInterval = this.ttl; this._prev = this._next = this; } get max() { return this._max; } set max(max) { this._max = max; } get maxBytes() { return this._maxBytes; } set maxBytes(maxBytes) { this._maxBytes = typeof maxBytes === 'number' ? maxBytes : Bytes.parse(maxBytes); } get ttl() { return this._ttl; } set ttl(ttl) { this._ttl = typeof ttl === 'number' ? ttl : MS(ttl); } get ttlInterval() { return this._ttlInterval; } set ttlInterval(ttlInterval) { this._ttlInterval = typeof ttlInterval === 'number' ? ttlInterval : MS(ttlInterval); if (this.ttlInterval > this.ttl) this.ttlInterval = this.ttl; if (this._ttlP) { clearInterval(this._ttlP); this._ttlP = setInterval(this._ttlClean.bind(this), this._ttlInterval); } } get upsertCb() { return this._upsert; } set upsertCb(cb) { this._upsert = cb; } get bytes() { return this._totalBytes; } get tmpBytes() { return this._tmpBytes; } get size() { return this._map.size; } get tmpSize() { return this._tmpSize; } has(key) { return this._map.has(key); } set(key, value, bytes = 0, isPermanent = false) { var item; if (item = this._map.get(key)) { if (item.value === value && item.bytes === bytes && item.isPermanent === isPermanent) { item.lastAccess = Date.now(); return this; } this._delete(item); } this._set(key, value, bytes, isPermanent); return this; } setPermanent(key, value, bytes = 0) { return this.set(key, value, bytes, true); } _set(key, value, bytes, isPermanent) { var now = Date.now(); var ele = { key, value, bytes, createdAt: now, lastAccess: now, isPermanent: isPermanent, _prev: undefined, _next: undefined }; this._map.set(key, ele); this._totalBytes += bytes; if (!isPermanent) { var p = this._next; p._prev = ele; ele._next = p; ele._prev = this; this._next = ele; this._tmpSize++; this._tmpBytes += bytes; if (this._tmpSize > this._max) this._delete(this._prev); while (this._tmpBytes > this._maxBytes && this._prev != this) { this._delete(this._prev); } if (!this._ttlP) this._ttlP = setInterval(this._ttlClean.bind(this), this._ttlInterval); } return ele; } get(key, upsert, additionalUpsertCbArgs) { var ele; var p; var p2; if (ele = this._map.get(key)) { ele.lastAccess = Date.now(); if (!ele.isPermanent && ele._prev !== this) { p = ele._next; p2 = ele._prev; p2._next = p; p._prev = p2; p = this._next; p._prev = ele; ele._next = p; ele._prev = this; this._next = ele; } return ele.value; } else if (upsert) { if (typeof this._upsert !== 'function') throw new Error('Missing upsert callback!'); var upsertResult = this._upsert(key, additionalUpsertCbArgs); if (upsertResult instanceof Promise) { ele = this._set(key, upsertResult.then(({ value }) => value), 0, true); return upsertResult.then((r) => { if (ele === this._map.get(key)) { this._delete(ele); this._set(key, r.value, r.bytes || 0, !!r.isPermanent); } return r.value; }); } else { this._set(key, upsertResult.value, upsertResult.bytes || 0, !!upsertResult.isPermanent); return upsertResult.value; } } else return undefined; } peek(key) { return this._map.get(key)?.value; } pop() { if (this._prev !== this) { var oldest = this._prev; this._delete(oldest); return oldest.value; } return undefined; } getLRU() { return this._prev !== this ? this._prev.value : undefined; } upsert(key, ...args) { return this.get(key, true, args); } delete(key) { var el; if (el = this._map.get(key)) this._delete(el); return this; } _delete(ele) { this._map.delete(ele.key); var bytes = ele.bytes; if (!ele.isPermanent) { var p = ele._next; var p2 = ele._prev; p._prev = p2; p2._next = p; this._tmpBytes -= bytes; this._tmpSize--; } this._totalBytes -= bytes; } clearTemp() { var el = this._prev; var map = this._map; while (el !== this) { map.delete(el.key); el = el._next; } this._next = this._prev = this; this._totalBytes -= this._tmpBytes; this._tmpBytes = 0; this._tmpSize = 0; } clearAll() { this._next = this._prev = this; this._map.clear(); this._tmpBytes = this._tmpSize = this._totalBytes = 0; } *entries() { var it = this._map.entries(); var p = it.next(); var v; while (!p.done) { v = p.value; yield [v[0], v[1].value]; p = it.next(); } } keys() { return this._map.keys(); } *values() { var it = this._map.values(); var p = it.next(); while (!p.done) { yield p.value.value; } } forEach(cb, thisArg) { var it = this._map.values(); var p = it.next(); if (arguments.length === 1) thisArg = this; while (!p.done) { var e = p.value; cb.call(thisArg, e.value, e.key); } } _ttlClean() { var expires = Date.now() - this._ttl; var p = this._prev; var bytes = 0; var map = this._map; while (p !== this && p.lastAccess < expires) { bytes += p.bytes; map.delete(p.key); p = p._prev; } if (p === this) { this._prev = this._next = this; this._totalBytes -= bytes; if (this._totalBytes < 0) this._totalBytes = 0; this._tmpBytes = this._tmpSize = 0; clearInterval(this._ttlP); this._ttlP = undefined; } else { this._prev = p; p._next = this; } } *[Symbol.iterator]() { var it = this._map.values(); var v = it.next(); while (!v.done) { var entry = v.value; yield [entry.key, entry.value]; } } getMetadata(key) { return this._map.get(key); } }