Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fix-lru-ios18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Fixed unbounded LRU cache growth on iOS 18+.
36 changes: 36 additions & 0 deletions src/utils/lru.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,42 @@ test('default', () => {
expect(cache.get('g')).toBe(7)
})

test('eviction does not exceed maxSize under heavy load', () => {
const cache = new LruMap<boolean>(100)
for (let i = 0; i < 10_000; i++) {
cache.set(`key${i}`, true)
}
expect(cache.size).toBe(100)
expect(cache.has('key0')).toBe(false)
expect(cache.has('key9999')).toBe(true)
expect(cache.has('key9900')).toBe(true)
expect(cache.has('key9899')).toBe(false)
})

test('set existing key refreshes its position', () => {
const cache = new LruMap(3)
cache.set('a', 1)
cache.set('b', 2)
cache.set('c', 3)
// Refresh 'a' by re-setting it
cache.set('a', 10)
// Now 'b' is the oldest
cache.set('d', 4)
expect(cache.has('a')).toBe(true)
expect(cache.get('a')).toBe(10)
expect(cache.has('b')).toBe(false)
expect(cache.has('c')).toBe(true)
expect(cache.has('d')).toBe(true)
})

test('evicts empty-string key correctly', () => {
const cache = new LruMap<number>(1)
cache.set('', 1)
cache.set('x', 2)
expect(cache.has('')).toBe(false)
expect(cache.has('x')).toBe(true)
})

test('update touched keys', () => {
const cache = new LruMap(5)
cache.set('a', 1)
Expand Down
11 changes: 6 additions & 5 deletions src/utils/lru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@ export class LruMap<value = unknown> extends Map<string, value> {
override get(key: string) {
const value = super.get(key)

if (super.has(key) && value !== undefined) {
this.delete(key)
super.set(key, value)
if (super.has(key)) {
super.delete(key)
super.set(key, value as value)
}

return value
}

override set(key: string, value: value) {
if (super.has(key)) super.delete(key)
super.set(key, value)
if (this.maxSize && this.size > this.maxSize) {
const firstKey = this.keys().next().value
if (firstKey) this.delete(firstKey)
const firstKey = super.keys().next().value
if (firstKey !== undefined) super.delete(firstKey)
}
return this
}
Expand Down