diff --git a/packages/benchmark/cache-get-set.ts b/packages/benchmark/cache-get-set.ts index fa48dbd2..d2aad8ca 100644 --- a/packages/benchmark/cache-get-set.ts +++ b/packages/benchmark/cache-get-set.ts @@ -7,7 +7,7 @@ import { generateAlphaNumeric, getModuleName, printToConsole, -} from "index.js"; +} from "./src/index.js"; const bench = createBenchmark("Memory Cache Benchmark", 100000); diff --git a/packages/benchmark/memory-lru.ts b/packages/benchmark/memory-lru.ts index 1a1deae1..2e010643 100644 --- a/packages/benchmark/memory-lru.ts +++ b/packages/benchmark/memory-lru.ts @@ -1,12 +1,12 @@ import { CacheableMemory } from "@cacheable/memory"; +import { createLRU } from "lru.min"; +import QuickLRU from "quick-lru"; import { createBenchmark, generateAlphaNumeric, getModuleName, printToConsole, -} from "index.js"; -import { createLRU } from "lru.min"; -import QuickLRU from "quick-lru"; +} from "./src/index.js"; const bench = createBenchmark("Memory LRU Benchmark", 100000); diff --git a/packages/benchmark/memory.ts b/packages/benchmark/memory.ts index 3f9c1fc4..6061458d 100644 --- a/packages/benchmark/memory.ts +++ b/packages/benchmark/memory.ts @@ -1,13 +1,13 @@ import { CacheableMemory } from "@cacheable/memory"; import { BentoCache, bentostore } from "bentocache"; import { memoryDriver } from "bentocache/drivers/memory"; +import NodeCache from "node-cache"; import { createBenchmark, generateAlphaNumeric, getModuleName, printToConsole, -} from "index.js"; -import NodeCache from "node-cache"; +} from "./src/index.js"; const bench = createBenchmark("Memory Benchmark", 100000); diff --git a/packages/benchmark/node-cache.ts b/packages/benchmark/node-cache.ts index 7cab4861..cc44a54c 100644 --- a/packages/benchmark/node-cache.ts +++ b/packages/benchmark/node-cache.ts @@ -1,11 +1,11 @@ import { NodeCache as CacheableNodeCache } from "@cacheable/node-cache"; +import NodeCache from "node-cache"; import { createBenchmark, generateAlphaNumeric, getModuleName, printToConsole, -} from "index.js"; -import NodeCache from "node-cache"; +} from "./src/index.js"; const bench = createBenchmark("NodeCache Benchmark", 100000); diff --git a/packages/benchmark/src/index.ts b/packages/benchmark/src/index.ts index 5c0f0482..b43dbec4 100644 --- a/packages/benchmark/src/index.ts +++ b/packages/benchmark/src/index.ts @@ -1,9 +1,9 @@ // biome-ignore-all lint/suspicious/noExplicitAny: benchmarks import { faker } from "@faker-js/faker"; import { tinybenchPrinter } from "@monstermann/tinybench-pretty-printer"; -import { ObjectGenerator } from "object-generator.js"; import { Bench } from "tinybench"; import pkg from "../package.json" with { type: "json" }; +import { ObjectGenerator } from "./object-generator.js"; export function createBenchmark(name: string, iterations: number) { const bench = new Bench({ diff --git a/packages/benchmark/tsconfig.json b/packages/benchmark/tsconfig.json index 8a8df0ac..af893b26 100644 --- a/packages/benchmark/tsconfig.json +++ b/packages/benchmark/tsconfig.json @@ -3,7 +3,6 @@ "target": "ESNext", "module": "nodenext", "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */, - "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */, /* Emit */ "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, diff --git a/packages/cache-manager/README.md b/packages/cache-manager/README.md index a3029785..b65c2147 100644 --- a/packages/cache-manager/README.md +++ b/packages/cache-manager/README.md @@ -367,7 +367,7 @@ See unit tests in [`test/del.test.ts`](./test/del.test.ts) for more information. `mdel(keys: [key]): Promise` -Delete multiple keys, an error will be throw on any failed. +Delete multiple keys, an error will be throw on any failed. Cache manager makes one bulk deletion call per store, allowing adapters with native multi-delete support (such as `@keyv/redis`) to batch the work into fewer network round trips. ```ts await cache.mset([ diff --git a/packages/cache-manager/src/index.ts b/packages/cache-manager/src/index.ts index d6eec74d..8ebc1a80 100644 --- a/packages/cache-manager/src/index.ts +++ b/packages/cache-manager/src/index.ts @@ -80,6 +80,7 @@ export type Events = { error?: unknown; }) => void; del: (data: { key: string; error?: unknown }) => void; + mdel: (data: { keys: string[]; error?: unknown }) => void; clear: (error?: unknown) => void; refresh: (data: { key: string; value: T; error?: unknown }) => void; }; @@ -289,11 +290,13 @@ export const createCache = (options?: CreateCacheOptions): Cache => { const mdel = async (keys: string[]) => { try { - const promises: Array> = []; - for (const key of keys) { - promises.push(...stores.map(async (store) => store.delete(key))); + if (keys.length === 0) { + eventEmitter.emit("mdel", { keys }); + return true; } + const promises = stores.map(async (store) => store.deleteMany(keys)); + if (nonBlocking) { Promise.all(promises); eventEmitter.emit("mdel", { keys }); diff --git a/packages/cache-manager/test/mdel.test.ts b/packages/cache-manager/test/mdel.test.ts index 97e6962b..535550c0 100644 --- a/packages/cache-manager/test/mdel.test.ts +++ b/packages/cache-manager/test/mdel.test.ts @@ -1,4 +1,5 @@ import { faker } from "@faker-js/faker"; +import { createKeyv } from "@keyv/redis"; import { Keyv } from "keyv"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createCache } from "../src/index.js"; @@ -33,6 +34,78 @@ 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 not dispatch an empty key list to Redis", async () => { + const redis = createKeyv(); + const cache = createCache({ stores: [redis], nonBlocking: false }); + const deleteManyHandler = vi.spyOn(redis, "deleteMany"); + const listener = vi.fn(); + cache.on("mdel", listener); + + try { + await expect(cache.mdel([])).resolves.toBe(true); + expect(deleteManyHandler).not.toHaveBeenCalled(); + expect(listener).toHaveBeenCalledOnce(); + expect(listener).toHaveBeenCalledWith({ keys: [] }); + } finally { + await redis.disconnect(); + } + }); + + it("should delete keys through the native Redis deleteMany", async () => { + const redis = createKeyv(); + redis.throwOnErrors = true; + const cache = createCache({ stores: [redis], nonBlocking: false }); + + try { + await cache.mset(list); + const nativeDeleteManyHandler = vi.spyOn(redis.store, "deleteMany"); + const keys = [list[0].key, list[1].key]; + + await expect(cache.mdel(keys)).resolves.toBe(true); + + expect(nativeDeleteManyHandler).toHaveBeenCalledOnce(); + await expect(redis.get(list.map(({ key }) => key))).resolves.toEqual([ + undefined, + undefined, + list[2].value, + ]); + } finally { + await Promise.allSettled(list.map(async ({ key }) => redis.delete(key))); + await redis.disconnect(); + } + }); + it("should work blocking", async () => { let resolveDeleted: (value: boolean) => void = () => undefined; const deletePromise = new Promise((_resolve) => { @@ -41,7 +114,9 @@ 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 @@ -49,7 +124,7 @@ describe("mdel", () => { .catch(deleteRejected) .then(deleteResolved); - expect(delHandler).toBeCalledTimes(list.length); + expect(delHandler).toHaveBeenCalledOnce(); await sleep(200); @@ -70,7 +145,9 @@ 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 @@ -78,7 +155,7 @@ describe("mdel", () => { .catch(deleteRejected) .then(deleteResolved); - expect(delHandler).toBeCalledTimes(list.length); + expect(delHandler).toHaveBeenCalledOnce(); await sleep(1); diff --git a/packages/cache-manager/tsconfig.json b/packages/cache-manager/tsconfig.json index d81c7e61..35a98d34 100644 --- a/packages/cache-manager/tsconfig.json +++ b/packages/cache-manager/tsconfig.json @@ -2,8 +2,7 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ /* Emit */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ diff --git a/packages/flat-cache/tsconfig.json b/packages/flat-cache/tsconfig.json index d6359664..35a98d34 100644 --- a/packages/flat-cache/tsconfig.json +++ b/packages/flat-cache/tsconfig.json @@ -2,8 +2,7 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ /* Emit */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ @@ -23,4 +22,4 @@ "ESNext", "DOM" ] } -} \ No newline at end of file +} diff --git a/packages/memory/tsconfig.json b/packages/memory/tsconfig.json index d6359664..35a98d34 100644 --- a/packages/memory/tsconfig.json +++ b/packages/memory/tsconfig.json @@ -2,8 +2,7 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ /* Emit */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ @@ -23,4 +22,4 @@ "ESNext", "DOM" ] } -} \ No newline at end of file +} diff --git a/packages/net/tsconfig.json b/packages/net/tsconfig.json index d6359664..35a98d34 100644 --- a/packages/net/tsconfig.json +++ b/packages/net/tsconfig.json @@ -2,8 +2,7 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ /* Emit */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ @@ -23,4 +22,4 @@ "ESNext", "DOM" ] } -} \ No newline at end of file +} diff --git a/packages/node-cache/src/store.ts b/packages/node-cache/src/store.ts index f74572a6..de485ea5 100644 --- a/packages/node-cache/src/store.ts +++ b/packages/node-cache/src/store.ts @@ -1,7 +1,7 @@ import { Stats, shorthandToMilliseconds } from "@cacheable/utils"; import { Hookified } from "hookified"; -import type { NodeCacheStats, PartialNodeCacheItem } from "index.js"; import Keyv from "keyv"; +import type { NodeCacheStats, PartialNodeCacheItem } from "./index.js"; export type NodeCacheStoreOptions = { /** diff --git a/packages/node-cache/tsconfig.json b/packages/node-cache/tsconfig.json index d6359664..35a98d34 100644 --- a/packages/node-cache/tsconfig.json +++ b/packages/node-cache/tsconfig.json @@ -2,8 +2,7 @@ "compilerOptions": { "target": "ESNext", "module": "ESNext", - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + "moduleResolution": "bundler", /* Specify how TypeScript looks up a file from a given module specifier. */ /* Emit */ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ @@ -23,4 +22,4 @@ "ESNext", "DOM" ] } -} \ No newline at end of file +}