diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c331883..a7634e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,11 +10,11 @@ jobs: fail-fast: false matrix: node-version: - - 14 - - 12 + - 20 + - 18 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/.gitignore b/.gitignore index 748ccb6..a042fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules yarn.lock .nyc_output +coverage diff --git a/index.d.ts b/index.d.ts index a865f3d..1b19e62 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -export interface Options { +export type Options = { /** The maximum number of milliseconds an item should remain in the cache. @@ -22,14 +22,10 @@ export interface Options { Useful for side effects or for items like object URLs that need explicit cleanup (`revokeObjectURL`). */ onEviction?: (key: KeyType, value: ValueType) => void; -} - -export default class QuickLRU extends Map implements Iterable<[KeyType, ValueType]> { - /** - The stored item count. - */ - readonly size: number; +}; +// eslint-disable-next-line @typescript-eslint/naming-convention +export default class QuickLRU extends Map implements Iterable<[KeyType, ValueType]> { /** Simple ["Least Recently Used" (LRU) cache](https://en.m.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_.28LRU.29). @@ -101,6 +97,11 @@ export default class QuickLRU extends Map implements Iterabl */ resize(maxSize: number): void; + /** + The stored item count. + */ + get size(): number; + /** Iterable for all the keys. */ diff --git a/index.js b/index.js index 3a72c60..bb948de 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,11 @@ export default class QuickLRU extends Map { + #size = 0; + #cache = new Map(); + #oldCache = new Map(); + #maxSize; + #maxAge; + #onEviction; + constructor(options = {}) { super(); @@ -10,30 +17,30 @@ export default class QuickLRU extends Map { throw new TypeError('`maxAge` must be a number greater than 0'); } - // TODO: Use private class fields when ESLint supports them. - this.maxSize = options.maxSize; - this.maxAge = options.maxAge || Number.POSITIVE_INFINITY; - this.onEviction = options.onEviction; - this.cache = new Map(); - this.oldCache = new Map(); - this._size = 0; + this.#maxSize = options.maxSize; + this.#maxAge = options.maxAge || Number.POSITIVE_INFINITY; + this.#onEviction = options.onEviction; + } + + // For tests. + get __oldCache() { + return this.#oldCache; } - // TODO: Use private class methods when targeting Node.js 16. - _emitEvictions(cache) { - if (typeof this.onEviction !== 'function') { + #emitEvictions(cache) { + if (typeof this.#onEviction !== 'function') { return; } for (const [key, item] of cache) { - this.onEviction(key, item.value); + this.#onEviction(key, item.value); } } - _deleteIfExpired(key, item) { + #deleteIfExpired(key, item) { if (typeof item.expiry === 'number' && item.expiry <= Date.now()) { - if (typeof this.onEviction === 'function') { - this.onEviction(key, item.value); + if (typeof this.#onEviction === 'function') { + this.#onEviction(key, item.value); } return this.delete(key); @@ -42,54 +49,54 @@ export default class QuickLRU extends Map { return false; } - _getOrDeleteIfExpired(key, item) { - const deleted = this._deleteIfExpired(key, item); + #getOrDeleteIfExpired(key, item) { + const deleted = this.#deleteIfExpired(key, item); if (deleted === false) { return item.value; } } - _getItemValue(key, item) { - return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value; + #getItemValue(key, item) { + return item.expiry ? this.#getOrDeleteIfExpired(key, item) : item.value; } - _peek(key, cache) { + #peek(key, cache) { const item = cache.get(key); - return this._getItemValue(key, item); + return this.#getItemValue(key, item); } - _set(key, value) { - this.cache.set(key, value); - this._size++; + #set(key, value) { + this.#cache.set(key, value); + this.#size++; - if (this._size >= this.maxSize) { - this._size = 0; - this._emitEvictions(this.oldCache); - this.oldCache = this.cache; - this.cache = new Map(); + if (this.#size >= this.#maxSize) { + this.#size = 0; + this.#emitEvictions(this.#oldCache); + this.#oldCache = this.#cache; + this.#cache = new Map(); } } - _moveToRecent(key, item) { - this.oldCache.delete(key); - this._set(key, item); + #moveToRecent(key, item) { + this.#oldCache.delete(key); + this.#set(key, item); } - * _entriesAscending() { - for (const item of this.oldCache) { + * #entriesAscending() { + for (const item of this.#oldCache) { const [key, value] = item; - if (!this.cache.has(key)) { - const deleted = this._deleteIfExpired(key, value); + if (!this.#cache.has(key)) { + const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield item; } } } - for (const item of this.cache) { + for (const item of this.#cache) { const [key, value] = item; - const deleted = this._deleteIfExpired(key, value); + const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield item; } @@ -97,73 +104,72 @@ export default class QuickLRU extends Map { } get(key) { - if (this.cache.has(key)) { - const item = this.cache.get(key); - - return this._getItemValue(key, item); + if (this.#cache.has(key)) { + const item = this.#cache.get(key); + return this.#getItemValue(key, item); } - if (this.oldCache.has(key)) { - const item = this.oldCache.get(key); - if (this._deleteIfExpired(key, item) === false) { - this._moveToRecent(key, item); + if (this.#oldCache.has(key)) { + const item = this.#oldCache.get(key); + if (this.#deleteIfExpired(key, item) === false) { + this.#moveToRecent(key, item); return item.value; } } } - set(key, value, {maxAge = this.maxAge} = {}) { - const expiry = - typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY ? - Date.now() + maxAge : - undefined; - if (this.cache.has(key)) { - this.cache.set(key, { + set(key, value, {maxAge = this.#maxAge} = {}) { + const expiry = typeof maxAge === 'number' && maxAge !== Number.POSITIVE_INFINITY + ? (Date.now() + maxAge) + : undefined; + + if (this.#cache.has(key)) { + this.#cache.set(key, { value, - expiry + expiry, }); } else { - this._set(key, {value, expiry}); + this.#set(key, {value, expiry}); } return this; } has(key) { - if (this.cache.has(key)) { - return !this._deleteIfExpired(key, this.cache.get(key)); + if (this.#cache.has(key)) { + return !this.#deleteIfExpired(key, this.#cache.get(key)); } - if (this.oldCache.has(key)) { - return !this._deleteIfExpired(key, this.oldCache.get(key)); + if (this.#oldCache.has(key)) { + return !this.#deleteIfExpired(key, this.#oldCache.get(key)); } return false; } peek(key) { - if (this.cache.has(key)) { - return this._peek(key, this.cache); + if (this.#cache.has(key)) { + return this.#peek(key, this.#cache); } - if (this.oldCache.has(key)) { - return this._peek(key, this.oldCache); + if (this.#oldCache.has(key)) { + return this.#peek(key, this.#oldCache); } } delete(key) { - const deleted = this.cache.delete(key); + const deleted = this.#cache.delete(key); if (deleted) { - this._size--; + this.#size--; } - return this.oldCache.delete(key) || deleted; + return this.#oldCache.delete(key) || deleted; } clear() { - this.cache.clear(); - this.oldCache.clear(); - this._size = 0; + this.#cache.clear(); + this.#oldCache.clear(); + this.#size = 0; } resize(newSize) { @@ -171,23 +177,23 @@ export default class QuickLRU extends Map { throw new TypeError('`maxSize` must be a number greater than 0'); } - const items = [...this._entriesAscending()]; + 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; + this.#cache = new Map(items); + this.#oldCache = new Map(); + this.#size = items.length; } else { if (removeCount > 0) { - this._emitEvictions(items.slice(0, removeCount)); + this.#emitEvictions(items.slice(0, removeCount)); } - this.oldCache = new Map(items.slice(removeCount)); - this.cache = new Map(); - this._size = 0; + this.#oldCache = new Map(items.slice(removeCount)); + this.#cache = new Map(); + this.#size = 0; } - this.maxSize = newSize; + this.#maxSize = newSize; } * keys() { @@ -203,18 +209,18 @@ export default class QuickLRU extends Map { } * [Symbol.iterator]() { - for (const item of this.cache) { + for (const item of this.#cache) { const [key, value] = item; - const deleted = this._deleteIfExpired(key, value); + const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } } - for (const item of this.oldCache) { + for (const item of this.#oldCache) { const [key, value] = item; - if (!this.cache.has(key)) { - const deleted = this._deleteIfExpired(key, value); + if (!this.#cache.has(key)) { + const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } @@ -223,22 +229,22 @@ export default class QuickLRU extends Map { } * entriesDescending() { - let items = [...this.cache]; + let items = [...this.#cache]; for (let i = items.length - 1; i >= 0; --i) { const item = items[i]; const [key, value] = item; - const deleted = this._deleteIfExpired(key, value); + const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } } - items = [...this.oldCache]; + items = [...this.#oldCache]; for (let i = items.length - 1; i >= 0; --i) { const item = items[i]; const [key, value] = item; - if (!this.cache.has(key)) { - const deleted = this._deleteIfExpired(key, value); + if (!this.#cache.has(key)) { + const deleted = this.#deleteIfExpired(key, value); if (deleted === false) { yield [key, value.value]; } @@ -247,24 +253,24 @@ export default class QuickLRU extends Map { } * entriesAscending() { - for (const [key, value] of this._entriesAscending()) { + for (const [key, value] of this.#entriesAscending()) { yield [key, value.value]; } } get size() { - if (!this._size) { - return this.oldCache.size; + if (!this.#size) { + return this.#oldCache.size; } let oldCacheSize = 0; - for (const key of this.oldCache.keys()) { - if (!this.cache.has(key)) { + for (const key of this.#oldCache.keys()) { + if (!this.#cache.has(key)) { oldCacheSize++; } } - return Math.min(this._size + oldCacheSize, this.maxSize); + return Math.min(this.#size + oldCacheSize, this.#maxSize); } entries() { diff --git a/package.json b/package.json index a18b727..0c69298 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,15 @@ "url": "https://sindresorhus.com" }, "type": "module", - "exports": "./index.js", + "exports": { + "types": "./index.d.ts", + "default": "./index.js" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "scripts": { - "//test": "xo && nyc ava && tsd", - "test": "xo && ava" + "test": "xo && nyc ava && tsd" }, "files": [ "index.js", @@ -37,10 +39,10 @@ "buffer" ], "devDependencies": { - "ava": "^3.15.0", + "ava": "^5.3.1", "nyc": "^15.1.0", - "tsd": "^0.14.0", - "xo": "^0.37.1" + "tsd": "^0.29.0", + "xo": "^0.56.0" }, "nyc": { "reporter": [ diff --git a/readme.md b/readme.md index d4780c8..2ba1aea 100644 --- a/readme.md +++ b/readme.md @@ -8,8 +8,8 @@ Inspired by the [`hashlru` algorithm](https://github.com/dominictarr/hashlru#alg ## Install -``` -$ npm install quick-lru +```sh +npm install quick-lru ``` ## Usage @@ -136,22 +136,10 @@ Loop over entries calling the `callbackFunction` for each entry (ascending in re **This method exists for `Map` compatibility. Prefer [.entriesAscending()](#entriesascending) instead.** -#### .size +#### .size *(getter)* The stored item count. ## Related - [yocto-queue](https://github.com/sindresorhus/yocto-queue) - Tiny queue data structure - ---- - -
- - Get professional support for this package with a Tidelift subscription - -
- - Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. -
-
diff --git a/test.js b/test.js index 3654580..c475b82 100644 --- a/test.js +++ b/test.js @@ -1,3 +1,4 @@ +import {setTimeout as delay} from 'node:timers/promises'; import test from 'ava'; import QuickLRU from './index.js'; @@ -9,12 +10,6 @@ const lruWithDuplicates = () => { return lru; }; -// TODO: Use `import {setTimeout as delay} from 'timers/promises';` when targeting Node.js 16. -const delay = ms => - new Promise(resolve => { - setTimeout(resolve, ms); - }); - test('main', t => { t.throws(() => { new QuickLRU(); // eslint-disable-line no-new @@ -194,7 +189,7 @@ test('checks total cache size does not exceed `maxSize`', t => { lru.set('1', 1); lru.set('2', 2); lru.get('1'); - t.is(lru.oldCache.has('1'), false); + t.is(lru.__oldCache.has('1'), false); }); test('`onEviction` option method is called after `maxSize` is exceeded', t => { @@ -324,7 +319,7 @@ test('max age - once an item expires, the eviction function should be called', a const lru = new QuickLRU({ maxSize: 2, maxAge: 100, - onEviction + onEviction, }); lru.set(expectKey, expectValue); @@ -354,7 +349,7 @@ test('max age - once an non-recent item expires, the eviction function should be const lru = new QuickLRU({ maxSize: 2, maxAge: 100, - onEviction + onEviction, }); lru.set('1', 'test'); @@ -388,7 +383,7 @@ test('max age - on resize, max aged items should also be evicted', async t => { const lru = new QuickLRU({ maxSize: 3, maxAge: 100, - onEviction + onEviction, }); lru.set('1', 'test'); @@ -600,9 +595,9 @@ test('max age - `forEach()` should not return expired entries', async t => { lru.set('5', 'loco'); const entries = []; - lru.forEach((value, key) => { + for (const [key, value] of lru.entries()) { entries.push([key, value]); - }); + } t.deepEqual(entries, [['4', 'coco'], ['5', 'loco']]); }); @@ -666,9 +661,9 @@ test('forEach calls the cb function for each cache item oldest-first', t => { lru.set('2', 8); const entries = []; - lru.forEach((value, key) => { + for (const [key, value] of lru.entries()) { entries.push([key, value]); - }); + } t.deepEqual(entries, [['1', 1], ['3', 7], ['2', 8]]); });