@ti-engine/core 1.7.1 → 1.7.2
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/CHANGELOG.md +383 -377
- package/LICENSE.md +321 -321
- package/README.md +597 -597
- package/bin/localization/labels.json +122 -122
- package/bin/settings.json +41 -41
- package/bin/start-instance.js +164 -164
- package/components/auditing.js +191 -191
- package/components/connection-observer.js +72 -72
- package/components/definitions.types.js +248 -248
- package/components/exchange/default/default-message-exchange.js +136 -136
- package/components/exchange/default/default-message-receiver.js +101 -101
- package/components/exchange/default/default-message-sender.js +100 -100
- package/components/exchange/message-dispatcher.js +168 -168
- package/components/exchange/message-exchange.js +449 -449
- package/components/exchange/message-handler.js +235 -235
- package/components/exchange/message-memory-cache.js +190 -190
- package/components/exchange/message-observer.js +126 -126
- package/components/exchange/message-receiver.js +181 -181
- package/components/exchange/message-sender.js +143 -143
- package/components/exchange/message-tracer.js +212 -212
- package/components/service-caller.js +370 -370
- package/components/service-consumer.js +131 -131
- package/components/service-executor.js +278 -278
- package/components/service-instance.js +316 -316
- package/components/service-provider.js +251 -251
- package/integrations/redis-integration.js +591 -591
- package/package.json +89 -89
- package/utils/cache.js +772 -772
- package/utils/config.js +103 -103
- package/utils/exceptions.js +368 -368
- package/utils/localization.js +298 -298
- package/utils/logger.js +82 -82
- package/utils/tools.js +632 -632
|
@@ -1,592 +1,592 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
-
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
-
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
-
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
-
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const ConnectionObserver = require( "#connection-observer" );
|
|
10
|
-
const Redis = require( "ioredis" );
|
|
11
|
-
const tools = require( "#tools" );
|
|
12
|
-
const logger = require( "#logger" );
|
|
13
|
-
const exceptions = require( "#exceptions" );
|
|
14
|
-
const _ = require( "lodash" );
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Enum for listing all used Redis cache commands.
|
|
18
|
-
*
|
|
19
|
-
* @readonly
|
|
20
|
-
* @enum {string}
|
|
21
|
-
* @typedef {string} TiRedisCommand
|
|
22
|
-
*/
|
|
23
|
-
let cacheCommandsEnum = tools.enum( {
|
|
24
|
-
ADD_TO_SET: [ "sadd", "add to set", "https://redis.io/docs/latest/commands/sadd/" ],
|
|
25
|
-
DELETE_VALUE: [ "del", "delete value", "https://redis.io/docs/latest/commands/del/" ],
|
|
26
|
-
EXPIRE: [ "expire", "expire", "https://redis.io/docs/latest/commands/expire/" ],
|
|
27
|
-
GET_ALL_FROM_SET: [ "smembers", "get all set members", "https://redis.io/docs/latest/commands/smembers/" ],
|
|
28
|
-
GET_VALUE: [ "get", "get value", "https://redis.io/docs/latest/commands/get/" ],
|
|
29
|
-
HASH_GET: [ "hget", "hash get", "https://redis.io/docs/latest/commands/hget/" ],
|
|
30
|
-
HASH_GET_ALL: [ "hgetall", "hash get all", "https://redis.io/docs/latest/commands/hgetall/" ],
|
|
31
|
-
HASH_REMOVE: [ "hdel", "hash remove", "https://redis.io/docs/latest/commands/hdel/" ],
|
|
32
|
-
HASH_EXPIRE: [ "hexpire", "hash expire", "https://redis.io/docs/latest/commands/hexpire/" ],
|
|
33
|
-
HASH_SET: [ "hset", "", "https://redis.io/docs/latest/commands/hset/" ],
|
|
34
|
-
HASH_SET_MANY: [ "hmset", "(deprecated) use HSET with multiple fields", "https://redis.io/docs/latest/commands/hmset/" ],
|
|
35
|
-
IS_SET_MEMBER: [ "sismember", "", "https://redis.io/docs/latest/commands/sismember/" ],
|
|
36
|
-
JSON_ARRAY_APPEND: [ "json.arrappend", "", "https://redis.io/docs/latest/commands/json.arrappend/" ],
|
|
37
|
-
JSON_GET: [ "json.get", "", "https://redis.io/docs/latest/commands/json.get/" ],
|
|
38
|
-
JSON_MERGE: [ "json.merge", "", "https://redis.io/docs/latest/commands/json.merge/" ],
|
|
39
|
-
JSON_MGET: [ "json.mget", "", "https://redis.io/docs/latest/commands/json.mget/" ],
|
|
40
|
-
JSON_SET: [ "json.set", "", "https://redis.io/docs/latest/commands/json.set/" ],
|
|
41
|
-
KEYS: [ "keys", "(warning: O(N), use SCAN where possible)", "https://redis.io/docs/latest/commands/keys/" ],
|
|
42
|
-
LIST_PUSH: [ "lpush", "list push", "https://redis.io/docs/latest/commands/lpush/" ],
|
|
43
|
-
LIST_POP_TAIL_BLOCKING: [ "brpop", "list pop tail blocking", "https://redis.io/docs/latest/commands/brpop/" ],
|
|
44
|
-
LIST_POP_TAIL_PUSH_HEAD_BLOCKING: [ "brpoplpush", "list pop tail push head blocking", "https://redis.io/docs/latest/commands/brpoplpush/" ],
|
|
45
|
-
LIST_REMOVE: [ "lrem", "list remove", "https://redis.io/docs/latest/commands/lrem/" ],
|
|
46
|
-
SET_VALUE: [ "set", "set value", "https://redis.io/docs/latest/commands/set/" ],
|
|
47
|
-
UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/docs/latest/commands/sunion/" ]
|
|
48
|
-
} );
|
|
49
|
-
module.exports.cacheCommands = cacheCommandsEnum;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Enum for listing all client statuses.
|
|
53
|
-
*
|
|
54
|
-
* @readonly
|
|
55
|
-
* @enum {number}
|
|
56
|
-
* @typedef {number} TiRedisClientStatus
|
|
57
|
-
*/
|
|
58
|
-
let clientStatusEnum = tools.enum( {
|
|
59
|
-
UNINITIALIZED: [ 0, "uninitialized", "Redis client is offline and not yet initialized." ],
|
|
60
|
-
CONNECTED: [ 1, "connected", "Redis client is connected and online." ],
|
|
61
|
-
CONNECTING: [ 2, "connecting", "Redis client is connecting to server." ],
|
|
62
|
-
DISRUPTED: [ 3, "disrupted", "Redis client is temporarily disconnected from server due to a disruption." ],
|
|
63
|
-
SHUTTING_DOWN: [ 4, "shutting down", "Redis client is shutting down." ],
|
|
64
|
-
DISCONNECTED: [ 5, "disconnected", "Redis client is permanently disconnected from server." ]
|
|
65
|
-
} );
|
|
66
|
-
module.exports.clientStatus = clientStatusEnum;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Enum for listing the Redis key override modes.
|
|
70
|
-
*
|
|
71
|
-
* @readonly
|
|
72
|
-
* @enum {string}
|
|
73
|
-
* @typedef {string} TiRedisOverrideMode
|
|
74
|
-
*/
|
|
75
|
-
let cacheOverrideModeEnum = tools.enum( {
|
|
76
|
-
DEFAULT: [ "", "default", "Standard Redis behaviour when setting new key." ],
|
|
77
|
-
NX: [ "nx", "nx", "Sets the key only if it does not already exist." ],
|
|
78
|
-
XX: [ "xx", "xx", "Sets the key only if it already exists." ]
|
|
79
|
-
} );
|
|
80
|
-
module.exports.cacheOverrideMode = cacheOverrideModeEnum;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Used to create a Redis Cache client.
|
|
84
|
-
* <br/>
|
|
85
|
-
* NOTE: This client is set to automatically resend all pending commands on connection recovery with no limit on the retry attempts.
|
|
86
|
-
* This is done to avoid losing any pending commands in case of a connection failure. For a different behavior, use custom implementation.
|
|
87
|
-
*
|
|
88
|
-
* @class RedisClient
|
|
89
|
-
* @public
|
|
90
|
-
*/
|
|
91
|
-
class RedisClient {
|
|
92
|
-
|
|
93
|
-
#clientIdentifier;
|
|
94
|
-
#clientStatus = clientStatusEnum.UNINITIALIZED;
|
|
95
|
-
#retryMaxInterval = 1000;
|
|
96
|
-
#retryMaxAttempts = undefined;
|
|
97
|
-
#redisConnection = undefined;
|
|
98
|
-
#redisClientID;
|
|
99
|
-
#serverInfo = {};
|
|
100
|
-
#serverFeatures = {};
|
|
101
|
-
#connectionObservers = [];
|
|
102
|
-
#messageHandlersByChannel = new Map();
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* @constructor
|
|
106
|
-
* @param {string} identifier
|
|
107
|
-
* @returns {RedisClient}
|
|
108
|
-
*/
|
|
109
|
-
constructor( identifier ) {
|
|
110
|
-
this.#clientIdentifier = identifier || "redis-client-" + tools.getUUID();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/* Public interface */
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Used to return the Redis client identifier assigned internally.
|
|
117
|
-
*
|
|
118
|
-
* @property
|
|
119
|
-
* @returns {string}
|
|
120
|
-
* @public
|
|
121
|
-
*/
|
|
122
|
-
get identifier() {
|
|
123
|
-
return this.#clientIdentifier;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Used to return the client ID assigned by the Redis server.
|
|
128
|
-
*
|
|
129
|
-
* @property
|
|
130
|
-
* @returns {number}
|
|
131
|
-
* @public
|
|
132
|
-
*/
|
|
133
|
-
get clientID() {
|
|
134
|
-
return this.#redisClientID;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Used to return the Redis client status.
|
|
139
|
-
*
|
|
140
|
-
* @property
|
|
141
|
-
* @returns {number}
|
|
142
|
-
* @public
|
|
143
|
-
*/
|
|
144
|
-
get clientStatus() {
|
|
145
|
-
return this.#clientStatus;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Used to return the Redis server version.
|
|
150
|
-
*
|
|
151
|
-
* @property
|
|
152
|
-
* @returns {number}
|
|
153
|
-
* @public
|
|
154
|
-
*/
|
|
155
|
-
get serverVersion() {
|
|
156
|
-
return this.#serverInfo[ "redis_version" ];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Verify if Redis server supports JSON data types.
|
|
161
|
-
*
|
|
162
|
-
* @property
|
|
163
|
-
* @returns {boolean}
|
|
164
|
-
* @public
|
|
165
|
-
*/
|
|
166
|
-
get isJSONSupported() {
|
|
167
|
-
// Detect RedisJSON module variants (e.g., ReJSON, ReJSON2):
|
|
168
|
-
return !_.isNil( this.#serverFeatures[ "ReJSON" ] ) || !_.isNil( this.#serverFeatures[ "ReJSON2" ] );
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Used to initialize the Redis client.
|
|
173
|
-
*
|
|
174
|
-
* @method
|
|
175
|
-
* @param {string} host
|
|
176
|
-
* @param {number} port
|
|
177
|
-
* @param {string} authKey
|
|
178
|
-
* @param {string} user
|
|
179
|
-
* @param {number} defaultDB
|
|
180
|
-
* @param {number} [retryMaxIntervalMs=1000] Optional max backoff interval.
|
|
181
|
-
* @param {number|undefined} [retryMaxAttempts=undefined] Optional max (re)connection attempts before abort.
|
|
182
|
-
* @public
|
|
183
|
-
*/
|
|
184
|
-
initialize( host, port, authKey, user, defaultDB, retryMaxIntervalMs = 1000, retryMaxAttempts = undefined ) {
|
|
185
|
-
return new Promise( ( resolve, reject ) => {
|
|
186
|
-
if ( this.#redisConnection ) {
|
|
187
|
-
resolve();
|
|
188
|
-
} else {
|
|
189
|
-
this.#setupClient( host, port, authKey, user, defaultDB, retryMaxIntervalMs, retryMaxAttempts ).then( () => {
|
|
190
|
-
// Fetch the server information and store it:
|
|
191
|
-
return this.#fetchServerInfo();
|
|
192
|
-
} ).then( () => {
|
|
193
|
-
return this.#getClientID();
|
|
194
|
-
} ).then( ( clientID ) => {
|
|
195
|
-
// Store the connection ID:
|
|
196
|
-
this.#redisClientID = clientID;
|
|
197
|
-
resolve();
|
|
198
|
-
} ).catch( ( error ) => {
|
|
199
|
-
reject( exceptions.raise( error ) );
|
|
200
|
-
} );
|
|
201
|
-
}
|
|
202
|
-
} );
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Used to register a new {@link ConnectionObserver} for events related to the Redis connection state.
|
|
207
|
-
*
|
|
208
|
-
* @method
|
|
209
|
-
* @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
|
|
210
|
-
* @public
|
|
211
|
-
*/
|
|
212
|
-
addConnectionObserver( connectionObserver ) {
|
|
213
|
-
if ( connectionObserver instanceof ConnectionObserver ) {
|
|
214
|
-
this.#connectionObservers.push( connectionObserver );
|
|
215
|
-
} else {
|
|
216
|
-
logger.log( `Attempting to add '${ connectionObserver.constructor.name }' as connection observer but it's not a child-class of 'ConnectionObserver'!`, logger.logSeverity.WARNING );
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Used to execute multiple commands within a Redis transaction.
|
|
222
|
-
*
|
|
223
|
-
* @method
|
|
224
|
-
* @param {Array[]} commands
|
|
225
|
-
* @returns {Promise<*>}
|
|
226
|
-
* @public
|
|
227
|
-
*/
|
|
228
|
-
executeCommands( commands ) {
|
|
229
|
-
return new Promise( ( resolve, reject ) => {
|
|
230
|
-
this.#redisConnection.multi( commands ).exec().then( ( results ) => {
|
|
231
|
-
resolve( results );
|
|
232
|
-
} ).catch( ( error ) => {
|
|
233
|
-
reject( exceptions.raise( error ) );
|
|
234
|
-
} );
|
|
235
|
-
} );
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Used to send a new blocking command to Redis.
|
|
240
|
-
* <br/>
|
|
241
|
-
* WARNING: This will reserve the client connection until a result is received.
|
|
242
|
-
*
|
|
243
|
-
* @method
|
|
244
|
-
* @param {string} command
|
|
245
|
-
* @param {Array} commandArguments
|
|
246
|
-
* @returns {Promise<*>}
|
|
247
|
-
* @public
|
|
248
|
-
*/
|
|
249
|
-
blockingCommand( command, commandArguments ) {
|
|
250
|
-
return new Promise( ( resolve, reject ) => {
|
|
251
|
-
this.#redisConnection[ command ].apply( this.#redisConnection, commandArguments ).then( ( results ) => {
|
|
252
|
-
resolve( results );
|
|
253
|
-
} ).catch( ( error ) => {
|
|
254
|
-
reject( exceptions.raise( error ) );
|
|
255
|
-
} );
|
|
256
|
-
} );
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Used to publish a message to the specified channel.
|
|
261
|
-
*
|
|
262
|
-
* @method
|
|
263
|
-
* @param {string} channel
|
|
264
|
-
* @param {(Object|string)} message
|
|
265
|
-
* @returns {Promise<number>}
|
|
266
|
-
* @public
|
|
267
|
-
*/
|
|
268
|
-
publishCommand( channel, message ) {
|
|
269
|
-
return new Promise( ( resolve, reject ) => {
|
|
270
|
-
this.#redisConnection.publish( channel, tools.stringifyJSON( message ) ).then( ( receivedBy ) => {
|
|
271
|
-
resolve( receivedBy );
|
|
272
|
-
} ).catch( ( error ) => {
|
|
273
|
-
reject( exceptions.raise( error ) );
|
|
274
|
-
} );
|
|
275
|
-
} );
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Used to subscribe to the specified channel for messages.
|
|
280
|
-
* <br/>
|
|
281
|
-
* NOTE: Call unsubscribeCommand(channel) to detach later.
|
|
282
|
-
*
|
|
283
|
-
* @method
|
|
284
|
-
* @param {string} channel Unique identifier of the channel to subscribe to.
|
|
285
|
-
* @param {function( Object )} messageHandler Will execute this handler every time a new message is received.
|
|
286
|
-
* @returns {Promise}
|
|
287
|
-
* @public
|
|
288
|
-
*/
|
|
289
|
-
subscribeCommand( channel, messageHandler ) {
|
|
290
|
-
return new Promise( ( resolve, reject ) => {
|
|
291
|
-
// Avoid attaching multiple listeners to the same channel:
|
|
292
|
-
if ( this.#messageHandlersByChannel.has( channel ) ) {
|
|
293
|
-
logger.log( "Attempting to subscribe to message channel '" + channel + "' while already subscribed to it.", logger.logSeverity.WARNING );
|
|
294
|
-
resolve();
|
|
295
|
-
} else {
|
|
296
|
-
const onMessage = ( subscribedChannel, message ) => {
|
|
297
|
-
if ( subscribedChannel === channel && typeof messageHandler === "function" ) {
|
|
298
|
-
messageHandler( tools.parseJSON( message ) );
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
this.#redisConnection.once( "subscribe", ( subscribedChannel ) => {
|
|
303
|
-
if ( subscribedChannel === channel ) {
|
|
304
|
-
logger.log( "Subscription to message channel '" + channel + "' successful.", logger.logSeverity.DEBUG );
|
|
305
|
-
resolve();
|
|
306
|
-
}
|
|
307
|
-
} );
|
|
308
|
-
|
|
309
|
-
this.#redisConnection.on( "message", onMessage );
|
|
310
|
-
this.#messageHandlersByChannel.set( channel, onMessage );
|
|
311
|
-
|
|
312
|
-
this.#redisConnection.subscribe( channel ).catch( ( error ) => {
|
|
313
|
-
// Cleanup partial state if subscribe fails:
|
|
314
|
-
const handler = this.#messageHandlersByChannel.get( channel );
|
|
315
|
-
if ( handler ) {
|
|
316
|
-
this.#redisConnection.off( "message", handler );
|
|
317
|
-
this.#messageHandlersByChannel.delete( channel );
|
|
318
|
-
}
|
|
319
|
-
reject( exceptions.raise( error ) );
|
|
320
|
-
} );
|
|
321
|
-
}
|
|
322
|
-
} );
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Used to unsubscribe from a channel and remove its message handler.
|
|
327
|
-
*
|
|
328
|
-
* @method
|
|
329
|
-
* @param {string} channel Unique identifier of the channel to unsubscribe from.
|
|
330
|
-
* @returns {Promise}
|
|
331
|
-
* @public
|
|
332
|
-
*/
|
|
333
|
-
unsubscribeCommand( channel ) {
|
|
334
|
-
return new Promise( ( resolve, reject ) => {
|
|
335
|
-
const handler = this.#messageHandlersByChannel.get( channel );
|
|
336
|
-
if ( handler ) {
|
|
337
|
-
this.#redisConnection.off( "message", handler );
|
|
338
|
-
this.#messageHandlersByChannel.delete( channel );
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
this.#redisConnection.unsubscribe( channel ).then( () => {
|
|
342
|
-
resolve();
|
|
343
|
-
} ).catch( ( error ) => {
|
|
344
|
-
reject( exceptions.raise( error ) );
|
|
345
|
-
} );
|
|
346
|
-
} );
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Used to execute any Redis command in an unmanaged way.
|
|
351
|
-
* <br/>
|
|
352
|
-
* WARNING: Use this only if there is no other implemented function in this module and the command
|
|
353
|
-
* you want to execute is not supported by the 'multi' Redis command (implemented in {@link RedisClient.executeCommands}).
|
|
354
|
-
* Make sure to handle the result as it will be returned raw.
|
|
355
|
-
*
|
|
356
|
-
* @method
|
|
357
|
-
* @param {string[]} commandArguments
|
|
358
|
-
* @returns {Promise<Object>}
|
|
359
|
-
* @public
|
|
360
|
-
*/
|
|
361
|
-
callCommand( commandArguments ) {
|
|
362
|
-
return new Promise( ( resolve, reject ) => {
|
|
363
|
-
this.#redisConnection[ "call" ].apply( this.#redisConnection, commandArguments ).then( ( result ) => {
|
|
364
|
-
resolve( result );
|
|
365
|
-
} ).catch( ( error ) => {
|
|
366
|
-
reject( exceptions.raise( error ) );
|
|
367
|
-
} );
|
|
368
|
-
} );
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Used to gracefully close the Redis connection.
|
|
373
|
-
* Attempts to quit(), then falls back to disconnect() on timeout.
|
|
374
|
-
*
|
|
375
|
-
* @method
|
|
376
|
-
* @param {number} [timeoutMs=1000]
|
|
377
|
-
* @returns {Promise}
|
|
378
|
-
* @public
|
|
379
|
-
*/
|
|
380
|
-
shutDown( timeoutMs = 1000 ) {
|
|
381
|
-
return new Promise( ( resolve ) => {
|
|
382
|
-
let finished = false;
|
|
383
|
-
this.#clientStatus = clientStatusEnum.SHUTTING_DOWN;
|
|
384
|
-
const done = () => {
|
|
385
|
-
if ( !finished ) {
|
|
386
|
-
finished = true;
|
|
387
|
-
resolve();
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
const timeout = setTimeout( () => {
|
|
392
|
-
try {
|
|
393
|
-
this.#redisConnection.disconnect();
|
|
394
|
-
} catch {
|
|
395
|
-
// do nothing here...
|
|
396
|
-
}
|
|
397
|
-
done();
|
|
398
|
-
}, timeoutMs );
|
|
399
|
-
|
|
400
|
-
this.#redisConnection.quit().then( () => {
|
|
401
|
-
clearTimeout( timeout );
|
|
402
|
-
done();
|
|
403
|
-
} ).catch( () => {
|
|
404
|
-
clearTimeout( timeout );
|
|
405
|
-
try {
|
|
406
|
-
this.#redisConnection.disconnect();
|
|
407
|
-
} catch {
|
|
408
|
-
// do nothing here...
|
|
409
|
-
}
|
|
410
|
-
done();
|
|
411
|
-
} );
|
|
412
|
-
} );
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/* Private interface */
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Used to set up the connection to Redis server.
|
|
419
|
-
* <br/>
|
|
420
|
-
* NOTE: This method will only resolve once the server sends a 'ready' event.
|
|
421
|
-
*
|
|
422
|
-
* @method
|
|
423
|
-
* @param {string} host
|
|
424
|
-
* @param {number} port
|
|
425
|
-
* @param {string} authKey
|
|
426
|
-
* @param {string} user
|
|
427
|
-
* @param {number} defaultDB
|
|
428
|
-
* @param {number} [retryMaxIntervalMs=1000] Optional max backoff interval.
|
|
429
|
-
* @param {number|undefined} [retryMaxAttempts=undefined] Optional max (re)connection attempts before abort.
|
|
430
|
-
* @returns {Promise}
|
|
431
|
-
* @private
|
|
432
|
-
*/
|
|
433
|
-
#setupClient( host, port, authKey, user, defaultDB, retryMaxIntervalMs, retryMaxAttempts ) {
|
|
434
|
-
return new Promise( ( resolve, reject ) => {
|
|
435
|
-
try {
|
|
436
|
-
this.#retryMaxInterval = retryMaxIntervalMs;
|
|
437
|
-
this.#retryMaxAttempts = retryMaxAttempts;
|
|
438
|
-
|
|
439
|
-
let retryStrategy = ( attempt ) => {
|
|
440
|
-
let retryInterval = Math.min( attempt * 50, this.#retryMaxInterval );
|
|
441
|
-
if ( this.#retryMaxAttempts != null && attempt > this.#retryMaxAttempts ) {
|
|
442
|
-
logger.log( "In Redis retry strategy: reached max attempts for command retry. Aborting...", logger.logSeverity.WARNING, { attempts: attempt } );
|
|
443
|
-
retryInterval = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
|
|
444
|
-
}
|
|
445
|
-
return retryInterval;
|
|
446
|
-
};
|
|
447
|
-
|
|
448
|
-
let reconnectOnError = ( error ) => {
|
|
449
|
-
logger.log( `In Redis reconnect on error strategy: ${ error.message }`, logger.logSeverity.ERROR, error );
|
|
450
|
-
if ( error.message.includes( "READONLY" ) ) {
|
|
451
|
-
// Returning 2 will also resubmit the failed command:
|
|
452
|
-
return 2;
|
|
453
|
-
} else {
|
|
454
|
-
return 0;
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
let options = {
|
|
459
|
-
port: port,
|
|
460
|
-
host: host,
|
|
461
|
-
username: user,
|
|
462
|
-
password: authKey,
|
|
463
|
-
db: defaultDB,
|
|
464
|
-
autoResendUnfulfilledCommands: true,
|
|
465
|
-
maxRetriesPerRequest: null,
|
|
466
|
-
retryStrategy: retryStrategy,
|
|
467
|
-
reconnectOnError: reconnectOnError
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
/** @type Redis */
|
|
471
|
-
this.#redisConnection = new Redis( options );
|
|
472
|
-
this.#clientStatus = clientStatusEnum.CONNECTING;
|
|
473
|
-
|
|
474
|
-
this.#redisConnection.once( "ready", () => {
|
|
475
|
-
this.#clientStatus = clientStatusEnum.CONNECTED;
|
|
476
|
-
logger.log( `Connection to Redis server '${ this.#redisConnection.options.host }:${ this.#redisConnection.options.port }' established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
477
|
-
this.#notifyConnectionObservers();
|
|
478
|
-
|
|
479
|
-
this.#redisConnection.on( "ready", () => {
|
|
480
|
-
this.#clientStatus = clientStatusEnum.CONNECTED;
|
|
481
|
-
logger.log( `Connection to Redis server '${ this.#redisConnection.options.host }:${ this.#redisConnection.options.port }' reestablished by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
482
|
-
this.#notifyConnectionObservers();
|
|
483
|
-
} );
|
|
484
|
-
|
|
485
|
-
resolve();
|
|
486
|
-
} );
|
|
487
|
-
this.#redisConnection.on( "error", ( error ) => {
|
|
488
|
-
this.#clientStatus = clientStatusEnum.DISRUPTED;
|
|
489
|
-
logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
|
|
490
|
-
this.#notifyConnectionObservers();
|
|
491
|
-
} );
|
|
492
|
-
this.#redisConnection.on( "reconnecting", ( retryInterval ) => {
|
|
493
|
-
this.#clientStatus = clientStatusEnum.CONNECTING;
|
|
494
|
-
logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ retryInterval } ms.`, logger.logSeverity.DEBUG );
|
|
495
|
-
} );
|
|
496
|
-
this.#redisConnection.on( "end", () => {
|
|
497
|
-
if ( this.#clientStatus !== clientStatusEnum.SHUTTING_DOWN ) {
|
|
498
|
-
this.#clientStatus = clientStatusEnum.DISCONNECTED;
|
|
499
|
-
logger.log( `Client '${ this.identifier }' cannot reconnect to Redis server and has been shut down.`, logger.logSeverity.WARNING );
|
|
500
|
-
this.#notifyConnectionObservers();
|
|
501
|
-
}
|
|
502
|
-
} );
|
|
503
|
-
} catch ( error ) {
|
|
504
|
-
reject( exceptions.raise( error ) );
|
|
505
|
-
}
|
|
506
|
-
} );
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Used to notify all connection observers about the current connection state.
|
|
511
|
-
*
|
|
512
|
-
* @method
|
|
513
|
-
* @private
|
|
514
|
-
*/
|
|
515
|
-
#notifyConnectionObservers() {
|
|
516
|
-
// Notify all connection observers about the event:
|
|
517
|
-
_.forEach( this.#connectionObservers, ( connectionObserver ) => {
|
|
518
|
-
if ( this.#clientStatus === clientStatusEnum.CONNECTED ) {
|
|
519
|
-
connectionObserver.onConnectionRecovered( this.#clientIdentifier );
|
|
520
|
-
} else if ( this.#clientStatus === clientStatusEnum.DISRUPTED ) {
|
|
521
|
-
connectionObserver.onConnectionDisrupted( this.#clientIdentifier );
|
|
522
|
-
} else if ( this.#clientStatus === clientStatusEnum.DISCONNECTED ) {
|
|
523
|
-
connectionObserver.onConnectionLost( this.#clientIdentifier );
|
|
524
|
-
}
|
|
525
|
-
} );
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Used to fetch and store Redis server information.
|
|
530
|
-
*
|
|
531
|
-
* @method
|
|
532
|
-
* @returns {Promise}
|
|
533
|
-
* @private
|
|
534
|
-
*/
|
|
535
|
-
#fetchServerInfo() {
|
|
536
|
-
return new Promise( ( resolve, reject ) => {
|
|
537
|
-
this.#redisConnection.info().then( ( result ) => {
|
|
538
|
-
this.#serverInfo = {};
|
|
539
|
-
if ( _.isString( result ) ) {
|
|
540
|
-
let rawData = _.split( result, "\r\n" );
|
|
541
|
-
_.forEach( rawData, ( entry ) => {
|
|
542
|
-
let details = _.split( entry, ":" );
|
|
543
|
-
if ( !_.startsWith( details[ 0 ], "#" ) && details[ 0 ] !== "" && details[ 0 ] ) {
|
|
544
|
-
if ( _.isNaN( _.toNumber( details[ 1 ] ) ) ) {
|
|
545
|
-
this.#serverInfo[ details[ 0 ] ] = details[ 1 ];
|
|
546
|
-
} else {
|
|
547
|
-
this.#serverInfo[ details[ 0 ] ] = _.toNumber( details[ 1 ] );
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
} );
|
|
551
|
-
}
|
|
552
|
-
return this.#redisConnection.module( "LIST" );
|
|
553
|
-
} ).then( ( result ) => {
|
|
554
|
-
this.#serverFeatures = {};
|
|
555
|
-
if ( _.isArray( result ) ) {
|
|
556
|
-
_.forEach( result, ( entry ) => {
|
|
557
|
-
this.#serverFeatures[ entry[ 1 ] ] = entry[ 3 ];
|
|
558
|
-
} );
|
|
559
|
-
}
|
|
560
|
-
resolve();
|
|
561
|
-
} ).catch( ( error ) => {
|
|
562
|
-
logger.log( `Failed to fetch Redis server information by client '${ this.identifier }'!`, logger.logSeverity.WARNING, error );
|
|
563
|
-
reject( exceptions.raise( error ) );
|
|
564
|
-
} );
|
|
565
|
-
} );
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Used to fetch and store the Redis client ID.
|
|
570
|
-
*
|
|
571
|
-
* @method
|
|
572
|
-
* @returns {Promise<number>}
|
|
573
|
-
* @private
|
|
574
|
-
*/
|
|
575
|
-
#getClientID() {
|
|
576
|
-
let commandArguments = [ "client", "id" ];
|
|
577
|
-
return this.callCommand( commandArguments ).then( ( clientID ) => Number( clientID ) );
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Create and return a new Redis client.
|
|
584
|
-
*
|
|
585
|
-
* @method
|
|
586
|
-
* @param {string} identifier
|
|
587
|
-
* @return {RedisClient}
|
|
588
|
-
* @public
|
|
589
|
-
*/
|
|
590
|
-
module.exports.createRedisClient = ( identifier ) => {
|
|
591
|
-
return Object.freeze( new RedisClient( identifier ) );
|
|
1
|
+
/*
|
|
2
|
+
* The ti-engine is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using node.js.
|
|
3
|
+
* Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
5
|
+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
6
|
+
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const ConnectionObserver = require( "#connection-observer" );
|
|
10
|
+
const Redis = require( "ioredis" );
|
|
11
|
+
const tools = require( "#tools" );
|
|
12
|
+
const logger = require( "#logger" );
|
|
13
|
+
const exceptions = require( "#exceptions" );
|
|
14
|
+
const _ = require( "lodash" );
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Enum for listing all used Redis cache commands.
|
|
18
|
+
*
|
|
19
|
+
* @readonly
|
|
20
|
+
* @enum {string}
|
|
21
|
+
* @typedef {string} TiRedisCommand
|
|
22
|
+
*/
|
|
23
|
+
let cacheCommandsEnum = tools.enum( {
|
|
24
|
+
ADD_TO_SET: [ "sadd", "add to set", "https://redis.io/docs/latest/commands/sadd/" ],
|
|
25
|
+
DELETE_VALUE: [ "del", "delete value", "https://redis.io/docs/latest/commands/del/" ],
|
|
26
|
+
EXPIRE: [ "expire", "expire", "https://redis.io/docs/latest/commands/expire/" ],
|
|
27
|
+
GET_ALL_FROM_SET: [ "smembers", "get all set members", "https://redis.io/docs/latest/commands/smembers/" ],
|
|
28
|
+
GET_VALUE: [ "get", "get value", "https://redis.io/docs/latest/commands/get/" ],
|
|
29
|
+
HASH_GET: [ "hget", "hash get", "https://redis.io/docs/latest/commands/hget/" ],
|
|
30
|
+
HASH_GET_ALL: [ "hgetall", "hash get all", "https://redis.io/docs/latest/commands/hgetall/" ],
|
|
31
|
+
HASH_REMOVE: [ "hdel", "hash remove", "https://redis.io/docs/latest/commands/hdel/" ],
|
|
32
|
+
HASH_EXPIRE: [ "hexpire", "hash expire", "https://redis.io/docs/latest/commands/hexpire/" ],
|
|
33
|
+
HASH_SET: [ "hset", "", "https://redis.io/docs/latest/commands/hset/" ],
|
|
34
|
+
HASH_SET_MANY: [ "hmset", "(deprecated) use HSET with multiple fields", "https://redis.io/docs/latest/commands/hmset/" ],
|
|
35
|
+
IS_SET_MEMBER: [ "sismember", "", "https://redis.io/docs/latest/commands/sismember/" ],
|
|
36
|
+
JSON_ARRAY_APPEND: [ "json.arrappend", "", "https://redis.io/docs/latest/commands/json.arrappend/" ],
|
|
37
|
+
JSON_GET: [ "json.get", "", "https://redis.io/docs/latest/commands/json.get/" ],
|
|
38
|
+
JSON_MERGE: [ "json.merge", "", "https://redis.io/docs/latest/commands/json.merge/" ],
|
|
39
|
+
JSON_MGET: [ "json.mget", "", "https://redis.io/docs/latest/commands/json.mget/" ],
|
|
40
|
+
JSON_SET: [ "json.set", "", "https://redis.io/docs/latest/commands/json.set/" ],
|
|
41
|
+
KEYS: [ "keys", "(warning: O(N), use SCAN where possible)", "https://redis.io/docs/latest/commands/keys/" ],
|
|
42
|
+
LIST_PUSH: [ "lpush", "list push", "https://redis.io/docs/latest/commands/lpush/" ],
|
|
43
|
+
LIST_POP_TAIL_BLOCKING: [ "brpop", "list pop tail blocking", "https://redis.io/docs/latest/commands/brpop/" ],
|
|
44
|
+
LIST_POP_TAIL_PUSH_HEAD_BLOCKING: [ "brpoplpush", "list pop tail push head blocking", "https://redis.io/docs/latest/commands/brpoplpush/" ],
|
|
45
|
+
LIST_REMOVE: [ "lrem", "list remove", "https://redis.io/docs/latest/commands/lrem/" ],
|
|
46
|
+
SET_VALUE: [ "set", "set value", "https://redis.io/docs/latest/commands/set/" ],
|
|
47
|
+
UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/docs/latest/commands/sunion/" ]
|
|
48
|
+
} );
|
|
49
|
+
module.exports.cacheCommands = cacheCommandsEnum;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Enum for listing all client statuses.
|
|
53
|
+
*
|
|
54
|
+
* @readonly
|
|
55
|
+
* @enum {number}
|
|
56
|
+
* @typedef {number} TiRedisClientStatus
|
|
57
|
+
*/
|
|
58
|
+
let clientStatusEnum = tools.enum( {
|
|
59
|
+
UNINITIALIZED: [ 0, "uninitialized", "Redis client is offline and not yet initialized." ],
|
|
60
|
+
CONNECTED: [ 1, "connected", "Redis client is connected and online." ],
|
|
61
|
+
CONNECTING: [ 2, "connecting", "Redis client is connecting to server." ],
|
|
62
|
+
DISRUPTED: [ 3, "disrupted", "Redis client is temporarily disconnected from server due to a disruption." ],
|
|
63
|
+
SHUTTING_DOWN: [ 4, "shutting down", "Redis client is shutting down." ],
|
|
64
|
+
DISCONNECTED: [ 5, "disconnected", "Redis client is permanently disconnected from server." ]
|
|
65
|
+
} );
|
|
66
|
+
module.exports.clientStatus = clientStatusEnum;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enum for listing the Redis key override modes.
|
|
70
|
+
*
|
|
71
|
+
* @readonly
|
|
72
|
+
* @enum {string}
|
|
73
|
+
* @typedef {string} TiRedisOverrideMode
|
|
74
|
+
*/
|
|
75
|
+
let cacheOverrideModeEnum = tools.enum( {
|
|
76
|
+
DEFAULT: [ "", "default", "Standard Redis behaviour when setting new key." ],
|
|
77
|
+
NX: [ "nx", "nx", "Sets the key only if it does not already exist." ],
|
|
78
|
+
XX: [ "xx", "xx", "Sets the key only if it already exists." ]
|
|
79
|
+
} );
|
|
80
|
+
module.exports.cacheOverrideMode = cacheOverrideModeEnum;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Used to create a Redis Cache client.
|
|
84
|
+
* <br/>
|
|
85
|
+
* NOTE: This client is set to automatically resend all pending commands on connection recovery with no limit on the retry attempts.
|
|
86
|
+
* This is done to avoid losing any pending commands in case of a connection failure. For a different behavior, use custom implementation.
|
|
87
|
+
*
|
|
88
|
+
* @class RedisClient
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
class RedisClient {
|
|
92
|
+
|
|
93
|
+
#clientIdentifier;
|
|
94
|
+
#clientStatus = clientStatusEnum.UNINITIALIZED;
|
|
95
|
+
#retryMaxInterval = 1000;
|
|
96
|
+
#retryMaxAttempts = undefined;
|
|
97
|
+
#redisConnection = undefined;
|
|
98
|
+
#redisClientID;
|
|
99
|
+
#serverInfo = {};
|
|
100
|
+
#serverFeatures = {};
|
|
101
|
+
#connectionObservers = [];
|
|
102
|
+
#messageHandlersByChannel = new Map();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @constructor
|
|
106
|
+
* @param {string} identifier
|
|
107
|
+
* @returns {RedisClient}
|
|
108
|
+
*/
|
|
109
|
+
constructor( identifier ) {
|
|
110
|
+
this.#clientIdentifier = identifier || "redis-client-" + tools.getUUID();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Public interface */
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Used to return the Redis client identifier assigned internally.
|
|
117
|
+
*
|
|
118
|
+
* @property
|
|
119
|
+
* @returns {string}
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
get identifier() {
|
|
123
|
+
return this.#clientIdentifier;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Used to return the client ID assigned by the Redis server.
|
|
128
|
+
*
|
|
129
|
+
* @property
|
|
130
|
+
* @returns {number}
|
|
131
|
+
* @public
|
|
132
|
+
*/
|
|
133
|
+
get clientID() {
|
|
134
|
+
return this.#redisClientID;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Used to return the Redis client status.
|
|
139
|
+
*
|
|
140
|
+
* @property
|
|
141
|
+
* @returns {number}
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
get clientStatus() {
|
|
145
|
+
return this.#clientStatus;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Used to return the Redis server version.
|
|
150
|
+
*
|
|
151
|
+
* @property
|
|
152
|
+
* @returns {number}
|
|
153
|
+
* @public
|
|
154
|
+
*/
|
|
155
|
+
get serverVersion() {
|
|
156
|
+
return this.#serverInfo[ "redis_version" ];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Verify if Redis server supports JSON data types.
|
|
161
|
+
*
|
|
162
|
+
* @property
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
* @public
|
|
165
|
+
*/
|
|
166
|
+
get isJSONSupported() {
|
|
167
|
+
// Detect RedisJSON module variants (e.g., ReJSON, ReJSON2):
|
|
168
|
+
return !_.isNil( this.#serverFeatures[ "ReJSON" ] ) || !_.isNil( this.#serverFeatures[ "ReJSON2" ] );
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Used to initialize the Redis client.
|
|
173
|
+
*
|
|
174
|
+
* @method
|
|
175
|
+
* @param {string} host
|
|
176
|
+
* @param {number} port
|
|
177
|
+
* @param {string} authKey
|
|
178
|
+
* @param {string} user
|
|
179
|
+
* @param {number} defaultDB
|
|
180
|
+
* @param {number} [retryMaxIntervalMs=1000] Optional max backoff interval.
|
|
181
|
+
* @param {number|undefined} [retryMaxAttempts=undefined] Optional max (re)connection attempts before abort.
|
|
182
|
+
* @public
|
|
183
|
+
*/
|
|
184
|
+
initialize( host, port, authKey, user, defaultDB, retryMaxIntervalMs = 1000, retryMaxAttempts = undefined ) {
|
|
185
|
+
return new Promise( ( resolve, reject ) => {
|
|
186
|
+
if ( this.#redisConnection ) {
|
|
187
|
+
resolve();
|
|
188
|
+
} else {
|
|
189
|
+
this.#setupClient( host, port, authKey, user, defaultDB, retryMaxIntervalMs, retryMaxAttempts ).then( () => {
|
|
190
|
+
// Fetch the server information and store it:
|
|
191
|
+
return this.#fetchServerInfo();
|
|
192
|
+
} ).then( () => {
|
|
193
|
+
return this.#getClientID();
|
|
194
|
+
} ).then( ( clientID ) => {
|
|
195
|
+
// Store the connection ID:
|
|
196
|
+
this.#redisClientID = clientID;
|
|
197
|
+
resolve();
|
|
198
|
+
} ).catch( ( error ) => {
|
|
199
|
+
reject( exceptions.raise( error ) );
|
|
200
|
+
} );
|
|
201
|
+
}
|
|
202
|
+
} );
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Used to register a new {@link ConnectionObserver} for events related to the Redis connection state.
|
|
207
|
+
*
|
|
208
|
+
* @method
|
|
209
|
+
* @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
|
|
210
|
+
* @public
|
|
211
|
+
*/
|
|
212
|
+
addConnectionObserver( connectionObserver ) {
|
|
213
|
+
if ( connectionObserver instanceof ConnectionObserver ) {
|
|
214
|
+
this.#connectionObservers.push( connectionObserver );
|
|
215
|
+
} else {
|
|
216
|
+
logger.log( `Attempting to add '${ connectionObserver.constructor.name }' as connection observer but it's not a child-class of 'ConnectionObserver'!`, logger.logSeverity.WARNING );
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Used to execute multiple commands within a Redis transaction.
|
|
222
|
+
*
|
|
223
|
+
* @method
|
|
224
|
+
* @param {Array[]} commands
|
|
225
|
+
* @returns {Promise<*>}
|
|
226
|
+
* @public
|
|
227
|
+
*/
|
|
228
|
+
executeCommands( commands ) {
|
|
229
|
+
return new Promise( ( resolve, reject ) => {
|
|
230
|
+
this.#redisConnection.multi( commands ).exec().then( ( results ) => {
|
|
231
|
+
resolve( results );
|
|
232
|
+
} ).catch( ( error ) => {
|
|
233
|
+
reject( exceptions.raise( error ) );
|
|
234
|
+
} );
|
|
235
|
+
} );
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Used to send a new blocking command to Redis.
|
|
240
|
+
* <br/>
|
|
241
|
+
* WARNING: This will reserve the client connection until a result is received.
|
|
242
|
+
*
|
|
243
|
+
* @method
|
|
244
|
+
* @param {string} command
|
|
245
|
+
* @param {Array} commandArguments
|
|
246
|
+
* @returns {Promise<*>}
|
|
247
|
+
* @public
|
|
248
|
+
*/
|
|
249
|
+
blockingCommand( command, commandArguments ) {
|
|
250
|
+
return new Promise( ( resolve, reject ) => {
|
|
251
|
+
this.#redisConnection[ command ].apply( this.#redisConnection, commandArguments ).then( ( results ) => {
|
|
252
|
+
resolve( results );
|
|
253
|
+
} ).catch( ( error ) => {
|
|
254
|
+
reject( exceptions.raise( error ) );
|
|
255
|
+
} );
|
|
256
|
+
} );
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Used to publish a message to the specified channel.
|
|
261
|
+
*
|
|
262
|
+
* @method
|
|
263
|
+
* @param {string} channel
|
|
264
|
+
* @param {(Object|string)} message
|
|
265
|
+
* @returns {Promise<number>}
|
|
266
|
+
* @public
|
|
267
|
+
*/
|
|
268
|
+
publishCommand( channel, message ) {
|
|
269
|
+
return new Promise( ( resolve, reject ) => {
|
|
270
|
+
this.#redisConnection.publish( channel, tools.stringifyJSON( message ) ).then( ( receivedBy ) => {
|
|
271
|
+
resolve( receivedBy );
|
|
272
|
+
} ).catch( ( error ) => {
|
|
273
|
+
reject( exceptions.raise( error ) );
|
|
274
|
+
} );
|
|
275
|
+
} );
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Used to subscribe to the specified channel for messages.
|
|
280
|
+
* <br/>
|
|
281
|
+
* NOTE: Call unsubscribeCommand(channel) to detach later.
|
|
282
|
+
*
|
|
283
|
+
* @method
|
|
284
|
+
* @param {string} channel Unique identifier of the channel to subscribe to.
|
|
285
|
+
* @param {function( Object )} messageHandler Will execute this handler every time a new message is received.
|
|
286
|
+
* @returns {Promise}
|
|
287
|
+
* @public
|
|
288
|
+
*/
|
|
289
|
+
subscribeCommand( channel, messageHandler ) {
|
|
290
|
+
return new Promise( ( resolve, reject ) => {
|
|
291
|
+
// Avoid attaching multiple listeners to the same channel:
|
|
292
|
+
if ( this.#messageHandlersByChannel.has( channel ) ) {
|
|
293
|
+
logger.log( "Attempting to subscribe to message channel '" + channel + "' while already subscribed to it.", logger.logSeverity.WARNING );
|
|
294
|
+
resolve();
|
|
295
|
+
} else {
|
|
296
|
+
const onMessage = ( subscribedChannel, message ) => {
|
|
297
|
+
if ( subscribedChannel === channel && typeof messageHandler === "function" ) {
|
|
298
|
+
messageHandler( tools.parseJSON( message ) );
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
this.#redisConnection.once( "subscribe", ( subscribedChannel ) => {
|
|
303
|
+
if ( subscribedChannel === channel ) {
|
|
304
|
+
logger.log( "Subscription to message channel '" + channel + "' successful.", logger.logSeverity.DEBUG );
|
|
305
|
+
resolve();
|
|
306
|
+
}
|
|
307
|
+
} );
|
|
308
|
+
|
|
309
|
+
this.#redisConnection.on( "message", onMessage );
|
|
310
|
+
this.#messageHandlersByChannel.set( channel, onMessage );
|
|
311
|
+
|
|
312
|
+
this.#redisConnection.subscribe( channel ).catch( ( error ) => {
|
|
313
|
+
// Cleanup partial state if subscribe fails:
|
|
314
|
+
const handler = this.#messageHandlersByChannel.get( channel );
|
|
315
|
+
if ( handler ) {
|
|
316
|
+
this.#redisConnection.off( "message", handler );
|
|
317
|
+
this.#messageHandlersByChannel.delete( channel );
|
|
318
|
+
}
|
|
319
|
+
reject( exceptions.raise( error ) );
|
|
320
|
+
} );
|
|
321
|
+
}
|
|
322
|
+
} );
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Used to unsubscribe from a channel and remove its message handler.
|
|
327
|
+
*
|
|
328
|
+
* @method
|
|
329
|
+
* @param {string} channel Unique identifier of the channel to unsubscribe from.
|
|
330
|
+
* @returns {Promise}
|
|
331
|
+
* @public
|
|
332
|
+
*/
|
|
333
|
+
unsubscribeCommand( channel ) {
|
|
334
|
+
return new Promise( ( resolve, reject ) => {
|
|
335
|
+
const handler = this.#messageHandlersByChannel.get( channel );
|
|
336
|
+
if ( handler ) {
|
|
337
|
+
this.#redisConnection.off( "message", handler );
|
|
338
|
+
this.#messageHandlersByChannel.delete( channel );
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this.#redisConnection.unsubscribe( channel ).then( () => {
|
|
342
|
+
resolve();
|
|
343
|
+
} ).catch( ( error ) => {
|
|
344
|
+
reject( exceptions.raise( error ) );
|
|
345
|
+
} );
|
|
346
|
+
} );
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Used to execute any Redis command in an unmanaged way.
|
|
351
|
+
* <br/>
|
|
352
|
+
* WARNING: Use this only if there is no other implemented function in this module and the command
|
|
353
|
+
* you want to execute is not supported by the 'multi' Redis command (implemented in {@link RedisClient.executeCommands}).
|
|
354
|
+
* Make sure to handle the result as it will be returned raw.
|
|
355
|
+
*
|
|
356
|
+
* @method
|
|
357
|
+
* @param {string[]} commandArguments
|
|
358
|
+
* @returns {Promise<Object>}
|
|
359
|
+
* @public
|
|
360
|
+
*/
|
|
361
|
+
callCommand( commandArguments ) {
|
|
362
|
+
return new Promise( ( resolve, reject ) => {
|
|
363
|
+
this.#redisConnection[ "call" ].apply( this.#redisConnection, commandArguments ).then( ( result ) => {
|
|
364
|
+
resolve( result );
|
|
365
|
+
} ).catch( ( error ) => {
|
|
366
|
+
reject( exceptions.raise( error ) );
|
|
367
|
+
} );
|
|
368
|
+
} );
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Used to gracefully close the Redis connection.
|
|
373
|
+
* Attempts to quit(), then falls back to disconnect() on timeout.
|
|
374
|
+
*
|
|
375
|
+
* @method
|
|
376
|
+
* @param {number} [timeoutMs=1000]
|
|
377
|
+
* @returns {Promise}
|
|
378
|
+
* @public
|
|
379
|
+
*/
|
|
380
|
+
shutDown( timeoutMs = 1000 ) {
|
|
381
|
+
return new Promise( ( resolve ) => {
|
|
382
|
+
let finished = false;
|
|
383
|
+
this.#clientStatus = clientStatusEnum.SHUTTING_DOWN;
|
|
384
|
+
const done = () => {
|
|
385
|
+
if ( !finished ) {
|
|
386
|
+
finished = true;
|
|
387
|
+
resolve();
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const timeout = setTimeout( () => {
|
|
392
|
+
try {
|
|
393
|
+
this.#redisConnection.disconnect();
|
|
394
|
+
} catch {
|
|
395
|
+
// do nothing here...
|
|
396
|
+
}
|
|
397
|
+
done();
|
|
398
|
+
}, timeoutMs );
|
|
399
|
+
|
|
400
|
+
this.#redisConnection.quit().then( () => {
|
|
401
|
+
clearTimeout( timeout );
|
|
402
|
+
done();
|
|
403
|
+
} ).catch( () => {
|
|
404
|
+
clearTimeout( timeout );
|
|
405
|
+
try {
|
|
406
|
+
this.#redisConnection.disconnect();
|
|
407
|
+
} catch {
|
|
408
|
+
// do nothing here...
|
|
409
|
+
}
|
|
410
|
+
done();
|
|
411
|
+
} );
|
|
412
|
+
} );
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* Private interface */
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Used to set up the connection to Redis server.
|
|
419
|
+
* <br/>
|
|
420
|
+
* NOTE: This method will only resolve once the server sends a 'ready' event.
|
|
421
|
+
*
|
|
422
|
+
* @method
|
|
423
|
+
* @param {string} host
|
|
424
|
+
* @param {number} port
|
|
425
|
+
* @param {string} authKey
|
|
426
|
+
* @param {string} user
|
|
427
|
+
* @param {number} defaultDB
|
|
428
|
+
* @param {number} [retryMaxIntervalMs=1000] Optional max backoff interval.
|
|
429
|
+
* @param {number|undefined} [retryMaxAttempts=undefined] Optional max (re)connection attempts before abort.
|
|
430
|
+
* @returns {Promise}
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
#setupClient( host, port, authKey, user, defaultDB, retryMaxIntervalMs, retryMaxAttempts ) {
|
|
434
|
+
return new Promise( ( resolve, reject ) => {
|
|
435
|
+
try {
|
|
436
|
+
this.#retryMaxInterval = retryMaxIntervalMs;
|
|
437
|
+
this.#retryMaxAttempts = retryMaxAttempts;
|
|
438
|
+
|
|
439
|
+
let retryStrategy = ( attempt ) => {
|
|
440
|
+
let retryInterval = Math.min( attempt * 50, this.#retryMaxInterval );
|
|
441
|
+
if ( this.#retryMaxAttempts != null && attempt > this.#retryMaxAttempts ) {
|
|
442
|
+
logger.log( "In Redis retry strategy: reached max attempts for command retry. Aborting...", logger.logSeverity.WARNING, { attempts: attempt } );
|
|
443
|
+
retryInterval = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
|
|
444
|
+
}
|
|
445
|
+
return retryInterval;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
let reconnectOnError = ( error ) => {
|
|
449
|
+
logger.log( `In Redis reconnect on error strategy: ${ error.message }`, logger.logSeverity.ERROR, error );
|
|
450
|
+
if ( error.message.includes( "READONLY" ) ) {
|
|
451
|
+
// Returning 2 will also resubmit the failed command:
|
|
452
|
+
return 2;
|
|
453
|
+
} else {
|
|
454
|
+
return 0;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
let options = {
|
|
459
|
+
port: port,
|
|
460
|
+
host: host,
|
|
461
|
+
username: user,
|
|
462
|
+
password: authKey,
|
|
463
|
+
db: defaultDB,
|
|
464
|
+
autoResendUnfulfilledCommands: true,
|
|
465
|
+
maxRetriesPerRequest: null,
|
|
466
|
+
retryStrategy: retryStrategy,
|
|
467
|
+
reconnectOnError: reconnectOnError
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
/** @type Redis */
|
|
471
|
+
this.#redisConnection = new Redis( options );
|
|
472
|
+
this.#clientStatus = clientStatusEnum.CONNECTING;
|
|
473
|
+
|
|
474
|
+
this.#redisConnection.once( "ready", () => {
|
|
475
|
+
this.#clientStatus = clientStatusEnum.CONNECTED;
|
|
476
|
+
logger.log( `Connection to Redis server '${ this.#redisConnection.options.host }:${ this.#redisConnection.options.port }' established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
477
|
+
this.#notifyConnectionObservers();
|
|
478
|
+
|
|
479
|
+
this.#redisConnection.on( "ready", () => {
|
|
480
|
+
this.#clientStatus = clientStatusEnum.CONNECTED;
|
|
481
|
+
logger.log( `Connection to Redis server '${ this.#redisConnection.options.host }:${ this.#redisConnection.options.port }' reestablished by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
482
|
+
this.#notifyConnectionObservers();
|
|
483
|
+
} );
|
|
484
|
+
|
|
485
|
+
resolve();
|
|
486
|
+
} );
|
|
487
|
+
this.#redisConnection.on( "error", ( error ) => {
|
|
488
|
+
this.#clientStatus = clientStatusEnum.DISRUPTED;
|
|
489
|
+
logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
|
|
490
|
+
this.#notifyConnectionObservers();
|
|
491
|
+
} );
|
|
492
|
+
this.#redisConnection.on( "reconnecting", ( retryInterval ) => {
|
|
493
|
+
this.#clientStatus = clientStatusEnum.CONNECTING;
|
|
494
|
+
logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ retryInterval } ms.`, logger.logSeverity.DEBUG );
|
|
495
|
+
} );
|
|
496
|
+
this.#redisConnection.on( "end", () => {
|
|
497
|
+
if ( this.#clientStatus !== clientStatusEnum.SHUTTING_DOWN ) {
|
|
498
|
+
this.#clientStatus = clientStatusEnum.DISCONNECTED;
|
|
499
|
+
logger.log( `Client '${ this.identifier }' cannot reconnect to Redis server and has been shut down.`, logger.logSeverity.WARNING );
|
|
500
|
+
this.#notifyConnectionObservers();
|
|
501
|
+
}
|
|
502
|
+
} );
|
|
503
|
+
} catch ( error ) {
|
|
504
|
+
reject( exceptions.raise( error ) );
|
|
505
|
+
}
|
|
506
|
+
} );
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Used to notify all connection observers about the current connection state.
|
|
511
|
+
*
|
|
512
|
+
* @method
|
|
513
|
+
* @private
|
|
514
|
+
*/
|
|
515
|
+
#notifyConnectionObservers() {
|
|
516
|
+
// Notify all connection observers about the event:
|
|
517
|
+
_.forEach( this.#connectionObservers, ( connectionObserver ) => {
|
|
518
|
+
if ( this.#clientStatus === clientStatusEnum.CONNECTED ) {
|
|
519
|
+
connectionObserver.onConnectionRecovered( this.#clientIdentifier );
|
|
520
|
+
} else if ( this.#clientStatus === clientStatusEnum.DISRUPTED ) {
|
|
521
|
+
connectionObserver.onConnectionDisrupted( this.#clientIdentifier );
|
|
522
|
+
} else if ( this.#clientStatus === clientStatusEnum.DISCONNECTED ) {
|
|
523
|
+
connectionObserver.onConnectionLost( this.#clientIdentifier );
|
|
524
|
+
}
|
|
525
|
+
} );
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Used to fetch and store Redis server information.
|
|
530
|
+
*
|
|
531
|
+
* @method
|
|
532
|
+
* @returns {Promise}
|
|
533
|
+
* @private
|
|
534
|
+
*/
|
|
535
|
+
#fetchServerInfo() {
|
|
536
|
+
return new Promise( ( resolve, reject ) => {
|
|
537
|
+
this.#redisConnection.info().then( ( result ) => {
|
|
538
|
+
this.#serverInfo = {};
|
|
539
|
+
if ( _.isString( result ) ) {
|
|
540
|
+
let rawData = _.split( result, "\r\n" );
|
|
541
|
+
_.forEach( rawData, ( entry ) => {
|
|
542
|
+
let details = _.split( entry, ":" );
|
|
543
|
+
if ( !_.startsWith( details[ 0 ], "#" ) && details[ 0 ] !== "" && details[ 0 ] ) {
|
|
544
|
+
if ( _.isNaN( _.toNumber( details[ 1 ] ) ) ) {
|
|
545
|
+
this.#serverInfo[ details[ 0 ] ] = details[ 1 ];
|
|
546
|
+
} else {
|
|
547
|
+
this.#serverInfo[ details[ 0 ] ] = _.toNumber( details[ 1 ] );
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} );
|
|
551
|
+
}
|
|
552
|
+
return this.#redisConnection.module( "LIST" );
|
|
553
|
+
} ).then( ( result ) => {
|
|
554
|
+
this.#serverFeatures = {};
|
|
555
|
+
if ( _.isArray( result ) ) {
|
|
556
|
+
_.forEach( result, ( entry ) => {
|
|
557
|
+
this.#serverFeatures[ entry[ 1 ] ] = entry[ 3 ];
|
|
558
|
+
} );
|
|
559
|
+
}
|
|
560
|
+
resolve();
|
|
561
|
+
} ).catch( ( error ) => {
|
|
562
|
+
logger.log( `Failed to fetch Redis server information by client '${ this.identifier }'!`, logger.logSeverity.WARNING, error );
|
|
563
|
+
reject( exceptions.raise( error ) );
|
|
564
|
+
} );
|
|
565
|
+
} );
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Used to fetch and store the Redis client ID.
|
|
570
|
+
*
|
|
571
|
+
* @method
|
|
572
|
+
* @returns {Promise<number>}
|
|
573
|
+
* @private
|
|
574
|
+
*/
|
|
575
|
+
#getClientID() {
|
|
576
|
+
let commandArguments = [ "client", "id" ];
|
|
577
|
+
return this.callCommand( commandArguments ).then( ( clientID ) => Number( clientID ) );
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Create and return a new Redis client.
|
|
584
|
+
*
|
|
585
|
+
* @method
|
|
586
|
+
* @param {string} identifier
|
|
587
|
+
* @return {RedisClient}
|
|
588
|
+
* @public
|
|
589
|
+
*/
|
|
590
|
+
module.exports.createRedisClient = ( identifier ) => {
|
|
591
|
+
return Object.freeze( new RedisClient( identifier ) );
|
|
592
592
|
};
|