@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 CHANGED
@@ -2,6 +2,38 @@
2
2
 
3
3
  This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
4
4
 
5
+ ## Version 1.3.1
6
+ * feat(service caller): refactor the entire service call execution flow for clarity and better performance
7
+ * feat(service caller): create a new private class `ServiceCallProcessor` to handle individual service calls in a contained scope
8
+ * feat(message observer): add new property `priority` to the message observer class. It is used to determine the order in which the observers are notified about the messages
9
+ * feat(message observer)!: change method `onMessage` to now return the message it received. This allows for the message to be modified before it is passed to the next observer based on `priority`
10
+ * feat(message handler)!: change method `onMessage` to `notifyMessageObservers` for clarity. It now implements the `MessageObserver` functionality for prioritization and message modification
11
+ * fix(service caller): fix an issue which did not allow for a service call to be marked as completed thus being entered into the trace log as still pending
12
+ * fix(redis integration): remove hardcoded `#retryMaxAttempts` value in `#setupClient` method
13
+ * fix(redis integration): fix an issue which was setting the client status to `DISCONNECTED` during a normal shut down procedure
14
+
15
+ ## Version 1.3.0
16
+ * feat(redis integration)!: change `reconnectOnError` behavior to also resubmit the failed command in case the error was of type `READONLY`
17
+ * feat(redis integration): improve the reliability and usage of the event notification mechanism for connection observers
18
+ * feat(redis integration): implement listener to the `end` event on Redis connection to capture when connection can no longer be recovered
19
+ * feat(redis integration): notify connection observers `onConnectionLost` event
20
+ * feat(redis integration): add redis client platform-specific status. It is used internally by the platform and can also be accessed via `redisClient.clientStatus` property
21
+ * feat(connection observer): add new event handler `onConnectionLost` that will be invoked when the observed connection is irrevocably lost
22
+ * feat(cache)!: implement `onConnectionLost` handler that will cause the service instance to immediately stop since it cannot work without the cache
23
+ * fix(service caller): fix multiple promise reject condition when a service call timed out and the service handler still attempted to complete with subsequent error
24
+ * fix(service instance): limit health check reporting to one attempt at a time to avoid unnecessary cache requests and log spam
25
+ * fix(redis integration): fix a multiple promise resolve condition on Redis `reconnect` event
26
+ * fix(redis integration): fix broken event propagation on `disrupted` events to some connection observers
27
+
28
+ ## Version 1.2.5
29
+ * feat(cache): extend method `expireValue` to work with has set fields as well
30
+
31
+ ## Version 1.2.4
32
+ * feat(cache): expose the cache module as export in `package.json`
33
+ * feat(cache): add method `hashDeleteField` to remove a hash-set field. This implements the `hdel` Redis command
34
+ * fix(tools): optimize method `stringifyJSON` not to call unnecessary decycling of the value if it's not an object
35
+ * fix(cache): replace `hmset` with `hset` Redis command in both methods that set hash-set values. Also remove unnecessary `_.isObjectLike` call in `hashSetFields` method
36
+
5
37
  ## Version 1.2.3
6
38
  * feat(start instance): add support for providing a custom path to the `.env` file to be used at service startup as process argument. Accepted arguments are `--env`, `--env-file`, `--dotenv`, `--dotenv-path`, and `-e`. The path itself should be relative to the working directory and should include the file name
7
39
 
