@warlock.js/cache 5.16.0 → 5.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"mock-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/mock-cache-driver.ts"],"sourcesContent":["import type {\n CacheCall,\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheTtl,\n MockCacheOptions,\n} from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/**\n * In-memory cache driver with introspection helpers, intended for use as a\n * test double in downstream packages.\n *\n * **Role.** Drop-in replacement for any real driver in test setups, with\n * extra surface that makes behavioral assertions easy: every public op gets\n * recorded into {@link MockCacheDriver.callLog}, and {@link wasCalled} /\n * {@link getStored} / {@link reset} let tests verify side effects without\n * pulling in a real Redis / Postgres / file system.\n *\n * **Responsibility.**\n * - Owns: in-memory storage backed by a `Map`, the `callLog`, TTL handling\n * via `parseCachedData`, `onConflict` policies, and tag-index storage.\n * - Does NOT own: similarity retrieval (vectors are recorded into the\n * `callLog` but `similar()` throws — use `MemoryCacheDriver` for tests\n * that need real nearest-neighbor scoring), connection lifecycle\n * (no-op `connect`/`disconnect`), or eviction (no `maxSize`).\n *\n * Register it like any other driver — sub-paths are not part of the\n * package's export convention; the same single barrel ships it next to\n * the production drivers.\n *\n * @example\n * import { cache, MockCacheDriver } from \"@warlock.js/cache\";\n *\n * beforeEach(async () => {\n * cache.setCacheConfigurations({\n * default: \"mock\",\n * drivers: { mock: MockCacheDriver },\n * options: { mock: {} },\n * });\n * await cache.init();\n * });\n *\n * it(\"invalidates the user cache after update\", async () => {\n * await userService.update(42, { name: \"Jane\" });\n *\n * const driver = cache.currentDriver as MockCacheDriver;\n * expect(driver.wasCalled(\"remove\", \"users.42\")).toBe(true);\n * });\n */\nexport class MockCacheDriver\n extends BaseCacheDriver<MockCacheDriver, MockCacheOptions>\n implements CacheDriver<MockCacheDriver, MockCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"mock\";\n\n /**\n * {@inheritdoc}\n */\n public options: MockCacheOptions = {};\n\n /**\n * Storage backing the mock — keyed by post-`parseKey` string. Public-readonly\n * so tests can introspect raw entries when {@link getStored} isn't enough.\n */\n public readonly storage: Map<string, CacheData> = new Map();\n\n /**\n * Ordered record of every public operation routed through this driver.\n * Pushed to before each op runs; tests assert via {@link wasCalled} or by\n * inspecting the array directly.\n */\n public readonly callLog: CacheCall[] = [];\n\n /**\n * Standard driver setup. Mirrors the null driver — no connection, no\n * resources to release.\n */\n public async connect(): Promise<void> {\n this.recordCall(\"connect\", undefined);\n await super.connect();\n }\n\n /**\n * Standard driver teardown.\n */\n public async disconnect(): Promise<void> {\n this.recordCall(\"disconnect\", undefined);\n await super.disconnect();\n }\n\n /**\n * Wipe everything under `namespace`. Matches `MemoryCacheDriver` semantics\n * — the namespace itself and any key with the namespace prefix is removed.\n */\n public async removeNamespace(namespace: string): Promise<void> {\n const parsed = this.parseKey(namespace);\n\n this.recordCall(\"removeNamespace\", parsed, [namespace]);\n this.log(\"clearing\", parsed);\n\n const prefix = parsed + \".\";\n\n for (const key of [...this.storage.keys()]) {\n if (key === parsed || key.startsWith(prefix)) {\n this.storage.delete(key);\n }\n }\n\n this.log(\"cleared\", parsed);\n }\n\n /**\n * Standard `set` with full `onConflict` support. Honors scope-default TTL\n * via {@link BaseCacheDriver.resolveSetOptions}.\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n this.recordCall(\"set\", parsedKey, [value, ttlOrOptions]);\n this.log(\"caching\", parsedKey);\n\n const existing = onConflict === \"upsert\" ? null : await this.get(key);\n const exists = existing !== null;\n\n if (onConflict === \"create\" && exists) {\n const result: CacheSetResult = { wasSet: false, existing };\n\n return result;\n }\n\n if (onConflict === \"update\" && !exists) {\n const result: CacheSetResult = { wasSet: false, existing: null };\n\n return result;\n }\n\n const data = this.prepareDataForStorage(value, ttl, staleAt);\n this.storage.set(parsedKey, data);\n\n if (tags && tags.length > 0) {\n await this.applyTags(parsedKey, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n\n if (onConflict === \"create\" || onConflict === \"update\") {\n const result: CacheSetResult = { wasSet: true, existing: null };\n\n return result;\n }\n\n return value;\n }\n\n /**\n * Standard `get` with TTL handling. Emits `hit` / `miss` events to keep\n * downstream metrics tests realistic.\n */\n public async get<T = any>(key: CacheKey): Promise<T | null> {\n const parsedKey = this.parseKey(key);\n\n this.recordCall(\"get\", parsedKey);\n this.log(\"fetching\", parsedKey);\n\n const data = this.storage.get(parsedKey);\n\n if (!data) {\n this.log(\"notFound\", parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n const value = await this.parseCachedData(parsedKey, data);\n\n if (value === null) {\n // expired — the parse helper already logged + queued the cleanup\n this.storage.delete(parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n await this.emit(\"hit\", { key: parsedKey, value });\n\n return value as T;\n }\n\n /**\n * Read the raw {@link CacheData} wrapper from the in-memory `Map`,\n * including `staleAt` metadata. Returns `null` for missing or expired\n * entries — `swr()` consumes this to branch on freshness.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const entry = this.storage.get(parsedKey);\n\n if (!entry) {\n return null;\n }\n\n if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {\n return null;\n }\n\n return entry;\n }\n\n /**\n * Standard `remove` — drops the entry and emits the `removed` event.\n */\n public async remove(key: CacheKey): Promise<void> {\n const parsedKey = this.parseKey(key);\n\n this.recordCall(\"remove\", parsedKey);\n this.log(\"removing\", parsedKey);\n\n this.storage.delete(parsedKey);\n\n this.log(\"removed\", parsedKey);\n await this.emit(\"removed\", { key: parsedKey });\n }\n\n /**\n * Standard `flush` — wipes the entire mock store + tag index. Does NOT\n * touch the call log; use {@link reset} to clear that as well.\n */\n public async flush(): Promise<void> {\n this.recordCall(\"flush\", undefined);\n this.log(\"flushing\");\n\n this.storage.clear();\n\n this.log(\"flushed\");\n await this.emit(\"flushed\");\n }\n\n /**\n * Was a given operation invoked? When `key` is provided, the match is\n * post-`parseKey` so callers pass the same key shape they used at the\n * call site — strings or objects, both resolve to the same parsed key.\n *\n * @example\n * driver.wasCalled(\"set\"); // any set\n * driver.wasCalled(\"set\", \"users.42\"); // set on this specific key\n * driver.wasCalled(\"set\", { id: 42 }); // same — object key normalized\n */\n public wasCalled(operation: string, key?: CacheKey): boolean {\n if (key === undefined) {\n return this.callLog.some((call) => call.operation === operation);\n }\n\n const parsedKey = this.parseKey(key);\n\n return this.callLog.some(\n (call) => call.operation === operation && call.key === parsedKey,\n );\n }\n\n /**\n * Return the raw stored value for `key`, bypassing TTL handling and clone\n * protection. Useful when a test wants to assert on the persisted shape\n * (or assert that an entry expired without going through `get`).\n *\n * Returns `undefined` when the key isn't present.\n */\n public getStored<T = any>(key: CacheKey): T | undefined {\n const parsedKey = this.parseKey(key);\n const entry = this.storage.get(parsedKey);\n\n if (!entry) {\n return undefined;\n }\n\n return entry.data as T;\n }\n\n /**\n * Wipe everything — storage, tag index, and the call log. Pair with\n * Vitest's `beforeEach` to get clean isolation between tests.\n */\n public reset(): void {\n this.storage.clear();\n this.callLog.length = 0;\n }\n\n /**\n * Append a row to {@link callLog}. Internal helper called by every\n * recorded op before the actual work runs.\n */\n protected recordCall(\n operation: string,\n key: string | undefined,\n args: unknown[] = [],\n ): void {\n this.callLog.push({\n operation,\n key,\n args,\n timestamp: Date.now(),\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mock-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/mock-cache-driver.ts"],"sourcesContent":["import type {\n CacheCall,\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheTtl,\n MockCacheOptions,\n} from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/**\n * In-memory cache driver with introspection helpers, intended for use as a\n * test double in downstream packages.\n *\n * **Role.** Drop-in replacement for any real driver in test setups, with\n * extra surface that makes behavioral assertions easy: every public op gets\n * recorded into {@link MockCacheDriver.callLog}, and {@link wasCalled} /\n * {@link getStored} / {@link reset} let tests verify side effects without\n * pulling in a real Redis / Postgres / file system.\n *\n * **Responsibility.**\n * - Owns: in-memory storage backed by a `Map`, the `callLog`, TTL handling\n * via `parseCachedData`, `onConflict` policies, and tag-index storage.\n * - Does NOT own: similarity retrieval (vectors are recorded into the\n * `callLog` but `similar()` throws — use `MemoryCacheDriver` for tests\n * that need real nearest-neighbor scoring), connection lifecycle\n * (no-op `connect`/`disconnect`), or eviction (no `maxSize`).\n *\n * Register it like any other driver — sub-paths are not part of the\n * package's export convention; the same single barrel ships it next to\n * the production drivers.\n *\n * @example\n * import { cache, MockCacheDriver } from \"@warlock.js/cache\";\n *\n * beforeEach(async () => {\n * cache.setCacheConfigurations({\n * default: \"mock\",\n * drivers: { mock: MockCacheDriver },\n * options: { mock: {} },\n * });\n * await cache.init();\n * });\n *\n * it(\"invalidates the user cache after update\", async () => {\n * await userService.update(42, { name: \"Jane\" });\n *\n * const driver = cache.currentDriver as MockCacheDriver;\n * expect(driver.wasCalled(\"remove\", \"users.42\")).toBe(true);\n * });\n */\nexport class MockCacheDriver\n extends BaseCacheDriver<MockCacheDriver, MockCacheOptions>\n implements CacheDriver<MockCacheDriver, MockCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"mock\";\n\n /**\n * {@inheritdoc}\n */\n public options: MockCacheOptions = {};\n\n /**\n * Storage backing the mock — keyed by post-`parseKey` string. Public-readonly\n * so tests can introspect raw entries when {@link getStored} isn't enough.\n */\n public readonly storage: Map<string, CacheData> = new Map();\n\n /**\n * Ordered record of every public operation routed through this driver.\n * Pushed to before each op runs; tests assert via {@link wasCalled} or by\n * inspecting the array directly.\n */\n public readonly callLog: CacheCall[] = [];\n\n /**\n * Standard driver setup. Mirrors the null driver — no connection, no\n * resources to release.\n */\n public async connect(): Promise<void> {\n this.recordCall(\"connect\", undefined);\n await super.connect();\n }\n\n /**\n * Standard driver teardown.\n */\n public async disconnect(): Promise<void> {\n this.recordCall(\"disconnect\", undefined);\n await super.disconnect();\n }\n\n /**\n * Wipe everything under `namespace`. Matches `MemoryCacheDriver` semantics\n * — the namespace itself and any key with the namespace prefix is removed.\n */\n public async removeNamespace(namespace: string): Promise<void> {\n const parsed = this.parseKey(namespace);\n\n this.recordCall(\"removeNamespace\", parsed, [namespace]);\n this.log(\"clearing\", parsed);\n\n const prefix = parsed + \".\";\n\n for (const key of [...this.storage.keys()]) {\n if (key === parsed || key.startsWith(prefix)) {\n this.storage.delete(key);\n }\n }\n\n this.log(\"cleared\", parsed);\n }\n\n /**\n * Standard `set` with full `onConflict` support. Honors scope-default TTL\n * via {@link BaseCacheDriver.resolveSetOptions}.\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n this.recordCall(\"set\", parsedKey, [value, ttlOrOptions]);\n this.log(\"caching\", parsedKey);\n\n const existing = onConflict === \"upsert\" ? null : await this.get(key);\n const exists = existing !== null;\n\n if (onConflict === \"create\" && exists) {\n const result: CacheSetResult = { wasSet: false, existing };\n\n return result;\n }\n\n if (onConflict === \"update\" && !exists) {\n const result: CacheSetResult = { wasSet: false, existing: null };\n\n return result;\n }\n\n const data = this.prepareDataForStorage(value, ttl, staleAt);\n this.storage.set(parsedKey, data);\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n\n if (onConflict === \"create\" || onConflict === \"update\") {\n const result: CacheSetResult = { wasSet: true, existing: null };\n\n return result;\n }\n\n return value;\n }\n\n /**\n * Standard `get` with TTL handling. Emits `hit` / `miss` events to keep\n * downstream metrics tests realistic.\n */\n public async get<T = any>(key: CacheKey): Promise<T | null> {\n const parsedKey = this.parseKey(key);\n\n this.recordCall(\"get\", parsedKey);\n this.log(\"fetching\", parsedKey);\n\n const data = this.storage.get(parsedKey);\n\n if (!data) {\n this.log(\"notFound\", parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n const value = await this.parseCachedData(parsedKey, data);\n\n if (value === null) {\n // expired — the parse helper already logged + queued the cleanup\n this.storage.delete(parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n await this.emit(\"hit\", { key: parsedKey, value });\n\n return value as T;\n }\n\n /**\n * Read the raw {@link CacheData} wrapper from the in-memory `Map`,\n * including `staleAt` metadata. Returns `null` for missing or expired\n * entries — `swr()` consumes this to branch on freshness.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const entry = this.storage.get(parsedKey);\n\n if (!entry) {\n return null;\n }\n\n if (entry.expiresAt !== undefined && entry.expiresAt <= Date.now()) {\n return null;\n }\n\n return entry;\n }\n\n /**\n * Standard `remove` — drops the entry and emits the `removed` event.\n */\n public async remove(key: CacheKey): Promise<void> {\n const parsedKey = this.parseKey(key);\n\n this.recordCall(\"remove\", parsedKey);\n this.log(\"removing\", parsedKey);\n\n this.storage.delete(parsedKey);\n\n this.log(\"removed\", parsedKey);\n await this.emit(\"removed\", { key: parsedKey });\n }\n\n /**\n * Standard `flush` — wipes the entire mock store + tag index. Does NOT\n * touch the call log; use {@link reset} to clear that as well.\n */\n public async flush(): Promise<void> {\n this.recordCall(\"flush\", undefined);\n this.log(\"flushing\");\n\n this.storage.clear();\n\n this.log(\"flushed\");\n await this.emit(\"flushed\");\n }\n\n /**\n * Was a given operation invoked? When `key` is provided, the match is\n * post-`parseKey` so callers pass the same key shape they used at the\n * call site — strings or objects, both resolve to the same parsed key.\n *\n * @example\n * driver.wasCalled(\"set\"); // any set\n * driver.wasCalled(\"set\", \"users.42\"); // set on this specific key\n * driver.wasCalled(\"set\", { id: 42 }); // same — object key normalized\n */\n public wasCalled(operation: string, key?: CacheKey): boolean {\n if (key === undefined) {\n return this.callLog.some((call) => call.operation === operation);\n }\n\n const parsedKey = this.parseKey(key);\n\n return this.callLog.some(\n (call) => call.operation === operation && call.key === parsedKey,\n );\n }\n\n /**\n * Return the raw stored value for `key`, bypassing TTL handling and clone\n * protection. Useful when a test wants to assert on the persisted shape\n * (or assert that an entry expired without going through `get`).\n *\n * Returns `undefined` when the key isn't present.\n */\n public getStored<T = any>(key: CacheKey): T | undefined {\n const parsedKey = this.parseKey(key);\n const entry = this.storage.get(parsedKey);\n\n if (!entry) {\n return undefined;\n }\n\n return entry.data as T;\n }\n\n /**\n * Wipe everything — storage, tag index, and the call log. Pair with\n * Vitest's `beforeEach` to get clean isolation between tests.\n */\n public reset(): void {\n this.storage.clear();\n this.callLog.length = 0;\n }\n\n /**\n * Append a row to {@link callLog}. Internal helper called by every\n * recorded op before the actual work runs.\n */\n protected recordCall(\n operation: string,\n key: string | undefined,\n args: unknown[] = [],\n ): void {\n this.callLog.push({\n operation,\n key,\n args,\n timestamp: Date.now(),\n });\n }\n}\n"],"mappings":"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"}
@@ -215,7 +215,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
215
215
  wasSet: false,
