From f169b908e993b6a10621d77241acdc7e8f0a9855 Mon Sep 17 00:00:00 2001 From: Adam Boudjemaa Date: Thu, 5 Mar 2026 11:10:13 +0100 Subject: [PATCH 1/6] fix(lru): prevent unbounded cache growth on iOS 18+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2911 — `isAddress()` causes memory leak on iOS 18+ when called with 8k+ unique addresses because `LruMap` entries are never evicted. Three issues in `LruMap.set()`: 1. `this.keys()` on a Map subclass can return stale iterators on iOS 18 JavaScriptCore — changed to `super.keys()` to bypass subclass dispatch 2. Truthy check `if (firstKey)` silently skips eviction for falsy keys (e.g. empty string) — changed to `firstKey !== undefined` 3. Re-setting an existing key didn't refresh its LRU position — added delete-before-set to move updated keys to end of insertion order --- src/utils/lru.test.ts | 28 ++++++++++++++++++++++++++++ src/utils/lru.ts | 5 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/utils/lru.test.ts b/src/utils/lru.test.ts index 2449644863..c59c5cefa2 100644 --- a/src/utils/lru.test.ts +++ b/src/utils/lru.test.ts @@ -20,6 +20,34 @@ 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('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..425a722f2d 100644 --- a/src/utils/lru.ts +++ b/src/utils/lru.ts @@ -23,10 +23,11 @@ export class LruMap extends Map { } 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 } From b65bac8c109d9c8b02e0655e878445af24529ba0 Mon Sep 17 00:00:00 2001 From: Adam Boudjemaa Date: Fri, 6 Mar 2026 08:17:39 +0100 Subject: [PATCH 2/6] add changeset for lru fix --- .changeset/fix-lru-ios18.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-lru-ios18.md diff --git a/.changeset/fix-lru-ios18.md b/.changeset/fix-lru-ios18.md new file mode 100644 index 0000000000..899be4e527 --- /dev/null +++ b/.changeset/fix-lru-ios18.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Fix unbounded LRU cache growth on iOS 18+ From 040f86fd0978172ff72ec87e8c119a348809a376 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:51:17 +1300 Subject: [PATCH 3/6] fix: use super.delete in get() for JSC consistency Amp-Thread-ID: https://ampcode.com/threads/T-019cfd58-c79c-779f-92ea-4af5742b9af2 Co-authored-by: Amp --- src/utils/lru.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/lru.ts b/src/utils/lru.ts index 425a722f2d..4a9579bbe7 100644 --- a/src/utils/lru.ts +++ b/src/utils/lru.ts @@ -15,7 +15,7 @@ export class LruMap extends Map { const value = super.get(key) if (super.has(key) && value !== undefined) { - this.delete(key) + super.delete(key) super.set(key, value) } From 03db85fc0d4b0d03102d004bebe2804f7a92fb0b Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 18 Mar 2026 08:56:19 +1300 Subject: [PATCH 4/6] fix: get() refreshes position for all values, add empty-string key test Amp-Thread-ID: https://ampcode.com/threads/T-019cfd58-c79c-779f-92ea-4af5742b9af2 Co-authored-by: Amp --- src/utils/lru.test.ts | 8 ++++++++ src/utils/lru.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils/lru.test.ts b/src/utils/lru.test.ts index c59c5cefa2..d0311df0b9 100644 --- a/src/utils/lru.test.ts +++ b/src/utils/lru.test.ts @@ -48,6 +48,14 @@ test('set existing key refreshes its position', () => { 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 4a9579bbe7..612544a9be 100644 --- a/src/utils/lru.ts +++ b/src/utils/lru.ts @@ -14,7 +14,7 @@ export class LruMap extends Map { override get(key: string) { const value = super.get(key) - if (super.has(key) && value !== undefined) { + if (super.has(key)) { super.delete(key) super.set(key, value) } From 9fdd7c8294bff1e699ee7ee41d88763bf437e5f3 Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:07:21 +1300 Subject: [PATCH 5/6] fix: resolve TS2345 type error in get() Amp-Thread-ID: https://ampcode.com/threads/T-019cfd58-c79c-779f-92ea-4af5742b9af2 Co-authored-by: Amp --- src/utils/lru.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/lru.ts b/src/utils/lru.ts index 612544a9be..4d884e6b08 100644 --- a/src/utils/lru.ts +++ b/src/utils/lru.ts @@ -16,7 +16,7 @@ export class LruMap extends Map { if (super.has(key)) { super.delete(key) - super.set(key, value) + super.set(key, value as value) } return value From 46c1e9c822d8f871a17dc3a3bf560a7451a103ab Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Wed, 18 Mar 2026 09:10:16 +1300 Subject: [PATCH 6/6] Update fix-lru-ios18.md --- .changeset/fix-lru-ios18.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-lru-ios18.md b/.changeset/fix-lru-ios18.md index 899be4e527..c07ee58c5b 100644 --- a/.changeset/fix-lru-ios18.md +++ b/.changeset/fix-lru-ios18.md @@ -2,4 +2,4 @@ "viem": patch --- -Fix unbounded LRU cache growth on iOS 18+ +Fixed unbounded LRU cache growth on iOS 18+.