diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c546fbd..c331883 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,6 @@ jobs: node-version: - 14 - 12 - - 10 steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 @@ -20,7 +19,8 @@ jobs: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 - if: matrix.node-version == 14 - with: - fail_ci_if_error: true + # TODO: Disabled until `nyc` supports ESM. + # - uses: codecov/codecov-action@v1 + # if: matrix.node-version == 14 + # with: + # fail_ci_if_error: true diff --git a/index.d.ts b/index.d.ts index 5da0a73..ca4455a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,32 +1,30 @@ -declare namespace QuickLRU { - interface Options { - /** - The maximum number of milliseconds an item should remain in the cache. +export interface Options { + /** + The maximum number of milliseconds an item should remain in the cache. - @default Infinity + @default Infinity - By default, `maxAge` will be `Infinity`, which means that items will never expire. - Lazy expiration upon the next write or read call. + By default, `maxAge` will be `Infinity`, which means that items will never expire. + Lazy expiration upon the next write or read call. - Individual expiration of an item can be specified by the `set(key, value, maxAge)` method. - */ - readonly maxAge?: number; + Individual expiration of an item can be specified by the `set(key, value, maxAge)` method. + */ + readonly maxAge?: number; - /** - The maximum number of items before evicting the least recently used items. - */ - readonly maxSize: number; + /** + The maximum number of items before evicting the least recently used items. + */ + readonly maxSize: number; - /** - Called right before an item is evicted from the cache. + /** + Called right before an item is evicted from the cache. - Useful for side effects or for items like object URLs that need explicit cleanup (`revokeObjectURL`). - */ - onEviction?: (key: KeyType, value: ValueType) => void; - } + Useful for side effects or for items like object URLs that need explicit cleanup (`revokeObjectURL`). + */ + onEviction?: (key: KeyType, value: ValueType) => void; } -declare class QuickLRU implements Iterable<[KeyType, ValueType]> { +export default class QuickLRU implements Iterable<[KeyType, ValueType]> { /** The stored item count. */ @@ -39,7 +37,7 @@ declare class QuickLRU implements Iterable<[KeyType, ValueTy @example ``` - import QuickLRU = require('quick-lru'); + import QuickLRU from 'quick-lru'; const lru = new QuickLRU({maxSize: 1000}); @@ -52,7 +50,7 @@ declare class QuickLRU implements Iterable<[KeyType, ValueTy //=> '🌈' ``` */ - constructor(options: QuickLRU.Options); + constructor(options: Options); [Symbol.iterator](): IterableIterator<[KeyType, ValueType]>; @@ -123,5 +121,3 @@ declare class QuickLRU implements Iterable<[KeyType, ValueTy */ entriesDescending(): IterableIterator<[KeyType, ValueType]>; } - -export = QuickLRU; diff --git a/index.js b/index.js index bb4655b..a80b0ab 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,4 @@ -'use strict'; - -class QuickLRU { +export default class QuickLRU { constructor(options = {}) { if (!(options.maxSize && options.maxSize > 0)) { throw new TypeError('`maxSize` must be a number greater than 0'); @@ -10,14 +8,16 @@ class QuickLRU { 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 || Infinity; + this.maxAge = options.maxAge || Number.POSITIVE_INFINITY; this.onEviction = options.onEviction; this.cache = new Map(); this.oldCache = new Map(); this._size = 0; } + // TODO: Use private class methods when targeting Node.js 16. _emitEvictions(cache) { if (typeof this.onEviction !== 'function') { return; @@ -110,7 +110,7 @@ class QuickLRU { } } - set(key, value, {maxAge = this.maxAge === Infinity ? undefined : Date.now() + this.maxAge} = {}) { + set(key, value, {maxAge = this.maxAge === Number.POSITIVE_INFINITY ? undefined : Date.now() + this.maxAge} = {}) { if (this.cache.has(key)) { this.cache.set(key, { value, @@ -259,5 +259,3 @@ class QuickLRU { return Math.min(this._size + oldCacheSize, this.maxSize); } } - -module.exports = QuickLRU; diff --git a/index.test-d.ts b/index.test-d.ts index 18b494f..b3af718 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import QuickLRU = require('.'); +import QuickLRU from './index.js'; const lru = new QuickLRU({maxSize: 1000, maxAge: 200}); @@ -8,7 +8,6 @@ expectType(lru.get('🦄')); expectType(lru.has('🦄')); expectType(lru.peek('🦄')); expectType(lru.delete('🦄')); -expectType(lru.clear()); expectType(lru.size); for (const [key, value] of lru) { diff --git a/package.json b/package.json index 0bf6d21..b8e56ba 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,14 @@ "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, + "type": "module", + "exports": "./index.js", "engines": { - "node": ">=10" + "node": ">=12" }, "scripts": { - "test": "xo && nyc ava && tsd" + "//test": "xo && nyc ava && tsd", + "test": "xo && ava && tsd" }, "files": [ "index.js", @@ -34,10 +37,10 @@ "buffer" ], "devDependencies": { - "ava": "^2.4.0", + "ava": "^3.15.0", "nyc": "^15.1.0", - "tsd": "^0.13.1", - "xo": "^0.35.0" + "tsd": "^0.14.0", + "xo": "^0.37.1" }, "nyc": { "reporter": [ diff --git a/readme.md b/readme.md index 39388b7..305c799 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,7 @@ $ npm install quick-lru ## Usage ```js -const QuickLRU = require('quick-lru'); +import QuickLRU from 'quick-lru'; const lru = new QuickLRU({maxSize: 1000}); diff --git a/test.js b/test.js index 39bfc7a..1118d6d 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,5 @@ import test from 'ava'; -import QuickLRU from '.'; +import QuickLRU from './index.js'; const lruWithDuplicates = () => { const lru = new QuickLRU({maxSize: 2}); @@ -9,7 +9,8 @@ const lruWithDuplicates = () => { return lru; }; -const sleep = ms => +// TODO: Use `import {setTimeout as delay} from 'timers/promises';` when targeting Node.js 16. +const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); }); @@ -17,13 +18,13 @@ const sleep = ms => test('main', t => { t.throws(() => { new QuickLRU(); // eslint-disable-line no-new - }, /maxSize/); + }, {message: /maxSize/}); }); test('max age - incorrect value', t => { t.throws(() => { new QuickLRU({maxSize: 10, maxAge: 0}); // eslint-disable-line no-new - }, /maxAge/); + }, {message: /maxAge/}); }); test('.get() / .set()', t => { @@ -219,7 +220,7 @@ test('`onEviction` option method is called after `maxSize` is exceeded', t => { test('set(expiry) - an individual item could have custom expiration', async t => { const lru = new QuickLRU({maxSize: 10}); lru.set('1', 'test', {maxAge: Date.now() + 100}); - await sleep(200); + await delay(200); t.false(lru.has('1')); }); @@ -227,9 +228,9 @@ test('set(expiry) - items without expiration will never expired', async t => { const lru = new QuickLRU({maxSize: 10}); lru.set('1', 'test', {maxAge: Date.now() + 100}); lru.set('2', 'boo'); - await sleep(200); + await delay(200); t.false(lru.has('1')); - await sleep(200); + await delay(200); t.true(lru.has('2')); }); @@ -237,9 +238,9 @@ test('set(expiry) - not a number expires should not be take in account', async t const lru = new QuickLRU({maxSize: 10}); lru.set('1', 'test', 'string'); lru.set('2', 'boo'); - await sleep(200); + await delay(200); t.true(lru.has('1')); - await sleep(200); + await delay(200); t.true(lru.has('2')); }); @@ -247,16 +248,16 @@ test('set(expiry) - local expires prevails over the global maxAge', async t => { const lru = new QuickLRU({maxSize: 10, maxAge: 1000}); lru.set('1', 'test', {maxAge: Date.now() + 100}); lru.set('2', 'boo'); - await sleep(300); + await delay(300); t.false(lru.has('1')); - await sleep(200); + await delay(200); t.true(lru.has('2')); }); test('max age - should remove the item if has expired on call `get()` method upon the same key', async t => { const lru = new QuickLRU({maxSize: 10, maxAge: 90}); lru.set('1', 'test'); - await sleep(200); + await delay(200); t.is(lru.get('1'), undefined); }); @@ -265,16 +266,16 @@ test('max age - a non-recent item can also expire', async t => { lru.set('1', 'test1'); lru.set('2', 'test2'); lru.set('3', 'test4'); - await sleep(200); + await delay(200); t.is(lru.get('1'), undefined); }); test('max age - setting the item again should refresh the expiration time', async t => { const lru = new QuickLRU({maxSize: 2, maxAge: 100}); lru.set('1', 'test'); - await sleep(50); + await delay(50); lru.set('1', 'test2'); - await sleep(50); + await delay(50); t.is(lru.get('1'), 'test2'); }); @@ -282,9 +283,9 @@ test('max age - setting an item with a local expiration date', async t => { const lru = new QuickLRU({maxSize: 2, maxAge: 100}); lru.set('1', 'test'); lru.set('2', 'test2', {maxAge: Date.now() + 500}); - await sleep(200); + await delay(200); t.true(lru.has('2')); - await sleep(300); + await delay(300); t.false(lru.has('2')); }); @@ -292,7 +293,7 @@ test('max age - setting an item with a empty object as options parameter must us const lru = new QuickLRU({maxSize: 2, maxAge: 100}); lru.set('1', 'test'); lru.set('2', 'test2', {}); - await sleep(200); + await delay(200); t.false(lru.has('2')); }); @@ -318,7 +319,7 @@ test('max age - once an item expires, the eviction function should be called', a lru.set(expectKey, expectValue); - await sleep(200); + await delay(200); t.is(lru.get('1'), undefined); t.true(isCalled); @@ -352,7 +353,7 @@ test('max age - once an non-recent item expires, the eviction function should be lru.set('4', 'test4'); lru.set('5', 'test5'); - await sleep(200); + await delay(200); t.is(lru.get('1'), undefined); t.true(isCalled); @@ -388,7 +389,7 @@ test('max age - on resize, max aged items should also be evicted', async t => { lru.resize(2); - await sleep(200); + await delay(200); t.false(lru.has('1')); t.true(isCalled); @@ -399,14 +400,14 @@ test('max age - on resize, max aged items should also be evicted', async t => { test('max age - an item that is not expired can also be peek', async t => { const lru = new QuickLRU({maxSize: 10, maxAge: 400}); lru.set('1', 'test'); - await sleep(200); + await delay(200); t.is(lru.peek('1'), 'test'); }); test('max age - peeking the item should also remove the item if it has expired', async t => { const lru = new QuickLRU({maxSize: 10, maxAge: 100}); lru.set('1', 'test'); - await sleep(200); + await delay(200); t.is(lru.peek('1'), undefined); }); @@ -415,7 +416,7 @@ test('max age - peeking the item should also remove expired items that are not r lru.set('1', 'test'); lru.set('2', 'test'); lru.set('3', 'test'); - await sleep(200); + await delay(200); t.is(lru.peek('1'), undefined); }); @@ -424,7 +425,7 @@ test('max age - non-recent items that are not expired are also valid', async t = lru.set('1', 'test'); lru.set('2', 'test2'); lru.set('3', 'test4'); - await sleep(100); + await delay(100); t.is(lru.get('1'), 'test'); }); @@ -433,7 +434,7 @@ test('max age - has method should delete the item if expired and return false', lru.set('1', undefined); lru.set('2', 'test'); lru.set('3', 'test'); - await sleep(200); + await delay(200); t.false(lru.has('1')); }); @@ -458,7 +459,7 @@ test('max age - `.keys()` should return keys that are not expirated', async t => lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); lru.set('4', 'loco'); t.deepEqual([...lru.keys()].sort(), ['4']); @@ -469,7 +470,7 @@ test('max age - `.keys()` should return an empty list if all items has expired', lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); t.deepEqual([...lru.keys()].sort(), []); }); @@ -479,7 +480,7 @@ test('max age - `.values()` should return an empty if all items has expired', as lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); t.deepEqual([...lru.values()].sort(), []); }); @@ -489,7 +490,7 @@ test('max age - `.values()` should return the values that are not expired', asyn lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); lru.set('5', 'loco'); t.deepEqual([...lru.values()].sort(), ['loco']); @@ -500,7 +501,7 @@ test('max age - `entriesDescending()` should not return expired entries', async lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); lru.set('4', 'coco'); lru.set('5', 'loco'); @@ -512,7 +513,7 @@ test('max age - `entriesDescending()` should not return expired entries from old lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); lru.set('4', 'coco'); lru.set('5', 'loco'); @@ -524,7 +525,7 @@ test('max age - `entriesDescending()` should return all entries in desc order if lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); lru.set('4', 'coco'); lru.set('5', 'loco'); @@ -536,7 +537,7 @@ test('max age - `entriesAscending()` should not return expired entries', async t lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); lru.set('4', 'coco'); lru.set('5', 'loco'); @@ -548,7 +549,7 @@ test('max age - `entriesAscending() should not return expired entries even if ar lru.set('1', undefined); lru.set('2', 'test2'); lru.set('3', 'test3'); - await sleep(200); + await delay(200); lru.set('4', 'coco'); lru.set('5', 'loco'); @@ -559,7 +560,7 @@ test('max age - `entriesAscending()` should return the entries that are not expi const lru = new QuickLRU({maxSize: 10, maxAge: 100}); lru.set('1', undefined); lru.set('2', 'test2'); - await sleep(200); + await delay(200); lru.set('3', 'test3'); lru.set('4', 'coco'); lru.set('5', 'loco'); @@ -571,7 +572,7 @@ test('max age - `.[Symbol.iterator]()` should not return expired items', async t const lru = new QuickLRU({maxSize: 2, maxAge: 100}); lru.set('key', 'value'); lru.set('key3', 1); - await sleep(200); + await delay(200); lru.set('key4', 2); t.deepEqual([...lru].sort(), [['key4', 2]]); @@ -582,7 +583,7 @@ test('max age - `.[Symbol.iterator]()` should not return expired items that are lru.set('keyunique', 'value'); lru.set('key3unique', 1); lru.set('key4unique', 2); - await sleep(200); + await delay(200); t.deepEqual([...lru].sort(), []); }); @@ -631,7 +632,7 @@ test('resize omits evictions', t => { lru.set('2', 2); lru.set('3', 3); lru.resize(1); - t.true(calls.length >= 1); + t.true(calls.length > 0); t.true(calls.some(([key]) => key === '1')); }); @@ -661,5 +662,5 @@ test('resize checks parameter bounds', t => { const lru = new QuickLRU({maxSize: 2}); t.throws(() => { lru.resize(-1); - }, /maxSize/); + }, {message: /maxSize/}); });