Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/node-cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion packages/node-cache/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
},
"dependencies": {
"@cacheable/utils": "workspace:^",
"hookified": "^1.15.0",
"hookified": "^2.1.0",
"keyv": "^5.6.0"
},
"files": [
Expand Down
2 changes: 1 addition & 1 deletion packages/node-cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class NodeCache<T> extends Hookified {
private intervalId: number | NodeJS.Timeout = 0;

constructor(options?: NodeCacheOptions) {
super();
super({ throwOnHookError: false });

if (options) {
this.options = { ...this.options, ...options };
Expand Down
95 changes: 86 additions & 9 deletions packages/node-cache/src/store.ts
Original file line number Diff line number Diff line change
@@ -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<T> = {
Expand All @@ -17,6 +17,7 @@ export type NodeCacheStoreOptions<T> = {
export class NodeCacheStore<T> extends Hookified {
private _keyv: Keyv<T>;
private _ttl?: number | string;
private _stats: Stats = new Stats({ enabled: true });

constructor(options?: NodeCacheStoreOptions<T>) {
super();
Expand Down Expand Up @@ -68,8 +69,12 @@ export class NodeCacheStore<T> extends Hookified {
value: T,
ttl?: number | string,
): Promise<boolean> {
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;
}

Expand All @@ -80,8 +85,12 @@ export class NodeCacheStore<T> extends Hookified {
*/
public async mset(list: Array<PartialNodeCacheItem<T>>): Promise<void> {
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();
}
}

Expand All @@ -91,7 +100,14 @@ export class NodeCacheStore<T> extends Hookified {
* @returns {any | undefined}
*/
public async get<V = T>(key: string | number): Promise<V | undefined> {
return this._keyv.get<V>(key.toString());
const result = await this._keyv.get<V>(key.toString());
if (result === undefined) {
this._stats.incrementMisses();
} else {
this._stats.incrementHits();
}

return result;
}

/**
Expand All @@ -104,7 +120,14 @@ export class NodeCacheStore<T> extends Hookified {
): Promise<Record<string, V | undefined>> {
const result: Record<string, V | undefined> = {};
for (const key of keys) {
result[key.toString()] = await this._keyv.get<V>(key.toString());
const value = await this._keyv.get<V>(key.toString());
if (value === undefined) {
this._stats.incrementMisses();
} else {
this._stats.incrementHits();
}

result[key.toString()] = value;
}

return result;
Expand All @@ -116,7 +139,19 @@ export class NodeCacheStore<T> extends Hookified {
* @returns {boolean}
*/
public async del(key: string | number): Promise<boolean> {
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;
}

/**
Expand All @@ -125,6 +160,17 @@ export class NodeCacheStore<T> extends Hookified {
* @returns {boolean}
*/
public async mdel(keys: Array<string | number>): Promise<boolean> {
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()));
}

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

/**
Expand Down Expand Up @@ -162,14 +209,44 @@ export class NodeCacheStore<T> extends Hookified {
* @returns {T | undefined}
*/
public async take<V = T>(key: string | number): Promise<V | undefined> {
const result = await this._keyv.get<V>(key.toString());
const keyValue = key.toString();
const result = await this._keyv.get<V>(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}
Expand Down
104 changes: 104 additions & 0 deletions packages/node-cache/test/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MyType>();
Expand Down
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading