in-memoriam
Version:
Easy to use, high-speed O(1) for all operations, in-memory cache with both ttl and capacity support.
125 lines (109 loc) • 3.19 kB
JavaScript
;
/**
* A very simple cache with a very simple reaper thread that runs
* once a second.
*
* @constructor
* @param {Number} Capacity Maximum number of items in the cache. When
* exceeded, oldest item is evicted to make room.
* @param {Number} [ttl] Number of milliseconds items can live in the cache.
* If not specified, objects stay in the cache until removed
* due to cache capacity being exceeded.
* For immutable data, leaving ttl empty is recommended.
*/
var IDLL = require('./IndexedDoubleLinkedList');
module.exports = function (capacity, ttl) {
var self = this;
// Store the entries in a doubly-linked circular loop. This makes it trivial
// to find the oldest item in O(1) time by looking at newestEntry.previous.
// It also makes it trivial to take an entry and make it the newest one if
// it is created or refreshed via a get.
var cache = new IDLL();
self.stats = {
hits: 0,
misses: 0,
inserts: 0,
updates: 0,
evictions: 0,
expirations: 0,
deletes: 0,
size: cache.length
};
/**
* Adds an item to the cache. Will overwrite any existing value and evict
* an old item if the cache is full.
*
* @param {String} key
* @param {*} value
*/
self.set = function (key, value) {
var entry = cache.remove(key);
if (entry) {
self.stats.updates++;
}
// New item, make room for it if needed
if (self.stats.size() === capacity) {
var oldest = cache.getOldest();
cache.remove(oldest.key);
self.stats.evictions++;
}
entry = cache.add(key, value);
self.stats.inserts++;
// Schedule the potential removal of this key
if (ttl) {
entry.expires = (new Date()).getTime() + ttl;
setTimeout(evictOrReschedule, ttl, key);
}
};
/**
* Gets the key and if found, make it the newest one to avoid eviction
*/
self.get = function (key) {
var entry = cache.remove(key);
if (entry) {
self.stats.hits++;
// Put it back in, which will make it the newest item
cache.add(key, entry.value);
return entry.value;
}
self.stats.misses++;
return null;
};
/**
* Remove an existing key
*
* @param {String} key
*
* @returns {Object} the entry that was removed or null if key was not found
*/
self.remove = function (key) {
var entry = cache.remove(key);
if (entry) {
self.stats.deletes++;
}
return entry;
};
// Private methods
/**
* Deletes the key if it expired, or sets a new watch on it
* if it has not.
*/
function evictOrReschedule(key) {
// The entry may already have been evicted due to cache hitting capacity
var entry = cache.get(key);
if (!entry) {
return;
}
var now = new Date();
// ms left till object expires
var timeLeft = entry.expires - now.getTime();
if (timeLeft > 0) {
// Renew the scheduled removal
setTimeout(evictOrReschedule, timeLeft, key);
return;
}
// Item has expired
cache.remove(key);
self.expirations++;
}
};