Skip to content

Commit 6292b97

Browse files
authored
node-cache - chore: (breaking) upgrading hookified adding stats (#1609)
* node-cache - chore: (breaking) upgrading hookified * throwOnHookErrors updated * adding stats
1 parent 8ac7cbf commit 6292b97

6 files changed

Lines changed: 206 additions & 13 deletions

File tree

packages/node-cache/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,18 @@ If you need key limits with an external store, configure the limit at the storag
413413

414414
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.
415415

416+
## Upgraded `hookified` to v2
417+
418+
The underlying `hookified` dependency has been upgraded from v1 to v2. Both `NodeCache` and `NodeCacheStore` extend `Hookified`. Key changes in hookified v2:
419+
420+
- `logger` property renamed to `eventLogger`
421+
- `Hook` type renamed to `HookFn`
422+
- `onHook` signature changed to handle `IHook` interface
423+
- Removed `throwHookErrors` configuration option
424+
- `throwOnEmptyListeners` default changed to `true`
425+
426+
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.
427+
416428
# How to Contribute
417429

418430
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`.

packages/node-cache/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
},
5454
"dependencies": {
5555
"@cacheable/utils": "workspace:^",
56-
"hookified": "^1.15.0",
56+
"hookified": "^2.1.0",
5757
"keyv": "^5.6.0"
5858
},
5959
"files": [

packages/node-cache/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export class NodeCache<T> extends Hookified {
9898
private intervalId: number | NodeJS.Timeout = 0;
9999

100100
constructor(options?: NodeCacheOptions) {
101-
super();
101+
super({ throwOnHookError: false });
102102

103103
if (options) {
104104
this.options = { ...this.options, ...options };

packages/node-cache/src/store.ts

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { shorthandToMilliseconds } from "@cacheable/utils";
1+
import { Stats, shorthandToMilliseconds } from "@cacheable/utils";
22
import { Hookified } from "hookified";
3-
import type { PartialNodeCacheItem } from "index.js";
3+
import type { NodeCacheStats, PartialNodeCacheItem } from "index.js";
44
import Keyv from "keyv";
55

66
export type NodeCacheStoreOptions<T> = {
@@ -17,6 +17,7 @@ export type NodeCacheStoreOptions<T> = {
1717
export class NodeCacheStore<T> extends Hookified {
1818
private _keyv: Keyv<T>;
1919
private _ttl?: number | string;
20+
private _stats: Stats = new Stats({ enabled: true });
2021

2122
constructor(options?: NodeCacheStoreOptions<T>) {
2223
super();
@@ -68,8 +69,12 @@ export class NodeCacheStore<T> extends Hookified {
6869
value: T,
6970
ttl?: number | string,
7071
): Promise<boolean> {
72+
const keyValue = key.toString();
7173
const finalTtl = this.resolveTtl(ttl);
72-
await this._keyv.set(key.toString(), value, finalTtl);
74+
await this._keyv.set(keyValue, value, finalTtl);
75+
this._stats.incrementKSize(keyValue);
76+
this._stats.incrementVSize(value);
77+
this._stats.incrementCount();
7378
return true;
7479
}
7580

@@ -80,8 +85,12 @@ export class NodeCacheStore<T> extends Hookified {
8085
*/
8186
public async mset(list: Array<PartialNodeCacheItem<T>>): Promise<void> {
8287
for (const item of list) {
88+
const keyValue = item.key.toString();
8389
const finalTtl = this.resolveTtl(item.ttl);
84-
await this._keyv.set(item.key.toString(), item.value, finalTtl);
90+
await this._keyv.set(keyValue, item.value, finalTtl);
91+
this._stats.incrementKSize(keyValue);
92+
this._stats.incrementVSize(item.value);
93+
this._stats.incrementCount();
8594
}
8695
}
8796

@@ -91,7 +100,14 @@ export class NodeCacheStore<T> extends Hookified {
91100
* @returns {any | undefined}
92101
*/
93102
public async get<V = T>(key: string | number): Promise<V | undefined> {
94-
return this._keyv.get<V>(key.toString());
103+
const result = await this._keyv.get<V>(key.toString());
104+
if (result === undefined) {
105+
this._stats.incrementMisses();
106+
} else {
107+
this._stats.incrementHits();
108+
}
109+
110+
return result;
95111
}
96112

97113
/**
@@ -104,7 +120,14 @@ export class NodeCacheStore<T> extends Hookified {
104120
): Promise<Record<string, V | undefined>> {
105121
const result: Record<string, V | undefined> = {};
106122
for (const key of keys) {
107-
result[key.toString()] = await this._keyv.get<V>(key.toString());
123+
const value = await this._keyv.get<V>(key.toString());
124+
if (value === undefined) {
125+
this._stats.incrementMisses();
126+
} else {
127+
this._stats.incrementHits();
128+
}
129+
130+
result[key.toString()] = value;
108131
}
109132

110133
return result;
@@ -116,7 +139,19 @@ export class NodeCacheStore<T> extends Hookified {
116139
* @returns {boolean}
117140
*/
118141
public async del(key: string | number): Promise<boolean> {
119-
return this._keyv.delete(key.toString());
142+
const keyValue = key.toString();
143+
const value = await this._keyv.get(keyValue);
144+
const result = await this._keyv.delete(keyValue);
145+
if (result) {
146+
this._stats.decreaseKSize(keyValue);
147+
if (value !== undefined) {
148+
this._stats.decreaseVSize(value);
149+
}
150+
151+
this._stats.decreaseCount();
152+
}
153+
154+
return result;
120155
}
121156

122157
/**
@@ -125,6 +160,17 @@ export class NodeCacheStore<T> extends Hookified {
125160
* @returns {boolean}
126161
*/
127162
public async mdel(keys: Array<string | number>): Promise<boolean> {
163+
for (const key of keys) {
164+
const keyValue = key.toString();
165+
const value = await this._keyv.get(keyValue);
166+
this._stats.decreaseKSize(keyValue);
167+
if (value !== undefined) {
168+
this._stats.decreaseVSize(value);
169+
}
170+
171+
this._stats.decreaseCount();
172+
}
173+
128174
return this._keyv.delete(keys.map((key) => key.toString()));
129175
}
130176

@@ -134,6 +180,7 @@ export class NodeCacheStore<T> extends Hookified {
134180
*/
135181
public async clear(): Promise<void> {
136182
await this._keyv.clear();
183+
this._stats.resetStoreValues();
137184
}
138185

139186
/**
@@ -162,14 +209,44 @@ export class NodeCacheStore<T> extends Hookified {
162209
* @returns {T | undefined}
163210
*/
164211
public async take<V = T>(key: string | number): Promise<V | undefined> {
165-
const result = await this._keyv.get<V>(key.toString());
212+
const keyValue = key.toString();
213+
const result = await this._keyv.get<V>(keyValue);
166214
if (result !== undefined) {
167-
await this._keyv.delete(key.toString());
215+
await this._keyv.delete(keyValue);
216+
this._stats.incrementHits();
217+
this._stats.decreaseKSize(keyValue);
218+
this._stats.decreaseVSize(result);
219+
this._stats.decreaseCount();
220+
} else {
221+
this._stats.incrementMisses();
168222
}
169223

170224
return result;
171225
}
172226

227+
/**
228+
* Gets the stats of the cache
229+
* @returns {NodeCacheStats} the stats of the cache
230+
*/
231+
public getStats(): NodeCacheStats {
232+
return {
233+
keys: this._stats.count,
234+
hits: this._stats.hits,
235+
misses: this._stats.misses,
236+
ksize: this._stats.ksize,
237+
vsize: this._stats.vsize,
238+
};
239+
}
240+
241+
/**
242+
* Flush the stats.
243+
* @returns {void}
244+
*/
245+
public flushStats(): void {
246+
this._stats = new Stats({ enabled: true });
247+
this.emit("flush_stats");
248+
}
249+
173250
/**
174251
* Disconnect from the cache.
175252
* @returns {void}

packages/node-cache/test/store.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,110 @@ describe("NodeCacheStore", () => {
148148
expect(result).toBe("value");
149149
});
150150

151+
test("should return initial stats with all zeros", () => {
152+
const store = new NodeCacheStore();
153+
const stats = store.getStats();
154+
expect(stats).toEqual({ keys: 0, hits: 0, misses: 0, ksize: 0, vsize: 0 });
155+
});
156+
test("should track hits and misses on get", async () => {
157+
const store = new NodeCacheStore();
158+
await store.set("test", "value");
159+
await store.get("test");
160+
await store.get("missing");
161+
const stats = store.getStats();
162+
expect(stats.hits).toBe(1);
163+
expect(stats.misses).toBe(1);
164+
});
165+
test("should track hits and misses on mget", async () => {
166+
const store = new NodeCacheStore();
167+
await store.set("key1", "value1");
168+
await store.mget(["key1", "missing1", "missing2"]);
169+
const stats = store.getStats();
170+
expect(stats.hits).toBe(1);
171+
expect(stats.misses).toBe(2);
172+
});
173+
test("should track ksize, vsize, and keys on set", async () => {
174+
const store = new NodeCacheStore();
175+
await store.set("foo", "bar");
176+
const stats = store.getStats();
177+
expect(stats.keys).toBe(1);
178+
expect(stats.ksize).toBeGreaterThan(0);
179+
expect(stats.vsize).toBeGreaterThan(0);
180+
});
181+
test("should track ksize, vsize, and keys on mset", async () => {
182+
const store = new NodeCacheStore();
183+
await store.mset([
184+
{ key: "a", value: "1" },
185+
{ key: "b", value: "2" },
186+
]);
187+
const stats = store.getStats();
188+
expect(stats.keys).toBe(2);
189+
expect(stats.ksize).toBeGreaterThan(0);
190+
expect(stats.vsize).toBeGreaterThan(0);
191+
});
192+
test("should decrease stats on del", async () => {
193+
const store = new NodeCacheStore();
194+
await store.set("foo", "bar");
195+
const before = store.getStats();
196+
expect(before.keys).toBe(1);
197+
await store.del("foo");
198+
const after = store.getStats();
199+
expect(after.keys).toBe(0);
200+
expect(after.ksize).toBe(0);
201+
expect(after.vsize).toBe(0);
202+
});
203+
test("should decrease stats on mdel", async () => {
204+
const store = new NodeCacheStore();
205+
await store.set("a", "1");
206+
await store.set("b", "2");
207+
expect(store.getStats().keys).toBe(2);
208+
await store.mdel(["a", "b"]);
209+
const stats = store.getStats();
210+
expect(stats.keys).toBe(0);
211+
expect(stats.ksize).toBe(0);
212+
expect(stats.vsize).toBe(0);
213+
});
214+
test("should track stats on take", async () => {
215+
const store = new NodeCacheStore();
216+
await store.set("foo", "bar");
217+
await store.take("foo");
218+
await store.take("missing");
219+
const stats = store.getStats();
220+
expect(stats.hits).toBe(1);
221+
expect(stats.misses).toBe(1);
222+
expect(stats.keys).toBe(0);
223+
});
224+
test("should reset store values on clear but preserve hits/misses", async () => {
225+
const store = new NodeCacheStore();
226+
await store.set("foo", "bar");
227+
await store.get("foo");
228+
await store.get("missing");
229+
await store.clear();
230+
const stats = store.getStats();
231+
expect(stats.keys).toBe(0);
232+
expect(stats.ksize).toBe(0);
233+
expect(stats.vsize).toBe(0);
234+
expect(stats.hits).toBe(1);
235+
expect(stats.misses).toBe(1);
236+
});
237+
test("should flush all stats", async () => {
238+
const store = new NodeCacheStore();
239+
await store.set("foo", "bar");
240+
await store.get("foo");
241+
store.flushStats();
242+
const stats = store.getStats();
243+
expect(stats).toEqual({ keys: 0, hits: 0, misses: 0, ksize: 0, vsize: 0 });
244+
});
245+
test("should emit flush_stats event on flushStats", async () => {
246+
const store = new NodeCacheStore();
247+
let emitted = false;
248+
store.on("flush_stats", () => {
249+
emitted = true;
250+
});
251+
store.flushStats();
252+
expect(emitted).toBe(true);
253+
});
254+
151255
test("should propagate class-level generic type through get, mget, and take", async () => {
152256
type MyType = { name: string; age: number };
153257
const store = new NodeCacheStore<MyType>();

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)