@warlock.js/cache 5.19.1 → 5.21.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/cjs/index.cjs +1439 -331
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/cache-manager.d.mts +4 -0
  5. package/esm/cache-manager.d.mts.map +1 -1
  6. package/esm/cache-manager.mjs +23 -5
  7. package/esm/cache-manager.mjs.map +1 -1
  8. package/esm/cached/auto-key.d.mts +14 -14
  9. package/esm/cached/auto-key.d.mts.map +1 -1
  10. package/esm/cached/auto-key.mjs +50 -34
  11. package/esm/cached/auto-key.mjs.map +1 -1
  12. package/esm/cached/cached.mjs +4 -0
  13. package/esm/cached/cached.mjs.map +1 -1
  14. package/esm/drivers/base-cache-driver.d.mts +84 -8
  15. package/esm/drivers/base-cache-driver.d.mts.map +1 -1
  16. package/esm/drivers/base-cache-driver.mjs +152 -42
  17. package/esm/drivers/base-cache-driver.mjs.map +1 -1
  18. package/esm/drivers/file-cache-driver.d.mts +15 -0
  19. package/esm/drivers/file-cache-driver.d.mts.map +1 -1
  20. package/esm/drivers/file-cache-driver.mjs +68 -18
  21. package/esm/drivers/file-cache-driver.mjs.map +1 -1
  22. package/esm/drivers/in-memory-tag-index.d.mts +44 -0
  23. package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
  24. package/esm/drivers/in-memory-tag-index.mjs +73 -0
  25. package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
  26. package/esm/drivers/index.d.mts +1 -1
  27. package/esm/drivers/index.mjs +1 -1
  28. package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
  29. package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
  30. package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
  31. package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
  32. package/esm/drivers/memory-cache-driver.d.mts +87 -30
  33. package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
  34. package/esm/drivers/memory-cache-driver.mjs +214 -96
  35. package/esm/drivers/memory-cache-driver.mjs.map +1 -1
  36. package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
  37. package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
  38. package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
  39. package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
  40. package/esm/drivers/mock-cache-driver.d.mts +41 -0
  41. package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
  42. package/esm/drivers/mock-cache-driver.mjs +105 -8
  43. package/esm/drivers/mock-cache-driver.mjs.map +1 -1
  44. package/esm/drivers/null-cache-driver.d.mts +8 -2
  45. package/esm/drivers/null-cache-driver.d.mts.map +1 -1
  46. package/esm/drivers/null-cache-driver.mjs +10 -2
  47. package/esm/drivers/null-cache-driver.mjs.map +1 -1
  48. package/esm/drivers/pg-cache-driver.d.mts +77 -3
  49. package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
  50. package/esm/drivers/pg-cache-driver.mjs +229 -7
  51. package/esm/drivers/pg-cache-driver.mjs.map +1 -1
  52. package/esm/drivers/redis-cache-driver.d.mts +86 -1
  53. package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
  54. package/esm/drivers/redis-cache-driver.mjs +252 -16
  55. package/esm/drivers/redis-cache-driver.mjs.map +1 -1
  56. package/esm/index.d.mts +2 -2
  57. package/esm/index.mjs +2 -2
  58. package/esm/list/memory-cache-list.d.mts +11 -8
  59. package/esm/list/memory-cache-list.d.mts.map +1 -1
  60. package/esm/list/memory-cache-list.mjs +39 -37
  61. package/esm/list/memory-cache-list.mjs.map +1 -1
  62. package/esm/scoped-cache.d.mts +9 -3
  63. package/esm/scoped-cache.d.mts.map +1 -1
  64. package/esm/scoped-cache.mjs +19 -7
  65. package/esm/scoped-cache.mjs.map +1 -1
  66. package/esm/tagged-cache.d.mts +20 -0
  67. package/esm/tagged-cache.d.mts.map +1 -1
  68. package/esm/tagged-cache.mjs +62 -24
  69. package/esm/tagged-cache.mjs.map +1 -1
  70. package/esm/tagged-scoped-cache.d.mts +7 -3
  71. package/esm/tagged-scoped-cache.d.mts.map +1 -1
  72. package/esm/tagged-scoped-cache.mjs +19 -7
  73. package/esm/tagged-scoped-cache.mjs.map +1 -1
  74. package/esm/types.d.mts +41 -3
  75. package/esm/types.d.mts.map +1 -1
  76. package/esm/types.mjs.map +1 -1
  77. package/esm/utils.d.mts +9 -3
  78. package/esm/utils.d.mts.map +1 -1
  79. package/esm/utils.mjs +16 -6
  80. package/esm/utils.mjs.map +1 -1
  81. package/llms-full.txt +1685 -1666
  82. package/llms.txt +18 -18
  83. package/package.json +4 -4
  84. package/skills/apply-cache-patterns/SKILL.md +98 -97
  85. package/skills/configure-pg-cache/SKILL.md +6 -5
  86. package/skills/configure-set-options/SKILL.md +102 -96
  87. package/skills/handle-cache-errors/SKILL.md +92 -92
  88. package/skills/index.md +20 -0
  89. package/skills/observe-cache/SKILL.md +103 -103
  90. package/skills/pick-cache-driver/SKILL.md +129 -123
  91. package/skills/test-cache-code/SKILL.md +219 -219
  92. package/skills/use-cache-atomic/SKILL.md +57 -65
  93. package/skills/use-cache-bulk/SKILL.md +57 -57
  94. package/skills/use-cache-list/SKILL.md +85 -85
  95. package/skills/use-cache-lock/SKILL.md +113 -104
  96. package/skills/use-cache-namespace/SKILL.md +88 -88
  97. package/skills/use-cache-similarity/SKILL.md +94 -94
  98. package/skills/use-cache-tags/SKILL.md +7 -4
  99. package/skills/use-cache-update-merge/SKILL.md +84 -84
  100. package/skills/use-cache-utils/SKILL.md +89 -89
  101. package/skills/use-cached-hof/SKILL.md +100 -99
  102. package/skills/use-swr/SKILL.md +104 -104
