diff --git a/lib/lru-cache.js b/lib/lru-cache.js index c03c2cb..abfa346 100644 --- a/lib/lru-cache.js +++ b/lib/lru-cache.js @@ -1,75 +1,102 @@ -// Cache any data with a timestamp, -// remove only the oldest data. +// In-memory KV, remove the oldest data when the capacity is reached. var typeEnum = { unit: 0, heap: 1, }; -function Cache(size, type) { - if (!this instanceof Cache) { return new Cache(size, type); } +function CacheSlot(key, value) { + this.key = key; + this.value = value; + this.older = null; // Newest slot that is older than this slot. + this.newer = null; // Oldest slot that is newer than this slot. +} + +function Cache(capacity, type) { + if (!(this instanceof Cache)) { return new Cache(capacity, type); } type = type || 'unit'; - this.size = size; + this.capacity = capacity; this.type = typeEnum[type]; - if (this.type === typeEnum.unit) { this.size -= 1; } - // `cache` contains {content, index}. - // - content: the actual data that is cached. - // - index: the position in `order` of the data. - this.cache = Object.create(null); - this.order = []; // list of cache keys from oldest to newest. + this.cache = new Map(); // Maps cache keys to CacheSlots. + this.newest = null; // Newest slot in the cache. + this.oldest = null; } Cache.prototype = { - set: function addToCache(cacheIndex, cached) { - if (this.cache[cacheIndex] !== undefined) { - this.order.splice(this.cache[cacheIndex].index, 1); - // Put the new element at the end of `order`. - this.cache[cacheIndex].index = this.order.length; - this.cache[cacheIndex].content = cached; - this.order.push(cacheIndex); - } else { - // If the cache is full, remove the oldest data - // (ie, the data requested longest ago.) - var numberToRemove = this.limitReached(); - if (numberToRemove > this.order.length) { numberToRemove = this.order.length; } - for (var i = 0; i < numberToRemove; i++) { - // Remove `order`'s oldest element, the first. - delete this.cache[this.order[0]]; - this.order.shift(); + set: function addToCache(cacheKey, cached) { + var slot = this.cache.get(cacheKey); + if (slot === undefined) { + slot = new CacheSlot(cacheKey, cached); + this.cache.set(cacheKey, slot); + } + this.makeNewest(slot); + var numItemsToRemove = this.limitReached(); + if (numItemsToRemove > 0) { + for (var i = 0; i < numItemsToRemove; i++) { + this.removeOldest(); } - - this.cache[cacheIndex] = { - index: this.order.length, - content: cached, - }; - this.order.push(cacheIndex); } }, - get: function getFromCache(cacheIndex) { - if (this.cache[cacheIndex] !== undefined) { - this.order.splice(this.cache[cacheIndex].index, 1); - // Put the new element at the end of `order`. - this.cache[cacheIndex].index = this.order.length; - this.order.push(cacheIndex); - return this.cache[cacheIndex].content; - } else { return; } + get: function getFromCache(cacheKey) { + var slot = this.cache.get(cacheKey); + if (slot !== undefined) { + this.makeNewest(slot); + return slot.value; + } }, - has: function hasInCache(cacheIndex) { - return this.cache[cacheIndex] !== undefined; + has: function hasInCache(cacheKey) { + return this.cache.has(cacheKey); + }, + + makeNewest: function makeNewestSlot(slot) { + var previousNewest = this.newest; + if (previousNewest === slot) { return; } + var older = slot.older; + var newer = slot.newer; + + if (older !== null) { + older.newer = newer; + } else if (newer !== null) { + this.oldest = newer; + } + if (newer !== null) { + newer.older = older; + } + this.newest = slot; + + if (previousNewest !== null) { + slot.older = previousNewest; + slot.newer = null; + previousNewest.newer = slot; + } else { + // If previousNewest is null, the cache used to be empty. + this.oldest = slot; + } + }, + + removeOldest: function removeOldest() { + var cacheKey = this.oldest.key; + if (this.oldest !== null) { + this.oldest = this.oldest.newer; + if (this.oldest !== null) { + this.oldest.older = null; + } + } + this.cache.delete(cacheKey); }, // Returns the number of elements to remove if we're past the limit. limitReached: function heuristic() { if (this.type === typeEnum.unit) { // Remove the excess. - return Math.max(0, (this.order.length - this.size)); + return Math.max(0, (this.cache.size - this.capacity)); } else if (this.type === typeEnum.heap) { - if (getHeapSize() >= this.size) { + if (getHeapSize() >= this.capacity) { console.log('LRU HEURISTIC heap:', getHeapSize()); // Remove half of them. - return (this.order.length >> 1); + return (this.cache.size >> 1); } else { return 0; } } else { console.error("Unknown heuristic '" + this.type + "' for LRU cache."); diff --git a/test/lru-cache.js b/test/lru-cache.js new file mode 100644 index 0000000..1916024 --- /dev/null +++ b/test/lru-cache.js @@ -0,0 +1,137 @@ +var LRU = require('../lib/lru-cache.js'); +module.exports = [ + ["should support being called without new", function(done, assert) { + var cache = LRU(1); + assert(cache instanceof LRU); + done(); + }], + ["should support a zero capacity", function(done, assert) { + var cache = new LRU(0); + cache.set('key', 'value'); + assert.equal(cache.cache.size, 0); + done(); + }], + ["should support a one capacity", function(done, assert) { + var cache = new LRU(1); + cache.set('key1', 'value1'); + assert.equal(cache.cache.size, 1); + assert.equal(cache.newest, cache.cache.get('key1')); + assert.equal(cache.oldest, cache.cache.get('key1')); + cache.set('key2', 'value2'); + assert.equal(cache.cache.size, 1); + assert.equal(cache.newest, cache.cache.get('key2')); + assert.equal(cache.oldest, cache.cache.get('key2')); + assert.equal(cache.get('key1'), undefined); + assert.equal(cache.get('key2'), 'value2'); + done(); + }], + ["should remove the oldest element when reaching capacity", + function(done, assert) { + var cache = new LRU(2); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + cache.set('key3', 'value3'); + var slot1 = cache.cache.get('key1'); + var slot2 = cache.cache.get('key2'); + var slot3 = cache.cache.get('key3'); + assert.equal(cache.cache.size, 2); + assert.equal(cache.oldest, slot2); + assert.equal(cache.newest, slot3); + assert.equal(slot2.older, null); + assert.equal(slot2.newer, slot3); + assert.equal(slot3.older, slot2); + assert.equal(slot3.newer, null); + assert.equal(cache.cache.get('key1'), undefined); + assert.equal(cache.get('key1'), undefined); + assert.equal(cache.get('key2'), 'value2'); + assert.equal(cache.get('key3'), 'value3'); + done(); + }], + ["should make sure that resetting a key in cache makes it newest", + function(done, assert) { + var cache = new LRU(2); + cache.set('key', 'value'); + cache.set('key2', 'value2'); + assert.equal(cache.oldest, cache.cache.get('key')); + assert.equal(cache.newest, cache.cache.get('key2')); + cache.set('key', 'value'); + assert.equal(cache.oldest, cache.cache.get('key2')); + assert.equal(cache.newest, cache.cache.get('key')); + done(); + }], + ["should make sure that getting a key in cache makes it newest", + function(done, assert) { + // When the key is oldest. + var cache = new LRU(2); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + var slot1 = cache.cache.get('key1'); + var slot2 = cache.cache.get('key2'); + assert.equal(cache.oldest, slot1); + assert.equal(cache.newest, slot2); + assert.equal(slot1.older, null); + assert.equal(slot1.newer, slot2); + assert.equal(slot2.older, slot1); + assert.equal(slot2.newer, null); + + assert.equal(cache.get('key1'), 'value1'); + + var slot1 = cache.cache.get('key1'); + var slot2 = cache.cache.get('key2'); + assert.equal(cache.oldest, slot2); + assert.equal(cache.newest, slot1); + assert.equal(slot2.older, null); + assert.equal(slot2.newer, slot1); + assert.equal(slot1.older, slot2); + assert.equal(slot1.newer, null); + + + // When the key is newest. + cache = new LRU(2); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + + assert.equal(cache.get('key2'), 'value2'); + + var slot1 = cache.cache.get('key1'); + var slot2 = cache.cache.get('key2'); + assert.equal(cache.oldest, slot1); + assert.equal(cache.newest, slot2); + assert.equal(slot1.older, null); + assert.equal(slot1.newer, slot2); + assert.equal(slot2.older, slot1); + assert.equal(slot2.newer, null); + + // When the key is in the middle. + cache = new LRU(3); + cache.set('key1', 'value1'); + cache.set('key2', 'value2'); + cache.set('key3', 'value3'); + var slot1 = cache.cache.get('key1'); + var slot2 = cache.cache.get('key2'); + var slot3 = cache.cache.get('key3'); + assert.equal(cache.oldest, slot1); + assert.equal(cache.newest, slot3); + assert.equal(slot1.older, null); + assert.equal(slot1.newer, slot2); + assert.equal(slot2.older, slot1); + assert.equal(slot2.newer, slot3); + assert.equal(slot3.older, slot2); + assert.equal(slot3.newer, null); + + assert.equal(cache.get('key2'), 'value2'); + + var slot1 = cache.cache.get('key1'); + var slot2 = cache.cache.get('key2'); + var slot3 = cache.cache.get('key3'); + assert.equal(cache.oldest, slot1); + assert.equal(cache.newest, slot2); + assert.equal(slot1.older, null); + assert.equal(slot1.newer, slot3); + assert.equal(slot3.older, slot1); + assert.equal(slot3.newer, slot2); + assert.equal(slot2.older, slot3); + assert.equal(slot2.newer, null); + done(); + }], +]; diff --git a/test/test.js b/test/test.js index ddf197c..e7947b0 100644 --- a/test/test.js +++ b/test/test.js @@ -142,4 +142,6 @@ test('The server', [ server.kill(); server.on('exit', function() { done(); }); }], -]);}); +]);}) + +.then(function() { test('The LRU cache', require('./lru-cache.js')); });