@warlock.js/cache 5.29.0 → 5.30.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.
- package/CHANGELOG.md +12 -0
- package/cjs/index.cjs +91 -47
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +1 -1
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +1 -1
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +1 -1
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/auto-key.mjs +1 -1
- package/esm/cached/auto-key.mjs.map +1 -1
- package/esm/cached/cached.d.mts +1 -1
- package/esm/cached/cached.d.mts.map +1 -1
- package/esm/cached/cached.mjs +1 -1
- package/esm/cached/cached.mjs.map +1 -1
- package/esm/cached/normalize-args.d.mts +1 -1
- package/esm/cached/normalize-args.d.mts.map +1 -1
- package/esm/cached/normalize-args.mjs +1 -1
- package/esm/cached/normalize-args.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +11 -5
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +29 -13
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +1 -1
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs +2 -2
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -1
- package/esm/drivers/in-memory-tag-index.mjs +1 -1
- package/esm/drivers/in-memory-tag-index.mjs.map +1 -1
- package/esm/drivers/index.d.mts +1 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +3 -2
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +1 -1
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +3 -2
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +1 -1
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +3 -2
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +5 -1
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/null-cache-driver.mjs +17 -10
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +1 -1
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +3 -2
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +14 -2
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +24 -4
- package/esm/drivers/redis-cache-driver.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/list/memory-cache-list.d.mts +1 -1
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs +1 -1
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/metrics.d.mts +1 -1
- package/esm/metrics.d.mts.map +1 -1
- package/esm/metrics.mjs +1 -1
- package/esm/metrics.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +1 -1
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +1 -1
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +1 -1
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +1 -1
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +1 -1
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +1 -1
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +1 -1
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +1 -1
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs +1 -1
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +2 -0
- package/package.json +5 -4
- package/skills/cache-basics/SKILL.md +2 -0
|
@@ -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, CacheError, CacheUnsupportedError } from \"../types\";\nimport { parseTtl } from \"../utils\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/** Compare-and-set attempts before `update()` gives up under contention. */\nconst PG_UPDATE_MAX_ATTEMPTS = 10;\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 * {@inheritdoc}\n *\n * Single `DELETE … WHERE key AND value` — atomic in Postgres.\n */\n protected async deleteIfEquals(key: CacheKey, expected: unknown): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n const res = await this.pgClient.query(\n `DELETE FROM ${this.table}\n WHERE key = $1 AND value = $2::jsonb AND (expires_at IS NULL OR expires_at > now())\n RETURNING 1`,\n [parsedKey, JSON.stringify(expected)],\n );\n\n if (res.rows.length === 0) {\n return false;\n }\n\n await this.emit(\"removed\", { key: parsedKey });\n\n return true;\n }\n\n /**\n * {@inheritdoc}\n *\n * One `INSERT … ON CONFLICT DO UPDATE` that unions the stored JSON array with\n * `members`. Postgres row-locks the conflicting row, so concurrent adders on\n * any number of servers serialize and none is lost.\n *\n * The index stays a row of the main table holding a JSON array — the same\n * shape earlier versions wrote — so no schema change or migration is needed\n * and existing indexes keep working. (The `tags` column only records tags\n * passed inline to `set()`, not relationships added afterwards, so it can't\n * serve as the invalidation index.)\n */\n public async tagAdd(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const t = this.table;\n\n await this.pgClient.query(\n `/* cache:tag-add */ INSERT INTO ${t} (key, value)\n VALUES ($1, to_jsonb($2::text[]))\n ON CONFLICT (key) DO UPDATE\n SET value = (\n SELECT COALESCE(jsonb_agg(u.member), '[]'::jsonb)\n FROM (\n SELECT x.member FROM jsonb_array_elements_text(\n CASE WHEN jsonb_typeof(${t}.value) = 'array' THEN ${t}.value ELSE '[]'::jsonb END\n ) AS x(member)\n UNION\n SELECT y.member FROM unnest($2::text[]) AS y(member)\n ) AS u\n ),\n expires_at = NULL`,\n [this.parseKey(tagKey), members],\n );\n }\n\n /**\n * {@inheritdoc}\n *\n * One `UPDATE` that filters `members` out of the stored array (members added\n * concurrently are kept), then a conditional `DELETE` of the row if it ended\n * up empty.\n */\n public async tagRemove(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const t = this.table;\n const parsedKey = this.parseKey(tagKey);\n\n await this.pgClient.query(\n `/* cache:tag-remove */ UPDATE ${t}\n SET value = (\n SELECT COALESCE(jsonb_agg(x.member), '[]'::jsonb)\n FROM jsonb_array_elements_text(\n CASE WHEN jsonb_typeof(${t}.value) = 'array' THEN ${t}.value ELSE '[]'::jsonb END\n ) AS x(member)\n WHERE x.member <> ALL($2::text[])\n )\n WHERE key = $1`,\n [parsedKey, members],\n );\n\n await this.pgClient.query(\n `/* cache:tag-drop-empty */ DELETE FROM ${t} WHERE key = $1 AND value = '[]'::jsonb`,\n [parsedKey],\n );\n }\n\n /**\n * TTL in seconds bound to `now() + make_interval(secs => $n)`, so expiry is\n * computed on the DB clock (immune to app/DB clock skew). `null` — from\n * `Infinity` / 0 / undefined — yields a NULL `expires_at` (never expires).\n */\n protected ttlToSeconds(ttl?: number): number | null {\n if (!ttl || ttl === Infinity) {\n return null;\n }\n\n return ttl;\n }\n\n /**\n * Delete expired rows in batches of `limit`. Returns how many were removed.\n * Also invoked opportunistically (~1 in 200 writes) by `set()`.\n */\n public async prune(limit = 1000): Promise<number> {\n const t = this.table;\n const res = await this.pgClient.query(\n `DELETE FROM ${t} WHERE ctid IN (SELECT ctid FROM ${t} WHERE expires_at < now() LIMIT $1)`,\n [limit],\n );\n\n return res.rowCount ?? 0;\n }\n\n /**\n * Fire-and-forget `prune()` on roughly 1 in 200 writes; errors are logged.\n */\n protected maybePrune(): void {\n if (Math.random() >= 1 / 200) {\n return;\n }\n\n this.prune().catch(error => this.logError(\"prune failed\", error));\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.ttlToSeconds(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 = ttl seconds (expires_at = now() + make_interval), $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 = [\n \"$1\",\n \"$2::jsonb\",\n \"now() + make_interval(secs => $3::double precision)\",\n \"$4\",\n \"$5\",\n ];\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 this.maybePrune();\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 this.maybePrune();\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 this.maybePrune();\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 * Decode a JSONB column value (node-postgres usually parses it already;\n * some pools hand back a string).\n */\n protected decodeJson(value: unknown): any {\n if (typeof value === \"string\") {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * One atomic `INSERT … ON CONFLICT DO UPDATE` statement, so concurrent\n * increments from any number of servers never lose an update. A live row\n * keeps its `expires_at`; a missing or expired row restarts from `value`\n * with the driver default TTL. A non-numeric live value is rejected.\n */\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n const expired = `${t}.expires_at IS NOT NULL AND ${t}.expires_at <= now()`;\n\n const { rows } = await this.pgClient.query(\n `/* cache:increment */ INSERT INTO ${t}(key, value, expires_at)\n VALUES ($1, to_jsonb($2::numeric), now() + make_interval(secs => $3::double precision))\n ON CONFLICT (key) DO UPDATE\n SET value = CASE WHEN ${expired} THEN EXCLUDED.value\n ELSE to_jsonb((${t}.value #>> '{}')::numeric + $2::numeric) END,\n expires_at = CASE WHEN ${expired} THEN EXCLUDED.expires_at ELSE ${t}.expires_at END,\n stale_at = CASE WHEN ${expired} THEN NULL ELSE ${t}.stale_at END\n WHERE (${expired}) OR jsonb_typeof(${t}.value) = 'number'\n RETURNING value`,\n [parsedKey, value, this.ttlToSeconds(this.ttl)],\n );\n\n if (rows.length === 0) {\n throw new Error(`Cannot increment non-numeric value for key: ${parsedKey}`);\n }\n\n const newValue = Number(this.decodeJson(rows[0].value));\n\n await this.emit(\"set\", { key: parsedKey, value: newValue, ttl: this.ttl });\n\n return newValue;\n }\n\n /**\n * {@inheritdoc}\n *\n * One atomic `DELETE … RETURNING`, so a value is handed out exactly once\n * across servers. Expired rows count as missing.\n */\n public async pull(key: CacheKey): Promise<any | null> {\n const parsedKey = this.parseKey(key);\n\n const { rows } = await this.pgClient.query(\n `/* cache:pull */ DELETE FROM ${this.table}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())\n RETURNING value`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n const value = this.decodeJson(rows[0].value);\n\n await this.emit(\"hit\", { key: parsedKey, value });\n await this.emit(\"removed\", { key: parsedKey });\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * Atomic across servers via optimistic compare-and-set: read the live value,\n * run `fn`, then write with `UPDATE … WHERE value = <what was read>` (or a\n * create-only insert for a missing key), retrying when another writer got\n * there first. `fn` may run more than once under contention; after\n * {@link PG_UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.\n *\n * Without an explicit `ttl` the row keeps its `expires_at`; a new key gets\n * the driver default TTL. Returning `null` deletes the key (compare-and-delete).\n */\n public async update<T = any>(\n key: CacheKey,\n fn: (current: T | null) => T | null | Promise<T | null>,\n options: { ttl?: CacheTtl } = {},\n ): Promise<T | null> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n const live = `(expires_at IS NULL OR expires_at > now())`;\n const explicitTtl = options.ttl !== undefined ? parseTtl(options.ttl) : undefined;\n\n for (let attempt = 0; attempt < PG_UPDATE_MAX_ATTEMPTS; attempt++) {\n const read = await this.pgClient.query(\n `SELECT value FROM ${t}\n WHERE key = $1 AND ${live}`,\n [parsedKey],\n );\n\n const exists = read.rows.length > 0;\n const current = exists ? (this.decodeJson(read.rows[0].value) as T) : null;\n const expected = exists ? JSON.stringify(current) : null;\n const result = await fn(current);\n\n if (result === null) {\n if (!exists) {\n return null;\n }\n\n const deleted = await this.pgClient.query(\n `/* cache:cas-delete */ DELETE FROM ${t}\n WHERE key = $1 AND value = $2::jsonb AND ${live}\n RETURNING 1`,\n [parsedKey, expected],\n );\n\n if (deleted.rows.length > 0) {\n await this.emit(\"removed\", { key: parsedKey });\n\n return null;\n }\n\n continue;\n }\n\n if (!exists) {\n const created = (await this.set(key, result, {\n onConflict: \"create\",\n ...(explicitTtl !== undefined ? { ttl: explicitTtl } : {}),\n })) as CacheSetResult;\n\n if (created.wasSet) {\n return result;\n }\n\n continue;\n }\n\n const params: unknown[] = [parsedKey, JSON.stringify(result), expected];\n let expirySql = \"\";\n\n if (explicitTtl !== undefined) {\n params.push(this.ttlToSeconds(explicitTtl));\n expirySql = `, expires_at = now() + make_interval(secs => $4::double precision)`;\n }\n\n const updated = await this.pgClient.query(\n `/* cache:cas-update */ UPDATE ${t}\n SET value = $2::jsonb, stale_at = NULL${expirySql}\n WHERE key = $1 AND value = $3::jsonb AND ${live}\n RETURNING 1`,\n params,\n );\n\n if (updated.rows.length > 0) {\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: explicitTtl });\n\n return result;\n }\n }\n\n throw new CacheError(\n `cache.update(): '${parsedKey}' changed on every attempt (${PG_UPDATE_MAX_ATTEMPTS} tries) — too much write contention.`,\n );\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":["../../../../../../../../packages/cache/src/drivers/pg-cache-driver.ts"],"sourcesContent":["import { maskEmailsInText } from \"@warlock.js/logger\";\nimport 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, CacheError, CacheUnsupportedError } from \"../types\";\nimport { parseTtl } from \"../utils\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/** Compare-and-set attempts before `update()` gives up under contention. */\nconst PG_UPDATE_MAX_ATTEMPTS = 10;\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 * {@inheritdoc}\n *\n * Single `DELETE … WHERE key AND value` — atomic in Postgres.\n */\n protected async deleteIfEquals(key: CacheKey, expected: unknown): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n const res = await this.pgClient.query(\n `DELETE FROM ${this.table}\n WHERE key = $1 AND value = $2::jsonb AND (expires_at IS NULL OR expires_at > now())\n RETURNING 1`,\n [parsedKey, JSON.stringify(expected)],\n );\n\n if (res.rows.length === 0) {\n return false;\n }\n\n await this.emit(\"removed\", { key: parsedKey });\n\n return true;\n }\n\n /**\n * {@inheritdoc}\n *\n * One `INSERT … ON CONFLICT DO UPDATE` that unions the stored JSON array with\n * `members`. Postgres row-locks the conflicting row, so concurrent adders on\n * any number of servers serialize and none is lost.\n *\n * The index stays a row of the main table holding a JSON array — the same\n * shape earlier versions wrote — so no schema change or migration is needed\n * and existing indexes keep working. (The `tags` column only records tags\n * passed inline to `set()`, not relationships added afterwards, so it can't\n * serve as the invalidation index.)\n */\n public async tagAdd(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const t = this.table;\n\n await this.pgClient.query(\n `/* cache:tag-add */ INSERT INTO ${t} (key, value)\n VALUES ($1, to_jsonb($2::text[]))\n ON CONFLICT (key) DO UPDATE\n SET value = (\n SELECT COALESCE(jsonb_agg(u.member), '[]'::jsonb)\n FROM (\n SELECT x.member FROM jsonb_array_elements_text(\n CASE WHEN jsonb_typeof(${t}.value) = 'array' THEN ${t}.value ELSE '[]'::jsonb END\n ) AS x(member)\n UNION\n SELECT y.member FROM unnest($2::text[]) AS y(member)\n ) AS u\n ),\n expires_at = NULL`,\n [this.parseKey(tagKey), members],\n );\n }\n\n /**\n * {@inheritdoc}\n *\n * One `UPDATE` that filters `members` out of the stored array (members added\n * concurrently are kept), then a conditional `DELETE` of the row if it ended\n * up empty.\n */\n public async tagRemove(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const t = this.table;\n const parsedKey = this.parseKey(tagKey);\n\n await this.pgClient.query(\n `/* cache:tag-remove */ UPDATE ${t}\n SET value = (\n SELECT COALESCE(jsonb_agg(x.member), '[]'::jsonb)\n FROM jsonb_array_elements_text(\n CASE WHEN jsonb_typeof(${t}.value) = 'array' THEN ${t}.value ELSE '[]'::jsonb END\n ) AS x(member)\n WHERE x.member <> ALL($2::text[])\n )\n WHERE key = $1`,\n [parsedKey, members],\n );\n\n await this.pgClient.query(\n `/* cache:tag-drop-empty */ DELETE FROM ${t} WHERE key = $1 AND value = '[]'::jsonb`,\n [parsedKey],\n );\n }\n\n /**\n * TTL in seconds bound to `now() + make_interval(secs => $n)`, so expiry is\n * computed on the DB clock (immune to app/DB clock skew). `null` — from\n * `Infinity` / 0 / undefined — yields a NULL `expires_at` (never expires).\n */\n protected ttlToSeconds(ttl?: number): number | null {\n if (!ttl || ttl === Infinity) {\n return null;\n }\n\n return ttl;\n }\n\n /**\n * Delete expired rows in batches of `limit`. Returns how many were removed.\n * Also invoked opportunistically (~1 in 200 writes) by `set()`.\n */\n public async prune(limit = 1000): Promise<number> {\n const t = this.table;\n const res = await this.pgClient.query(\n `DELETE FROM ${t} WHERE ctid IN (SELECT ctid FROM ${t} WHERE expires_at < now() LIMIT $1)`,\n [limit],\n );\n\n return res.rowCount ?? 0;\n }\n\n /**\n * Fire-and-forget `prune()` on roughly 1 in 200 writes; errors are logged.\n */\n protected maybePrune(): void {\n if (Math.random() >= 1 / 200) {\n return;\n }\n\n this.prune().catch(error => this.logError(\"prune failed\", error));\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.ttlToSeconds(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 = ttl seconds (expires_at = now() + make_interval), $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 = [\n \"$1\",\n \"$2::jsonb\",\n \"now() + make_interval(secs => $3::double precision)\",\n \"$4\",\n \"$5\",\n ];\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 this.maybePrune();\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 this.maybePrune();\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 this.maybePrune();\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 * Decode a JSONB column value (node-postgres usually parses it already;\n * some pools hand back a string).\n */\n protected decodeJson(value: unknown): any {\n if (typeof value === \"string\") {\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n }\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * One atomic `INSERT … ON CONFLICT DO UPDATE` statement, so concurrent\n * increments from any number of servers never lose an update. A live row\n * keeps its `expires_at`; a missing or expired row restarts from `value`\n * with the driver default TTL. A non-numeric live value is rejected.\n */\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n const expired = `${t}.expires_at IS NOT NULL AND ${t}.expires_at <= now()`;\n\n const { rows } = await this.pgClient.query(\n `/* cache:increment */ INSERT INTO ${t}(key, value, expires_at)\n VALUES ($1, to_jsonb($2::numeric), now() + make_interval(secs => $3::double precision))\n ON CONFLICT (key) DO UPDATE\n SET value = CASE WHEN ${expired} THEN EXCLUDED.value\n ELSE to_jsonb((${t}.value #>> '{}')::numeric + $2::numeric) END,\n expires_at = CASE WHEN ${expired} THEN EXCLUDED.expires_at ELSE ${t}.expires_at END,\n stale_at = CASE WHEN ${expired} THEN NULL ELSE ${t}.stale_at END\n WHERE (${expired}) OR jsonb_typeof(${t}.value) = 'number'\n RETURNING value`,\n [parsedKey, value, this.ttlToSeconds(this.ttl)],\n );\n\n if (rows.length === 0) {\n throw new Error(`Cannot increment non-numeric value for key: ${maskEmailsInText(parsedKey)}`);\n }\n\n const newValue = Number(this.decodeJson(rows[0].value));\n\n await this.emit(\"set\", { key: parsedKey, value: newValue, ttl: this.ttl });\n\n return newValue;\n }\n\n /**\n * {@inheritdoc}\n *\n * One atomic `DELETE … RETURNING`, so a value is handed out exactly once\n * across servers. Expired rows count as missing.\n */\n public async pull(key: CacheKey): Promise<any | null> {\n const parsedKey = this.parseKey(key);\n\n const { rows } = await this.pgClient.query(\n `/* cache:pull */ DELETE FROM ${this.table}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())\n RETURNING value`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n const value = this.decodeJson(rows[0].value);\n\n await this.emit(\"hit\", { key: parsedKey, value });\n await this.emit(\"removed\", { key: parsedKey });\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * Atomic across servers via optimistic compare-and-set: read the live value,\n * run `fn`, then write with `UPDATE … WHERE value = <what was read>` (or a\n * create-only insert for a missing key), retrying when another writer got\n * there first. `fn` may run more than once under contention; after\n * {@link PG_UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.\n *\n * Without an explicit `ttl` the row keeps its `expires_at`; a new key gets\n * the driver default TTL. Returning `null` deletes the key (compare-and-delete).\n */\n public async update<T = any>(\n key: CacheKey,\n fn: (current: T | null) => T | null | Promise<T | null>,\n options: { ttl?: CacheTtl } = {},\n ): Promise<T | null> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n const live = `(expires_at IS NULL OR expires_at > now())`;\n const explicitTtl = options.ttl !== undefined ? parseTtl(options.ttl) : undefined;\n\n for (let attempt = 0; attempt < PG_UPDATE_MAX_ATTEMPTS; attempt++) {\n const read = await this.pgClient.query(\n `SELECT value FROM ${t}\n WHERE key = $1 AND ${live}`,\n [parsedKey],\n );\n\n const exists = read.rows.length > 0;\n const current = exists ? (this.decodeJson(read.rows[0].value) as T) : null;\n const expected = exists ? JSON.stringify(current) : null;\n const result = await fn(current);\n\n if (result === null) {\n if (!exists) {\n return null;\n }\n\n const deleted = await this.pgClient.query(\n `/* cache:cas-delete */ DELETE FROM ${t}\n WHERE key = $1 AND value = $2::jsonb AND ${live}\n RETURNING 1`,\n [parsedKey, expected],\n );\n\n if (deleted.rows.length > 0) {\n await this.emit(\"removed\", { key: parsedKey });\n\n return null;\n }\n\n continue;\n }\n\n if (!exists) {\n const created = (await this.set(key, result, {\n onConflict: \"create\",\n ...(explicitTtl !== undefined ? { ttl: explicitTtl } : {}),\n })) as CacheSetResult;\n\n if (created.wasSet) {\n return result;\n }\n\n continue;\n }\n\n const params: unknown[] = [parsedKey, JSON.stringify(result), expected];\n let expirySql = \"\";\n\n if (explicitTtl !== undefined) {\n params.push(this.ttlToSeconds(explicitTtl));\n expirySql = `, expires_at = now() + make_interval(secs => $4::double precision)`;\n }\n\n const updated = await this.pgClient.query(\n `/* cache:cas-update */ UPDATE ${t}\n SET value = $2::jsonb, stale_at = NULL${expirySql}\n WHERE key = $1 AND value = $3::jsonb AND ${live}\n RETURNING 1`,\n params,\n );\n\n if (updated.rows.length > 0) {\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: explicitTtl });\n\n return result;\n }\n }\n\n throw new CacheError(\n `cache.update(): '${parsedKey}' changed on every attempt (${PG_UPDATE_MAX_ATTEMPTS} tries) — too much write contention.`,\n );\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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"}
|
|
@@ -2,7 +2,7 @@ import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
|
2
2
|
import { CacheData, CacheDriver, CacheKey, CacheSetOptions, CacheTtl, RedisOptions } from "../types.mjs";
|
|
3
3
|
import { createClient } from "redis";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../../packages/cache/src/drivers/redis-cache-driver.d.ts
|
|
6
6
|
/** KEYS[1]=key, ARGV[1]=expected raw value. 1 when deleted, 0 when not the owner. */
|
|
7
7
|
declare const REDIS_COMPARE_DELETE_SCRIPT = "if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) return 1 else return 0 end";
|
|
8
8
|
/**
|
|
@@ -19,6 +19,18 @@ declare const REDIS_COMPARE_DELETE_SCRIPT = "if redis.call('GET', KEYS[1]) == AR
|
|
|
19
19
|
declare const REDIS_CAS_SET_SCRIPT = "local cur = redis.call('GET', KEYS[1])\nif ARGV[3] == '1' then\n if cur then return 0 end\nelseif cur ~= ARGV[1] then\n return 0\nend\nlocal ttl = tonumber(ARGV[4])\nif ARGV[5] == 'keep' and ARGV[3] ~= '1' then\n redis.call('SET', KEYS[1], ARGV[2], 'KEEPTTL')\nelseif ttl > 0 then\n redis.call('SET', KEYS[1], ARGV[2], 'EX', ttl)\nelse\n redis.call('SET', KEYS[1], ARGV[2])\nend\nreturn 1";
|
|
20
20
|
/** KEYS[1]=key. Fallback for clients without `getDel`: returns the value and deletes it. */
|
|
21
21
|
declare const REDIS_GET_DEL_SCRIPT = "local v = redis.call('GET', KEYS[1])\nif v then redis.call('DEL', KEYS[1]) end\nreturn v";
|
|
22
|
+
/**
|
|
23
|
+
* Increment that gives a counter without an expiry the driver's default TTL,
|
|
24
|
+
* in one round trip.
|
|
25
|
+
*
|
|
26
|
+
* KEYS[1]=key; ARGV[1]=amount; ARGV[2]=ttl seconds ('0' = none).
|
|
27
|
+
*
|
|
28
|
+
* Replaces `INCRBY` + `EXPIRE … NX`: the `NX` flag needs Redis >= 7.0, and on
|
|
29
|
+
* Redis 6 the second command failed after the count had already gone up.
|
|
30
|
+
* `TTL == -1` means "exists without an expiry", so a TTL set earlier (by
|
|
31
|
+
* `set(..., { ttl })`) is never shortened or reset.
|
|
32
|
+
*/
|
|
33
|
+
declare const REDIS_INCREMENT_SCRIPT = "local v = redis.call('INCRBY', KEYS[1], ARGV[1])\nlocal ttl = tonumber(ARGV[2])\nif ttl > 0 and redis.call('TTL', KEYS[1]) == -1 then\n redis.call('EXPIRE', KEYS[1], ttl)\nend\nreturn v";
|
|
22
34
|
declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions> implements CacheDriver<ReturnType<typeof createClient>, RedisOptions> {
|
|
23
35
|
/**
|
|
24
36
|
* Cache driver name
|
|
@@ -167,5 +179,5 @@ declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createC
|
|
|
167
179
|
setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean>;
|
|
168
180
|
}
|
|
169
181
|
//#endregion
|
|
170
|
-
export { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver };
|
|
182
|
+
export { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, REDIS_INCREMENT_SCRIPT, RedisCacheDriver };
|
|
171
183
|
//# sourceMappingURL=redis-cache-driver.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-cache-driver.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"redis-cache-driver.d.mts","names":[],"sources":["../../../../../../../../packages/cache/src/drivers/redis-cache-driver.ts"],"mappings":";;;;;;cA4Da,2BAAA;AAAb;;;;AAAwC;AAaxC;;;;AAAiC;AAiBjC;AA9BA,cAaa,oBAAA;;cAiBA,oBAAA;AAAoB;AAejC;;;;AAAmC;AAiBnC;;;;;AAhCiC,cAepB,sBAAA;AAAA,cAiBA,gBAAA,SACH,eAAA,CAAgB,UAAA,QAAkB,YAAA,GAAe,YAAA,aAC9C,WAAA,CAAY,UAAA,QAAkB,YAAA,GAAe,YAAA;EAAjC;;;EAKhB,IAAA;EAwFqC;;;EAnFrC,UAAA,CAAW,OAAA,EAAS,YAAA;EAqHgB;;;EAxG9B,eAAA,CAAgB,SAAA,WAAiB,OAAA;EAgIJ;;;;EAAA,UA1D1B,UAAA,CAAW,IAAA,aAAiB,OAAA;EA2GrC;;;;;;;EAtFM,MAAA,CAAO,MAAA,EAAQ,QAAA,EAAU,OAAA,aAAoB,OAAA;EAyMjB;;;EA5L5B,UAAA,CAAW,MAAA,EAAQ,QAAA,GAAW,OAAA;EAuOb;;;;;EA5NjB,SAAA,CAAU,MAAA,EAAQ,QAAA,EAAU,OAAA,aAAoB,OAAA;EA+YtC;;;EAlYV,SAAA,CAAU,MAAA,EAAQ,QAAA,GAAW,OAAA;EAyab;;;;;;;;EAAA,UA7Zb,UAAA,IAAc,SAAA,UAAmB,GAAA,GAAM,MAAA,UAAgB,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;EA6flC;;;EAzd7C,GAAA,CACX,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EA3MQ;;;;;;EAAA,UA0RK,cAAA,CAAe,GAAA,EAAK,QAAA,EAAU,QAAA,YAAoB,OAAA;EA1RvD;;;;EAAA,UAiTD,UAAA,CAAW,SAAA;EAvSd;;;;;;EAAA,UAiTS,QAAA,CAAS,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,SAAA;EA9NtB;;;;;;;EAAA,UAuPX,eAAA,CAAgB,GAAA,EAAK,QAAA,GAAW,OAAA;EArNhB;;;EAuOnB,GAAA,CAAI,GAAA,EAAK,QAAA,GAAQ,OAAA;EA5NC;;;EA2QlB,MAAA,CAAO,GAAA,EAAK,QAAA,GAAQ,OAAA;EA9PpB;;;;;;;EAqRA,KAAA,IAAK,OAAA;EAzQqD;;;EA2R1D,OAAA,IAAO,OAAA;EA3RwE;;;;;;;;EAmW/E,UAAA,IAAU,OAAA;EA5OP;;;;;;EA+PH,IAAA,CAAK,GAAA,EAAK,QAAA,GAAW,OAAA;EA9NlB;;;;;;;;;;;;EAmQH,MAAA,UACX,GAAA,EAAK,QAAA,EACL,EAAA,GAAK,OAAA,EAAS,CAAA,YAAa,CAAA,UAAW,OAAA,CAAQ,CAAA,UAC9C,OAAA;IAAW,GAAA,GAAM,QAAA;EAAA,IAChB,OAAA,CAAQ,CAAA;EA7KS;;;;EA6OP,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EApMtC;;;;EAkOP,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EAvIxC;;;;EA0JL,KAAA,CAAM,GAAA,EAAK,QAAA,EAAU,KAAA,OAAY,GAAA,YAAe,OAAA;AAAA"}
|
|
@@ -3,7 +3,7 @@ import { parseTtl, safeErrorInfo } from "../utils.mjs";
|
|
|
3
3
|
import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
4
4
|
import { log } from "@warlock.js/logger";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../../packages/cache/src/drivers/redis-cache-driver.ts
|
|
7
7
|
/**
|
|
8
8
|
* Cached Redis module (loaded once, reused)
|
|
9
9
|
*/
|
|
@@ -67,6 +67,23 @@ return 1`;
|
|
|
67
67
|
const REDIS_GET_DEL_SCRIPT = `local v = redis.call('GET', KEYS[1])
|
|
68
68
|
if v then redis.call('DEL', KEYS[1]) end
|
|
69
69
|
return v`;
|
|
70
|
+
/**
|
|
71
|
+
* Increment that gives a counter without an expiry the driver's default TTL,
|
|
72
|
+
* in one round trip.
|
|
73
|
+
*
|
|
74
|
+
* KEYS[1]=key; ARGV[1]=amount; ARGV[2]=ttl seconds ('0' = none).
|
|
75
|
+
*
|
|
76
|
+
* Replaces `INCRBY` + `EXPIRE … NX`: the `NX` flag needs Redis >= 7.0, and on
|
|
77
|
+
* Redis 6 the second command failed after the count had already gone up.
|
|
78
|
+
* `TTL == -1` means "exists without an expiry", so a TTL set earlier (by
|
|
79
|
+
* `set(..., { ttl })`) is never shortened or reset.
|
|
80
|
+
*/
|
|
81
|
+
const REDIS_INCREMENT_SCRIPT = `local v = redis.call('INCRBY', KEYS[1], ARGV[1])
|
|
82
|
+
local ttl = tonumber(ARGV[2])
|
|
83
|
+
if ttl > 0 and redis.call('TTL', KEYS[1]) == -1 then
|
|
84
|
+
redis.call('EXPIRE', KEYS[1], ttl)
|
|
85
|
+
end
|
|
86
|
+
return v`;
|
|
70
87
|
/** Compare-and-set attempts before `update()` gives up under contention. */
|
|
71
88
|
const UPDATE_MAX_ATTEMPTS = 10;
|
|
72
89
|
/** Keys deleted per UNLINK/DEL while draining a namespace. */
|
|
@@ -498,8 +515,11 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
498
515
|
async increment(key, value = 1) {
|
|
499
516
|
const parsedKey = this.parseKey(key);
|
|
500
517
|
this.log("caching", parsedKey);
|
|
501
|
-
const
|
|
502
|
-
|
|
518
|
+
const defaultTtl = Number.isFinite(this.ttl) && this.ttl > 0 ? Math.ceil(this.ttl) : 0;
|
|
519
|
+
const result = defaultTtl > 0 ? Number(await this.client?.eval(REDIS_INCREMENT_SCRIPT, {
|
|
520
|
+
keys: [parsedKey],
|
|
521
|
+
arguments: [String(value), String(defaultTtl)]
|
|
522
|
+
})) : await this.client?.incrBy(parsedKey, value);
|
|
503
523
|
this.log("cached", parsedKey);
|
|
504
524
|
await this.emit("set", {
|
|
505
525
|
key: parsedKey,
|
|
@@ -552,5 +572,5 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
552
572
|
};
|
|
553
573
|
|
|
554
574
|
//#endregion
|
|
555
|
-
export { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver };
|
|
575
|
+
export { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, REDIS_INCREMENT_SCRIPT, RedisCacheDriver };
|
|
556
576
|
//# sourceMappingURL=redis-cache-driver.mjs.map
|
|
@@ -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\";\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, CacheError, CacheUnsupportedError } from \"../types\";\nimport { parseTtl, 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// Lua scripts (atomic ownership primitives)\n// ============================================================\n\n/** KEYS[1]=key, ARGV[1]=expected raw value. 1 when deleted, 0 when not the owner. */\nexport const REDIS_COMPARE_DELETE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) return 1 else return 0 end`;\n\n/**\n * Compare-and-set used by `update()`.\n *\n * KEYS[1]=key; ARGV[1]=expected raw value (ignored when ARGV[3]=='1');\n * ARGV[2]=new raw value; ARGV[3]='1' when the key is expected to be missing;\n * ARGV[4]=ttl seconds to apply ('0' = no expiry); ARGV[5]='keep' to keep the\n * existing TTL instead (only when the key existed).\n *\n * Returns 1 when written, 0 when the current value no longer matches.\n * `KEEPTTL` requires Redis >= 6.\n */\nexport const REDIS_CAS_SET_SCRIPT = `local cur = redis.call('GET', KEYS[1])\nif ARGV[3] == '1' then\n if cur then return 0 end\nelseif cur ~= ARGV[1] then\n return 0\nend\nlocal ttl = tonumber(ARGV[4])\nif ARGV[5] == 'keep' and ARGV[3] ~= '1' then\n redis.call('SET', KEYS[1], ARGV[2], 'KEEPTTL')\nelseif ttl > 0 then\n redis.call('SET', KEYS[1], ARGV[2], 'EX', ttl)\nelse\n redis.call('SET', KEYS[1], ARGV[2])\nend\nreturn 1`;\n\n/** KEYS[1]=key. Fallback for clients without `getDel`: returns the value and deletes it. */\nexport const REDIS_GET_DEL_SCRIPT = `local v = redis.call('GET', KEYS[1])\nif v then redis.call('DEL', KEYS[1]) end\nreturn v`;\n\n/** Compare-and-set attempts before `update()` gives up under contention. */\nconst UPDATE_MAX_ATTEMPTS = 10;\n\n/** Keys deleted per UNLINK/DEL while draining a namespace. */\nconst REMOVE_BATCH_SIZE = 500;\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 escaped = namespace.replace(/[\\\\*?[\\]]/g, \"\\\\$&\");\n\n // Match the namespace key itself plus `<ns>.*` — never bare `<ns>*`, which\n // would also swallow siblings like `users2`. An empty namespace (no prefix)\n // means \"everything\".\n const pattern = escaped === \"\" ? \"*\" : `${escaped}.*`;\n\n const deleted: string[] = [];\n\n if (this.client) {\n if (escaped !== \"\") {\n const removedSelf = await this.deleteKeys([namespace]);\n\n if (removedSelf > 0) {\n deleted.push(namespace);\n }\n }\n\n // `SCAN` (cursor-based, non-blocking) instead of `KEYS` — `KEYS` is O(N)\n // and blocks the single-threaded Redis event loop. Delete per batch while\n // scanning so memory stays bounded on large keyspaces.\n let batch: string[] = [];\n\n for await (const scanned of this.client.scanIterator({\n MATCH: pattern,\n COUNT: 100,\n })) {\n // node-redis 5 yields a batch of keys for each SCAN cursor response;\n // older supported clients yielded one key at a time. Normalize both\n // forms before passing keys to the command parser.\n const keys = Array.isArray(scanned) ? scanned : [scanned];\n\n for (const key of keys) {\n batch.push(key as unknown as string);\n\n if (batch.length >= REMOVE_BATCH_SIZE) {\n await this.deleteKeys(batch);\n deleted.push(...batch);\n batch = [];\n }\n }\n }\n\n if (batch.length > 0) {\n await this.deleteKeys(batch);\n deleted.push(...batch);\n }\n }\n\n if (deleted.length === 0) {\n this.log(\"notFound\", namespace);\n return;\n }\n\n this.log(\"cleared\", namespace);\n\n return deleted;\n }\n\n /**\n * Delete keys with `UNLINK` (non-blocking free), falling back to `DEL` when\n * the client lacks it. Returns the number of keys removed.\n */\n protected async deleteKeys(keys: string[]): Promise<number> {\n const client = this.client as any;\n\n if (typeof client.unlink === \"function\") {\n return Number(await client.unlink(keys)) || 0;\n }\n\n return Number(await client.del(keys)) || 0;\n }\n\n // ============================================================\n // Tag index — a native Redis SET per tag\n // ============================================================\n\n /**\n * {@inheritdoc}\n *\n * `SADD` — atomic across servers, so concurrent tagged writes on any number\n * of instances never drop a member. The set key lives inside the prefix\n * namespace, so `flush()` / `removeNamespace()` sweep it too.\n */\n public async tagAdd(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const parsedKey = this.parseKey(tagKey);\n\n await this.withTagSet(parsedKey, (client) => client.sAdd(parsedKey, members));\n }\n\n /**\n * {@inheritdoc}\n */\n public async tagMembers(tagKey: CacheKey): Promise<string[]> {\n const parsedKey = this.parseKey(tagKey);\n\n return this.withTagSet(parsedKey, async (client) => (await client.sMembers(parsedKey)) ?? []);\n }\n\n /**\n * {@inheritdoc}\n *\n * `SREM` — Redis drops the set once its last member is removed.\n */\n public async tagRemove(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const parsedKey = this.parseKey(tagKey);\n\n await this.withTagSet(parsedKey, (client) => client.sRem(parsedKey, members));\n }\n\n /**\n * {@inheritdoc}\n */\n public async tagDelete(tagKey: CacheKey): Promise<void> {\n await this.client?.del(this.parseKey(tagKey));\n }\n\n /**\n * Run a SET command against a tag key, upgrading a pre-5.20 index first.\n *\n * Earlier versions stored the index as a JSON-array STRING under the same\n * key, which makes any SET command fail with `WRONGTYPE`. On that error the\n * legacy array is read, the string deleted, its members re-added with\n * `SADD`, and the command retried once.\n */\n protected async withTagSet<T>(parsedKey: string, run: (client: any) => Promise<T>): Promise<T> {\n const client = this.client as any;\n\n try {\n return await run(client);\n } catch (error) {\n if (!/WRONGTYPE/i.test(String((error as Error)?.message ?? error))) {\n throw error;\n }\n\n const raw: string | null = await client.get(parsedKey);\n let legacy: unknown = [];\n\n try {\n legacy = raw === null ? [] : JSON.parse(raw);\n } catch {\n legacy = [];\n }\n\n await client.del(parsedKey);\n\n const members = Array.isArray(legacy)\n ? legacy.filter((member): member is string => typeof member === \"string\")\n : [];\n\n if (members.length > 0) {\n await client.sAdd(parsedKey, members);\n }\n\n return run(client);\n }\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 // A plain write invalidates any freshness marker left by an earlier SWR\n // write, otherwise the old `staleAt` would apply to the new value.\n await this.client?.del(this.swrMetaKey(parsedKey));\n } else {\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 * {@inheritdoc}\n *\n * Values are stored as `JSON.stringify(value)`, so the compare runs against\n * the serialized form inside Lua.\n */\n protected async deleteIfEquals(key: CacheKey, expected: unknown): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n const deleted = await (this.client as any).eval(REDIS_COMPARE_DELETE_SCRIPT, {\n keys: [parsedKey],\n arguments: [JSON.stringify(expected)],\n });\n\n if (Number(deleted) !== 1) {\n return false;\n }\n\n // Mirror remove(): drop the SWR sidecar and emit the event.\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"removed\", { key: parsedKey });\n\n return true;\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 `${parsedKey}::swrmeta`;\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 // Two separate DELs (not one multi-key DEL) so it stays cluster-slot safe.\n await this.client?.del(key);\n await this.client?.del(this.swrMetaKey(key));\n\n this.log(\"removed\", key);\n\n await this.emit(\"removed\", { key });\n }\n\n /**\n * {@inheritDoc}\n *\n * WARNING: without a `globalPrefix` this runs `FLUSHDB`, which clears the\n * WHOLE selected Redis database (including keys not written by this driver).\n * Configure a `globalPrefix` to only delete this driver's keys.\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?.flushDb();\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 // Drop the half-initialised client so a later connect() can retry.\n this.clientDriver = undefined as unknown as typeof this.clientDriver;\n\n throw 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 * {@inheritdoc}\n *\n * Atomic across servers: `GETDEL` (Redis >= 6.2), or an equivalent Lua\n * script when the client has no `getDel`. A value is handed out once.\n */\n public async pull(key: CacheKey): Promise<any | null> {\n const parsedKey = this.parseKey(key);\n const client = this.client as any;\n\n const raw: string | null | undefined =\n typeof client.getDel === \"function\"\n ? await client.getDel(parsedKey)\n : await client.eval(REDIS_GET_DEL_SCRIPT, { keys: [parsedKey], arguments: [] });\n\n if (raw === null || raw === undefined) {\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n await this.client?.del(this.swrMetaKey(parsedKey));\n\n const value = JSON.parse(raw);\n\n await this.emit(\"hit\", { key: parsedKey, value });\n await this.emit(\"removed\", { key: parsedKey });\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * Atomic across servers via optimistic compare-and-set: read the raw value,\n * run `fn`, then write only if the value is still what was read (Lua), and\n * retry otherwise. `fn` may therefore run more than once under contention;\n * after {@link UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.\n *\n * Without an explicit `ttl` the existing expiry is kept (`KEEPTTL`); a new\n * key gets the driver default TTL. Returning `null` deletes the key\n * (compare-and-delete).\n */\n public async update<T = any>(\n key: CacheKey,\n fn: (current: T | null) => T | null | Promise<T | null>,\n options: { ttl?: CacheTtl } = {},\n ): Promise<T | null> {\n const parsedKey = this.parseKey(key);\n const client = this.client as any;\n\n const explicitTtl = options.ttl !== undefined ? parseTtl(options.ttl) : undefined;\n\n for (let attempt = 0; attempt < UPDATE_MAX_ATTEMPTS; attempt++) {\n const raw: string | null = (await client.get(parsedKey)) ?? null;\n const current = raw === null ? null : (JSON.parse(raw) as T);\n const result = await fn(current);\n\n if (result === null) {\n if (raw === null) {\n return null;\n }\n\n const deleted = await client.eval(REDIS_COMPARE_DELETE_SCRIPT, {\n keys: [parsedKey],\n arguments: [raw],\n });\n\n if (Number(deleted) === 1) {\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"removed\", { key: parsedKey });\n\n return null;\n }\n\n continue;\n }\n\n const isNew = raw === null;\n const ttlSeconds = explicitTtl ?? (isNew ? this.ttl : 0);\n const keep = explicitTtl === undefined && !isNew;\n\n const written = await client.eval(REDIS_CAS_SET_SCRIPT, {\n keys: [parsedKey],\n arguments: [\n raw ?? \"\",\n JSON.stringify(result),\n isNew ? \"1\" : \"0\",\n String(Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : 0),\n keep ? \"keep\" : \"\",\n ],\n });\n\n if (Number(written) === 1) {\n // A plain write invalidates any SWR freshness marker.\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: ttlSeconds });\n\n return result;\n }\n }\n\n throw new CacheError(\n `cache.update(): '${parsedKey}' changed on every attempt (${UPDATE_MAX_ATTEMPTS} tries) — too much write contention.`,\n );\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 // A fresh counter has no TTL; apply the driver default (only if none set).\n if (this.ttl !== Infinity && this.ttl > 0) {\n await this.client?.expire(parsedKey, this.ttl, \"NX\");\n }\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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"}
|
|
1
|
+
{"version":3,"file":"redis-cache-driver.mjs","names":[],"sources":["../../../../../../../../packages/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, CacheError, CacheUnsupportedError } from \"../types\";\nimport { parseTtl, 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// Lua scripts (atomic ownership primitives)\n// ============================================================\n\n/** KEYS[1]=key, ARGV[1]=expected raw value. 1 when deleted, 0 when not the owner. */\nexport const REDIS_COMPARE_DELETE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) return 1 else return 0 end`;\n\n/**\n * Compare-and-set used by `update()`.\n *\n * KEYS[1]=key; ARGV[1]=expected raw value (ignored when ARGV[3]=='1');\n * ARGV[2]=new raw value; ARGV[3]='1' when the key is expected to be missing;\n * ARGV[4]=ttl seconds to apply ('0' = no expiry); ARGV[5]='keep' to keep the\n * existing TTL instead (only when the key existed).\n *\n * Returns 1 when written, 0 when the current value no longer matches.\n * `KEEPTTL` requires Redis >= 6.\n */\nexport const REDIS_CAS_SET_SCRIPT = `local cur = redis.call('GET', KEYS[1])\nif ARGV[3] == '1' then\n if cur then return 0 end\nelseif cur ~= ARGV[1] then\n return 0\nend\nlocal ttl = tonumber(ARGV[4])\nif ARGV[5] == 'keep' and ARGV[3] ~= '1' then\n redis.call('SET', KEYS[1], ARGV[2], 'KEEPTTL')\nelseif ttl > 0 then\n redis.call('SET', KEYS[1], ARGV[2], 'EX', ttl)\nelse\n redis.call('SET', KEYS[1], ARGV[2])\nend\nreturn 1`;\n\n/** KEYS[1]=key. Fallback for clients without `getDel`: returns the value and deletes it. */\nexport const REDIS_GET_DEL_SCRIPT = `local v = redis.call('GET', KEYS[1])\nif v then redis.call('DEL', KEYS[1]) end\nreturn v`;\n\n/**\n * Increment that gives a counter without an expiry the driver's default TTL,\n * in one round trip.\n *\n * KEYS[1]=key; ARGV[1]=amount; ARGV[2]=ttl seconds ('0' = none).\n *\n * Replaces `INCRBY` + `EXPIRE … NX`: the `NX` flag needs Redis >= 7.0, and on\n * Redis 6 the second command failed after the count had already gone up.\n * `TTL == -1` means \"exists without an expiry\", so a TTL set earlier (by\n * `set(..., { ttl })`) is never shortened or reset.\n */\nexport const REDIS_INCREMENT_SCRIPT = `local v = redis.call('INCRBY', KEYS[1], ARGV[1])\nlocal ttl = tonumber(ARGV[2])\nif ttl > 0 and redis.call('TTL', KEYS[1]) == -1 then\n redis.call('EXPIRE', KEYS[1], ttl)\nend\nreturn v`;\n\n/** Compare-and-set attempts before `update()` gives up under contention. */\nconst UPDATE_MAX_ATTEMPTS = 10;\n\n/** Keys deleted per UNLINK/DEL while draining a namespace. */\nconst REMOVE_BATCH_SIZE = 500;\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 escaped = namespace.replace(/[\\\\*?[\\]]/g, \"\\\\$&\");\n\n // Match the namespace key itself plus `<ns>.*` — never bare `<ns>*`, which\n // would also swallow siblings like `users2`. An empty namespace (no prefix)\n // means \"everything\".\n const pattern = escaped === \"\" ? \"*\" : `${escaped}.*`;\n\n const deleted: string[] = [];\n\n if (this.client) {\n if (escaped !== \"\") {\n const removedSelf = await this.deleteKeys([namespace]);\n\n if (removedSelf > 0) {\n deleted.push(namespace);\n }\n }\n\n // `SCAN` (cursor-based, non-blocking) instead of `KEYS` — `KEYS` is O(N)\n // and blocks the single-threaded Redis event loop. Delete per batch while\n // scanning so memory stays bounded on large keyspaces.\n let batch: string[] = [];\n\n for await (const scanned of this.client.scanIterator({\n MATCH: pattern,\n COUNT: 100,\n })) {\n // node-redis 5 yields a batch of keys for each SCAN cursor response;\n // older supported clients yielded one key at a time. Normalize both\n // forms before passing keys to the command parser.\n const keys = Array.isArray(scanned) ? scanned : [scanned];\n\n for (const key of keys) {\n batch.push(key as unknown as string);\n\n if (batch.length >= REMOVE_BATCH_SIZE) {\n await this.deleteKeys(batch);\n deleted.push(...batch);\n batch = [];\n }\n }\n }\n\n if (batch.length > 0) {\n await this.deleteKeys(batch);\n deleted.push(...batch);\n }\n }\n\n if (deleted.length === 0) {\n this.log(\"notFound\", namespace);\n return;\n }\n\n this.log(\"cleared\", namespace);\n\n return deleted;\n }\n\n /**\n * Delete keys with `UNLINK` (non-blocking free), falling back to `DEL` when\n * the client lacks it. Returns the number of keys removed.\n */\n protected async deleteKeys(keys: string[]): Promise<number> {\n const client = this.client as any;\n\n if (typeof client.unlink === \"function\") {\n return Number(await client.unlink(keys)) || 0;\n }\n\n return Number(await client.del(keys)) || 0;\n }\n\n // ============================================================\n // Tag index — a native Redis SET per tag\n // ============================================================\n\n /**\n * {@inheritdoc}\n *\n * `SADD` — atomic across servers, so concurrent tagged writes on any number\n * of instances never drop a member. The set key lives inside the prefix\n * namespace, so `flush()` / `removeNamespace()` sweep it too.\n */\n public async tagAdd(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const parsedKey = this.parseKey(tagKey);\n\n await this.withTagSet(parsedKey, (client) => client.sAdd(parsedKey, members));\n }\n\n /**\n * {@inheritdoc}\n */\n public async tagMembers(tagKey: CacheKey): Promise<string[]> {\n const parsedKey = this.parseKey(tagKey);\n\n return this.withTagSet(parsedKey, async (client) => (await client.sMembers(parsedKey)) ?? []);\n }\n\n /**\n * {@inheritdoc}\n *\n * `SREM` — Redis drops the set once its last member is removed.\n */\n public async tagRemove(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const parsedKey = this.parseKey(tagKey);\n\n await this.withTagSet(parsedKey, (client) => client.sRem(parsedKey, members));\n }\n\n /**\n * {@inheritdoc}\n */\n public async tagDelete(tagKey: CacheKey): Promise<void> {\n await this.client?.del(this.parseKey(tagKey));\n }\n\n /**\n * Run a SET command against a tag key, upgrading a pre-5.20 index first.\n *\n * Earlier versions stored the index as a JSON-array STRING under the same\n * key, which makes any SET command fail with `WRONGTYPE`. On that error the\n * legacy array is read, the string deleted, its members re-added with\n * `SADD`, and the command retried once.\n */\n protected async withTagSet<T>(parsedKey: string, run: (client: any) => Promise<T>): Promise<T> {\n const client = this.client as any;\n\n try {\n return await run(client);\n } catch (error) {\n if (!/WRONGTYPE/i.test(String((error as Error)?.message ?? error))) {\n throw error;\n }\n\n const raw: string | null = await client.get(parsedKey);\n let legacy: unknown = [];\n\n try {\n legacy = raw === null ? [] : JSON.parse(raw);\n } catch {\n legacy = [];\n }\n\n await client.del(parsedKey);\n\n const members = Array.isArray(legacy)\n ? legacy.filter((member): member is string => typeof member === \"string\")\n : [];\n\n if (members.length > 0) {\n await client.sAdd(parsedKey, members);\n }\n\n return run(client);\n }\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 // A plain write invalidates any freshness marker left by an earlier SWR\n // write, otherwise the old `staleAt` would apply to the new value.\n await this.client?.del(this.swrMetaKey(parsedKey));\n } else {\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 * {@inheritdoc}\n *\n * Values are stored as `JSON.stringify(value)`, so the compare runs against\n * the serialized form inside Lua.\n */\n protected async deleteIfEquals(key: CacheKey, expected: unknown): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n const deleted = await (this.client as any).eval(REDIS_COMPARE_DELETE_SCRIPT, {\n keys: [parsedKey],\n arguments: [JSON.stringify(expected)],\n });\n\n if (Number(deleted) !== 1) {\n return false;\n }\n\n // Mirror remove(): drop the SWR sidecar and emit the event.\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"removed\", { key: parsedKey });\n\n return true;\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 `${parsedKey}::swrmeta`;\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 // Two separate DELs (not one multi-key DEL) so it stays cluster-slot safe.\n await this.client?.del(key);\n await this.client?.del(this.swrMetaKey(key));\n\n this.log(\"removed\", key);\n\n await this.emit(\"removed\", { key });\n }\n\n /**\n * {@inheritDoc}\n *\n * WARNING: without a `globalPrefix` this runs `FLUSHDB`, which clears the\n * WHOLE selected Redis database (including keys not written by this driver).\n * Configure a `globalPrefix` to only delete this driver's keys.\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?.flushDb();\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 // Drop the half-initialised client so a later connect() can retry.\n this.clientDriver = undefined as unknown as typeof this.clientDriver;\n\n throw 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 * {@inheritdoc}\n *\n * Atomic across servers: `GETDEL` (Redis >= 6.2), or an equivalent Lua\n * script when the client has no `getDel`. A value is handed out once.\n */\n public async pull(key: CacheKey): Promise<any | null> {\n const parsedKey = this.parseKey(key);\n const client = this.client as any;\n\n const raw: string | null | undefined =\n typeof client.getDel === \"function\"\n ? await client.getDel(parsedKey)\n : await client.eval(REDIS_GET_DEL_SCRIPT, { keys: [parsedKey], arguments: [] });\n\n if (raw === null || raw === undefined) {\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n await this.client?.del(this.swrMetaKey(parsedKey));\n\n const value = JSON.parse(raw);\n\n await this.emit(\"hit\", { key: parsedKey, value });\n await this.emit(\"removed\", { key: parsedKey });\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * Atomic across servers via optimistic compare-and-set: read the raw value,\n * run `fn`, then write only if the value is still what was read (Lua), and\n * retry otherwise. `fn` may therefore run more than once under contention;\n * after {@link UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.\n *\n * Without an explicit `ttl` the existing expiry is kept (`KEEPTTL`); a new\n * key gets the driver default TTL. Returning `null` deletes the key\n * (compare-and-delete).\n */\n public async update<T = any>(\n key: CacheKey,\n fn: (current: T | null) => T | null | Promise<T | null>,\n options: { ttl?: CacheTtl } = {},\n ): Promise<T | null> {\n const parsedKey = this.parseKey(key);\n const client = this.client as any;\n\n const explicitTtl = options.ttl !== undefined ? parseTtl(options.ttl) : undefined;\n\n for (let attempt = 0; attempt < UPDATE_MAX_ATTEMPTS; attempt++) {\n const raw: string | null = (await client.get(parsedKey)) ?? null;\n const current = raw === null ? null : (JSON.parse(raw) as T);\n const result = await fn(current);\n\n if (result === null) {\n if (raw === null) {\n return null;\n }\n\n const deleted = await client.eval(REDIS_COMPARE_DELETE_SCRIPT, {\n keys: [parsedKey],\n arguments: [raw],\n });\n\n if (Number(deleted) === 1) {\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"removed\", { key: parsedKey });\n\n return null;\n }\n\n continue;\n }\n\n const isNew = raw === null;\n const ttlSeconds = explicitTtl ?? (isNew ? this.ttl : 0);\n const keep = explicitTtl === undefined && !isNew;\n\n const written = await client.eval(REDIS_CAS_SET_SCRIPT, {\n keys: [parsedKey],\n arguments: [\n raw ?? \"\",\n JSON.stringify(result),\n isNew ? \"1\" : \"0\",\n String(Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : 0),\n keep ? \"keep\" : \"\",\n ],\n });\n\n if (Number(written) === 1) {\n // A plain write invalidates any SWR freshness marker.\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: ttlSeconds });\n\n return result;\n }\n }\n\n throw new CacheError(\n `cache.update(): '${parsedKey}' changed on every attempt (${UPDATE_MAX_ATTEMPTS} tries) — too much write contention.`,\n );\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 // A fresh counter has no TTL; apply the driver default (only if none set),\n // atomically with the increment and without Redis 7's `EXPIRE … NX`.\n const defaultTtl = Number.isFinite(this.ttl) && this.ttl > 0 ? Math.ceil(this.ttl) : 0;\n const result =\n defaultTtl > 0\n ? Number(\n await (this.client as any)?.eval(REDIS_INCREMENT_SCRIPT, {\n keys: [parsedKey],\n arguments: [String(value), String(defaultTtl)],\n }),\n )\n : 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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"}
|
package/esm/index.d.mts
CHANGED
|
@@ -6,7 +6,7 @@ import { MemoryExtendedCacheDriver } from "./drivers/memory-extended-cache-drive
|
|
|
6
6
|
import { MockCacheDriver } from "./drivers/mock-cache-driver.mjs";
|
|
7
7
|
import { NullCacheDriver } from "./drivers/null-cache-driver.mjs";
|
|
8
8
|
import { PgCacheDriver } from "./drivers/pg-cache-driver.mjs";
|
|
9
|
-
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
9
|
+
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, REDIS_INCREMENT_SCRIPT, RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
10
10
|
import { CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, DriverClass, FileCacheOptions, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheOptions, MemoryExtendedCacheOptions, MockCacheOptions, NullCacheDriverOptions, PgCacheOptions, PgClientLike, RedisOptions, RememberOptions, ScopedCacheContract, TaggedCacheDriver, TaggedScopedCacheContract } from "./types.mjs";
|
|
11
11
|
import { CacheMetricsCollector } from "./metrics.mjs";
|
|
12
12
|
import { CacheManager, cache } from "./cache-manager.mjs";
|
|
@@ -18,4 +18,4 @@ import { ScopedCache } from "./scoped-cache.mjs";
|
|
|
18
18
|
import { TaggedCache } from "./tagged-cache.mjs";
|
|
19
19
|
import { TaggedScopedCache } from "./tagged-scoped-cache.mjs";
|
|
20
20
|
import { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo } from "./utils.mjs";
|
|
21
|
-
export { BaseCacheDriver, CACHE_FOR, CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheManager, CacheMetricsCollector, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, CachedFn, CachedOptions, DriverClass, FileCacheDriver, FileCacheOptions, LRUMemoryCacheDriver, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheDriver, MemoryCacheList, MemoryCacheOptions, MemoryExtendedCacheDriver, MemoryExtendedCacheOptions, MockCacheDriver, MockCacheOptions, NormalizedCachedConfig, NormalizedSetOptions, NullCacheDriver, NullCacheDriverOptions, PgCacheDriver, PgCacheOptions, PgClientLike, REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver, RedisOptions, RememberOptions, ScopedCache, ScopedCacheContract, TaggedCache, TaggedCacheDriver, TaggedScopedCache, TaggedScopedCacheContract, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
|
21
|
+
export { BaseCacheDriver, CACHE_FOR, CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheManager, CacheMetricsCollector, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, CachedFn, CachedOptions, DriverClass, FileCacheDriver, FileCacheOptions, LRUMemoryCacheDriver, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheDriver, MemoryCacheList, MemoryCacheOptions, MemoryExtendedCacheDriver, MemoryExtendedCacheOptions, MockCacheDriver, MockCacheOptions, NormalizedCachedConfig, NormalizedSetOptions, NullCacheDriver, NullCacheDriverOptions, PgCacheDriver, PgCacheOptions, PgClientLike, REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, REDIS_INCREMENT_SCRIPT, RedisCacheDriver, RedisOptions, RememberOptions, ScopedCache, ScopedCacheContract, TaggedCache, TaggedCacheDriver, TaggedScopedCache, TaggedScopedCacheContract, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
package/esm/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { MemoryExtendedCacheDriver } from "./drivers/memory-extended-cache-drive
|
|
|
18
18
|
import { MockCacheDriver } from "./drivers/mock-cache-driver.mjs";
|
|
19
19
|
import { NullCacheDriver } from "./drivers/null-cache-driver.mjs";
|
|
20
20
|
import { PgCacheDriver } from "./drivers/pg-cache-driver.mjs";
|
|
21
|
-
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
21
|
+
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, REDIS_INCREMENT_SCRIPT, RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
22
22
|
import "./drivers/index.mjs";
|
|
23
23
|
|
|
24
|
-
export { BaseCacheDriver, CACHE_FOR, CacheConcurrencyError, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheError, CacheManager, CacheMetricsCollector, CacheUnsupportedError, FileCacheDriver, LRUMemoryCacheDriver, MemoryCacheDriver, MemoryCacheList, MemoryExtendedCacheDriver, MockCacheDriver, NullCacheDriver, PgCacheDriver, REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver, ScopedCache, TaggedCache, TaggedScopedCache, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
|
24
|
+
export { BaseCacheDriver, CACHE_FOR, CacheConcurrencyError, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheError, CacheManager, CacheMetricsCollector, CacheUnsupportedError, FileCacheDriver, LRUMemoryCacheDriver, MemoryCacheDriver, MemoryCacheList, MemoryExtendedCacheDriver, MockCacheDriver, NullCacheDriver, PgCacheDriver, REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, REDIS_INCREMENT_SCRIPT, RedisCacheDriver, ScopedCache, TaggedCache, TaggedScopedCache, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-cache-list.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"memory-cache-list.d.mts","names":[],"sources":["../../../../../../../../packages/cache/src/list/memory-cache-list.ts"],"mappings":";;;;;AAwBA;;;;;;;;;;;;;;;;;;;;cAAa,eAAA,eAA8B,iBAAA,CAAkB,CAAA;EAAA,iBAExC,MAAA;EAAA,iBACA,GAAA;cADA,MAAA,EAAQ,WAAA,YACR,GAAA,EAAK,QAAA;EAHG;;;EAAA,QASb,IAAA;EANK;;;;;EAAA,QAgBL,MAAA;EAVA;;;EA2BP,IAAA,IAAQ,KAAA,EAAO,CAAA,KAAM,OAAA;EAAN;;;EAWf,OAAA,IAAW,KAAA,EAAO,CAAA,KAAM,OAAA;EAAN;;;EAWlB,GAAA,IAAO,OAAA,CAAQ,CAAA;EAAA;;;EAOf,KAAA,IAAS,OAAA,CAAQ,CAAA;EAOX;;;EAAA,KAAA,CAAM,KAAA,WAAgB,GAAA,YAAe,OAAA,CAAQ,CAAA;EAAA;;;EAQ7C,GAAA,IAAO,OAAA,CAAQ,CAAA;EAOf;;;EAAA,MAAA,IAAU,OAAA;EASU;;;;EAApB,IAAA,CAAK,KAAA,UAAe,GAAA,WAAc,OAAA;EAclB;;;EAAhB,KAAA,IAAS,OAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-cache-list.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"memory-cache-list.mjs","names":[],"sources":["../../../../../../../../packages/cache/src/list/memory-cache-list.ts"],"sourcesContent":["import type { CacheDriver, CacheKey, CacheListAccessor } from \"../types\";\n\n/**\n * Generic array-backed {@link CacheListAccessor}.\n *\n * Stores the full list as a single cache entry and performs read-mutate-write\n * for every operation. Correct for any driver, but O(n) per op. The Redis\n * driver overrides `list()` to return a native-command accessor instead.\n *\n * **Role.** Fallback list accessor bound to a driver + key. Every mutation\n * runs through `driver.update()`, so it is serialized in-process and keeps the\n * entry's remaining TTL.\n *\n * **Responsibility.**\n * - Owns: translating list operations into array mutations + driver writes.\n * - Does NOT own: cross-process concurrency control (callers should wrap in a\n * distributed lock when multi-process writers are possible), or tagging of\n * list entries.\n *\n * @example\n * // Never constructed directly — obtained via driver.list():\n * const list = cache.list<Event>(\"recent-events\");\n * await list.push(event);\n */\nexport class MemoryCacheList<T> implements CacheListAccessor<T> {\n public constructor(\n private readonly driver: CacheDriver<any, any>,\n private readonly key: CacheKey,\n ) {}\n\n /**\n * Read the backing array from the driver. Returns an empty array on miss.\n */\n private async read(): Promise<T[]> {\n const current = (await this.driver.get(this.key)) as T[] | null;\n return Array.isArray(current) ? [...current] : [];\n }\n\n /**\n * Serialized read-modify-write through `driver.update()`. No ttl is passed,\n * so the driver keeps the entry's remaining TTL. An empty list removes the\n * entry to keep the store clean.\n */\n private async mutate<R>(mutation: (items: T[]) => R): Promise<R> {\n let result!: R;\n\n await this.driver.update<T[]>(this.key, current => {\n const items = Array.isArray(current) ? [...current] : [];\n\n result = mutation(items);\n\n return items.length === 0 ? null : items;\n });\n\n return result;\n }\n\n /**\n * {@inheritdoc}\n */\n public push(...items: T[]): Promise<number> {\n return this.mutate(current => {\n current.push(...items);\n\n return current.length;\n });\n }\n\n /**\n * {@inheritdoc}\n */\n public unshift(...items: T[]): Promise<number> {\n return this.mutate(current => {\n current.unshift(...items);\n\n return current.length;\n });\n }\n\n /**\n * {@inheritdoc}\n */\n public pop(): Promise<T | null> {\n return this.mutate(current => (current.length === 0 ? null : (current.pop() as T)));\n }\n\n /**\n * {@inheritdoc}\n */\n public shift(): Promise<T | null> {\n return this.mutate(current => (current.length === 0 ? null : (current.shift() as T)));\n }\n\n /**\n * {@inheritdoc}\n */\n public async slice(start?: number, end?: number): Promise<T[]> {\n const current = await this.read();\n return current.slice(start, end);\n }\n\n /**\n * {@inheritdoc}\n */\n public async all(): Promise<T[]> {\n return this.read();\n }\n\n /**\n * {@inheritdoc}\n */\n public async length(): Promise<number> {\n const current = await this.read();\n return current.length;\n }\n\n /**\n * Keep the inclusive range `[start, end]` like Redis LTRIM; negative\n * indexes count from the end.\n */\n public async trim(start: number, end: number): Promise<void> {\n await this.mutate(current => {\n const length = current.length;\n const from = start < 0 ? Math.max(length + start, 0) : start;\n const to = end < 0 ? length + end : end;\n const kept = current.slice(from, to + 1);\n\n current.splice(0, current.length, ...kept);\n });\n }\n\n /**\n * {@inheritdoc}\n */\n public async clear(): Promise<void> {\n await this.driver.remove(this.key);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,kBAAb,MAAgE;CAC9D,AAAO,YACL,AAAiB,QACjB,AAAiB,KACjB;EAFiB;EACA;CAChB;;;;CAKH,MAAc,OAAqB;EACjC,MAAM,UAAW,MAAM,KAAK,OAAO,IAAI,KAAK,GAAG;EAC/C,OAAO,MAAM,QAAQ,OAAO,IAAI,CAAC,GAAG,OAAO,IAAI,CAAC;CAClD;;;;;;CAOA,MAAc,OAAU,UAAyC;EAC/D,IAAI;EAEJ,MAAM,KAAK,OAAO,OAAY,KAAK,MAAK,YAAW;GACjD,MAAM,QAAQ,MAAM,QAAQ,OAAO,IAAI,CAAC,GAAG,OAAO,IAAI,CAAC;GAEvD,SAAS,SAAS,KAAK;GAEvB,OAAO,MAAM,WAAW,IAAI,OAAO;EACrC,CAAC;EAED,OAAO;CACT;;;;CAKA,AAAO,KAAK,GAAG,OAA6B;EAC1C,OAAO,KAAK,QAAO,YAAW;GAC5B,QAAQ,KAAK,GAAG,KAAK;GAErB,OAAO,QAAQ;EACjB,CAAC;CACH;;;;CAKA,AAAO,QAAQ,GAAG,OAA6B;EAC7C,OAAO,KAAK,QAAO,YAAW;GAC5B,QAAQ,QAAQ,GAAG,KAAK;GAExB,OAAO,QAAQ;EACjB,CAAC;CACH;;;;CAKA,AAAO,MAAyB;EAC9B,OAAO,KAAK,QAAO,YAAY,QAAQ,WAAW,IAAI,OAAQ,QAAQ,IAAI,CAAQ;CACpF;;;;CAKA,AAAO,QAA2B;EAChC,OAAO,KAAK,QAAO,YAAY,QAAQ,WAAW,IAAI,OAAQ,QAAQ,MAAM,CAAQ;CACtF;;;;CAKA,MAAa,MAAM,OAAgB,KAA4B;EAE7D,QAAO,MADe,KAAK,KAAK,EAClB,CAAC,MAAM,OAAO,GAAG;CACjC;;;;CAKA,MAAa,MAAoB;EAC/B,OAAO,KAAK,KAAK;CACnB;;;;CAKA,MAAa,SAA0B;EAErC,QAAO,MADe,KAAK,KAAK,EAClB,CAAC;CACjB;;;;;CAMA,MAAa,KAAK,OAAe,KAA4B;EAC3D,MAAM,KAAK,QAAO,YAAW;GAC3B,MAAM,SAAS,QAAQ;GACvB,MAAM,OAAO,QAAQ,IAAI,KAAK,IAAI,SAAS,OAAO,CAAC,IAAI;GACvD,MAAM,KAAK,MAAM,IAAI,SAAS,MAAM;GACpC,MAAM,OAAO,QAAQ,MAAM,MAAM,KAAK,CAAC;GAEvC,QAAQ,OAAO,GAAG,QAAQ,QAAQ,GAAG,IAAI;EAC3C,CAAC;CACH;;;;CAKA,MAAa,QAAuB;EAClC,MAAM,KAAK,OAAO,OAAO,KAAK,GAAG;CACnC;AACF"}
|
package/esm/metrics.d.mts
CHANGED
package/esm/metrics.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"metrics.d.mts","names":[],"sources":["../../../../../../../packages/cache/src/metrics.ts"],"mappings":";;;;;AAGiB;;;;;KAgBZ,cAAA;EACH,IAAA;EACA,MAAA;EACA,IAAA;EACA,OAAA;EACA,MAAA;EACA,cAAA,YAEa;EAAb,aAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+BW,qBAAA;EAoEJ;;;;;EAAA,mBA9DY,UAAA;EAoGT;;;;EAAA,mBA9FS,SAAA,EAAW,cAAA;EA6Gd;;;;EAAA,mBAvGG,QAAA,EAAU,GAAA,SAAY,cAAA;EA6HI;EAAA,UA1HnC,SAAA;cAES,UAAA;EAqJK;;;;EA5IjB,WAAA,CACL,KAAA,gDACA,IAAA,EAAM,cAAA;EAmKwB;;;AAAc;;EAjIvC,aAAA,CAAc,MAAA,UAAgB,UAAA;;;;;;EAU9B,QAAA,IAAY,oBAAA;;;;;EAkBZ,KAAA;;;;;YAUG,SAAA,CAAU,UAAA,WAAqB,cAAA;;;;;YAe/B,KAAA,CAAM,MAAA,EAAQ,cAAA,GAAiB,IAAA,CAAK,oBAAA;;;;;;YAsBpC,cAAA,CAAe,OAAA,aAAoB,oBAAA;;;;;YA6BnC,aAAA,CAAc,MAAA,EAAQ,cAAA,EAAgB,UAAA;;YAYtC,cAAA,IAAkB,cAAA;;YAalB,aAAA,CAAc,MAAA,EAAQ,cAAA;AAAA"}
|
package/esm/metrics.mjs
CHANGED