Skip to content

Commit

Permalink
Add .resize() method (#24)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
skeggse and sindresorhus committed Nov 25, 2020
1 parent 74ff03d commit debb8de
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 9 deletions.
17 changes: 17 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ declare class QuickLRU<KeyType, ValueType>
*/
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.
*/
Expand All @@ -92,6 +99,16 @@ declare class QuickLRU<KeyType, ValueType>
Iterable for all the values.
*/
values(): IterableIterator<ValueType>;

/**
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;
71 changes: 62 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down
14 changes: 14 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
77 changes: 77 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});

0 comments on commit debb8de

Please sign in to comment.