@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.
- package/CHANGELOG.md +32 -0
- package/bin/localization/labels.json +64 -64
- package/bin/start-instance.js +1 -1
- package/components/connection-observer.js +19 -4
- package/components/exchange/message-exchange.js +16 -1
- package/components/exchange/message-handler.js +37 -7
- package/components/exchange/message-observer.js +51 -4
- package/components/exchange/message-receiver.js +1 -1
- package/components/service-caller.js +247 -146
- package/components/service-executor.js +19 -4
- package/components/service-instance.js +8 -3
- package/docs/ti-engine-icon.ico +0 -0
- package/docs/ti-engine-logo.avif +0 -0
- package/integrations/redis-integration.js +86 -23
- package/package.json +2 -1
- package/utils/cache.js +52 -5
- package/utils/exceptions.js +2 -1
- package/utils/tools.js +2 -3
|
@@ -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
|
-
* @
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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( "
|
|
458
|
-
if (
|
|
459
|
-
|
|
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.
|
|
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 = [
|
|
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.
|
|
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.
|
|
560
|
+
let commandHashSetFields = [ redis.cacheCommands.HASH_SET, key ];
|
|
539
561
|
_.forEach( fields, ( field ) => {
|
|
540
562
|
commandHashSetFields.push( field.name );
|
|
541
|
-
commandHashSetFields.push(
|
|
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/>
|
package/utils/exceptions.js
CHANGED
|
@@ -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
|
-
|
|
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;
|