@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.
- package/CHANGELOG.md +61 -1
- package/cjs/index.cjs +1439 -331
- package/cjs/index.cjs.map +1 -1
- package/esm/cache-manager.d.mts +4 -0
- package/esm/cache-manager.d.mts.map +1 -1
- package/esm/cache-manager.mjs +23 -5
- package/esm/cache-manager.mjs.map +1 -1
- package/esm/cached/auto-key.d.mts +14 -14
- package/esm/cached/auto-key.d.mts.map +1 -1
- package/esm/cached/auto-key.mjs +50 -34
- package/esm/cached/auto-key.mjs.map +1 -1
- package/esm/cached/cached.mjs +4 -0
- package/esm/cached/cached.mjs.map +1 -1
- package/esm/drivers/base-cache-driver.d.mts +84 -8
- package/esm/drivers/base-cache-driver.d.mts.map +1 -1
- package/esm/drivers/base-cache-driver.mjs +152 -42
- package/esm/drivers/base-cache-driver.mjs.map +1 -1
- package/esm/drivers/file-cache-driver.d.mts +15 -0
- package/esm/drivers/file-cache-driver.d.mts.map +1 -1
- package/esm/drivers/file-cache-driver.mjs +68 -18
- package/esm/drivers/file-cache-driver.mjs.map +1 -1
- package/esm/drivers/in-memory-tag-index.d.mts +44 -0
- package/esm/drivers/in-memory-tag-index.d.mts.map +1 -0
- package/esm/drivers/in-memory-tag-index.mjs +73 -0
- package/esm/drivers/in-memory-tag-index.mjs.map +1 -0
- package/esm/drivers/index.d.mts +1 -1
- package/esm/drivers/index.mjs +1 -1
- package/esm/drivers/lru-memory-cache-driver.d.mts +43 -0
- package/esm/drivers/lru-memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/lru-memory-cache-driver.mjs +96 -0
- package/esm/drivers/lru-memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-cache-driver.d.mts +87 -30
- package/esm/drivers/memory-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-cache-driver.mjs +214 -96
- package/esm/drivers/memory-cache-driver.mjs.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.d.mts +9 -5
- package/esm/drivers/memory-extended-cache-driver.d.mts.map +1 -1
- package/esm/drivers/memory-extended-cache-driver.mjs +10 -23
- package/esm/drivers/memory-extended-cache-driver.mjs.map +1 -1
- package/esm/drivers/mock-cache-driver.d.mts +41 -0
- package/esm/drivers/mock-cache-driver.d.mts.map +1 -1
- package/esm/drivers/mock-cache-driver.mjs +105 -8
- package/esm/drivers/mock-cache-driver.mjs.map +1 -1
- package/esm/drivers/null-cache-driver.d.mts +8 -2
- package/esm/drivers/null-cache-driver.d.mts.map +1 -1
- package/esm/drivers/null-cache-driver.mjs +10 -2
- package/esm/drivers/null-cache-driver.mjs.map +1 -1
- package/esm/drivers/pg-cache-driver.d.mts +77 -3
- package/esm/drivers/pg-cache-driver.d.mts.map +1 -1
- package/esm/drivers/pg-cache-driver.mjs +229 -7
- package/esm/drivers/pg-cache-driver.mjs.map +1 -1
- package/esm/drivers/redis-cache-driver.d.mts +86 -1
- package/esm/drivers/redis-cache-driver.d.mts.map +1 -1
- package/esm/drivers/redis-cache-driver.mjs +252 -16
- 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 +11 -8
- package/esm/list/memory-cache-list.d.mts.map +1 -1
- package/esm/list/memory-cache-list.mjs +39 -37
- package/esm/list/memory-cache-list.mjs.map +1 -1
- package/esm/scoped-cache.d.mts +9 -3
- package/esm/scoped-cache.d.mts.map +1 -1
- package/esm/scoped-cache.mjs +19 -7
- package/esm/scoped-cache.mjs.map +1 -1
- package/esm/tagged-cache.d.mts +20 -0
- package/esm/tagged-cache.d.mts.map +1 -1
- package/esm/tagged-cache.mjs +62 -24
- package/esm/tagged-cache.mjs.map +1 -1
- package/esm/tagged-scoped-cache.d.mts +7 -3
- package/esm/tagged-scoped-cache.d.mts.map +1 -1
- package/esm/tagged-scoped-cache.mjs +19 -7
- package/esm/tagged-scoped-cache.mjs.map +1 -1
- package/esm/types.d.mts +41 -3
- package/esm/types.d.mts.map +1 -1
- package/esm/types.mjs.map +1 -1
- package/esm/utils.d.mts +9 -3
- package/esm/utils.d.mts.map +1 -1
- package/esm/utils.mjs +16 -6
- package/esm/utils.mjs.map +1 -1
- package/llms-full.txt +1685 -1666
- package/llms.txt +18 -18
- package/package.json +4 -4
- package/skills/apply-cache-patterns/SKILL.md +98 -97
- package/skills/configure-pg-cache/SKILL.md +6 -5
- package/skills/configure-set-options/SKILL.md +102 -96
- package/skills/handle-cache-errors/SKILL.md +92 -92
- package/skills/index.md +20 -0
- package/skills/observe-cache/SKILL.md +103 -103
- package/skills/pick-cache-driver/SKILL.md +129 -123
- package/skills/test-cache-code/SKILL.md +219 -219
- package/skills/use-cache-atomic/SKILL.md +57 -65
- package/skills/use-cache-bulk/SKILL.md +57 -57
- package/skills/use-cache-list/SKILL.md +85 -85
- package/skills/use-cache-lock/SKILL.md +113 -104
- package/skills/use-cache-namespace/SKILL.md +88 -88
- package/skills/use-cache-similarity/SKILL.md +94 -94
- package/skills/use-cache-tags/SKILL.md +7 -4
- package/skills/use-cache-update-merge/SKILL.md +84 -84
- package/skills/use-cache-utils/SKILL.md +89 -89
- package/skills/use-cached-hof/SKILL.md +100 -99
- 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
|
-
*
|
|
101
|
-
*
|
|
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
|
-
|
|
173
|
+
ttlToSeconds(ttl) {
|
|
104
174
|
if (!ttl || ttl === Infinity) return null;
|
|
105
|
-
return
|
|
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.
|
|
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":";;;;;;;;;AAoBA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BnB,IAAa,gBAAb,cACU,gBAEV;;;cAIgB;;;;;;;CAad,AAAO,WAAW,SAAyB;EACzC,IAAI,CAAC,WAAW,CAAC,QAAQ,UAAU,OAAO,QAAQ,OAAO,UAAU,YACjE,MAAM,IAAI,wBACR,gHACF;EAGF,MAAM,QAAQ,QAAQ,SAAS;EAC/B,IAAI,CAAC,WAAW,KAAK,KAAK,GACxB,MAAM,IAAI,wBACR,wCAAwC,MAAM,oCAChD;EAGF,IAAI,QAAQ,QAAQ;GAClB,MAAM,MAAM,QAAQ,OAAO;GAC3B,IAAI,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,GACnC,MAAM,IAAI,wBACR,sEAAsE,IAAI,EAC5E;GAEF,MAAM,MAAM,QAAQ,OAAO,SAAS;GACpC,IAAI,QAAQ,UAAU,QAAQ,WAC5B,MAAM,IAAI,wBACR,mEAAmE,IAAI,GACzE;EAEJ;EAEA,OAAO,MAAM,WAAW;GAAE,GAAG;GAAS;EAAM,CAAC;CAC/C;;;;;;;CAQA,MAAgB,oBAAmC;EACjD,IAAI,CAAC,KAAK,QAAQ,QAChB,MAAM,IAAI,sBACR,oHACF;EAGF,IAAI,KAAK,gBAAgB,MACvB;EAGF,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,qDACF;EAEA,IAAI,KAAK,WAAW,GAClB,MAAM,IAAI,wBACR,qHACF;EAGF,KAAK,cAAc;CACrB;;;;;;CAOA,AAAU,aAAa,QAA0B;EAC/C,OAAO,IAAI,OAAO,KAAK,GAAG,EAAE;CAC9B;;;;;CAMA,IAAc,QAAgB;EAC5B,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;CAMA,IAAc,WAAyB;EACrC,OAAO,KAAK,QAAQ;CACtB;;;;;CAMA,AAAU,eAAe,KAA2B;EAClD,IAAI,CAAC,OAAO,QAAQ,UAClB,OAAO;EAGT,OAAO,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,GAAI;CACzC;;;;;;;;CASA,AAAO,SAAiB;EACtB,MAAM,IAAI,KAAK;EACf,MAAM,MAAM,KAAK,QAAQ;EAEzB,MAAM,UAAU;GACd;GACA;GACA;GACA;GACA;EACF;EAEA,IAAI,KAAK;GAEP,QAAQ,QAAQ,SAAS,KAAK,QAAQ,QAAQ,SAAS,KAAK;GAC5D,QAAQ,KAAK,sBAAsB,IAAI,WAAW,EAAE;EACtD;EAEA,MAAM,QAAQ;GACZ,8BAA8B,EAAE;GAChC,GAAG;GACH;GACA,kCAAkC,EAAE,iBAAiB,EAAE;GACvD,kCAAkC,EAAE,WAAW,EAAE;EACnD;EAEA,IAAI,KAAK;GACP,MAAM,MAAM,IAAI,SAAS;GACzB,MAAM,KACJ,kCAAkC,EAAE,gBAAgB,EAAE,SAAS,IAAI,gCACrE;EACF;EAEA,OAAO,MAAM,KAAK,IAAI;CACxB;;;;CAKA,MAAa,UAAU;EAGrB,KAAK,IAAI,YAAY;EACrB,KAAK,IAAI,WAAW;EACpB,MAAM,KAAK,KAAK,WAAW;CAC7B;;;;;;CAOA,MAAa,aAAa;EACxB,KAAK,IAAI,cAAc;EACvB,MAAM,KAAK,KAAK,cAAc;CAChC;;;;CAKA,MAAa,IACX,KACA,OACA,cACc;EACd,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,EAAE,KAAK,MAAM,YAAY,QAAQ,YAAY,KAAK,kBAAkB,YAAY;EAEtF,IAAI,QAAQ;GACV,IAAI,CAAC,KAAK,QAAQ,QAChB,MAAM,IAAI,sBACR,0IACF;GAGF,MAAM,WAAW,KAAK,QAAQ,OAAO;GACrC,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,wBACR,yDAAyD,SAAS,QAAQ,OAAO,OAAO,EAC1F;GAGF,MAAM,KAAK,kBAAkB;EAC/B;EAEA,KAAK,IAAI,WAAW,SAAS;EAE7B,MAAM,YAAY,KAAK,eAAe,GAAG;EACzC,MAAM,cAAc,YAAY,SAAY,IAAI,KAAK,OAAO,IAAI;EAChE,MAAM,UAAU,QAAQ,CAAC;EACzB,MAAM,aAAa,KAAK,UAAU,KAAK;EACvC,MAAM,aAAa,SAAS,KAAK,aAAa,MAAM,IAAI;EAExD,MAAM,IAAI,KAAK;EAMf,MAAM,OAAO;GAAC;GAAO;GAAS;GAAc;GAAY;EAAM;EAC9D,MAAM,eAAe;GAAC;GAAM;GAAa;GAAM;GAAM;EAAI;EACzD,MAAM,SAAoB;GAAC;GAAW;GAAY;GAAW;GAAa;EAAO;EACjF,IAAI,eAAe,MAAM;GACvB,KAAK,KAAK,WAAW;GACrB,aAAa,KAAK,IAAI,OAAO,SAAS,EAAE,SAAS;GACjD,OAAO,KAAK,UAAU;EACxB;EACA,MAAM,UAAU,KAAK,KAAK,IAAI;EAC9B,MAAM,UAAU,aAAa,KAAK,IAAI;EACtC,MAAM,YAAY,KACf,MAAM,CAAC,CAAC,CACR,KAAK,MAAM,GAAG,EAAE,cAAc,GAAG,CAAC,CAClC,KAAK,IAAI;EACZ,MAAM,kBAAkB,KACrB,MAAM,CAAC,CAAC,CACR,KAAK,GAAG,MAAM,GAAG,EAAE,KAAK,aAAa,IAAI,IAAI,CAAC,CAC9C,KAAK,IAAI;EAEZ,IAAI,eAAe,UAAU;GAG3B,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,eAAe,EAAE,GAAG,QAAQ;mBACjB,QAAQ;;iBAEV,UAAU;mBACR,EAAE,8BAA8B,EAAE;2BAE7C,MACF;GAEA,IAAI,KAAK,WAAW,GAGlB,OAAO;IAAE,QAAQ;IAAO,gBADD,KAAK,IAAI,GAAG;GACF;GAGnC,IAAI,QAAQ,KAAK,SAAS,GACxB,MAAM,KAAK,UAAU,KAAK,IAAI;GAGhC,KAAK,IAAI,UAAU,SAAS;GAC5B,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;IAAO;GAAI,CAAC;GACrD,OAAO;IAAE,QAAQ;IAAM,UAAU;GAAK;EACxC;EAEA,IAAI,eAAe,UAAU;GAE3B,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,UAAU,EAAE;eACL,gBAAgB;;2BAGvB,MACF;GAEA,IAAI,KAAK,WAAW,GAClB,OAAO;IAAE,QAAQ;IAAO,UAAU;GAAK;GAGzC,IAAI,QAAQ,KAAK,SAAS,GACxB,MAAM,KAAK,UAAU,KAAK,IAAI;GAGhC,KAAK,IAAI,UAAU,SAAS;GAC5B,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;IAAO;GAAI,CAAC;GACrD,OAAO;IAAE,QAAQ;IAAM,UAAU;GAAK;EACxC;EAGA,MAAM,KAAK,SAAS,MAClB,eAAe,EAAE,GAAG,QAAQ;iBACjB,QAAQ;;eAEV,aACT,MACF;EAEA,IAAI,QAAQ,KAAK,SAAS,GACxB,MAAM,KAAK,UAAU,KAAK,IAAI;EAGhC,KAAK,IAAI,UAAU,SAAS;EAC5B,MAAM,KAAK,KAAK,OAAO;GAAE,KAAK;GAAW;GAAO;EAAI,CAAC;EACrD,OAAO;CACT;;;;CAKA,MAAa,IAAI,KAAe;EAC9B,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,KAAK,IAAI,YAAY,SAAS;EAE9B,MAAM,IAAI,KAAK;EACf,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,qBAAqB,EAAE;uEAEvB,CAAC,SAAS,CACZ;EAEA,IAAI,KAAK,WAAW,GAAG;GACrB,KAAK,IAAI,YAAY,SAAS;GAC9B,MAAM,KAAK,KAAK,QAAQ,EAAE,KAAK,UAAU,CAAC;GAC1C,OAAO;EACT;EAEA,KAAK,IAAI,WAAW,SAAS;EAI7B,IAAI,QAAQ,KAAK,EAAE,CAAC;EACpB,IAAI,OAAO,UAAU,UACnB,IAAI;GACF,QAAQ,KAAK,MAAM,KAAK;EAC1B,QAAQ,CAER;EAGF,IAAI,UAAU,QAAQ,UAAU,QAAW;GACzC,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;GAAM,CAAC;GAChD,OAAO;EACT;EAEA,MAAM,OAAO,OAAO;EACpB,IAAI,SAAS,YAAY,SAAS,YAAY,SAAS,WAAW;GAChE,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;GAAM,CAAC;GAChD,OAAO;EACT;EAEA,IAAI;GACF,MAAM,SAAS,gBAAgB,KAAK;GACpC,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW,OAAO;GAAO,CAAC;GACxD,OAAO;EACT,SAAS,OAAO;GACd,KAAK,SAAS,oCAAoC,aAAa,KAAK;GACpE,MAAM;EACR;CACF;;;;;;CAOA,MAAgB,SAAS,KAA0C;EACjE,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,IAAI,KAAK;EAEf,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,2CAA2C,EAAE;uEAE7C,CAAC,SAAS,CACZ;EAEA,IAAI,KAAK,WAAW,GAClB,OAAO;EAGT,IAAI,OAAO,KAAK,EAAE,CAAC;EACnB,IAAI,OAAO,SAAS,UAClB,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ,CAGR;EAGF,MAAM,QAAmB,EAAE,KAAK;EAEhC,MAAM,eAAe,KAAK,EAAE,CAAC;EAC7B,IAAI,cAEF,MAAM,aADY,wBAAwB,OAAO,eAAe,IAAI,KAAK,YAAY,EAC1D,CAAC,QAAQ;EAGtC,MAAM,aAAa,KAAK,EAAE,CAAC;EAC3B,IAAI,YAEF,MAAM,WADU,sBAAsB,OAAO,aAAa,IAAI,KAAK,UAAU,EACtD,CAAC,QAAQ;EAGlC,OAAO;CACT;;;;CAKA,MAAa,OAAO,KAAe;EACjC,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,KAAK,IAAI,YAAY,SAAS;EAE9B,MAAM,IAAI,KAAK;EACf,MAAM,KAAK,SAAS,MAAM,eAAe,EAAE,kBAAkB,CAAC,SAAS,CAAC;EAExE,KAAK,IAAI,WAAW,SAAS;EAC7B,MAAM,KAAK,KAAK,WAAW,EAAE,KAAK,UAAU,CAAC;CAC/C;;;;;;;CAQA,MAAa,gBAAgB,WAAmB;EAC9C,MAAM,SAAS,KAAK,SAAS,SAAS;EACtC,KAAK,IAAI,YAAY,UAAU,OAAO;EAEtC,MAAM,IAAI,KAAK;EAEf,IAAI,WAAW,IACb,MAAM,KAAK,SAAS,MAAM,eAAe,GAAG;OACvC;GAEL,MAAM,UAAU,OAAO,QAAQ,OAAO,MAAM,CAAC,CAAC,QAAQ,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAM,KAAK;GACtF,MAAM,KAAK,SAAS,MAAM,eAAe,EAAE,6CAA6C,CACtF,QACA,GAAG,QAAQ,GACb,CAAC;EACH;EAEA,KAAK,IAAI,WAAW,UAAU,OAAO;EACrC,OAAO;CACT;;;;;;;;CASA,MAAa,QAAQ;EACnB,KAAK,IAAI,UAAU;EAEnB,IAAI,KAAK,QAAQ,cACf,MAAM,KAAK,gBAAgB,EAAE;OAE7B,MAAM,KAAK,SAAS,MAAM,eAAe,KAAK,OAAO;EAGvD,KAAK,IAAI,SAAS;EAClB,MAAM,KAAK,KAAK,SAAS;CAC3B;;;;;;;;;;;;;;;;;CAkBA,MAAa,QACX,QACA,SAC+B;EAC/B,IAAI,CAAC,KAAK,QAAQ,QAChB,MAAM,IAAI,sBACR,oHACF;EAGF,MAAM,WAAW,KAAK,QAAQ,OAAO;EACrC,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,wBACR,yDAAyD,SAAS,QAAQ,OAAO,OAAO,EAC1F;EAGF,IAAI,CAAC,OAAO,UAAU,QAAQ,IAAI,KAAK,QAAQ,QAAQ,GACrD,MAAM,IAAI,wBACR,iEAAiE,QAAQ,KAAK,EAChF;EAGF,MAAM,KAAK,kBAAkB;EAE7B,MAAM,IAAI,KAAK;EAEf,MAAM,SAAoB,CADP,KAAK,aAAa,MACD,CAAC;EACrC,IAAI,YAAY;EAEhB,IAAI,QAAQ,QAAQ,QAAQ,KAAK,SAAS,GAAG;GAC3C,OAAO,KAAK,QAAQ,IAAI;GACxB,YAAY,gBAAgB,OAAO;EACrC;EAEA,OAAO,KAAK,QAAQ,IAAI;EACxB,MAAM,YAAY,IAAI,OAAO;EAE7B,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC;cACQ,EAAE;;;WAGL,UAAU;;eAEN,aACT,MACF;EAEA,MAAM,OAA6B,CAAC;EACpC,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,QAAQ,OAAO,IAAI,KAAK;GAC9B,IAAI,QAAQ,cAAc,UAAa,QAAQ,QAAQ,WACrD;GAGF,IAAI,QAAQ,IAAI;GAChB,IAAI,OAAO,UAAU,UACnB,IAAI;IACF,QAAQ,KAAK,MAAM,KAAK;GAC1B,QAAQ,CAER;GAIF,IAAI,UAAU,QAAQ,UAAU,QAAW;IACzC,MAAM,KAAK,OAAO;IAClB,IAAI,OAAO,YAAY,OAAO,YAAY,OAAO,WAC/C,QAAQ,gBAAgB,KAAK;GAEjC;GAEA,KAAK,KAAK;IAAE,KAAK,IAAI;IAAK;IAAO;GAAM,CAAC;EAC1C;EAEA,OAAO;CACT;AACF"}
|
|
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":";;;;;;AAiBA,MAAM,yBAAyB;;;;;;AAO/B,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BnB,IAAa,gBAAb,cACU,gBAEV;;;cAIgB;;;;;;;CAad,AAAO,WAAW,SAAyB;EACzC,IAAI,CAAC,WAAW,CAAC,QAAQ,UAAU,OAAO,QAAQ,OAAO,UAAU,YACjE,MAAM,IAAI,wBACR,gHACF;EAGF,MAAM,QAAQ,QAAQ,SAAS;EAC/B,IAAI,CAAC,WAAW,KAAK,KAAK,GACxB,MAAM,IAAI,wBACR,wCAAwC,MAAM,oCAChD;EAGF,IAAI,QAAQ,QAAQ;GAClB,MAAM,MAAM,QAAQ,OAAO;GAC3B,IAAI,CAAC,OAAO,UAAU,GAAG,KAAK,OAAO,GACnC,MAAM,IAAI,wBACR,sEAAsE,IAAI,EAC5E;GAEF,MAAM,MAAM,QAAQ,OAAO,SAAS;GACpC,IAAI,QAAQ,UAAU,QAAQ,WAC5B,MAAM,IAAI,wBACR,mEAAmE,IAAI,GACzE;EAEJ;EAEA,OAAO,MAAM,WAAW;GAAE,GAAG;GAAS;EAAM,CAAC;CAC/C;;;;;;;CAQA,MAAgB,oBAAmC;EACjD,IAAI,CAAC,KAAK,QAAQ,QAChB,MAAM,IAAI,sBACR,oHACF;EAGF,IAAI,KAAK,gBAAgB,MACvB;EAGF,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,qDACF;EAEA,IAAI,KAAK,WAAW,GAClB,MAAM,IAAI,wBACR,qHACF;EAGF,KAAK,cAAc;CACrB;;;;;;CAOA,AAAU,aAAa,QAA0B;EAC/C,OAAO,IAAI,OAAO,KAAK,GAAG,EAAE;CAC9B;;;;;CAMA,IAAc,QAAgB;EAC5B,OAAO,KAAK,QAAQ,SAAS;CAC/B;;;;;CAMA,IAAc,WAAyB;EACrC,OAAO,KAAK,QAAQ;CACtB;;;;;;CAOA,MAAgB,eAAe,KAAe,UAAqC;EACjF,MAAM,YAAY,KAAK,SAAS,GAAG;EASnC,KAAI,MAPc,KAAK,SAAS,MAC9B,eAAe,KAAK,MAAM;;qBAG1B,CAAC,WAAW,KAAK,UAAU,QAAQ,CAAC,CACtC,EAEO,CAAC,KAAK,WAAW,GACtB,OAAO;EAGT,MAAM,KAAK,KAAK,WAAW,EAAE,KAAK,UAAU,CAAC;EAE7C,OAAO;CACT;;;;;;;;;;;;;;CAeA,MAAa,OAAO,QAAkB,SAAkC;EACtE,IAAI,QAAQ,WAAW,GACrB;EAGF,MAAM,IAAI,KAAK;EAEf,MAAM,KAAK,SAAS,MAClB,mCAAmC,EAAE;;;;;;;wCAOH,EAAE,yBAAyB,EAAE;;;;;;6BAO/D,CAAC,KAAK,SAAS,MAAM,GAAG,OAAO,CACjC;CACF;;;;;;;;CASA,MAAa,UAAU,QAAkB,SAAkC;EACzE,IAAI,QAAQ,WAAW,GACrB;EAGF,MAAM,IAAI,KAAK;EACf,MAAM,YAAY,KAAK,SAAS,MAAM;EAEtC,MAAM,KAAK,SAAS,MAClB,iCAAiC,EAAE;;;;oCAIL,EAAE,yBAAyB,EAAE;;;;wBAK3D,CAAC,WAAW,OAAO,CACrB;EAEA,MAAM,KAAK,SAAS,MAClB,0CAA0C,EAAE,0CAC5C,CAAC,SAAS,CACZ;CACF;;;;;;CAOA,AAAU,aAAa,KAA6B;EAClD,IAAI,CAAC,OAAO,QAAQ,UAClB,OAAO;EAGT,OAAO;CACT;;;;;CAMA,MAAa,MAAM,QAAQ,KAAuB;EAChD,MAAM,IAAI,KAAK;EAMf,QAAO,MALW,KAAK,SAAS,MAC9B,eAAe,EAAE,mCAAmC,EAAE,sCACtD,CAAC,KAAK,CACR,EAEU,CAAC,YAAY;CACzB;;;;CAKA,AAAU,aAAmB;EAC3B,IAAI,KAAK,OAAO,KAAK,IAAI,KACvB;EAGF,KAAK,MAAM,CAAC,CAAC,OAAM,UAAS,KAAK,SAAS,gBAAgB,KAAK,CAAC;CAClE;;;;;;;;CASA,AAAO,SAAiB;EACtB,MAAM,IAAI,KAAK;EACf,MAAM,MAAM,KAAK,QAAQ;EAEzB,MAAM,UAAU;GACd;GACA;GACA;GACA;GACA;EACF;EAEA,IAAI,KAAK;GAEP,QAAQ,QAAQ,SAAS,KAAK,QAAQ,QAAQ,SAAS,KAAK;GAC5D,QAAQ,KAAK,sBAAsB,IAAI,WAAW,EAAE;EACtD;EAEA,MAAM,QAAQ;GACZ,8BAA8B,EAAE;GAChC,GAAG;GACH;GACA,kCAAkC,EAAE,iBAAiB,EAAE;GACvD,kCAAkC,EAAE,WAAW,EAAE;EACnD;EAEA,IAAI,KAAK;GACP,MAAM,MAAM,IAAI,SAAS;GACzB,MAAM,KACJ,kCAAkC,EAAE,gBAAgB,EAAE,SAAS,IAAI,gCACrE;EACF;EAEA,OAAO,MAAM,KAAK,IAAI;CACxB;;;;CAKA,MAAa,UAAU;EAGrB,KAAK,IAAI,YAAY;EACrB,KAAK,IAAI,WAAW;EACpB,MAAM,KAAK,KAAK,WAAW;CAC7B;;;;;;CAOA,MAAa,aAAa;EACxB,KAAK,IAAI,cAAc;EACvB,MAAM,KAAK,KAAK,cAAc;CAChC;;;;CAKA,MAAa,IACX,KACA,OACA,cACc;EACd,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,EAAE,KAAK,MAAM,YAAY,QAAQ,YAAY,KAAK,kBAAkB,YAAY;EAEtF,IAAI,QAAQ;GACV,IAAI,CAAC,KAAK,QAAQ,QAChB,MAAM,IAAI,sBACR,0IACF;GAGF,MAAM,WAAW,KAAK,QAAQ,OAAO;GACrC,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,wBACR,yDAAyD,SAAS,QAAQ,OAAO,OAAO,EAC1F;GAGF,MAAM,KAAK,kBAAkB;EAC/B;EAEA,KAAK,IAAI,WAAW,SAAS;EAE7B,MAAM,YAAY,KAAK,aAAa,GAAG;EACvC,MAAM,cAAc,YAAY,SAAY,IAAI,KAAK,OAAO,IAAI;EAChE,MAAM,UAAU,QAAQ,CAAC;EACzB,MAAM,aAAa,KAAK,UAAU,KAAK;EACvC,MAAM,aAAa,SAAS,KAAK,aAAa,MAAM,IAAI;EAExD,MAAM,IAAI,KAAK;EAMf,MAAM,OAAO;GAAC;GAAO;GAAS;GAAc;GAAY;EAAM;EAC9D,MAAM,eAAe;GACnB;GACA;GACA;GACA;GACA;EACF;EACA,MAAM,SAAoB;GAAC;GAAW;GAAY;GAAW;GAAa;EAAO;EACjF,IAAI,eAAe,MAAM;GACvB,KAAK,KAAK,WAAW;GACrB,aAAa,KAAK,IAAI,OAAO,SAAS,EAAE,SAAS;GACjD,OAAO,KAAK,UAAU;EACxB;EACA,MAAM,UAAU,KAAK,KAAK,IAAI;EAC9B,MAAM,UAAU,aAAa,KAAK,IAAI;EACtC,MAAM,YAAY,KACf,MAAM,CAAC,CAAC,CACR,KAAK,MAAM,GAAG,EAAE,cAAc,GAAG,CAAC,CAClC,KAAK,IAAI;EACZ,MAAM,kBAAkB,KACrB,MAAM,CAAC,CAAC,CACR,KAAK,GAAG,MAAM,GAAG,EAAE,KAAK,aAAa,IAAI,IAAI,CAAC,CAC9C,KAAK,IAAI;EAEZ,IAAI,eAAe,UAAU;GAG3B,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,eAAe,EAAE,GAAG,QAAQ;mBACjB,QAAQ;;iBAEV,UAAU;mBACR,EAAE,8BAA8B,EAAE;2BAE7C,MACF;GAEA,IAAI,KAAK,WAAW,GAGlB,OAAO;IAAE,QAAQ;IAAO,gBADD,KAAK,IAAI,GAAG;GACF;GAGnC,IAAI,QAAQ,KAAK,SAAS,GACxB,MAAM,KAAK,UAAU,KAAK,IAAI;GAGhC,KAAK,IAAI,UAAU,SAAS;GAC5B,KAAK,WAAW;GAChB,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;IAAO;GAAI,CAAC;GACrD,OAAO;IAAE,QAAQ;IAAM,UAAU;GAAK;EACxC;EAEA,IAAI,eAAe,UAAU;GAE3B,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,UAAU,EAAE;eACL,gBAAgB;;2BAGvB,MACF;GAEA,IAAI,KAAK,WAAW,GAClB,OAAO;IAAE,QAAQ;IAAO,UAAU;GAAK;GAGzC,IAAI,QAAQ,KAAK,SAAS,GACxB,MAAM,KAAK,UAAU,KAAK,IAAI;GAGhC,KAAK,IAAI,UAAU,SAAS;GAC5B,KAAK,WAAW;GAChB,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;IAAO;GAAI,CAAC;GACrD,OAAO;IAAE,QAAQ;IAAM,UAAU;GAAK;EACxC;EAGA,MAAM,KAAK,SAAS,MAClB,eAAe,EAAE,GAAG,QAAQ;iBACjB,QAAQ;;eAEV,aACT,MACF;EAEA,IAAI,QAAQ,KAAK,SAAS,GACxB,MAAM,KAAK,UAAU,KAAK,IAAI;EAGhC,KAAK,IAAI,UAAU,SAAS;EAC5B,KAAK,WAAW;EAChB,MAAM,KAAK,KAAK,OAAO;GAAE,KAAK;GAAW;GAAO;EAAI,CAAC;EACrD,OAAO;CACT;;;;CAKA,MAAa,IAAI,KAAe;EAC9B,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,KAAK,IAAI,YAAY,SAAS;EAE9B,MAAM,IAAI,KAAK;EACf,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,qBAAqB,EAAE;uEAEvB,CAAC,SAAS,CACZ;EAEA,IAAI,KAAK,WAAW,GAAG;GACrB,KAAK,IAAI,YAAY,SAAS;GAC9B,MAAM,KAAK,KAAK,QAAQ,EAAE,KAAK,UAAU,CAAC;GAC1C,OAAO;EACT;EAEA,KAAK,IAAI,WAAW,SAAS;EAI7B,IAAI,QAAQ,KAAK,EAAE,CAAC;EACpB,IAAI,OAAO,UAAU,UACnB,IAAI;GACF,QAAQ,KAAK,MAAM,KAAK;EAC1B,QAAQ,CAER;EAGF,IAAI,UAAU,QAAQ,UAAU,QAAW;GACzC,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;GAAM,CAAC;GAChD,OAAO;EACT;EAEA,MAAM,OAAO,OAAO;EACpB,IAAI,SAAS,YAAY,SAAS,YAAY,SAAS,WAAW;GAChE,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW;GAAM,CAAC;GAChD,OAAO;EACT;EAEA,IAAI;GACF,MAAM,SAAS,gBAAgB,KAAK;GACpC,MAAM,KAAK,KAAK,OAAO;IAAE,KAAK;IAAW,OAAO;GAAO,CAAC;GACxD,OAAO;EACT,SAAS,OAAO;GACd,KAAK,SAAS,oCAAoC,aAAa,KAAK;GACpE,MAAM;EACR;CACF;;;;;;CAOA,MAAgB,SAAS,KAA0C;EACjE,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,IAAI,KAAK;EAEf,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,2CAA2C,EAAE;uEAE7C,CAAC,SAAS,CACZ;EAEA,IAAI,KAAK,WAAW,GAClB,OAAO;EAGT,IAAI,OAAO,KAAK,EAAE,CAAC;EACnB,IAAI,OAAO,SAAS,UAClB,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;EACxB,QAAQ,CAGR;EAGF,MAAM,QAAmB,EAAE,KAAK;EAEhC,MAAM,eAAe,KAAK,EAAE,CAAC;EAC7B,IAAI,cAEF,MAAM,aADY,wBAAwB,OAAO,eAAe,IAAI,KAAK,YAAY,EAC1D,CAAC,QAAQ;EAGtC,MAAM,aAAa,KAAK,EAAE,CAAC;EAC3B,IAAI,YAEF,MAAM,WADU,sBAAsB,OAAO,aAAa,IAAI,KAAK,UAAU,EACtD,CAAC,QAAQ;EAGlC,OAAO;CACT;;;;;CAMA,AAAU,WAAW,OAAqB;EACxC,IAAI,OAAO,UAAU,UACnB,IAAI;GACF,OAAO,KAAK,MAAM,KAAK;EACzB,QAAQ;GACN,OAAO;EACT;EAGF,OAAO;CACT;;;;;;;;;CAUA,MAAa,UAAU,KAAe,QAAgB,GAAoB;EACxE,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,IAAI,KAAK;EACf,MAAM,UAAU,GAAG,EAAE,8BAA8B,EAAE;EAErD,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,qCAAqC,EAAE;;;iCAGZ,QAAQ;2CACE,EAAE;sCACP,QAAQ,iCAAiC,EAAE;oCAC7C,QAAQ,kBAAkB,EAAE;kBAC9C,QAAQ,oBAAoB,EAAE;yBAE1C;GAAC;GAAW;GAAO,KAAK,aAAa,KAAK,GAAG;EAAC,CAChD;EAEA,IAAI,KAAK,WAAW,GAClB,MAAM,IAAI,MAAM,+CAA+C,WAAW;EAG5E,MAAM,WAAW,OAAO,KAAK,WAAW,KAAK,EAAE,CAAC,KAAK,CAAC;EAEtD,MAAM,KAAK,KAAK,OAAO;GAAE,KAAK;GAAW,OAAO;GAAU,KAAK,KAAK;EAAI,CAAC;EAEzE,OAAO;CACT;;;;;;;CAQA,MAAa,KAAK,KAAoC;EACpD,MAAM,YAAY,KAAK,SAAS,GAAG;EAEnC,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC,gCAAgC,KAAK,MAAM;;yBAG3C,CAAC,SAAS,CACZ;EAEA,IAAI,KAAK,WAAW,GAAG;GACrB,MAAM,KAAK,KAAK,QAAQ,EAAE,KAAK,UAAU,CAAC;GAE1C,OAAO;EACT;EAEA,MAAM,QAAQ,KAAK,WAAW,KAAK,EAAE,CAAC,KAAK;EAE3C,MAAM,KAAK,KAAK,OAAO;GAAE,KAAK;GAAW;EAAM,CAAC;EAChD,MAAM,KAAK,KAAK,WAAW,EAAE,KAAK,UAAU,CAAC;EAE7C,OAAO;CACT;;;;;;;;;;;;;CAcA,MAAa,OACX,KACA,IACA,UAA8B,CAAC,GACZ;EACnB,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,MAAM,IAAI,KAAK;EACf,MAAM,OAAO;EACb,MAAM,cAAc,QAAQ,QAAQ,SAAY,SAAS,QAAQ,GAAG,IAAI;EAExE,KAAK,IAAI,UAAU,GAAG,UAAU,wBAAwB,WAAW;GACjE,MAAM,OAAO,MAAM,KAAK,SAAS,MAC/B,qBAAqB,EAAE;8BACD,QACtB,CAAC,SAAS,CACZ;GAEA,MAAM,SAAS,KAAK,KAAK,SAAS;GAClC,MAAM,UAAU,SAAU,KAAK,WAAW,KAAK,KAAK,EAAE,CAAC,KAAK,IAAU;GACtE,MAAM,WAAW,SAAS,KAAK,UAAU,OAAO,IAAI;GACpD,MAAM,SAAS,MAAM,GAAG,OAAO;GAE/B,IAAI,WAAW,MAAM;IACnB,IAAI,CAAC,QACH,OAAO;IAUT,KAAI,MAPkB,KAAK,SAAS,MAClC,sCAAsC,EAAE;sDACI,KAAK;yBAEjD,CAAC,WAAW,QAAQ,CACtB,EAEW,CAAC,KAAK,SAAS,GAAG;KAC3B,MAAM,KAAK,KAAK,WAAW,EAAE,KAAK,UAAU,CAAC;KAE7C,OAAO;IACT;IAEA;GACF;GAEA,IAAI,CAAC,QAAQ;IAMX,KAAI,MALmB,KAAK,IAAI,KAAK,QAAQ;KAC3C,YAAY;KACZ,GAAI,gBAAgB,SAAY,EAAE,KAAK,YAAY,IAAI,CAAC;IAC1D,CAAC,EAEU,CAAC,QACV,OAAO;IAGT;GACF;GAEA,MAAM,SAAoB;IAAC;IAAW,KAAK,UAAU,MAAM;IAAG;GAAQ;GACtE,IAAI,YAAY;GAEhB,IAAI,gBAAgB,QAAW;IAC7B,OAAO,KAAK,KAAK,aAAa,WAAW,CAAC;IAC1C,YAAY;GACd;GAUA,KAAI,MARkB,KAAK,SAAS,MAClC,iCAAiC,EAAE;iDACM,UAAU;oDACP,KAAK;uBAEjD,MACF,EAEW,CAAC,KAAK,SAAS,GAAG;IAC3B,MAAM,KAAK,KAAK,OAAO;KAAE,KAAK;KAAW,OAAO;KAAQ,KAAK;IAAY,CAAC;IAE1E,OAAO;GACT;EACF;EAEA,MAAM,IAAI,WACR,oBAAoB,UAAU,8BAA8B,uBAAuB,qCACrF;CACF;;;;CAKA,MAAa,OAAO,KAAe;EACjC,MAAM,YAAY,KAAK,SAAS,GAAG;EACnC,KAAK,IAAI,YAAY,SAAS;EAE9B,MAAM,IAAI,KAAK;EACf,MAAM,KAAK,SAAS,MAAM,eAAe,EAAE,kBAAkB,CAAC,SAAS,CAAC;EAExE,KAAK,IAAI,WAAW,SAAS;EAC7B,MAAM,KAAK,KAAK,WAAW,EAAE,KAAK,UAAU,CAAC;CAC/C;;;;;;;CAQA,MAAa,gBAAgB,WAAmB;EAC9C,MAAM,SAAS,KAAK,SAAS,SAAS;EACtC,KAAK,IAAI,YAAY,UAAU,OAAO;EAEtC,MAAM,IAAI,KAAK;EAEf,IAAI,WAAW,IACb,MAAM,KAAK,SAAS,MAAM,eAAe,GAAG;OACvC;GAEL,MAAM,UAAU,OAAO,QAAQ,OAAO,MAAM,CAAC,CAAC,QAAQ,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAM,KAAK;GACtF,MAAM,KAAK,SAAS,MAAM,eAAe,EAAE,6CAA6C,CACtF,QACA,GAAG,QAAQ,GACb,CAAC;EACH;EAEA,KAAK,IAAI,WAAW,UAAU,OAAO;EACrC,OAAO;CACT;;;;;;;;CASA,MAAa,QAAQ;EACnB,KAAK,IAAI,UAAU;EAEnB,IAAI,KAAK,QAAQ,cACf,MAAM,KAAK,gBAAgB,EAAE;OAE7B,MAAM,KAAK,SAAS,MAAM,eAAe,KAAK,OAAO;EAGvD,KAAK,IAAI,SAAS;EAClB,MAAM,KAAK,KAAK,SAAS;CAC3B;;;;;;;;;;;;;;;;;CAkBA,MAAa,QACX,QACA,SAC+B;EAC/B,IAAI,CAAC,KAAK,QAAQ,QAChB,MAAM,IAAI,sBACR,oHACF;EAGF,MAAM,WAAW,KAAK,QAAQ,OAAO;EACrC,IAAI,OAAO,WAAW,UACpB,MAAM,IAAI,wBACR,yDAAyD,SAAS,QAAQ,OAAO,OAAO,EAC1F;EAGF,IAAI,CAAC,OAAO,UAAU,QAAQ,IAAI,KAAK,QAAQ,QAAQ,GACrD,MAAM,IAAI,wBACR,iEAAiE,QAAQ,KAAK,EAChF;EAGF,MAAM,KAAK,kBAAkB;EAE7B,MAAM,IAAI,KAAK;EAEf,MAAM,SAAoB,CADP,KAAK,aAAa,MACD,CAAC;EACrC,IAAI,YAAY;EAEhB,IAAI,QAAQ,QAAQ,QAAQ,KAAK,SAAS,GAAG;GAC3C,OAAO,KAAK,QAAQ,IAAI;GACxB,YAAY,gBAAgB,OAAO;EACrC;EAEA,OAAO,KAAK,QAAQ,IAAI;EACxB,MAAM,YAAY,IAAI,OAAO;EAE7B,MAAM,EAAE,SAAS,MAAM,KAAK,SAAS,MACnC;cACQ,EAAE;;;WAGL,UAAU;;eAEN,aACT,MACF;EAEA,MAAM,OAA6B,CAAC;EACpC,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,QAAQ,OAAO,IAAI,KAAK;GAC9B,IAAI,QAAQ,cAAc,UAAa,QAAQ,QAAQ,WACrD;GAGF,IAAI,QAAQ,IAAI;GAChB,IAAI,OAAO,UAAU,UACnB,IAAI;IACF,QAAQ,KAAK,MAAM,KAAK;GAC1B,QAAQ,CAER;GAIF,IAAI,UAAU,QAAQ,UAAU,QAAW;IACzC,MAAM,KAAK,OAAO;IAClB,IAAI,OAAO,YAAY,OAAO,YAAY,OAAO,WAC/C,QAAQ,gBAAgB,KAAK;GAEjC;GAEA,KAAK,KAAK;IAAE,KAAK,IAAI;IAAK;IAAO;GAAM,CAAC;EAC1C;EAEA,OAAO;CACT;AACF"}
|
|
@@ -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":"
|
|
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"}
|