From e293224fe43aaf7c0e0f3f15375e62a230814e4b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 00:32:55 +0000 Subject: [PATCH] cacheable - fix: count falsy values in delete stats Before, delete read the value with get() and changed the stats only for a truthy value. A cached 0, "", false, null, or undefined value did not decrease count, ksize, or vsize, and did not increase deletes. Now delete reads the raw entry with get(key, { raw: true }). It changes the stats for each entry that is in the cache. It does not use getRaw(), because getRaw() does not catch a store error. With getRaw(), a failed primary read stops the delete. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DQyS4TFa7JFA5PEcKuLj4T --- packages/cacheable/src/index.ts | 9 +++--- packages/cacheable/test/index.test.ts | 43 +++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/cacheable/src/index.ts b/packages/cacheable/src/index.ts index 2f2846a2..345b4dc8 100644 --- a/packages/cacheable/src/index.ts +++ b/packages/cacheable/src/index.ts @@ -1096,11 +1096,12 @@ export class Cacheable extends Hookified { let result = false; const promises = []; if (this.stats.enabled) { - const statResult = await this._primary.get>(key); - /* v8 ignore next -- @preserve */ - if (statResult) { + // Use the raw entry, because a cached value can be falsy or undefined. + // Use get() and not getRaw(), because get() catches a store error. + const statResult = await this._primary.get(key, { raw: true }); + if (statResult !== undefined) { 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 1318cbc0..00761a02 100644 --- a/packages/cacheable/test/index.test.ts +++ b/packages/cacheable/test/index.test.ts @@ -1087,10 +1087,47 @@ describe("cacheable stats enabled", async () => { }); test("should get stats on delete", async () => { const cacheable = new Cacheable({ stats: true }); + // Falsy values are cached values, and delete must count them. + const deletedItems = [ + { key: "key", value: "value" }, + { key: "zero", value: 0 }, + { key: "empty", value: "" }, + { key: "false", value: false }, + { key: "null", value: null }, + { key: "undefined", value: undefined }, + ]; + await cacheable.setMany([ + ...deletedItems, + { key: "remain", value: { id: "remain" } }, + ]); + // The stats of a cache that holds only "remain" are the expected result. + const expected = new Cacheable({ stats: true }); + await expected.set("remain", { id: "remain" }); + + for (const item of deletedItems) { + await cacheable.delete(item.key); + } + + await cacheable.delete("missing"); + expect(cacheable.stats.deletes).toBe(deletedItems.length); + expect(cacheable.stats.count).toBe(1); + expect(cacheable.stats.ksize).toBe(expected.stats.ksize); + expect(cacheable.stats.vsize).toBe(expected.stats.vsize); + }); + test("should delete with stats when the primary store read fails", async () => { + const store = new KeyvCacheableMemory(); + const cacheable = new Cacheable({ + primary: new Keyv({ store }), + stats: true, + }); await cacheable.set("key", "value"); - await cacheable.delete("key"); - expect(cacheable.stats.deletes).toBe(1); - expect(cacheable.stats.count).toBe(0); + // A store error on the stats read must not stop the delete. + const getSpy = vi + .spyOn(store, "get") + .mockRejectedValue(new Error("store get error")); + await expect(cacheable.delete("key")).resolves.toBe(true); + getSpy.mockRestore(); + expect(await cacheable.get("key")).toBeUndefined(); }); test("should get stats on delete many", async () => { const cacheable = new Cacheable({ stats: true });