From 33af2ab3e6d6dab82f57d300977677cd73550d0a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 23:08:00 +0000 Subject: [PATCH 1/2] cacheable - fix: size each key value in deleteMany stats (#1738) Before, deleteMany with stats sized the full array of values once for each key. The time was cubic in the input size, and it blocked the event loop. It also changed count, ksize, and deletes for keys that were not in the cache. Now deleteMany sizes only the value of each key and skips missing keys. Stats.roughSizeOfObject uses a Set for visited objects, so each call is linear. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DQyS4TFa7JFA5PEcKuLj4T --- packages/cacheable/src/index.ts | 10 ++++++++-- packages/cacheable/test/index.test.ts | 15 +++++++++++---- packages/utils/src/stats.ts | 7 ++++--- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/cacheable/src/index.ts b/packages/cacheable/src/index.ts index 1410bd31..c2864eb8 100644 --- a/packages/cacheable/src/index.ts +++ b/packages/cacheable/src/index.ts @@ -1155,8 +1155,14 @@ export class Cacheable extends Hookified { const primaryBackfillBarrier = this.cancelPrimaryBackfills(keys); await primaryBackfillBarrier; if (this.stats.enabled) { - const statResult = (await this._primary.get(keys)) as unknown; - for (const key of keys) { + const statResults = await this._primary.get(keys); + for (const [index, key] of keys.entries()) { + // Size only the value of this key, and skip keys that are not in the cache. + const statResult = statResults[index]; + if (statResult === undefined) { + continue; + } + this.stats.decreaseKSize(key); this.stats.decreaseVSize(statResult); this.stats.decreaseCount(); diff --git a/packages/cacheable/test/index.test.ts b/packages/cacheable/test/index.test.ts index 214f275c..2ef8e2cf 100644 --- a/packages/cacheable/test/index.test.ts +++ b/packages/cacheable/test/index.test.ts @@ -1095,12 +1095,19 @@ describe("cacheable stats enabled", async () => { test("should get stats on delete many", async () => { const cacheable = new Cacheable({ stats: true }); await cacheable.setMany([ - { key: "key1", value: "value1" }, - { key: "key2", value: "value2" }, + { key: "key1", value: { id: "value1" } }, + { key: "key2", value: { id: "value2" } }, + { key: "key3", value: { id: "value3" } }, ]); - await cacheable.deleteMany(["key1", "key2"]); + // The stats of a cache that holds only key3 are the expected result. + const expected = new Cacheable({ stats: true }); + await expected.set("key3", { id: "value3" }); + + await cacheable.deleteMany(["key1", "key2", "missing"]); expect(cacheable.stats.deletes).toBe(2); - expect(cacheable.stats.count).toBe(0); + expect(cacheable.stats.count).toBe(1); + expect(cacheable.stats.ksize).toBe(expected.stats.ksize); + expect(cacheable.stats.vsize).toBe(expected.stats.vsize); }); test("should get stats on clear", async () => { const cacheable = new Cacheable({ stats: true }); diff --git a/packages/utils/src/stats.ts b/packages/utils/src/stats.ts index 0e23a992..9eba5d58 100644 --- a/packages/utils/src/stats.ts +++ b/packages/utils/src/stats.ts @@ -483,7 +483,8 @@ export class Stats { } public roughSizeOfObject(object: any): number { - const objectList: any[] = []; + // Use a Set so that the visited check is O(1) and the full walk stays linear. + const objectList = new Set(); const stack: any[] = [object]; let bytes = 0; @@ -504,12 +505,12 @@ export class Stats { } // Check for circular references - if this object was already processed, skip it - if (objectList.includes(value)) { + if (objectList.has(value)) { continue; } // object as default - objectList.push(value); + objectList.add(value); // Estimate object overhead, and then recursively estimate the size of properties for (const key in value) { From eed4a05e64e029cd922ee7f7b4faa7dc8a5daee0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 23:15:24 +0000 Subject: [PATCH 2/2] cacheable - fix: count cached undefined values in deleteMany stats Before, deleteMany read values with get(). A cached undefined value and a missing key both gave undefined, so deleteMany did not change the stats for a cached undefined value. Now deleteMany reads the raw entries with getManyRaw() and sizes the value of each entry. Also split the code comment into two instructions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DQyS4TFa7JFA5PEcKuLj4T --- packages/cacheable/src/index.ts | 7 ++++--- packages/cacheable/test/index.test.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/cacheable/src/index.ts b/packages/cacheable/src/index.ts index c2864eb8..2f2846a2 100644 --- a/packages/cacheable/src/index.ts +++ b/packages/cacheable/src/index.ts @@ -1155,16 +1155,17 @@ export class Cacheable extends Hookified { const primaryBackfillBarrier = this.cancelPrimaryBackfills(keys); await primaryBackfillBarrier; if (this.stats.enabled) { - const statResults = await this._primary.get(keys); + // Use the raw entries, because a cached value can be undefined. + const statResults = await this._primary.getManyRaw(keys); for (const [index, key] of keys.entries()) { - // Size only the value of this key, and skip keys that are not in the cache. + // Skip a key that is not in the cache. Size only the value of this key. const statResult = statResults[index]; if (statResult === undefined) { continue; } this.stats.decreaseKSize(key); - this.stats.decreaseVSize(statResult); + this.stats.decreaseVSize(statResult.value); this.stats.decreaseCount(); this.stats.incrementDeletes(); } diff --git a/packages/cacheable/test/index.test.ts b/packages/cacheable/test/index.test.ts index 2ef8e2cf..1318cbc0 100644 --- a/packages/cacheable/test/index.test.ts +++ b/packages/cacheable/test/index.test.ts @@ -1096,7 +1096,7 @@ describe("cacheable stats enabled", async () => { const cacheable = new Cacheable({ stats: true }); await cacheable.setMany([ { key: "key1", value: { id: "value1" } }, - { key: "key2", value: { id: "value2" } }, + { key: "key2", value: undefined }, { key: "key3", value: { id: "value3" } }, ]); // The stats of a cache that holds only key3 are the expected result.