transitory
Version:
In-memory cache with high hit rates via LFU eviction. Supports time-based expiration, automatic loading and metrics.
215 lines • 6.53 kB
JavaScript
import { AbstractCache } from '../AbstractCache';
import { RemovalReason } from '../RemovalReason';
import { ON_REMOVE, ON_MAINTENANCE, TRIGGER_REMOVE, MAINTENANCE } from '../symbols';
const DATA = Symbol('boundlessData');
const EVICTION_DELAY = 5000;
/**
* Boundless cache.
*/
export class BoundlessCache extends AbstractCache {
constructor(options) {
super();
this[DATA] = {
values: new Map(),
removalListener: options.removalListener || null,
evictionTimeout: null
};
}
/**
* The maximum size the cache can be. Will be -1 if the cache is unbounded.
*
* @returns
* maximum size, always `-1`
*/
get maxSize() {
return -1;
}
/**
* The current size of the cache.
*
* @returns
* entries in the cache
*/
get size() {
return this[DATA].values.size;
}
/**
* The size of the cache weighted via the activate estimator.
*
* @returns
* entries in the cache
*/
get weightedSize() {
return this.size;
}
/**
* 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`
*/
set(key, value) {
const data = this[DATA];
const old = data.values.get(key);
// Update with the new value
data.values.set(key, value);
// Schedule an eviction
if (!data.evictionTimeout) {
data.evictionTimeout = setTimeout(() => this[MAINTENANCE](), EVICTION_DELAY);
}
// Return the value we replaced
if (old !== undefined) {
this[TRIGGER_REMOVE](key, old, RemovalReason.REPLACED);
return old;
}
else {
return null;
}
}
/**
* 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`
*/
getIfPresent(key) {
const data = this[DATA];
const value = data.values.get(key);
return value === undefined ? null : value;
}
/**
* 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`
*/
peek(key) {
const data = this[DATA];
const value = data.values.get(key);
return value === undefined ? null : value;
}
/**
* 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`
*/
delete(key) {
const data = this[DATA];
const old = data.values.get(key);
data.values.delete(key);
if (old !== undefined) {
// Trigger removal events
this[TRIGGER_REMOVE](key, old, RemovalReason.EXPLICIT);
// Queue an eviction event if one is not set
if (!data.evictionTimeout) {
data.evictionTimeout = setTimeout(() => this[MAINTENANCE](), EVICTION_DELAY);
}
return old;
}
else {
return null;
}
}
/**
* Check if the given key exists in the cache.
*
* @param key -
* key to check
* @returns
* `true` if value currently exists, `false` otherwise
*/
has(key) {
const data = this[DATA];
return data.values.has(key);
}
/**
* Clear all of the cached data.
*/
clear() {
const data = this[DATA];
const oldValues = data.values;
// Simply replace the value map new data
data.values = new Map();
// Trigger removal events for all of the content in the cache
for (const [key, value] of oldValues.entries()) {
this[TRIGGER_REMOVE](key, value, RemovalReason.EXPLICIT);
}
}
/**
* 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
*/
keys() {
this[MAINTENANCE]();
return Array.from(this[DATA].values.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.
*/
cleanUp() {
// Simply request eviction so extra layers can handle this
this[MAINTENANCE]();
}
/**
* 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.
*/
get metrics() {
throw new Error('Metrics are not supported by this cache');
}
[TRIGGER_REMOVE](key, value, reason) {
const data = this[DATA];
// Trigger any extended remove listeners
const onRemove = this[ON_REMOVE];
if (onRemove) {
onRemove(key, value, reason);
}
if (data.removalListener) {
data.removalListener(key, value, reason);
}
}
[MAINTENANCE]() {
// Trigger the onEvict listener if one exists
const onEvict = this[ON_MAINTENANCE];
if (onEvict) {
onEvict();
}
const data = this[DATA];
if (data.evictionTimeout) {
clearTimeout(data.evictionTimeout);
data.evictionTimeout = null;
}
}
}
//# sourceMappingURL=BoundlessCache.js.map