@@ -1,7 +1,10 @@
1
- import { CacheConfigurationError, CacheUnsupportedError } from "../types.mjs";
1
+ import { CacheConfigurationError, CacheError, CacheUnsupportedError } from "../types.mjs";
2
+ import { parseTtl } from "../utils.mjs";
2
3
  import { BaseCacheDriver } from "./base-cache-driver.mjs";
3
4
 
4
5
  //#region ../cache/src/drivers/pg-cache-driver.ts
6
+ /** Compare-and-set attempts before `update()` gives up under contention. */
7
+ const PG_UPDATE_MAX_ATTEMPTS = 10;
5
8
  /**
6
9
  * Allowed characters in a Postgres identifier (table name). We accept the
7
10
  * conservative ASCII subset and reject anything else — interpolating an
@@ -97,12 +100,94 @@ var PgCacheDriver = class extends BaseCacheDriver {
97
100
  return this.options.client;
98
101
  }
99
102
  /**
100
- * Compute an absolute `expires_at` Date for the given relative TTL in seconds,
101
- * or `null` when the entry should not expire (`Infinity` / 0 / undefined).
103
+ * {@inheritdoc}
104
+ *
105
+ * Single `DELETE … WHERE key AND value` — atomic in Postgres.
106
+ */
107
+ async deleteIfEquals(key, expected) {
108
+ const parsedKey = this.parseKey(key);
109
+ if ((await this.pgClient.query(`DELETE FROM ${this.table}
110
+ WHERE key = $1 AND value = $2::jsonb AND (expires_at IS NULL OR expires_at > now())
111
+ RETURNING 1`, [parsedKey, JSON.stringify(expected)])).rows.length === 0) return false;
112
+ await this.emit("removed", { key: parsedKey });
113
+ return true;
114
+ }
115
+ /**
116
+ * {@inheritdoc}
117
+ *
118
+ * One `INSERT … ON CONFLICT DO UPDATE` that unions the stored JSON array with
119
+ * `members`. Postgres row-locks the conflicting row, so concurrent adders on
120
+ * any number of servers serialize and none is lost.
121
+ *
122
+ * The index stays a row of the main table holding a JSON array — the same
123
+ * shape earlier versions wrote — so no schema change or migration is needed
124
+ * and existing indexes keep working. (The `tags` column only records tags
125
+ * passed inline to `set()`, not relationships added afterwards, so it can't
126
+ * serve as the invalidation index.)
127
+ */
128
+ async tagAdd(tagKey, members) {
129
+ if (members.length === 0) return;
130
+ const t = this.table;
131
+ await this.pgClient.query(`/* cache:tag-add */ INSERT INTO ${t} (key, value)
132
+ VALUES ($1, to_jsonb($2::text[]))
133
+ ON CONFLICT (key) DO UPDATE
134
+ SET value = (
135
+ SELECT COALESCE(jsonb_agg(u.member), '[]'::jsonb)
136
+ FROM (
137
+ SELECT x.member FROM jsonb_array_elements_text(
138
+ CASE WHEN jsonb_typeof(${t}.value) = 'array' THEN ${t}.value ELSE '[]'::jsonb END
139
+ ) AS x(member)
140
+ UNION
141
+ SELECT y.member FROM unnest($2::text[]) AS y(member)
142
+ ) AS u
143
+ ),
144
+ expires_at = NULL`, [this.parseKey(tagKey), members]);
145
+ }
146
+ /**
147
+ * {@inheritdoc}
148
+ *
149
+ * One `UPDATE` that filters `members` out of the stored array (members added
150
+ * concurrently are kept), then a conditional `DELETE` of the row if it ended
151
+ * up empty.
152
+ */
153
+ async tagRemove(tagKey, members) {
154
+ if (members.length === 0) return;
155
+ const t = this.table;
156
+ const parsedKey = this.parseKey(tagKey);
157
+ await this.pgClient.query(`/* cache:tag-remove */ UPDATE ${t}
158
+ SET value = (
159
+ SELECT COALESCE(jsonb_agg(x.member), '[]'::jsonb)
160
+ FROM jsonb_array_elements_text(
161
+ CASE WHEN jsonb_typeof(${t}.value) = 'array' THEN ${t}.value ELSE '[]'::jsonb END
162
+ ) AS x(member)
163
+ WHERE x.member <> ALL($2::text[])
164
+ )
165
+ WHERE key = $1`, [parsedKey, members]);
166
+ await this.pgClient.query(`/* cache:tag-drop-empty */ DELETE FROM ${t} WHERE key = $1 AND value = '[]'::jsonb`, [parsedKey]);
167
+ }
168
+ /**
169
+ * TTL in seconds bound to `now() + make_interval(secs => $n)`, so expiry is
170
+ * computed on the DB clock (immune to app/DB clock skew). `null` — from
171
+ * `Infinity` / 0 / undefined — yields a NULL `expires_at` (never expires).
102
172
  */