216
216
  existing: await this.get(key)
217
217
  };
218
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
218
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
219
219
  this.log("cached", parsedKey);
220
220
  await this.emit("set", {
221
221
  key: parsedKey,
@@ -236,7 +236,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
236
236
  wasSet: false,
237
237
  existing: null
238
238
  };
239
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
239
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
240
240
  this.log("cached", parsedKey);
241
241
  await this.emit("set", {
242
242
  key: parsedKey,
@@ -252,7 +252,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
252
252
  VALUES (${valList})
253
253
  ON CONFLICT (key) DO UPDATE
254
254
  SET ${setClause}`, params);
255
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
255
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
256
256
  this.log("cached", parsedKey);
257
257
  await this.emit("set", {
258
258
  key: parsedKey,
@@ -1 +1 @@
1
- {"version":3,"file":"pg-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/pg-cache-driver.ts"],"sourcesContent":["import type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheSimilarHit,\n CacheSimilarOptions,\n CacheTtl,\n PgCacheOptions,\n PgClientLike,\n} from \"../types\";\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/**\n * Allowed characters in a Postgres identifier (table name). We accept the\n * conservative ASCII subset and reject anything else — interpolating an\n * arbitrary string into DDL would be a SQL-injection footgun.\n */\nconst SAFE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Postgres cache driver with optional pgvector similarity support.\n *\n * Connection lifecycle is the caller's responsibility — pass an already-built\n * `pg.Pool` or `pg.Client` via the `client` option. The driver never closes it\n * on `cache.disconnect()`, so the same pool can serve queries elsewhere in\n * the app.\n *\n * Schema is not auto-migrated. Call `driver.schema()` to get the DDL string\n * and run it through whichever migration tool you use.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { cache, PgCacheDriver } from \"@warlock.js/cache\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n *\n * cache.setCacheConfigurations({\n * default: \"pg\",\n * drivers: { pg: PgCacheDriver },\n * options: { pg: { client: pool, table: \"warlock_cache\", ttl: \"1h\" } },\n * });\n *\n * await cache.init();\n *\n * // Run once, via your own migration tooling:\n * // await pool.query(driver.schema());\n */\nexport class PgCacheDriver\n extends BaseCacheDriver<PgClientLike, PgCacheOptions>\n implements CacheDriver<PgClientLike, PgCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"pg\";\n\n /**\n * Cached result of the pgvector extension check. Populated lazily on first\n * vector op so the driver doesn't probe the database at construction time.\n */\n protected vectorReady?: boolean;\n\n /**\n * {@inheritdoc}\n *\n * Validates `client` presence and the table name before storing options.\n */\n public setOptions(options: PgCacheOptions) {\n if (!options || !options.client || typeof options.client.query !== \"function\") {\n throw new CacheConfigurationError(\n \"Pg cache driver requires a 'client' option implementing { query(text, values) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n const table = options.table ?? \"warlock_cache\";\n if (!SAFE_IDENT.test(table)) {\n throw new CacheConfigurationError(\n `Pg cache driver: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n if (options.vector) {\n const dim = options.vector.dimensions;\n if (!Number.isInteger(dim) || dim <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.dimensions must be a positive integer; got ${dim}.`,\n );\n }\n const idx = options.vector.index ?? \"hnsw\";\n if (idx !== \"hnsw\" && idx !== \"ivfflat\") {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.index must be 'hnsw' or 'ivfflat'; got '${idx}'.`,\n );\n }\n }\n\n return super.setOptions({ ...options, table });\n }\n\n /**\n * Lazy pgvector availability check. Runs once on the first vector op and\n * caches the result — subsequent ops are zero-overhead. Throws\n * {@link CacheConfigurationError} if the extension isn't installed; throws\n * {@link CacheUnsupportedError} if `vector` config wasn't provided at all.\n */\n protected async ensureVectorReady(): Promise<void> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n if (this.vectorReady === true) {\n return;\n }\n\n const { rows } = await this.pgClient.query(\n `SELECT 1 FROM pg_extension WHERE extname = 'vector'`,\n );\n\n if (rows.length === 0) {\n throw new CacheConfigurationError(\n \"'pg' driver: pgvector extension not installed. Run 'CREATE EXTENSION vector;' or remove the 'vector' config option.\",\n );\n }\n\n this.vectorReady = true;\n }\n\n /**\n * Format a numeric vector for pgvector ingestion. The `vector` type accepts\n * a string literal `'[1,2,3]'` cast via `::vector` — this avoids depending\n * on the binary protocol and works against any pg client.\n */\n protected formatVector(vector: number[]): string {\n return `[${vector.join(\",\")}]`;\n }\n\n /**\n * Resolved table name. Always defined post-`setOptions` — the validator\n * fills in the default.\n */\n protected get table(): string {\n return this.options.table ?? \"warlock_cache\";\n }\n\n /**\n * The user-supplied `pg.Pool` / `pg.Client`. Use this rather than `this.client`\n * (which has a generic fallback to `this`) for actual queries.\n */\n protected get pgClient(): PgClientLike {\n return this.options.client;\n }\n\n /**\n * Compute an absolute `expires_at` Date for the given relative TTL in seconds,\n * or `null` when the entry should not expire (`Infinity` / 0 / undefined).\n */\n protected ttlToExpiresAt(ttl?: number): Date | null {\n if (!ttl || ttl === Infinity) {\n return null;\n }\n\n return new Date(Date.now() + ttl * 1000);\n }\n\n /**\n * Return the SQL needed to provision the cache table + index. Run once via\n * the caller's migration tooling — the driver never auto-migrates.\n *\n * @example\n * await pool.query(driver.schema());\n */\n public schema(): string {\n const t = this.table;\n const vec = this.options.vector;\n\n const columns = [\n ` key TEXT PRIMARY KEY,`,\n ` value JSONB NOT NULL,`,\n ` expires_at TIMESTAMPTZ,`,\n ` stale_at TIMESTAMPTZ,`,\n ` tags TEXT[] NOT NULL DEFAULT '{}'::TEXT[]`,\n ];\n\n if (vec) {\n // Trailing comma on the previous line to make this a valid column list.\n columns[columns.length - 1] = columns[columns.length - 1] + \",\";\n columns.push(` embedding VECTOR(${vec.dimensions})`);\n }\n\n const lines = [\n `CREATE TABLE IF NOT EXISTS ${t} (`,\n ...columns,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_expires_at ON ${t} (expires_at);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_tags ON ${t} USING GIN (tags);`,\n ];\n\n if (vec) {\n const idx = vec.index ?? \"hnsw\";\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${t}_embedding ON ${t} USING ${idx} (embedding vector_cosine_ops);`,\n );\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async connect() {\n // No-op — caller owns the connection. We emit `connected` for symmetry\n // with the other drivers' lifecycle events.\n this.log(\"connecting\");\n this.log(\"connected\");\n await this.emit(\"connected\");\n }\n\n /**\n * {@inheritdoc}\n *\n * Does NOT close the user-supplied client — lifecycle stays with the caller.\n */\n public async disconnect() {\n this.log(\"disconnected\");\n await this.emit(\"disconnected\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: cannot index a vector without options.vector configuration — set { dimensions } and recreate the table via driver.schema().\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n await this.ensureVectorReady();\n }\n\n this.log(\"caching\", parsedKey);\n\n const expiresAt = this.ttlToExpiresAt(ttl);\n const staleAtDate = staleAt !== undefined ? new Date(staleAt) : null;\n const tagsArr = tags ?? [];\n const serialized = JSON.stringify(value);\n const vecLiteral = vector ? this.formatVector(vector) : null;\n\n const t = this.table;\n\n // Build column / placeholder / param triplets dynamically so the same code\n // path serves both KV-only and vector-aware writes. Param order is fixed:\n // $1 = key, $2 = value (jsonb), $3 = expires_at, $4 = stale_at,\n // $5 = tags, $6 = embedding::vector (only present when vecLiteral !== null).\n const cols = [\"key\", \"value\", \"expires_at\", \"stale_at\", \"tags\"];\n const placeholders = [\"$1\", \"$2::jsonb\", \"$3\", \"$4\", \"$5\"];\n const params: unknown[] = [parsedKey, serialized, expiresAt, staleAtDate, tagsArr];\n if (vecLiteral !== null) {\n cols.push(\"embedding\");\n placeholders.push(`$${params.length + 1}::vector`);\n params.push(vecLiteral);\n }\n const colList = cols.join(\", \");\n const valList = placeholders.join(\", \");\n const setClause = cols\n .slice(1)\n .map((c) => `${c} = EXCLUDED.${c}`)\n .join(\", \");\n const updateSetClause = cols\n .slice(1)\n .map((c, i) => `${c} = ${placeholders[i + 1]}`)\n .join(\", \");\n\n if (onConflict === \"create\") {\n // Race-safe insert: if another worker already holds the key (and the row\n // hasn't expired), DO NOTHING; we then SELECT to surface the existing value.\n const { rows } = await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}\n WHERE ${t}.expires_at IS NOT NULL AND ${t}.expires_at < now()\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n // Conflict + existing row not expired → fetch existing for the result.\n const existing = await this.get(key);\n return { wasSet: false, existing } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(parsedKey, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n if (onConflict === \"update\") {\n // Update only when the key exists AND hasn't expired.\n const { rows } = await this.pgClient.query(\n `UPDATE ${t}\n SET ${updateSetClause}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n return { wasSet: false, existing: null } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(parsedKey, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n // upsert (default)\n await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}`,\n params,\n );\n\n if (tags && tags.length > 0) {\n await this.applyTags(parsedKey, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return value;\n }\n\n /**\n * {@inheritdoc}\n */\n public async get(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"fetching\", parsedKey);\n\n const t = this.table;\n const { rows } = await this.pgClient.query(\n `SELECT value FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n this.log(\"notFound\", parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n return null;\n }\n\n this.log(\"fetched\", parsedKey);\n\n // pg's JSONB type round-trips through node-postgres as a parsed JS value\n // already — but some pool implementations may hand back a string. Be defensive.\n let value = rows[0].value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Leave it as-is; cloning below will fail loud if it's truly broken.\n }\n }\n\n if (value === null || value === undefined) {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n const type = typeof value;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n try {\n const cloned = structuredClone(value);\n await this.emit(\"hit\", { key: parsedKey, value: cloned });\n return cloned;\n } catch (error) {\n this.logError(`Failed to clone cached value for ${parsedKey}`, error);\n throw error;\n }\n }\n\n /**\n * Read the raw {@link CacheData} wrapper, including `staleAt` metadata.\n * Returns `null` for missing or expired rows — `swr()` consumes this to\n * branch on freshness without going through `get()`'s clone-and-emit path.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n\n const { rows } = await this.pgClient.query(\n `SELECT value, expires_at, stale_at FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n return null;\n }\n\n let data = rows[0].value;\n if (typeof data === \"string\") {\n try {\n data = JSON.parse(data);\n } catch {\n // Leave as-is — getEntry is metadata-shaped, the caller will hit the\n // same parse path on the public `get()` if they care.\n }\n }\n\n const entry: CacheData = { data };\n\n const expiresAtRaw = rows[0].expires_at as Date | string | null;\n if (expiresAtRaw) {\n const expiresAt = expiresAtRaw instanceof Date ? expiresAtRaw : new Date(expiresAtRaw);\n entry.expiresAt = expiresAt.getTime();\n }\n\n const staleAtRaw = rows[0].stale_at as Date | string | null;\n if (staleAtRaw) {\n const staleAt = staleAtRaw instanceof Date ? staleAtRaw : new Date(staleAtRaw);\n entry.staleAt = staleAt.getTime();\n }\n\n return entry;\n }\n\n /**\n * {@inheritdoc}\n */\n public async remove(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"removing\", parsedKey);\n\n const t = this.table;\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1`, [parsedKey]);\n\n this.log(\"removed\", parsedKey);\n await this.emit(\"removed\", { key: parsedKey });\n }\n\n /**\n * {@inheritdoc}\n *\n * Deletes all rows whose key equals the namespace exactly or starts with\n * `<namespace>.` — same boundary semantics as the other drivers.\n */\n public async removeNamespace(namespace: string) {\n const parsed = this.parseKey(namespace);\n this.log(\"clearing\", parsed || \"(all)\");\n\n const t = this.table;\n\n if (parsed === \"\") {\n await this.pgClient.query(`DELETE FROM ${t}`);\n } else {\n // Escape `_` and `%` in the prefix so they aren't treated as LIKE wildcards.\n const escaped = parsed.replace(/\\\\/g, \"\\\\\\\\\").replace(/_/g, \"\\\\_\").replace(/%/g, \"\\\\%\");\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1 OR key LIKE $2 ESCAPE '\\\\'`, [\n parsed,\n `${escaped}.%`,\n ]);\n }\n\n this.log(\"cleared\", parsed || \"(all)\");\n return this;\n }\n\n /**\n * {@inheritdoc}\n *\n * Honors `globalPrefix` — when configured, scopes the flush to entries\n * under the prefix rather than truncating the entire table (which could\n * wipe sibling tenants sharing the same Postgres database).\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await this.pgClient.query(`DELETE FROM ${this.table}`);\n }\n\n this.log(\"flushed\");\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritdoc}\n *\n * pgvector-backed similarity. Uses the `<=>` cosine-distance operator\n * (lower distance = higher similarity) and converts to cosine similarity\n * as `1 - distance` so the returned `score` matches the rest of the\n * package (`[0, 1]`, higher is more similar).\n *\n * Honors `topK`, `threshold`, and an optional `tags` filter (native\n * `tags && $tags` overlap query — much faster than the meta-key path).\n *\n * Throws {@link CacheUnsupportedError} when `options.vector` was not\n * configured at driver setup; throws {@link CacheConfigurationError} when\n * the pgvector extension is missing or the query vector's dimension count\n * doesn't match the configured one.\n */\n public async similar<T = any>(\n vector: number[],\n options: CacheSimilarOptions,\n ): Promise<CacheSimilarHit<T>[]> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n if (!Number.isInteger(options.topK) || options.topK <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: similar.topK must be a positive integer; got ${options.topK}.`,\n );\n }\n\n await this.ensureVectorReady();\n\n const t = this.table;\n const vecLiteral = this.formatVector(vector);\n const params: unknown[] = [vecLiteral];\n let tagFilter = \"\";\n\n if (options.tags && options.tags.length > 0) {\n params.push(options.tags);\n tagFilter = `AND tags && $${params.length}`;\n }\n\n params.push(options.topK);\n const topKParam = `$${params.length}`;\n\n const { rows } = await this.pgClient.query(\n `SELECT key, value, 1 - (embedding <=> $1::vector) AS score\n FROM ${t}\n WHERE embedding IS NOT NULL\n AND (expires_at IS NULL OR expires_at > now())\n ${tagFilter}\n ORDER BY embedding <=> $1::vector\n LIMIT ${topKParam}`,\n params,\n );\n\n const hits: CacheSimilarHit<T>[] = [];\n for (const row of rows) {\n const score = Number(row.score);\n if (options.threshold !== undefined && score < options.threshold) {\n continue;\n }\n\n let value = row.value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Surface as-is; if non-JSON crept in, the consumer will notice.\n }\n }\n\n // Match get() cloning semantics so consumers can't mutate cached state.\n if (value !== null && value !== undefined) {\n const ty = typeof value;\n if (ty !== \"string\" && ty !== \"number\" && ty !== \"boolean\") {\n value = structuredClone(value);\n }\n }\n\n hits.push({ key: row.key, value, score });\n }\n\n return hits;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pg-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/pg-cache-driver.ts"],"sourcesContent":["import type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheSimilarHit,\n CacheSimilarOptions,\n CacheTtl,\n PgCacheOptions,\n PgClientLike,\n} from \"../types\";\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/**\n * Allowed characters in a Postgres identifier (table name). We accept the\n * conservative ASCII subset and reject anything else — interpolating an\n * arbitrary string into DDL would be a SQL-injection footgun.\n */\nconst SAFE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Postgres cache driver with optional pgvector similarity support.\n *\n * Connection lifecycle is the caller's responsibility — pass an already-built\n * `pg.Pool` or `pg.Client` via the `client` option. The driver never closes it\n * on `cache.disconnect()`, so the same pool can serve queries elsewhere in\n * the app.\n *\n * Schema is not auto-migrated. Call `driver.schema()` to get the DDL string\n * and run it through whichever migration tool you use.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { cache, PgCacheDriver } from \"@warlock.js/cache\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n *\n * cache.setCacheConfigurations({\n * default: \"pg\",\n * drivers: { pg: PgCacheDriver },\n * options: { pg: { client: pool, table: \"warlock_cache\", ttl: \"1h\" } },\n * });\n *\n * await cache.init();\n *\n * // Run once, via your own migration tooling:\n * // await pool.query(driver.schema());\n */\nexport class PgCacheDriver\n extends BaseCacheDriver<PgClientLike, PgCacheOptions>\n implements CacheDriver<PgClientLike, PgCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"pg\";\n\n /**\n * Cached result of the pgvector extension check. Populated lazily on first\n * vector op so the driver doesn't probe the database at construction time.\n */\n protected vectorReady?: boolean;\n\n /**\n * {@inheritdoc}\n *\n * Validates `client` presence and the table name before storing options.\n */\n public setOptions(options: PgCacheOptions) {\n if (!options || !options.client || typeof options.client.query !== \"function\") {\n throw new CacheConfigurationError(\n \"Pg cache driver requires a 'client' option implementing { query(text, values) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n const table = options.table ?? \"warlock_cache\";\n if (!SAFE_IDENT.test(table)) {\n throw new CacheConfigurationError(\n `Pg cache driver: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n if (options.vector) {\n const dim = options.vector.dimensions;\n if (!Number.isInteger(dim) || dim <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.dimensions must be a positive integer; got ${dim}.`,\n );\n }\n const idx = options.vector.index ?? \"hnsw\";\n if (idx !== \"hnsw\" && idx !== \"ivfflat\") {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.index must be 'hnsw' or 'ivfflat'; got '${idx}'.`,\n );\n }\n }\n\n return super.setOptions({ ...options, table });\n }\n\n /**\n * Lazy pgvector availability check. Runs once on the first vector op and\n * caches the result — subsequent ops are zero-overhead. Throws\n * {@link CacheConfigurationError} if the extension isn't installed; throws\n * {@link CacheUnsupportedError} if `vector` config wasn't provided at all.\n */\n protected async ensureVectorReady(): Promise<void> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n if (this.vectorReady === true) {\n return;\n }\n\n const { rows } = await this.pgClient.query(\n `SELECT 1 FROM pg_extension WHERE extname = 'vector'`,\n );\n\n if (rows.length === 0) {\n throw new CacheConfigurationError(\n \"'pg' driver: pgvector extension not installed. Run 'CREATE EXTENSION vector;' or remove the 'vector' config option.\",\n );\n }\n\n this.vectorReady = true;\n }\n\n /**\n * Format a numeric vector for pgvector ingestion. The `vector` type accepts\n * a string literal `'[1,2,3]'` cast via `::vector` — this avoids depending\n * on the binary protocol and works against any pg client.\n */\n protected formatVector(vector: number[]): string {\n return `[${vector.join(\",\")}]`;\n }\n\n /**\n * Resolved table name. Always defined post-`setOptions` — the validator\n * fills in the default.\n */\n protected get table(): string {\n return this.options.table ?? \"warlock_cache\";\n }\n\n /**\n * The user-supplied `pg.Pool` / `pg.Client`. Use this rather than `this.client`\n * (which has a generic fallback to `this`) for actual queries.\n */\n protected get pgClient(): PgClientLike {\n return this.options.client;\n }\n\n /**\n * Compute an absolute `expires_at` Date for the given relative TTL in seconds,\n * or `null` when the entry should not expire (`Infinity` / 0 / undefined).\n */\n protected ttlToExpiresAt(ttl?: number): Date | null {\n if (!ttl || ttl === Infinity) {\n return null;\n }\n\n return new Date(Date.now() + ttl * 1000);\n }\n\n /**\n * Return the SQL needed to provision the cache table + index. Run once via\n * the caller's migration tooling — the driver never auto-migrates.\n *\n * @example\n * await pool.query(driver.schema());\n */\n public schema(): string {\n const t = this.table;\n const vec = this.options.vector;\n\n const columns = [\n ` key TEXT PRIMARY KEY,`,\n ` value JSONB NOT NULL,`,\n ` expires_at TIMESTAMPTZ,`,\n ` stale_at TIMESTAMPTZ,`,\n ` tags TEXT[] NOT NULL DEFAULT '{}'::TEXT[]`,\n ];\n\n if (vec) {\n // Trailing comma on the previous line to make this a valid column list.\n columns[columns.length - 1] = columns[columns.length - 1] + \",\";\n columns.push(` embedding VECTOR(${vec.dimensions})`);\n }\n\n const lines = [\n `CREATE TABLE IF NOT EXISTS ${t} (`,\n ...columns,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_expires_at ON ${t} (expires_at);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_tags ON ${t} USING GIN (tags);`,\n ];\n\n if (vec) {\n const idx = vec.index ?? \"hnsw\";\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${t}_embedding ON ${t} USING ${idx} (embedding vector_cosine_ops);`,\n );\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async connect() {\n // No-op — caller owns the connection. We emit `connected` for symmetry\n // with the other drivers' lifecycle events.\n this.log(\"connecting\");\n this.log(\"connected\");\n await this.emit(\"connected\");\n }\n\n /**\n * {@inheritdoc}\n *\n * Does NOT close the user-supplied client — lifecycle stays with the caller.\n */\n public async disconnect() {\n this.log(\"disconnected\");\n await this.emit(\"disconnected\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: cannot index a vector without options.vector configuration — set { dimensions } and recreate the table via driver.schema().\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n await this.ensureVectorReady();\n }\n\n this.log(\"caching\", parsedKey);\n\n const expiresAt = this.ttlToExpiresAt(ttl);\n const staleAtDate = staleAt !== undefined ? new Date(staleAt) : null;\n const tagsArr = tags ?? [];\n const serialized = JSON.stringify(value);\n const vecLiteral = vector ? this.formatVector(vector) : null;\n\n const t = this.table;\n\n // Build column / placeholder / param triplets dynamically so the same code\n // path serves both KV-only and vector-aware writes. Param order is fixed:\n // $1 = key, $2 = value (jsonb), $3 = expires_at, $4 = stale_at,\n // $5 = tags, $6 = embedding::vector (only present when vecLiteral !== null).\n const cols = [\"key\", \"value\", \"expires_at\", \"stale_at\", \"tags\"];\n const placeholders = [\"$1\", \"$2::jsonb\", \"$3\", \"$4\", \"$5\"];\n const params: unknown[] = [parsedKey, serialized, expiresAt, staleAtDate, tagsArr];\n if (vecLiteral !== null) {\n cols.push(\"embedding\");\n placeholders.push(`$${params.length + 1}::vector`);\n params.push(vecLiteral);\n }\n const colList = cols.join(\", \");\n const valList = placeholders.join(\", \");\n const setClause = cols\n .slice(1)\n .map((c) => `${c} = EXCLUDED.${c}`)\n .join(\", \");\n const updateSetClause = cols\n .slice(1)\n .map((c, i) => `${c} = ${placeholders[i + 1]}`)\n .join(\", \");\n\n if (onConflict === \"create\") {\n // Race-safe insert: if another worker already holds the key (and the row\n // hasn't expired), DO NOTHING; we then SELECT to surface the existing value.\n const { rows } = await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}\n WHERE ${t}.expires_at IS NOT NULL AND ${t}.expires_at < now()\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n // Conflict + existing row not expired → fetch existing for the result.\n const existing = await this.get(key);\n return { wasSet: false, existing } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n if (onConflict === \"update\") {\n // Update only when the key exists AND hasn't expired.\n const { rows } = await this.pgClient.query(\n `UPDATE ${t}\n SET ${updateSetClause}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n return { wasSet: false, existing: null } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n // upsert (default)\n await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}`,\n params,\n );\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return value;\n }\n\n /**\n * {@inheritdoc}\n */\n public async get(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"fetching\", parsedKey);\n\n const t = this.table;\n const { rows } = await this.pgClient.query(\n `SELECT value FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n this.log(\"notFound\", parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n return null;\n }\n\n this.log(\"fetched\", parsedKey);\n\n // pg's JSONB type round-trips through node-postgres as a parsed JS value\n // already — but some pool implementations may hand back a string. Be defensive.\n let value = rows[0].value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Leave it as-is; cloning below will fail loud if it's truly broken.\n }\n }\n\n if (value === null || value === undefined) {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n const type = typeof value;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n try {\n const cloned = structuredClone(value);\n await this.emit(\"hit\", { key: parsedKey, value: cloned });\n return cloned;\n } catch (error) {\n this.logError(`Failed to clone cached value for ${parsedKey}`, error);\n throw error;\n }\n }\n\n /**\n * Read the raw {@link CacheData} wrapper, including `staleAt` metadata.\n * Returns `null` for missing or expired rows — `swr()` consumes this to\n * branch on freshness without going through `get()`'s clone-and-emit path.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n\n const { rows } = await this.pgClient.query(\n `SELECT value, expires_at, stale_at FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n return null;\n }\n\n let data = rows[0].value;\n if (typeof data === \"string\") {\n try {\n data = JSON.parse(data);\n } catch {\n // Leave as-is — getEntry is metadata-shaped, the caller will hit the\n // same parse path on the public `get()` if they care.\n }\n }\n\n const entry: CacheData = { data };\n\n const expiresAtRaw = rows[0].expires_at as Date | string | null;\n if (expiresAtRaw) {\n const expiresAt = expiresAtRaw instanceof Date ? expiresAtRaw : new Date(expiresAtRaw);\n entry.expiresAt = expiresAt.getTime();\n }\n\n const staleAtRaw = rows[0].stale_at as Date | string | null;\n if (staleAtRaw) {\n const staleAt = staleAtRaw instanceof Date ? staleAtRaw : new Date(staleAtRaw);\n entry.staleAt = staleAt.getTime();\n }\n\n return entry;\n }\n\n /**\n * {@inheritdoc}\n */\n public async remove(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"removing\", parsedKey);\n\n const t = this.table;\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1`, [parsedKey]);\n\n this.log(\"removed\", parsedKey);\n await this.emit(\"removed\", { key: parsedKey });\n }\n\n /**\n * {@inheritdoc}\n *\n * Deletes all rows whose key equals the namespace exactly or starts with\n * `<namespace>.` — same boundary semantics as the other drivers.\n */\n public async removeNamespace(namespace: string) {\n const parsed = this.parseKey(namespace);\n this.log(\"clearing\", parsed || \"(all)\");\n\n const t = this.table;\n\n if (parsed === \"\") {\n await this.pgClient.query(`DELETE FROM ${t}`);\n } else {\n // Escape `_` and `%` in the prefix so they aren't treated as LIKE wildcards.\n const escaped = parsed.replace(/\\\\/g, \"\\\\\\\\\").replace(/_/g, \"\\\\_\").replace(/%/g, \"\\\\%\");\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1 OR key LIKE $2 ESCAPE '\\\\'`, [\n parsed,\n `${escaped}.%`,\n ]);\n }\n\n this.log(\"cleared\", parsed || \"(all)\");\n return this;\n }\n\n /**\n * {@inheritdoc}\n *\n * Honors `globalPrefix` — when configured, scopes the flush to entries\n * under the prefix rather than truncating the entire table (which could\n * wipe sibling tenants sharing the same Postgres database).\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await this.pgClient.query(`DELETE FROM ${this.table}`);\n }\n\n this.log(\"flushed\");\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritdoc}\n *\n * pgvector-backed similarity. Uses the `<=>` cosine-distance operator\n * (lower distance = higher similarity) and converts to cosine similarity\n * as `1 - distance` so the returned `score` matches the rest of the\n * package (`[0, 1]`, higher is more similar).\n *\n * Honors `topK`, `threshold`, and an optional `tags` filter (native\n * `tags && $tags` overlap query — much faster than the meta-key path).\n *\n * Throws {@link CacheUnsupportedError} when `options.vector` was not\n * configured at driver setup; throws {@link CacheConfigurationError} when\n * the pgvector extension is missing or the query vector's dimension count\n * doesn't match the configured one.\n */\n public async similar<T = any>(\n vector: number[],\n options: CacheSimilarOptions,\n ): Promise<CacheSimilarHit<T>[]> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n if (!Number.isInteger(options.topK) || options.topK <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: similar.topK must be a positive integer; got ${options.topK}.`,\n );\n }\n\n await this.ensureVectorReady();\n\n const t = this.table;\n const vecLiteral = this.formatVector(vector);\n const params: unknown[] = [vecLiteral];\n let tagFilter = \"\";\n\n if (options.tags && options.tags.length > 0) {\n params.push(options.tags);\n tagFilter = `AND tags && $${params.length}`;\n }\n\n params.push(options.topK);\n const topKParam = `$${params.length}`;\n\n const { rows } = await this.pgClient.query(\n `SELECT key, value, 1 - (embedding <=> $1::vector) AS score\n FROM ${t}\n WHERE embedding IS NOT NULL\n AND (expires_at IS NULL OR expires_at > now())\n ${tagFilter}\n ORDER BY embedding <=> $1::vector\n LIMIT ${topKParam}`,\n params,\n );\n\n const hits: CacheSimilarHit<T>[] = [];\n for (const row of rows) {\n const score = Number(row.score);\n if (options.threshold !== undefined && score < options.threshold) {\n continue;\n }\n\n let value = row.value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Surface as-is; if non-JSON crept in, the consumer will notice.\n }\n }\n\n // Match get() cloning semantics so consumers can't mutate cached state.\n if (value !== null && value !== undefined) {\n const ty = typeof value;\n if (ty !== \"string\" && ty !== \"number\" && ty !== \"boolean\") {\n value = structuredClone(value);\n }\n }\n\n hits.push({ key: row.key, value, score });\n }\n\n return hits;\n }\n}\n"],"mappings":"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"}
@@ -92,7 +92,7 @@ var RedisCacheDriver = class extends BaseCacheDriver {
92
92
  wasSet: false,
93
93
  existing: onConflict === "create" ? await this.get(key) : null
94
94
  };
95
- if (tags && tags.length > 0) await this.applyTags(parsedKey, tags);
95
+ if (tags && tags.length > 0) await this.applyTags(key, tags);
96
96
  if (staleAt !== void 0) {
97
97
  const sidecarOptions = {};
98
98
  if (hasExpiry) sidecarOptions.EX = ttl;
@@ -1 +1 @@
1
- {"version":3,"file":"redis-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\r\nimport type { createClient } from \"redis\";\r\nimport type {\r\n CacheData,\r\n CacheDriver,\r\n CacheKey,\r\n CacheSetOptions,\r\n CacheSetResult,\r\n CacheTtl,\r\n RedisOptions,\r\n} from \"../types\";\r\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\r\nimport { safeErrorInfo } from \"../utils\";\r\nimport { BaseCacheDriver } from \"./base-cache-driver\";\r\n\r\n// ============================================================\r\n// Lazy-loaded Redis SDK Types\r\n// ============================================================\r\n\r\n/**\r\n * Cached Redis module (loaded once, reused)\r\n */\r\nlet RedisClient: typeof import(\"redis\");\r\n\r\nlet isModuleExists: boolean | null = null;\r\n\r\n/**\r\n * Installation instructions for Redis package\r\n */\r\nconst REDIS_INSTALL_INSTRUCTIONS = `\r\nRedis cache driver requires the redis package.\r\nInstall it with:\r\n\r\n npm install redis\r\n\r\nOr with your preferred package manager:\r\n\r\n pnpm add redis\r\n yarn add redis\r\n`.trim();\r\n\r\n/**\r\n * Load Redis module\r\n */\r\nasync function loadRedis() {\r\n try {\r\n RedisClient = await import(\"redis\");\r\n isModuleExists = true;\r\n } catch {\r\n isModuleExists = false;\r\n }\r\n}\r\n\r\nloadRedis();\r\n\r\n// ============================================================\r\n// RedisCacheDriver Class\r\n// ============================================================\r\n\r\nexport class RedisCacheDriver\r\n extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions>\r\n implements CacheDriver<ReturnType<typeof createClient>, RedisOptions>\r\n{\r\n /**\r\n * Cache driver name\r\n */\r\n public name = \"redis\";\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public setOptions(options: RedisOptions) {\r\n if (!options.url && !options.host) {\r\n throw new CacheConfigurationError(\r\n \"Redis driver requires either 'url' or 'host' option to be configured.\",\r\n );\r\n }\r\n\r\n return super.setOptions(options);\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async removeNamespace(namespace: string) {\r\n namespace = this.parseKey(namespace);\r\n\r\n this.log(\"clearing\", namespace);\r\n\r\n // Escape Redis glob metacharacters so a namespace carrying `*`/`?`/`[`\r\n // cannot widen the match and delete keys outside its own prefix.\r\n const pattern = namespace.replace(/[\\\\*?[\\]]/g, \"\\\\$&\");\r\n\r\n // `SCAN` (cursor-based, non-blocking) instead of `KEYS` — `KEYS` is O(N)\r\n // and blocks the single-threaded Redis event loop for the full scan,\r\n // which can stall every other tenant/consumer on a large keyspace.\r\n const keys: string[] = [];\r\n\r\n if (this.client) {\r\n for await (const key of this.client.scanIterator({\r\n MATCH: `${pattern}*`,\r\n COUNT: 100,\r\n })) {\r\n keys.push(key as unknown as string);\r\n }\r\n }\r\n\r\n if (keys.length === 0) {\r\n this.log(\"notFound\", namespace);\r\n return;\r\n }\r\n\r\n await this.client?.del(keys);\r\n\r\n this.log(\"cleared\", namespace);\r\n\r\n return keys;\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async set(\r\n key: CacheKey,\r\n value: any,\r\n ttlOrOptions?: CacheTtl | CacheSetOptions,\r\n ): Promise<any> {\r\n const parsedKey = this.parseKey(key);\r\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\r\n\r\n if (vector) {\r\n throw new CacheUnsupportedError(\r\n \"'redis' driver does not yet support similarity retrieval. Phase 2 (RediSearch) is on the backlog — use a memory driver or the 'pg' driver (with pgvector) for now.\",\r\n );\r\n }\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const serialized = JSON.stringify(value);\r\n const hasExpiry = Boolean(ttl) && ttl !== Infinity;\r\n\r\n let reply: string | null | undefined;\r\n\r\n if (onConflict === \"create\") {\r\n const options: { NX: true; EX?: number } = { NX: true };\r\n if (hasExpiry) {\r\n options.EX = ttl as number;\r\n }\r\n reply = await this.client?.set(parsedKey, serialized, options);\r\n } else if (onConflict === \"update\") {\r\n const options: { XX: true; EX?: number } = { XX: true };\r\n if (hasExpiry) {\r\n options.EX = ttl as number;\r\n }\r\n reply = await this.client?.set(parsedKey, serialized, options);\r\n } else if (hasExpiry) {\r\n reply = await this.client?.set(parsedKey, serialized, { EX: ttl as number });\r\n } else {\r\n reply = await this.client?.set(parsedKey, serialized);\r\n }\r\n\r\n const wasSet = reply === \"OK\";\r\n\r\n if ((onConflict === \"create\" || onConflict === \"update\") && !wasSet) {\r\n const existing = onConflict === \"create\" ? ((await this.get(key)) as any) : null;\r\n return { wasSet: false, existing } satisfies CacheSetResult;\r\n }\r\n\r\n if (tags && tags.length > 0) {\r\n await this.applyTags(parsedKey, tags);\r\n }\r\n\r\n if (staleAt !== undefined) {\r\n // Sidecar key for SWR freshness — keeps the main value JSON\r\n // backwards-compatible with entries written before SWR landed.\r\n const sidecarOptions: { EX?: number } = {};\r\n\r\n if (hasExpiry) {\r\n sidecarOptions.EX = ttl as number;\r\n }\r\n\r\n await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);\r\n }\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n await this.emit(\"set\", { key: parsedKey, value, ttl });\r\n\r\n if (onConflict === \"create\" || onConflict === \"update\") {\r\n return { wasSet: true, existing: null } satisfies CacheSetResult;\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * Build the sidecar key Redis uses to track SWR freshness without\r\n * wrapping the main value JSON.\r\n */\r\n protected swrMetaKey(parsedKey: string): string {\r\n return `__swrmeta:${parsedKey}`;\r\n }\r\n\r\n /**\r\n * Read the raw {@link CacheData} wrapper, fetching the value and the\r\n * SWR sidecar in parallel. Returns `null` when the main key is missing\r\n * or expired (Redis handles expiry natively, so the absence of the\r\n * value alone tells us).\r\n */\r\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n const [valueRaw, staleAtRaw] = await Promise.all([\r\n this.client?.get(parsedKey),\r\n this.client?.get(this.swrMetaKey(parsedKey)),\r\n ]);\r\n\r\n if (!valueRaw) {\r\n return null;\r\n }\r\n\r\n const data = JSON.parse(valueRaw);\r\n const staleAt = staleAtRaw ? Number(staleAtRaw) : undefined;\r\n\r\n return staleAt !== undefined ? { data, staleAt } : { data };\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n *\r\n * Redis tracks expiry natively (the payload carries no `expiresAt`), so read\r\n * the remaining lifetime with the `TTL` command. Redis returns `-2` for a\r\n * missing key and `-1` for a key with no expiry.\r\n */\r\n protected async getRemainingTtl(key: CacheKey): Promise<number | undefined> {\r\n const parsedKey = this.parseKey(key);\r\n const ttl = await this.client?.ttl(parsedKey);\r\n\r\n if (ttl === undefined || ttl === -2) {\r\n return undefined;\r\n }\r\n\r\n if (ttl === -1) {\r\n return Infinity;\r\n }\r\n\r\n return ttl;\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async get(key: CacheKey) {\r\n key = this.parseKey(key);\r\n\r\n this.log(\"fetching\", key);\r\n\r\n const value = await this.client?.get(key);\r\n\r\n if (!value) {\r\n this.log(\"notFound\", key);\r\n // Emit miss event\r\n await this.emit(\"miss\", { key });\r\n return null;\r\n }\r\n\r\n this.log(\"fetched\", key);\r\n\r\n // Parse and return the value directly (Redis handles expiration natively)\r\n const parsedValue = JSON.parse(value);\r\n\r\n // Apply cloning for immutability protection\r\n if (parsedValue === null || parsedValue === undefined) {\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: parsedValue });\r\n return parsedValue;\r\n }\r\n\r\n const type = typeof parsedValue;\r\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: parsedValue });\r\n return parsedValue;\r\n }\r\n\r\n try {\r\n const clonedValue = structuredClone(parsedValue);\r\n // Emit hit event\r\n await this.emit(\"hit\", { key, value: clonedValue });\r\n return clonedValue;\r\n } catch (error) {\r\n this.logError(`Failed to clone cached value for ${key}`, error);\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async remove(key: CacheKey) {\r\n key = this.parseKey(key);\r\n\r\n this.log(\"removing\", key);\r\n\r\n // Drop the SWR sidecar alongside the main key — keeps metadata from\r\n // surviving a `remove` and confusing a later `swr` read.\r\n await this.client?.del([key, this.swrMetaKey(key)]);\r\n\r\n this.log(\"removed\", key);\r\n\r\n await this.emit(\"removed\", { key });\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async flush() {\r\n this.log(\"flushing\");\r\n\r\n if (this.options.globalPrefix) {\r\n await this.removeNamespace(\"\");\r\n } else {\r\n await this.client?.flushAll();\r\n }\r\n\r\n this.log(\"flushed\");\r\n\r\n // Emit flushed event\r\n await this.emit(\"flushed\");\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n */\r\n public async connect() {\r\n if (this.clientDriver) return;\r\n\r\n if (!isModuleExists) {\r\n throw new Error(REDIS_INSTALL_INSTRUCTIONS);\r\n }\r\n\r\n const options = this.options;\r\n\r\n if (options && !options.url && options.host) {\r\n const auth =\r\n options.password || options.username ? `${options.username}:${options.password}@` : \"\";\r\n\r\n if (!options.url) {\r\n const host = options.host || \"localhost\";\r\n const port = options.port || 6379;\r\n options.url = `redis://${auth}${host}:${port}`;\r\n }\r\n }\r\n\r\n const clientOptions = {\r\n ...options,\r\n ...(this.options.clientOptions || {}),\r\n };\r\n\r\n try {\r\n this.log(\"connecting\");\r\n const { createClient } = RedisClient;\r\n\r\n this.client = createClient(clientOptions);\r\n\r\n this.client.on(\"error\", (error: Error) => {\r\n // Never pass the raw `error` object to the logger — connection\r\n // errors can carry the connection URL (with password) in the\r\n // message/cause. Only the redacted message survives past this point.\r\n const { message } = safeErrorInfo(error);\r\n\r\n if ((error as any).code === \"ECONNREFUSED\") {\r\n this.log(\"connectionFailed\", message);\r\n } else {\r\n this.log(\"error\", message);\r\n }\r\n });\r\n\r\n await this.client.connect();\r\n\r\n this.log(\"connected\");\r\n await this.emit(\"connected\");\r\n } catch (error) {\r\n // Boot-time cache connection failure is unrecoverable in practice —\r\n // `fatal` aligns Redis with the cascade drivers and herald connector\r\n // for clean \"page on fatal only\" alerting. Only the redacted\r\n // `{ message, code }` shape is logged; the raw error (which may carry\r\n // the connection URL/password) never reaches stdout or the logger.\r\n log.fatal(\"cache\", \"redis\", \"Failed to connect\", safeErrorInfo(error));\r\n await this.emit(\"error\", { error });\r\n }\r\n }\r\n\r\n /**\r\n * {@inheritDoc}\r\n *\r\n * Guards against disconnecting when the client was never created. The base\r\n * `client` getter falls back to `this` when no client is set, so we check\r\n * the backing `clientDriver` directly — using `this.client` for this guard\r\n * would always be truthy and crash with \"this.quit is not a function\".\r\n */\r\n public async disconnect() {\r\n if (!this.clientDriver) {\r\n return;\r\n }\r\n\r\n this.log(\"disconnecting\");\r\n\r\n await this.clientDriver.quit();\r\n\r\n this.log(\"disconnected\");\r\n await this.emit(\"disconnected\");\r\n }\r\n\r\n /**\r\n * Atomic increment using Redis native INCRBY command\r\n * {@inheritdoc}\r\n */\r\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const result = await this.client?.incrBy(parsedKey, value);\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\r\n\r\n return result || 0;\r\n }\r\n\r\n /**\r\n * Atomic decrement using Redis native DECRBY command\r\n * {@inheritdoc}\r\n */\r\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n const result = await this.client?.decrBy(parsedKey, value);\r\n\r\n this.log(\"cached\", parsedKey);\r\n\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\r\n\r\n return result || 0;\r\n }\r\n\r\n /**\r\n * Set if not exists (atomic operation)\r\n * Returns true if key was set, false if key already existed\r\n */\r\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\r\n const parsedKey = this.parseKey(key);\r\n\r\n this.log(\"caching\", parsedKey);\r\n\r\n if (ttl === undefined) {\r\n ttl = this.ttl;\r\n }\r\n\r\n let result: string | null;\r\n\r\n // Use Redis native SET with NX option\r\n if (ttl && ttl !== Infinity) {\r\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\r\n NX: true,\r\n EX: ttl,\r\n });\r\n } else {\r\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\r\n NX: true,\r\n });\r\n }\r\n\r\n const wasSet = result === \"OK\";\r\n\r\n if (wasSet) {\r\n this.log(\"cached\", parsedKey);\r\n // Emit set event\r\n await this.emit(\"set\", { key: parsedKey, value, ttl });\r\n } else {\r\n this.log(\"notFound\", parsedKey);\r\n }\r\n\r\n return wasSet;\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"redis-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type { createClient } from \"redis\";\nimport type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheTtl,\n RedisOptions,\n} from \"../types\";\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\nimport { safeErrorInfo } from \"../utils\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n// ============================================================\n// Lazy-loaded Redis SDK Types\n// ============================================================\n\n/**\n * Cached Redis module (loaded once, reused)\n */\nlet RedisClient: typeof import(\"redis\");\n\nlet isModuleExists: boolean | null = null;\n\n/**\n * Installation instructions for Redis package\n */\nconst REDIS_INSTALL_INSTRUCTIONS = `\nRedis cache driver requires the redis package.\nInstall it with:\n\n npm install redis\n\nOr with your preferred package manager:\n\n pnpm add redis\n yarn add redis\n`.trim();\n\n/**\n * Load Redis module\n */\nasync function loadRedis() {\n try {\n RedisClient = await import(\"redis\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n}\n\nloadRedis();\n\n// ============================================================\n// RedisCacheDriver Class\n// ============================================================\n\nexport class RedisCacheDriver\n extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions>\n implements CacheDriver<ReturnType<typeof createClient>, RedisOptions>\n{\n /**\n * Cache driver name\n */\n public name = \"redis\";\n\n /**\n * {@inheritdoc}\n */\n public setOptions(options: RedisOptions) {\n if (!options.url && !options.host) {\n throw new CacheConfigurationError(\n \"Redis driver requires either 'url' or 'host' option to be configured.\",\n );\n }\n\n return super.setOptions(options);\n }\n\n /**\n * {@inheritDoc}\n */\n public async removeNamespace(namespace: string) {\n namespace = this.parseKey(namespace);\n\n this.log(\"clearing\", namespace);\n\n // Escape Redis glob metacharacters so a namespace carrying `*`/`?`/`[`\n // cannot widen the match and delete keys outside its own prefix.\n const pattern = namespace.replace(/[\\\\*?[\\]]/g, \"\\\\$&\");\n\n // `SCAN` (cursor-based, non-blocking) instead of `KEYS` — `KEYS` is O(N)\n // and blocks the single-threaded Redis event loop for the full scan,\n // which can stall every other tenant/consumer on a large keyspace.\n const keys: string[] = [];\n\n if (this.client) {\n for await (const key of this.client.scanIterator({\n MATCH: `${pattern}*`,\n COUNT: 100,\n })) {\n keys.push(key as unknown as string);\n }\n }\n\n if (keys.length === 0) {\n this.log(\"notFound\", namespace);\n return;\n }\n\n await this.client?.del(keys);\n\n this.log(\"cleared\", namespace);\n\n return keys;\n }\n\n /**\n * {@inheritDoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n throw new CacheUnsupportedError(\n \"'redis' driver does not yet support similarity retrieval. Phase 2 (RediSearch) is on the backlog — use a memory driver or the 'pg' driver (with pgvector) for now.\",\n );\n }\n\n this.log(\"caching\", parsedKey);\n\n const serialized = JSON.stringify(value);\n const hasExpiry = Boolean(ttl) && ttl !== Infinity;\n\n let reply: string | null | undefined;\n\n if (onConflict === \"create\") {\n const options: { NX: true; EX?: number } = { NX: true };\n if (hasExpiry) {\n options.EX = ttl as number;\n }\n reply = await this.client?.set(parsedKey, serialized, options);\n } else if (onConflict === \"update\") {\n const options: { XX: true; EX?: number } = { XX: true };\n if (hasExpiry) {\n options.EX = ttl as number;\n }\n reply = await this.client?.set(parsedKey, serialized, options);\n } else if (hasExpiry) {\n reply = await this.client?.set(parsedKey, serialized, { EX: ttl as number });\n } else {\n reply = await this.client?.set(parsedKey, serialized);\n }\n\n const wasSet = reply === \"OK\";\n\n if ((onConflict === \"create\" || onConflict === \"update\") && !wasSet) {\n const existing = onConflict === \"create\" ? ((await this.get(key)) as any) : null;\n return { wasSet: false, existing } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n if (staleAt !== undefined) {\n // Sidecar key for SWR freshness — keeps the main value JSON\n // backwards-compatible with entries written before SWR landed.\n const sidecarOptions: { EX?: number } = {};\n\n if (hasExpiry) {\n sidecarOptions.EX = ttl as number;\n }\n\n await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);\n }\n\n this.log(\"cached\", parsedKey);\n\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n\n if (onConflict === \"create\" || onConflict === \"update\") {\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n return value;\n }\n\n /**\n * Build the sidecar key Redis uses to track SWR freshness without\n * wrapping the main value JSON.\n */\n protected swrMetaKey(parsedKey: string): string {\n return `__swrmeta:${parsedKey}`;\n }\n\n /**\n * Read the raw {@link CacheData} wrapper, fetching the value and the\n * SWR sidecar in parallel. Returns `null` when the main key is missing\n * or expired (Redis handles expiry natively, so the absence of the\n * value alone tells us).\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n\n const [valueRaw, staleAtRaw] = await Promise.all([\n this.client?.get(parsedKey),\n this.client?.get(this.swrMetaKey(parsedKey)),\n ]);\n\n if (!valueRaw) {\n return null;\n }\n\n const data = JSON.parse(valueRaw);\n const staleAt = staleAtRaw ? Number(staleAtRaw) : undefined;\n\n return staleAt !== undefined ? { data, staleAt } : { data };\n }\n\n /**\n * {@inheritdoc}\n *\n * Redis tracks expiry natively (the payload carries no `expiresAt`), so read\n * the remaining lifetime with the `TTL` command. Redis returns `-2` for a\n * missing key and `-1` for a key with no expiry.\n */\n protected async getRemainingTtl(key: CacheKey): Promise<number | undefined> {\n const parsedKey = this.parseKey(key);\n const ttl = await this.client?.ttl(parsedKey);\n\n if (ttl === undefined || ttl === -2) {\n return undefined;\n }\n\n if (ttl === -1) {\n return Infinity;\n }\n\n return ttl;\n }\n\n /**\n * {@inheritDoc}\n */\n public async get(key: CacheKey) {\n key = this.parseKey(key);\n\n this.log(\"fetching\", key);\n\n const value = await this.client?.get(key);\n\n if (!value) {\n this.log(\"notFound\", key);\n // Emit miss event\n await this.emit(\"miss\", { key });\n return null;\n }\n\n this.log(\"fetched\", key);\n\n // Parse and return the value directly (Redis handles expiration natively)\n const parsedValue = JSON.parse(value);\n\n // Apply cloning for immutability protection\n if (parsedValue === null || parsedValue === undefined) {\n // Emit hit event\n await this.emit(\"hit\", { key, value: parsedValue });\n return parsedValue;\n }\n\n const type = typeof parsedValue;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n // Emit hit event\n await this.emit(\"hit\", { key, value: parsedValue });\n return parsedValue;\n }\n\n try {\n const clonedValue = structuredClone(parsedValue);\n // Emit hit event\n await this.emit(\"hit\", { key, value: clonedValue });\n return clonedValue;\n } catch (error) {\n this.logError(`Failed to clone cached value for ${key}`, error);\n throw error;\n }\n }\n\n /**\n * {@inheritDoc}\n */\n public async remove(key: CacheKey) {\n key = this.parseKey(key);\n\n this.log(\"removing\", key);\n\n // Drop the SWR sidecar alongside the main key — keeps metadata from\n // surviving a `remove` and confusing a later `swr` read.\n await this.client?.del([key, this.swrMetaKey(key)]);\n\n this.log(\"removed\", key);\n\n await this.emit(\"removed\", { key });\n }\n\n /**\n * {@inheritDoc}\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await this.client?.flushAll();\n }\n\n this.log(\"flushed\");\n\n // Emit flushed event\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritDoc}\n */\n public async connect() {\n if (this.clientDriver) return;\n\n if (!isModuleExists) {\n throw new Error(REDIS_INSTALL_INSTRUCTIONS);\n }\n\n const options = this.options;\n\n if (options && !options.url && options.host) {\n const auth =\n options.password || options.username ? `${options.username}:${options.password}@` : \"\";\n\n if (!options.url) {\n const host = options.host || \"localhost\";\n const port = options.port || 6379;\n options.url = `redis://${auth}${host}:${port}`;\n }\n }\n\n const clientOptions = {\n ...options,\n ...(this.options.clientOptions || {}),\n };\n\n try {\n this.log(\"connecting\");\n const { createClient } = RedisClient;\n\n this.client = createClient(clientOptions);\n\n this.client.on(\"error\", (error: Error) => {\n // Never pass the raw `error` object to the logger — connection\n // errors can carry the connection URL (with password) in the\n // message/cause. Only the redacted message survives past this point.\n const { message } = safeErrorInfo(error);\n\n if ((error as any).code === \"ECONNREFUSED\") {\n this.log(\"connectionFailed\", message);\n } else {\n this.log(\"error\", message);\n }\n });\n\n await this.client.connect();\n\n this.log(\"connected\");\n await this.emit(\"connected\");\n } catch (error) {\n // Boot-time cache connection failure is unrecoverable in practice —\n // `fatal` aligns Redis with the cascade drivers and herald connector\n // for clean \"page on fatal only\" alerting. Only the redacted\n // `{ message, code }` shape is logged; the raw error (which may carry\n // the connection URL/password) never reaches stdout or the logger.\n log.fatal(\"cache\", \"redis\", \"Failed to connect\", safeErrorInfo(error));\n await this.emit(\"error\", { error });\n }\n }\n\n /**\n * {@inheritDoc}\n *\n * Guards against disconnecting when the client was never created. The base\n * `client` getter falls back to `this` when no client is set, so we check\n * the backing `clientDriver` directly — using `this.client` for this guard\n * would always be truthy and crash with \"this.quit is not a function\".\n */\n public async disconnect() {\n if (!this.clientDriver) {\n return;\n }\n\n this.log(\"disconnecting\");\n\n await this.clientDriver.quit();\n\n this.log(\"disconnected\");\n await this.emit(\"disconnected\");\n }\n\n /**\n * Atomic increment using Redis native INCRBY command\n * {@inheritdoc}\n */\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n const result = await this.client?.incrBy(parsedKey, value);\n\n this.log(\"cached\", parsedKey);\n\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\n\n return result || 0;\n }\n\n /**\n * Atomic decrement using Redis native DECRBY command\n * {@inheritdoc}\n */\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n const result = await this.client?.decrBy(parsedKey, value);\n\n this.log(\"cached\", parsedKey);\n\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\n\n return result || 0;\n }\n\n /**\n * Set if not exists (atomic operation)\n * Returns true if key was set, false if key already existed\n */\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n if (ttl === undefined) {\n ttl = this.ttl;\n }\n\n let result: string | null;\n\n // Use Redis native SET with NX option\n if (ttl && ttl !== Infinity) {\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\n NX: true,\n EX: ttl,\n });\n } else {\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\n NX: true,\n });\n }\n\n const wasSet = result === \"OK\";\n\n if (wasSet) {\n this.log(\"cached\", parsedKey);\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n } else {\n this.log(\"notFound\", parsedKey);\n }\n\n return wasSet;\n }\n}\n"],"mappings":"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"}
@@ -25,15 +25,28 @@ declare class TaggedCache implements TaggedCacheDriver {
25
25
  /**
26
26
  * Store tag-key relationship
27
27
  */
28
- protected storeTaggedKey(key: string): Promise<void>;
28
+ protected storeTaggedKey(key: CacheKey): Promise<void>;
29
29
  /**
30
30
  * Public alias of the tag-index writer. Called by `BaseCacheDriver.applyTags`
31
31
  * when tags are passed inline through `CacheSetOptions.tags`.
32
32
  *
33
+ * Takes the caller's key, never the driver-parsed one — see {@link indexedKey}.
34
+ *
33
35
  * @internal — public for cross-class use within this package; not part of the
34
36
  * stable consumer API.
35
37
  */
36
- storeTagRelationship(parsedKey: string): Promise<void>;
38
+ storeTagRelationship(key: CacheKey): Promise<void>;
39
+ /**
40
+ * The form a key takes inside a tag index: normalized but un-prefixed.
41
+ *
42
+ * The index is read back and deleted through `driver.remove(key)`, which
43
+ * applies `globalPrefix` itself — so the index must hold the key *before*
44
+ * the prefix, or invalidation would prefix it twice and delete nothing.
45
+ * With a function prefix, the current prefix is re-applied at invalidation
46
+ * time; the tag index key is resolved under that same prefix, so the two
47
+ * agree as long as the prefix is stable for a given request/tenant.
48
+ */
49
+ protected indexedKey(key: CacheKey): string;
37
50
  /**
38
51
  * Get all keys associated with tags
39
52
  */
@@ -1 +1 @@
1
- {"version":3,"file":"tagged-cache.d.mts","names":[],"sources":["../../../../../../cache/src/tagged-cache.ts"],"mappings":";;;;;AAYA;;cAAa,WAAA,YAAuB,iBAAA;EAShB;;;EAAA,UALR,SAAA;EAmD+B;;;EAAA,UA9C/B,MAAA,EAAQ,WAAA;EAmEU;;;cA9DT,IAAA,YAAgB,MAAA,EAAQ,WAAA;EAmFlB;;;EAAA,UA3Ef,MAAA,CAAO,GAAA;EAsHK;;;EAAA,UA/GN,cAAA,CAAe,GAAA,WAAc,OAAA;EA0H1C;;;;;;;EA/GU,oBAAA,CAAqB,SAAA,WAAoB,OAAA;EAqKI;;;EAAA,UAtJ1C,aAAA,IAAiB,OAAA,CAAQ,GAAA;EAvDP;;;EAyErB,GAAA,CACX,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;;;;EAaU,GAAA,CAAI,GAAA,EAAK,QAAA,GAAW,OAAA;EApEvB;;;EA2EG,MAAA,CAAO,GAAA,EAAK,QAAA,GAAW,OAAA;EApES;;;EAsFhC,UAAA,IAAc,OAAA;EA5DX;;;;EA8EH,KAAA,IAAS,OAAA;EA3DpB;;;EAkEW,GAAA,CAAI,GAAA,EAAK,QAAA,GAAW,OAAA;EAhE/B;;;EAuEW,QAAA,CACX,GAAA,EAAK,QAAA,EACL,GAAA,UACA,QAAA,QAAgB,OAAA,QACf,OAAA;EA7Dc;;;EA6EJ,IAAA,CAAK,GAAA,EAAK,QAAA,GAAW,OAAA;EAtEd;;;EAmFP,OAAA,CAAQ,GAAA,EAAK,QAAA,EAAU,KAAA,QAAa,OAAA;EA/CpC;;;EAsDA,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EA/CzC;;;EAiEJ,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;AAAA"}
1
+ {"version":3,"file":"tagged-cache.d.mts","names":[],"sources":["../../../../../../cache/src/tagged-cache.ts"],"mappings":";;;;;AAaA;;cAAa,WAAA,YAAuB,iBAAA;EAShB;;;EAAA,UALR,SAAA;EAsC6B;;;EAAA,UAjC7B,MAAA,EAAQ,WAAA;EAgEe;;;cA3Dd,IAAA,YAAgB,MAAA,EAAQ,WAAA;EAiFxC;;;EAAA,UAzEO,MAAA,CAAO,GAAA;EA2FmB;;;EAAA,UApFpB,cAAA,CAAe,GAAA,EAAK,QAAA,GAAW,OAAA;EA+Hd;;;;;;;;;EAlHpB,oBAAA,CAAqB,GAAA,EAAK,QAAA,GAAW,OAAA;EAmLtB;;;;;;;;;;EAAA,UA3JlB,UAAA,CAAW,GAAA,EAAK,QAAA;EApDiB;;;EAAA,UA2D3B,aAAA,IAAiB,OAAA,CAAQ,GAAA;EA5CzB;;;EA8DH,GAAA,CACX,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EArDU;;;EAgEA,GAAA,CAAI,GAAA,EAAK,QAAA,GAAW,OAAA;EAxCvB;;;EA+CG,MAAA,CAAO,GAAA,EAAK,QAAA,GAAW,OAAA;EAxCH;;;EA0DpB,UAAA,IAAc,OAAA;EAvCzB;;;;EAyDW,KAAA,IAAS,OAAA;EAtDnB;;;EA6DU,GAAA,CAAI,GAAA,EAAK,QAAA,GAAW,OAAA;EAlDA;;;EAyDpB,QAAA,CACX,GAAA,EAAK,QAAA,EACL,GAAA,UACA,QAAA,QAAgB,OAAA,QACf,OAAA;EAtDiC;;;EAsEvB,IAAA,CAAK,GAAA,EAAK,QAAA,GAAW,OAAA;EAlCZ;;;EA+CT,OAAA,CAAQ,GAAA,EAAK,QAAA,EAAU,KAAA,QAAa,OAAA;EAxChB;;;EA+CpB,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EAtCxD;;;EAwDW,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;AAAA"}
@@ -1,3 +1,5 @@
1
+ import { parseCacheKey } from "./utils.mjs";
2
+
1
3
  //#region ../cache/src/tagged-cache.ts
2
4
  /**
3
5
  * Tagged Cache Wrapper
@@ -27,20 +29,36 @@ var TaggedCache = class {
27
29
  * Public alias of the tag-index writer. Called by `BaseCacheDriver.applyTags`
28
30
  * when tags are passed inline through `CacheSetOptions.tags`.
29
31
  *
32
+ * Takes the caller's key, never the driver-parsed one — see {@link indexedKey}.
33
+ *
30
34
  * @internal — public for cross-class use within this package; not part of the
31
35
  * stable consumer API.
32
36
  */
33
- async storeTagRelationship(parsedKey) {
37
+ async storeTagRelationship(key) {
38
+ const indexedKey = this.indexedKey(key);
34
39
  for (const tag of this.cacheTags) {
35
40
  const tagKey = this.tagKey(tag);
36
41
  const keys = await this.driver.get(tagKey) || [];
37
- if (!keys.includes(parsedKey)) {
38
- keys.push(parsedKey);
42
+ if (!keys.includes(indexedKey)) {
43
+ keys.push(indexedKey);
39
44
  await this.driver.set(tagKey, keys, Infinity);
40
45
  }
41
46
  }
42
47
  }
43
48
  /**
49
+ * The form a key takes inside a tag index: normalized but un-prefixed.
50
+ *
51
+ * The index is read back and deleted through `driver.remove(key)`, which
52
+ * applies `globalPrefix` itself — so the index must hold the key *before*
53
+ * the prefix, or invalidation would prefix it twice and delete nothing.
54
+ * With a function prefix, the current prefix is re-applied at invalidation
55
+ * time; the tag index key is resolved under that same prefix, so the two
56
+ * agree as long as the prefix is stable for a given request/tenant.
57
+ */
58
+ indexedKey(key) {
59
+ return parseCacheKey(key);
60
+ }
61
+ /**
44
62
  * Get all keys associated with tags
45
63
  */
46
64
  async getTaggedKeys() {
@@ -56,9 +74,8 @@ var TaggedCache = class {
56
74
  * {@inheritdoc}
57
75
  */
58
76
  async set(key, value, ttlOrOptions) {
59
- const parsedKey = this.driver.parseKey(key);
60
77
  await this.driver.set(key, value, ttlOrOptions);
61
- await this.storeTaggedKey(parsedKey);
78
+ await this.storeTaggedKey(key);
62
79
  return value;
63
80
  }
64
81
  /**
@@ -71,11 +88,11 @@ var TaggedCache = class {
71
88
  * {@inheritdoc}
72
89
  */
73
90
  async remove(key) {
74
- const parsedKey = this.driver.parseKey(key);
91
+ const indexedKey = this.indexedKey(key);
75
92
  await this.driver.remove(key);
76
93
  for (const tag of this.cacheTags) {
77
94
  const tagKey = this.tagKey(tag);
78
- const updatedKeys = (await this.driver.get(tagKey) || []).filter((k) => k !== parsedKey);
95
+ const updatedKeys = (await this.driver.get(tagKey) || []).filter((k) => k !== indexedKey);
79
96
  await this.driver.set(tagKey, updatedKeys, Infinity);
80
97
  }
81
98
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tagged-cache.mjs","names":[],"sources":["../../../../../../cache/src/tagged-cache.ts"],"sourcesContent":["import type {\r\n CacheDriver,\r\n CacheKey,\r\n CacheSetOptions,\r\n CacheTtl,\r\n TaggedCacheDriver,\r\n} from \"./types\";\r\n\r\n/**\r\n * Tagged Cache Wrapper\r\n * Wraps a cache driver to automatically manage tag relationships\r\n */\r\nexport class TaggedCache implements TaggedCacheDriver {\r\n /**\r\n * The tags associated with this tagged cache instance\r\n */\r\n protected cacheTags: string[];\r\n\r\n /**\r\n * The underlying cache driver\r\n */\r\n protected driver: CacheDriver<any, any>;\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(tags: string[], driver: CacheDriver<any, any>) {\r\n this.cacheTags = tags;\r\n this.driver = driver;\r\n }\r\n\r\n /**\r\n * Get the tag key prefix for storing tag-key relationships\r\n */\r\n protected tagKey(tag: string): string {\r\n return `cache:tags:${tag}`;\r\n }\r\n\r\n /**\r\n * Store tag-key relationship\r\n */\r\n protected async storeTaggedKey(key: string): Promise<void> {\r\n await this.storeTagRelationship(key);\r\n }\r\n\r\n /**\r\n * Public alias of the tag-index writer. Called by `BaseCacheDriver.applyTags`\r\n * when tags are passed inline through `CacheSetOptions.tags`.\r\n *\r\n * @internal — public for cross-class use within this package; not part of the\r\n * stable consumer API.\r\n */\r\n public async storeTagRelationship(parsedKey: string): Promise<void> {\r\n for (const tag of this.cacheTags) {\r\n const tagKey = this.tagKey(tag);\r\n const keys = (await this.driver.get(tagKey)) || [];\r\n\r\n if (!keys.includes(parsedKey)) {\r\n keys.push(parsedKey);\r\n await this.driver.set(tagKey, keys, Infinity);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Get all keys associated with tags\r\n */\r\n protected async getTaggedKeys(): Promise<Set<string>> {\r\n const allKeys = new Set<string>();\r\n\r\n for (const tag of this.cacheTags) {\r\n const tagKey = this.tagKey(tag);\r\n const keys = (await this.driver.get(tagKey)) || [];\r\n\r\n for (const key of keys) {\r\n allKeys.add(key);\r\n }\r\n }\r\n\r\n return allKeys;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async set(\r\n key: CacheKey,\r\n value: any,\r\n ttlOrOptions?: CacheTtl | CacheSetOptions,\r\n ): Promise<any> {\r\n const parsedKey = this.driver.parseKey(key);\r\n\r\n await this.driver.set(key, value, ttlOrOptions);\r\n\r\n await this.storeTaggedKey(parsedKey);\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async get(key: CacheKey): Promise<any | null> {\r\n return this.driver.get(key);\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async remove(key: CacheKey): Promise<void> {\r\n const parsedKey = this.driver.parseKey(key);\r\n\r\n // Remove the value\r\n await this.driver.remove(key);\r\n\r\n // Remove from all tag relationships\r\n for (const tag of this.cacheTags) {\r\n const tagKey = this.tagKey(tag);\r\n const keys = (await this.driver.get(tagKey)) || [];\r\n const updatedKeys = keys.filter((k: string) => k !== parsedKey);\r\n await this.driver.set(tagKey, updatedKeys, Infinity);\r\n }\r\n }\r\n\r\n /**\r\n * Invalidate (clear) all keys associated with the current tags\r\n */\r\n public async invalidate(): Promise<void> {\r\n const keysToRemove = await this.getTaggedKeys();\r\n\r\n // Remove all tagged keys\r\n for (const key of keysToRemove) {\r\n await this.driver.remove(key);\r\n }\r\n\r\n // Clear tag relationship keys\r\n for (const tag of this.cacheTags) {\r\n await this.driver.remove(this.tagKey(tag));\r\n }\r\n }\r\n\r\n /**\r\n * Flush all keys associated with the current tags\r\n * @deprecated Use invalidate() instead for better semantics\r\n */\r\n public async flush(): Promise<void> {\r\n return this.invalidate();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async has(key: CacheKey): Promise<boolean> {\r\n return this.driver.has(key);\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async remember(\r\n key: CacheKey,\r\n ttl: number,\r\n callback: () => Promise<any>,\r\n ): Promise<any> {\r\n const value = await this.get(key);\r\n\r\n if (value !== null) {\r\n return value;\r\n }\r\n\r\n const result = await callback();\r\n await this.set(key, result, ttl);\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async pull(key: CacheKey): Promise<any | null> {\r\n const value = await this.get(key);\r\n\r\n if (value !== null) {\r\n await this.remove(key);\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async forever(key: CacheKey, value: any): Promise<any> {\r\n return this.set(key, value, Infinity);\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\r\n const current = (await this.get(key)) || 0;\r\n\r\n if (typeof current !== \"number\") {\r\n throw new Error(\r\n `Cannot increment non-numeric value for key: ${this.driver.parseKey(key)}`,\r\n );\r\n }\r\n\r\n const newValue = current + value;\r\n await this.set(key, newValue);\r\n\r\n return newValue;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\r\n return this.increment(key, -value);\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"tagged-cache.mjs","names":[],"sources":["../../../../../../cache/src/tagged-cache.ts"],"sourcesContent":["import type {\r\n CacheDriver,\r\n CacheKey,\r\n CacheSetOptions,\r\n CacheTtl,\r\n TaggedCacheDriver,\r\n} from \"./types\";\r\nimport { parseCacheKey } from \"./utils\";\r\n\r\n/**\r\n * Tagged Cache Wrapper\r\n * Wraps a cache driver to automatically manage tag relationships\r\n */\r\nexport class TaggedCache implements TaggedCacheDriver {\r\n /**\r\n * The tags associated with this tagged cache instance\r\n */\r\n protected cacheTags: string[];\r\n\r\n /**\r\n * The underlying cache driver\r\n */\r\n protected driver: CacheDriver<any, any>;\r\n\r\n /**\r\n * Constructor\r\n */\r\n public constructor(tags: string[], driver: CacheDriver<any, any>) {\r\n this.cacheTags = tags;\r\n this.driver = driver;\r\n }\r\n\r\n /**\r\n * Get the tag key prefix for storing tag-key relationships\r\n */\r\n protected tagKey(tag: string): string {\r\n return `cache:tags:${tag}`;\r\n }\r\n\r\n /**\r\n * Store tag-key relationship\r\n */\r\n protected async storeTaggedKey(key: CacheKey): Promise<void> {\r\n await this.storeTagRelationship(key);\r\n }\r\n\r\n /**\r\n * Public alias of the tag-index writer. Called by `BaseCacheDriver.applyTags`\r\n * when tags are passed inline through `CacheSetOptions.tags`.\r\n *\r\n * Takes the caller's key, never the driver-parsed one — see {@link indexedKey}.\r\n *\r\n * @internal — public for cross-class use within this package; not part of the\r\n * stable consumer API.\r\n */\r\n public async storeTagRelationship(key: CacheKey): Promise<void> {\r\n const indexedKey = this.indexedKey(key);\r\n\r\n for (const tag of this.cacheTags) {\r\n const tagKey = this.tagKey(tag);\r\n const keys = (await this.driver.get(tagKey)) || [];\r\n\r\n if (!keys.includes(indexedKey)) {\r\n keys.push(indexedKey);\r\n await this.driver.set(tagKey, keys, Infinity);\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * The form a key takes inside a tag index: normalized but un-prefixed.\r\n *\r\n * The index is read back and deleted through `driver.remove(key)`, which\r\n * applies `globalPrefix` itself — so the index must hold the key *before*\r\n * the prefix, or invalidation would prefix it twice and delete nothing.\r\n * With a function prefix, the current prefix is re-applied at invalidation\r\n * time; the tag index key is resolved under that same prefix, so the two\r\n * agree as long as the prefix is stable for a given request/tenant.\r\n */\r\n protected indexedKey(key: CacheKey): string {\r\n return parseCacheKey(key);\r\n }\r\n\r\n /**\r\n * Get all keys associated with tags\r\n */\r\n protected async getTaggedKeys(): Promise<Set<string>> {\r\n const allKeys = new Set<string>();\r\n\r\n for (const tag of this.cacheTags) {\r\n const tagKey = this.tagKey(tag);\r\n const keys = (await this.driver.get(tagKey)) || [];\r\n\r\n for (const key of keys) {\r\n allKeys.add(key);\r\n }\r\n }\r\n\r\n return allKeys;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async set(\r\n key: CacheKey,\r\n value: any,\r\n ttlOrOptions?: CacheTtl | CacheSetOptions,\r\n ): Promise<any> {\r\n await this.driver.set(key, value, ttlOrOptions);\r\n\r\n await this.storeTaggedKey(key);\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async get(key: CacheKey): Promise<any | null> {\r\n return this.driver.get(key);\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async remove(key: CacheKey): Promise<void> {\r\n const indexedKey = this.indexedKey(key);\r\n\r\n // Remove the value\r\n await this.driver.remove(key);\r\n\r\n // Remove from all tag relationships\r\n for (const tag of this.cacheTags) {\r\n const tagKey = this.tagKey(tag);\r\n const keys = (await this.driver.get(tagKey)) || [];\r\n const updatedKeys = keys.filter((k: string) => k !== indexedKey);\r\n await this.driver.set(tagKey, updatedKeys, Infinity);\r\n }\r\n }\r\n\r\n /**\r\n * Invalidate (clear) all keys associated with the current tags\r\n */\r\n public async invalidate(): Promise<void> {\r\n const keysToRemove = await this.getTaggedKeys();\r\n\r\n // Remove all tagged keys — indexed un-prefixed, so `remove()` prefixes once\r\n for (const key of keysToRemove) {\r\n await this.driver.remove(key);\r\n }\r\n\r\n // Clear tag relationship keys\r\n for (const tag of this.cacheTags) {\r\n await this.driver.remove(this.tagKey(tag));\r\n }\r\n }\r\n\r\n /**\r\n * Flush all keys associated with the current tags\r\n * @deprecated Use invalidate() instead for better semantics\r\n */\r\n public async flush(): Promise<void> {\r\n return this.invalidate();\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async has(key: CacheKey): Promise<boolean> {\r\n return this.driver.has(key);\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async remember(\r\n key: CacheKey,\r\n ttl: number,\r\n callback: () => Promise<any>,\r\n ): Promise<any> {\r\n const value = await this.get(key);\r\n\r\n if (value !== null) {\r\n return value;\r\n }\r\n\r\n const result = await callback();\r\n await this.set(key, result, ttl);\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async pull(key: CacheKey): Promise<any | null> {\r\n const value = await this.get(key);\r\n\r\n if (value !== null) {\r\n await this.remove(key);\r\n }\r\n\r\n return value;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async forever(key: CacheKey, value: any): Promise<any> {\r\n return this.set(key, value, Infinity);\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\r\n const current = (await this.get(key)) || 0;\r\n\r\n if (typeof current !== \"number\") {\r\n throw new Error(\r\n `Cannot increment non-numeric value for key: ${this.driver.parseKey(key)}`,\r\n );\r\n }\r\n\r\n const newValue = current + value;\r\n await this.set(key, newValue);\r\n\r\n return newValue;\r\n }\r\n\r\n /**\r\n * {@inheritdoc}\r\n */\r\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\r\n return this.increment(key, -value);\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tagged-scoped-cache.d.mts","names":[],"sources":["../../../../../../cache/src/tagged-scoped-cache.ts"],"mappings":";;;;;;AA4CA;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,iBAAA,YAA6B,yBAAA;EA+Gb;;;;;EAAA,mBAzGR,KAAA,EAAO,WAAA;EAwHuB;;;;EAAA,mBAlH9B,UAAA;EAZqB;;;;cAkBrB,KAAA,EAAO,WAAA,EAAa,UAAA;EAZb;;;;EAqBnB,GAAA,CACL,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EAboC;;;EAsBhC,GAAA,UAAa,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,CAAA;EAX1C;;;EAkBK,GAAA,CAAI,GAAA,EAAK,QAAA,GAAW,OAAA;EAhBxB;;;;EAwBI,MAAA,CAAO,GAAA,EAAK,QAAA,GAAW,OAAA;EAfM;;;EAsB7B,IAAA,UAAc,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,CAAA;EAflC;;;;EAuBJ,OAAA,UAAiB,GAAA,EAAK,QAAA,EAAU,KAAA,EAAO,CAAA,GAAI,OAAA,CAAQ,CAAA;EAf5B;;;;;EA2BjB,KAAA,CAAM,GAAA,EAAK,QAAA,EAAU,KAAA,OAAY,GAAA,YAAe,OAAA;EApBhB;;;EA+CtC,QAAA,UACL,GAAA,EAAK,QAAA,EACL,YAAA,EAAc,QAAA,GAAW,eAAA,EACzB,QAAA,QAAgB,OAAA,CAAQ,CAAA,IACvB,OAAA,CAAQ,CAAA;EA3Ca;;;;EAwDjB,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAiB,OAAA;EA5CpC;;;EAmDN,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAiB,OAAA;EAnDH;;;;;EA4DjC,UAAA,IAAc,OAAA;EA/BX;;;;;EAAA,UA8CN,cAAA,CAAe,GAAA,EAAK,QAAA;AAAA"}
1
+ {"version":3,"file":"tagged-scoped-cache.d.mts","names":[],"sources":["../../../../../../cache/src/tagged-scoped-cache.ts"],"mappings":";;;;;;AA4CA;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,iBAAA,YAA6B,yBAAA;EAgHb;;;;;EAAA,mBA1GR,KAAA,EAAO,WAAA;EAyHuB;;;;EAAA,mBAnH9B,UAAA;EAZqB;;;;cAkBrB,KAAA,EAAO,WAAA,EAAa,UAAA;EAZb;;;;EAqBnB,GAAA,CACL,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EAboC;;;EAsBhC,GAAA,UAAa,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,CAAA;EAX1C;;;EAkBK,GAAA,CAAI,GAAA,EAAK,QAAA,GAAW,OAAA;EAhBxB;;;;EAwBI,MAAA,CAAO,GAAA,EAAK,QAAA,GAAW,OAAA;EAfM;;;EAsB7B,IAAA,UAAc,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,CAAA;EAflC;;;;EAuBJ,OAAA,UAAiB,GAAA,EAAK,QAAA,EAAU,KAAA,EAAO,CAAA,GAAI,OAAA,CAAQ,CAAA;EAf5B;;;;;EA2BjB,KAAA,CAAM,GAAA,EAAK,QAAA,EAAU,KAAA,OAAY,GAAA,YAAe,OAAA;EApBhB;;;EAgDtC,QAAA,UACL,GAAA,EAAK,QAAA,EACL,YAAA,EAAc,QAAA,GAAW,eAAA,EACzB,QAAA,QAAgB,OAAA,CAAQ,CAAA,IACvB,OAAA,CAAQ,CAAA;EA5Ca;;;;EAyDjB,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAiB,OAAA;EA7CpC;;;EAoDN,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAiB,OAAA;EApDH;;;;;EA6DjC,UAAA,IAAc,OAAA;EA/BX;;;;;EAAA,UA8CN,cAAA,CAAe,GAAA,EAAK,QAAA;AAAA"}
@@ -91,8 +91,7 @@ var TaggedScopedCache = class {
91
91
  const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);
92
92
  if (!allTags || allTags.length === 0) return true;
93
93
  const scopedKey = this.buildScopedKey(key);
94
- const parsedKey = this.scope.source.parseKey(scopedKey);
95
- await this.scope.source.tags(allTags).storeTagRelationship(parsedKey);
94
+ await this.scope.source.tags(allTags).storeTagRelationship(scopedKey);
96
95
  return true;
97
96
  }
98
97
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"tagged-scoped-cache.mjs","names":[],"sources":["../../../../../../cache/src/tagged-scoped-cache.ts"],"sourcesContent":["import type { ScopedCache } from \"./scoped-cache\";\nimport type {\n CacheKey,\n CacheSetOptions,\n CacheTtl,\n RememberOptions,\n TaggedScopedCacheContract,\n} from \"./types\";\nimport {\n injectTags,\n mergeTagSets,\n normalizeToOptions,\n normalizeToRememberOptions,\n parseCacheKey,\n} from \"./utils\";\n\n/**\n * One-shot tagged write handle on top of a {@link ScopedCache}.\n *\n * **Role.** Returned by `scope.tags([...])`. Adds a fixed list of tags to\n * every write produced through this handle, on top of any tags the parent\n * scope already contributes. Stateless except for the captured tag list.\n *\n * **Responsibility.**\n * - Owns: appending the handle's tags to writes, delegating tag-index\n * bookkeeping for `setNX` (which lacks an inline `tags` knob on the driver\n * contract), and computing the union for `invalidate()` calls.\n * - Does NOT own: storage, prefix-prepending (delegated to the scope),\n * default `ttl` (delegated to the scope), or any kind of long-lived state.\n *\n * Tags compose additively: scope tags + handle tags + per-call tags, all\n * unioned and deduped. The handle never replaces scope tags — `invalidate()`\n * always sees the full union.\n *\n * @example\n * // Inside application code — scope provides the per-user tag automatically:\n * const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });\n *\n * await feed.tags([\"unread\"]).set(\"messages.1\", message);\n * // → tagged with [user.<id>, unread]\n *\n * await feed.tags([\"unread\"]).invalidate();\n * // → wipes everything tagged with user.<id> OR unread.\n */\nexport class TaggedScopedCache implements TaggedScopedCacheContract {\n /**\n * The {@link ScopedCache} this handle delegates to. Held by reference so\n * scope-default changes (none today, but the option is preserved for the\n * future) are visible without rebuilding the handle.\n */\n protected readonly scope: ScopedCache;\n\n /**\n * Tags this handle contributes to every write. Cloned at the call site so\n * later mutation of the input array doesn't leak in.\n */\n protected readonly handleTags: string[];\n\n /**\n * Build a tagged handle. Constructed via `scope.tags([...])` — users never\n * call this directly.\n */\n public constructor(scope: ScopedCache, handleTags: string[]) {\n this.scope = scope;\n this.handleTags = [...handleTags];\n }\n\n /**\n * Write the scoped key with the handle's tags appended to whatever the\n * caller passed. Scope-level tags are added on top by the scope itself.\n */\n public set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const options = injectTags(normalizeToOptions(ttlOrOptions), this.handleTags);\n\n return this.scope.set(key, value, options);\n }\n\n /**\n * Read the scoped key. Tags don't affect reads — pass-through.\n */\n public get<T = any>(key: CacheKey): Promise<T | null> {\n return this.scope.get<T>(key);\n }\n\n /**\n * Check presence of the scoped key.\n */\n public has(key: CacheKey): Promise<boolean> {\n return this.scope.has(key);\n }\n\n /**\n * Remove the scoped key. The tag-index entry will eventually be cleaned up\n * by `invalidate()`; we don't proactively rewrite it here for cost reasons.\n */\n public remove(key: CacheKey): Promise<void> {\n return this.scope.remove(key);\n }\n\n /**\n * Read-and-remove the scoped key.\n */\n public pull<T = any>(key: CacheKey): Promise<T | null> {\n return this.scope.pull<T>(key);\n }\n\n /**\n * Permanent write with handle tags applied. Bypasses both the scope's and\n * the caller's TTL (forever means forever) — only tags get injected.\n */\n public forever<T = any>(key: CacheKey, value: T): Promise<T> {\n return this.scope.set(key, value, {\n ttl: Infinity,\n tags: this.handleTags,\n }) as Promise<T>;\n }\n\n /**\n * Atomic create-or-skip with the handle's tags applied on success. The\n * driver contract has no inline `tags` knob on `setNX`, so we register the\n * tag relationship manually after a successful write.\n */\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\n const wasSet = await this.scope.setNX(key, value, ttl);\n\n if (!wasSet) {\n return false;\n }\n\n const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);\n\n if (!allTags || allTags.length === 0) {\n return true;\n }\n\n const scopedKey = this.buildScopedKey(key);\n const parsedKey = this.scope.source.parseKey(scopedKey);\n const tagged = this.scope.source.tags(allTags) as unknown as {\n storeTagRelationship: (parsed: string) => Promise<void>;\n };\n\n await tagged.storeTagRelationship(parsedKey);\n\n return true;\n }\n\n /**\n * Read-or-compute with handle tags appended on the cache-miss write.\n */\n public remember<T = any>(\n key: CacheKey,\n ttlOrOptions: CacheTtl | RememberOptions,\n callback: () => Promise<T>,\n ): Promise<T> {\n const options = injectTags(\n normalizeToRememberOptions(ttlOrOptions),\n this.handleTags,\n );\n\n return this.scope.remember<T>(key, options, callback);\n }\n\n /**\n * Atomic counter increment on the scoped key. Tags aren't applied to\n * subsequent increments — they're attached at first-write time.\n */\n public increment(key: CacheKey, value?: number): Promise<number> {\n return this.scope.increment(key, value);\n }\n\n /**\n * Atomic counter decrement on the scoped key. See {@link increment}.\n */\n public decrement(key: CacheKey, value?: number): Promise<number> {\n return this.scope.decrement(key, value);\n }\n\n /**\n * Wipe every entry tagged with the union of (scope tags + handle tags).\n * Tags are global across the package, so this reaches outside the scope's\n * prefix when scope tags are also used elsewhere.\n */\n public async invalidate(): Promise<void> {\n const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);\n\n if (!allTags || allTags.length === 0) {\n return;\n }\n\n await this.scope.source.tags(allTags).invalidate();\n }\n\n /**\n * Compute the source-side key the same way `ScopedCache.scopedKey` does —\n * needed for `setNX`, where we have to register the tag relationship by\n * hand because the driver contract doesn't accept inline tags there.\n */\n protected buildScopedKey(key: CacheKey): string {\n const keyString = typeof key === \"string\" ? key : parseCacheKey(key);\n\n if (!keyString) {\n return this.scope.prefix;\n }\n\n return `${this.scope.prefix}.${keyString}`;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,IAAa,oBAAb,MAAoE;;;;;CAkBlE,AAAO,YAAY,OAAoB,YAAsB;EAC3D,KAAK,QAAQ;EACb,KAAK,aAAa,CAAC,GAAG,UAAU;CAClC;;;;;CAMA,AAAO,IACL,KACA,OACA,cACc;EACd,MAAM,UAAU,WAAW,mBAAmB,YAAY,GAAG,KAAK,UAAU;EAE5E,OAAO,KAAK,MAAM,IAAI,KAAK,OAAO,OAAO;CAC3C;;;;CAKA,AAAO,IAAa,KAAkC;EACpD,OAAO,KAAK,MAAM,IAAO,GAAG;CAC9B;;;;CAKA,AAAO,IAAI,KAAiC;EAC1C,OAAO,KAAK,MAAM,IAAI,GAAG;CAC3B;;;;;CAMA,AAAO,OAAO,KAA8B;EAC1C,OAAO,KAAK,MAAM,OAAO,GAAG;CAC9B;;;;CAKA,AAAO,KAAc,KAAkC;EACrD,OAAO,KAAK,MAAM,KAAQ,GAAG;CAC/B;;;;;CAMA,AAAO,QAAiB,KAAe,OAAsB;EAC3D,OAAO,KAAK,MAAM,IAAI,KAAK,OAAO;GAChC,KAAK;GACL,MAAM,KAAK;EACb,CAAC;CACH;;;;;;CAOA,MAAa,MAAM,KAAe,OAAY,KAAgC;EAG5E,IAAI,CAAC,MAFgB,KAAK,MAAM,MAAM,KAAK,OAAO,GAAG,GAGnD,OAAO;EAGT,MAAM,UAAU,aAAa,KAAK,MAAM,SAAS,MAAM,KAAK,UAAU;EAEtE,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC,OAAO;EAGT,MAAM,YAAY,KAAK,eAAe,GAAG;EACzC,MAAM,YAAY,KAAK,MAAM,OAAO,SAAS,SAAS;EAKtD,MAJe,KAAK,MAAM,OAAO,KAAK,OAI3B,CAAC,CAAC,qBAAqB,SAAS;EAE3C,OAAO;CACT;;;;CAKA,AAAO,SACL,KACA,cACA,UACY;EACZ,MAAM,UAAU,WACd,2BAA2B,YAAY,GACvC,KAAK,UACP;EAEA,OAAO,KAAK,MAAM,SAAY,KAAK,SAAS,QAAQ;CACtD;;;;;CAMA,AAAO,UAAU,KAAe,OAAiC;EAC/D,OAAO,KAAK,MAAM,UAAU,KAAK,KAAK;CACxC;;;;CAKA,AAAO,UAAU,KAAe,OAAiC;EAC/D,OAAO,KAAK,MAAM,UAAU,KAAK,KAAK;CACxC;;;;;;CAOA,MAAa,aAA4B;EACvC,MAAM,UAAU,aAAa,KAAK,MAAM,SAAS,MAAM,KAAK,UAAU;EAEtE,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC;EAGF,MAAM,KAAK,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW;CACnD;;;;;;CAOA,AAAU,eAAe,KAAuB;EAC9C,MAAM,YAAY,OAAO,QAAQ,WAAW,MAAM,cAAc,GAAG;EAEnE,IAAI,CAAC,WACH,OAAO,KAAK,MAAM;EAGpB,OAAO,GAAG,KAAK,MAAM,OAAO,GAAG;CACjC;AACF"}
1
+ {"version":3,"file":"tagged-scoped-cache.mjs","names":[],"sources":["../../../../../../cache/src/tagged-scoped-cache.ts"],"sourcesContent":["import type { ScopedCache } from \"./scoped-cache\";\nimport type {\n CacheKey,\n CacheSetOptions,\n CacheTtl,\n RememberOptions,\n TaggedScopedCacheContract,\n} from \"./types\";\nimport {\n injectTags,\n mergeTagSets,\n normalizeToOptions,\n normalizeToRememberOptions,\n parseCacheKey,\n} from \"./utils\";\n\n/**\n * One-shot tagged write handle on top of a {@link ScopedCache}.\n *\n * **Role.** Returned by `scope.tags([...])`. Adds a fixed list of tags to\n * every write produced through this handle, on top of any tags the parent\n * scope already contributes. Stateless except for the captured tag list.\n *\n * **Responsibility.**\n * - Owns: appending the handle's tags to writes, delegating tag-index\n * bookkeeping for `setNX` (which lacks an inline `tags` knob on the driver\n * contract), and computing the union for `invalidate()` calls.\n * - Does NOT own: storage, prefix-prepending (delegated to the scope),\n * default `ttl` (delegated to the scope), or any kind of long-lived state.\n *\n * Tags compose additively: scope tags + handle tags + per-call tags, all\n * unioned and deduped. The handle never replaces scope tags — `invalidate()`\n * always sees the full union.\n *\n * @example\n * // Inside application code — scope provides the per-user tag automatically:\n * const feed = cache.namespace(`feed.${userId}`, { tags: [`user.${userId}`] });\n *\n * await feed.tags([\"unread\"]).set(\"messages.1\", message);\n * // → tagged with [user.<id>, unread]\n *\n * await feed.tags([\"unread\"]).invalidate();\n * // → wipes everything tagged with user.<id> OR unread.\n */\nexport class TaggedScopedCache implements TaggedScopedCacheContract {\n /**\n * The {@link ScopedCache} this handle delegates to. Held by reference so\n * scope-default changes (none today, but the option is preserved for the\n * future) are visible without rebuilding the handle.\n */\n protected readonly scope: ScopedCache;\n\n /**\n * Tags this handle contributes to every write. Cloned at the call site so\n * later mutation of the input array doesn't leak in.\n */\n protected readonly handleTags: string[];\n\n /**\n * Build a tagged handle. Constructed via `scope.tags([...])` — users never\n * call this directly.\n */\n public constructor(scope: ScopedCache, handleTags: string[]) {\n this.scope = scope;\n this.handleTags = [...handleTags];\n }\n\n /**\n * Write the scoped key with the handle's tags appended to whatever the\n * caller passed. Scope-level tags are added on top by the scope itself.\n */\n public set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const options = injectTags(normalizeToOptions(ttlOrOptions), this.handleTags);\n\n return this.scope.set(key, value, options);\n }\n\n /**\n * Read the scoped key. Tags don't affect reads — pass-through.\n */\n public get<T = any>(key: CacheKey): Promise<T | null> {\n return this.scope.get<T>(key);\n }\n\n /**\n * Check presence of the scoped key.\n */\n public has(key: CacheKey): Promise<boolean> {\n return this.scope.has(key);\n }\n\n /**\n * Remove the scoped key. The tag-index entry will eventually be cleaned up\n * by `invalidate()`; we don't proactively rewrite it here for cost reasons.\n */\n public remove(key: CacheKey): Promise<void> {\n return this.scope.remove(key);\n }\n\n /**\n * Read-and-remove the scoped key.\n */\n public pull<T = any>(key: CacheKey): Promise<T | null> {\n return this.scope.pull<T>(key);\n }\n\n /**\n * Permanent write with handle tags applied. Bypasses both the scope's and\n * the caller's TTL (forever means forever) — only tags get injected.\n */\n public forever<T = any>(key: CacheKey, value: T): Promise<T> {\n return this.scope.set(key, value, {\n ttl: Infinity,\n tags: this.handleTags,\n }) as Promise<T>;\n }\n\n /**\n * Atomic create-or-skip with the handle's tags applied on success. The\n * driver contract has no inline `tags` knob on `setNX`, so we register the\n * tag relationship manually after a successful write.\n */\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\n const wasSet = await this.scope.setNX(key, value, ttl);\n\n if (!wasSet) {\n return false;\n }\n\n const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);\n\n if (!allTags || allTags.length === 0) {\n return true;\n }\n\n // Register the un-prefixed scoped key — the tag index never holds the\n // driver-parsed form, so `invalidate()` can remove it via `remove(key)`.\n const scopedKey = this.buildScopedKey(key);\n const tagged = this.scope.source.tags(allTags) as unknown as {\n storeTagRelationship: (key: CacheKey) => Promise<void>;\n };\n\n await tagged.storeTagRelationship(scopedKey);\n\n return true;\n }\n\n /**\n * Read-or-compute with handle tags appended on the cache-miss write.\n */\n public remember<T = any>(\n key: CacheKey,\n ttlOrOptions: CacheTtl | RememberOptions,\n callback: () => Promise<T>,\n ): Promise<T> {\n const options = injectTags(\n normalizeToRememberOptions(ttlOrOptions),\n this.handleTags,\n );\n\n return this.scope.remember<T>(key, options, callback);\n }\n\n /**\n * Atomic counter increment on the scoped key. Tags aren't applied to\n * subsequent increments — they're attached at first-write time.\n */\n public increment(key: CacheKey, value?: number): Promise<number> {\n return this.scope.increment(key, value);\n }\n\n /**\n * Atomic counter decrement on the scoped key. See {@link increment}.\n */\n public decrement(key: CacheKey, value?: number): Promise<number> {\n return this.scope.decrement(key, value);\n }\n\n /**\n * Wipe every entry tagged with the union of (scope tags + handle tags).\n * Tags are global across the package, so this reaches outside the scope's\n * prefix when scope tags are also used elsewhere.\n */\n public async invalidate(): Promise<void> {\n const allTags = mergeTagSets(this.scope.defaults.tags, this.handleTags);\n\n if (!allTags || allTags.length === 0) {\n return;\n }\n\n await this.scope.source.tags(allTags).invalidate();\n }\n\n /**\n * Compute the source-side key the same way `ScopedCache.scopedKey` does —\n * needed for `setNX`, where we have to register the tag relationship by\n * hand because the driver contract doesn't accept inline tags there.\n */\n protected buildScopedKey(key: CacheKey): string {\n const keyString = typeof key === \"string\" ? key : parseCacheKey(key);\n\n if (!keyString) {\n return this.scope.prefix;\n }\n\n return `${this.scope.prefix}.${keyString}`;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,IAAa,oBAAb,MAAoE;;;;;CAkBlE,AAAO,YAAY,OAAoB,YAAsB;EAC3D,KAAK,QAAQ;EACb,KAAK,aAAa,CAAC,GAAG,UAAU;CAClC;;;;;CAMA,AAAO,IACL,KACA,OACA,cACc;EACd,MAAM,UAAU,WAAW,mBAAmB,YAAY,GAAG,KAAK,UAAU;EAE5E,OAAO,KAAK,MAAM,IAAI,KAAK,OAAO,OAAO;CAC3C;;;;CAKA,AAAO,IAAa,KAAkC;EACpD,OAAO,KAAK,MAAM,IAAO,GAAG;CAC9B;;;;CAKA,AAAO,IAAI,KAAiC;EAC1C,OAAO,KAAK,MAAM,IAAI,GAAG;CAC3B;;;;;CAMA,AAAO,OAAO,KAA8B;EAC1C,OAAO,KAAK,MAAM,OAAO,GAAG;CAC9B;;;;CAKA,AAAO,KAAc,KAAkC;EACrD,OAAO,KAAK,MAAM,KAAQ,GAAG;CAC/B;;;;;CAMA,AAAO,QAAiB,KAAe,OAAsB;EAC3D,OAAO,KAAK,MAAM,IAAI,KAAK,OAAO;GAChC,KAAK;GACL,MAAM,KAAK;EACb,CAAC;CACH;;;;;;CAOA,MAAa,MAAM,KAAe,OAAY,KAAgC;EAG5E,IAAI,CAAC,MAFgB,KAAK,MAAM,MAAM,KAAK,OAAO,GAAG,GAGnD,OAAO;EAGT,MAAM,UAAU,aAAa,KAAK,MAAM,SAAS,MAAM,KAAK,UAAU;EAEtE,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC,OAAO;EAKT,MAAM,YAAY,KAAK,eAAe,GAAG;EAKzC,MAJe,KAAK,MAAM,OAAO,KAAK,OAI3B,CAAC,CAAC,qBAAqB,SAAS;EAE3C,OAAO;CACT;;;;CAKA,AAAO,SACL,KACA,cACA,UACY;EACZ,MAAM,UAAU,WACd,2BAA2B,YAAY,GACvC,KAAK,UACP;EAEA,OAAO,KAAK,MAAM,SAAY,KAAK,SAAS,QAAQ;CACtD;;;;;CAMA,AAAO,UAAU,KAAe,OAAiC;EAC/D,OAAO,KAAK,MAAM,UAAU,KAAK,KAAK;CACxC;;;;CAKA,AAAO,UAAU,KAAe,OAAiC;EAC/D,OAAO,KAAK,MAAM,UAAU,KAAK,KAAK;CACxC;;;;;;CAOA,MAAa,aAA4B;EACvC,MAAM,UAAU,aAAa,KAAK,MAAM,SAAS,MAAM,KAAK,UAAU;EAEtE,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC;EAGF,MAAM,KAAK,MAAM,OAAO,KAAK,OAAO,CAAC,CAAC,WAAW;CACnD;;;;;;CAOA,AAAU,eAAe,KAAuB;EAC9C,MAAM,YAAY,OAAO,QAAQ,WAAW,MAAM,cAAc,GAAG;EAEnE,IAAI,CAAC,WACH,OAAO,KAAK,MAAM;EAGpB,OAAO,GAAG,KAAK,MAAM,OAAO,GAAG;CACjC;AACF"}
package/package.json CHANGED
@@ -5,9 +5,9 @@
5
5
  "environment": "server"
6
6
  },
7
7
  "dependencies": {
8
- "@warlock.js/fs": "5.16.0",
8
+ "@warlock.js/fs": "5.17.0",
9
9
  "@mongez/reinforcements": "^4.0.1",
10
- "@warlock.js/logger": "5.16.0",
10
+ "@warlock.js/logger": "5.17.0",
11
11
  "ms": "^2.1.3"
12
12
  },
13
13
  "peerDependencies": {
@@ -46,7 +46,7 @@
46
46
  ],
47
47
  "author": "hassanzohdy",
48
48
  "license": "MIT",
49
- "version": "5.16.0",
49
+ "version": "5.17.0",
50
50
  "main": "./cjs/index.cjs",
51
51
  "module": "./esm/index.mjs",
52
52
  "types": "./esm/index.d.mts",