Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/cache-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ See unit tests in [`test/del.test.ts`](./test/del.test.ts) for more information.

`mdel(keys: [key]): Promise<true>`

Delete multiple keys, an error will be throw on any failed.
Delete multiple keys, an error will be throw on any failed. Keys are deleted with a single bulk operation per store, so stores with native multi-delete support (such as `@keyv/redis`) use one command instead of one per key.

```ts
await cache.mset([
Expand Down
5 changes: 1 addition & 4 deletions packages/cache-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,7 @@ export const createCache = (options?: CreateCacheOptions): Cache => {

const mdel = async (keys: string[]) => {
try {
const promises: Array<Promise<boolean>> = [];
for (const key of keys) {
promises.push(...stores.map(async (store) => store.delete(key)));
}
const promises = stores.map(async (store) => store.deleteMany(keys));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve empty-list behavior before bulk deletion

When keys is empty and the cache uses the documented @keyv/redis store, this now invokes its native deleteMany([]), which delegates to Redis DEL without any key arguments and is rejected by Redis. Previously the empty loop produced Promise.all([]) and resolved true; the new empty-list test only exercises Keyv's in-memory fallback, so it does not cover this adapter-specific regression. Return early for an empty array before dispatching to stores.

Useful? React with 👍 / 👎.


if (nonBlocking) {
Promise.all(promises);
Expand Down
49 changes: 45 additions & 4 deletions packages/cache-manager/test/mdel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,43 @@ describe("mdel", () => {
await expect(cache.get(list[2].key)).resolves.toEqual(list[2].value);
});

it("should delete every key with a single store call", async () => {
const cache = createCache({ stores: [keyv], nonBlocking: false });
await cache.mset(list);
const keys = list.map(({ key }) => key);
const deleteManyHandler = vi.spyOn(keyv, "deleteMany");

await expect(cache.mdel(keys)).resolves.toBe(true);

expect(deleteManyHandler).toHaveBeenCalledOnce();
expect(deleteManyHandler).toHaveBeenCalledWith(keys);
});

it("should call deleteMany once per store", async () => {
const secondKeyv = new Keyv();
const cache = createCache({
stores: [keyv, secondKeyv],
nonBlocking: false,
});
await cache.mset(list);
const keys = list.map(({ key }) => key);
const firstHandler = vi.spyOn(keyv, "deleteMany");
const secondHandler = vi.spyOn(secondKeyv, "deleteMany");

await cache.mdel(keys);

expect(firstHandler).toHaveBeenCalledOnce();
expect(firstHandler).toHaveBeenCalledWith(keys);
expect(secondHandler).toHaveBeenCalledOnce();
expect(secondHandler).toHaveBeenCalledWith(keys);
});

it("should resolve on an empty key list", async () => {
const cache = createCache({ stores: [keyv], nonBlocking: false });

await expect(cache.mdel([])).resolves.toBe(true);
});

it("should work blocking", async () => {
let resolveDeleted: (value: boolean) => void = () => undefined;
const deletePromise = new Promise<boolean>((_resolve) => {
Expand All @@ -41,15 +78,17 @@ describe("mdel", () => {
const cache = createCache({ stores: [keyv], nonBlocking: false });
await cache.mset(list);

const delHandler = vi.spyOn(keyv, "delete").mockReturnValue(deletePromise);
const delHandler = vi
.spyOn(keyv, "deleteMany")
.mockReturnValue(deletePromise);
const deleteResolved = vi.fn();
const deleteRejected = vi.fn();
cache
.mdel(list.map(({ key }) => key))
.catch(deleteRejected)
.then(deleteResolved);

expect(delHandler).toBeCalledTimes(list.length);
expect(delHandler).toHaveBeenCalledOnce();

await sleep(200);

Expand All @@ -70,15 +109,17 @@ describe("mdel", () => {
const cache = createCache({ stores: [keyv], nonBlocking: true });
await cache.mset(list);

const delHandler = vi.spyOn(keyv, "delete").mockReturnValue(deletePromise);
const delHandler = vi
.spyOn(keyv, "deleteMany")
.mockReturnValue(deletePromise);
const deleteResolved = vi.fn();
const deleteRejected = vi.fn();
cache
.mdel(list.map(({ key }) => key))
.catch(deleteRejected)
.then(deleteResolved);

expect(delHandler).toBeCalledTimes(list.length);
expect(delHandler).toHaveBeenCalledOnce();

await sleep(1);

Expand Down