Skip to content

Commit

Permalink
set dispose reason to 'expire' when ttl expires
Browse files Browse the repository at this point in the history
Fix: #330
  • Loading branch information
isaacs committed Jun 27, 2024
1 parent 01b4c0c commit cf8076e
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 13 deletions.
48 changes: 35 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1174,7 +1189,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
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
Expand Down Expand Up @@ -1566,7 +1581,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
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
}
}
Expand Down Expand Up @@ -1692,7 +1707,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
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)
Expand Down Expand Up @@ -1944,7 +1959,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
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
Expand Down Expand Up @@ -1975,7 +1990,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
// 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.
Expand Down Expand Up @@ -2256,7 +2271,7 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
// 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
Expand Down Expand Up @@ -2324,24 +2339,28 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
* 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]
if (this.#isBackgroundFetch(v)) {
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)
Expand Down Expand Up @@ -2376,17 +2395,20 @@ export class LRUCache<K extends {}, V extends {}, FC = unknown>
* 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)) {
v.__abortController.abort(new Error('deleted'))
} 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])
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions test/dispose.ts
Original file line number Diff line number Diff line change
@@ -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[] = []
Expand Down Expand Up @@ -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<number, number>({
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'],
])
})

0 comments on commit cf8076e

Please sign in to comment.