Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 32 additions & 33 deletions packages/router-core/src/lru-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,57 +13,56 @@ export function createLRUCache<TKey, TValue>(
let newest: Node | undefined

const touch = (entry: Node) => {
if (!entry.next) return
if (!entry.prev) {
entry.next.prev = undefined
oldest = entry.next
entry.next = undefined
if (newest) {
entry.prev = newest
newest.next = entry
}
const next = entry.next
if (!next) {
return
}
const prev = entry.prev
if (prev) {
prev.next = next
} else {
entry.prev.next = entry.next
entry.next.prev = entry.prev
entry.next = undefined
if (newest) {
newest.next = entry
entry.prev = newest
}
oldest = next
}
next.prev = prev
entry.prev = newest
entry.next = undefined
newest!.next = entry
newest = entry
}

return {
get(key) {
const entry = cache.get(key)
if (!entry) return undefined
if (!entry) {
return undefined
}
touch(entry)
return entry.value
},
set(key, value) {
const entry = cache.get(key)
if (entry) {
entry.value = value
touch(entry)
return
}
if (cache.size >= max && oldest) {
const toDelete = oldest
cache.delete(toDelete.key)
if (toDelete.next) {
oldest = toDelete.next
toDelete.next.prev = undefined
}
if (toDelete === newest) {
cache.delete(oldest.key)
oldest = oldest.next
if (oldest) {
oldest.prev = undefined
} else {
newest = undefined
}
}
const existing = cache.get(key)
if (existing) {
existing.value = value
touch(existing)
const newEntry: Node = { key, value, prev: newest }
if (newest) {
newest.next = newEntry
} else {
const entry: Node = { key, value, prev: newest }
if (newest) newest.next = entry
newest = entry
if (!oldest) oldest = entry
cache.set(key, entry)
oldest = newEntry
}
newest = newEntry
cache.set(key, newEntry)
},
clear() {
cache.clear()
Expand Down
87 changes: 87 additions & 0 deletions packages/router-core/tests/lru-cache.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { bench, describe } from 'vitest'
import { createLRUCache } from '../src/lru-cache'

const keys1000 = Array.from({ length: 1000 }, (_, i) => `key-${i}`)
const missing1000 = Array.from({ length: 1000 }, (_, i) => `missing-${i}`)
const new1000 = Array.from({ length: 1000 }, (_, i) => `new-${i}`)

function fillCache(
cache: { set: (key: string, value: number) => void },
count: number,
) {
for (let i = 0; i < count; i++) {
cache.set(keys1000[i]!, i)
}
}

describe('LRU cache', () => {
bench('newest hit', () => {
const cache = createLRUCache<string, number>(1000)
fillCache(cache, 1000)
for (let i = 0; i < 1000; i++) {
cache.get(keys1000[999]!)
}
})

bench('rotating hit', () => {
const cache = createLRUCache<string, number>(1000)
fillCache(cache, 1000)
for (let i = 0; i < 1000; i++) {
cache.get(keys1000[i]!)
}
})

bench('update newest while full', () => {
const cache = createLRUCache<string, number>(1000)
fillCache(cache, 1000)
for (let i = 0; i < 1000; i++) {
cache.set(keys1000[999]!, i)
}
})

bench('update oldest while full', () => {
const cache = createLRUCache<string, number>(1000)
fillCache(cache, 1000)
for (let i = 0; i < 1000; i++) {
cache.set(keys1000[0]!, i)
}
})

bench('update rotating entries while full', () => {
const cache = createLRUCache<string, number>(1000)
fillCache(cache, 1000)
for (let i = 0; i < 1000; i++) {
cache.set(keys1000[i]!, i + 1)
}
})

bench('miss-heavy get', () => {
const cache = createLRUCache<string, number>(64)
fillCache(cache, 64)
for (let i = 0; i < 1000; i++) {
cache.get(missing1000[i]!)
}
})

bench('insert churn', () => {
const cache = createLRUCache<string, number>(64)
fillCache(cache, 64)
for (let i = 0; i < 1000; i++) {
cache.set(new1000[i]!, i)
}
})

bench('mixed workload', () => {
const cache = createLRUCache<string, number>(64)
fillCache(cache, 64)
for (let i = 0; i < 1000; i++) {
cache.get(keys1000[i % 8]!)
if (i % 10 === 0) {
cache.get(missing1000[i]!)
}
if (i % 20 === 0) {
cache.set(new1000[i]!, i)
}
}
})
})
117 changes: 117 additions & 0 deletions packages/router-core/tests/lru.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,121 @@ describe('LRU Cache', () => {
expect(cache.get('b')).toBeUndefined()
expect(cache.get('a')).toBe(1)
})
it('does not change order on a missing get', () => {
const cache = createLRUCache<string, number>(2)
cache.set('a', 1)
cache.set('b', 2)
expect(cache.get('x')).toBeUndefined()
cache.set('c', 3)
expect(cache.get('a')).toBeUndefined()
expect(cache.get('b')).toBe(2)
})
it('moves updated entries to most recently used before capacity is reached', () => {
const cache = createLRUCache<string, number>(3)
cache.set('a', 1)
cache.set('b', 2)
cache.set('a', 3)
cache.set('c', 4)
cache.set('d', 5)
expect(cache.get('b')).toBeUndefined()
expect(cache.get('a')).toBe(3)
})
it('updates existing entries without evicting when full', () => {
const cache = createLRUCache<string, number>(3)
cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
cache.set('b', 4)
expect(cache.get('a')).toBe(1)
expect(cache.get('b')).toBe(4)
})
it('moves updated oldest entries to most recently used when full', () => {
const cache = createLRUCache<string, number>(3)
cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
cache.set('a', 4)
cache.set('d', 5)
expect(cache.get('b')).toBeUndefined()
expect(cache.get('a')).toBe(4)
})
it('keeps updated newest entries most recently used when full', () => {
const cache = createLRUCache<string, number>(3)
cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
cache.set('c', 4)
cache.set('d', 5)
expect(cache.get('a')).toBeUndefined()
expect(cache.get('c')).toBe(4)
})
it('handles repeated updates without duplicating entries', () => {
const cache = createLRUCache<string, number>(3)
cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
cache.set('a', 4)
cache.set('a', 5)
cache.set('b', 6)
cache.set('d', 7)
expect(cache.get('c')).toBeUndefined()
expect(cache.get('a')).toBe(5)
expect(cache.get('b')).toBe(6)
})
it('moves middle entries to most recently used on get', () => {
const cache = createLRUCache<string, number>(3)
cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
expect(cache.get('b')).toBe(2)
cache.set('d', 4)
expect(cache.get('a')).toBeUndefined()
expect(cache.get('c')).toBe(3)
expect(cache.get('b')).toBe(2)
})
it('clears entries and reuses the cache', () => {
const cache = createLRUCache<string, number>(2)
cache.clear()
cache.set('a', 1)
cache.set('b', 2)
cache.get('a')
cache.clear()
cache.clear()
expect(cache.get('a')).toBeUndefined()
cache.set('c', 3)
cache.set('d', 4)
cache.set('e', 5)
expect(cache.get('c')).toBeUndefined()
expect(cache.get('d')).toBe(4)
})
it('handles a max size of one', () => {
const cache = createLRUCache<string, number | undefined>(1)
cache.set('a', undefined)
expect(cache.get('a')).toBeUndefined()
cache.set('a', 2)
expect(cache.get('a')).toBe(2)
cache.set('b', 3)
expect(cache.get('a')).toBeUndefined()
expect(cache.get('b')).toBe(3)
})
it('caches undefined values', () => {
const cache = createLRUCache<string, number | undefined>(2)
cache.set('a', undefined)
cache.set('b', 2)
expect(cache.get('a')).toBeUndefined()
cache.set('c', 3)
expect(cache.get('b')).toBeUndefined()
})
it('uses non-string keys by identity', () => {
const cache = createLRUCache<object, number>(2)
const a = { id: 'a' }
const b = { id: 'b' }
const aLike = { id: 'a' }
cache.set(a, 1)
cache.set(b, 2)
expect(cache.get(aLike)).toBeUndefined()
expect(cache.get(a)).toBe(1)
cache.set({ id: 'c' }, 3)
expect(cache.get(b)).toBeUndefined()
})
})
Loading