From cf8076ec49a345d4f9c4e6f68249537f5864273a Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 27 Jun 2024 16:24:29 -0700 Subject: [PATCH] set dispose reason to 'expire' when ttl expires Fix: https://github.com/isaacs/node-lru-cache/issues/330 --- src/index.ts | 48 ++++++++++++++++++++++++++++++++------------- test/dispose.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/index.ts b/src/index.ts index f0bc438..432cda7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -201,8 +201,23 @@ export namespace LRUCache { /** * The reason why an item was removed from the cache, passed * to the {@link Disposer} methods. - */ - export type DisposeReason = 'evict' | 'set' | 'delete' + * + * - `evict`: The item was evicted because it is the least recently used, + * and the cache is full. + * - `set`: A new value was set, overwriting the old value being disposed. + * - `delete`: The item was explicitly deleted, either by calling + * {@link LRUCache#delete}, {@link LRUCache#clear}, or + * {@link LRUCache#set} with an undefined value. + * - `expire`: The item was removed due to exceeding its TTL. + * - `fetch`: A {@link OptionsBase#fetchMethod} operation returned + * `undefined` or was aborted, causing the item to be deleted. + */ + export type DisposeReason = + | 'evict' + | 'set' + | 'delete' + | 'expire' + | 'fetch' /** * A method called upon item removal, passed as the * {@link OptionsBase.dispose} and/or @@ -1174,7 +1189,7 @@ export class LRUCache if (ttl !== 0 && this.ttlAutopurge) { const t = setTimeout(() => { if (this.#isStale(index)) { - this.delete(this.#keyList[index] as K) + this.#delete(this.#keyList[index] as K, 'expire') } }, ttl + 1) // unref() not supported on all platforms @@ -1566,7 +1581,7 @@ export class LRUCache let deleted = false for (const i of this.#rindexes({ allowStale: true })) { if (this.#isStale(i)) { - this.delete(this.#keyList[i] as K) + this.#delete(this.#keyList[i] as K, 'expire') deleted = true } } @@ -1692,7 +1707,7 @@ export class LRUCache status.maxEntrySizeExceeded = true } // have to delete, in case something is there already. - this.delete(k) + this.#delete(k, 'set') return this } let index = this.#size === 0 ? undefined : this.#keyMap.get(k) @@ -1944,7 +1959,7 @@ export class LRUCache if (bf.__staleWhileFetching) { this.#valList[index as Index] = bf.__staleWhileFetching } else { - this.delete(k) + this.#delete(k, 'fetch') } } else { if (options.status) options.status.fetchUpdated = true @@ -1975,7 +1990,7 @@ export class LRUCache // the stale value is not removed from the cache when the fetch fails. const del = !noDelete || bf.__staleWhileFetching === undefined if (del) { - this.delete(k) + this.#delete(k, 'fetch') } else if (!allowStaleAborted) { // still replace the *promise* with the stale value, // since we are done with the promise at this point. @@ -2256,7 +2271,7 @@ export class LRUCache // delete only if not an in-flight background fetch if (!fetching) { if (!noDeleteOnStaleGet) { - this.delete(k) + this.#delete(k, 'expire') } if (status && allowStale) status.returnedStale = true return allowStale ? value : undefined @@ -2324,13 +2339,17 @@ export class LRUCache * Returns true if the key was deleted, false otherwise. */ delete(k: K) { + return this.#delete(k, 'delete') + } + + #delete(k: K, reason: LRUCache.DisposeReason) { let deleted = false if (this.#size !== 0) { const index = this.#keyMap.get(k) if (index !== undefined) { deleted = true if (this.#size === 1) { - this.clear() + this.#clear(reason) } else { this.#removeItemSize(index) const v = this.#valList[index] @@ -2338,10 +2357,10 @@ export class LRUCache v.__abortController.abort(new Error('deleted')) } else if (this.#hasDispose || this.#hasDisposeAfter) { if (this.#hasDispose) { - this.#dispose?.(v as V, k, 'delete') + this.#dispose?.(v as V, k, reason) } if (this.#hasDisposeAfter) { - this.#disposed?.push([v as V, k, 'delete']) + this.#disposed?.push([v as V, k, reason]) } } this.#keyMap.delete(k) @@ -2376,6 +2395,9 @@ export class LRUCache * Clear the cache entirely, throwing away all values. */ clear() { + return this.#clear('delete') + } + #clear(reason: LRUCache.DisposeReason) { for (const index of this.#rindexes({ allowStale: true })) { const v = this.#valList[index] if (this.#isBackgroundFetch(v)) { @@ -2383,10 +2405,10 @@ export class LRUCache } else { const k = this.#keyList[index] if (this.#hasDispose) { - this.#dispose?.(v as V, k as K, 'delete') + this.#dispose?.(v as V, k as K, reason) } if (this.#hasDisposeAfter) { - this.#disposed?.push([v as V, k as K, 'delete']) + this.#disposed?.push([v as V, k as K, reason]) } } } diff --git a/test/dispose.ts b/test/dispose.ts index 15f79d2..ce48215 100644 --- a/test/dispose.ts +++ b/test/dispose.ts @@ -1,5 +1,7 @@ +import { Clock } from 'clock-mock' import t from 'tap' import { LRUCache as LRU } from '../dist/esm/index.js' +import { LRUCache } from '../src/index.js' t.test('disposal', t => { const disposed: any[] = [] @@ -205,3 +207,53 @@ t.test('disposeAfter', t => { t.end() }) + +t.test('expiration reflected in dispose reason', async t => { + const clock = new Clock() + t.teardown(clock.enter()) + clock.advance(1) + const disposes: [number, number, LRUCache.DisposeReason][] = [] + const c = new LRUCache({ + ttl: 100, + max: 5, + dispose: (v, k, r) => disposes.push([k, v, r]), + }) + c.set(1, 1) + c.set(2, 2, { ttl: 10 }) + c.set(3, 3) + c.set(4, 4) + c.set(5, 5) + t.strictSame(disposes, []) + c.set(6, 6) + t.strictSame(disposes, [[1, 1, 'evict']]) + c.delete(6) + c.delete(5) + c.delete(4) + // test when it's the last one, and when it's not, because we + // delete with cache.clear() when it's the only entry. + t.strictSame(disposes, [ + [1, 1, 'evict'], + [6, 6, 'delete'], + [5, 5, 'delete'], + [4, 4, 'delete'], + ]) + clock.advance(20) + t.equal(c.get(2), undefined) + t.strictSame(disposes, [ + [1, 1, 'evict'], + [6, 6, 'delete'], + [5, 5, 'delete'], + [4, 4, 'delete'], + [2, 2, 'expire'], + ]) + clock.advance(200) + t.equal(c.get(3), undefined) + t.strictSame(disposes, [ + [1, 1, 'evict'], + [6, 6, 'delete'], + [5, 5, 'delete'], + [4, 4, 'delete'], + [2, 2, 'expire'], + [3, 3, 'expire'], + ]) +})