diff --git a/packages/cacheable/src/index.ts b/packages/cacheable/src/index.ts index 1410bd31..2f2846a2 100644 --- a/packages/cacheable/src/index.ts +++ b/packages/cacheable/src/index.ts @@ -1155,10 +1155,17 @@ 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) { + // 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()) { + // 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 214f275c..1318cbc0 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: undefined }, + { 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) {