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
28 changes: 17 additions & 11 deletions packages/cache-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { isObject } from "./is-object.js";
import { lt } from "./lt.js";
import { runIfFn } from "./run-if-fn.js";

const _storeLabel = (i: number) => (i === 0 ? "primary" : `secondary:${i - 1}`);

export type CreateCacheOptions = {
stores?: Keyv[];
ttl?: number;
Expand Down Expand Up @@ -105,16 +107,21 @@ export const createCache = (options?: CreateCacheOptions): Cache => {
eventEmitter.emit("get", { key, error });
}
} else {
for (const store of stores) {
for (let i = 0; i < stores.length; i++) {
const store = stores[i];
try {
const cacheValue = await store.get<T>(key);
if (cacheValue !== undefined) {
result = cacheValue;
eventEmitter.emit("get", { key, value: result });
eventEmitter.emit("get", {
key,
value: result,
store: _storeLabel(i),
});
break;
}
} catch (error) {
eventEmitter.emit("get", { key, error });
eventEmitter.emit("get", { key, error, store: _storeLabel(i) });
}
}
}
Expand Down Expand Up @@ -212,19 +219,18 @@ export const createCache = (options?: CreateCacheOptions): Cache => {
ttl?: number,
): Promise<T> => {
try {
const promises = stores.map(async (store, i) => {
await store.set(key, value, ttl ?? options?.ttl);
eventEmitter.emit("set", { key, value, store: _storeLabel(i) });
});

if (nonBlocking) {
Promise.all(
stores.map(async (store) =>
store.set(key, value, ttl ?? options?.ttl),
),
);
Promise.all(promises);
eventEmitter.emit("set", { key, value });
return value;
}

await Promise.all(
stores.map(async (store) => store.set(key, value, ttl ?? options?.ttl)),
);
await Promise.all(promises);
eventEmitter.emit("set", { key, value });
return value;
} catch (error) {
Expand Down
41 changes: 41 additions & 0 deletions packages/cache-manager/test/events.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// biome-ignore-all lint/suspicious/noExplicitAny: test file
import { faker } from "@faker-js/faker";
import { Keyv } from "keyv";
import { beforeEach, describe, expect, it, vi } from "vitest";
Expand Down Expand Up @@ -132,4 +133,44 @@ describe("events", () => {
error,
});
});

it("event: get includes store and value", async () => {
const listener = vi.fn();

cache.on("get", listener);

await cache.set(data.key, data.value);
await cache.get(data.key);

expect(listener).toHaveBeenCalled();

const payload = listener.mock.calls[0][0];

expect(payload).toMatchObject({
key: data.key,
value: data.value,
store: "primary",
});
});

it("event: get error includes store", async () => {
const l1 = new Keyv();
const l2 = new Keyv();

const cache = createCache({ stores: [l1, l2] });
const events: any[] = [];

cache.on("get", (e: any) => events.push(e));

l1.get = () => {
throw new Error("boom");
};

await expect(cache.get("nope")).resolves.toBeUndefined();

const errEvt = events.find((e) => e.error);

expect(errEvt).toBeTruthy();
expect(errEvt.store).toBe("primary");
});
});
49 changes: 49 additions & 0 deletions packages/cache-manager/test/multiple-stores.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// biome-ignore-all lint/suspicious/noExplicitAny: test file
import { faker } from "@faker-js/faker";
import { Keyv } from "keyv";
import { beforeEach, describe, expect, it, vi } from "vitest";
Expand Down Expand Up @@ -111,4 +112,52 @@ describe("multiple stores", () => {
"new",
);
});

it("event: get emits store id for secondary hit", async () => {
const events: any[] = [];

cache.on("get", (e: any) => events.push(e));

await keyv2.set(data.key, data.value);
await expect(cache.get(data.key)).resolves.toEqual(data.value);

const rec = events.find((e) => e.key === data.key);

expect(rec).toBeTruthy();
expect(typeof rec.store).toBe("string");
expect(rec.store).not.toBe("primary");
});

it("event: set emits store id per layer (primary + secondary)", async () => {
const events: any[] = [];

cache.on("set", (e: any) => events.push(e));

await cache.set(data.key, data.value, ttl);

const stores = events.filter((e) => e.key === data.key).map((e) => e.store);

expect(stores.length).toBeGreaterThanOrEqual(2);
expect(new Set(stores).size).toBeGreaterThanOrEqual(2);
expect(stores).toContain("primary");
expect(stores).toContain("secondary:0");
});

it("event: get includes store for deeper layers (secondary:1)", async () => {
const l1 = new Keyv();
const l2 = new Keyv();
const l3 = new Keyv();

const cache = createCache({ stores: [l1, l2, l3] });

const events: any[] = [];
cache.on("get", (e: any) => events.push(e));

await l3.set(data.key, data.value);
await expect(cache.get(data.key)).resolves.toEqual(data.value);

const rec = events.find((e) => e.key === data.key);

expect(rec.store).toBe("secondary:1");
});
});