Skip to content

Commit 4324698

Browse files
ztannerwyattjoh
andauthored
backport: implement LRU cache with invocation ID scoping for minimal mode response cache (#89122)
Backports: - #88509 Co-authored-by: Wyatt Johnson <accounts+github@wyattjoh.ca>
1 parent 23c4649 commit 4324698

File tree

11 files changed

+1507
-635
lines changed

11 files changed

+1507
-635
lines changed

packages/next/src/server/lib/lru-cache.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,75 @@ describe('LRUCache', () => {
226226
expect(cache.has('key149')).toBe(true) // recent keys retained
227227
})
228228
})
229+
230+
describe('onEvict Callback', () => {
231+
it('should call onEvict when an entry is evicted', () => {
232+
const evicted: Array<{ key: string; value: string }> = []
233+
const cache = new LRUCache<string>(2, undefined, (key, value) => {
234+
evicted.push({ key, value })
235+
})
236+
237+
cache.set('a', 'value-a')
238+
cache.set('b', 'value-b')
239+
expect(evicted.length).toBe(0)
240+
241+
cache.set('c', 'value-c') // should evict 'a'
242+
expect(evicted.length).toBe(1)
243+
expect(evicted[0]).toEqual({ key: 'a', value: 'value-a' })
244+
})
245+
246+
it('should not call onEvict when updating existing entry', () => {
247+
const evicted: string[] = []
248+
const cache = new LRUCache<string>(2, undefined, (key) => {
249+
evicted.push(key)
250+
})
251+
252+
cache.set('a', 'value-a')
253+
cache.set('a', 'new-value-a')
254+
expect(evicted.length).toBe(0)
255+
})
256+
257+
it('should call onEvict for each evicted entry when multiple are evicted', () => {
258+
const evicted: string[] = []
259+
const cache = new LRUCache<string>(
260+
10,
261+
(value) => value.length,
262+
(key) => {
263+
evicted.push(key)
264+
}
265+
)
266+
267+
cache.set('key1', 'ab') // size 2
268+
cache.set('key2', 'cd') // size 2
269+
cache.set('key3', 'ef') // size 2, total = 6
270+
cache.set('key4', 'ghijklmno') // size 9, should evict key1, key2, key3
271+
272+
expect(evicted).toEqual(['key1', 'key2', 'key3'])
273+
})
274+
275+
it('should work without onEvict callback', () => {
276+
const cache = new LRUCache<string>(2)
277+
cache.set('a', 'value-a')
278+
cache.set('b', 'value-b')
279+
cache.set('c', 'value-c') // should evict without error
280+
expect(cache.has('a')).toBe(false)
281+
})
282+
283+
it('should pass the evicted value to the callback', () => {
284+
const evicted: Array<{ id: number }> = []
285+
const cache = new LRUCache<{ id: number }>(
286+
1,
287+
undefined,
288+
(_key, value) => {
289+
evicted.push(value)
290+
}
291+
)
292+
293+
cache.set('obj1', { id: 1 })
294+
cache.set('obj2', { id: 2 }) // should evict obj1
295+
296+
expect(evicted.length).toBe(1)
297+
expect(evicted[0]).toEqual({ id: 1 })
298+
})
299+
})
229300
})

packages/next/src/server/lib/lru-cache.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,16 @@ export class LRUCache<T> {
5050
private totalSize: number = 0
5151
private readonly maxSize: number
5252
private readonly calculateSize: ((value: T) => number) | undefined
53+
private readonly onEvict: ((key: string, value: T) => void) | undefined
5354

54-
constructor(maxSize: number, calculateSize?: (value: T) => number) {
55+
constructor(
56+
maxSize: number,
57+
calculateSize?: (value: T) => number,
58+
onEvict?: (key: string, value: T) => void
59+
) {
5560
this.maxSize = maxSize
5661
this.calculateSize = calculateSize
62+
this.onEvict = onEvict
5763

5864
// Create sentinel nodes to simplify doubly-linked list operations
5965
// HEAD <-> TAIL (empty list)
@@ -144,6 +150,7 @@ export class LRUCache<T> {
144150
const tail = this.removeTail()
145151
this.cache.delete(tail.key)
146152
this.totalSize -= tail.size
153+
this.onEvict?.(tail.key, tail.data)
147154
}
148155
}
149156

@@ -191,6 +198,10 @@ export class LRUCache<T> {
191198
* Removes a specific key from the cache.
192199
* Updates both the hash map and doubly-linked list.
193200
*
201+
* Note: This is an explicit removal and does NOT trigger the `onEvict`
202+
* callback. Use this for intentional deletions where eviction tracking
203+
* is not needed.
204+
*
194205
* Time Complexity: O(1)
195206
*/
196207
public remove(key: string): void {

0 commit comments

Comments
 (0)