103
- ttlToExpiresAt(ttl) {
173
+ ttlToSeconds(ttl) {
104
174
  if (!ttl || ttl === Infinity) return null;
105
- return new Date(Date.now() + ttl * 1e3);
175
+ return ttl;
176
+ }
177
+ /**
178
+ * Delete expired rows in batches of `limit`. Returns how many were removed.
179
+ * Also invoked opportunistically (~1 in 200 writes) by `set()`.
180
+ */
181
+ async prune(limit = 1e3) {
182
+ const t = this.table;
183
+ return (await this.pgClient.query(`DELETE FROM ${t} WHERE ctid IN (SELECT ctid FROM ${t} WHERE expires_at < now() LIMIT $1)`, [limit])).rowCount ?? 0;
184
+ }
185
+ /**
186
+ * Fire-and-forget `prune()` on roughly 1 in 200 writes; errors are logged.
187
+ */
188
+ maybePrune() {
189
+ if (Math.random() >= 1 / 200) return;
190
+ this.prune().catch((error) => this.logError("prune failed", error));
106
191
  }
107
192
  /**
108
193
  * Return the SQL needed to provision the cache table + index. Run once via
@@ -168,7 +253,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
168
253
  await this.ensureVectorReady();
169
254
  }
170
255
  this.log("caching", parsedKey);
171
- const expiresAt = this.ttlToExpiresAt(ttl);
256
+ const expiresAt = this.ttlToSeconds(ttl);
172
257
  const staleAtDate = staleAt !== void 0 ? new Date(staleAt) : null;
173
258
  const tagsArr = tags ?? [];
174
259
  const serialized = JSON.stringify(value);
@@ -184,7 +269,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
184
269
  const placeholders = [
185
270
  "$1",
186
271
  "$2::jsonb",
187
- "$3",
272
+ "now() + make_interval(secs => $3::double precision)",
188
273
  "$4",
189
274
  "$5"
190
275
  ];
@@ -217,6 +302,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
217
302
  };
218
303
  if (tags && tags.length > 0) await this.applyTags(key, tags);
219
304
  this.log("cached", parsedKey);
305
+ this.maybePrune();
220
306
  await this.emit("set", {
221
307
  key: parsedKey,
222
308
  value,
@@ -238,6 +324,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
238
324
  };
239
325
  if (tags && tags.length > 0) await this.applyTags(key, tags);
240
326
  this.log("cached", parsedKey);
327
+ this.maybePrune();
241
328
  await this.emit("set", {
242
329
  key: parsedKey,
243
330
  value,
@@ -254,6 +341,7 @@ var PgCacheDriver = class extends BaseCacheDriver {
254
341
  SET ${setClause}`, params);
255
342
  if (tags && tags.length > 0) await this.applyTags(key, tags);
256
343
  this.log("cached", parsedKey);
344
+ this.maybePrune();
257
345
  await this.emit("set", {
258
346
  key: parsedKey,
259
347
  value,
@@ -330,6 +418,140 @@ var PgCacheDriver = class extends BaseCacheDriver {
330
418
  return entry;
331
419
  }
332
420
  /**
421
+ * Decode a JSONB column value (node-postgres usually parses it already;
422
+ * some pools hand back a string).
423
+ */
424
+ decodeJson(value) {
425
+ if (typeof value === "string") try {
426
+ return JSON.parse(value);
427
+ } catch {
428
+ return value;
429
+ }
430
+ return value;
431
+ }
432
+ /**
433
+ * {@inheritdoc}
434
+ *
435
+ * One atomic `INSERT … ON CONFLICT DO UPDATE` statement, so concurrent
436
+ * increments from any number of servers never lose an update. A live row
437
+ * keeps its `expires_at`; a missing or expired row restarts from `value`
438
+ * with the driver default TTL. A non-numeric live value is rejected.
439
+ */
440
+ async increment(key, value = 1) {
441
+ const parsedKey = this.parseKey(key);
442
+ const t = this.table;
443
+ const expired = `${t}.expires_at IS NOT NULL AND ${t}.expires_at <= now()`;
444
+ const { rows } = await this.pgClient.query(`/* cache:increment */ INSERT INTO ${t}(key, value, expires_at)
445
+ VALUES ($1, to_jsonb($2::numeric), now() + make_interval(secs => $3::double precision))
446
+ ON CONFLICT (key) DO UPDATE
447
+ SET value = CASE WHEN ${expired} THEN EXCLUDED.value
448
+ ELSE to_jsonb((${t}.value #>> '{}')::numeric + $2::numeric) END,
449
+ expires_at = CASE WHEN ${expired} THEN EXCLUDED.expires_at ELSE ${t}.expires_at END,
450
+ stale_at = CASE WHEN ${expired} THEN NULL ELSE ${t}.stale_at END
451
+ WHERE (${expired}) OR jsonb_typeof(${t}.value) = 'number'
452
+ RETURNING value`, [
453
+ parsedKey,
454
+ value,
455
+ this.ttlToSeconds(this.ttl)
456
+ ]);
457
+ if (rows.length === 0) throw new Error(`Cannot increment non-numeric value for key: ${parsedKey}`);
458
+ const newValue = Number(this.decodeJson(rows[0].value));
459
+ await this.emit("set", {
460
+ key: parsedKey,
461
+ value: newValue,
462
+ ttl: this.ttl
463
+ });
464
+ return newValue;
465
+ }
466
+ /**
467
+ * {@inheritdoc}
468
+ *
469
+ * One atomic `DELETE … RETURNING`, so a value is handed out exactly once
470
+ * across servers. Expired rows count as missing.
471
+ */
472
+ async pull(key) {
473
+ const parsedKey = this.parseKey(key);
474
+ const { rows } = await this.pgClient.query(`/* cache:pull */ DELETE FROM ${this.table}
475
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())
476
+ RETURNING value`, [parsedKey]);
477
+ if (rows.length === 0) {
478
+ await this.emit("miss", { key: parsedKey });
479
+ return null;
480
+ }
481
+ const value = this.decodeJson(rows[0].value);
482
+ await this.emit("hit", {
483
+ key: parsedKey,
484
+ value
485
+ });
486
+ await this.emit("removed", { key: parsedKey });
487
+ return value;
488
+ }
489
+ /**
490
+ * {@inheritdoc}
491
+ *
492
+ * Atomic across servers via optimistic compare-and-set: read the live value,
493
+ * run `fn`, then write with `UPDATE … WHERE value = <what was read>` (or a
494
+ * create-only insert for a missing key), retrying when another writer got
495
+ * there first. `fn` may run more than once under contention; after
496
+ * {@link PG_UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.
497
+ *
498
+ * Without an explicit `ttl` the row keeps its `expires_at`; a new key gets
499
+ * the driver default TTL. Returning `null` deletes the key (compare-and-delete).
500
+ */
501
+ async update(key, fn, options = {}) {
502
+ const parsedKey = this.parseKey(key);
503
+ const t = this.table;
504
+ const live = `(expires_at IS NULL OR expires_at > now())`;
505
+ const explicitTtl = options.ttl !== void 0 ? parseTtl(options.ttl) : void 0;
506
+ for (let attempt = 0; attempt < PG_UPDATE_MAX_ATTEMPTS; attempt++) {
507
+ const read = await this.pgClient.query(`SELECT value FROM ${t}
508
+ WHERE key = $1 AND ${live}`, [parsedKey]);
509
+ const exists = read.rows.length > 0;
510
+ const current = exists ? this.decodeJson(read.rows[0].value) : null;
511
+ const expected = exists ? JSON.stringify(current) : null;
512
+ const result = await fn(current);
513
+ if (result === null) {
514
+ if (!exists) return null;
515
+ if ((await this.pgClient.query(`/* cache:cas-delete */ DELETE FROM ${t}
516
+ WHERE key = $1 AND value = $2::jsonb AND ${live}
517
+ RETURNING 1`, [parsedKey, expected])).rows.length > 0) {
518
+ await this.emit("removed", { key: parsedKey });
519
+ return null;
520
+ }
521
+ continue;
522
+ }
523
+ if (!exists) {
524
+ if ((await this.set(key, result, {
525
+ onConflict: "create",
526
+ ...explicitTtl !== void 0 ? { ttl: explicitTtl } : {}
527
+ })).wasSet) return result;
528
+ continue;
529
+ }
530
+ const params = [
531
+ parsedKey,
532
+ JSON.stringify(result),
533
+ expected
534
+ ];
535
+ let expirySql = "";
536
+ if (explicitTtl !== void 0) {
537
+ params.push(this.ttlToSeconds(explicitTtl));
538
+ expirySql = `, expires_at = now() + make_interval(secs => $4::double precision)`;
539
+ }
540
+ if ((await this.pgClient.query(`/* cache:cas-update */ UPDATE ${t}
541
+ SET value = $2::jsonb, stale_at = NULL${expirySql}
542
+ WHERE key = $1 AND value = $3::jsonb AND ${live}
543
+ RETURNING 1`, params)).rows.length > 0) {
544
+ await this.emit("set", {
545
+ key: parsedKey,
546
+ value: result,
547
+ ttl: explicitTtl
548
+ });
549
+ return result;
550
+ }
551
+ }
552
+ throw new CacheError(`cache.update(): '${parsedKey}' changed on every attempt (${PG_UPDATE_MAX_ATTEMPTS} tries) — too much write contention.`);
553
+ }
554
+ /**
333
555
  * {@inheritdoc}
334
556
  */
335
557
  async remove(key) {
@@ -1 +1 @@
1
- {"version":3,"file":"pg-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/pg-cache-driver.ts"],"sourcesContent":["import type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheSimilarHit,\n CacheSimilarOptions,\n CacheTtl,\n PgCacheOptions,\n PgClientLike,\n} from \"../types\";\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n/**\n * Allowed characters in a Postgres identifier (table name). We accept the\n * conservative ASCII subset and reject anything else — interpolating an\n * arbitrary string into DDL would be a SQL-injection footgun.\n */\nconst SAFE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Postgres cache driver with optional pgvector similarity support.\n *\n * Connection lifecycle is the caller's responsibility — pass an already-built\n * `pg.Pool` or `pg.Client` via the `client` option. The driver never closes it\n * on `cache.disconnect()`, so the same pool can serve queries elsewhere in\n * the app.\n *\n * Schema is not auto-migrated. Call `driver.schema()` to get the DDL string\n * and run it through whichever migration tool you use.\n *\n * @example\n * import { Pool } from \"pg\";\n * import { cache, PgCacheDriver } from \"@warlock.js/cache\";\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n *\n * cache.setCacheConfigurations({\n * default: \"pg\",\n * drivers: { pg: PgCacheDriver },\n * options: { pg: { client: pool, table: \"warlock_cache\", ttl: \"1h\" } },\n * });\n *\n * await cache.init();\n *\n * // Run once, via your own migration tooling:\n * // await pool.query(driver.schema());\n */\nexport class PgCacheDriver\n extends BaseCacheDriver<PgClientLike, PgCacheOptions>\n implements CacheDriver<PgClientLike, PgCacheOptions>\n{\n /**\n * {@inheritdoc}\n */\n public name = \"pg\";\n\n /**\n * Cached result of the pgvector extension check. Populated lazily on first\n * vector op so the driver doesn't probe the database at construction time.\n */\n protected vectorReady?: boolean;\n\n /**\n * {@inheritdoc}\n *\n * Validates `client` presence and the table name before storing options.\n */\n public setOptions(options: PgCacheOptions) {\n if (!options || !options.client || typeof options.client.query !== \"function\") {\n throw new CacheConfigurationError(\n \"Pg cache driver requires a 'client' option implementing { query(text, values) } — pass a pg.Pool or pg.Client.\",\n );\n }\n\n const table = options.table ?? \"warlock_cache\";\n if (!SAFE_IDENT.test(table)) {\n throw new CacheConfigurationError(\n `Pg cache driver: invalid table name '${table}'. Allowed: [A-Za-z_][A-Za-z0-9_]*.`,\n );\n }\n\n if (options.vector) {\n const dim = options.vector.dimensions;\n if (!Number.isInteger(dim) || dim <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.dimensions must be a positive integer; got ${dim}.`,\n );\n }\n const idx = options.vector.index ?? \"hnsw\";\n if (idx !== \"hnsw\" && idx !== \"ivfflat\") {\n throw new CacheConfigurationError(\n `Pg cache driver: vector.index must be 'hnsw' or 'ivfflat'; got '${idx}'.`,\n );\n }\n }\n\n return super.setOptions({ ...options, table });\n }\n\n /**\n * Lazy pgvector availability check. Runs once on the first vector op and\n * caches the result — subsequent ops are zero-overhead. Throws\n * {@link CacheConfigurationError} if the extension isn't installed; throws\n * {@link CacheUnsupportedError} if `vector` config wasn't provided at all.\n */\n protected async ensureVectorReady(): Promise<void> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n if (this.vectorReady === true) {\n return;\n }\n\n const { rows } = await this.pgClient.query(\n `SELECT 1 FROM pg_extension WHERE extname = 'vector'`,\n );\n\n if (rows.length === 0) {\n throw new CacheConfigurationError(\n \"'pg' driver: pgvector extension not installed. Run 'CREATE EXTENSION vector;' or remove the 'vector' config option.\",\n );\n }\n\n this.vectorReady = true;\n }\n\n /**\n * Format a numeric vector for pgvector ingestion. The `vector` type accepts\n * a string literal `'[1,2,3]'` cast via `::vector` — this avoids depending\n * on the binary protocol and works against any pg client.\n */\n protected formatVector(vector: number[]): string {\n return `[${vector.join(\",\")}]`;\n }\n\n /**\n * Resolved table name. Always defined post-`setOptions` — the validator\n * fills in the default.\n */\n protected get table(): string {\n return this.options.table ?? \"warlock_cache\";\n }\n\n /**\n * The user-supplied `pg.Pool` / `pg.Client`. Use this rather than `this.client`\n * (which has a generic fallback to `this`) for actual queries.\n */\n protected get pgClient(): PgClientLike {\n return this.options.client;\n }\n\n /**\n * Compute an absolute `expires_at` Date for the given relative TTL in seconds,\n * or `null` when the entry should not expire (`Infinity` / 0 / undefined).\n */\n protected ttlToExpiresAt(ttl?: number): Date | null {\n if (!ttl || ttl === Infinity) {\n return null;\n }\n\n return new Date(Date.now() + ttl * 1000);\n }\n\n /**\n * Return the SQL needed to provision the cache table + index. Run once via\n * the caller's migration tooling — the driver never auto-migrates.\n *\n * @example\n * await pool.query(driver.schema());\n */\n public schema(): string {\n const t = this.table;\n const vec = this.options.vector;\n\n const columns = [\n ` key TEXT PRIMARY KEY,`,\n ` value JSONB NOT NULL,`,\n ` expires_at TIMESTAMPTZ,`,\n ` stale_at TIMESTAMPTZ,`,\n ` tags TEXT[] NOT NULL DEFAULT '{}'::TEXT[]`,\n ];\n\n if (vec) {\n // Trailing comma on the previous line to make this a valid column list.\n columns[columns.length - 1] = columns[columns.length - 1] + \",\";\n columns.push(` embedding VECTOR(${vec.dimensions})`);\n }\n\n const lines = [\n `CREATE TABLE IF NOT EXISTS ${t} (`,\n ...columns,\n `);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_expires_at ON ${t} (expires_at);`,\n `CREATE INDEX IF NOT EXISTS idx_${t}_tags ON ${t} USING GIN (tags);`,\n ];\n\n if (vec) {\n const idx = vec.index ?? \"hnsw\";\n lines.push(\n `CREATE INDEX IF NOT EXISTS idx_${t}_embedding ON ${t} USING ${idx} (embedding vector_cosine_ops);`,\n );\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async connect() {\n // No-op — caller owns the connection. We emit `connected` for symmetry\n // with the other drivers' lifecycle events.\n this.log(\"connecting\");\n this.log(\"connected\");\n await this.emit(\"connected\");\n }\n\n /**\n * {@inheritdoc}\n *\n * Does NOT close the user-supplied client — lifecycle stays with the caller.\n */\n public async disconnect() {\n this.log(\"disconnected\");\n await this.emit(\"disconnected\");\n }\n\n /**\n * {@inheritdoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: cannot index a vector without options.vector configuration — set { dimensions } and recreate the table via driver.schema().\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n await this.ensureVectorReady();\n }\n\n this.log(\"caching\", parsedKey);\n\n const expiresAt = this.ttlToExpiresAt(ttl);\n const staleAtDate = staleAt !== undefined ? new Date(staleAt) : null;\n const tagsArr = tags ?? [];\n const serialized = JSON.stringify(value);\n const vecLiteral = vector ? this.formatVector(vector) : null;\n\n const t = this.table;\n\n // Build column / placeholder / param triplets dynamically so the same code\n // path serves both KV-only and vector-aware writes. Param order is fixed:\n // $1 = key, $2 = value (jsonb), $3 = expires_at, $4 = stale_at,\n // $5 = tags, $6 = embedding::vector (only present when vecLiteral !== null).\n const cols = [\"key\", \"value\", \"expires_at\", \"stale_at\", \"tags\"];\n const placeholders = [\"$1\", \"$2::jsonb\", \"$3\", \"$4\", \"$5\"];\n const params: unknown[] = [parsedKey, serialized, expiresAt, staleAtDate, tagsArr];\n if (vecLiteral !== null) {\n cols.push(\"embedding\");\n placeholders.push(`$${params.length + 1}::vector`);\n params.push(vecLiteral);\n }\n const colList = cols.join(\", \");\n const valList = placeholders.join(\", \");\n const setClause = cols\n .slice(1)\n .map((c) => `${c} = EXCLUDED.${c}`)\n .join(\", \");\n const updateSetClause = cols\n .slice(1)\n .map((c, i) => `${c} = ${placeholders[i + 1]}`)\n .join(\", \");\n\n if (onConflict === \"create\") {\n // Race-safe insert: if another worker already holds the key (and the row\n // hasn't expired), DO NOTHING; we then SELECT to surface the existing value.\n const { rows } = await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}\n WHERE ${t}.expires_at IS NOT NULL AND ${t}.expires_at < now()\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n // Conflict + existing row not expired → fetch existing for the result.\n const existing = await this.get(key);\n return { wasSet: false, existing } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n if (onConflict === \"update\") {\n // Update only when the key exists AND hasn't expired.\n const { rows } = await this.pgClient.query(\n `UPDATE ${t}\n SET ${updateSetClause}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())\n RETURNING value`,\n params,\n );\n\n if (rows.length === 0) {\n return { wasSet: false, existing: null } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n // upsert (default)\n await this.pgClient.query(\n `INSERT INTO ${t}(${colList})\n VALUES (${valList})\n ON CONFLICT (key) DO UPDATE\n SET ${setClause}`,\n params,\n );\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n this.log(\"cached\", parsedKey);\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n return value;\n }\n\n /**\n * {@inheritdoc}\n */\n public async get(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"fetching\", parsedKey);\n\n const t = this.table;\n const { rows } = await this.pgClient.query(\n `SELECT value FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n this.log(\"notFound\", parsedKey);\n await this.emit(\"miss\", { key: parsedKey });\n return null;\n }\n\n this.log(\"fetched\", parsedKey);\n\n // pg's JSONB type round-trips through node-postgres as a parsed JS value\n // already — but some pool implementations may hand back a string. Be defensive.\n let value = rows[0].value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Leave it as-is; cloning below will fail loud if it's truly broken.\n }\n }\n\n if (value === null || value === undefined) {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n const type = typeof value;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n await this.emit(\"hit\", { key: parsedKey, value });\n return value;\n }\n\n try {\n const cloned = structuredClone(value);\n await this.emit(\"hit\", { key: parsedKey, value: cloned });\n return cloned;\n } catch (error) {\n this.logError(`Failed to clone cached value for ${parsedKey}`, error);\n throw error;\n }\n }\n\n /**\n * Read the raw {@link CacheData} wrapper, including `staleAt` metadata.\n * Returns `null` for missing or expired rows — `swr()` consumes this to\n * branch on freshness without going through `get()`'s clone-and-emit path.\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n const t = this.table;\n\n const { rows } = await this.pgClient.query(\n `SELECT value, expires_at, stale_at FROM ${t}\n WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n [parsedKey],\n );\n\n if (rows.length === 0) {\n return null;\n }\n\n let data = rows[0].value;\n if (typeof data === \"string\") {\n try {\n data = JSON.parse(data);\n } catch {\n // Leave as-is — getEntry is metadata-shaped, the caller will hit the\n // same parse path on the public `get()` if they care.\n }\n }\n\n const entry: CacheData = { data };\n\n const expiresAtRaw = rows[0].expires_at as Date | string | null;\n if (expiresAtRaw) {\n const expiresAt = expiresAtRaw instanceof Date ? expiresAtRaw : new Date(expiresAtRaw);\n entry.expiresAt = expiresAt.getTime();\n }\n\n const staleAtRaw = rows[0].stale_at as Date | string | null;\n if (staleAtRaw) {\n const staleAt = staleAtRaw instanceof Date ? staleAtRaw : new Date(staleAtRaw);\n entry.staleAt = staleAt.getTime();\n }\n\n return entry;\n }\n\n /**\n * {@inheritdoc}\n */\n public async remove(key: CacheKey) {\n const parsedKey = this.parseKey(key);\n this.log(\"removing\", parsedKey);\n\n const t = this.table;\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1`, [parsedKey]);\n\n this.log(\"removed\", parsedKey);\n await this.emit(\"removed\", { key: parsedKey });\n }\n\n /**\n * {@inheritdoc}\n *\n * Deletes all rows whose key equals the namespace exactly or starts with\n * `<namespace>.` — same boundary semantics as the other drivers.\n */\n public async removeNamespace(namespace: string) {\n const parsed = this.parseKey(namespace);\n this.log(\"clearing\", parsed || \"(all)\");\n\n const t = this.table;\n\n if (parsed === \"\") {\n await this.pgClient.query(`DELETE FROM ${t}`);\n } else {\n // Escape `_` and `%` in the prefix so they aren't treated as LIKE wildcards.\n const escaped = parsed.replace(/\\\\/g, \"\\\\\\\\\").replace(/_/g, \"\\\\_\").replace(/%/g, \"\\\\%\");\n await this.pgClient.query(`DELETE FROM ${t} WHERE key = $1 OR key LIKE $2 ESCAPE '\\\\'`, [\n parsed,\n `${escaped}.%`,\n ]);\n }\n\n this.log(\"cleared\", parsed || \"(all)\");\n return this;\n }\n\n /**\n * {@inheritdoc}\n *\n * Honors `globalPrefix` — when configured, scopes the flush to entries\n * under the prefix rather than truncating the entire table (which could\n * wipe sibling tenants sharing the same Postgres database).\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await this.pgClient.query(`DELETE FROM ${this.table}`);\n }\n\n this.log(\"flushed\");\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritdoc}\n *\n * pgvector-backed similarity. Uses the `<=>` cosine-distance operator\n * (lower distance = higher similarity) and converts to cosine similarity\n * as `1 - distance` so the returned `score` matches the rest of the\n * package (`[0, 1]`, higher is more similar).\n *\n * Honors `topK`, `threshold`, and an optional `tags` filter (native\n * `tags && $tags` overlap query — much faster than the meta-key path).\n *\n * Throws {@link CacheUnsupportedError} when `options.vector` was not\n * configured at driver setup; throws {@link CacheConfigurationError} when\n * the pgvector extension is missing or the query vector's dimension count\n * doesn't match the configured one.\n */\n public async similar<T = any>(\n vector: number[],\n options: CacheSimilarOptions,\n ): Promise<CacheSimilarHit<T>[]> {\n if (!this.options.vector) {\n throw new CacheUnsupportedError(\n \"'pg' driver: similarity retrieval requires the 'vector' config block. Set options.vector.dimensions and reconnect.\",\n );\n }\n\n const expected = this.options.vector.dimensions;\n if (vector.length !== expected) {\n throw new CacheConfigurationError(\n `Pg cache driver: vector dimension mismatch — expected ${expected}, got ${vector.length}.`,\n );\n }\n\n if (!Number.isInteger(options.topK) || options.topK <= 0) {\n throw new CacheConfigurationError(\n `Pg cache driver: similar.topK must be a positive integer; got ${options.topK}.`,\n );\n }\n\n await this.ensureVectorReady();\n\n const t = this.table;\n const vecLiteral = this.formatVector(vector);\n const params: unknown[] = [vecLiteral];\n let tagFilter = \"\";\n\n if (options.tags && options.tags.length > 0) {\n params.push(options.tags);\n tagFilter = `AND tags && $${params.length}`;\n }\n\n params.push(options.topK);\n const topKParam = `$${params.length}`;\n\n const { rows } = await this.pgClient.query(\n `SELECT key, value, 1 - (embedding <=> $1::vector) AS score\n FROM ${t}\n WHERE embedding IS NOT NULL\n AND (expires_at IS NULL OR expires_at > now())\n ${tagFilter}\n ORDER BY embedding <=> $1::vector\n LIMIT ${topKParam}`,\n params,\n );\n\n const hits: CacheSimilarHit<T>[] = [];\n for (const row of rows) {\n const score = Number(row.score);\n if (options.threshold !== undefined && score < options.threshold) {\n continue;\n }\n\n let value = row.value;\n if (typeof value === \"string\") {\n try {\n value = JSON.parse(value);\n } catch {\n // Surface as-is; if non-JSON crept in, the consumer will notice.\n }\n }\n\n // Match get() cloning semantics so consumers can't mutate cached state.\n if (value !== null && value !== undefined) {\n const ty = typeof value;\n if (ty !== \"string\" && ty !== \"number\" && ty !== \"boolean\") {\n value = structuredClone(value);\n }\n }\n\n hits.push({ key: row.key, value, score });\n }\n\n return hits;\n }\n}\n"],"mappings":"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"}
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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"}
@@ -3,6 +3,22 @@ import { CacheData, CacheDriver, CacheKey, CacheSetOptions, CacheTtl, RedisOptio
3
3
  import { createClient } from "redis";
4
4
 
5
5
  //#region ../cache/src/drivers/redis-cache-driver.d.ts
6
+ /** KEYS[1]=key, ARGV[1]=expected raw value. 1 when deleted, 0 when not the owner. */
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
+ /**
9
+ * Compare-and-set used by `update()`.
10
+ *
11
+ * KEYS[1]=key; ARGV[1]=expected raw value (ignored when ARGV[3]=='1');
12
+ * ARGV[2]=new raw value; ARGV[3]='1' when the key is expected to be missing;
13
+ * ARGV[4]=ttl seconds to apply ('0' = no expiry); ARGV[5]='keep' to keep the
14
+ * existing TTL instead (only when the key existed).
15
+ *
16
+ * Returns 1 when written, 0 when the current value no longer matches.
17
+ * `KEEPTTL` requires Redis >= 6.
18
+ */
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
+ /** KEYS[1]=key. Fallback for clients without `getDel`: returns the value and deletes it. */
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";
6
22
  declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions> implements CacheDriver<ReturnType<typeof createClient>, RedisOptions> {
7
23
  /**
8
24
  * Cache driver name
@@ -16,10 +32,53 @@ declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createC
16
32
  * {@inheritDoc}
17
33
  */
18
34
  removeNamespace(namespace: string): Promise<string[] | undefined>;
35
+ /**
36
+ * Delete keys with `UNLINK` (non-blocking free), falling back to `DEL` when
37
+ * the client lacks it. Returns the number of keys removed.
38
+ */
39
+ protected deleteKeys(keys: string[]): Promise<number>;
40
+ /**
41
+ * {@inheritdoc}
42
+ *
43
+ * `SADD` — atomic across servers, so concurrent tagged writes on any number
44
+ * of instances never drop a member. The set key lives inside the prefix
45
+ * namespace, so `flush()` / `removeNamespace()` sweep it too.
46
+ */
47
+ tagAdd(tagKey: CacheKey, members: string[]): Promise<void>;
48
+ /**
49
+ * {@inheritdoc}
50
+ */
51
+ tagMembers(tagKey: CacheKey): Promise<string[]>;
52
+ /**
53
+ * {@inheritdoc}
54
+ *
55
+ * `SREM` — Redis drops the set once its last member is removed.
56
+ */
57
+ tagRemove(tagKey: CacheKey, members: string[]): Promise<void>;
58
+ /**
59
+ * {@inheritdoc}
60
+ */
61
+ tagDelete(tagKey: CacheKey): Promise<void>;
62
+ /**
63
+ * Run a SET command against a tag key, upgrading a pre-5.20 index first.
64
+ *
65
+ * Earlier versions stored the index as a JSON-array STRING under the same
66
+ * key, which makes any SET command fail with `WRONGTYPE`. On that error the
67
+ * legacy array is read, the string deleted, its members re-added with
68
+ * `SADD`, and the command retried once.
69
+ */
70
+ protected withTagSet<T>(parsedKey: string, run: (client: any) => Promise<T>): Promise<T>;
19
71
  /**
20
72
  * {@inheritDoc}
21
73
  */
22
74
  set(key: CacheKey, value: any, ttlOrOptions?: CacheTtl | CacheSetOptions): Promise<any>;
75
+ /**
76
+ * {@inheritdoc}
77
+ *
78
+ * Values are stored as `JSON.stringify(value)`, so the compare runs against
79
+ * the serialized form inside Lua.
80
+ */
81
+ protected deleteIfEquals(key: CacheKey, expected: unknown): Promise<boolean>;
23
82
  /**
24
83
  * Build the sidecar key Redis uses to track SWR freshness without
25
84
  * wrapping the main value JSON.
@@ -50,6 +109,10 @@ declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createC
50
109
  remove(key: CacheKey): Promise<void>;
51
110
  /**
52
111
  * {@inheritDoc}
112
+ *
113
+ * WARNING: without a `globalPrefix` this runs `FLUSHDB`, which clears the
114
+ * WHOLE selected Redis database (including keys not written by this driver).
115
+ * Configure a `globalPrefix` to only delete this driver's keys.
53
116
  */
54
117
  flush(): Promise<void>;
55
118
  /**
@@ -65,6 +128,28 @@ declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createC
65
128
  * would always be truthy and crash with "this.quit is not a function".
66
129
  */
67
130
  disconnect(): Promise<void>;
131
+ /**
132
+ * {@inheritdoc}
133
+ *
134
+ * Atomic across servers: `GETDEL` (Redis >= 6.2), or an equivalent Lua
135
+ * script when the client has no `getDel`. A value is handed out once.
136
+ */
137
+ pull(key: CacheKey): Promise<any | null>;
138
+ /**
139
+ * {@inheritdoc}
140
+ *
141
+ * Atomic across servers via optimistic compare-and-set: read the raw value,
142
+ * run `fn`, then write only if the value is still what was read (Lua), and
143
+ * retry otherwise. `fn` may therefore run more than once under contention;
144
+ * after {@link UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.
145
+ *
146
+ * Without an explicit `ttl` the existing expiry is kept (`KEEPTTL`); a new
147
+ * key gets the driver default TTL. Returning `null` deletes the key
148
+ * (compare-and-delete).
149
+ */
150
+ update<T = any>(key: CacheKey, fn: (current: T | null) => T | null | Promise<T | null>, options?: {
151
+ ttl?: CacheTtl;
152
+ }): Promise<T | null>;
68
153
  /**
69
154
  * Atomic increment using Redis native INCRBY command
70
155
  * {@inheritdoc}
@@ -82,5 +167,5 @@ declare class RedisCacheDriver extends BaseCacheDriver<ReturnType<typeof createC
82
167
  setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean>;
83
168
  }
84
169
  //#endregion
85
- export { RedisCacheDriver };
170
+ export { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver };
86
171
  //# sourceMappingURL=redis-cache-driver.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"redis-cache-driver.d.mts","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"mappings":";;;;;cA2Da,gBAAA,SACH,eAAA,CAAgB,UAAA,QAAkB,YAAA,GAAe,YAAA,aAC9C,WAAA,CAAY,UAAA,QAAkB,YAAA,GAAe,YAAA;;AAF1D;;EAOS,IAAA;EANmC;;;EAWnC,UAAA,CAAW,OAAA,EAAS,YAAA;EAVJ;;;EAuBV,eAAA,CAAgB,SAAA,WAAiB,OAAA;EAuCvC;;;EADM,GAAA,CACX,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EAmF2B;;;;EAAA,UAVpB,UAAA,CAAW,SAAA;EAqDC;;;;;;EAAA,UA3CN,QAAA,CAAS,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,SAAA;EAiNrB;;;;;;;EAAA,UAxLZ,eAAA,CAAgB,GAAA,EAAK,QAAA,GAAW,OAAA;EA7K1B;;;EA+LT,GAAA,CAAI,GAAA,EAAK,QAAA,GAAQ,OAAA;EAhMY;;;EA+O7B,MAAA,CAAO,GAAA,EAAK,QAAA,GAAQ,OAAA;EA9OQ;;;EA+P5B,KAAA,IAAK,OAAA;EArPS;;;EAuQd,OAAA,IAAO,OAAA;EA1P0B;;;;;;;;EA6TjC,UAAA,IAAU,OAAA;EA1Mb;;;;EA2NG,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EAjNjB;;;;EAoO5B,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EA3MV;;;;EA8NnC,KAAA,CAAM,GAAA,EAAK,QAAA,EAAU,KAAA,OAAY,GAAA,YAAe,OAAA;AAAA"}
1
+ {"version":3,"file":"redis-cache-driver.d.mts","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"mappings":";;;;;;cA4Da,2BAAA;AAAb;;;;AAAwC;AAaxC;;;;AAAiC;AAiBjC;AA9BA,cAaa,oBAAA;;cAiBA,oBAAA;AAAA,cAcA,gBAAA,SACH,eAAA,CAAgB,UAAA,QAAkB,YAAA,GAAe,YAAA,aAC9C,WAAA,CAAY,UAAA,QAAkB,YAAA,GAAe,YAAA;EAF7C;;;EAOJ,IAAA;EANiB;;;EAWjB,UAAA,CAAW,OAAA,EAAS,YAAA;EAV6B;;;EAuB3C,eAAA,CAAgB,SAAA,WAAiB,OAAA;EA2FlB;;;;EAAA,UArBZ,UAAA,CAAW,IAAA,aAAiB,OAAA;EA6CiB;;;;;;;EAxBhD,MAAA,CAAO,MAAA,EAAQ,QAAA,EAAU,OAAA,aAAoB,OAAA;EAwFzC;;;EA3EJ,UAAA,CAAW,MAAA,EAAQ,QAAA,GAAW,OAAA;EA2JuB;;;;;EAhJrD,SAAA,CAAU,MAAA,EAAQ,QAAA,EAAU,OAAA,aAAoB,OAAA;EA4NvC;;;EA/MT,SAAA,CAAU,MAAA,EAAQ,QAAA,GAAW,OAAA;EAqRxB;;;;;;;;EAAA,UAzQF,UAAA,IAAc,SAAA,UAAmB,GAAA,GAAM,MAAA,UAAgB,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;EA6ZpD;;;EAzX3B,GAAA,CACX,GAAA,EAAK,QAAA,EACL,KAAA,OACA,YAAA,GAAe,QAAA,GAAW,eAAA,GACzB,OAAA;EAubyB;;;;;;EAAA,UAxWZ,cAAA,CAAe,GAAA,EAAK,QAAA,EAAU,QAAA,YAAoB,OAAA;EA1RvD;;;;EAAA,UAiTD,UAAA,CAAW,SAAA;EAlTqB;;;;;;EAAA,UA4T1B,QAAA,CAAS,GAAA,EAAK,QAAA,GAAW,OAAA,CAAQ,SAAA;EAjT1C;;;;;;;EAAA,UA0US,eAAA,CAAgB,GAAA,EAAK,QAAA,GAAW,OAAA;EAvPJ;;;EAyQ/B,GAAA,CAAI,GAAA,EAAK,QAAA,GAAQ,OAAA;EApPQ;;;EAmSzB,MAAA,CAAO,GAAA,EAAK,QAAA,GAAQ,OAAA;EAtRT;;;;;;;EA6SX,KAAA,IAAK,OAAA;EArRa;;;EAuSlB,OAAA,IAAO,OAAA;EA3RO;;;;;;;;EAmWd,UAAA,IAAU,OAAA;EA9ThB;;;;;;EAiVM,IAAA,CAAK,GAAA,EAAK,QAAA,GAAW,OAAA;EA/PlB;;;;;;;;;;;;EAoSH,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;EA9OqC;;;;EA8SnC,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EA7O7C;;;;EAqQA,SAAA,CAAU,GAAA,EAAK,QAAA,EAAU,KAAA,YAAoB,OAAA;EA9OxC;;;;EAiQL,KAAA,CAAM,GAAA,EAAK,QAAA,EAAU,KAAA,OAAY,GAAA,YAAe,OAAA;AAAA"}