@kolodny/lru-ttl-cache
Version:
Fast LRU and TTL cache with upsert and promise option
269 lines (268 loc) • 8.26 kB
JavaScript
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);
}
}