From debb8de939f1ad8b0501e1e256bf36134ee6bfcb Mon Sep 17 00:00:00 2001 From: Eli Skeggs Date: Wed, 25 Nov 2020 09:55:02 -0800 Subject: [PATCH] Add `.resize()` method (#24) Co-authored-by: Sindre Sorhus --- index.d.ts | 17 ++++++++++++ index.js | 71 ++++++++++++++++++++++++++++++++++++++++++------- readme.md | 14 ++++++++++ test.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index 06e070a..14b87cb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -83,6 +83,13 @@ declare class QuickLRU */ clear(): void; + /** + Update the `maxSize` in-place, discarding items as necessary. Insertion order is mostly preserved, though this is not a strong guarantee. + + Useful for on-the-fly tuning of cache sizes in live systems. + */ + resize(maxSize: number): void; + /** Iterable for all the keys. */ @@ -92,6 +99,16 @@ declare class QuickLRU Iterable for all the values. */ values(): IterableIterator; + + /** + Iterable for all entries, starting with the oldest (ascending in recency). + */ + entriesAscending(): IterableIterator<[KeyType, ValueType]>; + + /** + Iterable for all entries, starting with the newest (descending in recency). + */ + entriesDescending(): IterableIterator<[KeyType, ValueType]>; } export = QuickLRU; diff --git a/index.js b/index.js index 86075a0..d8e0eff 100644 --- a/index.js +++ b/index.js @@ -13,19 +13,23 @@ class QuickLRU { this._size = 0; } + _emitEvictions(cache) { + if (typeof this.onEviction !== 'function') { + return; + } + + for (const [key, value] of cache) { + this.onEviction(key, value); + } + } + _set(key, value) { this.cache.set(key, value); this._size++; if (this._size >= this.maxSize) { this._size = 0; - - if (typeof this.onEviction === 'function') { - for (const [key, value] of this.oldCache.entries()) { - this.onEviction(key, value); - } - } - + this._emitEvictions(this.oldCache); this.oldCache = this.cache; this.cache = new Map(); } @@ -82,6 +86,30 @@ class QuickLRU { this.oldCache.clear(); this._size = 0; } + + resize(newSize) { + if (!(newSize && newSize > 0)) { + throw new TypeError('`maxSize` must be a number greater than 0'); + } + + const items = [...this.entriesAscending()]; + const removeCount = items.length - newSize; + if (removeCount < 0) { + this.cache = new Map(items); + this.oldCache = new Map(); + this._size = items.length; + } else { + if (removeCount > 0) { + this._emitEvictions(items.slice(0, removeCount)); + } + + this.oldCache = new Map(items.slice(removeCount)); + this.cache = new Map(); + this._size = 0; + } + + this.maxSize = newSize; + } * keys() { for (const [key] of this) { @@ -96,16 +124,41 @@ class QuickLRU { } * [Symbol.iterator]() { - for (const item of this.cache) { - yield item; + yield * this.cache; + + for (const item of this.oldCache) { + const [key] = item; + if (!this.cache.has(key)) { + yield item; + } + } + } + + * entriesDescending() { + let items = [...this.cache]; + for (let i = items.length - 1; i >= 0; --i) { + yield items[i]; } + items = [...this.oldCache]; + for (let i = items.length - 1; i >= 0; --i) { + const item = items[i]; + const [key] = item; + if (!this.cache.has(key)) { + yield item; + } + } + } + + * entriesAscending() { for (const item of this.oldCache) { const [key] = item; if (!this.cache.has(key)) { yield item; } } + + yield * this.cache; } get size() { diff --git a/readme.md b/readme.md index b91bf97..f636d49 100644 --- a/readme.md +++ b/readme.md @@ -86,6 +86,12 @@ Returns `true` if the item is removed or `false` if the item doesn't exist. Delete all items. +#### .resize(maxSize) + +Update the `maxSize`, discarding items as necessary. Insertion order is mostly preserved, though this is not a strong guarantee. + +Useful for on-the-fly tuning of cache sizes in live systems. + #### .keys() Iterable for all the keys. @@ -94,6 +100,14 @@ Iterable for all the keys. Iterable for all the values. +#### .entriesAscending() + +Iterable for all entries, starting with the oldest (ascending in recency). + +#### .entriesDescending() + +Iterable for all entries, starting with the newest (descending in recency). + #### .size The stored item count. diff --git a/test.js b/test.js index 654625a..f5d6acc 100644 --- a/test.js +++ b/test.js @@ -204,3 +204,80 @@ test('`onEviction` option method is called after `maxSize` is exceeded', t => { t.is(actualValue, expectValue); t.true(isCalled); }); + +test('entriesAscending enumerates cache items oldest-first', t => { + const lru = new QuickLRU({maxSize: 3}); + lru.set('1', 1); + lru.set('2', 2); + lru.set('3', 3); + lru.set('3', 7); + lru.set('2', 8); + t.deepEqual([...lru.entriesAscending()], [['1', 1], ['3', 7], ['2', 8]]); +}); + +test('entriesDescending enumerates cache items newest-first', t => { + const lru = new QuickLRU({maxSize: 3}); + lru.set('t', 1); + lru.set('q', 2); + lru.set('a', 8); + lru.set('t', 4); + lru.set('v', 3); + t.deepEqual([...lru.entriesDescending()], [['v', 3], ['t', 4], ['a', 8], ['q', 2]]); +}); + +test('resize removes older items', t => { + const lru = new QuickLRU({maxSize: 2}); + lru.set('1', 1); + lru.set('2', 2); + lru.set('3', 3); + lru.resize(1); + t.is(lru.peek('1'), undefined); + t.is(lru.peek('3'), 3); + lru.set('3', 4); + t.is(lru.peek('3'), 4); + lru.set('4', 5); + t.is(lru.peek('4'), 5); + t.is(lru.peek('2'), undefined); +}); + +test('resize omits evictions', t => { + const calls = []; + const onEviction = (...args) => calls.push(args); + const lru = new QuickLRU({maxSize: 2, onEviction}); + + lru.set('1', 1); + lru.set('2', 2); + lru.set('3', 3); + lru.resize(1); + t.true(calls.length >= 1); + t.true(calls.some(([key]) => key === '1')); +}); + +test('resize increases capacity', t => { + const lru = new QuickLRU({maxSize: 2}); + lru.set('1', 1); + lru.set('2', 2); + lru.resize(3); + lru.set('3', 3); + lru.set('4', 4); + lru.set('5', 5); + t.deepEqual([...lru.entriesAscending()], [['1', 1], ['2', 2], ['3', 3], ['4', 4], ['5', 5]]); +}); + +test('resize does not conflict with the same number of items', t => { + const lru = new QuickLRU({maxSize: 2}); + lru.set('1', 1); + lru.set('2', 2); + lru.set('3', 3); + lru.resize(3); + lru.set('4', 4); + lru.set('5', 5); + t.deepEqual([...lru.entriesAscending()], [['1', 1], ['2', 2], ['3', 3], ['4', 4], ['5', 5]]); +}); + +test('resize checks parameter bounds', t => { + const lru = new QuickLRU({maxSize: 2}); + t.throws(() => { + lru.resize(-1); + }, /maxSize/); +});