diff --git a/README.md b/README.md index dab03f5..a571c6e 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ const cache = new LRU(100, 5000); | Method | Description | | --------------------------- | ---------------------------------------------- | -| `cleanup()` | Remove expired items without LRU update. Returns count of removed items. | +| `cleanup(fireOnEvict?)` | Remove expired items without LRU update. Returns count of removed items. Pass `true` to fire `onEvict` for each removed item. | | `clear()` | Remove all items. Returns `this` for chaining. | | `delete(key)` | Remove an item by key. Returns `this` for chaining. | | `entries(keys?)` | Get `[key, value]` pairs. Without keys: LRU order. With keys: input array order. | @@ -172,7 +172,7 @@ const cache = new LRU(100, 5000); | `expiresAt(key)` | Get expiration timestamp for a key. Returns `number | undefined`. | | `forEach(callback, thisArg?)` | Iterate over items in LRU order. Returns `this` for chaining. | | `get(key)` | Retrieve a value. Moves item to most recent. Returns value or `undefined`. | -| `getMany(keys)` | Batch retrieve multiple items. Returns object mapping keys to values (missing/expired keys omitted). | +| `getMany(keys)` | Batch retrieve multiple items. Returns object mapping keys to values (undefined for missing/expired keys). | | `has(key)` | Check if key exists and is not expired. Returns `boolean`. | | `hasAll(keys)` | Check if ALL keys exist. Returns `boolean`. | | `hasAny(keys)` | Check if ANY key exists. Returns `boolean`. | diff --git a/coverage.txt b/coverage.txt index e37deb0..78f784b 100644 --- a/coverage.txt +++ b/coverage.txt @@ -3,8 +3,8 @@ ℹ file | line % | branch % | funcs % | uncovered lines ℹ ---------------------------------------------------------- ℹ src | | | | -ℹ lru.js | 100.00 | 99.39 | 100.00 | +ℹ lru.js | 100.00 | 99.40 | 100.00 | ℹ ---------------------------------------------------------- -ℹ all files | 100.00 | 99.39 | 100.00 | +ℹ all files | 100.00 | 99.40 | 100.00 | ℹ ---------------------------------------------------------- ℹ end of coverage report diff --git a/docs/API.md b/docs/API.md index c233550..8e39237 100644 --- a/docs/API.md +++ b/docs/API.md @@ -163,15 +163,24 @@ console.log(cache.ttl); // 60000 ### `cleanup()` -Removes expired items without affecting LRU order. Silently removes expired items without triggering the `onEvict()` callback. +Removes expired items without affecting LRU order. By default it silently removes expired items without triggering the `onEvict()` callback. Pass `true` to fire `onEvict()` for each removed item. ```javascript cache.set("a", 1).set("b", 2); // ... wait for items to expire const removed = cache.cleanup(); console.log(removed); // 2 (number of items removed) + +// Fire onEvict for each purged item +const removedWithCallback = cache.cleanup(true); ``` +**Parameters:** + +| Name | Type | Default | Description | +| -------------- | --------- | ------- | ------------------------------------------------ | +| `fireOnEvict` | `boolean` | `false` | When `true`, invokes `onEvict()` for each removed item. | + **Returns:** `number` - Number of expired items removed **Note:** Only removes items when TTL is enabled (`ttl > 0`). Does not support method chaining (returns `number`). diff --git a/src/lru.js b/src/lru.js index 8b03036..d4c8a73 100644 --- a/src/lru.js +++ b/src/lru.js @@ -312,17 +312,16 @@ export class LRU { /** * Returns an array of all keys in the cache, ordered from least to most recently used. + * Expired items are skipped, consistent with entries(), values(), and toJSON(). * * @returns {string[]} Array of keys in LRU order. */ keys() { - const result = Array.from({ length: this.size }); - let x = this.first; - let i = 0; - - while (x !== null) { - result[i++] = x.key; - x = x.next; + const result = []; + for (let x = this.first; x !== null; x = x.next) { + if (!this.#isExpired(x)) { + result.push(x.key); + } } return result; @@ -548,9 +547,10 @@ export class LRU { * Remove expired items without affecting LRU order. * Unlike get(), this does not move items to the end. * + * @param {boolean} [fireOnEvict=false] - When true, invokes the onEvict callback for each removed item. * @returns {number} Number of expired items removed. */ - cleanup() { + cleanup(fireOnEvict = false) { if (this.ttl === 0 || this.size === 0) { return 0; } @@ -568,6 +568,13 @@ export class LRU { this.#unlink(x); x.prev = null; x.next = null; + if (fireOnEvict && this.#onEvict !== null) { + this.#onEvict({ + key: x.key, + value: x.value, + expiry: x.expiry, + }); + } } } x = next; diff --git a/tests/unit/lru.test.js b/tests/unit/lru.test.js index a15cb67..9113aaa 100644 --- a/tests/unit/lru.test.js +++ b/tests/unit/lru.test.js @@ -1063,6 +1063,36 @@ describe("LRU Cache", function () { const removed = cache.cleanup(); assert.equal(removed, 0); }); + + it("should fire onEvict for each removed item when fireOnEvict is true", async function () { + cache.set("a", 1); + cache.set("b", 2); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + const evicted = []; + cache.onEvict((item) => evicted.push(item.key)); + + const removed = cache.cleanup(true); + assert.equal(removed, 2); + assert.equal(cache.size, 0); + assert.deepEqual(evicted, ["a", "b"]); + }); + + it("should not fire onEvict when fireOnEvict is false (default)", async function () { + cache.set("a", 1); + cache.set("b", 2); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + const evicted = []; + cache.onEvict((item) => evicted.push(item.key)); + + const removed = cache.cleanup(); + assert.equal(removed, 2); + assert.equal(cache.size, 0); + assert.deepEqual(evicted, []); + }); }); describe("toJSON method", function () { @@ -1661,6 +1691,14 @@ describe("LRU Cache", function () { assert.deepEqual(cache.values(), []); }); + it("should skip expired items in keys()", async function () { + const cache = new LRU(5, 50, false); + cache.set("a", 1); + cache.set("b", 2); + await new Promise((resolve) => setTimeout(resolve, 80)); + assert.deepEqual(cache.keys(), []); + }); + it("should skip expired items in entries()", async function () { const cache = new LRU(5, 50, false); cache.set("a", 1); diff --git a/types/lru.d.ts b/types/lru.d.ts index 919d738..7da1e04 100644 --- a/types/lru.d.ts +++ b/types/lru.d.ts @@ -153,9 +153,10 @@ export class LRU { /** * Remove expired items without affecting LRU order. * Unlike get(), this does not move items to the end. + * @param fireOnEvict When true, invokes the onEvict callback for each removed item * @returns Number of expired items removed */ - cleanup(): number; + cleanup(fireOnEvict?: boolean): number; /** * Serialize cache to JSON-compatible format.