diff --git a/.changeset/fix-lru-ios18.md b/.changeset/fix-lru-ios18.md new file mode 100644 index 0000000000..c07ee58c5b --- /dev/null +++ b/.changeset/fix-lru-ios18.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Fixed unbounded LRU cache growth on iOS 18+. diff --git a/src/utils/lru.test.ts b/src/utils/lru.test.ts index 2449644863..d0311df0b9 100644 --- a/src/utils/lru.test.ts +++ b/src/utils/lru.test.ts @@ -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(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(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) diff --git a/src/utils/lru.ts b/src/utils/lru.ts index bc52c0de47..4d884e6b08 100644 --- a/src/utils/lru.ts +++ b/src/utils/lru.ts @@ -14,19 +14,20 @@ export class LruMap extends Map { 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 }