Skip to content

Commit cf8076e

Browse files
committed
set dispose reason to 'expire' when ttl expires
Fix: #330
1 parent 01b4c0c commit cf8076e

File tree

2 files changed

+87
-13
lines changed

2 files changed

+87
-13
lines changed

src/index.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,23 @@ export namespace LRUCache {
201201
/**
202202
* The reason why an item was removed from the cache, passed
203203
* to the {@link Disposer} methods.
204-
*/
205-
export type DisposeReason = 'evict' | 'set' | 'delete'
204+
*
205+
* - `evict`: The item was evicted because it is the least recently used,
206+
* and the cache is full.
207+
* - `set`: A new value was set, overwriting the old value being disposed.
208+
* - `delete`: The item was explicitly deleted, either by calling
209+
* {@link LRUCache#delete}, {@link LRUCache#clear}, or
210+
* {@link LRUCache#set} with an undefined value.
211+
* - `expire`: The item was removed due to exceeding its TTL.
212+
* - `fetch`: A {@link OptionsBase#fetchMethod} operation returned
213+
* `undefined` or was aborted, causing the item to be deleted.
214+
*/
215+
export type DisposeReason =
216+
| 'evict'
217+
| 'set'
218+
| 'delete'
219+
| 'expire'
220+
| 'fetch'
206221
/**
207222
* A method called upon item removal, passed as the
208223
* {@link OptionsBase.dispose} and/or
@@ -1174,7 +1189,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
11741189
if (ttl !== 0 && this.ttlAutopurge) {
11751190
const t = setTimeout(() => {
11761191
if (this.#isStale(index)) {
1177-
this.delete(this.#keyList[index] as K)
1192+
this.#delete(this.#keyList[index] as K, 'expire')
11781193
}
11791194
}, ttl + 1)
11801195
// unref() not supported on all platforms
@@ -1566,7 +1581,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
15661581
let deleted = false
15671582
for (const i of this.#rindexes({ allowStale: true })) {
15681583
if (this.#isStale(i)) {
1569-
this.delete(this.#keyList[i] as K)
1584+
this.#delete(this.#keyList[i] as K, 'expire')
15701585
deleted = true
15711586
}
15721587
}
@@ -1692,7 +1707,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
16921707
status.maxEntrySizeExceeded = true
16931708
}
16941709
// have to delete, in case something is there already.
1695-
this.delete(k)
1710+
this.#delete(k, 'set')
16961711
return this
16971712
}
16981713
let index = this.#size === 0 ? undefined : this.#keyMap.get(k)
@@ -1944,7 +1959,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
19441959
if (bf.__staleWhileFetching) {
19451960
this.#valList[index as Index] = bf.__staleWhileFetching
19461961
} else {
1947-
this.delete(k)
1962+
this.#delete(k, 'fetch')
19481963
}
19491964
} else {
19501965
if (options.status) options.status.fetchUpdated = true
@@ -1975,7 +1990,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
19751990
// the stale value is not removed from the cache when the fetch fails.
19761991
const del = !noDelete || bf.__staleWhileFetching === undefined
19771992
if (del) {
1978-
this.delete(k)
1993+
this.#delete(k, 'fetch')
19791994
} else if (!allowStaleAborted) {
19801995
// still replace the *promise* with the stale value,
19811996
// since we are done with the promise at this point.
@@ -2256,7 +2271,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
22562271
// delete only if not an in-flight background fetch
22572272
if (!fetching) {
22582273
if (!noDeleteOnStaleGet) {
2259-
this.delete(k)
2274+
this.#delete(k, 'expire')
22602275
}
22612276
if (status && allowStale) status.returnedStale = true
22622277
return allowStale ? value : undefined
@@ -2324,24 +2339,28 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
23242339
* Returns true if the key was deleted, false otherwise.
23252340
*/
23262341
delete(k: K) {
2342+
return this.#delete(k, 'delete')
2343+
}
2344+
2345+
#delete(k: K, reason: LRUCache.DisposeReason) {
23272346
let deleted = false
23282347
if (this.#size !== 0) {
23292348
const index = this.#keyMap.get(k)
23302349
if (index !== undefined) {
23312350
deleted = true
23322351
if (this.#size === 1) {
2333-
this.clear()
2352+
this.#clear(reason)
23342353
} else {
23352354
this.#removeItemSize(index)
23362355
const v = this.#valList[index]
23372356
if (this.#isBackgroundFetch(v)) {
23382357
v.__abortController.abort(new Error('deleted'))
23392358
} else if (this.#hasDispose || this.#hasDisposeAfter) {
23402359
if (this.#hasDispose) {
2341-
this.#dispose?.(v as V, k, 'delete')
2360+
this.#dispose?.(v as V, k, reason)
23422361
}
23432362
if (this.#hasDisposeAfter) {
2344-
this.#disposed?.push([v as V, k, 'delete'])
2363+
this.#disposed?.push([v as V, k, reason])
23452364
}
23462365
}
23472366
this.#keyMap.delete(k)
@@ -2376,17 +2395,20 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
23762395
* Clear the cache entirely, throwing away all values.
23772396
*/
23782397
clear() {
2398+
return this.#clear('delete')
2399+
}
2400+
#clear(reason: LRUCache.DisposeReason) {
23792401
for (const index of this.#rindexes({ allowStale: true })) {
23802402
const v = this.#valList[index]
23812403
if (this.#isBackgroundFetch(v)) {
23822404
v.__abortController.abort(new Error('deleted'))
23832405
} else {
23842406
const k = this.#keyList[index]
23852407
if (this.#hasDispose) {
2386-
this.#dispose?.(v as V, k as K, 'delete')
2408+
this.#dispose?.(v as V, k as K, reason)
23872409
}
23882410
if (this.#hasDisposeAfter) {
2389-
this.#disposed?.push([v as V, k as K, 'delete'])
2411+
this.#disposed?.push([v as V, k as K, reason])
23902412
}
23912413
}
23922414
}

test/dispose.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { Clock } from 'clock-mock'
12
import t from 'tap'
23
import { LRUCache as LRU } from '../dist/esm/index.js'
4+
import { LRUCache } from '../src/index.js'
35

46
t.test('disposal', t => {
57
const disposed: any[] = []
@@ -205,3 +207,53 @@ t.test('disposeAfter', t => {
205207

206208
t.end()
207209
})
210+
211+
t.test('expiration reflected in dispose reason', async t => {
212+
const clock = new Clock()
213+
t.teardown(clock.enter())
214+
clock.advance(1)
215+
const disposes: [number, number, LRUCache.DisposeReason][] = []
216+
const c = new LRUCache<number, number>({
217+
ttl: 100,
218+
max: 5,
219+
dispose: (v, k, r) => disposes.push([k, v, r]),
220+
})
221+
c.set(1, 1)
222+
c.set(2, 2, { ttl: 10 })
223+
c.set(3, 3)
224+
c.set(4, 4)
225+
c.set(5, 5)
226+
t.strictSame(disposes, [])
227+
c.set(6, 6)
228+
t.strictSame(disposes, [[1, 1, 'evict']])
229+
c.delete(6)
230+
c.delete(5)
231+
c.delete(4)
232+
// test when it's the last one, and when it's not, because we
233+
// delete with cache.clear() when it's the only entry.
234+
t.strictSame(disposes, [
235+
[1, 1, 'evict'],
236+
[6, 6, 'delete'],
237+
[5, 5, 'delete'],
238+
[4, 4, 'delete'],
239+
])
240+
clock.advance(20)
241+
t.equal(c.get(2), undefined)
242+
t.strictSame(disposes, [
243+
[1, 1, 'evict'],
244+
[6, 6, 'delete'],
245+
[5, 5, 'delete'],
246+
[4, 4, 'delete'],
247+
[2, 2, 'expire'],
248+
])
249+
clock.advance(200)
250+
t.equal(c.get(3), undefined)
251+
t.strictSame(disposes, [
252+
[1, 1, 'evict'],
253+
[6, 6, 'delete'],
254+
[5, 5, 'delete'],
255+
[4, 4, 'delete'],
256+
[2, 2, 'expire'],
257+
[3, 3, 'expire'],
258+
])
259+
})

0 commit comments

Comments
 (0)