@ti-engine/core 1.2.3 → 1.3.1

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.
@@ -28,6 +28,7 @@ let cacheCommandsEnum = tools.enum( {
28
28
  HASH_GET: [ "hget", "hash get", "https://redis.io/docs/latest/commands/hget/" ],
29
29
  HASH_GET_ALL: [ "hgetall", "hash get all", "https://redis.io/docs/latest/commands/hgetall/" ],
30
30
  HASH_REMOVE: [ "hdel", "hash remove", "https://redis.io/docs/latest/commands/hdel/" ],
31
+ HASH_EXPIRE: [ "hexpire", "hash expire", "https://redis.io/docs/latest/commands/hexpire/" ],
31
32
  HASH_SET: [ "hset", "", "https://redis.io/docs/latest/commands/hset/" ],
32
33
  HASH_SET_MANY: [ "hmset", "(deprecated) use HSET with multiple fields", "https://redis.io/docs/latest/commands/hmset/" ],
33
34
  IS_SET_MEMBER: [ "sismember", "", "https://redis.io/docs/latest/commands/sismember/" ],
@@ -43,6 +44,21 @@ let cacheCommandsEnum = tools.enum( {
43
44
  UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/docs/latest/commands/sunion/" ]
44
45
  } );
45
46
 
47
+ /**
48
+ * Enum for listing all client statuses.
49
+ *
50
+ * @readonly
51
+ * @enum {number}
52
+ */
53
+ let clientStatusEnum = tools.enum( {
54
+ UNINITIALIZED: [ 0, "uninitialized", "Redis client is offline and not yet initialized." ],
55
+ CONNECTED: [ 1, "connected", "Redis client is connected and online." ],
56
+ CONNECTING: [ 2, "connecting", "Redis client is connecting to server." ],
57
+ DISRUPTED: [ 3, "disrupted", "Redis client is temporarily disconnected from server due to a disruption." ],
58
+ SHUTTING_DOWN: [ 4, "shutting down", "Redis client is shutting down." ],
59
+ DISCONNECTED: [ 5, "disconnected", "Redis client is permanently disconnected from server." ]
60
+ } );
61
+
46
62
  /**
47
63
  * Enum for listing the Redis key override modes.
48
64
  *
@@ -63,6 +79,10 @@ module.exports.cacheCommands = cacheCommandsEnum;
63
79
  * @typedef {string} TiRedisOverrideMode
64
80
  */
65
81
  module.exports.cacheOverrideMode = cacheOverrideModeEnum;
82
+ /**
83
+ * @typedef {number} TiRedisClientStatus
84
+ */
85
+ module.exports.clientStatus = clientStatusEnum;
66
86
 
67
87
  /**
68
88
  * Used to create a Redis Cache client.
@@ -76,6 +96,7 @@ module.exports.cacheOverrideMode = cacheOverrideModeEnum;
76
96
  class RedisClient {
77
97
 
78
98
  #clientIdentifier;
99
+ #clientStatus = clientStatusEnum.UNINITIALIZED;
79
100
  #retryMaxInterval = 1000;
80
101
  #retryMaxAttempts = undefined;
81
102
  #redisConnection = undefined;
@@ -118,6 +139,17 @@ class RedisClient {
118
139
  return this.#redisClientID;
119
140
  }
120
141
 
142
+ /**
143
+ * Used to return the Redis client status.
144
+ *
145
+ * @property
146
+ * @returns {number}
147
+ * @public
148
+ */
149
+ get clientStatus() {
150
+ return this.#clientStatus;
151
+ }
152
+
121
153
  /**
122
154
  * Used to return the Redis server version.
123
155
  *
@@ -167,12 +199,6 @@ class RedisClient {
167
199
  } ).then( ( clientID ) => {
168
200
  // Store the client ID:
169
201
  this.#redisClientID = clientID;
170
-
171
- // Notify all connection observers about the initialization success:
172
- _.forEach( this.#connectionObservers, ( connectionObservers ) => {
173
- connectionObservers.onConnectionRecovered( this.#clientIdentifier );
174
- } );
175
-
176
202
  resolve();
177
203
  } ).catch( ( error ) => {
178
204
  reject( exceptions.raise( error ) );
@@ -359,6 +385,7 @@ class RedisClient {
359
385
  shutDown( timeoutMs = 1000 ) {
360
386
  return new Promise( ( resolve ) => {
361
387
  let finished = false;
388
+ this.#clientStatus = clientStatusEnum.SHUTTING_DOWN;
362
389
  const done = () => {
363
390
  if ( !finished ) {
364
391
  finished = true;
@@ -403,7 +430,8 @@ class RedisClient {
403
430
  * @param {number} defaultDB
404
431
  * @param {number} [retryMaxIntervalMs=1000] Optional max backoff interval.
405
432
  * @param {number|undefined} [retryMaxAttempts=undefined] Optional max (re)connection attempts before abort.
406
- * @public
433
+ * @returns {Promise}
434
+ * @private
407
435
  */
408
436
  #setupClient( host, port, authKey, user, defaultDB, retryMaxIntervalMs, retryMaxAttempts ) {
409
437
  return new Promise( ( resolve, reject ) => {
@@ -412,19 +440,22 @@ class RedisClient {
412
440
  this.#retryMaxAttempts = retryMaxAttempts;
413
441
 
414
442
  let retryStrategy = ( attempt ) => {
415
- let result = Math.min( attempt * 50, this.#retryMaxInterval );
416
-
443
+ let retryInterval = Math.min( attempt * 50, this.#retryMaxInterval );
417
444
  if ( this.#retryMaxAttempts != null && attempt > this.#retryMaxAttempts ) {
418
445
  logger.log( "In Redis retry strategy: reached max attempts for command retry. Aborting...", logger.logSeverity.WARNING, { attempts: attempt } );
419
- result = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
446
+ retryInterval = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
420
447
  }
421
-
422
- return result;
448
+ return retryInterval;
423
449
  };
424
450
 
425
451
  let reconnectOnError = ( error ) => {
426
452
  logger.log( `In Redis reconnect on error strategy: ${ error.message }`, logger.logSeverity.ERROR, error );
427
- return !!error.message.includes( "READONLY" );
453
+ if ( error.message.includes( "READONLY" ) ) {
454
+ // Returning 2 will also resubmit the failed command:
455
+ return 2;
456
+ } else {
457
+ return 0;
458
+ }
428
459
  };
429
460
 
430
461
  let options = {
@@ -441,22 +472,33 @@ class RedisClient {
441
472
 
442
473
  /** @type Redis */
443
474
  this.#redisConnection = new Redis( options );
475
+ this.#clientStatus = clientStatusEnum.CONNECTING;
476
+
477
+ this.#redisConnection.once( "ready", () => {
478
+ this.#clientStatus = clientStatusEnum.CONNECTED;
479
+ this.#notifyConnectionObservers();
480
+
481
+ this.#redisConnection.on( "ready", () => {
482
+ this.#clientStatus = clientStatusEnum.CONNECTED;
483
+ this.#notifyConnectionObservers();
484
+ } );
444
485
 
445
- this.#redisConnection.on( "ready", () => {
446
- logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
447
486
  resolve();
448
487
  } );
449
488
  this.#redisConnection.on( "error", ( error ) => {
489
+ this.#clientStatus = clientStatusEnum.DISRUPTED;
450
490
  logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
451
-
452
- // Notify all connection observers about this event:
453
- _.forEach( this.#connectionObservers, ( connectionObservers ) => {
454
- connectionObservers.onConnectionDisrupted( this.#clientIdentifier );
455
- } );
491
+ this.#notifyConnectionObservers();
492
+ } );
493
+ this.#redisConnection.on( "reconnecting", ( retryInterval ) => {
494
+ this.#clientStatus = clientStatusEnum.CONNECTING;
495
+ logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ retryInterval } ms.`, logger.logSeverity.DEBUG );
456
496
  } );
457
- this.#redisConnection.on( "reconnecting", ( info ) => {
458
- if ( info.attempt > 1 ) {
459
- logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ info.delay } ms. This is attempt ${ info.attempt }.`, logger.logSeverity.DEBUG );
497
+ this.#redisConnection.on( "end", () => {
498
+ if ( this.#clientStatus !== clientStatusEnum.SHUTTING_DOWN ) {
499
+ this.#clientStatus = clientStatusEnum.DISCONNECTED;
500
+ logger.log( `Client '${ this.identifier }' cannot reconnect to Redis server and has been shut down.`, logger.logSeverity.WARNING );
501
+ this.#notifyConnectionObservers();
460
502
  }
461
503
  } );
