diff --git a/packages/node-cache/README.md b/packages/node-cache/README.md index f0182ebe..a7344b81 100644 --- a/packages/node-cache/README.md +++ b/packages/node-cache/README.md @@ -413,6 +413,18 @@ If you need key limits with an external store, configure the limit at the storag The `stats` option and internal stats tracking have been removed from `NodeCacheStore`. The stats were collected internally but never exposed via a public API, making them effectively unused. +## Upgraded `hookified` to v2 + +The underlying `hookified` dependency has been upgraded from v1 to v2. Both `NodeCache` and `NodeCacheStore` extend `Hookified`. Key changes in hookified v2: + +- `logger` property renamed to `eventLogger` +- `Hook` type renamed to `HookFn` +- `onHook` signature changed to handle `IHook` interface +- Removed `throwHookErrors` configuration option +- `throwOnEmptyListeners` default changed to `true` + +If you use hooks or advanced event features from the `Hookified` base class directly, review the [hookified v2 changelog](https://github.com/jaredwray/hookified) for details. + # How to Contribute You can contribute by forking the repo and submitting a pull request. Please make sure to add tests and update the documentation. To learn more about how to contribute go to our main README [https://github.com/jaredwray/cacheable](https://github.com/jaredwray/cacheable). This will talk about how to `Open a Pull Request`, `Ask a Question`, or `Post an Issue`. diff --git a/packages/node-cache/package.json b/packages/node-cache/package.json index 479da0d3..61bd4ca8 100644 --- a/packages/node-cache/package.json +++ b/packages/node-cache/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@cacheable/utils": "workspace:^", - "hookified": "^1.15.0", + "hookified": "^2.1.0", "keyv": "^5.6.0" }, "files": [ diff --git a/packages/node-cache/src/index.ts b/packages/node-cache/src/index.ts index 770016c4..d52aeded 100644 --- a/packages/node-cache/src/index.ts +++ b/packages/node-cache/src/index.ts @@ -98,7 +98,7 @@ export class NodeCache extends Hookified { private intervalId: number | NodeJS.Timeout = 0; constructor(options?: NodeCacheOptions) { - super(); + super({ throwOnHookError: false }); if (options) { this.options = { ...this.options, ...options }; diff --git a/packages/node-cache/src/store.ts b/packages/node-cache/src/store.ts index e70a446d..93ea4736 100644 --- a/packages/node-cache/src/store.ts +++ b/packages/node-cache/src/store.ts @@ -1,6 +1,6 @@ -import { shorthandToMilliseconds } from "@cacheable/utils"; +import { Stats, shorthandToMilliseconds } from "@cacheable/utils"; import { Hookified } from "hookified"; -import type { PartialNodeCacheItem } from "index.js"; +import type { NodeCacheStats, PartialNodeCacheItem } from "index.js"; import Keyv from "keyv"; export type NodeCacheStoreOptions = { @@ -17,6 +17,7 @@ export type NodeCacheStoreOptions = { export class NodeCacheStore extends Hookified { private _keyv: Keyv; private _ttl?: number | string; + private _stats: Stats = new Stats({ enabled: true }); constructor(options?: NodeCacheStoreOptions) { super(); @@ -68,8 +69,12 @@ export class NodeCacheStore extends Hookified { value: T, ttl?: number | string, ): Promise { + const keyValue = key.toString(); const finalTtl = this.resolveTtl(ttl); - await this._keyv.set(key.toString(), value, finalTtl); + await this._keyv.set(keyValue, value, finalTtl); + this._stats.incrementKSize(keyValue); + this._stats.incrementVSize(value); + this._stats.incrementCount(); return true; } @@ -80,8 +85,12 @@ export class NodeCacheStore extends Hookified { */ public async mset(list: Array>): Promise { for (const item of list) { + const keyValue = item.key.toString(); const finalTtl = this.resolveTtl(item.ttl); - await this._keyv.set(item.key.toString(), item.value, finalTtl); + await this._keyv.set(keyValue, item.value, finalTtl); + this._stats.incrementKSize(keyValue); + this._stats.incrementVSize(item.value); + this._stats.incrementCount(); } } @@ -91,7 +100,14 @@ export class NodeCacheStore extends Hookified { * @returns {any | undefined} */ public async get(key: string | number): Promise { - return this._keyv.get(key.toString()); + const result = await this._keyv.get(key.toString()); + if (result === undefined) { + this._stats.incrementMisses(); + } else { + this._stats.incrementHits(); + } + + return result; } /** @@ -104,7 +120,14 @@ export class NodeCacheStore extends Hookified { ): Promise> { const result: Record = {}; for (const key of keys) { - result[key.toString()] = await this._keyv.get(key.toString()); + const value = await this._keyv.get(key.toString()); + if (value === undefined) { + this._stats.incrementMisses(); + } else { + this._stats.incrementHits(); + } + + result[key.toString()] = value; } return result; @@ -116,7 +139,19 @@ export class NodeCacheStore extends Hookified { * @returns {boolean} */ public async del(key: string | number): Promise { - return this._keyv.delete(key.toString()); + const keyValue = key.toString(); + const value = await this._keyv.get(keyValue); + const result = await this._keyv.delete(keyValue); + if (result) { + this._stats.decreaseKSize(keyValue); + if (value !== undefined) { + this._stats.decreaseVSize(value); + } + + this._stats.decreaseCount(); + } + + return result; } /** @@ -125,6 +160,17 @@ export class NodeCacheStore extends Hookified { * @returns {boolean} */ public async mdel(keys: Array): Promise { + for (const key of keys) { + const keyValue = key.toString(); + const value = await this._keyv.get(keyValue); + this._stats.decreaseKSize(keyValue); + if (value !== undefined) { + this._stats.decreaseVSize(value); + } + + this._stats.decreaseCount(); + } + return this._keyv.delete(keys.map((key) => key.toString())); } @@ -134,6 +180,7 @@ export class NodeCacheStore extends Hookified { */ public async clear(): Promise { await this._keyv.clear(); + this._stats.resetStoreValues(); } /** @@ -162,14 +209,44 @@ export class NodeCacheStore extends Hookified { * @returns {T | undefined} */ public async take(key: string | number): Promise { - const result = await this._keyv.get(key.toString()); + const keyValue = key.toString(); + const result = await this._keyv.get(keyValue); if (result !== undefined) { - await this._keyv.delete(key.toString()); + await this._keyv.delete(keyValue); + this._stats.incrementHits(); + this._stats.decreaseKSize(keyValue); + this._stats.decreaseVSize(result); + this._stats.decreaseCount(); + } else { + this._stats.incrementMisses(); } return result; } + /** + * Gets the stats of the cache + * @returns {NodeCacheStats} the stats of the cache + */ + public getStats(): NodeCacheStats { + return { + keys: this._stats.count, + hits: this._stats.hits, + misses: this._stats.misses, + ksize: this._stats.ksize, + vsize: this._stats.vsize, + }; + } + + /** + * Flush the stats. + * @returns {void} + */ + public flushStats(): void { + this._stats = new Stats({ enabled: true }); + this.emit("flush_stats"); + } + /** * Disconnect from the cache. * @returns {void} diff --git a/packages/node-cache/test/store.test.ts b/packages/node-cache/test/store.test.ts index caed09b1..89318059 100644 --- a/packages/node-cache/test/store.test.ts +++ b/packages/node-cache/test/store.test.ts @@ -148,6 +148,110 @@ describe("NodeCacheStore", () => { expect(result).toBe("value"); }); + test("should return initial stats with all zeros", () => { + const store = new NodeCacheStore(); + const stats = store.getStats(); + expect(stats).toEqual({ keys: 0, hits: 0, misses: 0, ksize: 0, vsize: 0 }); + }); + test("should track hits and misses on get", async () => { + const store = new NodeCacheStore(); + await store.set("test", "value"); + await store.get("test"); + await store.get("missing"); + const stats = store.getStats(); + expect(stats.hits).toBe(1); + expect(stats.misses).toBe(1); + }); + test("should track hits and misses on mget", async () => { + const store = new NodeCacheStore(); + await store.set("key1", "value1"); + await store.mget(["key1", "missing1", "missing2"]); + const stats = store.getStats(); + expect(stats.hits).toBe(1); + expect(stats.misses).toBe(2); + }); + test("should track ksize, vsize, and keys on set", async () => { + const store = new NodeCacheStore(); + await store.set("foo", "bar"); + const stats = store.getStats(); + expect(stats.keys).toBe(1); + expect(stats.ksize).toBeGreaterThan(0); + expect(stats.vsize).toBeGreaterThan(0); + }); + test("should track ksize, vsize, and keys on mset", async () => { + const store = new NodeCacheStore(); + await store.mset([ + { key: "a", value: "1" }, + { key: "b", value: "2" }, + ]); + const stats = store.getStats(); + expect(stats.keys).toBe(2); + expect(stats.ksize).toBeGreaterThan(0); + expect(stats.vsize).toBeGreaterThan(0); + }); + test("should decrease stats on del", async () => { + const store = new NodeCacheStore(); + await store.set("foo", "bar"); + const before = store.getStats(); + expect(before.keys).toBe(1); + await store.del("foo"); + const after = store.getStats(); + expect(after.keys).toBe(0); + expect(after.ksize).toBe(0); + expect(after.vsize).toBe(0); + }); + test("should decrease stats on mdel", async () => { + const store = new NodeCacheStore(); + await store.set("a", "1"); + await store.set("b", "2"); + expect(store.getStats().keys).toBe(2); + await store.mdel(["a", "b"]); + const stats = store.getStats(); + expect(stats.keys).toBe(0); + expect(stats.ksize).toBe(0); + expect(stats.vsize).toBe(0); + }); + test("should track stats on take", async () => { + const store = new NodeCacheStore(); + await store.set("foo", "bar"); + await store.take("foo"); + await store.take("missing"); + const stats = store.getStats(); + expect(stats.hits).toBe(1); + expect(stats.misses).toBe(1); + expect(stats.keys).toBe(0); + }); + test("should reset store values on clear but preserve hits/misses", async () => { + const store = new NodeCacheStore(); + await store.set("foo", "bar"); + await store.get("foo"); + await store.get("missing"); + await store.clear(); + const stats = store.getStats(); + expect(stats.keys).toBe(0); + expect(stats.ksize).toBe(0); + expect(stats.vsize).toBe(0); + expect(stats.hits).toBe(1); + expect(stats.misses).toBe(1); + }); + test("should flush all stats", async () => { + const store = new NodeCacheStore(); + await store.set("foo", "bar"); + await store.get("foo"); + store.flushStats(); + const stats = store.getStats(); + expect(stats).toEqual({ keys: 0, hits: 0, misses: 0, ksize: 0, vsize: 0 }); + }); + test("should emit flush_stats event on flushStats", async () => { + const store = new NodeCacheStore(); + let emitted = false; + store.on("flush_stats", () => { + emitted = true; + }); + store.flushStats(); + expect(emitted).toBe(true); + }); + test("should propagate class-level generic type through get, mget, and take", async () => { type MyType = { name: string; age: number }; const store = new NodeCacheStore(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff761a81..39046051 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,8 +278,8 @@ importers: specifier: workspace:^ version: link:../utils hookified: - specifier: ^1.15.0 - version: 1.15.0 + specifier: ^2.1.0 + version: 2.1.0 keyv: specifier: ^5.6.0 version: 5.6.0