@@ -1,65 +1,65 @@
1
- {
2
- "labels": {
3
- "general": {
4
- "exceptions": {
5
- "0": {
6
- "en": "Unidentified error encountered or unrecognized exception code provided."
7
- },
8
- "1000": {
9
- "en": "Error thrown by internal JS source."
10
- },
11
- "1001": {
12
- "en": "Attempt to construct an abstract class detected."
13
- },
14
- "1002": {
15
- "en": "Attempt to call an abstract method detected."
16
- },
17
- "1003": {
18
- "en": "Invalid or no service domain name provided at microservice startup."
19
- },
20
- "1004": {
21
- "en": "The system cache required for proper engine operation is unavailable."
22
- },
23
- "1005": {
24
- "en": "The provided service handler is not a proper function."
25
- },
26
- "1006": {
27
- "en": "The requested feature is not supported by current configuration or version."
28
- },
29
- "2000": {
30
- "en": "Invalid authorization token provided."
31
- },
32
- "2001": {
33
- "en": "Invalid or expired session encountered."
34
- },
35
- "2002": {
36
- "en": "Attempt for unauthorized access detected."
37
- },
38
- "2003": {
39
- "en": "The system detected tampering with the message received via message exchange."
40
- },
41
- "3000": {
42
- "en": "General error during cross-application communication."
43
- },
44
- "3001": {
45
- "en": "The message sender instance is currently unavailable."
46
- },
47
- "3002": {
48
- "en": "The execution of a service could not complete within the allowed timeout."
49
- },
50
- "3003": {
51
- "en": "The specified service is not found in the service registry."
52
- },
53
- "3004": {
54
- "en": "The specified service is not found in the service definition interface."
55
- },
56
- "3005": {
57
- "en": "No handler found in the interface for the specified service or service version."
58
- },
59
- "3010": {
60
- "en": "Connection retry attempts exceeded the configured limit."
61
- }
62
- }
63
- }
64
- }
1
+ {
2
+ "labels": {
3
+ "general": {
4
+ "exceptions": {
5
+ "0": {
6
+ "en": "Unidentified error encountered or unrecognized exception code provided."
7
+ },
8
+ "1000": {
9
+ "en": "Error thrown by internal JS source."
10
+ },
11
+ "1001": {
12
+ "en": "Attempt to construct an abstract class detected."
13
+ },
14
+ "1002": {
15
+ "en": "Attempt to call an abstract method detected."
16
+ },
17
+ "1003": {
18
+ "en": "Invalid or no service domain name provided at microservice startup."
19
+ },
20
+ "1004": {
21
+ "en": "The system cache required for proper engine operation is unavailable."
22
+ },
23
+ "1005": {
24
+ "en": "The provided service handler is not a proper function."
25
+ },
26
+ "1006": {
27
+ "en": "The requested feature is not supported by current configuration or version."
28
+ },
29
+ "2000": {
30
+ "en": "Invalid authorization token provided."
31
+ },
32
+ "2001": {
33
+ "en": "Invalid or expired session encountered."
34
+ },
35
+ "2002": {
36
+ "en": "Attempt for unauthorized access detected."
37
+ },
38
+ "2003": {
39
+ "en": "The system detected tampering with the message received via message exchange."
40
+ },
41
+ "3000": {
42
+ "en": "General error during cross-application communication."
43
+ },
44
+ "3001": {
45
+ "en": "The message sender instance is currently unavailable."
46
+ },
47
+ "3002": {
48
+ "en": "The execution of a service could not complete within the allowed timeout."
49
+ },
50
+ "3003": {
51
+ "en": "The specified service is not found in the service registry."
52
+ },
53
+ "3004": {
54
+ "en": "The specified service is not found in the service definition interface."
55
+ },
56
+ "3005": {
57
+ "en": "No handler found in the interface for the specified service or service version."
58
+ },
59
+ "3010": {
60
+ "en": "Connection retry attempts exceeded the configured limit."
61
+ }
62
+ }
63
+ }
64
+ }
65
65
  }
@@ -151,7 +151,7 @@ try {
151
151
  setImmediate( () => process.exit( 1 ) );
152
152
  } );
153
153
  } else {
154
- logger.log( `Attempting to start a module that does not implement the ServiceInstance abstract class!`, logger.logSeverity.ERROR );
154
+ logger.log( `Attempting to start a module that does not implement the ServiceInstance abstract class!`, logger.logSeverity.CRITICAL );
155
155
  setImmediate( () => process.exit( 1 ) );
156
156
  }
157
157
  } catch ( error ) {
@@ -1,6 +1,6 @@
1
1
  /*
2
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-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
4
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
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
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/>.
@@ -37,7 +37,8 @@ class ConnectionObserver {
37
37
  * @virtual
38
38
  * @public
39
39
  */
40
- onConnectionDisrupted( identifier ) { }
40
+ onConnectionDisrupted( identifier ) {
41
+ }
41
42
 
42
43
  /**
43
44
  * Needs to be invoked by the connection handler when the connection is recovered.
@@ -49,8 +50,22 @@ class ConnectionObserver {
49
50
  * @virtual
50
51
  * @public
51
52
  */
52
- onConnectionRecovered( identifier ) { }
53
+ onConnectionRecovered( identifier ) {
54
+ }
55
+
56
+ /**
57
+ * Needs to be invoked by the connection handler when the connection is irrevocably lost.
58
+ * <br/>
59
+ * NOTE: Override this to add custom functionality.
60
+ *
61
+ * @method
62
+ * @param {string} identifier The identifier of the observed connection.
63
+ * @virtual
64
+ * @public
65
+ */
66
+ onConnectionLost( identifier ) {
67
+ }
53
68
 
54
69
  }
55
70
 
