From 4e57566ce4148d89e2407a86e6e63dbfc232015b Mon Sep 17 00:00:00 2001 From: Ran-P Date: Thu, 16 Nov 2017 11:37:35 +0200 Subject: [PATCH 1/5] Add support for using object as keys --- index.js | 65 +++++++++++++++++----------- test.js | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index 64d05ef..7e4da6c 100644 --- a/index.js +++ b/index.js @@ -47,6 +47,7 @@ class Cache { } this.limit = limit; this.materialize = null; + this.generateKey = null; if (source instanceof Cache) this._cloneCache(source); @@ -56,16 +57,17 @@ class Cache { _cloneIterator(source) { for (let entry of source) - this.set(entry[0], entry[1]); + this._set(entry[0], entry[1]); } _cloneCache(source) { let link = source[_tail]; while (link) { - this.set(link.key, link.value, link); + this._set(link.key, link.value, link); link = link.previous; } this.materialize = source.materialize; + this.generateKey = source.generateKey; } @@ -126,7 +128,7 @@ class Cache { } - delete(key) { + _delete(key) { const link = this[_map].get(key); if (!link) return false; @@ -140,6 +142,11 @@ class Cache { return true; } + delete(keyObject){ + const key = this._generateKey(keyObject); + return this._delete(key); + } + // Evicts as many expired entries an least recently used entries to keep // cache under limit. This can be O(N) as it may iterate over the entire @@ -152,20 +159,23 @@ class Cache { if (this[_cost] <= limit) break; if (hasExpired(link)) - this.delete(link.key); + this._delete(link.key); } // Remove from the tail is potentiall O(N), we in practice we usually evict // as many entries as we add, so evict is O(1) spread over time while (this.size && this[_cost] > limit) { const leastRecent = this[_tail]; - this.delete(leastRecent.key); + this._delete(leastRecent.key); } } - + _generateKey(keyObj){ + return this.generateKey&&this.generateKey(keyObj)||keyObj; + } // Returns the key value if set and not evicted yet. - get(key) { + get(keyObj) { + const key = this._generateKey(keyObj); const link = this[_map].get(key); // Although we do have the value, the contract is that we don't return // expired values @@ -173,7 +183,7 @@ class Cache { this._moveLinkToHead(link); return link.value; } else if (this.materialize) - return this._materializeAndCache(key); + return this._materializeAndCache(key,keyObj); else return undefined; } @@ -193,24 +203,25 @@ class Cache { } - _materializeAndCache(key) { + _materializeAndCache(key,objKey) { const self = this; - const promise = Promise.resolve(key).then(this.materialize); + const promise = Promise.resolve(objKey).then(this.materialize); function deleteIfRejected() { const entry = self[_map].get(key); if (entry && entry.value === promise) - self.delete(key); + self._delete(key); } - this.set(key, promise); + this._set(key, promise); promise.catch(deleteIfRejected); return promise; } // Returns true if key has been set and not evicted yet. - has(key) { + has(keyObj) { + const key = this._generateKey(keyObj); const link = this[_map].get(key); if (!link) return false; @@ -223,7 +234,7 @@ class Cache { while (link) { // We take this opportunity to get rid of expired keys if (hasExpired(link)) - this.delete(link.key); + this._delete(link.key); else yield [link.key, link.value]; link = link.next; @@ -231,21 +242,19 @@ class Cache { } *values() { - // This could be `let [key] of` in future version - for (let entry of this.entries()) - yield entry[1]; + for (let [key,value] of this.entries()) + yield value; } *keys() { - // This could be `let [key, value] of` in future version - for (let entry of this.entries()) - yield entry[0]; + for (let [key] of this.entries()) + yield key; } forEach(callback, thisArg) { // This could be `let [key, value] of` in future version - for (let entry of this.entries()) - callback.call(thisArg, entry[1], entry[0], this); + for (let [key,value] of this.entries()) + callback.call(thisArg, value, key, this); } @@ -261,13 +270,13 @@ class Cache { // // The following two are equivalent: // - // set(key, value) - // set(key, value, { cost: 1, ttl: Infinity }) - set(key, value, options) { + // _set(key, value) + // _set(key, value, { cost: 1, ttl: Infinity }) + _set(key, value, options) { const cost = actualCost(options && options.cost); const expires = ttlToExpires(options && options.ttl); - this.delete(key); + this._delete(key); // If TTL is zero we're never going to return this key, we don't want to // evict older keys either @@ -312,6 +321,10 @@ class Cache { return this; } + set(keyObject,value,options){ + const key = this._generateKey(keyObject); + return this._set(key,value,options); + } // Util.inspect(cache) calls this, and Node's console.log uses inspect inspect(depth, inspectOptions) { diff --git a/test.js b/test.js index 1f35254..4341777 100644 --- a/test.js +++ b/test.js @@ -894,3 +894,131 @@ describe('With materialize function', function() { }); }); + + +describe('With materialize function using key object', function() { + + let cache; + let resolved; + + before(function() { + cache = new Cache(); + cache.materialize = function(key) { + return `${key.a.toUpperCase()}${key.b.toUpperCase()}`; + }; + cache.generateKey = function(keyObject){ + return `${keyObject.a}.${keyObject.b}`; + } + }); + + before(function() { + resolved = cache.get({a:'x',b:'y'}); + return resolved; + }); + + it('should resolve to materialized value', function() { + return resolved + .then(function(value) { + assert.equal(value, 'XY'); + }); + }); + + it('should have size of 1', function() { + assert.equal( cache.size, 1 ); + }); + + it('should have cost of 1', function() { + assert.equal( cache.size, 1 ); + }); + + it('should have new key', function() { + assert(cache.has({a:'x',b:'y'})); + }); + + + describe('get again', function() { + it('should return same value', function() { + const again = cache.get({a:'x',b:'y'}); + assert.equal(again, resolved); + }); + }); + + describe('unable to resolve', function() { + + let rejected; + + before(function() { + cache.clear(); + cache.materialize = function() { + throw new Error('fail'); + }; + rejected = cache.get({a:'y'}); + }); + + it('should reject the get', function() { + return rejected.then(function() { + throw new Error('Not expected to arrive here'); + }, function() { + // Not an error + }); + }); + + it('should still have size of 0', function() { + assert.equal( cache.size, 0 ); + }); + + it('should still have cost of 0', function() { + assert.equal( cache.size, 0 ); + }); + + it('should not have new key', function() { + assert.equal( cache.has({a:'y'}), false); + }); + + }); + + describe('materialize and set', function() { + + let promise; + + before(function() { + cache.clear(); + cache.materialize = function(keyObject) { + const lazy = Promise.resolve('ZZZ'); + lazy.then(function() { + cache.set(keyObject, lazy, { cost: 5 }); + }); + return lazy; + }; + promise = cache.get({a:'z'}); + }); + + it('should retrieve returned value', function() { + return promise + .then(function(value) { + assert.equal(value, 'ZZZ'); + }); + }); + + it('should have size of 1', function() { + assert.equal( cache.size, 1 ); + }); + + it('should have cost of 5', function() { + assert.equal( cache.cost, 5 ); + }); + + it('should have key', function() { + assert( cache.get({a:'z'}) ); + }); + + it('should keep returning value', function() { + return cache.get({a:'z'}) + .then(function(value) { + assert.equal(value, 'ZZZ'); + }); + }); + + }); + +}); From b8b4aa4dd8b8730e00df78a2878da0635507166a Mon Sep 17 00:00:00 2001 From: Ran-P Date: Sun, 19 Nov 2017 10:53:57 +0200 Subject: [PATCH 2/5] Add support for default ttl --- index.js | 586 +++++++++++------------ test.js | 1404 +++++++++++++++++++++++++++++------------------------- 2 files changed, 1037 insertions(+), 953 deletions(-) diff --git a/index.js b/index.js index 7e4da6c..cb3e07b 100644 --- a/index.js +++ b/index.js @@ -6,330 +6,332 @@ const Util = require('util'); // Convert any value into a limit: a positive number, zero or Infinity. function actualLimit(limit) { - return Number.isFinite(limit) ? Math.max(0, limit) : Infinity; + return Number.isFinite(limit) ? Math.max(0, limit) : Infinity; } // Convert any value into a cost: a positive number or zero. If no number // provided (undefined, NaN, etc) return the default cost of one. function actualCost(cost) { - return Number.isFinite(cost) ? Math.max(0, cost) : 1; -} - -// TTL (milliseconds) to expiration (timestamp) -function ttlToExpires(ttl) { - return Number.isInteger(ttl) ? Date.now() + ttl : Infinity; + return Number.isFinite(cost) ? Math.max(0, cost) : 1; } // Returns true if the entry has expired already function hasExpired(link) { - return (link.expires <= Date.now()); + return (link.expires <= Date.now()); } // 1. We don't want these properties to show up in console.log(cache) // 2. Private methods could come here, when io.js supports them -const _cost = Symbol('cost'); -const _head = Symbol('head'); -const _tail = Symbol('tail'); -const _limit = Symbol('limit'); -const _map = Symbol('map'); +const _cost = Symbol('cost'); +const _head = Symbol('head'); +const _tail = Symbol('tail'); +const _limit = Symbol('limit'); +const _map = Symbol('map'); class Cache { - constructor(limit, source) { - this[_map] = new Map(); - this[_cost] = 0; + constructor(limit, source) { + this[_map] = new Map(); + this[_cost] = 0; + + if (limit && limit[Symbol.iterator]) { + source = limit; + limit = (source instanceof Cache) ? source.limit : undefined; + } + this.limit = limit; + this.ttl = Infinity; + this.materialize = null; + this.generateKey = null; + + if (source instanceof Cache) + this._cloneCache(source); + else if (source) + this._cloneIterator(source); + } + + _cloneIterator(source) { + for (let entry of source) + this._set(entry[0], entry[1]); + } + + _cloneCache(source) { + let link = source[_tail]; + while (link) { + this._set(link.key, link.value, link); + link = link.previous; + } + this.materialize = source.materialize; + this.generateKey = source.generateKey; + this.ttl = source.ttl; + } + + + // Remove link from linked list. Used when deleting, and when moving link + // to head (during get). + _removeFromList(link) { + if (this[_head] === link) + this[_head] = link.next; + if (this[_tail] === link) + this[_tail] = link.previous; + + if (link.next) + link.next.previous = link.previous; + if (link.previous) + link.previous.next = link.next; + } + + // Prepend to linked list. Used when adding new link (set) or when moving + // existing link to beginning of linked list (get). + _prependToList(link) { + link.previous = null; + link.next = this[_head]; + if (this[_head]) + this[_head].previous = link; + this[_head] = link; + if (!this[_tail]) + this[_tail] = link; + } + + + get limit() { + // Getter because we use a setter + return this[_limit]; + } + + // Sets the cache limit + set limit(value) { + // You can use whatever value you want, but we need it to be a positive + // number, possibly Infinity + this[_limit] = actualLimit(value); + } + + // Returns the current cost + get cost() { + return this[_cost]; + } + + get size() { + return this[_map].size; + } + + + clear() { + this[_map].clear(); + this[_cost] = 0; + this[_head] = null; + this[_tail] = null; + } + + + _delete(key) { + const link = this[_map].get(key); + if (!link) + return false; + + this._removeFromList(link); + this[_map].delete(key); + + // Discount + this[_cost] = this[_cost] - link.cost; - if (limit && limit[Symbol.iterator]) { - source = limit; - limit = (source instanceof Cache) ? source.limit : undefined; + return true; } - this.limit = limit; - this.materialize = null; - this.generateKey = null; - - if (source instanceof Cache) - this._cloneCache(source); - else if (source) - this._cloneIterator(source); - } - - _cloneIterator(source) { - for (let entry of source) - this._set(entry[0], entry[1]); - } - - _cloneCache(source) { - let link = source[_tail]; - while (link) { - this._set(link.key, link.value, link); - link = link.previous; + + delete(keyObject) { + const key = this._generateKey(keyObject); + return this._delete(key); + } + + + // Evicts as many expired entries an least recently used entries to keep + // cache under limit. This can be O(N) as it may iterate over the entire + // cache twice, but in practice we generally evict as many entries as we add, + // so viewed over long time horizon, this is an O(1) operation. + _evict(limit) { + // Only evicts enough expired keys to make room for new key, if you need to + // evict all expired keys, use the iterator. + for (let link of this[_map].values()) { + if (this[_cost] <= limit) + break; + if (hasExpired(link)) + this._delete(link.key); + } + + // Remove from the tail is potentiall O(N), we in practice we usually evict + // as many entries as we add, so evict is O(1) spread over time + while (this.size && this[_cost] > limit) { + const leastRecent = this[_tail]; + this._delete(leastRecent.key); + } } - this.materialize = source.materialize; - this.generateKey = source.generateKey; - } - - - // Remove link from linked list. Used when deleting, and when moving link - // to head (during get). - _removeFromList(link) { - if (this[_head] === link) - this[_head] = link.next; - if (this[_tail] === link) - this[_tail] = link.previous; - - if (link.next) - link.next.previous = link.previous; - if (link.previous) - link.previous.next = link.next; - } - - // Prepend to linked list. Used when adding new link (set) or when moving - // existing link to beginning of linked list (get). - _prependToList(link) { - link.previous = null; - link.next = this[_head]; - if (this[_head]) - this[_head].previous = link; - this[_head] = link; - if (!this[_tail]) - this[_tail] = link; - } - - - get limit() { - // Getter because we use a setter - return this[_limit]; - } - - // Sets the cache limit - set limit(value) { - // You can use whatever value you want, but we need it to be a positive - // number, possibly Infinity - this[_limit] = actualLimit(value); - } - - // Returns the current cost - get cost() { - return this[_cost]; - } - - get size() { - return this[_map].size; - } - - - clear() { - this[_map].clear(); - this[_cost] = 0; - this[_head] = null; - this[_tail] = null; - } - - - _delete(key) { - const link = this[_map].get(key); - if (!link) - return false; - - this._removeFromList(link); - this[_map].delete(key); - - // Discount - this[_cost] = this[_cost] - link.cost; - - return true; - } - - delete(keyObject){ - const key = this._generateKey(keyObject); - return this._delete(key); - } - - - // Evicts as many expired entries an least recently used entries to keep - // cache under limit. This can be O(N) as it may iterate over the entire - // cache twice, but in practice we generally evict as many entries as we add, - // so viewed over long time horizon, this is an O(1) operation. - _evict(limit) { - // Only evicts enough expired keys to make room for new key, if you need to - // evict all expired keys, use the iterator. - for (let link of this[_map].values()) { - if (this[_cost] <= limit) - break; - if (hasExpired(link)) - this._delete(link.key); + + _generateKey(keyObj) { + return this.generateKey && this.generateKey(keyObj) || keyObj; } - // Remove from the tail is potentiall O(N), we in practice we usually evict - // as many entries as we add, so evict is O(1) spread over time - while (this.size && this[_cost] > limit) { - const leastRecent = this[_tail]; - this._delete(leastRecent.key); + // Returns the key value if set and not evicted yet. + get(keyObj) { + const key = this._generateKey(keyObj); + const link = this[_map].get(key); + // Although we do have the value, the contract is that we don't return + // expired values + if (link && !hasExpired(link)) { + this._moveLinkToHead(link); + return link.value; + } else if (this.materialize) + return this._materializeAndCache(key, keyObj); + else + return undefined; } - } - - _generateKey(keyObj){ - return this.generateKey&&this.generateKey(keyObj)||keyObj; - } - // Returns the key value if set and not evicted yet. - get(keyObj) { - const key = this._generateKey(keyObj); - const link = this[_map].get(key); - // Although we do have the value, the contract is that we don't return - // expired values - if (link && !hasExpired(link)) { - this._moveLinkToHead(link); - return link.value; - } else if (this.materialize) - return this._materializeAndCache(key,keyObj); - else - return undefined; - } - - - _moveLinkToHead(link) { - // Link becomes most recently used - const mostRecent = (this[_head] === link); - if (!mostRecent) { - // This is not the most CPU efficient, there's some redundant linked list - // changes that we can consolidate if we implemented a move; but in real - // life, you won't be able to measure the difference, so we opt to reuse - // existing methods, this gives us better test coverage - this._removeFromList(link); - this._prependToList(link); + + + _moveLinkToHead(link) { + // Link becomes most recently used + const mostRecent = (this[_head] === link); + if (!mostRecent) { + // This is not the most CPU efficient, there's some redundant linked list + // changes that we can consolidate if we implemented a move; but in real + // life, you won't be able to measure the difference, so we opt to reuse + // existing methods, this gives us better test coverage + this._removeFromList(link); + this._prependToList(link); + } } - } - _materializeAndCache(key,objKey) { - const self = this; - const promise = Promise.resolve(objKey).then(this.materialize); + _materializeAndCache(key, objKey) { + const self = this; + const promise = Promise.resolve(objKey).then(this.materialize); + + function deleteIfRejected() { + const entry = self[_map].get(key); + if (entry && entry.value === promise) + self._delete(key); + } - function deleteIfRejected() { - const entry = self[_map].get(key); - if (entry && entry.value === promise) - self._delete(key); + this._set(key, promise); + promise.catch(deleteIfRejected); + return promise; } - this._set(key, promise); - promise.catch(deleteIfRejected); - return promise; - } - - - // Returns true if key has been set and not evicted yet. - has(keyObj) { - const key = this._generateKey(keyObj); - const link = this[_map].get(key); - if (!link) - return false; - return !hasExpired(link); - } - - - *entries() { - let link = this[_head]; - while (link) { - // We take this opportunity to get rid of expired keys - if (hasExpired(link)) - this._delete(link.key); - else - yield [link.key, link.value]; - link = link.next; + + // Returns true if key has been set and not evicted yet. + has(keyObj) { + const key = this._generateKey(keyObj); + const link = this[_map].get(key); + if (!link) + return false; + return !hasExpired(link); + } + + + * entries() { + let link = this[_head]; + while (link) { + // We take this opportunity to get rid of expired keys + if (hasExpired(link)) + this._delete(link.key); + else + yield [link.key, link.value]; + link = link.next; + } } - } - - *values() { - for (let [key,value] of this.entries()) - yield value; - } - - *keys() { - for (let [key] of this.entries()) - yield key; - } - - forEach(callback, thisArg) { - // This could be `let [key, value] of` in future version - for (let [key,value] of this.entries()) - callback.call(thisArg, value, key, this); - } - - - // Stores the key and value. - // - // Each key is associated with a cost. The cost is a positive number, and - // the default value is 1. When the total cost is higher than the cache - // limit, it will start evicting least recently used values. You can use a - // cost of zero to keep the key indefinitely (or until it expires). - // - // Each key has a TTL associated with it. Expired keys are evicted first to - // make room for new keys. - // - // The following two are equivalent: - // - // _set(key, value) - // _set(key, value, { cost: 1, ttl: Infinity }) - _set(key, value, options) { - const cost = actualCost(options && options.cost); - const expires = ttlToExpires(options && options.ttl); - - this._delete(key); - - // If TTL is zero we're never going to return this key, we don't want to - // evict older keys either - if (expires <= Date.now()) - return this; - - // If this key can't fit, we don't want to evict other keys to make room - const canHoldKey = (cost <= this.limit); - if (!canHoldKey) - return this; - - // Evict enough keys to make room for this one - const leaveRoomForKey = (this.limit - cost); - this._evict(leaveRoomForKey); - - // Double linked list (previous, next) for O(1) reordering of recently used - // keys. Every place you see a link, it refes to an object with these - // properties. + + * values() { + for (let [key, value] of this.entries()) + yield value; + } + + * keys() { + for (let [key] of this.entries()) + yield key; + } + + forEach(callback, thisArg) { + // This could be `let [key, value] of` in future version + for (let [key, value] of this.entries()) + callback.call(thisArg, value, key, this); + } + + // TTL (milliseconds) to expiration (timestamp) + ttlToExpires(ttl) { + return Number.isInteger(ttl) ? Date.now() + ttl : this.ttl; + } + + // Stores the key and value. + // + // Each key is associated with a cost. The cost is a positive number, and + // the default value is 1. When the total cost is higher than the cache + // limit, it will start evicting least recently used values. You can use a + // cost of zero to keep the key indefinitely (or until it expires). // - // We need the key here as well so we can evict least recently used entries - const link = { - key, - value, - previous: null, - next: null, - cost, - expires, - - inspect(depth, inspectOptions) { - // console.log(cache) calls inspect(cache) on the Map, which ends up - // calling inspect on each map value (i.e. this link object). We want - // to show the stored value (just like a Map). - return Util.inspect(value, inspectOptions); - } - }; - - this._prependToList(link); - this[_map].set(key, link); - this[_cost] = this[_cost] + cost; - - // Map allows you to chain calls to set() - return this; - } - - set(keyObject,value,options){ - const key = this._generateKey(keyObject); - return this._set(key,value,options); - } - - // Util.inspect(cache) calls this, and Node's console.log uses inspect - inspect(depth, inspectOptions) { - return Util.inspect(this[_map], inspectOptions); - } + // Each key has a TTL associated with it. Expired keys are evicted first to + // make room for new keys. + // + // The following two are equivalent: + // + // _set(key, value) + // _set(key, value, { cost: 1, ttl: Infinity }) + _set(key, value, options) { + const cost = actualCost(options && options.cost); + const expires = this.ttlToExpires(options && options.ttl); + + this._delete(key); + + // If TTL is zero we're never going to return this key, we don't want to + // evict older keys either + if (expires <= Date.now()) + return this; + + // If this key can't fit, we don't want to evict other keys to make room + const canHoldKey = (cost <= this.limit); + if (!canHoldKey) + return this; + + // Evict enough keys to make room for this one + const leaveRoomForKey = (this.limit - cost); + this._evict(leaveRoomForKey); + + // Double linked list (previous, next) for O(1) reordering of recently used + // keys. Every place you see a link, it refes to an object with these + // properties. + // + // We need the key here as well so we can evict least recently used entries + const link = { + key, + value, + previous: null, + next: null, + cost, + expires, + + inspect(depth, inspectOptions) { + // console.log(cache) calls inspect(cache) on the Map, which ends up + // calling inspect on each map value (i.e. this link object). We want + // to show the stored value (just like a Map). + return Util.inspect(value, inspectOptions); + } + }; + + this._prependToList(link); + this[_map].set(key, link); + this[_cost] = this[_cost] + cost; + + // Map allows you to chain calls to set() + return this; + } + + set(keyObject, value, options) { + const key = this._generateKey(keyObject); + return this._set(key, value, options); + } + + // Util.inspect(cache) calls this, and Node's console.log uses inspect + inspect(depth, inspectOptions) { + return Util.inspect(this[_map], inspectOptions); + } } diff --git a/test.js b/test.js index 4341777..9eeb695 100644 --- a/test.js +++ b/test.js @@ -1,1020 +1,1102 @@ /* eslint-env mocha */ 'use strict'; -const assert = require('assert'); -const Cache = require('./index'); +const assert = require('assert'); +const Cache = require('./index'); // Until we get Array.from(iterator) or [...iterator] function arrayFrom(iterator) { - const array = []; - for (let item of iterator) - array.push(item); - return array; + const array = []; + for (let item of iterator) + array.push(item); + return array; } -describe('Cache with limit of zero', function() { +describe('Cache with limit of zero', function () { - let cache; + let cache; - before(function() { - cache = new Cache(0); - }); + before(function () { + cache = new Cache(0); + }); - it('should report a limit of zero', function() { - assert.equal(cache.limit, 0); - }); + it('should report a limit of zero', function () { + assert.equal(cache.limit, 0); + }); - it('should report a cost of zero', function() { - assert.equal(cache.cost, 0); - }); + it('should report a cost of zero', function () { + assert.equal(cache.cost, 0); + }); - it('should report a size of zero', function() { - assert.equal(cache.size, 0); - }); + it('should report a size of zero', function () { + assert.equal(cache.size, 0); + }); - describe('after setting two keys', function() { + describe('after setting two keys', function () { - before(function() { - cache - .set('x', 'XXX') - .set('y', 'YYY'); - }); + before(function () { + cache + .set('x', 'XXX') + .set('y', 'YYY'); + }); + + it('should have a size of zero', function () { + assert.equal(cache.size, 0); + }); + + it('should report a cost of zero', function () { + assert.equal(cache.cost, 0); + }); + + it('should have neither of these keys', function () { + assert.equal(cache.get('x'), undefined); + assert.equal(cache.get('y'), undefined); + }); + + it('should have no entries', function () { + const nextEntry = cache.entries().next(); + assert.equal(nextEntry.value, undefined); + assert.equal(nextEntry.done, true); + }); - it('should have a size of zero', function() { - assert.equal(cache.size, 0); }); - it('should report a cost of zero', function() { - assert.equal(cache.cost, 0); +}); + + +describe('Cache with negative limit', function () { + + let cache; + + before(function () { + cache = new Cache(-10); }); - it('should have neither of these keys', function() { - assert.equal(cache.get('x'), undefined); - assert.equal(cache.get('y'), undefined); + it('should report a limit of zero', function () { + assert.equal(cache.limit, 0); }); - it('should have no entries', function() { - const nextEntry = cache.entries().next(); - assert.equal(nextEntry.value, undefined); - assert.equal(nextEntry.done, true); + it('should report a cost of zero', function () { + assert.equal(cache.cost, 0); }); - }); + it('should report a size of zero', function () { + assert.equal(cache.size, 0); + }); }); -describe('Cache with negative limit', function() { +describe('Cache with three keys', function () { - let cache; + let cache; - before(function() { - cache = new Cache(-10); - }); + before(function () { + cache = new Cache(); + cache + .set('x', 'XXX') + .set('y', 'YYY') + .set('z', 'ZZZ'); + }); - it('should report a limit of zero', function() { - assert.equal(cache.limit, 0); - }); + it('should have a size of three', function () { + assert.equal(cache.size, 3); + }); - it('should report a cost of zero', function() { - assert.equal(cache.cost, 0); - }); + it('should have a cost of three', function () { + assert.equal(cache.cost, 3); + }); - it('should report a size of zero', function() { - assert.equal(cache.size, 0); - }); -}); + describe('get', function () { + it('should return value of key', function () { + assert.equal(cache.get('x'), 'XXX'); + assert.equal(cache.get('y'), 'YYY'); + assert.equal(cache.get('z'), 'ZZZ'); + }); -describe('Cache with three keys', function() { + it('should return undefined if key does not exist', function () { + assert.equal(cache.get('X'), undefined); + assert.equal(cache.get('a'), undefined); + assert.equal(cache.get(null), undefined); + }); - let cache; + }); - before(function() { - cache = new Cache(); - cache - .set('x', 'XXX') - .set('y', 'YYY') - .set('z', 'ZZZ'); - }); + describe('has', function () { - it('should have a size of three', function() { - assert.equal(cache.size, 3); - }); + it('should return true if key exists', function () { + assert.equal(cache.has('x'), true); + assert.equal(cache.has('y'), true); + assert.equal(cache.has('z'), true); + }); - it('should have a cost of three', function() { - assert.equal(cache.cost, 3); - }); + it('should return false if key does not exist', function () { + assert.equal(cache.has('X'), false); + assert.equal(cache.has('a'), false); + assert.equal(cache.has(null), false); + }); + }); - describe('get', function() { + describe('iterators', function () { - it('should return value of key', function() { - assert.equal(cache.get('x'), 'XXX'); - assert.equal(cache.get('y'), 'YYY'); - assert.equal(cache.get('z'), 'ZZZ'); - }); + before(function () { + // This sets the LRU order to y -> z -> x + cache.get('z'); + cache.get('y'); + }); - it('should return undefined if key does not exist', function() { - assert.equal(cache.get('X'), undefined); - assert.equal(cache.get('a'), undefined); - assert.equal(cache.get(null), undefined); - }); + it('should iterate from most to least recent', function () { + const entries = arrayFrom(cache[Symbol.iterator]()); + assert.deepEqual(entries, [['y', 'YYY'], ['z', 'ZZZ'], ['x', 'XXX']]); + }); - }); + it('should return all entries from most to least recent', function () { + const entries = arrayFrom(cache.entries()); + assert.deepEqual(entries, [['y', 'YYY'], ['z', 'ZZZ'], ['x', 'XXX']]); + }); - describe('has', function() { + it('should return all keys from most to least recent', function () { + const keys = arrayFrom(cache.keys()); + assert.deepEqual(keys, ['y', 'z', 'x']); + }); - it('should return true if key exists', function() { - assert.equal(cache.has('x'), true); - assert.equal(cache.has('y'), true); - assert.equal(cache.has('z'), true); - }); + it('should return all values from most to least recent', function () { + const values = arrayFrom(cache.values()); + assert.deepEqual(values, ['YYY', 'ZZZ', 'XXX']); + }); - it('should return false if key does not exist', function() { - assert.equal(cache.has('X'), false); - assert.equal(cache.has('a'), false); - assert.equal(cache.has(null), false); }); - }); + describe('forEach', function () { - describe('iterators', function() { + before(function () { + // This sets the LRU order to y -> z -> x + cache.get('z'); + cache.get('y'); + }); - before(function() { - // This sets the LRU order to y -> z -> x - cache.get('z'); - cache.get('y'); - }); + it('should call with value as first argument (most to least recent)', function () { + const values = []; + cache.forEach(function (value) { + values.push(value); + }); + assert.deepEqual(values, ['YYY', 'ZZZ', 'XXX']); + }); - it('should iterate from most to least recent', function() { - const entries = arrayFrom( cache[Symbol.iterator]() ); - assert.deepEqual(entries, [ [ 'y', 'YYY' ], [ 'z', 'ZZZ' ], [ 'x', 'XXX' ] ]); - }); + it('should call with key as second argument (most to least recent)', function () { + const keys = []; + cache.forEach(function (value, key) { + keys.push(key); + }); + assert.deepEqual(keys, ['y', 'z', 'x']); + }); - it('should return all entries from most to least recent', function() { - const entries = arrayFrom( cache.entries() ); - assert.deepEqual(entries, [ [ 'y', 'YYY' ], [ 'z', 'ZZZ' ], [ 'x', 'XXX' ] ]); - }); + it('should call with cache as third argument', function () { + cache.forEach(function (value, key, collection) { + assert.equal(collection, cache); + }); + }); - it('should return all keys from most to least recent', function() { - const keys = arrayFrom( cache.keys() ); - assert.deepEqual(keys, [ 'y', 'z', 'x' ]); - }); + it('should call with this = thisArg', function () { + const thisArg = {}; + cache.forEach(function () { + assert.equal(this, thisArg); + }, thisArg); + }); + + it('should call with this = undefined if no thisArg', function () { + cache.forEach(function () { + assert.equal(this, undefined); + }); + }); - it('should return all values from most to least recent', function() { - const values = arrayFrom( cache.values() ); - assert.deepEqual(values, [ 'YYY', 'ZZZ', 'XXX' ]); }); - }); - describe('forEach', function() { + describe('set existing key', function () { - before(function() { - // This sets the LRU order to y -> z -> x - cache.get('z'); - cache.get('y'); - }); + before(function () { + cache + .set('x', '') + .set('z', ''); + }); - it('should call with value as first argument (most to least recent)', function() { - const values = []; - cache.forEach(function(value) { - values.push(value); - }); - assert.deepEqual(values, [ 'YYY', 'ZZZ', 'XXX' ]); - }); + it('should not change size', function () { + assert.equal(cache.size, 3); + }); - it('should call with key as second argument (most to least recent)', function() { - const keys = []; - cache.forEach(function(value, key) { - keys.push(key); - }); - assert.deepEqual(keys, [ 'y', 'z', 'x' ]); - }); + it('should not change cost', function () { + assert.equal(cache.cost, 3); + }); - it('should call with cache as third argument', function() { - cache.forEach(function(value, key, collection) { - assert.equal(collection, cache); - }); - }); + it('should make last change the most recent entry', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, [['z', ''], ['x', ''], ['y', 'YYY']]); + }); - it('should call with this = thisArg', function() { - const thisArg = {}; - cache.forEach(function() { - assert.equal(this, thisArg); - }, thisArg); - }); + it('should return new key value when asked for', function () { + assert.equal(cache.get('x'), ''); + }); - it('should call with this = undefined if no thisArg', function() { - cache.forEach(function() { - assert.equal(this, undefined); - }); - }); - }); + describe('delete non-existing key', function () { + let returnValue; - describe('set existing key', function() { + before(function () { + cache.get('x'); + cache.get('z'); + returnValue = cache.delete('a'); + }); - before(function() { - cache - .set('x', '') - .set('z', ''); - }); + it('should return true', function () { + assert.equal(returnValue, false); + }); - it('should not change size', function() { - assert.equal(cache.size, 3); - }); + it('should not change size', function () { + assert.equal(cache.size, 3); + }); - it('should not change cost', function() { - assert.equal(cache.cost, 3); - }); + it('should not change cost', function () { + assert.equal(cache.cost, 3); + }); - it('should make last change the most recent entry', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, [ [ 'z', '' ], [ 'x', '' ], [ 'y', 'YYY' ] ]); - }); + it('should keep all the same keys', function () { + const entries = arrayFrom(cache.keys()); + assert.deepEqual(entries, ['z', 'x', 'y']); + }); - it('should return new key value when asked for', function() { - assert.equal(cache.get('x'), ''); - }); + }); - describe('delete non-existing key', function() { + describe('delete existing key', function () { - let returnValue; + let returnValue; - before(function() { - cache.get('x'); - cache.get('z'); - returnValue = cache.delete('a'); - }); + before(function () { + cache.get('x'); + cache.get('y'); // move to head to list to test conditional when removing + returnValue = cache.delete('y'); + }); - it('should return true', function() { - assert.equal(returnValue, false); - }); + it('should return true', function () { + assert.equal(returnValue, true); + }); - it('should not change size', function() { - assert.equal(cache.size, 3); - }); + it('should change size', function () { + assert.equal(cache.size, 2); + }); - it('should not change cost', function() { - assert.equal(cache.cost, 3); - }); + it('should change cost', function () { + assert.equal(cache.cost, 2); + }); - it('should keep all the same keys', function() { - const entries = arrayFrom( cache.keys() ); - assert.deepEqual(entries, [ 'z', 'x', 'y' ]); - }); + it('should not return key when iterating', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, [['x', ''], ['z', '']]); + }); - }); + it('should return undefined for key', function () { + assert.equal(cache.get('y'), undefined); + }); + it('should return existing key value when asked for', function () { + assert.equal(cache.get('x'), ''); + }); - describe('delete existing key', function() { + }); - let returnValue; - before(function() { - cache.get('x'); - cache.get('y'); // move to head to list to test conditional when removing - returnValue = cache.delete('y'); - }); + describe('clear cache', function () { + before(function () { + cache.clear(); + }); - it('should return true', function() { - assert.equal(returnValue, true); - }); + it('should reset size', function () { + assert.equal(cache.size, 0); + }); - it('should change size', function() { - assert.equal(cache.size, 2); - }); + it('should reset cost', function () { + assert.equal(cache.cost, 0); + }); - it('should change cost', function() { - assert.equal(cache.cost, 2); - }); + it('should have no keys', function () { + assert.equal(cache.has('x'), false); + assert.equal(cache.has('y'), false); + assert.equal(cache.has('z'), false); + }); - it('should not return key when iterating', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, [ [ 'x', '' ], [ 'z', '' ] ]); - }); + it('should have no values', function () { + assert.equal(cache.get('x'), undefined); + assert.equal(cache.get('y'), undefined); + assert.equal(cache.get('z'), undefined); + }); - it('should return undefined for key', function() { - assert.equal(cache.get('y'), undefined); - }); + it('should have no keys to iterate over', function () { + const keys = arrayFrom(cache.keys()); + assert.deepEqual(keys, []); + }); - it('should return existing key value when asked for', function() { - assert.equal(cache.get('x'), ''); - }); + it('should have no entries to iterate over', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, []); + }); + }); }); +}); - describe('clear cache', function() { - before(function() { - cache.clear(); - }); - it('should reset size', function() { - assert.equal(cache.size, 0); - }); +describe('Cache with limit of five', function () { - it('should reset cost', function() { - assert.equal(cache.cost, 0); - }); + let cache; - it('should have no keys', function() { - assert.equal(cache.has('x'), false); - assert.equal(cache.has('y'), false); - assert.equal(cache.has('z'), false); - }); + before(function () { + cache = new Cache(5); + }); - it('should have no values', function() { - assert.equal(cache.get('x'), undefined); - assert.equal(cache.get('y'), undefined); - assert.equal(cache.get('z'), undefined); - }); + it('should report a limit of five', function () { + assert.equal(cache.limit, 5); + }); - it('should have no keys to iterate over', function() { - const keys = arrayFrom( cache.keys() ); - assert.deepEqual(keys, []); - }); - it('should have no entries to iterate over', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, []); - }); + describe('after adding six keys', function () { - }); - }); + before(function () { + cache + .set('a', 1) + .set('b', 2) + .set('c', 3) + .set('d', 4) + .set('x', '!') + .set('e', 5) + .set('f', 6); + cache.delete('x'); // exercise some linked list logic + cache.set('g', 7); + }); -}); + it('should have size of 5', function () { + assert.equal(cache.size, 5); + }); + it('should have cost of 5', function () { + assert.equal(cache.cost, 5); + }); -describe('Cache with limit of five', function() { + it('should not have first key', function () { + assert.equal(cache.has('a'), false); + }); - let cache; + it('should not have second key', function () { + // Dropped to make room for g + assert.equal(cache.has('b'), false); + }); - before(function() { - cache = new Cache(5); - }); + it('should have third key', function () { + assert.equal(cache.has('c'), true); + }); - it('should report a limit of five', function() { - assert.equal(cache.limit, 5); - }); + it('should have seventh key', function () { + assert.equal(cache.has('g'), true); + }); + it('should have five entries to iterate over', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, [['g', 7], ['f', 6], ['e', 5], ['d', 4], ['c', 3]]); + }); - describe('after adding six keys', function() { - before(function() { - cache - .set('a', 1) - .set('b', 2) - .set('c', 3) - .set('d', 4) - .set('x', '!') - .set('e', 5) - .set('f', 6); - cache.delete('x'); // exercise some linked list logic - cache.set('g', 7); - }); + describe('set key with cost 3', function () { - it('should have size of 5', function() { - assert.equal(cache.size, 5); - }); + before(function () { + cache.set('h', 8, {cost: 3}); + }); - it('should have cost of 5', function() { - assert.equal(cache.cost, 5); - }); + it('should have size of 3', function () { + assert.equal(cache.size, 3); + }); - it('should not have first key', function() { - assert.equal(cache.has('a'), false); - }); + it('should still have cost of 5', function () { + assert.equal(cache.cost, 5); + }); - it('should not have second key', function() { - // Dropped to make room for g - assert.equal(cache.has('b'), false); - }); + it('should drop two oldest keys', function () { + assert.equal(cache.has('c'), false); + assert.equal(cache.has('d'), false); + }); - it('should have third key', function() { - assert.equal(cache.has('c'), true); - }); + it('should have five entries to iterate over', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, [['h', 8], ['g', 7], ['f', 6]]); + }); - it('should have seventh key', function() { - assert.equal(cache.has('g'), true); - }); - it('should have five entries to iterate over', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, [ [ 'g', 7 ], [ 'f', 6 ], [ 'e', 5 ], [ 'd', 4 ], [ 'c', 3 ] ]); - }); + describe('set key with cost 8', function () { + before(function () { + cache.set('i', 9, {cost: 8}); + }); - describe('set key with cost 3', function() { + it('should have size of 3', function () { + assert.equal(cache.size, 3); + }); - before(function() { - cache.set('h', 8, { cost: 3 }); - }); + it('should still have cost of 5', function () { + assert.equal(cache.cost, 5); + }); - it('should have size of 3', function() { - assert.equal(cache.size, 3); - }); + it('should not add new key', function () { + assert.equal(cache.has('i'), false); + }); - it('should still have cost of 5', function() { - assert.equal(cache.cost, 5); - }); + it('should keep all the same entries', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, [['h', 8], ['g', 7], ['f', 6]]); + }); + }); - it('should drop two oldest keys', function() { - assert.equal(cache.has('c'), false); - assert.equal(cache.has('d'), false); - }); - it('should have five entries to iterate over', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, [ [ 'h', 8 ], [ 'g', 7 ], [ 'f', 6 ] ]); - }); + describe('set another key with cost 3', function () { + before(function () { + cache.set('j', 10, {cost: 3}); + }); - describe('set key with cost 8', function() { + it('should have size of 1', function () { + assert.equal(cache.size, 1); + }); - before(function() { - cache.set('i', 9, { cost: 8 }); - }); + it('should drop cost to 3', function () { + assert.equal(cache.cost, 3); + }); - it('should have size of 3', function() { - assert.equal(cache.size, 3); - }); + it('should add new key', function () { + assert.equal(cache.has('j'), true); + }); - it('should still have cost of 5', function() { - assert.equal(cache.cost, 5); - }); + it('should drop all other keys', function () { + const keys = arrayFrom(cache.keys()); + assert.deepEqual(keys, ['j']); + }); - it('should not add new key', function() { - assert.equal(cache.has('i'), false); - }); + }); - it('should keep all the same entries', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, [ [ 'h', 8 ], [ 'g', 7 ], [ 'f', 6 ] ]); }); - }); + }); - describe('set another key with cost 3', function() { +}); - before(function() { - cache.set('j', 10, { cost: 3 }); - }); - it('should have size of 1', function() { - assert.equal(cache.size, 1); +describe('Cache with no limit', function () { + + let cache; + + before(function () { + cache = new Cache(); + }); + + it('should report a limit of Infinity', function () { + assert.equal(cache.limit, Infinity); + }); + + + describe('after setting two large keys', function () { + + before(function () { + cache + .set('x', 'XXX', {cost: Number.MAX_SAFE_INTEGER}) + .set('y', 'YYY', {cost: Number.MAX_SAFE_INTEGER}); }); - it('should drop cost to 3', function() { - assert.equal(cache.cost, 3); + it('should have size of 2', function () { + assert.equal(cache.size, 2); }); - it('should add new key', function() { - assert.equal(cache.has('j'), true); + it('should have cost double large', function () { + assert.equal(cache.cost, Number.MAX_SAFE_INTEGER * 2); }); - it('should drop all other keys', function() { - const keys = arrayFrom( cache.keys() ); - assert.deepEqual(keys, [ 'j' ]); + it('should still have both keys', function () { + assert.equal(cache.get('x'), 'XXX'); + assert.equal(cache.get('y'), 'YYY'); }); - }); + it('should have both entries', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, [['y', 'YYY'], ['x', 'XXX']]); + }); }); - }); - }); -describe('Cache with no limit', function() { - - let cache; - - before(function() { - cache = new Cache(); - }); - - it('should report a limit of Infinity', function() { - assert.equal(cache.limit, Infinity); - }); +describe('Iterate and delete', function () { + let cache; + let keys; - describe('after setting two large keys', function() { + before(function () { + cache = new Cache(); + cache.set('x').set('y').set('z'); - before(function() { - cache - .set('x', 'XXX', { cost: Number.MAX_SAFE_INTEGER }) - .set('y', 'YYY', { cost: Number.MAX_SAFE_INTEGER }); + keys = []; + for (let key of cache.keys()) { + keys.push(key); + cache.delete(key); + } }); - it('should have size of 2', function() { - assert.equal(cache.size, 2); + it('should iterate over all keys from most to least recent', function () { + assert.deepEqual(keys, ['z', 'y', 'x']); }); - it('should have cost double large', function() { - assert.equal(cache.cost, Number.MAX_SAFE_INTEGER * 2); + it('should delete all keys', function () { + const nextKey = cache.keys().next(); + assert(!nextKey.value && nextKey.done); }); - it('should still have both keys', function() { - assert.equal(cache.get('x'), 'XXX'); - assert.equal(cache.get('y'), 'YYY'); + it('should have size of 0', function () { + assert.equal(cache.size, 0); }); - it('should have both entries', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, [ [ 'y', 'YYY' ], [ 'x', 'XXX' ] ]); + it('should have cost of 0', function () { + assert.equal(cache.cost, 0); }); - }); - }); -describe('Iterate and delete', function() { +describe('Cache with expiring entries', function () { - let cache; - let keys; + let cache; - before(function() { - cache = new Cache(); - cache.set('x').set('y').set('z'); + before(function () { + cache = new Cache(4); + // `a` expires immediately, never gets stored + cache + .set('a', 1, {ttl: 0}) + .set('b', 2, {ttl: 50}) + .set('c', 3, {ttl: 10}) + .set('d', 4); + }); - keys = []; - for (let key of cache.keys()) { - keys.push(key); - cache.delete(key); - } - }); + before(function (done) { + // This will expire `c` + setTimeout(done, 10); + }); - it('should iterate over all keys from most to least recent', function() { - assert.deepEqual(keys, [ 'z', 'y', 'x' ]); - }); + it('should not store immediately expired keys', function () { + assert.equal(cache.size, 3); + }); - it('should delete all keys', function() { - const nextKey = cache.keys().next(); - assert( !nextKey.value && nextKey.done ); - }); - it('should have size of 0', function() { - assert.equal(cache.size, 0); - }); + describe('expired key', function () { - it('should have cost of 0', function() { - assert.equal(cache.cost, 0); - }); + it('should not have key', function () { + assert.equal(cache.has('a'), false); + }); -}); + it('should not have value', function () { + assert.equal(cache.get('a'), undefined); + }); + }); -describe('Cache with expiring entries', function() { + describe('not yet expired key', function () { - let cache; + it('should have key', function () { + assert.equal(cache.has('b'), true); + }); - before(function() { - cache = new Cache(4); - // `a` expires immediately, never gets stored - cache - .set('a', 1, { ttl: 0 }) - .set('b', 2, { ttl: 50 }) - .set('c', 3, { ttl: 10 }) - .set('d', 4); - }); + it('should have value', function () { + assert.equal(cache.get('b'), 2); + }); - before(function(done) { - // This will expire `c` - setTimeout(done, 10); - }); + }); - it('should not store immediately expired keys', function() { - assert.equal(cache.size, 3); - }); + describe('iterators', function () { - describe('expired key', function() { + before(function () { + assert.equal(cache.size, 3); + }); - it('should not have key', function() { - assert.equal(cache.has('a'), false); - }); + it('should skip expired keys', function () { + const keys = arrayFrom(cache.keys()); + assert.deepEqual(keys, ['b', 'd']); + }); - it('should not have value', function() { - assert.equal(cache.get('a'), undefined); - }); + it('should skip expired values', function () { + const values = arrayFrom(cache.values()); + assert.deepEqual(values, [2, 4]); + }); - }); + it('should skip expired entries', function () { + const entries = arrayFrom(cache.entries()); + assert.deepEqual(entries, [['b', 2], ['d', 4]]); + }); - describe('not yet expired key', function() { + it('should not forEach over expired entries', function () { + const keys = []; + cache.forEach(function (value, key) { + keys.push(key); + }); + assert.deepEqual(keys, ['b', 'd']); + }); - it('should have key', function() { - assert.equal(cache.has('b'), true); - }); + it('should also discard expired keys', function () { + assert.equal(cache.size, 2); + }); - it('should have value', function() { - assert.equal(cache.get('b'), 2); }); - }); + describe('set three keys', function () { - describe('iterators', function() { + before(function () { + cache.get('d'); // enforce recent order + cache + .set('e', 5) + .set('f', 6) + .set('g', 7, {ttl: 0}); + }); - before(function() { - assert.equal(cache.size, 3); - }); + it('should have a size 4', function () { + assert.equal(cache.size, 4); + }); - it('should skip expired keys', function() { - const keys = arrayFrom( cache.keys() ); - assert.deepEqual(keys, [ 'b', 'd' ]); - }); + it('should have a cost 4', function () { + assert.equal(cache.cost, 4); + }); - it('should skip expired values', function() { - const values = arrayFrom( cache.values() ); - assert.deepEqual(values, [ 2, 4 ]); - }); + it('should keep unexpired keys', function () { + assert.equal(cache.has('b'), true); + assert.equal(cache.has('d'), true); + }); - it('should skip expired entries', function() { - const entries = arrayFrom( cache.entries() ); - assert.deepEqual(entries, [ [ 'b', 2 ], [ 'd', 4 ] ]); - }); + it('should add new unexpired keys', function () { + assert.equal(cache.has('e'), true); + assert.equal(cache.has('f'), true); + }); - it('should not forEach over expired entries', function() { - const keys = []; - cache.forEach(function(value, key) { - keys.push(key); - }); - assert.deepEqual(keys, [ 'b', 'd' ]); - }); + it('should not add immediately expired keys', function () { + assert.equal(cache.has('g'), false); + }); + + it('should iterate over recent entries', function () { + const entries = arrayFrom(cache); + assert.deepEqual(entries, [['f', 6], ['e', 5], ['d', 4], ['b', 2]]); + }); - it('should also discard expired keys', function() { - assert.equal(cache.size, 2); }); - }); +}); +describe('Cache with expiring entries using default ttl', function () { - describe('set three keys', function() { + let cache; - before(function() { - cache.get('d'); // enforce recent order - cache - .set('e', 5) - .set('f', 6) - .set('g', 7, { ttl: 0 }); + before(function () { + cache = new Cache(3); + cache.ttl = 20; + // `a` expires immediately, never gets stored + cache + .set('b', 2, {ttl: 50}) + .set('c', 3, {ttl: 10}) + .set('d', 4); }); - it('should have a size 4', function() { - assert.equal(cache.size, 4); + before(function (done) { + // This will expire `c` & 'd' + setTimeout(done, 20); }); - it('should have a cost 4', function() { - assert.equal(cache.cost, 4); - }); - it('should keep unexpired keys', function() { - assert.equal(cache.has('b'), true); - assert.equal(cache.has('d'), true); - }); + describe('expired key', function () { - it('should add new unexpired keys', function() { - assert.equal(cache.has('e'), true); - assert.equal(cache.has('f'), true); - }); + it('should not have key', function () { + assert.equal(cache.has('d'), false); + }); - it('should not add immediately expired keys', function() { - assert.equal(cache.has('g'), false); - }); + it('should not have value', function () { + assert.equal(cache.get('d'), undefined); + }); - it('should iterate over recent entries', function() { - const entries = arrayFrom( cache ); - assert.deepEqual(entries, [ [ 'f', 6 ], [ 'e', 5 ], [ 'd', 4 ], [ 'b', 2 ] ]); }); - }); - -}); + describe('not yet expired key', function () { + it('should have key', function () { + assert.equal(cache.has('b'), true); + }); -describe('Clone Map', function() { + it('should have value', function () { + assert.equal(cache.get('b'), 2); + }); - let fromMap; + }); - before(function() { - const map = new Map([ [ 'a', 1 ], [ 'b', 2 ] ]); - fromMap = new Cache(2, map); - fromMap.materialize = function() {}; - }); - it('should have the same keys as the source, but most to least recent', function() { - const keys = arrayFrom( fromMap.keys() ); - assert.deepEqual(keys, [ 'b', 'a' ]); - }); + describe('iterators', function () { - it('should have the same values as the source, but most to least recent', function() { - const values = arrayFrom( fromMap.values() ); - assert.deepEqual(values, [ 2, 1 ]); - }); + before(function () { + assert.equal(cache.size, 2); + }); - it('should have same size as the source', function() { - assert.equal(fromMap.size, 2); - }); + it('should skip expired keys', function () { + const keys = arrayFrom(cache.keys()); + assert.deepEqual(keys, ['b']); + }); - it('should have same cost as the source', function() { - assert.equal(fromMap.cost, 2); - }); + it('should skip expired values', function () { + const values = arrayFrom(cache.values()); + assert.deepEqual(values, [2]); + }); + it('should skip expired entries', function () { + const entries = arrayFrom(cache.entries()); + assert.deepEqual(entries, [['b', 2]]); + }); - describe('Clone cache', function() { + it('should not forEach over expired entries', function () { + const keys = []; + cache.forEach(function (value, key) { + keys.push(key); + }); + assert.deepEqual(keys, ['b']); + }); - let fromCache; + it('should also discard expired keys', function () { + assert.equal(cache.size, 1); + }); - before(function() { - fromCache = new Cache(fromMap); }); - it('should have the same limit as the source', function() { - assert.equal(fromCache.limit, 2); - }); +}); + +describe('Clone Map', function () { - it('should have same keys as the source, most to least recent', function() { - const keys = arrayFrom( fromCache.keys() ); - assert.deepEqual(keys, [ 'b', 'a' ]); + let fromMap; + + before(function () { + const map = new Map([['a', 1], ['b', 2]]); + fromMap = new Cache(2, map); + fromMap.materialize = function () { + }; }); - it('should have same values as the source, most to least recent', function() { - const values = arrayFrom( fromCache.values() ); - assert.deepEqual(values, [ 2, 1 ]); + it('should have the same keys as the source, but most to least recent', function () { + const keys = arrayFrom(fromMap.keys()); + assert.deepEqual(keys, ['b', 'a']); }); - it('should have same size as the source', function() { - assert.equal(fromCache.size, 2); + it('should have the same values as the source, but most to least recent', function () { + const values = arrayFrom(fromMap.values()); + assert.deepEqual(values, [2, 1]); }); - it('should have same cost as the source', function() { - assert.equal(fromCache.cost, 2); + it('should have same size as the source', function () { + assert.equal(fromMap.size, 2); }); - it('should have same materialize function', function() { - assert.equal(fromCache.materialize, fromMap.materialize); + it('should have same cost as the source', function () { + assert.equal(fromMap.cost, 2); }); - }); -}); + describe('Clone cache', function () { -describe('With materialize function', function() { + let fromCache; - let cache; - let resolved; + before(function () { + fromCache = new Cache(fromMap); + }); - before(function() { - cache = new Cache(); - cache.materialize = function(key) { - const upper = key.toUpperCase(); - return `${upper}${upper}${upper}`; - }; - }); + it('should have the same limit as the source', function () { + assert.equal(fromCache.limit, 2); + }); - before(function() { - resolved = cache.get('x'); - return resolved; - }); + it('should have same keys as the source, most to least recent', function () { + const keys = arrayFrom(fromCache.keys()); + assert.deepEqual(keys, ['b', 'a']); + }); - it('should resolve to materialized value', function() { - return resolved - .then(function(value) { - assert.equal(value, 'XXX'); - }); - }); + it('should have same values as the source, most to least recent', function () { + const values = arrayFrom(fromCache.values()); + assert.deepEqual(values, [2, 1]); + }); - it('should have size of 1', function() { - assert.equal( cache.size, 1 ); - }); + it('should have same size as the source', function () { + assert.equal(fromCache.size, 2); + }); - it('should have cost of 1', function() { - assert.equal( cache.size, 1 ); - }); + it('should have same cost as the source', function () { + assert.equal(fromCache.cost, 2); + }); - it('should have new key', function() { - assert(cache.has('x')); - }); + it('should have same materialize function', function () { + assert.equal(fromCache.materialize, fromMap.materialize); + }); + }); +}); - describe('get again', function() { - it('should return same value', function() { - const again = cache.get('x'); - assert.equal(again, resolved); - }); - }); +describe('With materialize function', function () { - describe('unable to resolve', function() { + let cache; + let resolved; - let rejected; + before(function () { + cache = new Cache(); + cache.materialize = function (key) { + const upper = key.toUpperCase(); + return `${upper}${upper}${upper}`; + }; + }); + + before(function () { + resolved = cache.get('x'); + return resolved; + }); - before(function() { - cache.clear(); - cache.materialize = function() { - throw new Error('fail'); - }; - rejected = cache.get('y'); + it('should resolve to materialized value', function () { + return resolved + .then(function (value) { + assert.equal(value, 'XXX'); + }); }); - it('should reject the get', function() { - return rejected.then(function() { - throw new Error('Not expected to arrive here'); - }, function() { - // Not an error - }); + it('should have size of 1', function () { + assert.equal(cache.size, 1); }); - it('should still have size of 0', function() { - assert.equal( cache.size, 0 ); + it('should have cost of 1', function () { + assert.equal(cache.size, 1); }); - it('should still have cost of 0', function() { - assert.equal( cache.size, 0 ); + it('should have new key', function () { + assert(cache.has('x')); }); - it('should not have new key', function() { - assert.equal( cache.has('y'), false); + + describe('get again', function () { + it('should return same value', function () { + const again = cache.get('x'); + assert.equal(again, resolved); + }); }); - }); + describe('unable to resolve', function () { + + let rejected; + + before(function () { + cache.clear(); + cache.materialize = function () { + throw new Error('fail'); + }; + rejected = cache.get('y'); + }); + + it('should reject the get', function () { + return rejected.then(function () { + throw new Error('Not expected to arrive here'); + }, function () { + // Not an error + }); + }); - describe('materialize and set', function() { + it('should still have size of 0', function () { + assert.equal(cache.size, 0); + }); - let promise; + it('should still have cost of 0', function () { + assert.equal(cache.size, 0); + }); - before(function() { - cache.clear(); - cache.materialize = function(key) { - const lazy = Promise.resolve('ZZZ'); - lazy.then(function() { - cache.set(key, lazy, { cost: 5 }); + it('should not have new key', function () { + assert.equal(cache.has('y'), false); }); - return lazy; - }; - promise = cache.get('z'); + }); - it('should retrieve returned value', function() { - return promise - .then(function(value) { - assert.equal(value, 'ZZZ'); + + describe('materialize and set', function () { + + let promise; + + before(function () { + cache.clear(); + cache.materialize = function (key) { + const lazy = Promise.resolve('ZZZ'); + lazy.then(function () { + cache.set(key, lazy, {cost: 5}); + }); + return lazy; + }; + promise = cache.get('z'); }); - }); - it('should have size of 1', function() { - assert.equal( cache.size, 1 ); - }); + it('should retrieve returned value', function () { + return promise + .then(function (value) { + assert.equal(value, 'ZZZ'); + }); + }); - it('should have cost of 5', function() { - assert.equal( cache.cost, 5 ); - }); + it('should have size of 1', function () { + assert.equal(cache.size, 1); + }); - it('should have key', function() { - assert( cache.get('z') ); - }); + it('should have cost of 5', function () { + assert.equal(cache.cost, 5); + }); - it('should keep returning value', function() { - return cache.get('z') - .then(function(value) { - assert.equal(value, 'ZZZ'); + it('should have key', function () { + assert(cache.get('z')); }); - }); - }); + it('should keep returning value', function () { + return cache.get('z') + .then(function (value) { + assert.equal(value, 'ZZZ'); + }); + }); + + }); }); -describe('With materialize function using key object', function() { +describe('With materialize function using key object', function () { let cache; let resolved; - before(function() { + before(function () { cache = new Cache(); - cache.materialize = function(key) { + cache.materialize = function (key) { return `${key.a.toUpperCase()}${key.b.toUpperCase()}`; }; - cache.generateKey = function(keyObject){ - return `${keyObject.a}.${keyObject.b}`; + cache.generateKey = function (keyObject) { + return `${keyObject.a}.${keyObject.b}`; } }); - before(function() { - resolved = cache.get({a:'x',b:'y'}); + before(function () { + resolved = cache.get({a: 'x', b: 'y'}); return resolved; }); - it('should resolve to materialized value', function() { + it('should resolve to materialized value', function () { return resolved - .then(function(value) { + .then(function (value) { assert.equal(value, 'XY'); }); }); - it('should have size of 1', function() { - assert.equal( cache.size, 1 ); + it('should have size of 1', function () { + assert.equal(cache.size, 1); }); - it('should have cost of 1', function() { - assert.equal( cache.size, 1 ); + it('should have cost of 1', function () { + assert.equal(cache.size, 1); }); - it('should have new key', function() { - assert(cache.has({a:'x',b:'y'})); + it('should have new key', function () { + assert(cache.has({a: 'x', b: 'y'})); }); - describe('get again', function() { - it('should return same value', function() { - const again = cache.get({a:'x',b:'y'}); + describe('get again', function () { + it('should return same value', function () { + const again = cache.get({a: 'x', b: 'y'}); assert.equal(again, resolved); }); }); - describe('unable to resolve', function() { + describe('unable to resolve', function () { let rejected; - before(function() { + before(function () { cache.clear(); - cache.materialize = function() { + cache.materialize = function () { throw new Error('fail'); }; - rejected = cache.get({a:'y'}); + rejected = cache.get({a: 'y'}); }); - it('should reject the get', function() { - return rejected.then(function() { + it('should reject the get', function () { + return rejected.then(function () { throw new Error('Not expected to arrive here'); - }, function() { + }, function () { // Not an error }); }); - it('should still have size of 0', function() { - assert.equal( cache.size, 0 ); + it('should still have size of 0', function () { + assert.equal(cache.size, 0); }); - it('should still have cost of 0', function() { - assert.equal( cache.size, 0 ); + it('should still have cost of 0', function () { + assert.equal(cache.size, 0); }); - it('should not have new key', function() { - assert.equal( cache.has({a:'y'}), false); + it('should not have new key', function () { + assert.equal(cache.has({a: 'y'}), false); }); }); - describe('materialize and set', function() { + describe('materialize and set', function () { let promise; - before(function() { + before(function () { cache.clear(); - cache.materialize = function(keyObject) { + cache.materialize = function (keyObject) { const lazy = Promise.resolve('ZZZ'); - lazy.then(function() { - cache.set(keyObject, lazy, { cost: 5 }); + lazy.then(function () { + cache.set(keyObject, lazy, {cost: 5}); }); return lazy; }; - promise = cache.get({a:'z'}); + promise = cache.get({a: 'z'}); }); - it('should retrieve returned value', function() { + it('should retrieve returned value', function () { return promise - .then(function(value) { + .then(function (value) { assert.equal(value, 'ZZZ'); }); }); - it('should have size of 1', function() { - assert.equal( cache.size, 1 ); + it('should have size of 1', function () { + assert.equal(cache.size, 1); }); - it('should have cost of 5', function() { - assert.equal( cache.cost, 5 ); + it('should have cost of 5', function () { + assert.equal(cache.cost, 5); }); - it('should have key', function() { - assert( cache.get({a:'z'}) ); + it('should have key', function () { + assert(cache.get({a: 'z'})); }); - it('should keep returning value', function() { - return cache.get({a:'z'}) - .then(function(value) { + it('should keep returning value', function () { + return cache.get({a: 'z'}) + .then(function (value) { assert.equal(value, 'ZZZ'); }); }); From 44f241efb5f06ab6d1cdf148f9589f9fdb083b2b Mon Sep 17 00:00:00 2001 From: Ran-P Date: Sun, 19 Nov 2017 11:00:18 +0200 Subject: [PATCH 3/5] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d33cef7..5b2342c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "caching-map", - "version": "1.0.2", + "version": "1.0.3", "description": "LRU cache for people who like ES6 and promises", "main": "index.js", "engines": { From 282cd2b64371ffb7259b79c9c525bd6497bdd3af Mon Sep 17 00:00:00 2001 From: Ran-P Date: Sun, 19 Nov 2017 11:39:26 +0200 Subject: [PATCH 4/5] fix default ttl --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index cb3e07b..da604a0 100644 --- a/index.js +++ b/index.js @@ -257,7 +257,7 @@ class Cache { // TTL (milliseconds) to expiration (timestamp) ttlToExpires(ttl) { - return Number.isInteger(ttl) ? Date.now() + ttl : this.ttl; + return Number.isInteger(ttl) ? Date.now() + ttl : Date.now() + this.ttl; } // Stores the key and value. From 262242e4bebb312e90b1f81419f573f75ad28aa7 Mon Sep 17 00:00:00 2001 From: Ran-P Date: Sun, 19 Nov 2017 11:42:03 +0200 Subject: [PATCH 5/5] fix default ttl, update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b2342c..005fbd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "caching-map", - "version": "1.0.3", + "version": "1.0.4", "description": "LRU cache for people who like ES6 and promises", "main": "index.js", "engines": {