@warlock.js/cache 5.19.1 → 5.20.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 +55 -0
- 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,5 +1,5 @@
|
|
|
1
|
-
import { CacheConfigurationError, CacheUnsupportedError } from "../types.mjs";
|
|
2
|
-
import { safeErrorInfo } from "../utils.mjs";
|
|
1
|
+
import { CacheConfigurationError, CacheError, CacheUnsupportedError } from "../types.mjs";
|
|
2
|
+
import { parseTtl, safeErrorInfo } from "../utils.mjs";
|
|
3
3
|
import { BaseCacheDriver } from "./base-cache-driver.mjs";
|
|
4
4
|
import { log } from "@warlock.js/logger";
|
|
5
5
|
|
|
@@ -35,6 +35,42 @@ async function loadRedis() {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
loadRedis();
|
|
38
|
+
/** KEYS[1]=key, ARGV[1]=expected raw value. 1 when deleted, 0 when not the owner. */
|
|
39
|
+
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`;
|
|
40
|
+
/**
|
|
41
|
+
* Compare-and-set used by `update()`.
|
|
42
|
+
*
|
|
43
|
+
* KEYS[1]=key; ARGV[1]=expected raw value (ignored when ARGV[3]=='1');
|
|
44
|
+
* ARGV[2]=new raw value; ARGV[3]='1' when the key is expected to be missing;
|
|
45
|
+
* ARGV[4]=ttl seconds to apply ('0' = no expiry); ARGV[5]='keep' to keep the
|
|
46
|
+
* existing TTL instead (only when the key existed).
|
|
47
|
+
*
|
|
48
|
+
* Returns 1 when written, 0 when the current value no longer matches.
|
|
49
|
+
* `KEEPTTL` requires Redis >= 6.
|
|
50
|
+
*/
|
|
51
|
+
const REDIS_CAS_SET_SCRIPT = `local cur = redis.call('GET', KEYS[1])
|
|
52
|
+
if ARGV[3] == '1' then
|
|
53
|
+
if cur then return 0 end
|
|
54
|
+
elseif cur ~= ARGV[1] then
|
|
55
|
+
return 0
|
|
56
|
+
end
|
|
57
|
+
local ttl = tonumber(ARGV[4])
|
|
58
|
+
if ARGV[5] == 'keep' and ARGV[3] ~= '1' then
|
|
59
|
+
redis.call('SET', KEYS[1], ARGV[2], 'KEEPTTL')
|
|
60
|
+
elseif ttl > 0 then
|
|
61
|
+
redis.call('SET', KEYS[1], ARGV[2], 'EX', ttl)
|
|
62
|
+
else
|
|
63
|
+
redis.call('SET', KEYS[1], ARGV[2])
|
|
64
|
+
end
|
|
65
|
+
return 1`;
|
|
66
|
+
/** KEYS[1]=key. Fallback for clients without `getDel`: returns the value and deletes it. */
|
|
67
|
+
const REDIS_GET_DEL_SCRIPT = `local v = redis.call('GET', KEYS[1])
|
|
68
|
+
if v then redis.call('DEL', KEYS[1]) end
|
|
69
|
+
return v`;
|
|
70
|
+
/** Compare-and-set attempts before `update()` gives up under contention. */
|
|
71
|
+
const UPDATE_MAX_ATTEMPTS = 10;
|
|
72
|
+
/** Keys deleted per UNLINK/DEL while draining a namespace. */
|
|
73
|
+
const REMOVE_BATCH_SIZE = 500;
|
|
38
74
|
var RedisCacheDriver = class extends BaseCacheDriver {
|
|
39
75
|
constructor(..._args) {
|
|
40
76
|
super(..._args);
|
|
@@ -53,19 +89,110 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
53
89
|
async removeNamespace(namespace) {
|
|
54
90
|
namespace = this.parseKey(namespace);
|
|
55
91
|
this.log("clearing", namespace);
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
92
|
+
const escaped = namespace.replace(/[\\*?[\]]/g, "\\$&");
|
|
93
|
+
const pattern = escaped === "" ? "*" : `${escaped}.*`;
|
|
94
|
+
const deleted = [];
|
|
95
|
+
if (this.client) {
|
|
96
|
+
if (escaped !== "") {
|
|
97
|
+
if (await this.deleteKeys([namespace]) > 0) deleted.push(namespace);
|
|
98
|
+
}
|
|
99
|
+
let batch = [];
|
|
100
|
+
for await (const scanned of this.client.scanIterator({
|
|
101
|
+
MATCH: pattern,
|
|
102
|
+
COUNT: 100
|
|
103
|
+
})) {
|
|
104
|
+
const keys = Array.isArray(scanned) ? scanned : [scanned];
|
|
105
|
+
for (const key of keys) {
|
|
106
|
+
batch.push(key);
|
|
107
|
+
if (batch.length >= REMOVE_BATCH_SIZE) {
|
|
108
|
+
await this.deleteKeys(batch);
|
|
109
|
+
deleted.push(...batch);
|
|
110
|
+
batch = [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (batch.length > 0) {
|
|
115
|
+
await this.deleteKeys(batch);
|
|
116
|
+
deleted.push(...batch);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (deleted.length === 0) {
|
|
63
120
|
this.log("notFound", namespace);
|
|
64
121
|
return;
|
|
65
122
|
}
|
|
66
|
-
await this.client?.del(keys);
|
|
67
123
|
this.log("cleared", namespace);
|
|
68
|
-
return
|
|
124
|
+
return deleted;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Delete keys with `UNLINK` (non-blocking free), falling back to `DEL` when
|
|
128
|
+
* the client lacks it. Returns the number of keys removed.
|
|
129
|
+
*/
|
|
130
|
+
async deleteKeys(keys) {
|
|
131
|
+
const client = this.client;
|
|
132
|
+
if (typeof client.unlink === "function") return Number(await client.unlink(keys)) || 0;
|
|
133
|
+
return Number(await client.del(keys)) || 0;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* {@inheritdoc}
|
|
137
|
+
*
|
|
138
|
+
* `SADD` — atomic across servers, so concurrent tagged writes on any number
|
|
139
|
+
* of instances never drop a member. The set key lives inside the prefix
|
|
140
|
+
* namespace, so `flush()` / `removeNamespace()` sweep it too.
|
|
141
|
+
*/
|
|
142
|
+
async tagAdd(tagKey, members) {
|
|
143
|
+
if (members.length === 0) return;
|
|
144
|
+
const parsedKey = this.parseKey(tagKey);
|
|
145
|
+
await this.withTagSet(parsedKey, (client) => client.sAdd(parsedKey, members));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* {@inheritdoc}
|
|
149
|
+
*/
|
|
150
|
+
async tagMembers(tagKey) {
|
|
151
|
+
const parsedKey = this.parseKey(tagKey);
|
|
152
|
+
return this.withTagSet(parsedKey, async (client) => await client.sMembers(parsedKey) ?? []);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* {@inheritdoc}
|
|
156
|
+
*
|
|
157
|
+
* `SREM` — Redis drops the set once its last member is removed.
|
|
158
|
+
*/
|
|
159
|
+
async tagRemove(tagKey, members) {
|
|
160
|
+
if (members.length === 0) return;
|
|
161
|
+
const parsedKey = this.parseKey(tagKey);
|
|
162
|
+
await this.withTagSet(parsedKey, (client) => client.sRem(parsedKey, members));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* {@inheritdoc}
|
|
166
|
+
*/
|
|
167
|
+
async tagDelete(tagKey) {
|
|
168
|
+
await this.client?.del(this.parseKey(tagKey));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Run a SET command against a tag key, upgrading a pre-5.20 index first.
|
|
172
|
+
*
|
|
173
|
+
* Earlier versions stored the index as a JSON-array STRING under the same
|
|
174
|
+
* key, which makes any SET command fail with `WRONGTYPE`. On that error the
|
|
175
|
+
* legacy array is read, the string deleted, its members re-added with
|
|
176
|
+
* `SADD`, and the command retried once.
|
|
177
|
+
*/
|
|
178
|
+
async withTagSet(parsedKey, run) {
|
|
179
|
+
const client = this.client;
|
|
180
|
+
try {
|
|
181
|
+
return await run(client);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (!/WRONGTYPE/i.test(String(error?.message ?? error))) throw error;
|
|
184
|
+
const raw = await client.get(parsedKey);
|
|
185
|
+
let legacy = [];
|
|
186
|
+
try {
|
|
187
|
+
legacy = raw === null ? [] : JSON.parse(raw);
|
|
188
|
+
} catch {
|
|
189
|
+
legacy = [];
|
|
190
|
+
}
|
|
191
|
+
await client.del(parsedKey);
|
|
192
|
+
const members = Array.isArray(legacy) ? legacy.filter((member) => typeof member === "string") : [];
|
|
193
|
+
if (members.length > 0) await client.sAdd(parsedKey, members);
|
|
194
|
+
return run(client);
|
|
195
|
+
}
|
|
69
196
|
}
|
|
70
197
|
/**
|
|
71
198
|
* {@inheritDoc}
|
|
@@ -93,7 +220,8 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
93
220
|
existing: onConflict === "create" ? await this.get(key) : null
|
|
94
221
|
};
|
|
95
222
|
if (tags && tags.length > 0) await this.applyTags(key, tags);
|
|
96
|
-
if (staleAt
|
|
223
|
+
if (staleAt === void 0) await this.client?.del(this.swrMetaKey(parsedKey));
|
|
224
|
+
else {
|
|
97
225
|
const sidecarOptions = {};
|
|
98
226
|
if (hasExpiry) sidecarOptions.EX = ttl;
|
|
99
227
|
await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);
|
|
@@ -111,11 +239,28 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
111
239
|
return value;
|
|
112
240
|
}
|
|
113
241
|
/**
|
|
242
|
+
* {@inheritdoc}
|
|
243
|
+
*
|
|
244
|
+
* Values are stored as `JSON.stringify(value)`, so the compare runs against
|
|
245
|
+
* the serialized form inside Lua.
|
|
246
|
+
*/
|
|
247
|
+
async deleteIfEquals(key, expected) {
|
|
248
|
+
const parsedKey = this.parseKey(key);
|
|
249
|
+
const deleted = await this.client.eval(REDIS_COMPARE_DELETE_SCRIPT, {
|
|
250
|
+
keys: [parsedKey],
|
|
251
|
+
arguments: [JSON.stringify(expected)]
|
|
252
|
+
});
|
|
253
|
+
if (Number(deleted) !== 1) return false;
|
|
254
|
+
await this.client?.del(this.swrMetaKey(parsedKey));
|
|
255
|
+
await this.emit("removed", { key: parsedKey });
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
114
259
|
* Build the sidecar key Redis uses to track SWR freshness without
|
|
115
260
|
* wrapping the main value JSON.
|
|
116
261
|
*/
|
|
117
262
|
swrMetaKey(parsedKey) {
|
|
118
|
-
return
|
|
263
|
+
return `${parsedKey}::swrmeta`;
|
|
119
264
|
}
|
|
120
265
|
/**
|
|
121
266
|
* Read the raw {@link CacheData} wrapper, fetching the value and the
|
|
@@ -195,17 +340,22 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
195
340
|
async remove(key) {
|
|
196
341
|
key = this.parseKey(key);
|
|
197
342
|
this.log("removing", key);
|
|
198
|
-
await this.client?.del(
|
|
343
|
+
await this.client?.del(key);
|
|
344
|
+
await this.client?.del(this.swrMetaKey(key));
|
|
199
345
|
this.log("removed", key);
|
|
200
346
|
await this.emit("removed", { key });
|
|
201
347
|
}
|
|
202
348
|
/**
|
|
203
349
|
* {@inheritDoc}
|
|
350
|
+
*
|
|
351
|
+
* WARNING: without a `globalPrefix` this runs `FLUSHDB`, which clears the
|
|
352
|
+
* WHOLE selected Redis database (including keys not written by this driver).
|
|
353
|
+
* Configure a `globalPrefix` to only delete this driver's keys.
|
|
204
354
|
*/
|
|
205
355
|
async flush() {
|
|
206
356
|
this.log("flushing");
|
|
207
357
|
if (this.options.globalPrefix) await this.removeNamespace("");
|
|
208
|
-
else await this.client?.
|
|
358
|
+
else await this.client?.flushDb();
|
|
209
359
|
this.log("flushed");
|
|
210
360
|
await this.emit("flushed");
|
|
211
361
|
}
|
|
@@ -239,6 +389,8 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
239
389
|
} catch (error) {
|
|
240
390
|
log.fatal("cache", "redis", "Failed to connect", safeErrorInfo(error));
|
|
241
391
|
await this.emit("error", { error });
|
|
392
|
+
this.clientDriver = void 0;
|
|
393
|
+
throw error;
|
|
242
394
|
}
|
|
243
395
|
}
|
|
244
396
|
/**
|
|
@@ -257,6 +409,89 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
257
409
|
await this.emit("disconnected");
|
|
258
410
|
}
|
|
259
411
|
/**
|
|
412
|
+
* {@inheritdoc}
|
|
413
|
+
*
|
|
414
|
+
* Atomic across servers: `GETDEL` (Redis >= 6.2), or an equivalent Lua
|
|
415
|
+
* script when the client has no `getDel`. A value is handed out once.
|
|
416
|
+
*/
|
|
417
|
+
async pull(key) {
|
|
418
|
+
const parsedKey = this.parseKey(key);
|
|
419
|
+
const client = this.client;
|
|
420
|
+
const raw = typeof client.getDel === "function" ? await client.getDel(parsedKey) : await client.eval(REDIS_GET_DEL_SCRIPT, {
|
|
421
|
+
keys: [parsedKey],
|
|
422
|
+
arguments: []
|
|
423
|
+
});
|
|
424
|
+
if (raw === null || raw === void 0) {
|
|
425
|
+
await this.emit("miss", { key: parsedKey });
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
await this.client?.del(this.swrMetaKey(parsedKey));
|
|
429
|
+
const value = JSON.parse(raw);
|
|
430
|
+
await this.emit("hit", {
|
|
431
|
+
key: parsedKey,
|
|
432
|
+
value
|
|
433
|
+
});
|
|
434
|
+
await this.emit("removed", { key: parsedKey });
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* {@inheritdoc}
|
|
439
|
+
*
|
|
440
|
+
* Atomic across servers via optimistic compare-and-set: read the raw value,
|
|
441
|
+
* run `fn`, then write only if the value is still what was read (Lua), and
|
|
442
|
+
* retry otherwise. `fn` may therefore run more than once under contention;
|
|
443
|
+
* after {@link UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.
|
|
444
|
+
*
|
|
445
|
+
* Without an explicit `ttl` the existing expiry is kept (`KEEPTTL`); a new
|
|
446
|
+
* key gets the driver default TTL. Returning `null` deletes the key
|
|
447
|
+
* (compare-and-delete).
|
|
448
|
+
*/
|
|
449
|
+
async update(key, fn, options = {}) {
|
|
450
|
+
const parsedKey = this.parseKey(key);
|
|
451
|
+
const client = this.client;
|
|
452
|
+
const explicitTtl = options.ttl !== void 0 ? parseTtl(options.ttl) : void 0;
|
|
453
|
+
for (let attempt = 0; attempt < UPDATE_MAX_ATTEMPTS; attempt++) {
|
|
454
|
+
const raw = await client.get(parsedKey) ?? null;
|
|
455
|
+
const result = await fn(raw === null ? null : JSON.parse(raw));
|
|
456
|
+
if (result === null) {
|
|
457
|
+
if (raw === null) return null;
|
|
458
|
+
const deleted = await client.eval(REDIS_COMPARE_DELETE_SCRIPT, {
|
|
459
|
+
keys: [parsedKey],
|
|
460
|
+
arguments: [raw]
|
|
461
|
+
});
|
|
462
|
+
if (Number(deleted) === 1) {
|
|
463
|
+
await this.client?.del(this.swrMetaKey(parsedKey));
|
|
464
|
+
await this.emit("removed", { key: parsedKey });
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const isNew = raw === null;
|
|
470
|
+
const ttlSeconds = explicitTtl ?? (isNew ? this.ttl : 0);
|
|
471
|
+
const keep = explicitTtl === void 0 && !isNew;
|
|
472
|
+
const written = await client.eval(REDIS_CAS_SET_SCRIPT, {
|
|
473
|
+
keys: [parsedKey],
|
|
474
|
+
arguments: [
|
|
475
|
+
raw ?? "",
|
|
476
|
+
JSON.stringify(result),
|
|
477
|
+
isNew ? "1" : "0",
|
|
478
|
+
String(Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : 0),
|
|
479
|
+
keep ? "keep" : ""
|
|
480
|
+
]
|
|
481
|
+
});
|
|
482
|
+
if (Number(written) === 1) {
|
|
483
|
+
await this.client?.del(this.swrMetaKey(parsedKey));
|
|
484
|
+
await this.emit("set", {
|
|
485
|
+
key: parsedKey,
|
|
486
|
+
value: result,
|
|
487
|
+
ttl: ttlSeconds
|
|
488
|
+
});
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
throw new CacheError(`cache.update(): '${parsedKey}' changed on every attempt (${UPDATE_MAX_ATTEMPTS} tries) — too much write contention.`);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
260
495
|
* Atomic increment using Redis native INCRBY command
|
|
261
496
|
* {@inheritdoc}
|
|
262
497
|
*/
|
|
@@ -264,6 +499,7 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
264
499
|
const parsedKey = this.parseKey(key);
|
|
265
500
|
this.log("caching", parsedKey);
|
|
266
501
|
const result = await this.client?.incrBy(parsedKey, value);
|
|
502
|
+
if (this.ttl !== Infinity && this.ttl > 0) await this.client?.expire(parsedKey, this.ttl, "NX");
|
|
267
503
|
this.log("cached", parsedKey);
|
|
268
504
|
await this.emit("set", {
|
|
269
505
|
key: parsedKey,
|
|
@@ -316,5 +552,5 @@ var RedisCacheDriver = class extends BaseCacheDriver {
|
|
|
316
552
|
};
|
|
317
553
|
|
|
318
554
|
//#endregion
|
|
319
|
-
export { RedisCacheDriver };
|
|
555
|
+
export { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver };
|
|
320
556
|
//# sourceMappingURL=redis-cache-driver.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type { createClient } from \"redis\";\nimport type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheTtl,\n RedisOptions,\n} from \"../types\";\nimport { CacheConfigurationError, CacheUnsupportedError } from \"../types\";\nimport { safeErrorInfo } from \"../utils\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n// ============================================================\n// Lazy-loaded Redis SDK Types\n// ============================================================\n\n/**\n * Cached Redis module (loaded once, reused)\n */\nlet RedisClient: typeof import(\"redis\");\n\nlet isModuleExists: boolean | null = null;\n\n/**\n * Installation instructions for Redis package\n */\nconst REDIS_INSTALL_INSTRUCTIONS = `\nRedis cache driver requires the redis package.\nInstall it with:\n\n npm install redis\n\nOr with your preferred package manager:\n\n pnpm add redis\n yarn add redis\n`.trim();\n\n/**\n * Load Redis module\n */\nasync function loadRedis() {\n try {\n RedisClient = await import(\"redis\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n}\n\nloadRedis();\n\n// ============================================================\n// RedisCacheDriver Class\n// ============================================================\n\nexport class RedisCacheDriver\n extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions>\n implements CacheDriver<ReturnType<typeof createClient>, RedisOptions>\n{\n /**\n * Cache driver name\n */\n public name = \"redis\";\n\n /**\n * {@inheritdoc}\n */\n public setOptions(options: RedisOptions) {\n if (!options.url && !options.host) {\n throw new CacheConfigurationError(\n \"Redis driver requires either 'url' or 'host' option to be configured.\",\n );\n }\n\n return super.setOptions(options);\n }\n\n /**\n * {@inheritDoc}\n */\n public async removeNamespace(namespace: string) {\n namespace = this.parseKey(namespace);\n\n this.log(\"clearing\", namespace);\n\n // Escape Redis glob metacharacters so a namespace carrying `*`/`?`/`[`\n // cannot widen the match and delete keys outside its own prefix.\n const pattern = namespace.replace(/[\\\\*?[\\]]/g, \"\\\\$&\");\n\n // `SCAN` (cursor-based, non-blocking) instead of `KEYS` — `KEYS` is O(N)\n // and blocks the single-threaded Redis event loop for the full scan,\n // which can stall every other tenant/consumer on a large keyspace.\n const keys: string[] = [];\n\n if (this.client) {\n for await (const key of this.client.scanIterator({\n MATCH: `${pattern}*`,\n COUNT: 100,\n })) {\n keys.push(key as unknown as string);\n }\n }\n\n if (keys.length === 0) {\n this.log(\"notFound\", namespace);\n return;\n }\n\n await this.client?.del(keys);\n\n this.log(\"cleared\", namespace);\n\n return keys;\n }\n\n /**\n * {@inheritDoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n throw new CacheUnsupportedError(\n \"'redis' driver does not yet support similarity retrieval. Phase 2 (RediSearch) is on the backlog — use a memory driver or the 'pg' driver (with pgvector) for now.\",\n );\n }\n\n this.log(\"caching\", parsedKey);\n\n const serialized = JSON.stringify(value);\n const hasExpiry = Boolean(ttl) && ttl !== Infinity;\n\n let reply: string | null | undefined;\n\n if (onConflict === \"create\") {\n const options: { NX: true; EX?: number } = { NX: true };\n if (hasExpiry) {\n options.EX = ttl as number;\n }\n reply = await this.client?.set(parsedKey, serialized, options);\n } else if (onConflict === \"update\") {\n const options: { XX: true; EX?: number } = { XX: true };\n if (hasExpiry) {\n options.EX = ttl as number;\n }\n reply = await this.client?.set(parsedKey, serialized, options);\n } else if (hasExpiry) {\n reply = await this.client?.set(parsedKey, serialized, { EX: ttl as number });\n } else {\n reply = await this.client?.set(parsedKey, serialized);\n }\n\n const wasSet = reply === \"OK\";\n\n if ((onConflict === \"create\" || onConflict === \"update\") && !wasSet) {\n const existing = onConflict === \"create\" ? ((await this.get(key)) as any) : null;\n return { wasSet: false, existing } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n if (staleAt !== undefined) {\n // Sidecar key for SWR freshness — keeps the main value JSON\n // backwards-compatible with entries written before SWR landed.\n const sidecarOptions: { EX?: number } = {};\n\n if (hasExpiry) {\n sidecarOptions.EX = ttl as number;\n }\n\n await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);\n }\n\n this.log(\"cached\", parsedKey);\n\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n\n if (onConflict === \"create\" || onConflict === \"update\") {\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n return value;\n }\n\n /**\n * Build the sidecar key Redis uses to track SWR freshness without\n * wrapping the main value JSON.\n */\n protected swrMetaKey(parsedKey: string): string {\n return `__swrmeta:${parsedKey}`;\n }\n\n /**\n * Read the raw {@link CacheData} wrapper, fetching the value and the\n * SWR sidecar in parallel. Returns `null` when the main key is missing\n * or expired (Redis handles expiry natively, so the absence of the\n * value alone tells us).\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n\n const [valueRaw, staleAtRaw] = await Promise.all([\n this.client?.get(parsedKey),\n this.client?.get(this.swrMetaKey(parsedKey)),\n ]);\n\n if (!valueRaw) {\n return null;\n }\n\n const data = JSON.parse(valueRaw);\n const staleAt = staleAtRaw ? Number(staleAtRaw) : undefined;\n\n return staleAt !== undefined ? { data, staleAt } : { data };\n }\n\n /**\n * {@inheritdoc}\n *\n * Redis tracks expiry natively (the payload carries no `expiresAt`), so read\n * the remaining lifetime with the `TTL` command. Redis returns `-2` for a\n * missing key and `-1` for a key with no expiry.\n */\n protected async getRemainingTtl(key: CacheKey): Promise<number | undefined> {\n const parsedKey = this.parseKey(key);\n const ttl = await this.client?.ttl(parsedKey);\n\n if (ttl === undefined || ttl === -2) {\n return undefined;\n }\n\n if (ttl === -1) {\n return Infinity;\n }\n\n return ttl;\n }\n\n /**\n * {@inheritDoc}\n */\n public async get(key: CacheKey) {\n key = this.parseKey(key);\n\n this.log(\"fetching\", key);\n\n const value = await this.client?.get(key);\n\n if (!value) {\n this.log(\"notFound\", key);\n // Emit miss event\n await this.emit(\"miss\", { key });\n return null;\n }\n\n this.log(\"fetched\", key);\n\n // Parse and return the value directly (Redis handles expiration natively)\n const parsedValue = JSON.parse(value);\n\n // Apply cloning for immutability protection\n if (parsedValue === null || parsedValue === undefined) {\n // Emit hit event\n await this.emit(\"hit\", { key, value: parsedValue });\n return parsedValue;\n }\n\n const type = typeof parsedValue;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n // Emit hit event\n await this.emit(\"hit\", { key, value: parsedValue });\n return parsedValue;\n }\n\n try {\n const clonedValue = structuredClone(parsedValue);\n // Emit hit event\n await this.emit(\"hit\", { key, value: clonedValue });\n return clonedValue;\n } catch (error) {\n this.logError(`Failed to clone cached value for ${key}`, error);\n throw error;\n }\n }\n\n /**\n * {@inheritDoc}\n */\n public async remove(key: CacheKey) {\n key = this.parseKey(key);\n\n this.log(\"removing\", key);\n\n // Drop the SWR sidecar alongside the main key — keeps metadata from\n // surviving a `remove` and confusing a later `swr` read.\n await this.client?.del([key, this.swrMetaKey(key)]);\n\n this.log(\"removed\", key);\n\n await this.emit(\"removed\", { key });\n }\n\n /**\n * {@inheritDoc}\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await this.client?.flushAll();\n }\n\n this.log(\"flushed\");\n\n // Emit flushed event\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritDoc}\n */\n public async connect() {\n if (this.clientDriver) return;\n\n if (!isModuleExists) {\n throw new Error(REDIS_INSTALL_INSTRUCTIONS);\n }\n\n const options = this.options;\n\n if (options && !options.url && options.host) {\n const auth =\n options.password || options.username ? `${options.username}:${options.password}@` : \"\";\n\n if (!options.url) {\n const host = options.host || \"localhost\";\n const port = options.port || 6379;\n options.url = `redis://${auth}${host}:${port}`;\n }\n }\n\n const clientOptions = {\n ...options,\n ...(this.options.clientOptions || {}),\n };\n\n try {\n this.log(\"connecting\");\n const { createClient } = RedisClient;\n\n this.client = createClient(clientOptions);\n\n this.client.on(\"error\", (error: Error) => {\n // Never pass the raw `error` object to the logger — connection\n // errors can carry the connection URL (with password) in the\n // message/cause. Only the redacted message survives past this point.\n const { message } = safeErrorInfo(error);\n\n if ((error as any).code === \"ECONNREFUSED\") {\n this.log(\"connectionFailed\", message);\n } else {\n this.log(\"error\", message);\n }\n });\n\n await this.client.connect();\n\n this.log(\"connected\");\n await this.emit(\"connected\");\n } catch (error) {\n // Boot-time cache connection failure is unrecoverable in practice —\n // `fatal` aligns Redis with the cascade drivers and herald connector\n // for clean \"page on fatal only\" alerting. Only the redacted\n // `{ message, code }` shape is logged; the raw error (which may carry\n // the connection URL/password) never reaches stdout or the logger.\n log.fatal(\"cache\", \"redis\", \"Failed to connect\", safeErrorInfo(error));\n await this.emit(\"error\", { error });\n }\n }\n\n /**\n * {@inheritDoc}\n *\n * Guards against disconnecting when the client was never created. The base\n * `client` getter falls back to `this` when no client is set, so we check\n * the backing `clientDriver` directly — using `this.client` for this guard\n * would always be truthy and crash with \"this.quit is not a function\".\n */\n public async disconnect() {\n if (!this.clientDriver) {\n return;\n }\n\n this.log(\"disconnecting\");\n\n await this.clientDriver.quit();\n\n this.log(\"disconnected\");\n await this.emit(\"disconnected\");\n }\n\n /**\n * Atomic increment using Redis native INCRBY command\n * {@inheritdoc}\n */\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n const result = await this.client?.incrBy(parsedKey, value);\n\n this.log(\"cached\", parsedKey);\n\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\n\n return result || 0;\n }\n\n /**\n * Atomic decrement using Redis native DECRBY command\n * {@inheritdoc}\n */\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n const result = await this.client?.decrBy(parsedKey, value);\n\n this.log(\"cached\", parsedKey);\n\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\n\n return result || 0;\n }\n\n /**\n * Set if not exists (atomic operation)\n * Returns true if key was set, false if key already existed\n */\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n if (ttl === undefined) {\n ttl = this.ttl;\n }\n\n let result: string | null;\n\n // Use Redis native SET with NX option\n if (ttl && ttl !== Infinity) {\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\n NX: true,\n EX: ttl,\n });\n } else {\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\n NX: true,\n });\n }\n\n const wasSet = result === \"OK\";\n\n if (wasSet) {\n this.log(\"cached\", parsedKey);\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n } else {\n this.log(\"notFound\", parsedKey);\n }\n\n return wasSet;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"redis-cache-driver.mjs","names":[],"sources":["../../../../../../../cache/src/drivers/redis-cache-driver.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport type { createClient } from \"redis\";\nimport type {\n CacheData,\n CacheDriver,\n CacheKey,\n CacheSetOptions,\n CacheSetResult,\n CacheTtl,\n RedisOptions,\n} from \"../types\";\nimport { CacheConfigurationError, CacheError, CacheUnsupportedError } from \"../types\";\nimport { parseTtl, safeErrorInfo } from \"../utils\";\nimport { BaseCacheDriver } from \"./base-cache-driver\";\n\n// ============================================================\n// Lazy-loaded Redis SDK Types\n// ============================================================\n\n/**\n * Cached Redis module (loaded once, reused)\n */\nlet RedisClient: typeof import(\"redis\");\n\nlet isModuleExists: boolean | null = null;\n\n/**\n * Installation instructions for Redis package\n */\nconst REDIS_INSTALL_INSTRUCTIONS = `\nRedis cache driver requires the redis package.\nInstall it with:\n\n npm install redis\n\nOr with your preferred package manager:\n\n pnpm add redis\n yarn add redis\n`.trim();\n\n/**\n * Load Redis module\n */\nasync function loadRedis() {\n try {\n RedisClient = await import(\"redis\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n}\n\nloadRedis();\n\n// ============================================================\n// Lua scripts (atomic ownership primitives)\n// ============================================================\n\n/** KEYS[1]=key, ARGV[1]=expected raw value. 1 when deleted, 0 when not the owner. */\nexport const REDIS_COMPARE_DELETE_SCRIPT = `if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) return 1 else return 0 end`;\n\n/**\n * Compare-and-set used by `update()`.\n *\n * KEYS[1]=key; ARGV[1]=expected raw value (ignored when ARGV[3]=='1');\n * ARGV[2]=new raw value; ARGV[3]='1' when the key is expected to be missing;\n * ARGV[4]=ttl seconds to apply ('0' = no expiry); ARGV[5]='keep' to keep the\n * existing TTL instead (only when the key existed).\n *\n * Returns 1 when written, 0 when the current value no longer matches.\n * `KEEPTTL` requires Redis >= 6.\n */\nexport const REDIS_CAS_SET_SCRIPT = `local cur = redis.call('GET', KEYS[1])\nif ARGV[3] == '1' then\n if cur then return 0 end\nelseif cur ~= ARGV[1] then\n return 0\nend\nlocal ttl = tonumber(ARGV[4])\nif ARGV[5] == 'keep' and ARGV[3] ~= '1' then\n redis.call('SET', KEYS[1], ARGV[2], 'KEEPTTL')\nelseif ttl > 0 then\n redis.call('SET', KEYS[1], ARGV[2], 'EX', ttl)\nelse\n redis.call('SET', KEYS[1], ARGV[2])\nend\nreturn 1`;\n\n/** KEYS[1]=key. Fallback for clients without `getDel`: returns the value and deletes it. */\nexport const REDIS_GET_DEL_SCRIPT = `local v = redis.call('GET', KEYS[1])\nif v then redis.call('DEL', KEYS[1]) end\nreturn v`;\n\n/** Compare-and-set attempts before `update()` gives up under contention. */\nconst UPDATE_MAX_ATTEMPTS = 10;\n\n/** Keys deleted per UNLINK/DEL while draining a namespace. */\nconst REMOVE_BATCH_SIZE = 500;\n\n// ============================================================\n// RedisCacheDriver Class\n// ============================================================\n\nexport class RedisCacheDriver\n extends BaseCacheDriver<ReturnType<typeof createClient>, RedisOptions>\n implements CacheDriver<ReturnType<typeof createClient>, RedisOptions>\n{\n /**\n * Cache driver name\n */\n public name = \"redis\";\n\n /**\n * {@inheritdoc}\n */\n public setOptions(options: RedisOptions) {\n if (!options.url && !options.host) {\n throw new CacheConfigurationError(\n \"Redis driver requires either 'url' or 'host' option to be configured.\",\n );\n }\n\n return super.setOptions(options);\n }\n\n /**\n * {@inheritDoc}\n */\n public async removeNamespace(namespace: string) {\n namespace = this.parseKey(namespace);\n\n this.log(\"clearing\", namespace);\n\n // Escape Redis glob metacharacters so a namespace carrying `*`/`?`/`[`\n // cannot widen the match and delete keys outside its own prefix.\n const escaped = namespace.replace(/[\\\\*?[\\]]/g, \"\\\\$&\");\n\n // Match the namespace key itself plus `<ns>.*` — never bare `<ns>*`, which\n // would also swallow siblings like `users2`. An empty namespace (no prefix)\n // means \"everything\".\n const pattern = escaped === \"\" ? \"*\" : `${escaped}.*`;\n\n const deleted: string[] = [];\n\n if (this.client) {\n if (escaped !== \"\") {\n const removedSelf = await this.deleteKeys([namespace]);\n\n if (removedSelf > 0) {\n deleted.push(namespace);\n }\n }\n\n // `SCAN` (cursor-based, non-blocking) instead of `KEYS` — `KEYS` is O(N)\n // and blocks the single-threaded Redis event loop. Delete per batch while\n // scanning so memory stays bounded on large keyspaces.\n let batch: string[] = [];\n\n for await (const scanned of this.client.scanIterator({\n MATCH: pattern,\n COUNT: 100,\n })) {\n // node-redis 5 yields a batch of keys for each SCAN cursor response;\n // older supported clients yielded one key at a time. Normalize both\n // forms before passing keys to the command parser.\n const keys = Array.isArray(scanned) ? scanned : [scanned];\n\n for (const key of keys) {\n batch.push(key as unknown as string);\n\n if (batch.length >= REMOVE_BATCH_SIZE) {\n await this.deleteKeys(batch);\n deleted.push(...batch);\n batch = [];\n }\n }\n }\n\n if (batch.length > 0) {\n await this.deleteKeys(batch);\n deleted.push(...batch);\n }\n }\n\n if (deleted.length === 0) {\n this.log(\"notFound\", namespace);\n return;\n }\n\n this.log(\"cleared\", namespace);\n\n return deleted;\n }\n\n /**\n * Delete keys with `UNLINK` (non-blocking free), falling back to `DEL` when\n * the client lacks it. Returns the number of keys removed.\n */\n protected async deleteKeys(keys: string[]): Promise<number> {\n const client = this.client as any;\n\n if (typeof client.unlink === \"function\") {\n return Number(await client.unlink(keys)) || 0;\n }\n\n return Number(await client.del(keys)) || 0;\n }\n\n // ============================================================\n // Tag index — a native Redis SET per tag\n // ============================================================\n\n /**\n * {@inheritdoc}\n *\n * `SADD` — atomic across servers, so concurrent tagged writes on any number\n * of instances never drop a member. The set key lives inside the prefix\n * namespace, so `flush()` / `removeNamespace()` sweep it too.\n */\n public async tagAdd(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const parsedKey = this.parseKey(tagKey);\n\n await this.withTagSet(parsedKey, (client) => client.sAdd(parsedKey, members));\n }\n\n /**\n * {@inheritdoc}\n */\n public async tagMembers(tagKey: CacheKey): Promise<string[]> {\n const parsedKey = this.parseKey(tagKey);\n\n return this.withTagSet(parsedKey, async (client) => (await client.sMembers(parsedKey)) ?? []);\n }\n\n /**\n * {@inheritdoc}\n *\n * `SREM` — Redis drops the set once its last member is removed.\n */\n public async tagRemove(tagKey: CacheKey, members: string[]): Promise<void> {\n if (members.length === 0) {\n return;\n }\n\n const parsedKey = this.parseKey(tagKey);\n\n await this.withTagSet(parsedKey, (client) => client.sRem(parsedKey, members));\n }\n\n /**\n * {@inheritdoc}\n */\n public async tagDelete(tagKey: CacheKey): Promise<void> {\n await this.client?.del(this.parseKey(tagKey));\n }\n\n /**\n * Run a SET command against a tag key, upgrading a pre-5.20 index first.\n *\n * Earlier versions stored the index as a JSON-array STRING under the same\n * key, which makes any SET command fail with `WRONGTYPE`. On that error the\n * legacy array is read, the string deleted, its members re-added with\n * `SADD`, and the command retried once.\n */\n protected async withTagSet<T>(parsedKey: string, run: (client: any) => Promise<T>): Promise<T> {\n const client = this.client as any;\n\n try {\n return await run(client);\n } catch (error) {\n if (!/WRONGTYPE/i.test(String((error as Error)?.message ?? error))) {\n throw error;\n }\n\n const raw: string | null = await client.get(parsedKey);\n let legacy: unknown = [];\n\n try {\n legacy = raw === null ? [] : JSON.parse(raw);\n } catch {\n legacy = [];\n }\n\n await client.del(parsedKey);\n\n const members = Array.isArray(legacy)\n ? legacy.filter((member): member is string => typeof member === \"string\")\n : [];\n\n if (members.length > 0) {\n await client.sAdd(parsedKey, members);\n }\n\n return run(client);\n }\n }\n\n /**\n * {@inheritDoc}\n */\n public async set(\n key: CacheKey,\n value: any,\n ttlOrOptions?: CacheTtl | CacheSetOptions,\n ): Promise<any> {\n const parsedKey = this.parseKey(key);\n const { ttl, tags, onConflict, vector, staleAt } = this.resolveSetOptions(ttlOrOptions);\n\n if (vector) {\n throw new CacheUnsupportedError(\n \"'redis' driver does not yet support similarity retrieval. Phase 2 (RediSearch) is on the backlog — use a memory driver or the 'pg' driver (with pgvector) for now.\",\n );\n }\n\n this.log(\"caching\", parsedKey);\n\n const serialized = JSON.stringify(value);\n const hasExpiry = Boolean(ttl) && ttl !== Infinity;\n\n let reply: string | null | undefined;\n\n if (onConflict === \"create\") {\n const options: { NX: true; EX?: number } = { NX: true };\n if (hasExpiry) {\n options.EX = ttl as number;\n }\n reply = await this.client?.set(parsedKey, serialized, options);\n } else if (onConflict === \"update\") {\n const options: { XX: true; EX?: number } = { XX: true };\n if (hasExpiry) {\n options.EX = ttl as number;\n }\n reply = await this.client?.set(parsedKey, serialized, options);\n } else if (hasExpiry) {\n reply = await this.client?.set(parsedKey, serialized, { EX: ttl as number });\n } else {\n reply = await this.client?.set(parsedKey, serialized);\n }\n\n const wasSet = reply === \"OK\";\n\n if ((onConflict === \"create\" || onConflict === \"update\") && !wasSet) {\n const existing = onConflict === \"create\" ? ((await this.get(key)) as any) : null;\n return { wasSet: false, existing } satisfies CacheSetResult;\n }\n\n if (tags && tags.length > 0) {\n await this.applyTags(key, tags);\n }\n\n if (staleAt === undefined) {\n // A plain write invalidates any freshness marker left by an earlier SWR\n // write, otherwise the old `staleAt` would apply to the new value.\n await this.client?.del(this.swrMetaKey(parsedKey));\n } else {\n // Sidecar key for SWR freshness — keeps the main value JSON\n // backwards-compatible with entries written before SWR landed.\n const sidecarOptions: { EX?: number } = {};\n\n if (hasExpiry) {\n sidecarOptions.EX = ttl as number;\n }\n\n await this.client?.set(this.swrMetaKey(parsedKey), String(staleAt), sidecarOptions);\n }\n\n this.log(\"cached\", parsedKey);\n\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n\n if (onConflict === \"create\" || onConflict === \"update\") {\n return { wasSet: true, existing: null } satisfies CacheSetResult;\n }\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * Values are stored as `JSON.stringify(value)`, so the compare runs against\n * the serialized form inside Lua.\n */\n protected async deleteIfEquals(key: CacheKey, expected: unknown): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n const deleted = await (this.client as any).eval(REDIS_COMPARE_DELETE_SCRIPT, {\n keys: [parsedKey],\n arguments: [JSON.stringify(expected)],\n });\n\n if (Number(deleted) !== 1) {\n return false;\n }\n\n // Mirror remove(): drop the SWR sidecar and emit the event.\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"removed\", { key: parsedKey });\n\n return true;\n }\n\n /**\n * Build the sidecar key Redis uses to track SWR freshness without\n * wrapping the main value JSON.\n */\n protected swrMetaKey(parsedKey: string): string {\n return `${parsedKey}::swrmeta`;\n }\n\n /**\n * Read the raw {@link CacheData} wrapper, fetching the value and the\n * SWR sidecar in parallel. Returns `null` when the main key is missing\n * or expired (Redis handles expiry natively, so the absence of the\n * value alone tells us).\n */\n protected async getEntry(key: CacheKey): Promise<CacheData | null> {\n const parsedKey = this.parseKey(key);\n\n const [valueRaw, staleAtRaw] = await Promise.all([\n this.client?.get(parsedKey),\n this.client?.get(this.swrMetaKey(parsedKey)),\n ]);\n\n if (!valueRaw) {\n return null;\n }\n\n const data = JSON.parse(valueRaw);\n const staleAt = staleAtRaw ? Number(staleAtRaw) : undefined;\n\n return staleAt !== undefined ? { data, staleAt } : { data };\n }\n\n /**\n * {@inheritdoc}\n *\n * Redis tracks expiry natively (the payload carries no `expiresAt`), so read\n * the remaining lifetime with the `TTL` command. Redis returns `-2` for a\n * missing key and `-1` for a key with no expiry.\n */\n protected async getRemainingTtl(key: CacheKey): Promise<number | undefined> {\n const parsedKey = this.parseKey(key);\n const ttl = await this.client?.ttl(parsedKey);\n\n if (ttl === undefined || ttl === -2) {\n return undefined;\n }\n\n if (ttl === -1) {\n return Infinity;\n }\n\n return ttl;\n }\n\n /**\n * {@inheritDoc}\n */\n public async get(key: CacheKey) {\n key = this.parseKey(key);\n\n this.log(\"fetching\", key);\n\n const value = await this.client?.get(key);\n\n if (!value) {\n this.log(\"notFound\", key);\n // Emit miss event\n await this.emit(\"miss\", { key });\n return null;\n }\n\n this.log(\"fetched\", key);\n\n // Parse and return the value directly (Redis handles expiration natively)\n const parsedValue = JSON.parse(value);\n\n // Apply cloning for immutability protection\n if (parsedValue === null || parsedValue === undefined) {\n // Emit hit event\n await this.emit(\"hit\", { key, value: parsedValue });\n return parsedValue;\n }\n\n const type = typeof parsedValue;\n if (type === \"string\" || type === \"number\" || type === \"boolean\") {\n // Emit hit event\n await this.emit(\"hit\", { key, value: parsedValue });\n return parsedValue;\n }\n\n try {\n const clonedValue = structuredClone(parsedValue);\n // Emit hit event\n await this.emit(\"hit\", { key, value: clonedValue });\n return clonedValue;\n } catch (error) {\n this.logError(`Failed to clone cached value for ${key}`, error);\n throw error;\n }\n }\n\n /**\n * {@inheritDoc}\n */\n public async remove(key: CacheKey) {\n key = this.parseKey(key);\n\n this.log(\"removing\", key);\n\n // Drop the SWR sidecar alongside the main key — keeps metadata from\n // surviving a `remove` and confusing a later `swr` read.\n // Two separate DELs (not one multi-key DEL) so it stays cluster-slot safe.\n await this.client?.del(key);\n await this.client?.del(this.swrMetaKey(key));\n\n this.log(\"removed\", key);\n\n await this.emit(\"removed\", { key });\n }\n\n /**\n * {@inheritDoc}\n *\n * WARNING: without a `globalPrefix` this runs `FLUSHDB`, which clears the\n * WHOLE selected Redis database (including keys not written by this driver).\n * Configure a `globalPrefix` to only delete this driver's keys.\n */\n public async flush() {\n this.log(\"flushing\");\n\n if (this.options.globalPrefix) {\n await this.removeNamespace(\"\");\n } else {\n await this.client?.flushDb();\n }\n\n this.log(\"flushed\");\n\n // Emit flushed event\n await this.emit(\"flushed\");\n }\n\n /**\n * {@inheritDoc}\n */\n public async connect() {\n if (this.clientDriver) return;\n\n if (!isModuleExists) {\n throw new Error(REDIS_INSTALL_INSTRUCTIONS);\n }\n\n const options = this.options;\n\n if (options && !options.url && options.host) {\n const auth =\n options.password || options.username ? `${options.username}:${options.password}@` : \"\";\n\n if (!options.url) {\n const host = options.host || \"localhost\";\n const port = options.port || 6379;\n options.url = `redis://${auth}${host}:${port}`;\n }\n }\n\n const clientOptions = {\n ...options,\n ...(this.options.clientOptions || {}),\n };\n\n try {\n this.log(\"connecting\");\n const { createClient } = RedisClient;\n\n this.client = createClient(clientOptions);\n\n this.client.on(\"error\", (error: Error) => {\n // Never pass the raw `error` object to the logger — connection\n // errors can carry the connection URL (with password) in the\n // message/cause. Only the redacted message survives past this point.\n const { message } = safeErrorInfo(error);\n\n if ((error as any).code === \"ECONNREFUSED\") {\n this.log(\"connectionFailed\", message);\n } else {\n this.log(\"error\", message);\n }\n });\n\n await this.client.connect();\n\n this.log(\"connected\");\n await this.emit(\"connected\");\n } catch (error) {\n // Boot-time cache connection failure is unrecoverable in practice —\n // `fatal` aligns Redis with the cascade drivers and herald connector\n // for clean \"page on fatal only\" alerting. Only the redacted\n // `{ message, code }` shape is logged; the raw error (which may carry\n // the connection URL/password) never reaches stdout or the logger.\n log.fatal(\"cache\", \"redis\", \"Failed to connect\", safeErrorInfo(error));\n await this.emit(\"error\", { error });\n\n // Drop the half-initialised client so a later connect() can retry.\n this.clientDriver = undefined as unknown as typeof this.clientDriver;\n\n throw error;\n }\n }\n\n /**\n * {@inheritDoc}\n *\n * Guards against disconnecting when the client was never created. The base\n * `client` getter falls back to `this` when no client is set, so we check\n * the backing `clientDriver` directly — using `this.client` for this guard\n * would always be truthy and crash with \"this.quit is not a function\".\n */\n public async disconnect() {\n if (!this.clientDriver) {\n return;\n }\n\n this.log(\"disconnecting\");\n\n await this.clientDriver.quit();\n\n this.log(\"disconnected\");\n await this.emit(\"disconnected\");\n }\n\n /**\n * {@inheritdoc}\n *\n * Atomic across servers: `GETDEL` (Redis >= 6.2), or an equivalent Lua\n * script when the client has no `getDel`. A value is handed out once.\n */\n public async pull(key: CacheKey): Promise<any | null> {\n const parsedKey = this.parseKey(key);\n const client = this.client as any;\n\n const raw: string | null | undefined =\n typeof client.getDel === \"function\"\n ? await client.getDel(parsedKey)\n : await client.eval(REDIS_GET_DEL_SCRIPT, { keys: [parsedKey], arguments: [] });\n\n if (raw === null || raw === undefined) {\n await this.emit(\"miss\", { key: parsedKey });\n\n return null;\n }\n\n await this.client?.del(this.swrMetaKey(parsedKey));\n\n const value = JSON.parse(raw);\n\n await this.emit(\"hit\", { key: parsedKey, value });\n await this.emit(\"removed\", { key: parsedKey });\n\n return value;\n }\n\n /**\n * {@inheritdoc}\n *\n * Atomic across servers via optimistic compare-and-set: read the raw value,\n * run `fn`, then write only if the value is still what was read (Lua), and\n * retry otherwise. `fn` may therefore run more than once under contention;\n * after {@link UPDATE_MAX_ATTEMPTS} lost races it throws {@link CacheError}.\n *\n * Without an explicit `ttl` the existing expiry is kept (`KEEPTTL`); a new\n * key gets the driver default TTL. Returning `null` deletes the key\n * (compare-and-delete).\n */\n public async update<T = any>(\n key: CacheKey,\n fn: (current: T | null) => T | null | Promise<T | null>,\n options: { ttl?: CacheTtl } = {},\n ): Promise<T | null> {\n const parsedKey = this.parseKey(key);\n const client = this.client as any;\n\n const explicitTtl = options.ttl !== undefined ? parseTtl(options.ttl) : undefined;\n\n for (let attempt = 0; attempt < UPDATE_MAX_ATTEMPTS; attempt++) {\n const raw: string | null = (await client.get(parsedKey)) ?? null;\n const current = raw === null ? null : (JSON.parse(raw) as T);\n const result = await fn(current);\n\n if (result === null) {\n if (raw === null) {\n return null;\n }\n\n const deleted = await client.eval(REDIS_COMPARE_DELETE_SCRIPT, {\n keys: [parsedKey],\n arguments: [raw],\n });\n\n if (Number(deleted) === 1) {\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"removed\", { key: parsedKey });\n\n return null;\n }\n\n continue;\n }\n\n const isNew = raw === null;\n const ttlSeconds = explicitTtl ?? (isNew ? this.ttl : 0);\n const keep = explicitTtl === undefined && !isNew;\n\n const written = await client.eval(REDIS_CAS_SET_SCRIPT, {\n keys: [parsedKey],\n arguments: [\n raw ?? \"\",\n JSON.stringify(result),\n isNew ? \"1\" : \"0\",\n String(Number.isFinite(ttlSeconds) && ttlSeconds > 0 ? ttlSeconds : 0),\n keep ? \"keep\" : \"\",\n ],\n });\n\n if (Number(written) === 1) {\n // A plain write invalidates any SWR freshness marker.\n await this.client?.del(this.swrMetaKey(parsedKey));\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: ttlSeconds });\n\n return result;\n }\n }\n\n throw new CacheError(\n `cache.update(): '${parsedKey}' changed on every attempt (${UPDATE_MAX_ATTEMPTS} tries) — too much write contention.`,\n );\n }\n\n /**\n * Atomic increment using Redis native INCRBY command\n * {@inheritdoc}\n */\n public async increment(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n const result = await this.client?.incrBy(parsedKey, value);\n\n // A fresh counter has no TTL; apply the driver default (only if none set).\n if (this.ttl !== Infinity && this.ttl > 0) {\n await this.client?.expire(parsedKey, this.ttl, \"NX\");\n }\n\n this.log(\"cached\", parsedKey);\n\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\n\n return result || 0;\n }\n\n /**\n * Atomic decrement using Redis native DECRBY command\n * {@inheritdoc}\n */\n public async decrement(key: CacheKey, value: number = 1): Promise<number> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n const result = await this.client?.decrBy(parsedKey, value);\n\n this.log(\"cached\", parsedKey);\n\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value: result, ttl: undefined });\n\n return result || 0;\n }\n\n /**\n * Set if not exists (atomic operation)\n * Returns true if key was set, false if key already existed\n */\n public async setNX(key: CacheKey, value: any, ttl?: number): Promise<boolean> {\n const parsedKey = this.parseKey(key);\n\n this.log(\"caching\", parsedKey);\n\n if (ttl === undefined) {\n ttl = this.ttl;\n }\n\n let result: string | null;\n\n // Use Redis native SET with NX option\n if (ttl && ttl !== Infinity) {\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\n NX: true,\n EX: ttl,\n });\n } else {\n result = await this.client?.set(parsedKey, JSON.stringify(value), {\n NX: true,\n });\n }\n\n const wasSet = result === \"OK\";\n\n if (wasSet) {\n this.log(\"cached\", parsedKey);\n // Emit set event\n await this.emit(\"set\", { key: parsedKey, value, ttl });\n } else {\n this.log(\"notFound\", parsedKey);\n }\n\n return wasSet;\n }\n}\n"],"mappings":"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"}
|
package/esm/index.d.mts
CHANGED
|
@@ -6,7 +6,7 @@ import { MemoryExtendedCacheDriver } from "./drivers/memory-extended-cache-drive
|
|
|
6
6
|
import { MockCacheDriver } from "./drivers/mock-cache-driver.mjs";
|
|
7
7
|
import { NullCacheDriver } from "./drivers/null-cache-driver.mjs";
|
|
8
8
|
import { PgCacheDriver } from "./drivers/pg-cache-driver.mjs";
|
|
9
|
-
import { RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
9
|
+
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
10
10
|
import { CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, DriverClass, FileCacheOptions, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheOptions, MemoryExtendedCacheOptions, MockCacheOptions, NullCacheDriverOptions, PgCacheOptions, PgClientLike, RedisOptions, RememberOptions, ScopedCacheContract, TaggedCacheDriver, TaggedScopedCacheContract } from "./types.mjs";
|
|
11
11
|
import { CacheMetricsCollector } from "./metrics.mjs";
|
|
12
12
|
import { CacheManager, cache } from "./cache-manager.mjs";
|
|
@@ -18,4 +18,4 @@ import { ScopedCache } from "./scoped-cache.mjs";
|
|
|
18
18
|
import { TaggedCache } from "./tagged-cache.mjs";
|
|
19
19
|
import { TaggedScopedCache } from "./tagged-scoped-cache.mjs";
|
|
20
20
|
import { CACHE_FOR, cosineSimilarity, expiresAtToTtl, injectTags, mergeTagSets, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo } from "./utils.mjs";
|
|
21
|
-
export { BaseCacheDriver, CACHE_FOR, CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheManager, CacheMetricsCollector, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, CachedFn, CachedOptions, DriverClass, FileCacheDriver, FileCacheOptions, LRUMemoryCacheDriver, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheDriver, MemoryCacheList, MemoryCacheOptions, MemoryExtendedCacheDriver, MemoryExtendedCacheOptions, MockCacheDriver, MockCacheOptions, NormalizedCachedConfig, NormalizedSetOptions, NullCacheDriver, NullCacheDriverOptions, PgCacheDriver, PgCacheOptions, PgClientLike, RedisCacheDriver, RedisOptions, RememberOptions, ScopedCache, ScopedCacheContract, TaggedCache, TaggedCacheDriver, TaggedScopedCache, TaggedScopedCacheContract, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
|
21
|
+
export { BaseCacheDriver, CACHE_FOR, CacheCall, CacheConcurrencyError, CacheConfigurationError, CacheConfigurations, CacheConflictPolicy, CacheConnectionError, CacheData, CacheDriver, CacheDriverNotInitializedError, CacheError, CacheEventData, CacheEventHandler, CacheEventType, CacheKey, CacheListAccessor, CacheManager, CacheMetricsCollector, CacheMetricsSnapshot, CacheNamespaceOptions, CacheOperationType, CacheSetOptions, CacheSetResult, CacheSimilarHit, CacheSimilarOptions, CacheSwrOptions, CacheTtl, CacheUnsupportedError, CachedFn, CachedOptions, DriverClass, FileCacheDriver, FileCacheOptions, LRUMemoryCacheDriver, LRUMemoryCacheOptions, LockOptions, LockOutcome, MemoryCacheDriver, MemoryCacheList, MemoryCacheOptions, MemoryExtendedCacheDriver, MemoryExtendedCacheOptions, MockCacheDriver, MockCacheOptions, NormalizedCachedConfig, NormalizedSetOptions, NullCacheDriver, NullCacheDriverOptions, PgCacheDriver, PgCacheOptions, PgClientLike, REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver, RedisOptions, RememberOptions, ScopedCache, ScopedCacheContract, TaggedCache, TaggedCacheDriver, TaggedScopedCache, TaggedScopedCacheContract, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
package/esm/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { MemoryExtendedCacheDriver } from "./drivers/memory-extended-cache-drive
|
|
|
18
18
|
import { MockCacheDriver } from "./drivers/mock-cache-driver.mjs";
|
|
19
19
|
import { NullCacheDriver } from "./drivers/null-cache-driver.mjs";
|
|
20
20
|
import { PgCacheDriver } from "./drivers/pg-cache-driver.mjs";
|
|
21
|
-
import { RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
21
|
+
import { REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver } from "./drivers/redis-cache-driver.mjs";
|
|
22
22
|
import "./drivers/index.mjs";
|
|
23
23
|
|
|
24
|
-
export { BaseCacheDriver, CACHE_FOR, CacheConcurrencyError, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheError, CacheManager, CacheMetricsCollector, CacheUnsupportedError, FileCacheDriver, LRUMemoryCacheDriver, MemoryCacheDriver, MemoryCacheList, MemoryExtendedCacheDriver, MockCacheDriver, NullCacheDriver, PgCacheDriver, RedisCacheDriver, ScopedCache, TaggedCache, TaggedScopedCache, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
|
24
|
+
export { BaseCacheDriver, CACHE_FOR, CacheConcurrencyError, CacheConfigurationError, CacheConnectionError, CacheDriverNotInitializedError, CacheError, CacheManager, CacheMetricsCollector, CacheUnsupportedError, FileCacheDriver, LRUMemoryCacheDriver, MemoryCacheDriver, MemoryCacheList, MemoryExtendedCacheDriver, MockCacheDriver, NullCacheDriver, PgCacheDriver, REDIS_CAS_SET_SCRIPT, REDIS_COMPARE_DELETE_SCRIPT, REDIS_GET_DEL_SCRIPT, RedisCacheDriver, ScopedCache, TaggedCache, TaggedScopedCache, cache, cached, cosineSimilarity, deriveAutoKey, expiresAtToTtl, injectTags, mergeTagSets, normalizeCachedArgs, normalizeToOptions, normalizeToRememberOptions, parseCacheKey, parseTtl, redactCredentials, resolveTtl, safeErrorInfo };
|
|
@@ -9,13 +9,14 @@ import { CacheDriver, CacheKey, CacheListAccessor } from "../types.mjs";
|
|
|
9
9
|
* driver overrides `list()` to return a native-command accessor instead.
|
|
10
10
|
*
|
|
11
11
|
* **Role.** Fallback list accessor bound to a driver + key. Every mutation
|
|
12
|
-
*
|
|
12
|
+
* runs through `driver.update()`, so it is serialized in-process and keeps the
|
|
13
|
+
* entry's remaining TTL.
|
|
13
14
|
*
|
|
14
15
|
* **Responsibility.**
|
|
15
16
|
* - Owns: translating list operations into array mutations + driver writes.
|
|
16
|
-
* - Does NOT own: concurrency control (callers should wrap in a
|
|
17
|
-
* lock when multi-process writers are possible),
|
|
18
|
-
*
|
|
17
|
+
* - Does NOT own: cross-process concurrency control (callers should wrap in a
|
|
18
|
+
* distributed lock when multi-process writers are possible), or tagging of
|
|
19
|
+
* list entries.
|
|
19
20
|
*
|
|
20
21
|
* @example
|
|
21
22
|
* // Never constructed directly — obtained via driver.list():
|
|
@@ -31,10 +32,11 @@ declare class MemoryCacheList<T> implements CacheListAccessor<T> {
|
|
|
31
32
|
*/
|
|
32
33
|
private read;
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* Serialized read-modify-write through `driver.update()`. No ttl is passed,
|
|
36
|
+
* so the driver keeps the entry's remaining TTL. An empty list removes the
|
|
37
|
+
* entry to keep the store clean.
|
|
36
38
|
*/
|
|
37
|
-
private
|
|
39
|
+
private mutate;
|
|
38
40
|
/**
|
|
39
41
|
* {@inheritdoc}
|
|
40
42
|
*/
|
|
@@ -64,7 +66,8 @@ declare class MemoryCacheList<T> implements CacheListAccessor<T> {
|
|
|
64
66
|
*/
|
|
65
67
|
length(): Promise<number>;
|
|
66
68
|
/**
|
|
67
|
-
*
|
|
69
|
+
* Keep the inclusive range `[start, end]` like Redis LTRIM; negative
|
|
70
|
+
* indexes count from the end.
|
|
68
71
|
*/
|
|
69
72
|
trim(start: number, end: number): Promise<void>;
|
|
70
73
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-cache-list.d.mts","names":[],"sources":["../../../../../../../cache/src/list/memory-cache-list.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"memory-cache-list.d.mts","names":[],"sources":["../../../../../../../cache/src/list/memory-cache-list.ts"],"mappings":";;;;;AAwBA;;;;;;;;;;;;;;;;;;;;cAAa,eAAA,eAA8B,iBAAA,CAAkB,CAAA;EAAA,iBAExC,MAAA;EAAA,iBACA,GAAA;cADA,MAAA,EAAQ,WAAA,YACR,GAAA,EAAK,QAAA;EAHG;;;EAAA,QASb,IAAA;EANK;;;;;EAAA,QAgBL,MAAA;EAVA;;;EA2BP,IAAA,IAAQ,KAAA,EAAO,CAAA,KAAM,OAAA;EAAN;;;EAWf,OAAA,IAAW,KAAA,EAAO,CAAA,KAAM,OAAA;EAAN;;;EAWlB,GAAA,IAAO,OAAA,CAAQ,CAAA;EAAA;;;EAOf,KAAA,IAAS,OAAA,CAAQ,CAAA;EAOX;;;EAAA,KAAA,CAAM,KAAA,WAAgB,GAAA,YAAe,OAAA,CAAQ,CAAA;EAAA;;;EAQ7C,GAAA,IAAO,OAAA,CAAQ,CAAA;EAOf;;;EAAA,MAAA,IAAU,OAAA;EASU;;;;EAApB,IAAA,CAAK,KAAA,UAAe,GAAA,WAAc,OAAA;EAclB;;;EAAhB,KAAA,IAAS,OAAA;AAAA"}
|