56
- module.exports = ConnectionObserver;
71
+ module.exports = ConnectionObserver;
@@ -74,7 +74,7 @@ class MessageExchange extends MessageObserver {
74
74
  * @param {string} serviceDomainName The domain name of the microservice using the message exchange.
75
75
  */
76
76
  constructor( instanceID, serviceDomainName ) {
77
- super();
77
+ super( 9 );
78
78
 
79
79
  // make sure this abstract class cannot be instantiated:
80
80
  if ( new.target === MessageExchange ) {
@@ -394,12 +394,25 @@ class MessageExchange extends MessageObserver {
394
394
  _.pull( this.#disruptedConnections, identifier );
395
395
  }
396
396
 
397
+ /**
398
+ * Used to mark the connection with the provided identifier as disrupted.
399
+ *
400
+ * @method
401
+ * @param {string} identifier The identifier of the observed connection.
402
+ * @override
403
+ * @public
404
+ */
405
+ onConnectionLost( identifier ) {
406
+ // TODO: implement triggering of a graceful shutdown of the service instance
407
+ }
408
+
397
409
  /**
398
410
  * Used only for the purposes of the message tracer.
399
411
  *
400
412
  * @method
401
413
  * @param {string} identifier The identifier of the observed connection.
402
414
  * @param {Message} message The message for processing.
415
+ * @returns {Message} The message that was received.
403
416
  * @override
404
417
  * @public
405
418
  */
@@ -411,6 +424,8 @@ class MessageExchange extends MessageObserver {
411
424
  } else if ( MessageExchange.connectionNameResponsesIn === identifier ) {
412
425
  messageTracer.instance.recordTraceEntry( message, messageTracer.messageType.MESSAGE_RESPONSE, messageTracer.dispatchEvent.RECEIVED, messageTracer.messageState.PROCESSED );
413
426
  }
427
+
428
+ return message;
414
429
  }
415
430
 
416
431
  /**
@@ -54,7 +54,9 @@ class MessageHandler extends ConnectionObserver {
54
54
  * @returns {boolean}
55
55
  * @public
56
56
  */
57
- get isAvailable() { return this.#isAvailable; }
57
+ get isAvailable() {
58
+ return this.#isAvailable;
59
+ }
58
60
 
59
61
  /**
60
62
  * Used to set the isAvailable flag.
@@ -65,7 +67,9 @@ class MessageHandler extends ConnectionObserver {
65
67
  * @param {boolean} value
66
68
  * @public
67
69
  */
68
- set isAvailable( value ) { this.#isAvailable = value; }
70
+ set isAvailable( value ) {
71
+ this.#isAvailable = value;
72
+ }
69
73
 
70
74
  /**
71
75
  * Returns the connection identifier.
@@ -74,7 +78,9 @@ class MessageHandler extends ConnectionObserver {
74
78
  * @returns {string}
75
79
  * @public
76
80
  */
77
- get connectionIdentifier() { return this.#connectionIdentifier; }
81
+ get connectionIdentifier() {
82
+ return this.#connectionIdentifier;
83
+ }
78
84
 
79
85
  /**
80
86
  * Used to initialize and enable the communication capabilities of the handler.
@@ -128,9 +134,9 @@ class MessageHandler extends ConnectionObserver {
128
134
  */
129
135
  addMessageObserver( messageObserver ) {
130
136
  const MessageObserver = require( "#message-observer" );
131
-
132
137
  if ( messageObserver instanceof MessageObserver ) {
133
138
  this.#messageObservers.push( messageObserver );
139
+ this.#messageObservers = _.orderBy( this.#messageObservers, [ "priority" ], [ "desc" ] );
134
140
  } else {
135
141
  logger.log( `Attempting to add '${ messageObserver.constructor.name }' as message observer but it's not a child-class of 'MessageObserver'!`, logger.logSeverity.WARNING );
136
142
  }
@@ -138,19 +144,23 @@ class MessageHandler extends ConnectionObserver {
138
144
 
139
145
  /**
140
146
  * An event-triggered method that will notify any observers about a new message for handling.
147
+ * <br/>
148
+ * NOTE: Each observer will be notified in the order of their priority via their {@link MessageObserver.onMessage} method. Additionally, the message will be
149
+ * passed through each observer in the order of their priority. If the observer returns a modified message, it will be used instead of the original message!
141
150
  *
142
151
  * @method
143
152
  * @param {Message} message
144
153
  * @public
145
154
  */
146
- onMessage( message ) {
155
+ notifyMessageObservers( message ) {
156
+ let modifiedMessage = message;
147
157
  _.forEach( this.#messageObservers, ( messageObserver ) => {
148
- messageObserver.onMessage( this.#connectionIdentifier, message );
158
+ modifiedMessage = messageObserver.onMessage( this.#connectionIdentifier, modifiedMessage );
149
159
  } );
150
160
  }
151
161
 
152
162
  /**
153
- * An event-triggered method that will notify any observers about primary connection recovered state.
163
+ * An event-triggered method that will notify any observers about the primary connection recovered state.
154
164
  * <br/>
155
165
  * NOTE: You can override this to add custom functionality but make sure to also call the base method
156
166
  * using: super.onConnectionRecovered( identifier )
@@ -189,6 +199,26 @@ class MessageHandler extends ConnectionObserver {
189
199
  }
190
200
  }
191
201
 
202
+ /**
203
+ * An event-triggered method that will notify any observers about the primary connection having been lost.
204
+ * <br/>
205
+ * NOTE: You can override this to add custom functionality but make sure to also call the base method
206
+ * using: super.onConnectionLost( identifier )
207
+ *
208
+ * @method
209
+ * @param {string} identifier The identifier of the observed connection.
210
+ * @override
211
+ * @private
212
+ */
213
+ onConnectionLost( identifier ) {
214
+ if ( identifier === this.#connectionIdentifier ) {
215
+ this.#isAvailable = false;
216
+ _.forEach( this.#messageObservers, ( messageObserver ) => {
217
+ messageObserver.onConnectionLost( this.#connectionIdentifier );
218
+ } );
219
+ }
220
+ }
221
+
192
222
  }
193
223
 
194
224
  module.exports = MessageHandler;
@@ -1,12 +1,13 @@
1
1
  /*
2
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-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * Copyright © 2021-2025 Boris Kostadinov <kostadinov.boris@gmail.com>
4
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
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
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
7
  */
8
8
 
9
9
  const ConnectionObserver = require( "#connection-observer" );
10
+ const _ = require( "lodash" );
10
11
  const exceptions = require( "#exceptions" );
11
12
 
12
13
  /**
@@ -21,16 +22,45 @@ const exceptions = require( "#exceptions" );
21
22
  */
22
23
  class MessageObserver extends ConnectionObserver {
23
24
 
25
+ #priority = 0;
26
+
24
27
  /**
28
+ * @param {number} [priority=0] The priority of this observer. Higher values indicate higher priority.
25
29
  * @constructor
26
30
  */
27
- constructor() {
31
+ constructor( priority = 0 ) {
28
32
  super();
29
33
 
30
34
  // make sure this abstract class cannot be instantiated:
31
35
  if ( new.target === MessageObserver ) {
32
36
  throw exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_CLASS_INIT, { name: this.constructor.name } );
33
37
  }
38
+
39
+ this.#priority = _.isNumber( priority ) ? priority : 0;
40
+ }
41
+
42
+ /**
43
+ * Returns the priority of this observer.
44
+ * <br/>
45
+ * NOTE: Higher values indicate higher priority.
46
+ *
47
+ * @property
48
+ * @returns {number}
49
+ */
50
+ get priority() {
51
+ return this.#priority;
52
+ }
53
+
54
+ /**
55
+ * Used to set the priority of this observer.
56
+ * <br/>
57
+ * NOTE: Higher values indicate higher priority.
58
+ *
59
+ * @property
60
+ * @param {number} value
61
+ */
62
+ set priority( value ) {
63
+ this.#priority = _.isNumber( value ) ? value : 0;
34
64
  }
35
65
 
36
66
  /**
@@ -41,10 +71,13 @@ class MessageObserver extends ConnectionObserver {
41
71
  * @method
42
72
  * @param {string} identifier The identifier of the observed connection.
43
73
  * @param {Message} message The message for processing.
74
+ * @returns {Message} The message that was received.
44
75
  * @virtual
45
76
  * @public
46
77
  */
47
- onMessage( identifier, message ) { }
78
+ onMessage( identifier, message ) {
79
+ return message;
80
+ }
48
81
 
49
82
  /**
50
83
  * Needs to be invoked by the connection handler when the connection is disrupted.
@@ -74,6 +107,20 @@ class MessageObserver extends ConnectionObserver {
74
107
  super.onConnectionRecovered( identifier );
75
108
  }
76
109
 
110
+ /**
111
+ * Needs to be invoked by the connection handler when the connection is irrevocably lost.
112
+ * <br/>
113
+ * NOTE: Override this to add custom functionality.
114
+ *
115
+ * @method
116
+ * @param {string} identifier The identifier of the observed connection.
117
+ * @virtual
118
+ * @public
119
+ */
120
+ onConnectionLost( identifier ) {
121
+ super.onConnectionLost( identifier );
122
+ }
123
+
77
124
  }
78
125
 
79
- module.exports = MessageObserver;
126
+ module.exports = MessageObserver;
@@ -117,7 +117,7 @@ class MessageReceiver extends MessageHandler {
117
117
  this.onReceive().then( ( message ) => {
118
118
  return this.#postReceive( message );
119
119
  } ).then( ( message ) => {
120
- this.onMessage( message );
120
+ this.notifyMessageObservers( message );
121
121
  } ).catch( ( error ) => {
122
122
  if ( error.code !== exceptions.exceptionCode.E_COM_MESSAGE_RECEIVER_UNAVAILABLE ) {
123
123
  logger.log( `Error while trying to receive the next pending message from memory cache in receiver '${ this.connectionIdentifier }'! Resuming operation...`, logger.logSeverity.ERROR, error );