462
504
  } catch ( error ) {
@@ -465,10 +507,31 @@ class RedisClient {
465
507
  } );
466
508
  }
467
509
 
510
+ /**
511
+ * Used to notify all connection observers about the current connection state.
512
+ *
513
+ * @method
514
+ * @private
515
+ */
516
+ #notifyConnectionObservers() {
517
+ // Notify all connection observers about the event:
518
+ _.forEach( this.#connectionObservers, ( connectionObservers ) => {
519
+ if ( this.#clientStatus === clientStatusEnum.CONNECTED ) {
520
+ logger.log( `Connection to Redis server ${ this.#redisConnection.options.host }:${ this.#redisConnection.options.port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
521
+ connectionObservers.onConnectionRecovered( this.#clientIdentifier );
522
+ } else if ( this.#clientStatus === clientStatusEnum.DISRUPTED ) {
523
+ connectionObservers.onConnectionDisrupted( this.#clientIdentifier );
524
+ } else if ( this.#clientStatus === clientStatusEnum.DISCONNECTED ) {
525
+ connectionObservers.onConnectionLost( this.#clientIdentifier );
526
+ }
527
+ } );
528
+ }
529
+
468
530
  /**
469
531
  * Used to fetch and store Redis server information.
470
532
  *
471
533
  * @method
534
+ * @returns {Promise}
472
535
  * @private
473
536
  */
474
537
  #fetchServerInfo() {
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@ti-engine/core",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "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.",
5
5
  "author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
6
6
  "license": "GPL-3.0-or-later",
7
7
  "exports": {
8
8
  ".": "./bin/start-instance.js",
9
+ "./cache": "./utils/cache.js",
9
10
  "./exceptions": "./utils/exceptions.js",
10
11
  "./localization": "./utils/localization.js",
11
12
  "./logger": "./utils/logger.js",
package/utils/cache.js CHANGED
@@ -124,6 +124,22 @@ class CommonMemoryCache extends ConnectionObserver {
124
124
  }
125
125
  }
126
126
 
127
+ /**
128
+ * Needs to be invoked by the connection handler when the connection is irrevocably lost.
129
+ *
130
+ * @method
131
+ * @param {string} identifier The identifier of the observed connection.
132
+ * @override
133
+ * @public
134
+ */
135
+ onConnectionLost( identifier ) {
136
+ if ( identifier === this.#connectionIdentifier ) {
137
+ this.#isOperational = false;
138
+ // TODO: implement forced shut down of the service instance instead of crashing it outright
139
+ throw exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE );
140
+ }
141
+ }
142
+
127
143
  /**
128
144
  * Used to register a new {@link ConnectionObserver} for events related to the underlying Redis connection state.
129
145
  *
@@ -317,13 +333,19 @@ class CommonMemoryCache extends ConnectionObserver {
317
333
  * @method
318
334
  * @param {string} key
319
335
  * @param {number} seconds
336
+ * @param {string} [name] If you need to expire a field in a hash set instead, provide the name of the set here.
320
337
  * @returns {Promise<number>} This will resolve with the seconds as provided initially by the caller.
321
338
  * @public
322
339
  */
323
- expireValue( key, seconds ) {
340
+ expireValue( key, seconds, name ) {
324
341
  return new Promise( ( resolve, reject ) => {
325
342
  if ( this.#isOperational === true ) {
326
- let commandExpire = [ redis.cacheCommands.EXPIRE, key, seconds ];
343
+ let commandExpire = [];
344
+ if ( name ) {
345
+ commandExpire = [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ];
346
+ } else {
347
+ commandExpire = [ redis.cacheCommands.EXPIRE, key, seconds ];
348
+ }
327
349
  this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
328
350
  resolve( seconds );
329
351
  } ).catch( ( error ) => {
@@ -508,7 +530,7 @@ class CommonMemoryCache extends ConnectionObserver {
508
530
  hashSetField( key, name, value ) {
509
531
  return new Promise( ( resolve, reject ) => {
510
532
  if ( this.#isOperational === true ) {
511
- let commandHashSetField = [ redis.cacheCommands.HASH_SET_MANY, key, name, tools.stringifyJSON( value ) ];
533
+ let commandHashSetField = [ redis.cacheCommands.HASH_SET, key, name, tools.stringifyJSON( value ) ];
512
534
  this.#redisClient.executeCommands( [ commandHashSetField ] ).then( () => {
513
535
  resolve();
514
536
  } ).catch( ( error ) => {
@@ -535,10 +557,10 @@ class CommonMemoryCache extends ConnectionObserver {
535
557
  hashSetFields( key, fields ) {
536
558
  return new Promise( ( resolve, reject ) => {
537
559
  if ( this.#isOperational === true ) {
538
- let commandHashSetFields = [ redis.cacheCommands.HASH_SET_MANY, key ];
560
+ let commandHashSetFields = [ redis.cacheCommands.HASH_SET, key ];
539
561
  _.forEach( fields, ( field ) => {
540
562
  commandHashSetFields.push( field.name );
541
- commandHashSetFields.push( _.isObjectLike( field.value ) ? tools.stringifyJSON( field.value ) : field.value );
563
+ commandHashSetFields.push( tools.stringifyJSON( field.value ) );
542
564
  } );
543
565
  this.#redisClient.executeCommands( [ commandHashSetFields ] ).then( () => {
544
566
  resolve();
@@ -576,6 +598,31 @@ class CommonMemoryCache extends ConnectionObserver {
576
598
  } );
577
599
  }
578
600
 
601
+ /**
602
+ * Used to remove a single field from a hash.
603
+ *
604
+ * @method
605
+ * @param {string} key
606
+ * @param {string} field
607
+ * @return {Promise<boolean>} Will return 'true' if the field was removed, 'false' otherwise.
608
+ * @public
609
+ */
610
+ hashDeleteField( key, field ) {
611
+ return new Promise( ( resolve, reject ) => {
612
+ if ( this.#isOperational === true ) {
613
+ let commandHashGetField = [ redis.cacheCommands.HASH_REMOVE, key, field ];
614
+ this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
615
+ results = results[ 0 ];
616
+ resolve( ( results && results.length > 1 ) ? tools.toBool( results[ 1 ] ) : false );
617
+ } ).catch( ( error ) => {
618
+ reject( error );
619
+ } );
620
+ } else {
621
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
622
+ }
623
+ } );
624
+ }
625
+
579
626
  /**
580
627
  * Used to store a JSON variable.
581
628
  * <br/>
@@ -38,6 +38,7 @@ let exceptionCodeEnum = tools.enum( {
38
38
  E_COM_SERVICE_NOT_FOUND: [ 3004, "service not found", "The specified service is not found in the service definition interface." ],
39
39
  E_COM_SERVICE_HANDLER_NOT_FOUND: [ 3005, "service handler not found", "No handler found in the interface for the specified service or service version." ],
40
40
  E_COM_MESSAGE_RECEIVER_UNAVAILABLE: [ 3006, "message receiver unavailable", "The message receiver instance is currently unavailable." ],
41
+ E_COM_MESSAGE_EXCHANGE_BROKEN: [ 3007, "message exchange broken", "The message exchange is irrevocably broken and cannot be used any longer." ],
41
42
  // E_COM_UNRECOGNIZED_API_URL: [ 301, "unrecognized api url", "Attempt to access unrecognized or invalid API URL." ],
42
43
  // E_COM_MISSING_REQUIRED_ARGUMENTS: [ 302, "missing required arguments", "Attempt to execute operation without all required arguments." ],
43
44
  // E_COM_UNRECOGNIZED_RESPONSE_STRUCTURE: [ 303, "unrecognized response structure", "The received response has unrecognized structure and cannot be parsed or examined." ],
@@ -248,4 +249,4 @@ module.exports.raise = ( source, data, exceptionID ) => {
248
249
  */
249
250
  module.exports.isException = ( object ) => {
250
251
  return ( object instanceof Exception );
251
- };
252
+ };
package/utils/tools.js CHANGED
@@ -319,8 +319,7 @@ module.exports.retrocycle = ( $ ) => {
319
319
  * @public
320
320
  */
321
321
  module.exports.stringifyJSON = ( value ) => {
322
- let transformed = module.exports.decycle( value );
323
- return _.isObjectLike( transformed ) ? JSON.stringify( _.toPlainObject( transformed ) ) : value;
322
+ return _.isObjectLike( value ) ? JSON.stringify( _.toPlainObject( module.exports.decycle( value ) ) ) : value;
324
323
  };
325
324
 
326
325
  /**
@@ -537,4 +536,4 @@ class RetryPolicy {
537
536
 
538
537
  }
539
538
 
540
- module.exports.RetryPolicy = RetryPolicy;
539
+ module.exports.RetryPolicy = RetryPolicy;