diff --git a/lru-cache-with-delete.js b/lru-cache-with-delete.js index b0fd12e3..ad8d6896 100644 --- a/lru-cache-with-delete.js +++ b/lru-cache-with-delete.js @@ -35,8 +35,21 @@ function LRUCacheWithDelete(Keys, Values, capacity) { for (var k in LRUCache.prototype) LRUCacheWithDelete.prototype[k] = LRUCache.prototype[k]; -if (typeof Symbol !== 'undefined') + +/** + * If possible, attaching the + * * #.entries method to Symbol.iterator (allowing `for (const foo of cache) { ... }`) + * * the summaryString method to Symbol.toStringTag (allowing `\`${cache}\`` to work) + * * the inspect method to Symbol.for('nodejs.util.inspect.custom') (making `console.log(cache)` liveable) + */ +if (typeof Symbol !== 'undefined') { LRUCacheWithDelete.prototype[Symbol.iterator] = LRUCache.prototype[Symbol.iterator]; + Object.defineProperty(LRUCacheWithDelete.prototype, Symbol.toStringTag, { + get: function () { return `${this.constructor.name}:${this.size}/${this.capacity}`; }, + }); + LRUCacheWithDelete.prototype[Symbol.for('nodejs.util.inspect.custom')] = LRUCache.prototype.inspect; +} +Object.defineProperty(LRUCacheWithDelete.prototype, 'summary', Object.getOwnPropertyDescriptor(LRUCache.prototype, 'summary')); /** * Method used to clear the structure. diff --git a/lru-cache.js b/lru-cache.js index d87a4d4e..29f0cf5e 100644 --- a/lru-cache.js +++ b/lru-cache.js @@ -18,7 +18,8 @@ var Iterator = require('obliterator/iterator'), forEach = require('obliterator/foreach'), typed = require('./utils/typed-arrays.js'), - iterables = require('./utils/iterables.js'); + iterables = require('./utils/iterables.js'), + {snipToLast} = require('./utils/snip.js'); /** * LRUCache. @@ -368,29 +369,59 @@ LRUCache.prototype.entries = function() { }); }; +/** + * Return a short string for interpolation: `LRUCache:size/capacity` + */ +Object.defineProperty(LRUCache.prototype, 'summary', { + get: function() { return `${this.constructor.name}:${this.size}/${this.capacity}`; }, +}); + /** * Attaching the #.entries method to Symbol.iterator if possible. */ -if (typeof Symbol !== 'undefined') +if (typeof Symbol !== 'undefined') { LRUCache.prototype[Symbol.iterator] = LRUCache.prototype.entries; + Object.defineProperty(LRUCache.prototype, Symbol.toStringTag, { + get: function () { return this.summary; }, + }); +} + +LRUCache.defaultMaxToDump = 20; /** - * Convenience known methods. + * Provide a reasonably-sized view of the object. + * + * @param {number} [depth] - When < 0, only the toString() summary is returned + * @param {object} [options = {}] - settings for output + * @param {boolean} [options.all = false] - When true, returns the object with all properties, ignoring limits + * @param {number} [options.maxToDump = 20] - When size > maxToDump, lists only the + * youngest `maxToDump - 2`, a placeholder with the number + * omitted, and the single oldest item. The secret variable + * LRUCache.defaultMaxToDump determines the default limit. + * @return {Map} + * */ -LRUCache.prototype.inspect = function() { +LRUCache.prototype.inspect = function(depth, options = {}) { + if (arguments.length <= 1) { options = depth || {}; depth = 2; } + if (options.all) { var ret = {}; Object.assign(ret, this); return ret; } + if (depth < 0) { return this.toString(); } + var maxToDump = options.maxToDump || LRUCache.defaultMaxToDump; var proxy = new Map(); - var iterator = this.entries(), - step; + var last = [this.K[this.tail], this.V[this.tail]]; + snipToLast(this.entries(), proxy, {maxToDump, size: this.size, last}); - while ((step = iterator.next(), !step.done)) - proxy.set(step.value[0], step.value[1]); - - // Trick so that node displays the name of the constructor + // Trick so that node displays the name of the constructor (does not work in modern node) Object.defineProperty(proxy, 'constructor', { value: LRUCache, enumerable: false }); + if (typeof Symbol !== 'undefined') { + var self = this; + Object.defineProperty(proxy, Symbol.toStringTag, { + get: function () { return `${self.constructor.name}:${self.size}/${self.capacity}`; }, + }); + } return proxy; }; diff --git a/lru-map-with-delete.js b/lru-map-with-delete.js index 0e58fa76..4d9397bc 100644 --- a/lru-map-with-delete.js +++ b/lru-map-with-delete.js @@ -35,8 +35,21 @@ function LRUMapWithDelete(Keys, Values, capacity) { for (var k in LRUMap.prototype) LRUMapWithDelete.prototype[k] = LRUMap.prototype[k]; -if (typeof Symbol !== 'undefined') + +/** + * If possible, attaching the + * * #.entries method to Symbol.iterator (allowing `for (const foo of cache) { ... }`) + * * the summaryString method to Symbol.toStringTag (allowing `\`${cache}\`` to work) + * * the inspect method to Symbol.for('nodejs.util.inspect.custom') (making `console.log(cache)` liveable) + */ +if (typeof Symbol !== 'undefined') { LRUMapWithDelete.prototype[Symbol.iterator] = LRUMap.prototype[Symbol.iterator]; + Object.defineProperty(LRUMapWithDelete.prototype, Symbol.toStringTag, { + get: function () { return `${this.constructor.name}:${this.size}/${this.capacity}`; }, + }); + LRUMapWithDelete.prototype[Symbol.for('nodejs.util.inspect.custom')] = LRUMap.prototype.inspect; +} +Object.defineProperty(LRUMapWithDelete.prototype, 'summary', Object.getOwnPropertyDescriptor(LRUMap.prototype, 'summary')); /** * Method used to clear the structure. diff --git a/lru-map.js b/lru-map.js index 87f7aa38..f6c2c298 100644 --- a/lru-map.js +++ b/lru-map.js @@ -211,17 +211,27 @@ LRUMap.prototype.forEach = LRUCache.prototype.forEach; LRUMap.prototype.keys = LRUCache.prototype.keys; LRUMap.prototype.values = LRUCache.prototype.values; LRUMap.prototype.entries = LRUCache.prototype.entries; +LRUMap.prototype.summaryString = LRUCache.prototype.summaryString; /** - * Attaching the #.entries method to Symbol.iterator if possible. + * Inherit methods */ -if (typeof Symbol !== 'undefined') - LRUMap.prototype[Symbol.iterator] = LRUMap.prototype.entries; +LRUMap.prototype.inspect = LRUCache.prototype.inspect; /** - * Convenience known methods. + * If possible, attaching the + * * #.entries method to Symbol.iterator (allowing `for (const foo of cache) { ... }`) + * * the summaryString method to Symbol.toStringTag (allowing `\`${cache}\`` to work) + * * the inspect method to Symbol.for('nodejs.util.inspect.custom') (making `console.log(cache)` liveable) */ -LRUMap.prototype.inspect = LRUCache.prototype.inspect; +if (typeof Symbol !== 'undefined') { + LRUMap.prototype[Symbol.iterator] = LRUMap.prototype.entries; + Object.defineProperty(LRUMap.prototype, Symbol.toStringTag, { + get: function () { return `${this.constructor.name}:${this.size}/${this.capacity}`; }, + }); + LRUMap.prototype[Symbol.for('nodejs.util.inspect.custom')] = LRUCache.prototype.inspect; +} +Object.defineProperty(LRUMap.prototype, 'summary', Object.getOwnPropertyDescriptor(LRUCache.prototype, 'summary')); /** * Static @.from function taking an arbitrary iterable & converting it into diff --git a/test/lru-cache.js b/test/lru-cache.js index 2cfc6afa..44d887fc 100644 --- a/test/lru-cache.js +++ b/test/lru-cache.js @@ -7,6 +7,7 @@ var assert = require('assert'), LRUMap = require('../lru-map.js'), LRUCacheWithDelete = require('../lru-cache-with-delete.js'), LRUMapWithDelete = require('../lru-map-with-delete.js'); +var NodeUtil = require('util'); function makeTests(Cache, name) { describe(name, function() { @@ -62,7 +63,7 @@ function makeTests(Cache, name) { assert.strictEqual(cache.peek('two'), 5); assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['four', 4], ['two', 5]]); - if (name === 'LRUCache' || name === 'LRUCacheWithDelete') + if (/LRUCache/.test(name)) assert.strictEqual(Object.keys(cache.items).length, 3); else assert.strictEqual(cache.items.size, 3); @@ -222,7 +223,7 @@ function makeTests(Cache, name) { assert.deepStrictEqual(entries, Array.from(cache.entries())); }); - if ((name === 'LRUCacheWithDelete') || (name === 'LRUMapWithDelete')) { + if (/With/.test(name)) { it('should be possible to delete keys from a LRU cache.', function() { var cache = new Cache(3); @@ -302,6 +303,7 @@ function makeTests(Cache, name) { assert.equal(dead, missingMarker); cache.set('one', 'uno'); + cache.set('two', 'dos'); cache.set('three', 'tres'); @@ -488,6 +490,121 @@ function makeTests(Cache, name) { }); } + + describe('inspection', function() { + function makeExercisedCache(capacity) { + var cache = new Cache(capacity), ii; + cache.set(1, 'a'); cache.set(2, 'b'); cache.set('too old', 'c'); cache.set('oldest', 'd'); + for (ii = 0; ii < capacity - 3; ii++) { cache.set(ii * 2, ii * 2); } + cache.set(4, 'D'); cache.set(2, 'B'); cache.get(1); + cache.set(5, 'e'); cache.set(6, 'f'); + return cache; + } + + it('toString() states the name size and capacity', function () { + var cache = new Cache(null, null, 200); + cache.set(0, 'cero'); cache.set(1, 'uno'); + assert.deepStrictEqual(cache.toString(), `[object ${name}:2/200]`); + if (typeof Symbol !== 'undefined') { + assert.deepStrictEqual(cache[Symbol.toStringTag], `${name}:2/200`); + } + assert.deepStrictEqual(cache.summary, `${name}:2/200`); + cache.set(2, 'dos'); cache.set(3, 'tres'); + assert.deepStrictEqual(cache.toString(), `[object ${name}:4/200]`); + cache = makeExercisedCache(200); + assert.deepStrictEqual(cache.toString(), `[object ${name}:200/200]`); + }); + + if (typeof Symbol !== 'undefined') { + it('registers its inspect method for the console.log and friends to use', function () { + var cache = makeExercisedCache(7); + assert.deepStrictEqual(cache[Symbol.for('nodejs.util.inspect.custom')], cache.inspect); + }); + + it('attaches the summary getter to the magic [Symbol.toStringTag] property', function () { + var cache = makeExercisedCache(7); + assert.deepStrictEqual(cache[Symbol.toStringTag], cache.summary); + }); + } + + it('accepts limits on what inspect returns', function () { + var cache = new Cache(15), inspectedItems; + // empty + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, []); + // + cache.set(1, 'a'); + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, [[1, 'a']]); + // + cache.set(2, 'b'); + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, [[2, 'b'], [1, 'a']]); + // + cache.set(3, 'c'); + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, [[3, 'c'], [2, 'b'], [1, 'a']]); + // + cache.set(4, 'd'); + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, [[4, 'd'], [3, 'c'], [2, 'b'], [1, 'a']]); + // + cache.set(5, 'e'); + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, [[5, 'e'], [4, 'd'], [3, 'c'], [2, 'b'], [1, 'a']]); + // + cache.set(6, 'f'); + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, [[6, 'f'], [5, 'e'], [4, 'd'], ['_...', 2], [1, 'a']]); + // + var ii; + for (ii = 0; ii < 20; ii++) { cache.set(ii * 2, ii * 2); } + inspectedItems = Array.from(cache.inspect({maxToDump: 5}).entries()); + assert.deepStrictEqual(inspectedItems, [[38, 38], [36, 36], [34, 34], ['_...', 11], [10, 10]]); + }); + + it('puts a reasonable limit on what the console will show (large)', function () { + var cache = makeExercisedCache(600); + var asSeenInConsole = NodeUtil.inspect(cache); + // we're trying not to depend on what a given version of node actually serializes + var itemsDumped = /6 => 'f',\s*5 => 'e',\s*1 => 'a',(.|\n)+1168 => 1168,\s*'_\.\.\.' => 581,\s*'oldest' => 'd'/; + assert.deepStrictEqual(itemsDumped.test(asSeenInConsole), true); + assert.deepStrictEqual(new RegExp(`${name}:\[600/600\]`).test(asSeenInConsole), true); + assert.deepStrictEqual(asSeenInConsole.length < 800, true); + }); + + it('puts a reasonable limit on what the console will show (small)', function () { + var cache = makeExercisedCache(7); + var asSeenInConsole = NodeUtil.inspect(cache); + var itemsDumped = /6 => 'f',\s*5 => 'e',\s*1 => 'a',\s*2 => 'B',\s*4 => 'D',\s*0 => 0,\s*'oldest' => 'd'/; + assert.deepStrictEqual(itemsDumped.test(asSeenInConsole), true); + assert.deepStrictEqual(new RegExp(`${name}:7/7`).test(asSeenInConsole), true); + }); + + it('listens to advice about maximum inspection depth', function () { + var cache = makeExercisedCache(7); + var asSeenInConsole = NodeUtil.inspect({foo: {bar: cache}}); + var itemsDumped = /6 => 'f',\s*5 => 'e',\s*1 => 'a',\s*2 => 'B',\s*4 => 'D',\s*0 => 0,\s*'oldest' => 'd'/; + assert.deepStrictEqual(itemsDumped.test(asSeenInConsole), true); + assert.deepStrictEqual(asSeenInConsole.length > 150, true); + // Cannot depend on this existing in older versions. + // asSeenInConsole = NodeUtil.inspect({foo: {bar: [cache]}}); + // deepStrictEqual(asSeenInConsole.length < 70, true); + // deepStrictEqual(new RegExp(`\\[ \\[object ${name}:7/7\\] \\]`).test(asSeenInConsole), true); + }); + + it('allows inspection of the raw item if "all" is given', function () { + var cache = makeExercisedCache(250); + var inspected = cache.inspect({maxToDump: 8, all: true}); + var kk; + for (kk of ['items', 'K', 'V', 'size', 'capacity']) { + assert.deepStrictEqual(inspected[kk] === cache[kk], true); + } + assert.deepStrictEqual(Object.keys(cache), Object.keys(inspected)); + }); + + }); + }); } diff --git a/utils/snip.js b/utils/snip.js new file mode 100644 index 00000000..776dfab8 --- /dev/null +++ b/utils/snip.js @@ -0,0 +1,25 @@ +// +function snipToLast(iterator, proxy, {maxToDump = 20, size = Infinity, last = []}) { + var step; + + var ii = 0; + while ((step = iterator.next(), !step.done)) { + if (ii >= maxToDump - 2) { + if (ii >= size - 1) { + proxy.set(step.value[0], step.value[1]); + } else if (ii === size - 2) { + proxy.set(step.value[0], step.value[1]); + proxy.set(last[0], last[1]); + } else { + proxy.set('_...', size - ii - 1); + proxy.set(last[0], last[1]); + } + break; + } + proxy.set(step.value[0], step.value[1]); + ii += 1; + } + return proxy; +} + +module.exports.snipToLast = snipToLast;