@naturalcycles/redis-lib 3.0.1 → 3.1.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/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './redisClient';
2
2
  export * from './redisKeyValueDB';
3
+ export * from './redisHashKeyValueDB';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./redisClient"), exports);
5
5
  tslib_1.__exportStar(require("./redisKeyValueDB"), exports);
6
+ tslib_1.__exportStar(require("./redisHashKeyValueDB"), exports);
@@ -1,5 +1,4 @@
1
- /// <reference types="node" />
2
- import { CommonLogger, NullableBuffer, NullableString, Promisable, UnixTimestampNumber } from '@naturalcycles/js-lib';
1
+ import { AnyObject, CommonLogger, NullableBuffer, NullableString, Promisable, UnixTimestampNumber } from '@naturalcycles/js-lib';
3
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
4
3
  import type { Redis, RedisOptions } from 'ioredis';
5
4
  import type { ScanStreamOptions } from 'ioredis/built/types';
@@ -41,10 +40,18 @@ export declare class RedisClient implements CommonClient {
41
40
  mget(keys: string[]): Promise<NullableString[]>;
42
41
  mgetBuffer(keys: string[]): Promise<NullableBuffer[]>;
43
42
  set(key: string, value: string | number | Buffer): Promise<void>;
43
+ hgetall<T extends Record<string, string> = Record<string, string>>(key: string): Promise<T | null>;
44
+ hget(key: string, field: string): Promise<NullableString>;
45
+ hset(key: string, value: AnyObject): Promise<void>;
46
+ hdel(key: string, fields: string[]): Promise<void>;
47
+ hmget(key: string, fields: string[]): Promise<NullableString[]>;
48
+ hmgetBuffer(key: string, fields: string[]): Promise<NullableBuffer[]>;
49
+ hincr(key: string, field: string, increment?: number): Promise<number>;
44
50
  setWithTTL(key: string, value: string | number | Buffer, expireAt: UnixTimestampNumber): Promise<void>;
51
+ hsetWithTTL(key: string, value: AnyObject, expireAt: UnixTimestampNumber): Promise<void>;
45
52
  mset(obj: Record<string, string | number>): Promise<void>;
46
53
  msetBuffer(obj: Record<string, Buffer>): Promise<void>;
47
- incr(key: string): Promise<number>;
54
+ incr(key: string, by?: number): Promise<number>;
48
55
  ttl(key: string): Promise<number>;
49
56
  dropTable(table: string): Promise<void>;
50
57
  clearAll(): Promise<void>;
@@ -58,6 +65,8 @@ export declare class RedisClient implements CommonClient {
58
65
  */
59
66
  scanStreamFlat(opt: ScanStreamOptions): ReadableTyped<string>;
60
67
  scanCount(opt: ScanStreamOptions): Promise<number>;
68
+ hscanStream(key: string, opt: ScanStreamOptions): ReadableTyped<string[]>;
69
+ hscanCount(key: string, opt: ScanStreamOptions): Promise<number>;
61
70
  withPipeline(fn: (pipeline: ChainableCommander) => Promisable<void>): Promise<void>;
62
71
  private log;
63
72
  }
@@ -74,17 +74,50 @@ class RedisClient {
74
74
  async set(key, value) {
75
75
  await this.redis().set(key, value);
76
76
  }
77
+ async hgetall(key) {
78
+ const result = await this.redis().hgetall(key);
79
+ if (Object.keys(result).length === 0)
80
+ return null;
81
+ return result;
82
+ }
83
+ async hget(key, field) {
84
+ return await this.redis().hget(key, field);
85
+ }
86
+ async hset(key, value) {
87
+ await this.redis().hset(key, value);
88
+ }
89
+ async hdel(key, fields) {
90
+ await this.redis().hdel(key, ...fields);
91
+ }
92
+ async hmget(key, fields) {
93
+ return await this.redis().hmget(key, ...fields);
94
+ }
95
+ async hmgetBuffer(key, fields) {
96
+ return await this.redis().hmgetBuffer(key, ...fields);
97
+ }
98
+ async hincr(key, field, increment = 1) {
99
+ return await this.redis().hincrby(key, field, increment);
100
+ }
77
101
  async setWithTTL(key, value, expireAt) {
78
102
  await this.redis().set(key, value, 'EXAT', expireAt);
79
103
  }
104
+ async hsetWithTTL(key, value, expireAt) {
105
+ const valueKeys = Object.keys(value);
106
+ const numberOfKeys = valueKeys.length;
107
+ const keyList = valueKeys.join(' ');
108
+ const commandString = `HEXPIREAT ${key} ${expireAt} FIELDS ${numberOfKeys} ${keyList}`;
109
+ const [command, ...args] = commandString.split(' ');
110
+ await this.redis().hset(key, value);
111
+ await this.redis().call(command, args);
112
+ }
80
113
  async mset(obj) {
81
114
  await this.redis().mset(obj);
82
115
  }
83
116
  async msetBuffer(obj) {
84
117
  await this.redis().mset(obj);
85
118
  }
86
- async incr(key) {
87
- return await this.redis().incr(key);
119
+ async incr(key, by = 1) {
120
+ return await this.redis().incrby(key, by);
88
121
  }
89
122
  async ttl(key) {
90
123
  return await this.redis().ttl(key);
@@ -135,6 +168,17 @@ class RedisClient {
135
168
  });
136
169
  return count;
137
170
  }
171
+ hscanStream(key, opt) {
172
+ return this.redis().hscanStream(key, opt);
173
+ }
174
+ async hscanCount(key, opt) {
175
+ let count = 0;
176
+ const stream = this.redis().hscanStream(key, opt);
177
+ await stream.forEach((keyValueList) => {
178
+ count += keyValueList.length / 2;
179
+ });
180
+ return count;
181
+ }
138
182
  async withPipeline(fn) {
139
183
  const pipeline = this.redis().pipeline();
140
184
  await fn(pipeline);
@@ -0,0 +1,27 @@
1
+ import { CommonKeyValueDBSaveBatchOptions, CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib';
2
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib';
3
+ import { RedisClient } from './redisClient';
4
+ import { RedisKeyValueDBCfg } from './redisKeyValueDB';
5
+ export interface RedisHashKeyValueDBCfg extends RedisKeyValueDBCfg {
6
+ hashKey: string;
7
+ }
8
+ export declare class RedisHashKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
9
+ client: RedisClient;
10
+ keyOfHashField: string;
11
+ constructor(cfg: RedisHashKeyValueDBCfg);
12
+ ping(): Promise<void>;
13
+ [Symbol.asyncDispose](): Promise<void>;
14
+ getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]>;
15
+ deleteByIds(table: string, ids: string[]): Promise<void>;
16
+ saveBatch(table: string, entries: KeyValueDBTuple[], opt?: CommonKeyValueDBSaveBatchOptions): Promise<void>;
17
+ streamIds(table: string, limit?: number): ReadableTyped<string>;
18
+ streamValues(table: string, limit?: number): ReadableTyped<Buffer>;
19
+ streamEntries(table: string, limit?: number | undefined): ReadableTyped<KeyValueDBTuple>;
20
+ count(table: string): Promise<number>;
21
+ increment(table: string, id: string, by?: number): Promise<number>;
22
+ createTable(table: string, opt?: CommonDBCreateOptions): Promise<void>;
23
+ private idsToKeys;
24
+ private idToKey;
25
+ private keysToIds;
26
+ private keyToId;
27
+ }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisHashKeyValueDB = void 0;
4
+ const js_lib_1 = require("@naturalcycles/js-lib");
5
+ class RedisHashKeyValueDB {
6
+ constructor(cfg) {
7
+ this.client = cfg.client;
8
+ this.keyOfHashField = cfg.hashKey;
9
+ }
10
+ async ping() {
11
+ await this.client.ping();
12
+ }
13
+ async [Symbol.asyncDispose]() {
14
+ await this.client.disconnect();
15
+ }
16
+ async getByIds(table, ids) {
17
+ if (!ids.length)
18
+ return [];
19
+ // we assume that the order of returned values is the same as order of input ids
20
+ const bufs = await this.client.hmgetBuffer(this.keyOfHashField, this.idsToKeys(table, ids));
21
+ return bufs.map((buf, i) => [ids[i], buf]).filter(([_k, v]) => v !== null);
22
+ }
23
+ async deleteByIds(table, ids) {
24
+ if (!ids.length)
25
+ return;
26
+ await this.client.hdel(this.keyOfHashField, this.idsToKeys(table, ids));
27
+ }
28
+ async saveBatch(table, entries, opt) {
29
+ if (!entries.length)
30
+ return;
31
+ const entriesWithKey = entries.map(([k, v]) => [this.idToKey(table, k), v]);
32
+ const map = Object.fromEntries(entriesWithKey);
33
+ if (opt?.expireAt) {
34
+ await this.client.hsetWithTTL(this.keyOfHashField, map, opt.expireAt);
35
+ }
36
+ else {
37
+ await this.client.hset(this.keyOfHashField, map);
38
+ }
39
+ }
40
+ streamIds(table, limit) {
41
+ let stream = this.client
42
+ .hscanStream(this.keyOfHashField, {
43
+ match: `${table}:*`,
44
+ })
45
+ .flatMap(keyValueList => {
46
+ const keys = [];
47
+ keyValueList.forEach((keyOrValue, index) => {
48
+ if (index % 2 !== 0)
49
+ return;
50
+ keys.push(keyOrValue);
51
+ });
52
+ return this.keysToIds(table, keys);
53
+ });
54
+ if (limit) {
55
+ stream = stream.take(limit);
56
+ }
57
+ return stream;
58
+ }
59
+ streamValues(table, limit) {
60
+ return this.client
61
+ .hscanStream(this.keyOfHashField, {
62
+ match: `${table}:*`,
63
+ })
64
+ .flatMap(keyValueList => {
65
+ const values = [];
66
+ keyValueList.forEach((keyOrValue, index) => {
67
+ if (index % 2 !== 1)
68
+ return;
69
+ values.push(keyOrValue);
70
+ });
71
+ return values.map(v => Buffer.from(v));
72
+ })
73
+ .take(limit || Infinity);
74
+ }
75
+ streamEntries(table, limit) {
76
+ return this.client
77
+ .hscanStream(this.keyOfHashField, {
78
+ match: `${table}:*`,
79
+ })
80
+ .flatMap(keyValueList => {
81
+ const entries = (0, js_lib_1._chunk)(keyValueList, 2);
82
+ return entries.map(([k, v]) => {
83
+ return [this.keyToId(table, String(k)), Buffer.from(String(v))];
84
+ });
85
+ })
86
+ .take(limit || Infinity);
87
+ }
88
+ async count(table) {
89
+ return await this.client.hscanCount(this.keyOfHashField, {
90
+ match: `${table}:*`,
91
+ });
92
+ }
93
+ async increment(table, id, by = 1) {
94
+ return await this.client.hincr(this.keyOfHashField, this.idToKey(table, id), by);
95
+ }
96
+ async createTable(table, opt) {
97
+ if (!opt?.dropIfExists)
98
+ return;
99
+ await this.client.dropTable(table);
100
+ }
101
+ idsToKeys(table, ids) {
102
+ return ids.map(id => this.idToKey(table, id));
103
+ }
104
+ idToKey(table, id) {
105
+ return `${table}:${id}`;
106
+ }
107
+ keysToIds(table, keys) {
108
+ return keys.map(key => this.keyToId(table, key));
109
+ }
110
+ keyToId(table, key) {
111
+ return key.slice(table.length + 1);
112
+ }
113
+ }
114
+ exports.RedisHashKeyValueDB = RedisHashKeyValueDB;
@@ -1,4 +1,3 @@
1
- /// <reference types="node" />
2
1
  import { CommonKeyValueDBSaveBatchOptions, CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib';
3
2
  import { ReadableTyped } from '@naturalcycles/nodejs-lib';
4
3
  import { RedisClient } from './redisClient';
@@ -17,6 +16,7 @@ export declare class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposabl
17
16
  streamValues(table: string, limit?: number): ReadableTyped<Buffer>;
18
17
  streamEntries(table: string, limit?: number | undefined): ReadableTyped<KeyValueDBTuple>;
19
18
  count(table: string): Promise<number>;
19
+ increment(table: string, id: string, by?: number): Promise<number>;
20
20
  createTable(table: string, opt?: CommonDBCreateOptions): Promise<void>;
21
21
  private idsToKeys;
22
22
  private idToKey;
@@ -85,6 +85,9 @@ class RedisKeyValueDB {
85
85
  match: `${table}:*`,
86
86
  });
87
87
  }
88
+ async increment(table, id, by = 1) {
89
+ return await this.client.incr(this.idToKey(table, id), by);
90
+ }
88
91
  async createTable(table, opt) {
89
92
  if (!opt?.dropIfExists)
90
93
  return;
package/package.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "engines": {
36
36
  "node": ">=18.12"
37
37
  },
38
- "version": "3.0.1",
38
+ "version": "3.1.0",
39
39
  "description": "Redis implementation of CommonKeyValueDB interface",
40
40
  "author": "Natural Cycles Team",
41
41
  "license": "MIT"
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './redisClient'
2
2
  export * from './redisKeyValueDB'
3
+ export * from './redisHashKeyValueDB'
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AnyObject,
2
3
  CommonLogger,
3
4
  NullableBuffer,
4
5
  NullableString,
@@ -124,6 +125,38 @@ export class RedisClient implements CommonClient {
124
125
  await this.redis().set(key, value)
125
126
  }
126
127
 
128
+ async hgetall<T extends Record<string, string> = Record<string, string>>(
129
+ key: string,
130
+ ): Promise<T | null> {
131
+ const result = await this.redis().hgetall(key)
132
+ if (Object.keys(result).length === 0) return null
133
+ return result as T
134
+ }
135
+
136
+ async hget(key: string, field: string): Promise<NullableString> {
137
+ return await this.redis().hget(key, field)
138
+ }
139
+
140
+ async hset(key: string, value: AnyObject): Promise<void> {
141
+ await this.redis().hset(key, value)
142
+ }
143
+
144
+ async hdel(key: string, fields: string[]): Promise<void> {
145
+ await this.redis().hdel(key, ...fields)
146
+ }
147
+
148
+ async hmget(key: string, fields: string[]): Promise<NullableString[]> {
149
+ return await this.redis().hmget(key, ...fields)
150
+ }
151
+
152
+ async hmgetBuffer(key: string, fields: string[]): Promise<NullableBuffer[]> {
153
+ return await this.redis().hmgetBuffer(key, ...fields)
154
+ }
155
+
156
+ async hincr(key: string, field: string, increment: number = 1): Promise<number> {
157
+ return await this.redis().hincrby(key, field, increment)
158
+ }
159
+
127
160
  async setWithTTL(
128
161
  key: string,
129
162
  value: string | number | Buffer,
@@ -132,6 +165,16 @@ export class RedisClient implements CommonClient {
132
165
  await this.redis().set(key, value, 'EXAT', expireAt)
133
166
  }
134
167
 
168
+ async hsetWithTTL(key: string, value: AnyObject, expireAt: UnixTimestampNumber): Promise<void> {
169
+ const valueKeys = Object.keys(value)
170
+ const numberOfKeys = valueKeys.length
171
+ const keyList = valueKeys.join(' ')
172
+ const commandString = `HEXPIREAT ${key} ${expireAt} FIELDS ${numberOfKeys} ${keyList}`
173
+ const [command, ...args] = commandString.split(' ')
174
+ await this.redis().hset(key, value)
175
+ await this.redis().call(command!, args)
176
+ }
177
+
135
178
  async mset(obj: Record<string, string | number>): Promise<void> {
136
179
  await this.redis().mset(obj)
137
180
  }
@@ -140,8 +183,8 @@ export class RedisClient implements CommonClient {
140
183
  await this.redis().mset(obj)
141
184
  }
142
185
 
143
- async incr(key: string): Promise<number> {
144
- return await this.redis().incr(key)
186
+ async incr(key: string, by: number = 1): Promise<number> {
187
+ return await this.redis().incrby(key, by)
145
188
  }
146
189
 
147
190
  async ttl(key: string): Promise<number> {
@@ -205,6 +248,22 @@ export class RedisClient implements CommonClient {
205
248
  return count
206
249
  }
207
250
 
251
+ hscanStream(key: string, opt: ScanStreamOptions): ReadableTyped<string[]> {
252
+ return this.redis().hscanStream(key, opt)
253
+ }
254
+
255
+ async hscanCount(key: string, opt: ScanStreamOptions): Promise<number> {
256
+ let count = 0
257
+
258
+ const stream = this.redis().hscanStream(key, opt)
259
+
260
+ await stream.forEach((keyValueList: string[]) => {
261
+ count += keyValueList.length / 2
262
+ })
263
+
264
+ return count
265
+ }
266
+
208
267
  async withPipeline(fn: (pipeline: ChainableCommander) => Promisable<void>): Promise<void> {
209
268
  const pipeline = this.redis().pipeline()
210
269
  await fn(pipeline)
@@ -0,0 +1,144 @@
1
+ import {
2
+ CommonKeyValueDBSaveBatchOptions,
3
+ CommonDBCreateOptions,
4
+ CommonKeyValueDB,
5
+ KeyValueDBTuple,
6
+ } from '@naturalcycles/db-lib'
7
+ import { _chunk, StringMap } from '@naturalcycles/js-lib'
8
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib'
9
+ import { RedisClient } from './redisClient'
10
+ import { RedisKeyValueDBCfg } from './redisKeyValueDB'
11
+
12
+ export interface RedisHashKeyValueDBCfg extends RedisKeyValueDBCfg {
13
+ hashKey: string
14
+ }
15
+
16
+ export class RedisHashKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
17
+ client: RedisClient
18
+ keyOfHashField: string
19
+
20
+ constructor(cfg: RedisHashKeyValueDBCfg) {
21
+ this.client = cfg.client
22
+ this.keyOfHashField = cfg.hashKey
23
+ }
24
+
25
+ async ping(): Promise<void> {
26
+ await this.client.ping()
27
+ }
28
+
29
+ async [Symbol.asyncDispose](): Promise<void> {
30
+ await this.client.disconnect()
31
+ }
32
+
33
+ async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
34
+ if (!ids.length) return []
35
+ // we assume that the order of returned values is the same as order of input ids
36
+ const bufs = await this.client.hmgetBuffer(this.keyOfHashField, this.idsToKeys(table, ids))
37
+ return bufs.map((buf, i) => [ids[i], buf] as KeyValueDBTuple).filter(([_k, v]) => v !== null)
38
+ }
39
+
40
+ async deleteByIds(table: string, ids: string[]): Promise<void> {
41
+ if (!ids.length) return
42
+ await this.client.hdel(this.keyOfHashField, this.idsToKeys(table, ids))
43
+ }
44
+
45
+ async saveBatch(
46
+ table: string,
47
+ entries: KeyValueDBTuple[],
48
+ opt?: CommonKeyValueDBSaveBatchOptions,
49
+ ): Promise<void> {
50
+ if (!entries.length) return
51
+
52
+ const entriesWithKey = entries.map(([k, v]) => [this.idToKey(table, k), v])
53
+ const map: StringMap<any> = Object.fromEntries(entriesWithKey)
54
+
55
+ if (opt?.expireAt) {
56
+ await this.client.hsetWithTTL(this.keyOfHashField, map, opt.expireAt)
57
+ } else {
58
+ await this.client.hset(this.keyOfHashField, map)
59
+ }
60
+ }
61
+
62
+ streamIds(table: string, limit?: number): ReadableTyped<string> {
63
+ let stream = this.client
64
+ .hscanStream(this.keyOfHashField, {
65
+ match: `${table}:*`,
66
+ })
67
+ .flatMap(keyValueList => {
68
+ const keys: string[] = []
69
+ keyValueList.forEach((keyOrValue, index) => {
70
+ if (index % 2 !== 0) return
71
+ keys.push(keyOrValue)
72
+ })
73
+ return this.keysToIds(table, keys)
74
+ })
75
+
76
+ if (limit) {
77
+ stream = stream.take(limit)
78
+ }
79
+
80
+ return stream
81
+ }
82
+
83
+ streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
84
+ return this.client
85
+ .hscanStream(this.keyOfHashField, {
86
+ match: `${table}:*`,
87
+ })
88
+ .flatMap(keyValueList => {
89
+ const values: string[] = []
90
+ keyValueList.forEach((keyOrValue, index) => {
91
+ if (index % 2 !== 1) return
92
+ values.push(keyOrValue)
93
+ })
94
+ return values.map(v => Buffer.from(v))
95
+ })
96
+ .take(limit || Infinity)
97
+ }
98
+
99
+ streamEntries(table: string, limit?: number | undefined): ReadableTyped<KeyValueDBTuple> {
100
+ return this.client
101
+ .hscanStream(this.keyOfHashField, {
102
+ match: `${table}:*`,
103
+ })
104
+ .flatMap(keyValueList => {
105
+ const entries = _chunk(keyValueList, 2)
106
+ return entries.map(([k, v]) => {
107
+ return [this.keyToId(table, String(k)), Buffer.from(String(v))] satisfies KeyValueDBTuple
108
+ })
109
+ })
110
+ .take(limit || Infinity)
111
+ }
112
+
113
+ async count(table: string): Promise<number> {
114
+ return await this.client.hscanCount(this.keyOfHashField, {
115
+ match: `${table}:*`,
116
+ })
117
+ }
118
+
119
+ async increment(table: string, id: string, by: number = 1): Promise<number> {
120
+ return await this.client.hincr(this.keyOfHashField, this.idToKey(table, id), by)
121
+ }
122
+
123
+ async createTable(table: string, opt?: CommonDBCreateOptions): Promise<void> {
124
+ if (!opt?.dropIfExists) return
125
+
126
+ await this.client.dropTable(table)
127
+ }
128
+
129
+ private idsToKeys(table: string, ids: string[]): string[] {
130
+ return ids.map(id => this.idToKey(table, id))
131
+ }
132
+
133
+ private idToKey(table: string, id: string): string {
134
+ return `${table}:${id}`
135
+ }
136
+
137
+ private keysToIds(table: string, keys: string[]): string[] {
138
+ return keys.map(key => this.keyToId(table, key))
139
+ }
140
+
141
+ private keyToId(table: string, key: string): string {
142
+ return key.slice(table.length + 1)
143
+ }
144
+ }
@@ -118,6 +118,10 @@ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
118
118
  })
119
119
  }
120
120
 
121
+ async increment(table: string, id: string, by: number = 1): Promise<number> {
122
+ return await this.client.incr(this.idToKey(table, id), by)
123
+ }
124
+
121
125
  async createTable(table: string, opt?: CommonDBCreateOptions): Promise<void> {
122
126
  if (!opt?.dropIfExists) return
123
127