@naturalcycles/redis-lib 4.5.0 → 4.7.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/dist/redisClient.d.ts +21 -1
- package/dist/redisClient.js +58 -4
- package/package.json +3 -3
- package/src/redisClient.ts +72 -6
package/dist/redisClient.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface CommonClient extends AsyncDisposable {
|
|
|
13
13
|
export interface RedisClientCfg {
|
|
14
14
|
redisOptions?: RedisOptions;
|
|
15
15
|
/**
|
|
16
|
-
* Defaults to
|
|
16
|
+
* Defaults to consoleLogger.
|
|
17
17
|
*/
|
|
18
18
|
logger?: CommonLogger;
|
|
19
19
|
}
|
|
@@ -53,8 +53,28 @@ export declare class RedisClient implements CommonClient {
|
|
|
53
53
|
hsetWithTTL(_key: string, _value: AnyObject, _expireAt: UnixTimestamp): Promise<void>;
|
|
54
54
|
mset(obj: Record<string, string | number>): Promise<void>;
|
|
55
55
|
msetBuffer(obj: Record<string, Buffer>): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* For counters that are supposed to expire, use {@link incrWithTTL} instead.
|
|
58
|
+
*/
|
|
56
59
|
incr(key: string, by?: number): Promise<number>;
|
|
60
|
+
/**
|
|
61
|
+
* Increments the key and guarantees it has an expiry, in a single transaction.
|
|
62
|
+
*
|
|
63
|
+
* Returns the new value.
|
|
64
|
+
*
|
|
65
|
+
* Requires Redis 7.0+
|
|
66
|
+
*/
|
|
67
|
+
incrWithTTL(key: string, expireAt: UnixTimestamp, by?: number): Promise<number>;
|
|
68
|
+
/**
|
|
69
|
+
* For counters that are supposed to expire, use {@link incrBatchWithTTL} instead.
|
|
70
|
+
*/
|
|
57
71
|
incrBatch(incrementTuples: [string, number][]): Promise<[string, number][]>;
|
|
72
|
+
/**
|
|
73
|
+
* Batch version of {@link incrWithTTL}, all increments and expiries in a single transaction.
|
|
74
|
+
*
|
|
75
|
+
* Requires Redis 7.0+
|
|
76
|
+
*/
|
|
77
|
+
incrBatchWithTTL(incrementTuples: [string, number][], expireAt: UnixTimestamp): Promise<[string, number][]>;
|
|
58
78
|
ttl(key: string): Promise<number>;
|
|
59
79
|
dropTable(table: string): Promise<void>;
|
|
60
80
|
clearAll(): Promise<void>;
|
package/dist/redisClient.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { _assert } from '@naturalcycles/js-lib/error';
|
|
2
|
+
import { consoleLogger } from '@naturalcycles/js-lib/log';
|
|
1
3
|
import { _stringMapEntries } from '@naturalcycles/js-lib/types';
|
|
2
4
|
import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
3
5
|
/**
|
|
@@ -11,7 +13,7 @@ import { Pipeline } from '@naturalcycles/nodejs-lib/stream';
|
|
|
11
13
|
export class RedisClient {
|
|
12
14
|
constructor(cfg = {}) {
|
|
13
15
|
this.cfg = {
|
|
14
|
-
logger:
|
|
16
|
+
logger: consoleLogger,
|
|
15
17
|
...cfg,
|
|
16
18
|
redisOptions: {
|
|
17
19
|
showFriendlyErrorStack: true,
|
|
@@ -50,7 +52,7 @@ export class RedisClient {
|
|
|
50
52
|
async disconnect() {
|
|
51
53
|
const redis = await this.redis();
|
|
52
54
|
this.log('redis: quit...');
|
|
53
|
-
this.log(`redis: quit`, await redis.quit());
|
|
55
|
+
this.log({ msg: `redis: quit`, result: await redis.quit() });
|
|
54
56
|
this.connected = false;
|
|
55
57
|
}
|
|
56
58
|
async [Symbol.asyncDispose]() {
|
|
@@ -149,10 +151,37 @@ export class RedisClient {
|
|
|
149
151
|
const redis = await this.redis();
|
|
150
152
|
await redis.mset(obj);
|
|
151
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* For counters that are supposed to expire, use {@link incrWithTTL} instead.
|
|
156
|
+
*/
|
|
152
157
|
async incr(key, by = 1) {
|
|
153
158
|
const redis = await this.redis();
|
|
154
159
|
return await redis.incrby(key, by);
|
|
155
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Increments the key and guarantees it has an expiry, in a single transaction.
|
|
163
|
+
*
|
|
164
|
+
* Returns the new value.
|
|
165
|
+
*
|
|
166
|
+
* Requires Redis 7.0+
|
|
167
|
+
*/
|
|
168
|
+
async incrWithTTL(key, expireAt, by = 1) {
|
|
169
|
+
const redis = await this.redis();
|
|
170
|
+
const results = await redis.multi().incrby(key, by).expireat(key, expireAt, 'NX').exec();
|
|
171
|
+
const result = results?.[0];
|
|
172
|
+
_assert(result, `redis: incrWithTTL transaction returned no result, key: ${key}`);
|
|
173
|
+
// Redis does not roll back, so a failed EXPIREAT leaves the key incremented and without an
|
|
174
|
+
// expiry. It self-heals on the next successful call, as EXPIREAT NX applies to a TTL-less key.
|
|
175
|
+
for (const [err] of results) {
|
|
176
|
+
if (err)
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
const [, value] = result;
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* For counters that are supposed to expire, use {@link incrBatchWithTTL} instead.
|
|
184
|
+
*/
|
|
156
185
|
async incrBatch(incrementTuples) {
|
|
157
186
|
const results = {};
|
|
158
187
|
await this.withPipeline(async (pipeline) => {
|
|
@@ -165,6 +194,31 @@ export class RedisClient {
|
|
|
165
194
|
const validResults = _stringMapEntries(results).filter(([_, v]) => v !== undefined);
|
|
166
195
|
return validResults;
|
|
167
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Batch version of {@link incrWithTTL}, all increments and expiries in a single transaction.
|
|
199
|
+
*
|
|
200
|
+
* Requires Redis 7.0+
|
|
201
|
+
*/
|
|
202
|
+
async incrBatchWithTTL(incrementTuples, expireAt) {
|
|
203
|
+
const redis = await this.redis();
|
|
204
|
+
const multi = redis.multi();
|
|
205
|
+
for (const [key, increment] of incrementTuples) {
|
|
206
|
+
multi.incrby(key, increment);
|
|
207
|
+
multi.expireat(key, expireAt, 'NX');
|
|
208
|
+
}
|
|
209
|
+
// 2 commands are queued per key, so the increments sit at the even indexes
|
|
210
|
+
const results = await multi.exec();
|
|
211
|
+
const expectedLength = incrementTuples.length * 2;
|
|
212
|
+
_assert(results?.length === expectedLength, `redis: incrBatchWithTTL expected ${expectedLength} results, got ${results?.length}`);
|
|
213
|
+
for (const [err] of results) {
|
|
214
|
+
if (err)
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
return incrementTuples.map(([key], i) => {
|
|
218
|
+
const [, newValue] = results[i * 2];
|
|
219
|
+
return [key, newValue];
|
|
220
|
+
});
|
|
221
|
+
}
|
|
168
222
|
async ttl(key) {
|
|
169
223
|
const redis = await this.redis();
|
|
170
224
|
return await redis.ttl(key);
|
|
@@ -240,7 +294,7 @@ export class RedisClient {
|
|
|
240
294
|
await fn(pipeline);
|
|
241
295
|
await pipeline.exec();
|
|
242
296
|
}
|
|
243
|
-
log(
|
|
244
|
-
this.cfg.logger.log(
|
|
297
|
+
log(input) {
|
|
298
|
+
this.cfg.logger.log(input);
|
|
245
299
|
}
|
|
246
300
|
}
|
package/package.json
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"tslib": "^2"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
+
"@naturalcycles/dev-lib": "20.64.0",
|
|
12
13
|
"@types/node": "^26",
|
|
13
|
-
"typescript": "^7"
|
|
14
|
-
"@naturalcycles/dev-lib": "0.0.0"
|
|
14
|
+
"typescript": "^7"
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./dist/index.js"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=24.10.0"
|
|
40
40
|
},
|
|
41
|
-
"version": "4.
|
|
41
|
+
"version": "4.7.0",
|
|
42
42
|
"description": "Redis implementation of CommonKeyValueDB interface",
|
|
43
43
|
"author": "Natural Cycles Team",
|
|
44
44
|
"license": "MIT",
|
package/src/redisClient.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { _assert } from '@naturalcycles/js-lib/error'
|
|
2
|
+
import { consoleLogger } from '@naturalcycles/js-lib/log'
|
|
3
|
+
import type { CommonLogger, LogInput } from '@naturalcycles/js-lib/log'
|
|
2
4
|
import type {
|
|
3
5
|
AnyObject,
|
|
4
6
|
NullableBuffer,
|
|
@@ -24,7 +26,7 @@ export interface RedisClientCfg {
|
|
|
24
26
|
redisOptions?: RedisOptions
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
|
-
* Defaults to
|
|
29
|
+
* Defaults to consoleLogger.
|
|
28
30
|
*/
|
|
29
31
|
logger?: CommonLogger
|
|
30
32
|
}
|
|
@@ -40,7 +42,7 @@ export interface RedisClientCfg {
|
|
|
40
42
|
export class RedisClient implements CommonClient {
|
|
41
43
|
constructor(cfg: RedisClientCfg = {}) {
|
|
42
44
|
this.cfg = {
|
|
43
|
-
logger:
|
|
45
|
+
logger: consoleLogger,
|
|
44
46
|
...cfg,
|
|
45
47
|
redisOptions: {
|
|
46
48
|
showFriendlyErrorStack: true,
|
|
@@ -90,7 +92,7 @@ export class RedisClient implements CommonClient {
|
|
|
90
92
|
async disconnect(): Promise<void> {
|
|
91
93
|
const redis = await this.redis()
|
|
92
94
|
this.log('redis: quit...')
|
|
93
|
-
this.log(`redis: quit`, await redis.quit())
|
|
95
|
+
this.log({ msg: `redis: quit`, result: await redis.quit() })
|
|
94
96
|
this.connected = false
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -221,11 +223,40 @@ export class RedisClient implements CommonClient {
|
|
|
221
223
|
await redis.mset(obj)
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
/**
|
|
227
|
+
* For counters that are supposed to expire, use {@link incrWithTTL} instead.
|
|
228
|
+
*/
|
|
224
229
|
async incr(key: string, by = 1): Promise<number> {
|
|
225
230
|
const redis = await this.redis()
|
|
226
231
|
return await redis.incrby(key, by)
|
|
227
232
|
}
|
|
228
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Increments the key and guarantees it has an expiry, in a single transaction.
|
|
236
|
+
*
|
|
237
|
+
* Returns the new value.
|
|
238
|
+
*
|
|
239
|
+
* Requires Redis 7.0+
|
|
240
|
+
*/
|
|
241
|
+
async incrWithTTL(key: string, expireAt: UnixTimestamp, by = 1): Promise<number> {
|
|
242
|
+
const redis = await this.redis()
|
|
243
|
+
const results = await redis.multi().incrby(key, by).expireat(key, expireAt, 'NX').exec()
|
|
244
|
+
const result = results?.[0]
|
|
245
|
+
_assert(result, `redis: incrWithTTL transaction returned no result, key: ${key}`)
|
|
246
|
+
|
|
247
|
+
// Redis does not roll back, so a failed EXPIREAT leaves the key incremented and without an
|
|
248
|
+
// expiry. It self-heals on the next successful call, as EXPIREAT NX applies to a TTL-less key.
|
|
249
|
+
for (const [err] of results) {
|
|
250
|
+
if (err) throw err
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const [, value] = result
|
|
254
|
+
return value as number
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* For counters that are supposed to expire, use {@link incrBatchWithTTL} instead.
|
|
259
|
+
*/
|
|
229
260
|
async incrBatch(incrementTuples: [string, number][]): Promise<[string, number][]> {
|
|
230
261
|
const results: StringMap<number | undefined> = {}
|
|
231
262
|
|
|
@@ -245,6 +276,41 @@ export class RedisClient implements CommonClient {
|
|
|
245
276
|
return validResults
|
|
246
277
|
}
|
|
247
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Batch version of {@link incrWithTTL}, all increments and expiries in a single transaction.
|
|
281
|
+
*
|
|
282
|
+
* Requires Redis 7.0+
|
|
283
|
+
*/
|
|
284
|
+
async incrBatchWithTTL(
|
|
285
|
+
incrementTuples: [string, number][],
|
|
286
|
+
expireAt: UnixTimestamp,
|
|
287
|
+
): Promise<[string, number][]> {
|
|
288
|
+
const redis = await this.redis()
|
|
289
|
+
const multi = redis.multi()
|
|
290
|
+
|
|
291
|
+
for (const [key, increment] of incrementTuples) {
|
|
292
|
+
multi.incrby(key, increment)
|
|
293
|
+
multi.expireat(key, expireAt, 'NX')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 2 commands are queued per key, so the increments sit at the even indexes
|
|
297
|
+
const results = await multi.exec()
|
|
298
|
+
const expectedLength = incrementTuples.length * 2
|
|
299
|
+
_assert(
|
|
300
|
+
results?.length === expectedLength,
|
|
301
|
+
`redis: incrBatchWithTTL expected ${expectedLength} results, got ${results?.length}`,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
for (const [err] of results) {
|
|
305
|
+
if (err) throw err
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return incrementTuples.map(([key], i) => {
|
|
309
|
+
const [, newValue] = results[i * 2]!
|
|
310
|
+
return [key, newValue as number]
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
248
314
|
async ttl(key: string): Promise<number> {
|
|
249
315
|
const redis = await this.redis()
|
|
250
316
|
return await redis.ttl(key)
|
|
@@ -338,7 +404,7 @@ export class RedisClient implements CommonClient {
|
|
|
338
404
|
await pipeline.exec()
|
|
339
405
|
}
|
|
340
406
|
|
|
341
|
-
private log(
|
|
342
|
-
this.cfg.logger.log(
|
|
407
|
+
private log(input: LogInput): void {
|
|
408
|
+
this.cfg.logger.log(input)
|
|
343
409
|
}
|
|
344
410
|
}
|