transitory
Version:
In-memory cache with high hit rates via LFU eviction. Supports time-based expiration, automatic loading and metrics.
288 lines • 10.5 kB
JavaScript
;
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpirationCache = void 0;
var AbstractCache_1 = require("../AbstractCache");
var RemovalReason_1 = require("../RemovalReason");
var symbols_1 = require("../symbols");
var TimerWheel_1 = require("./TimerWheel");
var DATA = Symbol('expirationData');
/**
* Wrapper for another cache that provides evictions of times based on timers.
*
* Currently supports expiration based on maximum age.
*/
var ExpirationCache = /** @class */ (function (_super) {
__extends(ExpirationCache, _super);
function ExpirationCache(options) {
var _this = _super.call(this) || this;
_this[symbols_1.PARENT] = options.parent;
_this[DATA] = {
maxWriteAge: options.maxWriteAge,
maxNoReadAge: options.maxNoReadAge,
removalListener: options.removalListener || null,
timerWheel: new TimerWheel_1.TimerWheel(function (keys) {
var e_1, _a;
try {
for (var keys_1 = __values(keys), keys_1_1 = keys_1.next(); !keys_1_1.done; keys_1_1 = keys_1.next()) {
var key = keys_1_1.value;
_this.delete(key);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (keys_1_1 && !keys_1_1.done && (_a = keys_1.return)) _a.call(keys_1);
}
finally { if (e_1) throw e_1.error; }
}
})
};
// Custom onRemove handler for the parent cache
_this[symbols_1.PARENT][symbols_1.ON_REMOVE] = function (key, node, reason) {
var actualReason = node.isExpired() ? RemovalReason_1.RemovalReason.EXPIRED : reason;
_this[DATA].timerWheel.deschedule(node);
_this[symbols_1.TRIGGER_REMOVE](key, node.value, actualReason);
};
// Custom maintenance behaviour to advance the wheel
_this[symbols_1.PARENT][symbols_1.ON_MAINTENANCE] = _this[symbols_1.MAINTENANCE].bind(_this);
return _this;
}
Object.defineProperty(ExpirationCache.prototype, "maxSize", {
/**
* The maximum size the cache can be. Will be -1 if the cache is unbounded.
*
* @returns
* maximum size
*/
get: function () {
return this[symbols_1.PARENT].maxSize;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ExpirationCache.prototype, "size", {
/**
* The current size of the cache.
*
* @returns
* current size
*/
get: function () {
return this[symbols_1.PARENT].size;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ExpirationCache.prototype, "weightedSize", {
/**
* The size of the cache weighted via the activate estimator.
*
* @returns
* weighted size
*/
get: function () {
return this[symbols_1.PARENT].weightedSize;
},
enumerable: false,
configurable: true
});
/**
* Store a value tied to the specified key. Returns the previous value or
* `null` if no value currently exists for the given key.
*
* @param key -
* key to store value under
* @param value -
* value to store
* @returns
* current value or `null`
*/
ExpirationCache.prototype.set = function (key, value) {
var data = this[DATA];
var timerWheel = data.timerWheel;
var node = timerWheel.node(key, value);
var age = null;
if (data.maxWriteAge) {
age = data.maxWriteAge(key, value) || 0;
}
else if (data.maxNoReadAge) {
age = data.maxNoReadAge(key, value) || 0;
}
if (age !== null && !data.timerWheel.schedule(node, age)) {
// Age was not accepted by wheel, delete any previous value
return this.delete(key);
}
try {
var replaced = this[symbols_1.PARENT].set(key, node);
return replaced ? replaced.value : null;
}
catch (ex) {
timerWheel.deschedule(node);
throw ex;
}
};
/**
* Get the cached value for the specified key if it exists. Will return
* the value or `null` if no cached value exist. Updates the usage of the
* key.
*
* @param key -
* key to get
* @returns
* current value or `null`
*/
ExpirationCache.prototype.getIfPresent = function (key) {
var node = this[symbols_1.PARENT].getIfPresent(key);
if (node) {
if (node.isExpired()) {
// Check if the node is expired and return null if so
return null;
}
// Reschedule if we have a maximum age between reads
var data = this[DATA];
if (data.maxNoReadAge) {
var age = data.maxNoReadAge(key, node.value);
if (!data.timerWheel.schedule(node, age)) {
// Age was not accepted by wheel, expire it directly
this.delete(key);
}
}
return node.value;
}
return null;
};
/**
* Peek to see if a key is present without updating the usage of the
* key. Returns the value associated with the key or `null` if the key
* is not present.
*
* In many cases `has(key)` is a better option to see if a key is present.
*
* @param key -
* the key to check
* @returns
* value associated with key or `null`
*/
ExpirationCache.prototype.peek = function (key) {
var node = this[symbols_1.PARENT].peek(key);
return node && !node.isExpired() ? node.value : null;
};
/**
* Check if the given key exists in the cache.
*
* @param key -
* key to check
* @returns
* `true` if value currently exists, `false` otherwise
*/
ExpirationCache.prototype.has = function (key) {
var node = this[symbols_1.PARENT].peek(key);
return (node && !node.isExpired()) || false;
};
/**
* Delete a value in the cache. Returns the deleted value or `null` if
* there was no value associated with the key in the cache.
*
* @param key -
* the key to delete
* @returns
* deleted value or `null`
*/
ExpirationCache.prototype.delete = function (key) {
var node = this[symbols_1.PARENT].delete(key);
return node ? node.value : null;
};
/**
* Clear the cache removing all of the entries cached.
*/
ExpirationCache.prototype.clear = function () {
this[symbols_1.PARENT].clear();
};
/**
* Get all of the keys in the cache as an array. Can be used to iterate
* over all of the values in the cache, but be sure to protect against
* values being removed during iteration due to time-based expiration if
* used.
*
* @returns
* snapshot of keys
*/
ExpirationCache.prototype.keys = function () {
return this[symbols_1.PARENT].keys();
};
/**
* Request clean up of the cache by removing expired entries and
* old data. Clean up is done automatically a short time after sets and
* deletes, but if your cache uses time-based expiration and has very
* sporadic updates it might be a good idea to call `cleanUp()` at times.
*
* A good starting point would be to call `cleanUp()` in a `setInterval`
* with a delay of at least a few minutes.
*/
ExpirationCache.prototype.cleanUp = function () {
this[symbols_1.PARENT].cleanUp();
};
Object.defineProperty(ExpirationCache.prototype, "metrics", {
/**
* Get metrics for this cache. Returns an object with the keys `hits`,
* `misses` and `hitRate`. For caches that do not have metrics enabled
* trying to access metrics will throw an error.
*
* @returns
* metrics if available via the parent cache
*/
get: function () {
return this[symbols_1.PARENT].metrics;
},
enumerable: false,
configurable: true
});
ExpirationCache.prototype[symbols_1.MAINTENANCE] = function () {
this[DATA].timerWheel.advance();
var onMaintenance = this[symbols_1.ON_MAINTENANCE];
if (onMaintenance) {
onMaintenance();
}
};
ExpirationCache.prototype[symbols_1.TRIGGER_REMOVE] = function (key, value, reason) {
// Trigger any extended remove listeners
var onRemove = this[symbols_1.ON_REMOVE];
if (onRemove) {
onRemove(key, value, reason);
}
var data = this[DATA];
// Trigger the removal listener
if (data.removalListener) {
data.removalListener(key, value, reason);
}
};
return ExpirationCache;
}(AbstractCache_1.AbstractCache));
exports.ExpirationCache = ExpirationCache;
//# sourceMappingURL=ExpirationCache.js.map