@naturalcycles/redis-lib 4.0.1 → 4.0.3

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.
@@ -29,7 +29,7 @@ export declare class RedisClient implements CommonClient {
29
29
  cfg: Required<RedisClientCfg>;
30
30
  connected: boolean;
31
31
  private _redis?;
32
- redis(): Redis;
32
+ redis(): Promise<Redis>;
33
33
  connect(): Promise<void>;
34
34
  disconnect(): Promise<void>;
35
35
  [Symbol.asyncDispose](): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { Transform } from 'node:stream';
1
2
  import { _stringMapEntries } from '@naturalcycles/js-lib';
2
3
  /**
3
4
  Wraps the redis sdk with unified interface.
@@ -22,11 +23,11 @@ export class RedisClient {
22
23
  cfg;
23
24
  connected = false;
24
25
  _redis;
25
- redis() {
26
+ async redis() {
26
27
  if (this._redis)
27
28
  return this._redis;
28
29
  // lazy-load the library
29
- const redisLib = require('ioredis');
30
+ const { default: redisLib } = await import('ioredis');
30
31
  const redis = new redisLib.Redis(this.cfg.redisOptions);
31
32
  const { logger } = this.cfg;
32
33
  const redisEvents = ['connect', 'close', 'reconnecting', 'end'];
@@ -41,62 +42,78 @@ export class RedisClient {
41
42
  }
42
43
  async connect() {
43
44
  if (!this.connected) {
44
- await this.redis().connect();
45
+ const redis = await this.redis();
46
+ await redis.connect();
45
47
  this.connected = true;
46
48
  }
47
49
  }
48
50
  async disconnect() {
51
+ const redis = await this.redis();
49
52
  this.log('redis: quit...');
50
- this.log(`redis: quit`, await this.redis().quit());
53
+ this.log(`redis: quit`, await redis.quit());
51
54
  this.connected = false;
52
55
  }
53
56
  async [Symbol.asyncDispose]() {
54
57
  await this.disconnect();
55
58
  }
56
59
  async ping() {
57
- await this.redis().ping();
60
+ const redis = await this.redis();
61
+ await redis.ping();
58
62
  }
59
63
  async del(keys) {
60
- return await this.redis().del(keys);
64
+ const redis = await this.redis();
65
+ return await redis.del(keys);
61
66
  }
62
67
  async get(key) {
63
- return await this.redis().get(key);
68
+ const redis = await this.redis();
69
+ return await redis.get(key);
64
70
  }
65
71
  async getBuffer(key) {
66
- return await this.redis().getBuffer(key);
72
+ const redis = await this.redis();
73
+ return await redis.getBuffer(key);
67
74
  }
68
75
  async mget(keys) {
69
- return await this.redis().mget(keys);
76
+ const redis = await this.redis();
77
+ return await redis.mget(keys);
70
78
  }
71
79
  async mgetBuffer(keys) {
72
- return await this.redis().mgetBuffer(keys);
80
+ const redis = await this.redis();
81
+ return await redis.mgetBuffer(keys);
73
82
  }
74
83
  async set(key, value) {
75
- await this.redis().set(key, value);
84
+ const redis = await this.redis();
85
+ await redis.set(key, value);
76
86
  }
77
87
  async hgetall(key) {
78
- const result = await this.redis().hgetall(key);
88
+ const redis = await this.redis();
89
+ const result = await redis.hgetall(key);
79
90
  if (Object.keys(result).length === 0)
80
91
  return null;
81
92
  return result;
82
93
  }
83
94
  async hget(key, field) {
84
- return await this.redis().hget(key, field);
95
+ const redis = await this.redis();
96
+ return await redis.hget(key, field);
85
97
  }
86
98
  async hset(key, value) {
87
- await this.redis().hset(key, value);
99
+ const redis = await this.redis();
100
+ await redis.hset(key, value);
88
101
  }
89
102
  async hdel(key, fields) {
90
- await this.redis().hdel(key, ...fields);
103
+ const redis = await this.redis();
104
+ await redis.hdel(key, ...fields);
91
105
  }
92
106
  async hmget(key, fields) {
93
- return await this.redis().hmget(key, ...fields);
107
+ const redis = await this.redis();
108
+ return await redis.hmget(key, ...fields);
94
109
  }
95
110
  async hmgetBuffer(key, fields) {
96
- return await this.redis().hmgetBuffer(key, ...fields);
111
+ const redis = await this.redis();
112
+ return await redis.hmgetBuffer(key, ...fields);
97
113
  }
98
114
  async hincr(key, field, increment = 1) {
99
- return await this.redis().hincrby(key, field, increment);
115
+ const redis = await this.redis();
116
+ return await redis.hincrby(key, field, increment);
100
117
  }
101
118
  async hincrBatch(key, incrementTuples) {
102
119
  const results = {};
@@ -111,7 +128,8 @@ export class RedisClient {
111
128
  return validResults;
112
129
  }
113
130
  async setWithTTL(key, value, expireAt) {
114
- await this.redis().set(key, value, 'EXAT', expireAt);
131
+ const redis = await this.redis();
132
+ await redis.set(key, value, 'EXAT', expireAt);
115
133
  }
116
134
  async hsetWithTTL(_key, _value, _expireAt) {
117
135
  throw new Error('Not supported until Redis 7.4.0');
@@ -120,17 +138,20 @@ export class RedisClient {
120
138
  // const keyList = valueKeys.join(' ')
121
139
  // const commandString = `HEXPIREAT ${key} ${expireAt} FIELDS ${numberOfKeys} ${keyList}`
122
140
  // const [command, ...args] = commandString.split(' ')
123
- // await this.redis().hset(key, value)
124
- // await this.redis().call(command!, args)
141
+ // await redis.hset(key, value)
142
+ // await redis.call(command!, args)
125
143
  }
126
144
  async mset(obj) {
127
- await this.redis().mset(obj);
145
+ const redis = await this.redis();
146
+ await redis.mset(obj);
128
147
  }
129
148
  async msetBuffer(obj) {
130
- await this.redis().mset(obj);
149
+ const redis = await this.redis();
150
+ await redis.mset(obj);
131
151
  }
132
152
  async incr(key, by = 1) {
133
- return await this.redis().incrby(key, by);
153
+ const redis = await this.redis();
154
+ return await redis.incrby(key, by);
134
155
  }
135
156
  async incrBatch(incrementTuples) {
136
157
  const results = {};
@@ -145,7 +166,8 @@ export class RedisClient {
145
166
  return validResults;
146
167
  }
147
168
  async ttl(key) {
148
- return await this.redis().ttl(key);
169
+ const redis = await this.redis();
170
+ return await redis.ttl(key);
149
171
  }
150
172
  async dropTable(table) {
151
173
  let count = 0;
@@ -177,36 +199,45 @@ export class RedisClient {
177
199
  Returns BATCHES of keys in each iteration (as-is).
178
200
  */
179
201
  scanStream(opt) {
180
- return this.redis().scanStream(opt);
202
+ return createReadableFromAsync(async () => {
203
+ const redis = await this.redis();
204
+ return redis.scanStream(opt);
205
+ });
181
206
  }
182
207
  /**
183
208
  * Like scanStream, but flattens the stream of keys.
184
209
  */
185
210
  scanStreamFlat(opt) {
186
211
  // biome-ignore lint/correctness/noFlatMapIdentity: ok
187
- return this.redis().scanStream(opt).flatMap(keys => keys);
212
+ return this.scanStream(opt).flatMap(keys => keys);
188
213
  }
189
214
  async scanCount(opt) {
215
+ const redis = await this.redis();
190
216
  // todo: implement more efficiently, e.g via LUA?
191
217
  let count = 0;
192
- await this.redis().scanStream(opt).forEach(keys => {
218
+ await redis.scanStream(opt).forEach(keys => {
193
219
  count += keys.length;
194
220
  });
195
221
  return count;
196
222
  }
197
223
  hscanStream(key, opt) {
198
- return this.redis().hscanStream(key, opt);
224
+ return createReadableFromAsync(async () => {
225
+ const redis = await this.redis();
226
+ return redis.hscanStream(key, opt);
227
+ });
199
228
  }
200
229
  async hscanCount(key, opt) {
201
230
  let count = 0;
202
- const stream = this.redis().hscanStream(key, opt);
231
+ const redis = await this.redis();
232
+ const stream = redis.hscanStream(key, opt);
203
233
  await stream.forEach((keyValueList) => {
204
234
  count += keyValueList.length / 2;
205
235
  });
206
236
  return count;
207
237
  }
208
238
  async withPipeline(fn) {
209
- const pipeline = this.redis().pipeline();
239
+ const redis = await this.redis();
240
+ const pipeline = redis.pipeline();
210
241
  await fn(pipeline);
211
242
  await pipeline.exec();
212
243
  }
@@ -214,3 +245,20 @@ export class RedisClient {
214
245
  this.cfg.logger.log(...args);
215
246
  }
216
247
  }
248
+ /**
249
+ * Turn async function into Readable.
250
+ */
251
+ function createReadableFromAsync(fn) {
252
+ const transform = new Transform({
253
+ objectMode: true,
254
+ transform: (chunk, _encoding, cb) => {
255
+ cb(null, chunk);
256
+ },
257
+ });
258
+ void fn()
259
+ .then(readable => {
260
+ readable.on('error', err => transform.emit('error', err)).pipe(transform);
261
+ })
262
+ .catch(err => transform.emit('error', err));
263
+ return transform;
264
+ }
package/package.json CHANGED
@@ -11,12 +11,12 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@naturalcycles/db-lib": "^10",
14
- "@naturalcycles/js-lib": "^14",
15
- "@naturalcycles/nodejs-lib": "^13",
14
+ "@naturalcycles/js-lib": "^15",
15
+ "@naturalcycles/nodejs-lib": "^14",
16
16
  "ioredis": "^5"
17
17
  },
18
18
  "devDependencies": {
19
- "@naturalcycles/dev-lib": "^17",
19
+ "@naturalcycles/dev-lib": "^18",
20
20
  "@types/node": "^22",
21
21
  "@vitest/coverage-v8": "^3",
22
22
  "tsx": "^4",
@@ -43,7 +43,7 @@
43
43
  "engines": {
44
44
  "node": ">=22.10"
45
45
  },
46
- "version": "4.0.1",
46
+ "version": "4.0.3",
47
47
  "description": "Redis implementation of CommonKeyValueDB interface",
48
48
  "author": "Natural Cycles Team",
49
49
  "license": "MIT"
@@ -1,5 +1,7 @@
1
+ import { Transform } from 'node:stream'
1
2
  import type {
2
3
  AnyObject,
4
+ AsyncFunction,
3
5
  CommonLogger,
4
6
  NullableBuffer,
5
7
  NullableString,
@@ -10,7 +12,6 @@ import type {
10
12
  import { _stringMapEntries } from '@naturalcycles/js-lib'
11
13
  import type { ReadableTyped } from '@naturalcycles/nodejs-lib'
12
14
  import type { Redis, RedisOptions } from 'ioredis'
13
- import type * as RedisLib from 'ioredis'
14
15
  import type { ScanStreamOptions } from 'ioredis/built/types.js'
15
16
  import type { ChainableCommander } from 'ioredis/built/utils/RedisCommander.js'
16
17
 
@@ -57,11 +58,11 @@ export class RedisClient implements CommonClient {
57
58
 
58
59
  private _redis?: Redis
59
60
 
60
- redis(): Redis {
61
+ async redis(): Promise<Redis> {
61
62
  if (this._redis) return this._redis
62
63
 
63
64
  // lazy-load the library
64
- const redisLib = require('ioredis') as typeof RedisLib
65
+ const { default: redisLib } = await import('ioredis')
65
66
  const redis = new redisLib.Redis(this.cfg.redisOptions)
66
67
 
67
68
  const { logger } = this.cfg
@@ -82,14 +83,16 @@ export class RedisClient implements CommonClient {
82
83
 
83
84
  async connect(): Promise<void> {
84
85
  if (!this.connected) {
85
- await this.redis().connect()
86
+ const redis = await this.redis()
87
+ await redis.connect()
86
88
  this.connected = true
87
89
  }
88
90
  }
89
91
 
90
92
  async disconnect(): Promise<void> {
93
+ const redis = await this.redis()
91
94
  this.log('redis: quit...')
92
- this.log(`redis: quit`, await this.redis().quit())
95
+ this.log(`redis: quit`, await redis.quit())
93
96
  this.connected = false
94
97
  }
95
98
 
@@ -98,63 +101,77 @@ export class RedisClient implements CommonClient {
98
101
  }
99
102
 
100
103
  async ping(): Promise<void> {
101
- await this.redis().ping()
104
+ const redis = await this.redis()
105
+ await redis.ping()
102
106
  }
103
107
 
104
108
  async del(keys: string[]): Promise<number> {
105
- return await this.redis().del(keys)
109
+ const redis = await this.redis()
110
+ return await redis.del(keys)
106
111
  }
107
112
 
108
113
  async get(key: string): Promise<NullableString> {
109
- return await this.redis().get(key)
114
+ const redis = await this.redis()
115
+ return await redis.get(key)
110
116
  }
111
117
 
112
118
  async getBuffer(key: string): Promise<NullableBuffer> {
113
- return await this.redis().getBuffer(key)
119
+ const redis = await this.redis()
120
+ return await redis.getBuffer(key)
114
121
  }
115
122
 
116
123
  async mget(keys: string[]): Promise<NullableString[]> {
117
- return await this.redis().mget(keys)
124
+ const redis = await this.redis()
125
+ return await redis.mget(keys)
118
126
  }
119
127
 
120
128
  async mgetBuffer(keys: string[]): Promise<NullableBuffer[]> {
121
- return await this.redis().mgetBuffer(keys)
129
+ const redis = await this.redis()
130
+ return await redis.mgetBuffer(keys)
122
131
  }
123
132
 
124
133
  async set(key: string, value: string | number | Buffer): Promise<void> {
125
- await this.redis().set(key, value)
134
+ const redis = await this.redis()
135
+ await redis.set(key, value)
126
136
  }
127
137
 
128
138
  async hgetall<T extends Record<string, string> = Record<string, string>>(
129
139
  key: string,
130
140
  ): Promise<T | null> {
131
- const result = await this.redis().hgetall(key)
141
+ const redis = await this.redis()
142
+ const result = await redis.hgetall(key)
132
143
  if (Object.keys(result).length === 0) return null
133
144
  return result as T
134
145
  }
135
146
 
136
147
  async hget(key: string, field: string): Promise<NullableString> {
137
- return await this.redis().hget(key, field)
148
+ const redis = await this.redis()
149
+ return await redis.hget(key, field)
138
150
  }
139
151
 
140
152
  async hset(key: string, value: AnyObject): Promise<void> {
141
- await this.redis().hset(key, value)
153
+ const redis = await this.redis()
154
+ await redis.hset(key, value)
142
155
  }
143
156
 
144
157
  async hdel(key: string, fields: string[]): Promise<void> {
145
- await this.redis().hdel(key, ...fields)
158
+ const redis = await this.redis()
159
+ await redis.hdel(key, ...fields)
146
160
  }
147
161
 
148
162
  async hmget(key: string, fields: string[]): Promise<NullableString[]> {
149
- return await this.redis().hmget(key, ...fields)
163
+ const redis = await this.redis()
164
+ return await redis.hmget(key, ...fields)
150
165
  }
151
166
 
152
167
  async hmgetBuffer(key: string, fields: string[]): Promise<NullableBuffer[]> {
153
- return await this.redis().hmgetBuffer(key, ...fields)
168
+ const redis = await this.redis()
169
+ return await redis.hmgetBuffer(key, ...fields)
154
170
  }
155
171
 
156
172
  async hincr(key: string, field: string, increment = 1): Promise<number> {
157
- return await this.redis().hincrby(key, field, increment)
173
+ const redis = await this.redis()
174
+ return await redis.hincrby(key, field, increment)
158
175
  }
159
176
 
160
177
  async hincrBatch(key: string, incrementTuples: [string, number][]): Promise<[string, number][]> {
@@ -181,7 +198,8 @@ export class RedisClient implements CommonClient {
181
198
  value: string | number | Buffer,
182
199
  expireAt: UnixTimestamp,
183
200
  ): Promise<void> {
184
- await this.redis().set(key, value, 'EXAT', expireAt)
201
+ const redis = await this.redis()
202
+ await redis.set(key, value, 'EXAT', expireAt)
185
203
  }
186
204
 
187
205
  async hsetWithTTL(_key: string, _value: AnyObject, _expireAt: UnixTimestamp): Promise<void> {
@@ -191,20 +209,23 @@ export class RedisClient implements CommonClient {
191
209
  // const keyList = valueKeys.join(' ')
192
210
  // const commandString = `HEXPIREAT ${key} ${expireAt} FIELDS ${numberOfKeys} ${keyList}`
193
211
  // const [command, ...args] = commandString.split(' ')
194
- // await this.redis().hset(key, value)
195
- // await this.redis().call(command!, args)
212
+ // await redis.hset(key, value)
213
+ // await redis.call(command!, args)
196
214
  }
197
215
 
198
216
  async mset(obj: Record<string, string | number>): Promise<void> {
199
- await this.redis().mset(obj)
217
+ const redis = await this.redis()
218
+ await redis.mset(obj)
200
219
  }
201
220
 
202
221
  async msetBuffer(obj: Record<string, Buffer>): Promise<void> {
203
- await this.redis().mset(obj)
222
+ const redis = await this.redis()
223
+ await redis.mset(obj)
204
224
  }
205
225
 
206
226
  async incr(key: string, by = 1): Promise<number> {
207
- return await this.redis().incrby(key, by)
227
+ const redis = await this.redis()
228
+ return await redis.incrby(key, by)
208
229
  }
209
230
 
210
231
  async incrBatch(incrementTuples: [string, number][]): Promise<[string, number][]> {
@@ -227,7 +248,8 @@ export class RedisClient implements CommonClient {
227
248
  }
228
249
 
229
250
  async ttl(key: string): Promise<number> {
230
- return await this.redis().ttl(key)
251
+ const redis = await this.redis()
252
+ return await redis.ttl(key)
231
253
  }
232
254
 
233
255
  async dropTable(table: string): Promise<void> {
@@ -266,7 +288,10 @@ export class RedisClient implements CommonClient {
266
288
  Returns BATCHES of keys in each iteration (as-is).
267
289
  */
268
290
  scanStream(opt?: ScanStreamOptions): ReadableTyped<string[]> {
269
- return this.redis().scanStream(opt)
291
+ return createReadableFromAsync(async () => {
292
+ const redis = await this.redis()
293
+ return redis.scanStream(opt)
294
+ })
270
295
  }
271
296
 
272
297
  /**
@@ -274,14 +299,15 @@ export class RedisClient implements CommonClient {
274
299
  */
275
300
  scanStreamFlat(opt: ScanStreamOptions): ReadableTyped<string> {
276
301
  // biome-ignore lint/correctness/noFlatMapIdentity: ok
277
- return (this.redis().scanStream(opt) as ReadableTyped<string[]>).flatMap(keys => keys)
302
+ return this.scanStream(opt).flatMap(keys => keys)
278
303
  }
279
304
 
280
305
  async scanCount(opt: ScanStreamOptions): Promise<number> {
306
+ const redis = await this.redis()
281
307
  // todo: implement more efficiently, e.g via LUA?
282
308
  let count = 0
283
309
 
284
- await (this.redis().scanStream(opt) as ReadableTyped<string[]>).forEach(keys => {
310
+ await (redis.scanStream(opt) as ReadableTyped<string[]>).forEach(keys => {
285
311
  count += keys.length
286
312
  })
287
313
 
@@ -289,13 +315,17 @@ export class RedisClient implements CommonClient {
289
315
  }
290
316
 
291
317
  hscanStream(key: string, opt?: ScanStreamOptions): ReadableTyped<string[]> {
292
- return this.redis().hscanStream(key, opt)
318
+ return createReadableFromAsync(async () => {
319
+ const redis = await this.redis()
320
+ return redis.hscanStream(key, opt)
321
+ })
293
322
  }
294
323
 
295
324
  async hscanCount(key: string, opt?: ScanStreamOptions): Promise<number> {
296
325
  let count = 0
297
326
 
298
- const stream = this.redis().hscanStream(key, opt)
327
+ const redis = await this.redis()
328
+ const stream = redis.hscanStream(key, opt)
299
329
 
300
330
  await stream.forEach((keyValueList: string[]) => {
301
331
  count += keyValueList.length / 2
@@ -305,7 +335,8 @@ export class RedisClient implements CommonClient {
305
335
  }
306
336
 
307
337
  async withPipeline(fn: (pipeline: ChainableCommander) => Promisable<void>): Promise<void> {
308
- const pipeline = this.redis().pipeline()
338
+ const redis = await this.redis()
339
+ const pipeline = redis.pipeline()
309
340
  await fn(pipeline)
310
341
  await pipeline.exec()
311
342
  }
@@ -314,3 +345,23 @@ export class RedisClient implements CommonClient {
314
345
  this.cfg.logger.log(...args)
315
346
  }
316
347
  }
348
+
349
+ /**
350
+ * Turn async function into Readable.
351
+ */
352
+ function createReadableFromAsync<T>(fn: AsyncFunction<ReadableTyped<T>>): ReadableTyped<T> {
353
+ const transform = new Transform({
354
+ objectMode: true,
355
+ transform: (chunk, _encoding, cb) => {
356
+ cb(null, chunk)
357
+ },
358
+ })
359
+
360
+ void fn()
361
+ .then(readable => {
362
+ readable.on('error', err => transform.emit('error', err)).pipe(transform)
363
+ })
364
+ .catch(err => transform.emit('error', err))
365
+
366
+ return transform
367
+ }