@tekir/redis 0.1.3 → 0.1.4
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/dist/index.js +437 -3
- package/dist/redis.d.ts +28 -0
- package/package.json +4 -3
- package/src/redis.ts +64 -1
- package/dist/manager.js +0 -159
- package/dist/provider.js +0 -25
- package/dist/redis.js +0 -450
- package/dist/types.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,437 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// src/redis.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var nodeRequire = createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
class NodeRedisClient {
|
|
6
|
+
client;
|
|
7
|
+
messageHandlers = new Map;
|
|
8
|
+
constructor(url, options = {}) {
|
|
9
|
+
const imported = nodeRequire("ioredis");
|
|
10
|
+
const IORedis = imported.default || imported;
|
|
11
|
+
this.client = new IORedis(url, {
|
|
12
|
+
lazyConnect: true,
|
|
13
|
+
connectTimeout: options.connectionTimeout,
|
|
14
|
+
enableAutoPipelining: options.enableAutoPipelining,
|
|
15
|
+
maxRetriesPerRequest: options.maxRetries,
|
|
16
|
+
retryStrategy: options.autoReconnect === false ? null : undefined,
|
|
17
|
+
tls: options.tls === true ? {} : options.tls || undefined
|
|
18
|
+
});
|
|
19
|
+
this.client.on("message", (channel, message) => {
|
|
20
|
+
this.messageHandlers.get(channel)?.(message, channel);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async connect() {
|
|
24
|
+
if (this.client.status === "wait")
|
|
25
|
+
await this.client.connect();
|
|
26
|
+
else if (this.client.status !== "ready")
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
this.client.once("ready", resolve);
|
|
29
|
+
this.client.once("error", reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
close() {
|
|
33
|
+
this.client.disconnect();
|
|
34
|
+
}
|
|
35
|
+
get connected() {
|
|
36
|
+
return this.client.status === "ready";
|
|
37
|
+
}
|
|
38
|
+
get(key) {
|
|
39
|
+
return this.client.get(key);
|
|
40
|
+
}
|
|
41
|
+
set(key, value, ...args) {
|
|
42
|
+
return this.client.set(key, value, ...args);
|
|
43
|
+
}
|
|
44
|
+
del(...keys) {
|
|
45
|
+
return this.client.del(...keys);
|
|
46
|
+
}
|
|
47
|
+
async exists(key) {
|
|
48
|
+
return await this.client.exists(key) > 0;
|
|
49
|
+
}
|
|
50
|
+
incr(key) {
|
|
51
|
+
return this.client.incr(key);
|
|
52
|
+
}
|
|
53
|
+
decr(key) {
|
|
54
|
+
return this.client.decr(key);
|
|
55
|
+
}
|
|
56
|
+
expire(key, seconds) {
|
|
57
|
+
return this.client.expire(key, seconds);
|
|
58
|
+
}
|
|
59
|
+
ttl(key) {
|
|
60
|
+
return this.client.ttl(key);
|
|
61
|
+
}
|
|
62
|
+
hget(key, field) {
|
|
63
|
+
return this.client.hget(key, field);
|
|
64
|
+
}
|
|
65
|
+
hmset(key, fields) {
|
|
66
|
+
return this.client.hmset(key, ...fields);
|
|
67
|
+
}
|
|
68
|
+
hmget(key, fields) {
|
|
69
|
+
return this.client.hmget(key, ...fields);
|
|
70
|
+
}
|
|
71
|
+
hincrby(key, field, increment) {
|
|
72
|
+
return this.client.hincrby(key, field, increment);
|
|
73
|
+
}
|
|
74
|
+
sadd(key, ...members) {
|
|
75
|
+
return this.client.sadd(key, ...members);
|
|
76
|
+
}
|
|
77
|
+
srem(key, ...members) {
|
|
78
|
+
return this.client.srem(key, ...members);
|
|
79
|
+
}
|
|
80
|
+
async sismember(key, member) {
|
|
81
|
+
return await this.client.sismember(key, member) > 0;
|
|
82
|
+
}
|
|
83
|
+
smembers(key) {
|
|
84
|
+
return this.client.smembers(key);
|
|
85
|
+
}
|
|
86
|
+
publish(channel, message) {
|
|
87
|
+
return this.client.publish(channel, message);
|
|
88
|
+
}
|
|
89
|
+
async subscribe(channel, callback) {
|
|
90
|
+
this.messageHandlers.set(channel, callback);
|
|
91
|
+
await this.client.subscribe(channel);
|
|
92
|
+
}
|
|
93
|
+
async unsubscribe(channel) {
|
|
94
|
+
if (channel)
|
|
95
|
+
this.messageHandlers.delete(channel);
|
|
96
|
+
else
|
|
97
|
+
this.messageHandlers.clear();
|
|
98
|
+
await (channel ? this.client.unsubscribe(channel) : this.client.unsubscribe());
|
|
99
|
+
}
|
|
100
|
+
send(command, args = []) {
|
|
101
|
+
return this.client.call(command, ...args);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class Redis {
|
|
106
|
+
client;
|
|
107
|
+
prefix;
|
|
108
|
+
config;
|
|
109
|
+
constructor(config = {}) {
|
|
110
|
+
this.config = config;
|
|
111
|
+
this.prefix = (config.prefix || "").replace(/:+$/, "");
|
|
112
|
+
const RedisClient = process.versions.bun ? globalThis.Bun.RedisClient : NodeRedisClient;
|
|
113
|
+
const url = config.url || process.env.REDIS_URL || "redis://localhost:6379";
|
|
114
|
+
const isTls = config.tls != null || url.startsWith("rediss://");
|
|
115
|
+
if (!isTls && false) {}
|
|
116
|
+
this.client = new RedisClient(url, {
|
|
117
|
+
connectionTimeout: config.connectionTimeout,
|
|
118
|
+
idleTimeout: config.idleTimeout,
|
|
119
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
120
|
+
maxRetries: config.maxRetries ?? 10,
|
|
121
|
+
enableAutoPipelining: config.enableAutoPipelining ?? true,
|
|
122
|
+
tls: config.tls
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
key(k) {
|
|
126
|
+
return this.prefix ? `${this.prefix}:${k}` : k;
|
|
127
|
+
}
|
|
128
|
+
keyName(key) {
|
|
129
|
+
return this.key(key);
|
|
130
|
+
}
|
|
131
|
+
static maskUrl(url) {
|
|
132
|
+
return url.replace(/(\w+:\/\/)([^@/]+)@/, "$1***@");
|
|
133
|
+
}
|
|
134
|
+
async connect() {
|
|
135
|
+
await this.client.connect();
|
|
136
|
+
}
|
|
137
|
+
close() {
|
|
138
|
+
this.client.close();
|
|
139
|
+
}
|
|
140
|
+
get connected() {
|
|
141
|
+
return this.client.connected;
|
|
142
|
+
}
|
|
143
|
+
async get(key) {
|
|
144
|
+
return this.client.get(this.key(key));
|
|
145
|
+
}
|
|
146
|
+
async set(key, value) {
|
|
147
|
+
await this.client.set(this.key(key), String(value));
|
|
148
|
+
}
|
|
149
|
+
async setEx(key, value, seconds) {
|
|
150
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
151
|
+
throw new Error("Redis setEx seconds must be a positive number");
|
|
152
|
+
await this.client.send("SET", [this.key(key), String(value), "EX", String(Math.floor(seconds))]);
|
|
153
|
+
}
|
|
154
|
+
async del(...keys) {
|
|
155
|
+
await this.client.del(...keys.map((k) => this.key(k)));
|
|
156
|
+
}
|
|
157
|
+
async exists(key) {
|
|
158
|
+
return this.client.exists(this.key(key));
|
|
159
|
+
}
|
|
160
|
+
async incr(key) {
|
|
161
|
+
return this.client.incr(this.key(key));
|
|
162
|
+
}
|
|
163
|
+
async decr(key) {
|
|
164
|
+
return this.client.decr(this.key(key));
|
|
165
|
+
}
|
|
166
|
+
async expire(key, seconds) {
|
|
167
|
+
await this.client.expire(this.key(key), seconds);
|
|
168
|
+
}
|
|
169
|
+
async ttl(key) {
|
|
170
|
+
return this.client.ttl(this.key(key));
|
|
171
|
+
}
|
|
172
|
+
async hget(key, field) {
|
|
173
|
+
return this.client.hget(this.key(key), field);
|
|
174
|
+
}
|
|
175
|
+
async hmset(key, fields) {
|
|
176
|
+
await this.client.hmset(this.key(key), fields);
|
|
177
|
+
}
|
|
178
|
+
async hmget(key, fields) {
|
|
179
|
+
return this.client.hmget(this.key(key), fields);
|
|
180
|
+
}
|
|
181
|
+
async hincrby(key, field, increment) {
|
|
182
|
+
return this.client.hincrby(this.key(key), field, increment);
|
|
183
|
+
}
|
|
184
|
+
async sadd(key, ...members) {
|
|
185
|
+
return this.client.sadd(this.key(key), ...members);
|
|
186
|
+
}
|
|
187
|
+
async srem(key, ...members) {
|
|
188
|
+
return this.client.srem(this.key(key), ...members);
|
|
189
|
+
}
|
|
190
|
+
async sismember(key, member) {
|
|
191
|
+
return this.client.sismember(this.key(key), member);
|
|
192
|
+
}
|
|
193
|
+
async smembers(key) {
|
|
194
|
+
return this.client.smembers(this.key(key));
|
|
195
|
+
}
|
|
196
|
+
async publish(channel, message) {
|
|
197
|
+
await this.client.publish(channel, message);
|
|
198
|
+
}
|
|
199
|
+
async subscribe(channel, callback) {
|
|
200
|
+
await this.client.subscribe(channel, callback);
|
|
201
|
+
}
|
|
202
|
+
async unsubscribe(channel) {
|
|
203
|
+
await this.client.unsubscribe(channel);
|
|
204
|
+
}
|
|
205
|
+
async send(command, args = []) {
|
|
206
|
+
return this.client.send(command, args);
|
|
207
|
+
}
|
|
208
|
+
async getJSON(key) {
|
|
209
|
+
const val = await this.get(key);
|
|
210
|
+
if (val === null)
|
|
211
|
+
return null;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(val);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
console.warn(`[@tekir/redis] Failed to parse JSON for key "${key}": ${e.message}`);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async setJSON(key, value, expireSeconds) {
|
|
220
|
+
const payload = JSON.stringify(value);
|
|
221
|
+
if (payload === undefined)
|
|
222
|
+
throw new Error(`Redis cannot serialize undefined for key "${key}"`);
|
|
223
|
+
if (expireSeconds && expireSeconds > 0) {
|
|
224
|
+
await this.client.send("SET", [this.key(key), payload, "EX", String(Math.floor(expireSeconds))]);
|
|
225
|
+
} else {
|
|
226
|
+
await this.client.set(this.key(key), payload);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async remember(key, seconds, callback) {
|
|
230
|
+
const cached = await this.getJSON(key);
|
|
231
|
+
if (cached !== null)
|
|
232
|
+
return cached;
|
|
233
|
+
const lockKey = this.key(`${key}:__lock`);
|
|
234
|
+
const lockToken = crypto.randomUUID();
|
|
235
|
+
const acquired = await this.client.send("SET", [lockKey, lockToken, "NX", "EX", "10"]);
|
|
236
|
+
if (acquired == null) {
|
|
237
|
+
for (let i = 0;i < 300; i++) {
|
|
238
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
239
|
+
const waited = await this.getJSON(key);
|
|
240
|
+
if (waited !== null)
|
|
241
|
+
return waited;
|
|
242
|
+
}
|
|
243
|
+
throw new Error(`Redis remember timed out waiting for lock on "${key}"`);
|
|
244
|
+
}
|
|
245
|
+
const renewScript = `
|
|
246
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
247
|
+
return redis.call('EXPIRE', KEYS[1], 10)
|
|
248
|
+
end
|
|
249
|
+
return 0
|
|
250
|
+
`;
|
|
251
|
+
const renewTimer = setInterval(() => {
|
|
252
|
+
this.client.send("EVAL", [renewScript, "1", lockKey, lockToken]).catch(() => {});
|
|
253
|
+
}, 3000);
|
|
254
|
+
renewTimer.unref?.();
|
|
255
|
+
try {
|
|
256
|
+
const fresh = await this.getJSON(key);
|
|
257
|
+
if (fresh !== null)
|
|
258
|
+
return fresh;
|
|
259
|
+
const value = await callback();
|
|
260
|
+
await this.setJSON(key, value, seconds);
|
|
261
|
+
return value;
|
|
262
|
+
} finally {
|
|
263
|
+
clearInterval(renewTimer);
|
|
264
|
+
if (acquired != null) {
|
|
265
|
+
const releaseScript = `
|
|
266
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
267
|
+
return redis.call('DEL', KEYS[1])
|
|
268
|
+
end
|
|
269
|
+
return 0
|
|
270
|
+
`;
|
|
271
|
+
await this.client.send("EVAL", [releaseScript, "1", lockKey, lockToken]).catch(() => {});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async clearPrefix() {
|
|
276
|
+
if (!this.prefix)
|
|
277
|
+
return 0;
|
|
278
|
+
const pattern = `${this.prefix}:*`;
|
|
279
|
+
let cursor = "0";
|
|
280
|
+
let deleted = 0;
|
|
281
|
+
do {
|
|
282
|
+
const [next, batch] = await this.client.send("SCAN", [cursor, "MATCH", pattern, "COUNT", "100"]);
|
|
283
|
+
cursor = next;
|
|
284
|
+
if (batch.length) {
|
|
285
|
+
await this.client.send("DEL", batch);
|
|
286
|
+
deleted += batch.length;
|
|
287
|
+
}
|
|
288
|
+
} while (cursor !== "0");
|
|
289
|
+
return deleted;
|
|
290
|
+
}
|
|
291
|
+
async flushdb() {
|
|
292
|
+
await this.send("FLUSHDB", []);
|
|
293
|
+
}
|
|
294
|
+
getClient() {
|
|
295
|
+
return this.client;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// src/manager.ts
|
|
299
|
+
class RedisManager {
|
|
300
|
+
_connections = new Map;
|
|
301
|
+
_defaultName;
|
|
302
|
+
_config;
|
|
303
|
+
constructor(config = {}) {
|
|
304
|
+
this._config = config;
|
|
305
|
+
this._defaultName = config.default || "default";
|
|
306
|
+
if (!config.connections) {
|
|
307
|
+
this._connections.set(this._defaultName, new Redis(config));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
connection(name) {
|
|
311
|
+
const connName = name || this._defaultName;
|
|
312
|
+
if (!this._connections.has(connName)) {
|
|
313
|
+
const connConfig = this._config.connections?.[connName];
|
|
314
|
+
if (!connConfig) {
|
|
315
|
+
throw new Error(`[@tekir/redis] Connection "${connName}" is not configured. ` + `Available: ${this.connectionNames.join(", ")}`);
|
|
316
|
+
}
|
|
317
|
+
this._connections.set(connName, new Redis(connConfig));
|
|
318
|
+
}
|
|
319
|
+
return this._connections.get(connName);
|
|
320
|
+
}
|
|
321
|
+
get connectionNames() {
|
|
322
|
+
if (this._config.connections)
|
|
323
|
+
return Object.keys(this._config.connections);
|
|
324
|
+
return [this._defaultName];
|
|
325
|
+
}
|
|
326
|
+
close(name) {
|
|
327
|
+
if (name) {
|
|
328
|
+
this._connections.get(name)?.close();
|
|
329
|
+
this._connections.delete(name);
|
|
330
|
+
} else {
|
|
331
|
+
for (const [, conn] of this._connections)
|
|
332
|
+
conn.close();
|
|
333
|
+
this._connections.clear();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
get(key) {
|
|
337
|
+
return this.connection().get(key);
|
|
338
|
+
}
|
|
339
|
+
set(key, value) {
|
|
340
|
+
return this.connection().set(key, value);
|
|
341
|
+
}
|
|
342
|
+
del(...keys) {
|
|
343
|
+
return this.connection().del(...keys);
|
|
344
|
+
}
|
|
345
|
+
exists(key) {
|
|
346
|
+
return this.connection().exists(key);
|
|
347
|
+
}
|
|
348
|
+
incr(key) {
|
|
349
|
+
return this.connection().incr(key);
|
|
350
|
+
}
|
|
351
|
+
decr(key) {
|
|
352
|
+
return this.connection().decr(key);
|
|
353
|
+
}
|
|
354
|
+
expire(key, seconds) {
|
|
355
|
+
return this.connection().expire(key, seconds);
|
|
356
|
+
}
|
|
357
|
+
ttl(key) {
|
|
358
|
+
return this.connection().ttl(key);
|
|
359
|
+
}
|
|
360
|
+
hget(key, field) {
|
|
361
|
+
return this.connection().hget(key, field);
|
|
362
|
+
}
|
|
363
|
+
hmset(key, fields) {
|
|
364
|
+
return this.connection().hmset(key, fields);
|
|
365
|
+
}
|
|
366
|
+
hmget(key, fields) {
|
|
367
|
+
return this.connection().hmget(key, fields);
|
|
368
|
+
}
|
|
369
|
+
hincrby(key, field, increment) {
|
|
370
|
+
return this.connection().hincrby(key, field, increment);
|
|
371
|
+
}
|
|
372
|
+
sadd(key, ...members) {
|
|
373
|
+
return this.connection().sadd(key, ...members);
|
|
374
|
+
}
|
|
375
|
+
srem(key, ...members) {
|
|
376
|
+
return this.connection().srem(key, ...members);
|
|
377
|
+
}
|
|
378
|
+
sismember(key, member) {
|
|
379
|
+
return this.connection().sismember(key, member);
|
|
380
|
+
}
|
|
381
|
+
smembers(key) {
|
|
382
|
+
return this.connection().smembers(key);
|
|
383
|
+
}
|
|
384
|
+
publish(channel, message) {
|
|
385
|
+
return this.connection().publish(channel, message);
|
|
386
|
+
}
|
|
387
|
+
subscribe(channel, callback) {
|
|
388
|
+
return this.connection().subscribe(channel, callback);
|
|
389
|
+
}
|
|
390
|
+
unsubscribe(channel) {
|
|
391
|
+
return this.connection().unsubscribe(channel);
|
|
392
|
+
}
|
|
393
|
+
send(command, args = []) {
|
|
394
|
+
return this.connection().send(command, args);
|
|
395
|
+
}
|
|
396
|
+
keyName(key) {
|
|
397
|
+
return this.connection().keyName(key);
|
|
398
|
+
}
|
|
399
|
+
getJSON(key) {
|
|
400
|
+
return this.connection().getJSON(key);
|
|
401
|
+
}
|
|
402
|
+
setEx(key, value, seconds) {
|
|
403
|
+
return this.connection().setEx(key, value, seconds);
|
|
404
|
+
}
|
|
405
|
+
setJSON(key, value, expireSeconds) {
|
|
406
|
+
return this.connection().setJSON(key, value, expireSeconds);
|
|
407
|
+
}
|
|
408
|
+
remember(key, seconds, callback) {
|
|
409
|
+
return this.connection().remember(key, seconds, callback);
|
|
410
|
+
}
|
|
411
|
+
clearPrefix() {
|
|
412
|
+
return this.connection().clearPrefix();
|
|
413
|
+
}
|
|
414
|
+
flushdb() {
|
|
415
|
+
return this.connection().flushdb();
|
|
416
|
+
}
|
|
417
|
+
get connected() {
|
|
418
|
+
return this.connection().connected;
|
|
419
|
+
}
|
|
420
|
+
getClient() {
|
|
421
|
+
return this.connection().getClient();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// src/provider.ts
|
|
425
|
+
class RedisProvider {
|
|
426
|
+
async register(app) {
|
|
427
|
+
const config = app.use("config");
|
|
428
|
+
if (!config("redis"))
|
|
429
|
+
return;
|
|
430
|
+
app.instance("redis", new RedisManager(config("redis")));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
export {
|
|
434
|
+
RedisProvider,
|
|
435
|
+
RedisManager,
|
|
436
|
+
Redis
|
|
437
|
+
};
|
package/dist/redis.d.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
import type { RedisConnectionConfig } from './types';
|
|
2
|
+
export declare class NodeRedisClient {
|
|
3
|
+
private client;
|
|
4
|
+
private messageHandlers;
|
|
5
|
+
constructor(url: string, options?: Record<string, any>);
|
|
6
|
+
connect(): Promise<void>;
|
|
7
|
+
close(): void;
|
|
8
|
+
get connected(): boolean;
|
|
9
|
+
get(key: string): any;
|
|
10
|
+
set(key: string, value: string, ...args: Array<string | number>): any;
|
|
11
|
+
del(...keys: string[]): any;
|
|
12
|
+
exists(key: string): Promise<boolean>;
|
|
13
|
+
incr(key: string): any;
|
|
14
|
+
decr(key: string): any;
|
|
15
|
+
expire(key: string, seconds: number): any;
|
|
16
|
+
ttl(key: string): any;
|
|
17
|
+
hget(key: string, field: string): any;
|
|
18
|
+
hmset(key: string, fields: string[]): any;
|
|
19
|
+
hmget(key: string, fields: string[]): any;
|
|
20
|
+
hincrby(key: string, field: string, increment: number): any;
|
|
21
|
+
sadd(key: string, ...members: string[]): any;
|
|
22
|
+
srem(key: string, ...members: string[]): any;
|
|
23
|
+
sismember(key: string, member: string): Promise<boolean>;
|
|
24
|
+
smembers(key: string): any;
|
|
25
|
+
publish(channel: string, message: string): any;
|
|
26
|
+
subscribe(channel: string, callback: (message: string, channel: string) => void): Promise<void>;
|
|
27
|
+
unsubscribe(channel?: string): Promise<void>;
|
|
28
|
+
send(command: string, args?: string[]): any;
|
|
29
|
+
}
|
|
2
30
|
/**
|
|
3
31
|
* Redis client wrapper around Bun's native RedisClient.
|
|
4
32
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tekir/redis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Redis connection and client management",
|
|
5
5
|
"author": "dev@tekir.io",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@tekir/core": "^0.1.0"
|
|
42
|
+
"@tekir/core": "^0.1.0",
|
|
43
|
+
"ioredis": "^5.8.2"
|
|
43
44
|
},
|
|
44
45
|
"scripts": {
|
|
45
|
-
"build": "
|
|
46
|
+
"build": "bun ../../scripts/build-package.ts",
|
|
46
47
|
"prepublishOnly": "bun run build"
|
|
47
48
|
},
|
|
48
49
|
"exports": {
|
package/src/redis.ts
CHANGED
|
@@ -1,4 +1,65 @@
|
|
|
1
1
|
import type { RedisConnectionConfig } from './types'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
|
|
4
|
+
const nodeRequire = createRequire(import.meta.url)
|
|
5
|
+
|
|
6
|
+
export class NodeRedisClient {
|
|
7
|
+
private client: any
|
|
8
|
+
private messageHandlers = new Map<string, (message: string, channel: string) => void>()
|
|
9
|
+
|
|
10
|
+
constructor(url: string, options: Record<string, any> = {}) {
|
|
11
|
+
const imported = nodeRequire('ioredis')
|
|
12
|
+
const IORedis = imported.default || imported
|
|
13
|
+
this.client = new IORedis(url, {
|
|
14
|
+
lazyConnect: true,
|
|
15
|
+
connectTimeout: options.connectionTimeout,
|
|
16
|
+
enableAutoPipelining: options.enableAutoPipelining,
|
|
17
|
+
maxRetriesPerRequest: options.maxRetries,
|
|
18
|
+
retryStrategy: options.autoReconnect === false ? null : undefined,
|
|
19
|
+
tls: options.tls === true ? {} : options.tls || undefined,
|
|
20
|
+
})
|
|
21
|
+
this.client.on('message', (channel: string, message: string) => {
|
|
22
|
+
this.messageHandlers.get(channel)?.(message, channel)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async connect(): Promise<void> {
|
|
27
|
+
if (this.client.status === 'wait') await this.client.connect()
|
|
28
|
+
else if (this.client.status !== 'ready') await new Promise<void>((resolve, reject) => {
|
|
29
|
+
this.client.once('ready', resolve)
|
|
30
|
+
this.client.once('error', reject)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
close(): void { this.client.disconnect() }
|
|
34
|
+
get connected(): boolean { return this.client.status === 'ready' }
|
|
35
|
+
get(key: string) { return this.client.get(key) }
|
|
36
|
+
set(key: string, value: string, ...args: Array<string | number>) { return this.client.set(key, value, ...args) }
|
|
37
|
+
del(...keys: string[]) { return this.client.del(...keys) }
|
|
38
|
+
async exists(key: string) { return (await this.client.exists(key)) > 0 }
|
|
39
|
+
incr(key: string) { return this.client.incr(key) }
|
|
40
|
+
decr(key: string) { return this.client.decr(key) }
|
|
41
|
+
expire(key: string, seconds: number) { return this.client.expire(key, seconds) }
|
|
42
|
+
ttl(key: string) { return this.client.ttl(key) }
|
|
43
|
+
hget(key: string, field: string) { return this.client.hget(key, field) }
|
|
44
|
+
hmset(key: string, fields: string[]) { return this.client.hmset(key, ...fields) }
|
|
45
|
+
hmget(key: string, fields: string[]) { return this.client.hmget(key, ...fields) }
|
|
46
|
+
hincrby(key: string, field: string, increment: number) { return this.client.hincrby(key, field, increment) }
|
|
47
|
+
sadd(key: string, ...members: string[]) { return this.client.sadd(key, ...members) }
|
|
48
|
+
srem(key: string, ...members: string[]) { return this.client.srem(key, ...members) }
|
|
49
|
+
async sismember(key: string, member: string) { return (await this.client.sismember(key, member)) > 0 }
|
|
50
|
+
smembers(key: string) { return this.client.smembers(key) }
|
|
51
|
+
publish(channel: string, message: string) { return this.client.publish(channel, message) }
|
|
52
|
+
async subscribe(channel: string, callback: (message: string, channel: string) => void) {
|
|
53
|
+
this.messageHandlers.set(channel, callback)
|
|
54
|
+
await this.client.subscribe(channel)
|
|
55
|
+
}
|
|
56
|
+
async unsubscribe(channel?: string) {
|
|
57
|
+
if (channel) this.messageHandlers.delete(channel)
|
|
58
|
+
else this.messageHandlers.clear()
|
|
59
|
+
await (channel ? this.client.unsubscribe(channel) : this.client.unsubscribe())
|
|
60
|
+
}
|
|
61
|
+
send(command: string, args: string[] = []) { return this.client.call(command, ...args) }
|
|
62
|
+
}
|
|
2
63
|
|
|
3
64
|
/**
|
|
4
65
|
* Redis client wrapper around Bun's native RedisClient.
|
|
@@ -25,7 +86,9 @@ export class Redis {
|
|
|
25
86
|
this.config = config
|
|
26
87
|
this.prefix = (config.prefix || '').replace(/:+$/, '')
|
|
27
88
|
|
|
28
|
-
const
|
|
89
|
+
const RedisClient = process.versions.bun
|
|
90
|
+
? (globalThis as any).Bun.RedisClient
|
|
91
|
+
: NodeRedisClient
|
|
29
92
|
const url = config.url || process.env.REDIS_URL || 'redis://localhost:6379'
|
|
30
93
|
|
|
31
94
|
// Encourage TLS in production: a plaintext redis:// connection exposes
|
package/dist/manager.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { Redis } from './redis';
|
|
2
|
-
/**
|
|
3
|
-
* Manages multiple named Redis connections with lazy initialization.
|
|
4
|
-
*
|
|
5
|
-
* Provides proxy methods that delegate to the default connection for convenience,
|
|
6
|
-
* while also allowing access to any named connection via {@link RedisManager.connection}.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* const manager = new RedisManager({
|
|
11
|
-
* default: 'cache',
|
|
12
|
-
* connections: {
|
|
13
|
-
* cache: { url: 'redis://localhost:6379/0' },
|
|
14
|
-
* session: { url: 'redis://localhost:6379/1' },
|
|
15
|
-
* },
|
|
16
|
-
* })
|
|
17
|
-
* await manager.set('key', 'value') // uses 'cache'
|
|
18
|
-
* await manager.connection('session').set('k', 'v') // uses 'session'
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
export class RedisManager {
|
|
22
|
-
_connections = new Map();
|
|
23
|
-
_defaultName;
|
|
24
|
-
_config;
|
|
25
|
-
/**
|
|
26
|
-
* Create a new RedisManager.
|
|
27
|
-
*
|
|
28
|
-
* @param config - Redis configuration with optional named connections.
|
|
29
|
-
*/
|
|
30
|
-
constructor(config = {}) {
|
|
31
|
-
this._config = config;
|
|
32
|
-
this._defaultName = config.default || 'default';
|
|
33
|
-
// If no connections map, treat the whole config as a single connection
|
|
34
|
-
if (!config.connections) {
|
|
35
|
-
this._connections.set(this._defaultName, new Redis(config));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Get a named Redis connection. The connection is lazy-initialized on first access.
|
|
40
|
-
*
|
|
41
|
-
* @param name - The connection name. Defaults to the configured default connection.
|
|
42
|
-
* @returns The {@link Redis} instance for the named connection.
|
|
43
|
-
* @throws If the named connection is not configured.
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```ts
|
|
47
|
-
* const cache = manager.connection('cache')
|
|
48
|
-
* await cache.get('key')
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
connection(name) {
|
|
52
|
-
const connName = name || this._defaultName;
|
|
53
|
-
if (!this._connections.has(connName)) {
|
|
54
|
-
const connConfig = this._config.connections?.[connName];
|
|
55
|
-
if (!connConfig) {
|
|
56
|
-
throw new Error(`[@tekir/redis] Connection "${connName}" is not configured. ` +
|
|
57
|
-
`Available: ${this.connectionNames.join(', ')}`);
|
|
58
|
-
}
|
|
59
|
-
this._connections.set(connName, new Redis(connConfig));
|
|
60
|
-
}
|
|
61
|
-
return this._connections.get(connName);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* List all configured connection names.
|
|
65
|
-
*
|
|
66
|
-
* @returns An array of connection name strings.
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* ```ts
|
|
70
|
-
* manager.connectionNames // ['cache', 'session']
|
|
71
|
-
* ```
|
|
72
|
-
*/
|
|
73
|
-
get connectionNames() {
|
|
74
|
-
if (this._config.connections)
|
|
75
|
-
return Object.keys(this._config.connections);
|
|
76
|
-
return [this._defaultName];
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Close a specific named connection, or all connections if no name is given.
|
|
80
|
-
*
|
|
81
|
-
* @param name - The connection name to close. Omit to close all connections.
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* ```ts
|
|
85
|
-
* manager.close('session') // close one connection
|
|
86
|
-
* manager.close() // close all connections
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
close(name) {
|
|
90
|
-
if (name) {
|
|
91
|
-
this._connections.get(name)?.close();
|
|
92
|
-
this._connections.delete(name);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
for (const [, conn] of this._connections)
|
|
96
|
-
conn.close();
|
|
97
|
-
this._connections.clear();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// ─── Proxy methods delegating to the default connection ───
|
|
101
|
-
/** @see {@link Redis.get} */
|
|
102
|
-
get(key) { return this.connection().get(key); }
|
|
103
|
-
/** @see {@link Redis.set} */
|
|
104
|
-
set(key, value) { return this.connection().set(key, value); }
|
|
105
|
-
/** @see {@link Redis.del} */
|
|
106
|
-
del(...keys) { return this.connection().del(...keys); }
|
|
107
|
-
/** @see {@link Redis.exists} */
|
|
108
|
-
exists(key) { return this.connection().exists(key); }
|
|
109
|
-
/** @see {@link Redis.incr} */
|
|
110
|
-
incr(key) { return this.connection().incr(key); }
|
|
111
|
-
/** @see {@link Redis.decr} */
|
|
112
|
-
decr(key) { return this.connection().decr(key); }
|
|
113
|
-
/** @see {@link Redis.expire} */
|
|
114
|
-
expire(key, seconds) { return this.connection().expire(key, seconds); }
|
|
115
|
-
/** @see {@link Redis.ttl} */
|
|
116
|
-
ttl(key) { return this.connection().ttl(key); }
|
|
117
|
-
/** @see {@link Redis.hget} */
|
|
118
|
-
hget(key, field) { return this.connection().hget(key, field); }
|
|
119
|
-
/** @see {@link Redis.hmset} */
|
|
120
|
-
hmset(key, fields) { return this.connection().hmset(key, fields); }
|
|
121
|
-
/** @see {@link Redis.hmget} */
|
|
122
|
-
hmget(key, fields) { return this.connection().hmget(key, fields); }
|
|
123
|
-
/** @see {@link Redis.hincrby} */
|
|
124
|
-
hincrby(key, field, increment) { return this.connection().hincrby(key, field, increment); }
|
|
125
|
-
/** @see {@link Redis.sadd} */
|
|
126
|
-
sadd(key, ...members) { return this.connection().sadd(key, ...members); }
|
|
127
|
-
/** @see {@link Redis.srem} */
|
|
128
|
-
srem(key, ...members) { return this.connection().srem(key, ...members); }
|
|
129
|
-
/** @see {@link Redis.sismember} */
|
|
130
|
-
sismember(key, member) { return this.connection().sismember(key, member); }
|
|
131
|
-
/** @see {@link Redis.smembers} */
|
|
132
|
-
smembers(key) { return this.connection().smembers(key); }
|
|
133
|
-
/** @see {@link Redis.publish} */
|
|
134
|
-
publish(channel, message) { return this.connection().publish(channel, message); }
|
|
135
|
-
/** @see {@link Redis.subscribe} */
|
|
136
|
-
subscribe(channel, callback) { return this.connection().subscribe(channel, callback); }
|
|
137
|
-
/** @see {@link Redis.unsubscribe} */
|
|
138
|
-
unsubscribe(channel) { return this.connection().unsubscribe(channel); }
|
|
139
|
-
/** @see {@link Redis.send} */
|
|
140
|
-
send(command, args = []) { return this.connection().send(command, args); }
|
|
141
|
-
/** @see {@link Redis.keyName} */
|
|
142
|
-
keyName(key) { return this.connection().keyName(key); }
|
|
143
|
-
/** @see {@link Redis.getJSON} */
|
|
144
|
-
getJSON(key) { return this.connection().getJSON(key); }
|
|
145
|
-
/** @see {@link Redis.setEx} */
|
|
146
|
-
setEx(key, value, seconds) { return this.connection().setEx(key, value, seconds); }
|
|
147
|
-
/** @see {@link Redis.setJSON} */
|
|
148
|
-
setJSON(key, value, expireSeconds) { return this.connection().setJSON(key, value, expireSeconds); }
|
|
149
|
-
/** @see {@link Redis.remember} */
|
|
150
|
-
remember(key, seconds, callback) { return this.connection().remember(key, seconds, callback); }
|
|
151
|
-
/** @see {@link Redis.clearPrefix} */
|
|
152
|
-
clearPrefix() { return this.connection().clearPrefix(); }
|
|
153
|
-
/** @see {@link Redis.flushdb} */
|
|
154
|
-
flushdb() { return this.connection().flushdb(); }
|
|
155
|
-
/** @see {@link Redis.connected} */
|
|
156
|
-
get connected() { return this.connection().connected; }
|
|
157
|
-
/** @see {@link Redis.getClient} */
|
|
158
|
-
getClient() { return this.connection().getClient(); }
|
|
159
|
-
}
|
package/dist/provider.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { RedisManager } from './manager';
|
|
2
|
-
/**
|
|
3
|
-
* Service provider that reads the `redis` configuration and registers
|
|
4
|
-
* a {@link RedisManager} instance in the application container.
|
|
5
|
-
*/
|
|
6
|
-
export class RedisProvider {
|
|
7
|
-
/**
|
|
8
|
-
* Register the Redis manager into the application container.
|
|
9
|
-
*
|
|
10
|
-
* @param app - The Tekir application instance.
|
|
11
|
-
* @returns A promise that resolves once registration is complete.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* // In your providers list:
|
|
16
|
-
* app.register(new RedisProvider())
|
|
17
|
-
* ```
|
|
18
|
-
*/
|
|
19
|
-
async register(app) {
|
|
20
|
-
const config = app.use('config');
|
|
21
|
-
if (!config('redis'))
|
|
22
|
-
return;
|
|
23
|
-
app.instance('redis', new RedisManager(config('redis')));
|
|
24
|
-
}
|
|
25
|
-
}
|
package/dist/redis.js
DELETED
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Redis client wrapper around Bun's native RedisClient.
|
|
3
|
-
*
|
|
4
|
-
* Provides a high-level API for string, hash, set, and pub/sub operations,
|
|
5
|
-
* plus JSON helpers and a cache-aside `remember` method.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* const redis = new Redis({ url: 'redis://localhost:6379', prefix: 'app:' })
|
|
10
|
-
* await redis.set('greeting', 'hello')
|
|
11
|
-
* const val = await redis.get('greeting') // 'hello'
|
|
12
|
-
* ```
|
|
13
|
-
*/
|
|
14
|
-
export class Redis {
|
|
15
|
-
client;
|
|
16
|
-
prefix;
|
|
17
|
-
config;
|
|
18
|
-
/**
|
|
19
|
-
* @param config - Redis connection configuration.
|
|
20
|
-
*/
|
|
21
|
-
constructor(config = {}) {
|
|
22
|
-
this.config = config;
|
|
23
|
-
this.prefix = (config.prefix || '').replace(/:+$/, '');
|
|
24
|
-
const { RedisClient } = Bun;
|
|
25
|
-
const url = config.url || process.env.REDIS_URL || 'redis://localhost:6379';
|
|
26
|
-
// Encourage TLS in production: a plaintext redis:// connection exposes
|
|
27
|
-
// credentials and data to network observers. Warn once instead of throwing
|
|
28
|
-
// to stay backward compatible with local/dev setups.
|
|
29
|
-
const isTls = config.tls != null || url.startsWith('rediss://');
|
|
30
|
-
if (!isTls && process.env.NODE_ENV === 'production') {
|
|
31
|
-
console.warn(`[@tekir/redis] Connecting to ${Redis.maskUrl(url)} without TLS in production. ` +
|
|
32
|
-
`Use a rediss:// URL or set tls to encrypt credentials and data in transit.`);
|
|
33
|
-
}
|
|
34
|
-
this.client = new RedisClient(url, {
|
|
35
|
-
connectionTimeout: config.connectionTimeout,
|
|
36
|
-
idleTimeout: config.idleTimeout,
|
|
37
|
-
autoReconnect: config.autoReconnect ?? true,
|
|
38
|
-
maxRetries: config.maxRetries ?? 10,
|
|
39
|
-
enableAutoPipelining: config.enableAutoPipelining ?? true,
|
|
40
|
-
tls: config.tls,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
key(k) {
|
|
44
|
-
return this.prefix ? `${this.prefix}:${k}` : k;
|
|
45
|
-
}
|
|
46
|
-
/** Resolve the actual Redis key after applying this connection's namespace. */
|
|
47
|
-
keyName(key) { return this.key(key); }
|
|
48
|
-
/**
|
|
49
|
-
* Strip any credentials embedded in a Redis URL so it is safe to log.
|
|
50
|
-
*
|
|
51
|
-
* @param url - The connection URL, possibly containing `user:pass@host`.
|
|
52
|
-
* @returns The URL with the userinfo component masked.
|
|
53
|
-
*/
|
|
54
|
-
static maskUrl(url) {
|
|
55
|
-
return url.replace(/(\w+:\/\/)([^@/]+)@/, '$1***@');
|
|
56
|
-
}
|
|
57
|
-
// ─── Connection ────────────────────────────────────────
|
|
58
|
-
/**
|
|
59
|
-
* Open the connection to the Redis server.
|
|
60
|
-
*
|
|
61
|
-
* @returns A promise that resolves once the connection is established.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```ts
|
|
65
|
-
* await redis.connect()
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
async connect() { await this.client.connect(); }
|
|
69
|
-
/**
|
|
70
|
-
* Close the connection to the Redis server.
|
|
71
|
-
*/
|
|
72
|
-
close() { this.client.close(); }
|
|
73
|
-
/**
|
|
74
|
-
* Whether the client is currently connected to Redis.
|
|
75
|
-
*
|
|
76
|
-
* @returns `true` if the connection is active.
|
|
77
|
-
*/
|
|
78
|
-
get connected() { return this.client.connected; }
|
|
79
|
-
// ─── String operations ────────────────────────────────
|
|
80
|
-
/**
|
|
81
|
-
* Get the value of a key.
|
|
82
|
-
*
|
|
83
|
-
* @param key - The key to retrieve.
|
|
84
|
-
* @returns The string value, or `null` if the key does not exist.
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```ts
|
|
88
|
-
* const val = await redis.get('name') // 'Alice' | null
|
|
89
|
-
* ```
|
|
90
|
-
*/
|
|
91
|
-
async get(key) { return this.client.get(this.key(key)); }
|
|
92
|
-
/**
|
|
93
|
-
* Set the value of a key.
|
|
94
|
-
*
|
|
95
|
-
* @param key - The key to set.
|
|
96
|
-
* @param value - The string or numeric value to store.
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* ```ts
|
|
100
|
-
* await redis.set('counter', 42)
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
async set(key, value) { await this.client.set(this.key(key), String(value)); }
|
|
104
|
-
/** Atomically set a value and its expiration time. */
|
|
105
|
-
async setEx(key, value, seconds) {
|
|
106
|
-
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
107
|
-
throw new Error('Redis setEx seconds must be a positive number');
|
|
108
|
-
await this.client.send('SET', [this.key(key), String(value), 'EX', String(Math.floor(seconds))]);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Delete one or more keys.
|
|
112
|
-
*
|
|
113
|
-
* @param keys - The keys to delete.
|
|
114
|
-
*
|
|
115
|
-
* @example
|
|
116
|
-
* ```ts
|
|
117
|
-
* await redis.del('key1', 'key2')
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
async del(...keys) { await this.client.del(...keys.map(k => this.key(k))); }
|
|
121
|
-
/**
|
|
122
|
-
* Check whether a key exists.
|
|
123
|
-
*
|
|
124
|
-
* @param key - The key to check.
|
|
125
|
-
* @returns `true` if the key exists.
|
|
126
|
-
*/
|
|
127
|
-
async exists(key) { return this.client.exists(this.key(key)); }
|
|
128
|
-
/**
|
|
129
|
-
* Increment the integer value of a key by one.
|
|
130
|
-
*
|
|
131
|
-
* @param key - The key to increment.
|
|
132
|
-
* @returns The new value after incrementing.
|
|
133
|
-
*/
|
|
134
|
-
async incr(key) { return this.client.incr(this.key(key)); }
|
|
135
|
-
/**
|
|
136
|
-
* Decrement the integer value of a key by one.
|
|
137
|
-
*
|
|
138
|
-
* @param key - The key to decrement.
|
|
139
|
-
* @returns The new value after decrementing.
|
|
140
|
-
*/
|
|
141
|
-
async decr(key) { return this.client.decr(this.key(key)); }
|
|
142
|
-
/**
|
|
143
|
-
* Set a timeout on a key (in seconds).
|
|
144
|
-
*
|
|
145
|
-
* @param key - The key to set the expiry on.
|
|
146
|
-
* @param seconds - Time-to-live in seconds.
|
|
147
|
-
*/
|
|
148
|
-
async expire(key, seconds) { await this.client.expire(this.key(key), seconds); }
|
|
149
|
-
/**
|
|
150
|
-
* Get the remaining time-to-live of a key in seconds.
|
|
151
|
-
*
|
|
152
|
-
* @param key - The key to query.
|
|
153
|
-
* @returns Remaining TTL in seconds, `-1` if no expiry is set, `-2` if the key does not exist.
|
|
154
|
-
*/
|
|
155
|
-
async ttl(key) { return this.client.ttl(this.key(key)); }
|
|
156
|
-
// ─── Hash operations ──────────────────────────────────
|
|
157
|
-
/**
|
|
158
|
-
* Get the value of a single field in a hash.
|
|
159
|
-
*
|
|
160
|
-
* @param key - The hash key.
|
|
161
|
-
* @param field - The field name within the hash.
|
|
162
|
-
* @returns The field value, or `null` if the field or key does not exist.
|
|
163
|
-
*/
|
|
164
|
-
async hget(key, field) { return this.client.hget(this.key(key), field); }
|
|
165
|
-
/**
|
|
166
|
-
* Set multiple field-value pairs in a hash.
|
|
167
|
-
*
|
|
168
|
-
* @param key - The hash key.
|
|
169
|
-
* @param fields - An array of alternating field names and values (e.g. `['f1', 'v1', 'f2', 'v2']`).
|
|
170
|
-
*/
|
|
171
|
-
async hmset(key, fields) { await this.client.hmset(this.key(key), fields); }
|
|
172
|
-
/**
|
|
173
|
-
* Get the values of multiple fields in a hash.
|
|
174
|
-
*
|
|
175
|
-
* @param key - The hash key.
|
|
176
|
-
* @param fields - An array of field names to retrieve.
|
|
177
|
-
* @returns An array of values corresponding to the requested fields (`null` for missing fields).
|
|
178
|
-
*/
|
|
179
|
-
async hmget(key, fields) { return this.client.hmget(this.key(key), fields); }
|
|
180
|
-
/**
|
|
181
|
-
* Increment a numeric field in a hash by a given amount.
|
|
182
|
-
*
|
|
183
|
-
* @param key - The hash key.
|
|
184
|
-
* @param field - The field name to increment.
|
|
185
|
-
* @param increment - The integer amount to add.
|
|
186
|
-
* @returns The new value of the field after incrementing.
|
|
187
|
-
*/
|
|
188
|
-
async hincrby(key, field, increment) { return this.client.hincrby(this.key(key), field, increment); }
|
|
189
|
-
// ─── Set operations ───────────────────────────────────
|
|
190
|
-
/**
|
|
191
|
-
* Add one or more members to a set.
|
|
192
|
-
*
|
|
193
|
-
* @param key - The set key.
|
|
194
|
-
* @param members - The members to add.
|
|
195
|
-
* @returns The number of members that were added (excluding already-present members).
|
|
196
|
-
*/
|
|
197
|
-
async sadd(key, ...members) { return this.client.sadd(this.key(key), ...members); }
|
|
198
|
-
/**
|
|
199
|
-
* Remove one or more members from a set.
|
|
200
|
-
*
|
|
201
|
-
* @param key - The set key.
|
|
202
|
-
* @param members - The members to remove.
|
|
203
|
-
* @returns The number of members that were removed.
|
|
204
|
-
*/
|
|
205
|
-
async srem(key, ...members) { return this.client.srem(this.key(key), ...members); }
|
|
206
|
-
/**
|
|
207
|
-
* Check whether a value is a member of a set.
|
|
208
|
-
*
|
|
209
|
-
* @param key - The set key.
|
|
210
|
-
* @param member - The value to check for.
|
|
211
|
-
* @returns `true` if the member exists in the set.
|
|
212
|
-
*/
|
|
213
|
-
async sismember(key, member) { return this.client.sismember(this.key(key), member); }
|
|
214
|
-
/**
|
|
215
|
-
* Get all members of a set.
|
|
216
|
-
*
|
|
217
|
-
* @param key - The set key.
|
|
218
|
-
* @returns An array of all members in the set.
|
|
219
|
-
*/
|
|
220
|
-
async smembers(key) { return this.client.smembers(this.key(key)); }
|
|
221
|
-
// ─── Pub/Sub ──────────────────────────────────────────
|
|
222
|
-
/**
|
|
223
|
-
* Publish a message to a channel.
|
|
224
|
-
*
|
|
225
|
-
* @param channel - The channel name.
|
|
226
|
-
* @param message - The message string to publish.
|
|
227
|
-
*/
|
|
228
|
-
async publish(channel, message) { await this.client.publish(channel, message); }
|
|
229
|
-
/**
|
|
230
|
-
* Subscribe to a channel and receive messages via a callback.
|
|
231
|
-
*
|
|
232
|
-
* The message passed to the callback is the raw string received from Redis and
|
|
233
|
-
* is NOT deserialized by this wrapper. Treat it as untrusted input: validate
|
|
234
|
-
* it and never `eval` it. If you `JSON.parse` it, wrap the parse in a
|
|
235
|
-
* try/catch to guard against poisoned payloads.
|
|
236
|
-
*
|
|
237
|
-
* @param channel - The channel name to subscribe to.
|
|
238
|
-
* @param callback - Invoked for each message received on the channel.
|
|
239
|
-
*
|
|
240
|
-
* @example
|
|
241
|
-
* ```ts
|
|
242
|
-
* await redis.subscribe('events', (msg, ch) => {
|
|
243
|
-
* console.log(`Received on ${ch}: ${msg}`)
|
|
244
|
-
* })
|
|
245
|
-
* ```
|
|
246
|
-
*/
|
|
247
|
-
async subscribe(channel, callback) {
|
|
248
|
-
await this.client.subscribe(channel, callback);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Unsubscribe from a channel, or from all channels if none is specified.
|
|
252
|
-
*
|
|
253
|
-
* @param channel - The channel to unsubscribe from. Omit to unsubscribe from all.
|
|
254
|
-
*/
|
|
255
|
-
async unsubscribe(channel) { await this.client.unsubscribe(channel); }
|
|
256
|
-
// ─── Raw command ──────────────────────────────────────
|
|
257
|
-
/**
|
|
258
|
-
* Send a raw Redis command. ADVANCED / INTERNAL escape hatch.
|
|
259
|
-
*
|
|
260
|
-
* This bypasses the key prefix and every higher-level safeguard. The `command`
|
|
261
|
-
* and `args` are passed straight to Redis, so they MUST be trusted, statically
|
|
262
|
-
* known values. Never build a command name or its arguments from user input:
|
|
263
|
-
* doing so allows execution of dangerous commands (`FLUSHALL`, `CONFIG`,
|
|
264
|
-
* `EVAL`, `KEYS *`, ...) and lets callers escape the prefix namespace.
|
|
265
|
-
*
|
|
266
|
-
* @param command - A trusted, statically-known Redis command (e.g. `'PING'`, `'INFO'`).
|
|
267
|
-
* @param args - Arguments for the command. Must not contain untrusted input.
|
|
268
|
-
* @returns The raw response from Redis.
|
|
269
|
-
*
|
|
270
|
-
* @example
|
|
271
|
-
* ```ts
|
|
272
|
-
* const pong = await redis.send('PING') // 'PONG'
|
|
273
|
-
* ```
|
|
274
|
-
*/
|
|
275
|
-
async send(command, args = []) { return this.client.send(command, args); }
|
|
276
|
-
// ─── JSON helpers ─────────────────────────────────────
|
|
277
|
-
/**
|
|
278
|
-
* Get a value from Redis and parse it as JSON.
|
|
279
|
-
*
|
|
280
|
-
* @param key - The key to retrieve.
|
|
281
|
-
* @returns The parsed object, or `null` if the key does not exist or parsing fails.
|
|
282
|
-
*
|
|
283
|
-
* @example
|
|
284
|
-
* ```ts
|
|
285
|
-
* const user = await redis.getJSON<{ name: string }>('user:1')
|
|
286
|
-
* ```
|
|
287
|
-
*/
|
|
288
|
-
async getJSON(key) {
|
|
289
|
-
const val = await this.get(key);
|
|
290
|
-
if (val === null)
|
|
291
|
-
return null;
|
|
292
|
-
try {
|
|
293
|
-
return JSON.parse(val);
|
|
294
|
-
}
|
|
295
|
-
catch (e) {
|
|
296
|
-
// Surface corrupt/poisoned payloads instead of silently masking them as a
|
|
297
|
-
// cache miss. Returning null still preserves the previous behaviour for
|
|
298
|
-
// callers, but the warning makes the problem diagnosable.
|
|
299
|
-
console.warn(`[@tekir/redis] Failed to parse JSON for key "${key}": ${e.message}`);
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Serialize a value as JSON and store it in Redis, with an optional TTL.
|
|
305
|
-
*
|
|
306
|
-
* @param key - The key to store the value under.
|
|
307
|
-
* @param value - The value to JSON-serialize and store.
|
|
308
|
-
* @param expireSeconds - Optional time-to-live in seconds.
|
|
309
|
-
*
|
|
310
|
-
* @example
|
|
311
|
-
* ```ts
|
|
312
|
-
* await redis.setJSON('user:1', { name: 'Alice' }, 3600)
|
|
313
|
-
* ```
|
|
314
|
-
*/
|
|
315
|
-
async setJSON(key, value, expireSeconds) {
|
|
316
|
-
const payload = JSON.stringify(value);
|
|
317
|
-
if (payload === undefined)
|
|
318
|
-
throw new Error(`Redis cannot serialize undefined for key "${key}"`);
|
|
319
|
-
if (expireSeconds && expireSeconds > 0) {
|
|
320
|
-
// Atomic SET ... EX so a crash between writing the value and setting the
|
|
321
|
-
// TTL can never leave a permanent (TTL-less) key behind.
|
|
322
|
-
await this.client.send('SET', [this.key(key), payload, 'EX', String(Math.floor(expireSeconds))]);
|
|
323
|
-
}
|
|
324
|
-
else {
|
|
325
|
-
await this.client.set(this.key(key), payload);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Cache-aside helper: return the cached value if it exists, otherwise execute
|
|
330
|
-
* the callback, store the result in Redis with a TTL, and return it.
|
|
331
|
-
*
|
|
332
|
-
* @param key - The cache key.
|
|
333
|
-
* @param seconds - Time-to-live in seconds for the cached value.
|
|
334
|
-
* @param callback - Async function invoked to compute the value on a cache miss.
|
|
335
|
-
* @returns The cached or freshly computed value.
|
|
336
|
-
*
|
|
337
|
-
* On a cache miss this acquires a short-lived `SET NX` lock so that, under
|
|
338
|
-
* concurrent misses, only one caller runs `callback()` while the others wait
|
|
339
|
-
* for the freshly-populated value. This protects the backend from a stampede
|
|
340
|
-
* (thundering herd) when a hot key expires.
|
|
341
|
-
*
|
|
342
|
-
* @example
|
|
343
|
-
* ```ts
|
|
344
|
-
* const users = await redis.remember('all-users', 60, async () => {
|
|
345
|
-
* return db.query('SELECT * FROM users')
|
|
346
|
-
* })
|
|
347
|
-
* ```
|
|
348
|
-
*/
|
|
349
|
-
async remember(key, seconds, callback) {
|
|
350
|
-
const cached = await this.getJSON(key);
|
|
351
|
-
if (cached !== null)
|
|
352
|
-
return cached;
|
|
353
|
-
const lockKey = this.key(`${key}:__lock`);
|
|
354
|
-
const lockToken = crypto.randomUUID();
|
|
355
|
-
// Try to become the single flight that computes the value. SET NX EX gives
|
|
356
|
-
// a self-expiring lock so a crashed holder cannot deadlock other callers.
|
|
357
|
-
const acquired = await this.client.send('SET', [lockKey, lockToken, 'NX', 'EX', '10']);
|
|
358
|
-
if (acquired == null) {
|
|
359
|
-
// Another caller is computing it. Wait for the populated value, but do
|
|
360
|
-
// not run the callback without owning the lock: doing so would turn slow
|
|
361
|
-
// cache fills back into a stampede. A bounded timeout keeps callers from
|
|
362
|
-
// hanging forever if Redis itself is unhealthy.
|
|
363
|
-
for (let i = 0; i < 300; i++) {
|
|
364
|
-
await new Promise(r => setTimeout(r, 100));
|
|
365
|
-
const waited = await this.getJSON(key);
|
|
366
|
-
if (waited !== null)
|
|
367
|
-
return waited;
|
|
368
|
-
}
|
|
369
|
-
throw new Error(`Redis remember timed out waiting for lock on "${key}"`);
|
|
370
|
-
}
|
|
371
|
-
const renewScript = `
|
|
372
|
-
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
373
|
-
return redis.call('EXPIRE', KEYS[1], 10)
|
|
374
|
-
end
|
|
375
|
-
return 0
|
|
376
|
-
`;
|
|
377
|
-
const renewTimer = setInterval(() => {
|
|
378
|
-
void this.client.send('EVAL', [renewScript, '1', lockKey, lockToken]).catch(() => { });
|
|
379
|
-
}, 3000);
|
|
380
|
-
renewTimer.unref?.();
|
|
381
|
-
try {
|
|
382
|
-
// Re-check after acquiring the lock: a racing holder may have populated it.
|
|
383
|
-
const fresh = await this.getJSON(key);
|
|
384
|
-
if (fresh !== null)
|
|
385
|
-
return fresh;
|
|
386
|
-
const value = await callback();
|
|
387
|
-
await this.setJSON(key, value, seconds);
|
|
388
|
-
return value;
|
|
389
|
-
}
|
|
390
|
-
finally {
|
|
391
|
-
clearInterval(renewTimer);
|
|
392
|
-
// Only the owner may release the lock. A plain DEL lets a slow callback
|
|
393
|
-
// delete a successor's lock after its own 10s lease expired, reopening
|
|
394
|
-
// the stampede window. Waiters that never acquired it release nothing.
|
|
395
|
-
if (acquired != null) {
|
|
396
|
-
const releaseScript = `
|
|
397
|
-
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
398
|
-
return redis.call('DEL', KEYS[1])
|
|
399
|
-
end
|
|
400
|
-
return 0
|
|
401
|
-
`;
|
|
402
|
-
await this.client.send('EVAL', [releaseScript, '1', lockKey, lockToken]).catch(() => { });
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
/**
|
|
407
|
-
* Delete every key under this connection's prefix using a non-blocking SCAN.
|
|
408
|
-
*
|
|
409
|
-
* Unlike {@link Redis.flushdb}, this is scoped: it only removes keys that
|
|
410
|
-
* belong to this logical store (`<prefix>:*`), leaving other stores that
|
|
411
|
-
* share the same Redis database (sessions, queues, ...) untouched. If no
|
|
412
|
-
* prefix is configured this deletes nothing and returns `0`, to avoid
|
|
413
|
-
* accidentally wiping the whole database.
|
|
414
|
-
*
|
|
415
|
-
* @returns The number of keys deleted.
|
|
416
|
-
*/
|
|
417
|
-
async clearPrefix() {
|
|
418
|
-
if (!this.prefix)
|
|
419
|
-
return 0;
|
|
420
|
-
const pattern = `${this.prefix}:*`;
|
|
421
|
-
let cursor = '0';
|
|
422
|
-
let deleted = 0;
|
|
423
|
-
do {
|
|
424
|
-
const [next, batch] = await this.client.send('SCAN', [cursor, 'MATCH', pattern, 'COUNT', '100']);
|
|
425
|
-
cursor = next;
|
|
426
|
-
if (batch.length) {
|
|
427
|
-
await this.client.send('DEL', batch);
|
|
428
|
-
deleted += batch.length;
|
|
429
|
-
}
|
|
430
|
-
} while (cursor !== '0');
|
|
431
|
-
return deleted;
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Delete ALL keys in the currently selected database. DANGEROUS.
|
|
435
|
-
*
|
|
436
|
-
* This ignores the key prefix and removes every key in the database, including
|
|
437
|
-
* data owned by other logical stores (sessions, queues, other caches) that
|
|
438
|
-
* share the same Redis database. Prefer {@link Redis.clearPrefix} to delete
|
|
439
|
-
* only this store's keys. Never expose this to user-triggered code paths.
|
|
440
|
-
*
|
|
441
|
-
* @returns A promise that resolves once the database has been flushed.
|
|
442
|
-
*/
|
|
443
|
-
async flushdb() { await this.send('FLUSHDB', []); }
|
|
444
|
-
/**
|
|
445
|
-
* Get the underlying Bun `RedisClient` instance for advanced operations.
|
|
446
|
-
*
|
|
447
|
-
* @returns The raw Bun RedisClient.
|
|
448
|
-
*/
|
|
449
|
-
getClient() { return this.client; }
|
|
450
|
-
}
|
package/dist/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|