@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +383 -377
  2. package/LICENSE.md +321 -321
  3. package/README.md +597 -597
  4. package/bin/localization/labels.json +122 -122
  5. package/bin/settings.json +41 -41
  6. package/bin/start-instance.js +164 -164
  7. package/components/auditing.js +191 -191
  8. package/components/connection-observer.js +72 -72
  9. package/components/definitions.types.js +248 -248
  10. package/components/exchange/default/default-message-exchange.js +136 -136
  11. package/components/exchange/default/default-message-receiver.js +101 -101
  12. package/components/exchange/default/default-message-sender.js +100 -100
  13. package/components/exchange/message-dispatcher.js +168 -168
  14. package/components/exchange/message-exchange.js +449 -449
  15. package/components/exchange/message-handler.js +235 -235
  16. package/components/exchange/message-memory-cache.js +190 -190
  17. package/components/exchange/message-observer.js +126 -126
  18. package/components/exchange/message-receiver.js +181 -181
  19. package/components/exchange/message-sender.js +143 -143
  20. package/components/exchange/message-tracer.js +212 -212
  21. package/components/service-caller.js +370 -370
  22. package/components/service-consumer.js +131 -131
  23. package/components/service-executor.js +278 -278
  24. package/components/service-instance.js +316 -316
  25. package/components/service-provider.js +251 -251
  26. package/integrations/redis-integration.js +591 -591
  27. package/package.json +89 -89
  28. package/utils/cache.js +772 -772
  29. package/utils/config.js +103 -103
  30. package/utils/exceptions.js +368 -368
  31. package/utils/localization.js +298 -298
  32. package/utils/logger.js +82 -82
  33. 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
  };