@naturalcycles/redis-lib 2.0.0 → 3.0.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,2 @@
1
- import { RedisDB, RedisDBCfg } from './redis.db';
2
- export { RedisDBCfg, RedisDB };
1
+ export * from './redisClient';
2
+ export * from './redisKeyValueDB';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const redis_db_1 = require("./redis.db");
4
- exports.RedisDB = redis_db_1.RedisDB;
5
- //# sourceMappingURL=index.js.map
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./redisClient"), exports);
5
+ tslib_1.__exportStar(require("./redisKeyValueDB"), exports);
@@ -0,0 +1,63 @@
1
+ /// <reference types="node" />
2
+ import { CommonLogger, NullableBuffer, NullableString, Promisable, UnixTimestampNumber } from '@naturalcycles/js-lib';
3
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib';
4
+ import type { Redis, RedisOptions } from 'ioredis';
5
+ import type { ScanStreamOptions } from 'ioredis/built/types';
6
+ import type { ChainableCommander } from 'ioredis/built/utils/RedisCommander';
7
+ export interface CommonClient extends AsyncDisposable {
8
+ connected: boolean;
9
+ connect: () => Promise<void>;
10
+ disconnect: () => Promise<void>;
11
+ ping: () => Promise<void>;
12
+ }
13
+ export interface RedisClientCfg {
14
+ redisOptions?: RedisOptions;
15
+ /**
16
+ * Defaults to console.
17
+ */
18
+ logger?: CommonLogger;
19
+ }
20
+ /**
21
+ Wraps the redis sdk with unified interface.
22
+ Features:
23
+
24
+ - Lazy loading & initialization
25
+ - Reasonable defaults
26
+
27
+ */
28
+ export declare class RedisClient implements CommonClient {
29
+ constructor(cfg?: RedisClientCfg);
30
+ cfg: Required<RedisClientCfg>;
31
+ connected: boolean;
32
+ private _redis?;
33
+ redis(): Redis;
34
+ connect(): Promise<void>;
35
+ disconnect(): Promise<void>;
36
+ [Symbol.asyncDispose](): Promise<void>;
37
+ ping(): Promise<void>;
38
+ del(keys: string[]): Promise<number>;
39
+ get(key: string): Promise<NullableString>;
40
+ getBuffer(key: string): Promise<NullableBuffer>;
41
+ mget(keys: string[]): Promise<NullableString[]>;
42
+ mgetBuffer(keys: string[]): Promise<NullableBuffer[]>;
43
+ set(key: string, value: string | Buffer): Promise<void>;
44
+ setWithTTL(key: string, value: string | Buffer, expireAt: UnixTimestampNumber): Promise<void>;
45
+ mset(obj: Record<string, string>): Promise<void>;
46
+ msetBuffer(obj: Record<string, Buffer>): Promise<void>;
47
+ incr(key: string): Promise<number>;
48
+ ttl(key: string): Promise<number>;
49
+ dropTable(table: string): Promise<void>;
50
+ clearAll(): Promise<void>;
51
+ /**
52
+ Convenient type-safe wrapper.
53
+ Returns BATCHES of keys in each iteration (as-is).
54
+ */
55
+ scanStream(opt: ScanStreamOptions): ReadableTyped<string[]>;
56
+ /**
57
+ * Like scanStream, but flattens the stream of keys.
58
+ */
59
+ scanStreamFlat(opt: ScanStreamOptions): ReadableTyped<string>;
60
+ scanCount(opt: ScanStreamOptions): Promise<number>;
61
+ withPipeline(fn: (pipeline: ChainableCommander) => Promisable<void>): Promise<void>;
62
+ private log;
63
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisClient = void 0;
4
+ /**
5
+ Wraps the redis sdk with unified interface.
6
+ Features:
7
+
8
+ - Lazy loading & initialization
9
+ - Reasonable defaults
10
+
11
+ */
12
+ class RedisClient {
13
+ constructor(cfg = {}) {
14
+ this.connected = false;
15
+ this.cfg = {
16
+ logger: console,
17
+ ...cfg,
18
+ redisOptions: {
19
+ showFriendlyErrorStack: true,
20
+ lazyConnect: true,
21
+ ...cfg.redisOptions,
22
+ },
23
+ };
24
+ }
25
+ redis() {
26
+ if (this._redis)
27
+ return this._redis;
28
+ // lazy-load the library
29
+ const redisLib = require('ioredis');
30
+ const redis = new redisLib.Redis(this.cfg.redisOptions);
31
+ const { logger } = this.cfg;
32
+ const redisEvents = ['connect', 'close', 'reconnecting', 'end'];
33
+ redisEvents.forEach(e => redis.on(e, () => logger.log(`redis: ${e}`)));
34
+ const closeEvents = ['SIGINT', 'SIGTERM'];
35
+ closeEvents.forEach(e => process.once(e, () => redis.quit()));
36
+ redis.on('error', err => logger.error(err));
37
+ this.connected = true;
38
+ this._redis = redis;
39
+ this.log(`redis: created`);
40
+ return redis;
41
+ }
42
+ async connect() {
43
+ if (!this.connected) {
44
+ await this.redis().connect();
45
+ this.connected = true;
46
+ }
47
+ }
48
+ async disconnect() {
49
+ this.log('redis: quit...');
50
+ this.log(`redis: quit`, await this.redis().quit());
51
+ this.connected = false;
52
+ }
53
+ async [Symbol.asyncDispose]() {
54
+ await this.disconnect();
55
+ }
56
+ async ping() {
57
+ await this.redis().ping();
58
+ }
59
+ async del(keys) {
60
+ return await this.redis().del(keys);
61
+ }
62
+ async get(key) {
63
+ return await this.redis().get(key);
64
+ }
65
+ async getBuffer(key) {
66
+ return await this.redis().getBuffer(key);
67
+ }
68
+ async mget(keys) {
69
+ return await this.redis().mget(keys);
70
+ }
71
+ async mgetBuffer(keys) {
72
+ return await this.redis().mgetBuffer(keys);
73
+ }
74
+ async set(key, value) {
75
+ await this.redis().set(key, value);
76
+ }
77
+ async setWithTTL(key, value, expireAt) {
78
+ await this.redis().set(key, value, 'EXAT', expireAt);
79
+ }
80
+ async mset(obj) {
81
+ await this.redis().mset(obj);
82
+ }
83
+ async msetBuffer(obj) {
84
+ await this.redis().mset(obj);
85
+ }
86
+ async incr(key) {
87
+ return await this.redis().incr(key);
88
+ }
89
+ async ttl(key) {
90
+ return await this.redis().ttl(key);
91
+ }
92
+ async dropTable(table) {
93
+ let count = 0;
94
+ await this.withPipeline(async (pipeline) => {
95
+ await this.scanStream({
96
+ match: `${table}:*`,
97
+ }).forEach(keys => {
98
+ pipeline.del(keys);
99
+ count += keys.length;
100
+ });
101
+ });
102
+ this.log(`redis: dropped table ${table} (${count} keys)`);
103
+ }
104
+ async clearAll() {
105
+ this.log(`redis: clearAll...`);
106
+ let count = 0;
107
+ await this.withPipeline(async (pipeline) => {
108
+ await this.scanStream({
109
+ match: `*`,
110
+ }).forEach(keys => {
111
+ pipeline.del(keys);
112
+ count += keys.length;
113
+ });
114
+ });
115
+ this.log(`redis: clearAll removed ${count} keys`);
116
+ }
117
+ /**
118
+ Convenient type-safe wrapper.
119
+ Returns BATCHES of keys in each iteration (as-is).
120
+ */
121
+ scanStream(opt) {
122
+ return this.redis().scanStream(opt);
123
+ }
124
+ /**
125
+ * Like scanStream, but flattens the stream of keys.
126
+ */
127
+ scanStreamFlat(opt) {
128
+ return this.redis().scanStream(opt).flatMap(keys => keys);
129
+ }
130
+ async scanCount(opt) {
131
+ // todo: implement more efficiently, e.g via LUA?
132
+ let count = 0;
133
+ await this.redis().scanStream(opt).forEach(keys => {
134
+ count += keys.length;
135
+ });
136
+ return count;
137
+ }
138
+ async withPipeline(fn) {
139
+ const pipeline = this.redis().pipeline();
140
+ await fn(pipeline);
141
+ await pipeline.exec();
142
+ }
143
+ log(...args) {
144
+ this.cfg.logger.log(...args);
145
+ }
146
+ }
147
+ exports.RedisClient = RedisClient;
@@ -0,0 +1,25 @@
1
+ /// <reference types="node" />
2
+ import { CommonKeyValueDBSaveBatchOptions, CommonDBCreateOptions, CommonKeyValueDB, KeyValueDBTuple } from '@naturalcycles/db-lib';
3
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib';
4
+ import { RedisClient } from './redisClient';
5
+ export interface RedisKeyValueDBCfg {
6
+ client: RedisClient;
7
+ }
8
+ export declare class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
9
+ constructor(cfg: RedisKeyValueDBCfg);
10
+ client: RedisClient;
11
+ ping(): Promise<void>;
12
+ [Symbol.asyncDispose](): Promise<void>;
13
+ getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]>;
14
+ deleteByIds(table: string, ids: string[]): Promise<void>;
15
+ saveBatch(table: string, entries: KeyValueDBTuple[], opt?: CommonKeyValueDBSaveBatchOptions): Promise<void>;
16
+ streamIds(table: string, limit?: number): ReadableTyped<string>;
17
+ streamValues(table: string, limit?: number): ReadableTyped<Buffer>;
18
+ streamEntries(table: string, limit?: number | undefined): ReadableTyped<KeyValueDBTuple>;
19
+ count(table: string): Promise<number>;
20
+ createTable(table: string, opt?: CommonDBCreateOptions): Promise<void>;
21
+ private idsToKeys;
22
+ private idToKey;
23
+ private keysToIds;
24
+ private keyToId;
25
+ }
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisKeyValueDB = void 0;
4
+ const js_lib_1 = require("@naturalcycles/js-lib");
5
+ class RedisKeyValueDB {
6
+ constructor(cfg) {
7
+ this.client = cfg.client;
8
+ }
9
+ async ping() {
10
+ await this.client.ping();
11
+ }
12
+ async [Symbol.asyncDispose]() {
13
+ await this.client.disconnect();
14
+ }
15
+ async getByIds(table, ids) {
16
+ if (!ids.length)
17
+ return [];
18
+ // we assume that the order of returned values is the same as order of input ids
19
+ const bufs = await this.client.mgetBuffer(this.idsToKeys(table, ids));
20
+ return bufs.map((buf, i) => [ids[i], buf]).filter(([_k, v]) => v !== null);
21
+ }
22
+ async deleteByIds(table, ids) {
23
+ if (!ids.length)
24
+ return;
25
+ await this.client.del(this.idsToKeys(table, ids));
26
+ }
27
+ async saveBatch(table, entries, opt) {
28
+ if (!entries.length)
29
+ return;
30
+ if (opt?.expireAt) {
31
+ // There's no supported mset with TTL: https://stackoverflow.com/questions/16423342/redis-multi-set-with-a-ttl
32
+ // so we gonna use a pipeline instead
33
+ await this.client.withPipeline(pipeline => {
34
+ for (const [k, v] of entries) {
35
+ pipeline.set(this.idToKey(table, k), v, 'EXAT', opt.expireAt);
36
+ }
37
+ });
38
+ }
39
+ else {
40
+ const obj = Object.fromEntries(entries.map(([k, v]) => [this.idToKey(table, k), v]));
41
+ await this.client.msetBuffer(obj);
42
+ }
43
+ }
44
+ streamIds(table, limit) {
45
+ let stream = this.client
46
+ .scanStream({
47
+ match: `${table}:*`,
48
+ // count: limit, // count is actually a "batchSize", not a limit
49
+ })
50
+ .flatMap(keys => this.keysToIds(table, keys));
51
+ if (limit) {
52
+ stream = stream.take(limit);
53
+ }
54
+ return stream;
55
+ }
56
+ streamValues(table, limit) {
57
+ return this.client
58
+ .scanStream({
59
+ match: `${table}:*`,
60
+ })
61
+ .flatMap(async (keys) => {
62
+ return (await this.client.mgetBuffer(keys)).filter(js_lib_1._isTruthy);
63
+ }, {
64
+ concurrency: 16,
65
+ })
66
+ .take(limit || Infinity);
67
+ }
68
+ streamEntries(table, limit) {
69
+ return this.client
70
+ .scanStream({
71
+ match: `${table}:*`,
72
+ })
73
+ .flatMap(async (keys) => {
74
+ // casting as Buffer[], because values are expected to exist for given keys
75
+ const bufs = (await this.client.mgetBuffer(keys));
76
+ return (0, js_lib_1._zip)(this.keysToIds(table, keys), bufs);
77
+ }, {
78
+ concurrency: 16,
79
+ })
80
+ .take(limit || Infinity);
81
+ }
82
+ async count(table) {
83
+ // todo: implement more efficiently, e.g via LUA?
84
+ return await this.client.scanCount({
85
+ match: `${table}:*`,
86
+ });
87
+ }
88
+ async createTable(table, opt) {
89
+ if (!opt?.dropIfExists)
90
+ return;
91
+ await this.client.dropTable(table);
92
+ }
93
+ idsToKeys(table, ids) {
94
+ return ids.map(id => this.idToKey(table, id));
95
+ }
96
+ idToKey(table, id) {
97
+ return `${table}:${id}`;
98
+ }
99
+ keysToIds(table, keys) {
100
+ return keys.map(key => this.keyToId(table, key));
101
+ }
102
+ keyToId(table, key) {
103
+ return key.slice(table.length + 1);
104
+ }
105
+ }
106
+ exports.RedisKeyValueDB = RedisKeyValueDB;
package/package.json CHANGED
@@ -1,22 +1,18 @@
1
1
  {
2
2
  "name": "@naturalcycles/redis-lib",
3
- "scripts": {},
3
+ "scripts": {
4
+ "prepare": "husky"
5
+ },
4
6
  "dependencies": {
5
- "@naturalcycles/db-lib": "^2.0.2",
6
- "@naturalcycles/js-lib": "^8.5.1",
7
- "@naturalcycles/nodejs-lib": "^6.11.1",
8
- "@types/ioredis": "^4.0.15",
9
- "chalk": "^2.4.2",
10
- "ioredis": "^4.14.0"
7
+ "@naturalcycles/db-lib": "^9.9.2",
8
+ "@naturalcycles/js-lib": "^14.217.0",
9
+ "@naturalcycles/nodejs-lib": "^13.8.0",
10
+ "ioredis": "^5.3.2"
11
11
  },
12
12
  "devDependencies": {
13
- "@naturalcycles/dev-lib": "^8.1.0",
14
- "@types/node": "^12.7.2",
15
- "dotenv": "^8.1.0",
16
- "jest": "^24.9.0"
17
- },
18
- "resolutions": {
19
- "@types/hapi__joi": "15.0.4"
13
+ "@naturalcycles/dev-lib": "^13.49.2",
14
+ "@types/node": "^20.12.2",
15
+ "jest": "^29.7.0"
20
16
  },
21
17
  "files": [
22
18
  "dist",
@@ -29,6 +25,7 @@
29
25
  "main": "dist/index.js",
30
26
  "types": "dist/index.d.ts",
31
27
  "publishConfig": {
28
+ "provenance": true,
32
29
  "access": "public"
33
30
  },
34
31
  "repository": {
@@ -36,10 +33,10 @@
36
33
  "url": "https://github.com/NaturalCycles/redis-lib"
37
34
  },
38
35
  "engines": {
39
- "node": ">=10.13"
36
+ "node": ">=18.12"
40
37
  },
41
- "version": "2.0.0",
42
- "description": "Redis implementation of CommonDB interface",
38
+ "version": "3.0.0",
39
+ "description": "Redis implementation of CommonKeyValueDB interface",
43
40
  "author": "Natural Cycles Team",
44
41
  "license": "MIT"
45
42
  }
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ## @naturalcycles/redis-lib
2
2
 
3
- > Redis implementation of CommonDB interface
3
+ > Redis implementation of CommonKeyValueDB interface
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@naturalcycles/redis-lib/latest.svg)](https://www.npmjs.com/package/@naturalcycles/redis-lib)
6
6
  [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)
@@ -9,28 +9,22 @@
9
9
 
10
10
  - ...
11
11
 
12
- # DEBUG namespaces
13
-
14
- - `nc:redis-lib`
15
- - `ioredis`, `ioredis:redis`, `ioredis:connection`
16
-
17
- # Packaging
18
-
19
- - `engines.node >= 10.13`: Latest Node.js LTS
20
- - `main: dist/index.js`: commonjs, es2018
21
- - `types: dist/index.d.ts`: typescript types
22
- - `/src` folder with source `*.ts` files included
23
-
24
12
  # Starting / debugging Redis server on OSX
25
13
 
26
- brew install redis
27
- brew services start redis
28
- brew services stop redis
14
+ ```shell
15
+ brew install redis
16
+ brew services start redis
17
+ brew services stop redis
18
+
19
+ redis-server /usr/local/etc/redis.conf
29
20
 
30
- redis-server /usr/local/etc/redis.conf
21
+ redis-cli ping
22
+ redis-cli flushall
31
23
 
32
- redis-cli ping
33
- redis-cli flushall
24
+ # connect and list all keys
25
+ redis-cli
26
+ scan 0
27
+ ```
34
28
 
35
29
  Location and size of local DB:
36
30
 
package/src/index.ts CHANGED
@@ -1,3 +1,2 @@
1
- import { RedisDB, RedisDBCfg } from './redis.db'
2
-
3
- export { RedisDBCfg, RedisDB }
1
+ export * from './redisClient'
2
+ export * from './redisKeyValueDB'
@@ -0,0 +1,217 @@
1
+ import {
2
+ CommonLogger,
3
+ NullableBuffer,
4
+ NullableString,
5
+ Promisable,
6
+ UnixTimestampNumber,
7
+ } from '@naturalcycles/js-lib'
8
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib'
9
+ // eslint-disable-next-line import/no-duplicates
10
+ import type { Redis, RedisOptions } from 'ioredis'
11
+ // eslint-disable-next-line import/no-duplicates
12
+ import type * as RedisLib from 'ioredis'
13
+ import type { ScanStreamOptions } from 'ioredis/built/types'
14
+ import type { ChainableCommander } from 'ioredis/built/utils/RedisCommander'
15
+
16
+ export interface CommonClient extends AsyncDisposable {
17
+ connected: boolean
18
+ connect: () => Promise<void>
19
+ disconnect: () => Promise<void>
20
+ ping: () => Promise<void>
21
+ }
22
+
23
+ export interface RedisClientCfg {
24
+ redisOptions?: RedisOptions
25
+
26
+ /**
27
+ * Defaults to console.
28
+ */
29
+ logger?: CommonLogger
30
+ }
31
+
32
+ /**
33
+ Wraps the redis sdk with unified interface.
34
+ Features:
35
+
36
+ - Lazy loading & initialization
37
+ - Reasonable defaults
38
+
39
+ */
40
+ export class RedisClient implements CommonClient {
41
+ constructor(cfg: RedisClientCfg = {}) {
42
+ this.cfg = {
43
+ logger: console,
44
+ ...cfg,
45
+ redisOptions: {
46
+ showFriendlyErrorStack: true,
47
+ lazyConnect: true,
48
+ ...cfg.redisOptions,
49
+ },
50
+ }
51
+ }
52
+
53
+ cfg!: Required<RedisClientCfg>
54
+
55
+ connected = false
56
+
57
+ private _redis?: Redis
58
+
59
+ redis(): Redis {
60
+ if (this._redis) return this._redis
61
+
62
+ // lazy-load the library
63
+ const redisLib = require('ioredis') as typeof RedisLib
64
+ const redis = new redisLib.Redis(this.cfg.redisOptions)
65
+
66
+ const { logger } = this.cfg
67
+
68
+ const redisEvents = ['connect', 'close', 'reconnecting', 'end']
69
+ redisEvents.forEach(e => redis.on(e, () => logger.log(`redis: ${e}`)))
70
+
71
+ const closeEvents: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']
72
+ closeEvents.forEach(e => process.once(e, () => redis.quit()))
73
+
74
+ redis.on('error', err => logger.error(err))
75
+
76
+ this.connected = true
77
+ this._redis = redis
78
+ this.log(`redis: created`)
79
+ return redis
80
+ }
81
+
82
+ async connect(): Promise<void> {
83
+ if (!this.connected) {
84
+ await this.redis().connect()
85
+ this.connected = true
86
+ }
87
+ }
88
+
89
+ async disconnect(): Promise<void> {
90
+ this.log('redis: quit...')
91
+ this.log(`redis: quit`, await this.redis().quit())
92
+ this.connected = false
93
+ }
94
+
95
+ async [Symbol.asyncDispose](): Promise<void> {
96
+ await this.disconnect()
97
+ }
98
+
99
+ async ping(): Promise<void> {
100
+ await this.redis().ping()
101
+ }
102
+
103
+ async del(keys: string[]): Promise<number> {
104
+ return await this.redis().del(keys)
105
+ }
106
+
107
+ async get(key: string): Promise<NullableString> {
108
+ return await this.redis().get(key)
109
+ }
110
+
111
+ async getBuffer(key: string): Promise<NullableBuffer> {
112
+ return await this.redis().getBuffer(key)
113
+ }
114
+
115
+ async mget(keys: string[]): Promise<NullableString[]> {
116
+ return await this.redis().mget(keys)
117
+ }
118
+
119
+ async mgetBuffer(keys: string[]): Promise<NullableBuffer[]> {
120
+ return await this.redis().mgetBuffer(keys)
121
+ }
122
+
123
+ async set(key: string, value: string | Buffer): Promise<void> {
124
+ await this.redis().set(key, value)
125
+ }
126
+
127
+ async setWithTTL(
128
+ key: string,
129
+ value: string | Buffer,
130
+ expireAt: UnixTimestampNumber,
131
+ ): Promise<void> {
132
+ await this.redis().set(key, value, 'EXAT', expireAt)
133
+ }
134
+
135
+ async mset(obj: Record<string, string>): Promise<void> {
136
+ await this.redis().mset(obj)
137
+ }
138
+
139
+ async msetBuffer(obj: Record<string, Buffer>): Promise<void> {
140
+ await this.redis().mset(obj)
141
+ }
142
+
143
+ async incr(key: string): Promise<number> {
144
+ return await this.redis().incr(key)
145
+ }
146
+
147
+ async ttl(key: string): Promise<number> {
148
+ return await this.redis().ttl(key)
149
+ }
150
+
151
+ async dropTable(table: string): Promise<void> {
152
+ let count = 0
153
+
154
+ await this.withPipeline(async pipeline => {
155
+ await this.scanStream({
156
+ match: `${table}:*`,
157
+ }).forEach(keys => {
158
+ pipeline.del(keys)
159
+ count += keys.length
160
+ })
161
+ })
162
+
163
+ this.log(`redis: dropped table ${table} (${count} keys)`)
164
+ }
165
+
166
+ async clearAll(): Promise<void> {
167
+ this.log(`redis: clearAll...`)
168
+ let count = 0
169
+
170
+ await this.withPipeline(async pipeline => {
171
+ await this.scanStream({
172
+ match: `*`,
173
+ }).forEach(keys => {
174
+ pipeline.del(keys)
175
+ count += keys.length
176
+ })
177
+ })
178
+
179
+ this.log(`redis: clearAll removed ${count} keys`)
180
+ }
181
+
182
+ /**
183
+ Convenient type-safe wrapper.
184
+ Returns BATCHES of keys in each iteration (as-is).
185
+ */
186
+ scanStream(opt: ScanStreamOptions): ReadableTyped<string[]> {
187
+ return this.redis().scanStream(opt)
188
+ }
189
+
190
+ /**
191
+ * Like scanStream, but flattens the stream of keys.
192
+ */
193
+ scanStreamFlat(opt: ScanStreamOptions): ReadableTyped<string> {
194
+ return (this.redis().scanStream(opt) as ReadableTyped<string[]>).flatMap(keys => keys)
195
+ }
196
+
197
+ async scanCount(opt: ScanStreamOptions): Promise<number> {
198
+ // todo: implement more efficiently, e.g via LUA?
199
+ let count = 0
200
+
201
+ await (this.redis().scanStream(opt) as ReadableTyped<string[]>).forEach(keys => {
202
+ count += keys.length
203
+ })
204
+
205
+ return count
206
+ }
207
+
208
+ async withPipeline(fn: (pipeline: ChainableCommander) => Promisable<void>): Promise<void> {
209
+ const pipeline = this.redis().pipeline()
210
+ await fn(pipeline)
211
+ await pipeline.exec()
212
+ }
213
+
214
+ private log(...args: any[]): void {
215
+ this.cfg.logger.log(...args)
216
+ }
217
+ }
@@ -0,0 +1,142 @@
1
+ import {
2
+ CommonKeyValueDBSaveBatchOptions,
3
+ CommonDBCreateOptions,
4
+ CommonKeyValueDB,
5
+ KeyValueDBTuple,
6
+ } from '@naturalcycles/db-lib'
7
+ import { _isTruthy, _zip } from '@naturalcycles/js-lib'
8
+ import { ReadableTyped } from '@naturalcycles/nodejs-lib'
9
+ import { RedisClient } from './redisClient'
10
+
11
+ export interface RedisKeyValueDBCfg {
12
+ client: RedisClient
13
+ }
14
+
15
+ export class RedisKeyValueDB implements CommonKeyValueDB, AsyncDisposable {
16
+ constructor(cfg: RedisKeyValueDBCfg) {
17
+ this.client = cfg.client
18
+ }
19
+
20
+ client: RedisClient
21
+
22
+ async ping(): Promise<void> {
23
+ await this.client.ping()
24
+ }
25
+
26
+ async [Symbol.asyncDispose](): Promise<void> {
27
+ await this.client.disconnect()
28
+ }
29
+
30
+ async getByIds(table: string, ids: string[]): Promise<KeyValueDBTuple[]> {
31
+ if (!ids.length) return []
32
+ // we assume that the order of returned values is the same as order of input ids
33
+ const bufs = await this.client.mgetBuffer(this.idsToKeys(table, ids))
34
+ return bufs.map((buf, i) => [ids[i], buf] as KeyValueDBTuple).filter(([_k, v]) => v !== null)
35
+ }
36
+
37
+ async deleteByIds(table: string, ids: string[]): Promise<void> {
38
+ if (!ids.length) return
39
+ await this.client.del(this.idsToKeys(table, ids))
40
+ }
41
+
42
+ async saveBatch(
43
+ table: string,
44
+ entries: KeyValueDBTuple[],
45
+ opt?: CommonKeyValueDBSaveBatchOptions,
46
+ ): Promise<void> {
47
+ if (!entries.length) return
48
+
49
+ if (opt?.expireAt) {
50
+ // There's no supported mset with TTL: https://stackoverflow.com/questions/16423342/redis-multi-set-with-a-ttl
51
+ // so we gonna use a pipeline instead
52
+ await this.client.withPipeline(pipeline => {
53
+ for (const [k, v] of entries) {
54
+ pipeline.set(this.idToKey(table, k), v, 'EXAT', opt.expireAt!)
55
+ }
56
+ })
57
+ } else {
58
+ const obj: Record<string, Buffer> = Object.fromEntries(
59
+ entries.map(([k, v]) => [this.idToKey(table, k), v]) as KeyValueDBTuple[],
60
+ )
61
+ await this.client.msetBuffer(obj)
62
+ }
63
+ }
64
+
65
+ streamIds(table: string, limit?: number): ReadableTyped<string> {
66
+ let stream = this.client
67
+ .scanStream({
68
+ match: `${table}:*`,
69
+ // count: limit, // count is actually a "batchSize", not a limit
70
+ })
71
+ .flatMap(keys => this.keysToIds(table, keys))
72
+
73
+ if (limit) {
74
+ stream = stream.take(limit)
75
+ }
76
+
77
+ return stream
78
+ }
79
+
80
+ streamValues(table: string, limit?: number): ReadableTyped<Buffer> {
81
+ return this.client
82
+ .scanStream({
83
+ match: `${table}:*`,
84
+ })
85
+ .flatMap(
86
+ async keys => {
87
+ return (await this.client.mgetBuffer(keys)).filter(_isTruthy)
88
+ },
89
+ {
90
+ concurrency: 16,
91
+ },
92
+ )
93
+ .take(limit || Infinity)
94
+ }
95
+
96
+ streamEntries(table: string, limit?: number | undefined): ReadableTyped<KeyValueDBTuple> {
97
+ return this.client
98
+ .scanStream({
99
+ match: `${table}:*`,
100
+ })
101
+ .flatMap(
102
+ async keys => {
103
+ // casting as Buffer[], because values are expected to exist for given keys
104
+ const bufs = (await this.client.mgetBuffer(keys)) as Buffer[]
105
+ return _zip(this.keysToIds(table, keys), bufs)
106
+ },
107
+ {
108
+ concurrency: 16,
109
+ },
110
+ )
111
+ .take(limit || Infinity)
112
+ }
113
+
114
+ async count(table: string): Promise<number> {
115
+ // todo: implement more efficiently, e.g via LUA?
116
+ return await this.client.scanCount({
117
+ match: `${table}:*`,
118
+ })
119
+ }
120
+
121
+ async createTable(table: string, opt?: CommonDBCreateOptions): Promise<void> {
122
+ if (!opt?.dropIfExists) return
123
+
124
+ await this.client.dropTable(table)
125
+ }
126
+
127
+ private idsToKeys(table: string, ids: string[]): string[] {
128
+ return ids.map(id => this.idToKey(table, id))
129
+ }
130
+
131
+ private idToKey(table: string, id: string): string {
132
+ return `${table}:${id}`
133
+ }
134
+
135
+ private keysToIds(table: string, keys: string[]): string[] {
136
+ return keys.map(key => this.keyToId(table, key))
137
+ }
138
+
139
+ private keyToId(table: string, key: string): string {
140
+ return key.slice(table.length + 1)
141
+ }
142
+ }
package/CHANGELOG.md DELETED
@@ -1,32 +0,0 @@
1
- # [2.0.0](https://github.com/NaturalCycles/redis-lib/compare/v1.1.1...v2.0.0) (2019-10-19)
2
-
3
-
4
- ### Features
5
-
6
- * impl CommonDB 2.0 ([4830225](https://github.com/NaturalCycles/redis-lib/commit/4830225))
7
-
8
-
9
- ### BREAKING CHANGES
10
-
11
- * ^^^
12
-
13
- ## [1.1.1](https://github.com/NaturalCycles/redis-lib/compare/v1.1.0...v1.1.1) (2019-09-20)
14
-
15
-
16
- ### Bug Fixes
17
-
18
- * adapt to new db-lib ([d3555be](https://github.com/NaturalCycles/redis-lib/commit/d3555be))
19
-
20
- # [1.1.0](https://github.com/NaturalCycles/redis-lib/compare/v1.0.0...v1.1.0) (2019-08-23)
21
-
22
-
23
- ### Features
24
-
25
- * namespacePrefix; adopt to db-lib ([e3b35f4](https://github.com/NaturalCycles/redis-lib/commit/e3b35f4))
26
-
27
- # 1.0.0 (2019-08-20)
28
-
29
-
30
- ### Features
31
-
32
- * first version ([a80ee79](https://github.com/NaturalCycles/redis-lib/commit/a80ee79))
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAA,yCAAgD;AAE3B,kBAFZ,kBAAO,CAEY"}
@@ -1,45 +0,0 @@
1
- /// <reference types="node" />
2
- import { CommonDB, CommonDBOptions, CommonDBSaveOptions, DBQuery, RunQueryResult, SavedDBEntity } from '@naturalcycles/db-lib';
3
- import { RedisOptions } from 'ioredis';
4
- import * as Redis from 'ioredis';
5
- import { Readable } from 'stream';
6
- export interface RedisDBCfg {
7
- redisOptions?: RedisOptions;
8
- /**
9
- * If true - it will "emulate" queries by using SCAN $table_
10
- * which will load ALL table keys into memory and filter in-memory.
11
- * @default false
12
- */
13
- runQueries?: boolean;
14
- /**
15
- * If set - all keys will be prefixed by it.
16
- * So the key will look like:
17
- * ${namespacePrefix}${table}_${id}
18
- */
19
- namespacePrefix?: string;
20
- }
21
- /**
22
- * streamQuery doesn't support limit and order - it always returns unlimited unsorted results.
23
- */
24
- export declare class RedisDB implements CommonDB {
25
- constructor(cfg?: RedisDBCfg);
26
- cfg: Required<RedisDBCfg>;
27
- redis: Redis.Redis;
28
- protected create(): Redis.Redis;
29
- quit(): Promise<void>;
30
- resetCache(table?: string): Promise<void>;
31
- key(table: string, id: string): string;
32
- parseKey(table: string, key: string): {
33
- table: string;
34
- id: string;
35
- };
36
- serialize<T extends object>(obj: T): string;
37
- deserialize<T = any>(s?: string | null): T;
38
- saveBatch<DBM extends SavedDBEntity>(table: string, dbms: DBM[], opts?: CommonDBSaveOptions): Promise<void>;
39
- getByIds<DBM extends SavedDBEntity>(table: string, ids: string[], opts?: CommonDBOptions): Promise<DBM[]>;
40
- deleteByIds(table: string, ids: string[], opts?: CommonDBOptions): Promise<number>;
41
- streamQuery<DBM extends SavedDBEntity>(q: DBQuery<DBM>, opts?: CommonDBOptions): Readable;
42
- runQuery<DBM extends SavedDBEntity>(q: DBQuery<DBM>, opts?: CommonDBOptions): Promise<RunQueryResult<DBM>>;
43
- runQueryCount<DBM extends SavedDBEntity>(q: DBQuery<DBM>, opts?: CommonDBOptions): Promise<number>;
44
- deleteByQuery<DBM extends SavedDBEntity>(q: DBQuery<DBM>, opts?: CommonDBOptions): Promise<number>;
45
- }
package/dist/redis.db.js DELETED
@@ -1,120 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const inMemory_db_1 = require("@naturalcycles/db-lib/dist/inMemory.db");
4
- const js_lib_1 = require("@naturalcycles/js-lib");
5
- const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
6
- const chalk_1 = require("chalk");
7
- const Redis = require("ioredis");
8
- const stream_1 = require("stream");
9
- const log = nodejs_lib_1.Debug('nc:redis-lib');
10
- /**
11
- * streamQuery doesn't support limit and order - it always returns unlimited unsorted results.
12
- */
13
- class RedisDB {
14
- constructor(cfg = {}) {
15
- this.cfg = {
16
- runQueries: false,
17
- namespacePrefix: '',
18
- ...cfg,
19
- redisOptions: {
20
- showFriendlyErrorStack: true,
21
- lazyConnect: true,
22
- ...cfg.redisOptions,
23
- },
24
- };
25
- this.redis = this.create();
26
- }
27
- create() {
28
- const redis = new Redis(this.cfg.redisOptions);
29
- const redisEvents = ['connect', 'close', 'reconnecting', 'end'];
30
- redisEvents.forEach(e => redis.on(e, () => log(`event:`, chalk_1.default.bold(e))));
31
- const closeEvents = ['SIGINT', 'SIGTERM'];
32
- closeEvents.forEach(e => process.once(e, () => redis.quit()));
33
- redis.on('error', err => log.error(err));
34
- // log('connected')
35
- return redis;
36
- }
37
- async quit() {
38
- log('disconnecting...');
39
- log(`quit:`, await this.redis.quit());
40
- }
41
- async resetCache(table) {
42
- const pattern = `${this.cfg.namespacePrefix}${table || ''}*`;
43
- const keys = await this.redis.keys(pattern);
44
- if (keys.length) {
45
- await this.redis.del(...keys);
46
- }
47
- log(`resetCache deleted ${keys.length} keys under ${pattern}`);
48
- }
49
- key(table, id) {
50
- return this.cfg.namespacePrefix + [table, id].join('_');
51
- }
52
- parseKey(table, key) {
53
- return {
54
- table,
55
- id: key.substr(this.cfg.namespacePrefix.length + table.length + 1),
56
- };
57
- }
58
- serialize(obj) {
59
- return JSON.stringify(obj);
60
- }
61
- deserialize(s) {
62
- try {
63
- return s && JSON.parse(s);
64
- }
65
- catch (err) {
66
- log.error(s, typeof s, err);
67
- return undefined;
68
- }
69
- }
70
- async saveBatch(table, dbms, opts) {
71
- if (!dbms.length)
72
- return;
73
- await this.redis.mset(js_lib_1._flatten(dbms.map(dbm => [this.key(table, dbm.id), this.serialize(dbm)])));
74
- }
75
- async getByIds(table, ids, opts) {
76
- if (!ids.length)
77
- return [];
78
- const dbms = (await this.redis.mget(...ids.map(id => this.key(table, id))));
79
- return dbms.filter(Boolean).map(dbm => this.deserialize(dbm));
80
- }
81
- async deleteByIds(table, ids, opts) {
82
- if (!ids.length)
83
- return 0;
84
- return await this.redis.del(...ids.map(id => this.key(table, id)));
85
- }
86
- streamQuery(q, opts) {
87
- if (!this.cfg.runQueries)
88
- return nodejs_lib_1.readableFrom([]);
89
- const _this = this;
90
- return this.redis
91
- .scanStream({
92
- match: `${this.cfg.namespacePrefix}${q.table}_*`,
93
- })
94
- .pipe(new stream_1.Transform({
95
- objectMode: true,
96
- async transform(keys, _encoding, cb) {
97
- const ids = keys.map(k => _this.parseKey(q.table, k).id);
98
- const dbms = await _this.getByIds(q.table, ids);
99
- const items = inMemory_db_1.queryInMemory(q, dbms);
100
- // tslint:disable-next-line:no-invalid-this
101
- items.forEach(item => this.push(item)); // push multiple items!
102
- cb();
103
- },
104
- }));
105
- }
106
- async runQuery(q, opts) {
107
- const dbms = await nodejs_lib_1.streamToArray(this.streamQuery(q, opts));
108
- return { records: inMemory_db_1.queryInMemory(q, dbms) };
109
- }
110
- async runQueryCount(q, opts) {
111
- const { records } = await this.runQuery(q, opts);
112
- return records.length;
113
- }
114
- async deleteByQuery(q, opts) {
115
- const { records } = await this.runQuery(q, opts);
116
- return await this.deleteByIds(q.table, records.map(dbm => dbm.id), opts);
117
- }
118
- }
119
- exports.RedisDB = RedisDB;
120
- //# sourceMappingURL=redis.db.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redis.db.js","sourceRoot":"","sources":["../src/redis.db.ts"],"names":[],"mappings":"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"}
package/src/redis.db.ts DELETED
@@ -1,183 +0,0 @@
1
- import {
2
- CommonDB,
3
- CommonDBOptions,
4
- CommonDBSaveOptions,
5
- DBQuery,
6
- RunQueryResult,
7
- SavedDBEntity,
8
- } from '@naturalcycles/db-lib'
9
- import { queryInMemory } from '@naturalcycles/db-lib/dist/inMemory.db'
10
- import { _flatten } from '@naturalcycles/js-lib'
11
- import { Debug, readableFrom, streamToArray } from '@naturalcycles/nodejs-lib'
12
- import c from 'chalk'
13
- import { RedisOptions } from 'ioredis'
14
- import * as Redis from 'ioredis'
15
- import { Readable, Transform } from 'stream'
16
-
17
- const log = Debug('nc:redis-lib')
18
-
19
- export interface RedisDBCfg {
20
- redisOptions?: RedisOptions
21
-
22
- /**
23
- * If true - it will "emulate" queries by using SCAN $table_
24
- * which will load ALL table keys into memory and filter in-memory.
25
- * @default false
26
- */
27
- runQueries?: boolean
28
-
29
- /**
30
- * If set - all keys will be prefixed by it.
31
- * So the key will look like:
32
- * ${namespacePrefix}${table}_${id}
33
- */
34
- namespacePrefix?: string
35
- }
36
-
37
- /**
38
- * streamQuery doesn't support limit and order - it always returns unlimited unsorted results.
39
- */
40
- export class RedisDB implements CommonDB {
41
- constructor(cfg: RedisDBCfg = {}) {
42
- this.cfg = {
43
- runQueries: false,
44
- namespacePrefix: '',
45
- ...cfg,
46
- redisOptions: {
47
- showFriendlyErrorStack: true,
48
- lazyConnect: true,
49
- ...cfg.redisOptions,
50
- },
51
- }
52
-
53
- this.redis = this.create()
54
- }
55
-
56
- public cfg!: Required<RedisDBCfg>
57
-
58
- redis!: Redis.Redis
59
-
60
- protected create(): Redis.Redis {
61
- const redis = new Redis(this.cfg.redisOptions)
62
-
63
- const redisEvents = ['connect', 'close', 'reconnecting', 'end']
64
- redisEvents.forEach(e => redis.on(e, () => log(`event:`, c.bold(e))))
65
-
66
- const closeEvents: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']
67
- closeEvents.forEach(e => process.once(e, () => redis.quit()))
68
-
69
- redis.on('error', err => log.error(err))
70
-
71
- // log('connected')
72
- return redis
73
- }
74
-
75
- async quit(): Promise<void> {
76
- log('disconnecting...')
77
- log(`quit:`, await this.redis.quit())
78
- }
79
-
80
- async resetCache(table?: string): Promise<void> {
81
- const pattern = `${this.cfg.namespacePrefix}${table || ''}*`
82
- const keys = await this.redis.keys(pattern)
83
- if (keys.length) {
84
- await this.redis.del(...keys)
85
- }
86
- log(`resetCache deleted ${keys.length} keys under ${pattern}`)
87
- }
88
-
89
- key(table: string, id: string): string {
90
- return this.cfg.namespacePrefix + [table, id].join('_')
91
- }
92
-
93
- parseKey(table: string, key: string): { table: string; id: string } {
94
- return {
95
- table,
96
- id: key.substr(this.cfg.namespacePrefix.length + table.length + 1),
97
- }
98
- }
99
-
100
- serialize<T extends object>(obj: T): string {
101
- return JSON.stringify(obj)
102
- }
103
-
104
- deserialize<T = any>(s?: string | null): T {
105
- try {
106
- return s && JSON.parse(s)
107
- } catch (err) {
108
- log.error(s, typeof s, err)
109
- return undefined as any
110
- }
111
- }
112
-
113
- async saveBatch<DBM extends SavedDBEntity>(
114
- table: string,
115
- dbms: DBM[],
116
- opts?: CommonDBSaveOptions,
117
- ): Promise<void> {
118
- if (!dbms.length) return
119
- await this.redis.mset(_flatten(dbms.map(dbm => [this.key(table, dbm.id), this.serialize(dbm)])))
120
- }
121
-
122
- async getByIds<DBM extends SavedDBEntity>(
123
- table: string,
124
- ids: string[],
125
- opts?: CommonDBOptions,
126
- ): Promise<DBM[]> {
127
- if (!ids.length) return []
128
- const dbms = (await this.redis.mget(...ids.map(id => this.key(table, id)))) as string[]
129
- return dbms.filter(Boolean).map(dbm => this.deserialize<DBM>(dbm))
130
- }
131
-
132
- async deleteByIds(table: string, ids: string[], opts?: CommonDBOptions): Promise<number> {
133
- if (!ids.length) return 0
134
- return await this.redis.del(...ids.map(id => this.key(table, id)))
135
- }
136
-
137
- streamQuery<DBM extends SavedDBEntity>(q: DBQuery<DBM>, opts?: CommonDBOptions): Readable {
138
- if (!this.cfg.runQueries) return readableFrom([])
139
- const _this = this
140
-
141
- return this.redis
142
- .scanStream({
143
- match: `${this.cfg.namespacePrefix}${q.table}_*`,
144
- })
145
- .pipe(
146
- new Transform({
147
- objectMode: true,
148
- async transform(keys: string[], _encoding, cb) {
149
- const ids = keys.map(k => _this.parseKey(q.table, k).id)
150
- const dbms = await _this.getByIds<DBM>(q.table, ids)
151
- const items = queryInMemory(q, dbms)
152
- // tslint:disable-next-line:no-invalid-this
153
- items.forEach(item => this.push(item)) // push multiple items!
154
- cb()
155
- },
156
- }),
157
- )
158
- }
159
-
160
- async runQuery<DBM extends SavedDBEntity>(
161
- q: DBQuery<DBM>,
162
- opts?: CommonDBOptions,
163
- ): Promise<RunQueryResult<DBM>> {
164
- const dbms = await streamToArray<DBM>(this.streamQuery(q, opts))
165
- return { records: queryInMemory(q, dbms) }
166
- }
167
-
168
- async runQueryCount<DBM extends SavedDBEntity>(
169
- q: DBQuery<DBM>,
170
- opts?: CommonDBOptions,
171
- ): Promise<number> {
172
- const { records } = await this.runQuery(q, opts)
173
- return records.length
174
- }
175
-
176
- async deleteByQuery<DBM extends SavedDBEntity>(
177
- q: DBQuery<DBM>,
178
- opts?: CommonDBOptions,
179
- ): Promise<number> {
180
- const { records } = await this.runQuery(q, opts)
181
- return await this.deleteByIds(q.table, records.map(dbm => dbm.id), opts)
182
- }
183
- }