ioredis-toolkit 0.0.1
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/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/cache.d.ts +298 -0
- package/dist/cache.js +606 -0
- package/dist/client.d.ts +177 -0
- package/dist/client.js +958 -0
- package/dist/cluster-slot.d.ts +4 -0
- package/dist/cluster-slot.js +31 -0
- package/dist/cluster.d.ts +79 -0
- package/dist/cluster.js +156 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +63 -0
- package/dist/health.d.ts +39 -0
- package/dist/health.js +106 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +44 -0
- package/dist/lock.d.ts +215 -0
- package/dist/lock.js +385 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +40 -0
- package/dist/pubsub.d.ts +171 -0
- package/dist/pubsub.js +285 -0
- package/dist/ratelimiter.d.ts +162 -0
- package/dist/ratelimiter.js +289 -0
- package/dist/session/index.d.ts +23 -0
- package/dist/session/index.js +16 -0
- package/dist/session/revocation-store.d.ts +171 -0
- package/dist/session/revocation-store.js +310 -0
- package/dist/session/scripts/cleanup-index.lua +21 -0
- package/dist/session/scripts/conditional-update-encrypted.lua +60 -0
- package/dist/session/scripts/conditional-update.lua +63 -0
- package/dist/session/scripts/create.lua +68 -0
- package/dist/session/scripts/delete-by-user.lua +29 -0
- package/dist/session/scripts/delete.lua +15 -0
- package/dist/session/scripts/enforce-limit.lua +38 -0
- package/dist/session/scripts/revoke.lua +61 -0
- package/dist/session/scripts/rotate-encrypted.lua +107 -0
- package/dist/session/scripts/rotate.lua +119 -0
- package/dist/session/scripts/touch-encrypted.lua +89 -0
- package/dist/session/scripts/touch.lua +72 -0
- package/dist/session/scripts/validate.lua +90 -0
- package/dist/session/session-circuit-breaker.d.ts +42 -0
- package/dist/session/session-circuit-breaker.js +129 -0
- package/dist/session/session-config.d.ts +335 -0
- package/dist/session/session-config.js +162 -0
- package/dist/session/session-cookie.d.ts +72 -0
- package/dist/session/session-cookie.js +101 -0
- package/dist/session/session-encryption.d.ts +87 -0
- package/dist/session/session-encryption.js +139 -0
- package/dist/session/session-errors.d.ts +85 -0
- package/dist/session/session-errors.js +145 -0
- package/dist/session/session-health.d.ts +38 -0
- package/dist/session/session-health.js +60 -0
- package/dist/session/session-keys.d.ts +51 -0
- package/dist/session/session-keys.js +113 -0
- package/dist/session/session-manager.d.ts +59 -0
- package/dist/session/session-manager.js +94 -0
- package/dist/session/session-metrics.d.ts +33 -0
- package/dist/session/session-metrics.js +112 -0
- package/dist/session/session-repository.d.ts +161 -0
- package/dist/session/session-repository.js +683 -0
- package/dist/session/session-scripts.d.ts +36 -0
- package/dist/session/session-scripts.js +130 -0
- package/dist/session/session-serializer.d.ts +42 -0
- package/dist/session/session-serializer.js +248 -0
- package/dist/session/session-service.d.ts +104 -0
- package/dist/session/session-service.js +611 -0
- package/dist/session/session-token.d.ts +38 -0
- package/dist/session/session-token.js +86 -0
- package/dist/session/session-types.d.ts +253 -0
- package/dist/session/session-types.js +16 -0
- package/dist/types.d.ts +782 -0
- package/dist/types.js +140 -0
- package/package.json +97 -0
package/dist/cache.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import zlib from 'node:zlib';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { defaultLogger } from './logger.js';
|
|
4
|
+
const gzip = promisify(zlib.gzip);
|
|
5
|
+
const gunzip = promisify(zlib.gunzip);
|
|
6
|
+
/**
|
|
7
|
+
* Cache layer on top of {@link RedisClientWrapper} with JSON serialization,
|
|
8
|
+
* optional gzip compression and namespace support.
|
|
9
|
+
*
|
|
10
|
+
* Works in all three modes (standalone, sentinel, cluster): multi-key operations
|
|
11
|
+
* are slot-aware and pattern scans cover every cluster node.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const cache = new Cache(client, { defaultTTL: 3600, compressionThreshold: 1024 });
|
|
16
|
+
* await cache.set('user:1', { name: 'alice' });
|
|
17
|
+
* const user = await cache.get('user:1');
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class Cache {
|
|
21
|
+
client;
|
|
22
|
+
logger;
|
|
23
|
+
// private config: RedisConfig;
|
|
24
|
+
defaultTTL;
|
|
25
|
+
compressionThreshold;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a cache bound to a Redis client.
|
|
28
|
+
*
|
|
29
|
+
* @param client - The underlying {@link RedisClientWrapper}.
|
|
30
|
+
* @param config - Redis config; `defaultTTL` (seconds) and `compressionThreshold` (bytes)
|
|
31
|
+
* control cache behavior.
|
|
32
|
+
* @param logger - Optional pino-compatible logger; defaults to `console`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const cache = new Cache(client, { defaultTTL: 600, compressionThreshold: 2048 });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
constructor(client, config, logger = defaultLogger) {
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.logger = logger.child({ component: 'Cache' });
|
|
42
|
+
// this.config = config;
|
|
43
|
+
this.defaultTTL = config.defaultTTL || 3600;
|
|
44
|
+
this.compressionThreshold = config.compressionThreshold || 1024;
|
|
45
|
+
}
|
|
46
|
+
async serialize(value) {
|
|
47
|
+
// Convert to Buffer
|
|
48
|
+
let data;
|
|
49
|
+
if (Buffer.isBuffer(value)) {
|
|
50
|
+
data = value;
|
|
51
|
+
}
|
|
52
|
+
else if (typeof value === 'string') {
|
|
53
|
+
data = Buffer.from(value);
|
|
54
|
+
}
|
|
55
|
+
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
56
|
+
data = Buffer.from(String(value));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// JSON for objects
|
|
60
|
+
data = Buffer.from(JSON.stringify(value));
|
|
61
|
+
}
|
|
62
|
+
// Compress if large enough
|
|
63
|
+
if (data.length > this.compressionThreshold) {
|
|
64
|
+
try {
|
|
65
|
+
const compressed = await gzip(data);
|
|
66
|
+
return { data: compressed, compressed: true };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
this.logger.warn('Compression failed, storing uncompressed');
|
|
70
|
+
return { data, compressed: false };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { data, compressed: false };
|
|
74
|
+
}
|
|
75
|
+
async deserialize(data, compressed) {
|
|
76
|
+
let buffer = data;
|
|
77
|
+
if (compressed) {
|
|
78
|
+
try {
|
|
79
|
+
buffer = await gunzip(data);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.logger.warn('Decompression failed, trying raw data');
|
|
83
|
+
// Attempt to use raw data if decompression fails
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Try to parse as JSON if it looks like JSON
|
|
87
|
+
const str = buffer.toString();
|
|
88
|
+
try {
|
|
89
|
+
if (str.startsWith('{') || str.startsWith('[')) {
|
|
90
|
+
return JSON.parse(str);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Not JSON, return as string
|
|
95
|
+
}
|
|
96
|
+
return str;
|
|
97
|
+
}
|
|
98
|
+
getKey(key, namespace) {
|
|
99
|
+
return namespace ? `${namespace}:${key}` : key;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reads a cached value.
|
|
103
|
+
*
|
|
104
|
+
* Objects are parsed from JSON and compressed values are transparently
|
|
105
|
+
* decompressed. Strings that are not JSON are returned as-is.
|
|
106
|
+
*
|
|
107
|
+
* @param key - Cache key.
|
|
108
|
+
* @param namespace - Optional namespace prefix (`namespace:key`).
|
|
109
|
+
* @returns The stored value, or `null` when missing.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const user = await cache.get<User>('user:1');
|
|
114
|
+
* const token = await cache.get('token', 'auth');
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
async get(key, namespace) {
|
|
118
|
+
const fullKey = this.getKey(key, namespace);
|
|
119
|
+
const raw = await this.client.get(fullKey);
|
|
120
|
+
if (!raw)
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
// Check if stored with metadata
|
|
124
|
+
const parsed = JSON.parse(raw);
|
|
125
|
+
if (parsed._compressed && parsed._data) {
|
|
126
|
+
const data = Buffer.from(parsed._data, 'base64');
|
|
127
|
+
return this.deserialize(data, parsed._compressed);
|
|
128
|
+
}
|
|
129
|
+
// Legacy format - try to parse as JSON
|
|
130
|
+
return JSON.parse(raw);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Raw string value
|
|
134
|
+
return raw;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Stores a value in the cache.
|
|
139
|
+
*
|
|
140
|
+
* @param key - Cache key.
|
|
141
|
+
* @param value - Any serializable value (string, number, boolean, Buffer, object).
|
|
142
|
+
* @param options - `ttl` in seconds (defaults to `defaultTTL`), `namespace`,
|
|
143
|
+
* and `compress` (default `true`). Values larger than `compressionThreshold`
|
|
144
|
+
* bytes are gzip-compressed.
|
|
145
|
+
* @returns `true` when stored successfully.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* await cache.set('user:1', user, { ttl: 300 });
|
|
150
|
+
* await cache.set('token', 'abc', { namespace: 'auth', compress: false });
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
async set(key, value, options = {}) {
|
|
154
|
+
const fullKey = this.getKey(key, options.namespace);
|
|
155
|
+
const ttl = options.ttl || this.defaultTTL;
|
|
156
|
+
const shouldCompress = options.compress !== undefined ? options.compress : true;
|
|
157
|
+
try {
|
|
158
|
+
let rawValue;
|
|
159
|
+
if (shouldCompress) {
|
|
160
|
+
const { data, compressed } = await this.serialize(value);
|
|
161
|
+
if (compressed) {
|
|
162
|
+
// Store with metadata
|
|
163
|
+
rawValue = JSON.stringify({
|
|
164
|
+
_compressed: true,
|
|
165
|
+
_data: data.toString('base64'),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
rawValue = data;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
if (typeof value === 'string') {
|
|
174
|
+
rawValue = value;
|
|
175
|
+
}
|
|
176
|
+
else if (Buffer.isBuffer(value)) {
|
|
177
|
+
rawValue = value;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
rawValue = JSON.stringify(value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const result = await this.client.set(fullKey, rawValue, ttl);
|
|
184
|
+
this.logger.debug('Cache set', { key: fullKey, ttl, compressed: shouldCompress });
|
|
185
|
+
return result === 'OK';
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
this.logger.error('Cache set failed:', error);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Stores a value only if the key does not exist yet (`SETNX`).
|
|
194
|
+
*
|
|
195
|
+
* @param key - Cache key.
|
|
196
|
+
* @param value - The value to store.
|
|
197
|
+
* @param options - `ttl` in seconds and `namespace`.
|
|
198
|
+
* @returns `true` only when the value was actually stored.
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```ts
|
|
202
|
+
* const claimed = await cache.setNX('job:1', 'worker-1', { ttl: 60 });
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
async setNX(key, value, options = {}) {
|
|
206
|
+
const fullKey = this.getKey(key, options.namespace);
|
|
207
|
+
const ttl = options.ttl || this.defaultTTL;
|
|
208
|
+
try {
|
|
209
|
+
const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
210
|
+
const result = await this.client.setnx(fullKey, rawValue, ttl);
|
|
211
|
+
return result === 1;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
this.logger.error('Cache setNX failed:', error);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Stores a value only if the key does not exist yet, atomically with the TTL
|
|
220
|
+
* (`SET ... EX NX`).
|
|
221
|
+
*
|
|
222
|
+
* @param key - Cache key.
|
|
223
|
+
* @param value - The value to store.
|
|
224
|
+
* @param options - `ttl` in seconds and `namespace`.
|
|
225
|
+
* @returns `true` only when the value was actually stored.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* const locked = await cache.setEXNX('lock:order:42', 'txn-id', { ttl: 30 });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
async setEXNX(key, value, options = {}) {
|
|
233
|
+
const fullKey = this.getKey(key, options.namespace);
|
|
234
|
+
const ttl = options.ttl || this.defaultTTL;
|
|
235
|
+
try {
|
|
236
|
+
const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
237
|
+
const result = await this.client.setexnx(fullKey, rawValue, ttl);
|
|
238
|
+
return result === 'OK';
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
this.logger.error('Cache setEXNX failed:', error);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// Old mget - CROSSSLOT error in cluster mode when keys span different slots
|
|
246
|
+
// async mget<T = any>(keys: string[], namespace?: string): Promise<(T | null)[]> {
|
|
247
|
+
// const fullKeys = keys.map(k => this.getKey(k, namespace));
|
|
248
|
+
// const raw = await this.client.mget(...fullKeys);
|
|
249
|
+
//
|
|
250
|
+
// return Promise.all(
|
|
251
|
+
// raw.map(async (item) => {
|
|
252
|
+
// if (!item) return null;
|
|
253
|
+
// try {
|
|
254
|
+
// const parsed = JSON.parse(item);
|
|
255
|
+
// if (parsed._compressed && parsed._data) {
|
|
256
|
+
// const data = Buffer.from(parsed._data, 'base64');
|
|
257
|
+
// return this.deserialize<T>(data, parsed._compressed);
|
|
258
|
+
// }
|
|
259
|
+
// return parsed;
|
|
260
|
+
// } catch {
|
|
261
|
+
// return item as T;
|
|
262
|
+
// }
|
|
263
|
+
// })
|
|
264
|
+
// );
|
|
265
|
+
// }
|
|
266
|
+
// Cluster-safe: groups keys by slot via mgetClusterAware
|
|
267
|
+
/**
|
|
268
|
+
* Reads multiple cache keys in one call.
|
|
269
|
+
*
|
|
270
|
+
* Cluster-safe: keys are grouped by hash slot under the hood.
|
|
271
|
+
*
|
|
272
|
+
* @param keys - Cache keys to read.
|
|
273
|
+
* @param namespace - Optional namespace prefix applied to every key.
|
|
274
|
+
* @returns Values in input order; `null` for missing keys.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* const [a, b] = await cache.mget(['user:1', 'user:2']);
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
async mget(keys, namespace) {
|
|
282
|
+
const fullKeys = keys.map(k => this.getKey(k, namespace));
|
|
283
|
+
const raw = await this.client.mgetClusterAware(fullKeys);
|
|
284
|
+
return Promise.all(raw.map(async (item) => {
|
|
285
|
+
if (!item)
|
|
286
|
+
return null;
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(item);
|
|
289
|
+
if (parsed._compressed && parsed._data) {
|
|
290
|
+
const data = Buffer.from(parsed._data, 'base64');
|
|
291
|
+
return this.deserialize(data, parsed._compressed);
|
|
292
|
+
}
|
|
293
|
+
return parsed;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return item;
|
|
297
|
+
}
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
// Old mset - a pipeline whose keys span different slots is rejected in cluster mode
|
|
301
|
+
// async mset<T>(
|
|
302
|
+
// entries: Record<string, T>,
|
|
303
|
+
// options: CacheOptions = {}
|
|
304
|
+
// ): Promise<boolean> {
|
|
305
|
+
// const ttl = options.ttl || this.defaultTTL;
|
|
306
|
+
// const namespace = options.namespace;
|
|
307
|
+
//
|
|
308
|
+
// try {
|
|
309
|
+
// const pipeline = this.client.pipeline();
|
|
310
|
+
//
|
|
311
|
+
// for (const [key, value] of Object.entries(entries)) {
|
|
312
|
+
// const fullKey = this.getKey(key, namespace);
|
|
313
|
+
// const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
314
|
+
// pipeline.set(fullKey, rawValue, 'EX', ttl);
|
|
315
|
+
// }
|
|
316
|
+
//
|
|
317
|
+
// const results = await pipeline.exec();
|
|
318
|
+
// return !!results?.every((result: any) => result[1] === 'OK');
|
|
319
|
+
// } catch (error) {
|
|
320
|
+
// this.logger.error('Cache mset failed:', error as Record<string, any>);
|
|
321
|
+
// return false;
|
|
322
|
+
// }
|
|
323
|
+
// }
|
|
324
|
+
// Cluster-safe: one pipeline per hash slot
|
|
325
|
+
/**
|
|
326
|
+
* Stores multiple key/value entries in one call.
|
|
327
|
+
*
|
|
328
|
+
* Cluster-safe: entries are grouped by hash slot, one pipeline per slot.
|
|
329
|
+
*
|
|
330
|
+
* @param entries - Object mapping cache keys to values.
|
|
331
|
+
* @param options - `ttl` in seconds (defaults to `defaultTTL`) and `namespace`.
|
|
332
|
+
* @returns `true` when every entry was stored.
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```ts
|
|
336
|
+
* await cache.mset({ 'user:1': alice, 'user:2': bob }, { ttl: 300 });
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
async mset(entries, options = {}) {
|
|
340
|
+
const ttl = options.ttl || this.defaultTTL;
|
|
341
|
+
const namespace = options.namespace;
|
|
342
|
+
try {
|
|
343
|
+
const groups = new Map();
|
|
344
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
345
|
+
const fullKey = this.getKey(key, namespace);
|
|
346
|
+
const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
347
|
+
const slot = this.client.calculateSlot(fullKey);
|
|
348
|
+
if (!groups.has(slot)) {
|
|
349
|
+
groups.set(slot, []);
|
|
350
|
+
}
|
|
351
|
+
groups.get(slot).push([fullKey, rawValue]);
|
|
352
|
+
}
|
|
353
|
+
for (const group of groups.values()) {
|
|
354
|
+
const pipeline = this.client.pipeline();
|
|
355
|
+
for (const [fullKey, rawValue] of group) {
|
|
356
|
+
pipeline.set(fullKey, rawValue, 'EX', ttl);
|
|
357
|
+
}
|
|
358
|
+
const results = await pipeline.exec();
|
|
359
|
+
if (!results?.every((result) => result[1] === 'OK')) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
this.logger.error('Cache mset failed:', error);
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Deletes a cache key.
|
|
372
|
+
*
|
|
373
|
+
* @param key - Cache key.
|
|
374
|
+
* @param namespace - Optional namespace prefix.
|
|
375
|
+
* @returns `true` if the key existed and was deleted.
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* ```ts
|
|
379
|
+
* const removed = await cache.delete('user:1');
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
async delete(key, namespace) {
|
|
383
|
+
const fullKey = this.getKey(key, namespace);
|
|
384
|
+
const result = await this.client.del(fullKey);
|
|
385
|
+
return result > 0;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Checks whether a cache key exists.
|
|
389
|
+
*
|
|
390
|
+
* @param key - Cache key.
|
|
391
|
+
* @param namespace - Optional namespace prefix.
|
|
392
|
+
* @returns `true` if the key exists.
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* ```ts
|
|
396
|
+
* const cached = await cache.exists('user:1');
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
async exists(key, namespace) {
|
|
400
|
+
const fullKey = this.getKey(key, namespace);
|
|
401
|
+
const result = await this.client.exists(fullKey);
|
|
402
|
+
return result === 1;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Sets the TTL of an existing cache key.
|
|
406
|
+
*
|
|
407
|
+
* @param key - Cache key.
|
|
408
|
+
* @param ttl - TTL in seconds.
|
|
409
|
+
* @param namespace - Optional namespace prefix.
|
|
410
|
+
* @returns `true` if the TTL was applied.
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```ts
|
|
414
|
+
* const extended = await cache.expire('session:42', 3600);
|
|
415
|
+
* ```
|
|
416
|
+
*/
|
|
417
|
+
async expire(key, ttl, namespace) {
|
|
418
|
+
const fullKey = this.getKey(key, namespace);
|
|
419
|
+
const result = await this.client.expire(fullKey, ttl);
|
|
420
|
+
return result === 1;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Returns the remaining TTL of a cache key in seconds.
|
|
424
|
+
*
|
|
425
|
+
* @param key - Cache key.
|
|
426
|
+
* @param namespace - Optional namespace prefix.
|
|
427
|
+
* @returns Remaining TTL in seconds (`-2` if missing, `-1` if no TTL).
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```ts
|
|
431
|
+
* const secondsLeft = await cache.ttl('session:42');
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
async ttl(key, namespace) {
|
|
435
|
+
const fullKey = this.getKey(key, namespace);
|
|
436
|
+
return this.client.ttl(fullKey);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Atomically increments a cache counter.
|
|
440
|
+
*
|
|
441
|
+
* @param key - Counter key.
|
|
442
|
+
* @param by - Amount to increment by (default `1`; ignored by Redis, kept for API parity).
|
|
443
|
+
* @param namespace - Optional namespace prefix.
|
|
444
|
+
* @returns The new counter value.
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* ```ts
|
|
448
|
+
* const visits = await cache.increment('stats:visits');
|
|
449
|
+
* ```
|
|
450
|
+
*/
|
|
451
|
+
async increment(key, by = 1, namespace) {
|
|
452
|
+
const fullKey = this.getKey(key, namespace);
|
|
453
|
+
return this.client.incr(fullKey);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Atomically decrements a cache counter.
|
|
457
|
+
*
|
|
458
|
+
* @param key - Counter key.
|
|
459
|
+
* @param by - Amount to decrement by (default `1`; ignored by Redis, kept for API parity).
|
|
460
|
+
* @param namespace - Optional namespace prefix.
|
|
461
|
+
* @returns The new counter value.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* const stock = await cache.decrement('inventory:sku-1');
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
async decrement(key, by = 1, namespace) {
|
|
469
|
+
const fullKey = this.getKey(key, namespace);
|
|
470
|
+
return this.client.decr(fullKey);
|
|
471
|
+
}
|
|
472
|
+
// Hash helpers
|
|
473
|
+
/**
|
|
474
|
+
* Reads a field from a hash-style cache key.
|
|
475
|
+
*
|
|
476
|
+
* @param key - Cache key.
|
|
477
|
+
* @param field - Hash field.
|
|
478
|
+
* @param namespace - Optional namespace prefix.
|
|
479
|
+
* @returns The field value (JSON-parsed when possible), or `null`.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* const name = await cache.hget('user:1', 'name');
|
|
484
|
+
* ```
|
|
485
|
+
*/
|
|
486
|
+
async hget(key, field, namespace) {
|
|
487
|
+
const fullKey = this.getKey(key, namespace);
|
|
488
|
+
const result = await this.client.hget(fullKey, field);
|
|
489
|
+
if (!result)
|
|
490
|
+
return null;
|
|
491
|
+
try {
|
|
492
|
+
return JSON.parse(result);
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Writes a field into a hash-style cache key.
|
|
500
|
+
*
|
|
501
|
+
* @param key - Cache key.
|
|
502
|
+
* @param field - Hash field.
|
|
503
|
+
* @param value - Any serializable value (JSON-stringified unless it is a string).
|
|
504
|
+
* @param namespace - Optional namespace prefix.
|
|
505
|
+
* @returns `true` if a new field was created.
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* await cache.hset('user:1', 'age', 30);
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
async hset(key, field, value, namespace) {
|
|
513
|
+
const fullKey = this.getKey(key, namespace);
|
|
514
|
+
const rawValue = typeof value === 'string' ? value : JSON.stringify(value);
|
|
515
|
+
const result = await this.client.hset(fullKey, field, rawValue);
|
|
516
|
+
return result === 1;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Returns every field of a hash-style cache key.
|
|
520
|
+
*
|
|
521
|
+
* @param key - Cache key.
|
|
522
|
+
* @param namespace - Optional namespace prefix.
|
|
523
|
+
* @returns Object mapping fields to values (JSON-parsed when possible).
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* const profile = await cache.hgetall('user:1');
|
|
528
|
+
* ```
|
|
529
|
+
*/
|
|
530
|
+
async hgetall(key, namespace) {
|
|
531
|
+
const fullKey = this.getKey(key, namespace);
|
|
532
|
+
const result = await this.client.hgetall(fullKey);
|
|
533
|
+
const parsed = {};
|
|
534
|
+
for (const [field, value] of Object.entries(result)) {
|
|
535
|
+
try {
|
|
536
|
+
parsed[field] = JSON.parse(value);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
parsed[field] = value;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return parsed;
|
|
543
|
+
}
|
|
544
|
+
// Delete by pattern
|
|
545
|
+
/**
|
|
546
|
+
* Deletes every cache key matching a glob pattern.
|
|
547
|
+
*
|
|
548
|
+
* Cluster-safe: scans every node before deleting.
|
|
549
|
+
*
|
|
550
|
+
* @param pattern - Glob pattern, e.g. `'user:*'`.
|
|
551
|
+
* @param namespace - Optional namespace prefix (`namespace:pattern`).
|
|
552
|
+
* @returns The number of deleted keys.
|
|
553
|
+
*
|
|
554
|
+
* @example
|
|
555
|
+
* ```ts
|
|
556
|
+
* const removed = await cache.deletePattern('temp:*');
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
async deletePattern(pattern, namespace) {
|
|
560
|
+
const fullPattern = namespace ? `${namespace}:${pattern}` : pattern;
|
|
561
|
+
let deleted = 0;
|
|
562
|
+
for await (const key of this.client.scanIterator(fullPattern)) {
|
|
563
|
+
const result = await this.client.del(key);
|
|
564
|
+
deleted += result;
|
|
565
|
+
}
|
|
566
|
+
return deleted;
|
|
567
|
+
}
|
|
568
|
+
// Get all keys matching pattern
|
|
569
|
+
/**
|
|
570
|
+
* Lists every cache key matching a glob pattern.
|
|
571
|
+
*
|
|
572
|
+
* Cluster-safe: scans every node.
|
|
573
|
+
*
|
|
574
|
+
* @param pattern - Glob pattern, e.g. `'session:*'`.
|
|
575
|
+
* @param namespace - Optional namespace prefix (`namespace:pattern`).
|
|
576
|
+
* @returns Matching keys.
|
|
577
|
+
*
|
|
578
|
+
* @example
|
|
579
|
+
* ```ts
|
|
580
|
+
* const sessions = await cache.keys('session:*');
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
async keys(pattern, namespace) {
|
|
584
|
+
const fullPattern = namespace ? `${namespace}:${pattern}` : pattern;
|
|
585
|
+
const keys = [];
|
|
586
|
+
for await (const key of this.client.scanIterator(fullPattern)) {
|
|
587
|
+
keys.push(key);
|
|
588
|
+
}
|
|
589
|
+
return keys;
|
|
590
|
+
}
|
|
591
|
+
// Clear entire namespace
|
|
592
|
+
/**
|
|
593
|
+
* Deletes every key inside a namespace.
|
|
594
|
+
*
|
|
595
|
+
* @param namespace - Namespace to wipe (`namespace:*`).
|
|
596
|
+
* @returns The number of deleted keys.
|
|
597
|
+
*
|
|
598
|
+
* @example
|
|
599
|
+
* ```ts
|
|
600
|
+
* const cleared = await cache.clearNamespace('sessions');
|
|
601
|
+
* ```
|
|
602
|
+
*/
|
|
603
|
+
async clearNamespace(namespace) {
|
|
604
|
+
return this.deletePattern('*', namespace);
|
|
605
|
+
}
|
|
606
|
+
}
|