@ti-engine/core 1.1.10 → 1.2.0

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.
@@ -20,6 +20,7 @@ const redis = require( "#redis-integration" );
20
20
  class MessageMemoryCache {
21
21
 
22
22
  #redisClient = null;
23
+ #isShuttingDown = false;
23
24
 
24
25
  /**
25
26
  * @constructor
@@ -60,6 +61,7 @@ class MessageMemoryCache {
60
61
  * @public
61
62
  */
62
63
  shutDown( timeoutMs ) {
64
+ this.#isShuttingDown = true;
63
65
  return this.#redisClient.shutDown( timeoutMs );
64
66
  }
65
67
 
@@ -85,13 +87,17 @@ class MessageMemoryCache {
85
87
  */
86
88
  sendMessage( message, queue ) {
87
89
  return new Promise( ( resolve, reject ) => {
88
- let command = [ redis.cacheCommands.LIST_PUSH, queue, tools.stringifyJSON( message ) ];
89
- this.#redisClient.executeCommands( [ command ] ).then( ( results ) => {
90
- results = results[ 0 ];
91
- resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
92
- } ).catch( ( error ) => {
93
- reject( exceptions.raise( error ) );
94
- } );
90
+ if ( this.#isShuttingDown === true ) {
91
+ reject( exceptions.raise( exceptions.exceptionCode.E_COM_MESSAGE_SENDER_UNAVAILABLE ) );
92
+ } else {
93
+ let command = [ redis.cacheCommands.LIST_PUSH, queue, tools.stringifyJSON( message ) ];
94
+ this.#redisClient.executeCommands( [ command ] ).then( ( results ) => {
95
+ results = results[ 0 ];
96
+ resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
97
+ } ).catch( ( error ) => {
98
+ reject( exceptions.raise( error ) );
99
+ } );
100
+ }
95
101
  } );
96
102
  }
97
103
 
@@ -134,7 +140,11 @@ class MessageMemoryCache {
134
140
  results = ( results && results.length > 1 ) ? results[ 1 ] : undefined;
135
141
  resolve( tools.parseJSON( results ) );
136
142
  } ).catch( ( error ) => {
137
- reject( exceptions.raise( error ) );
143
+ if ( this.#isShuttingDown === true ) {
144
+ reject( exceptions.raise( exceptions.exceptionCode.E_COM_MESSAGE_RECEIVER_UNAVAILABLE ) );
145
+ } else {
146
+ reject( exceptions.raise( error ) );
147
+ }
138
148
  } );
139
149
  } );
140
150
  }
@@ -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/>.
@@ -22,6 +22,7 @@ const config = require( "#config" );
22
22
  class MessageReceiver extends MessageHandler {
23
23
 
24
24
  #receiveQueue;
25
+ #isReceiving = false;
25
26
 
26
27
  /**
27
28
  * @constructor
@@ -48,7 +49,31 @@ class MessageReceiver extends MessageHandler {
48
49
  * @returns {string}
49
50
  * @public
50
51
  */
51
- get receiveQueue() { return this.#receiveQueue; }
52
+ get receiveQueue() {
53
+ return this.#receiveQueue;
54
+ }
55
+
56
+ /**
57
+ * Indicates whether the receiver is currently receiving messages.
58
+ *
59
+ * @property
60
+ * @returns {boolean}
61
+ * @public
62
+ */
63
+ get isReceiving() {
64
+ return this.#isReceiving;
65
+ }
66
+
67
+ /**
68
+ * Used to set the value of the {@link MessageReceiver#isReceiving} property.
69
+ *
70
+ * @property
71
+ * @param {boolean} value
72
+ * @public
73
+ */
74
+ set isReceiving( value ) {
75
+ this.#isReceiving = value;
76
+ }
52
77
 
53
78
  /**
54
79
  * Used to initialize and enable the communication capabilities of the handler.
@@ -81,21 +106,26 @@ class MessageReceiver extends MessageHandler {
81
106
  /**
82
107
  * Used to receive messages.
83
108
  * <br/>
84
- * NOTE: This method will start a recursion of subsequent receives that will continue even if an individual message fetch fails.
109
+ * NOTE: This method will start a recursion of subsequent receive calls that will continue even if an individual message fetch fails.
85
110
  *
86
111
  * @method
112
+ * @recursion
87
113
  * @public
88
114
  */
89
115
  receive() {
90
- this.onReceive().then( ( message ) => {
91
- return this.#postReceive( message );
92
- } ).then( ( message ) => {
93
- this.onMessage( message );
94
- } ).catch( ( error ) => {
95
- logger.log( `Error while trying to receive the next pending message from memory cache in receiver '${ this.connectionIdentifier }'! Resuming operation...`, logger.logSeverity.ERROR, error );
96
- } ).finally( () => {
97
- this.receive();
98
- } );
116
+ if ( this.isReceiving === true ) {
117
+ this.onReceive().then( ( message ) => {
118
+ return this.#postReceive( message );
119
+ } ).then( ( message ) => {
120
+ this.onMessage( message );
121
+ } ).catch( ( error ) => {
122
+ if ( error.code !== exceptions.exceptionCode.E_COM_MESSAGE_RECEIVER_UNAVAILABLE ) {
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 );
124
+ }
125
+ } ).finally( () => {
126
+ this.receive();
127
+ } );
128
+ }
99
129
  }
100
130
 
101
131
  /**
@@ -147,4 +177,4 @@ class MessageReceiver extends MessageHandler {
147
177
 
148
178
  }
149
179
 
150
- module.exports = MessageReceiver;
180
+ module.exports = MessageReceiver;
@@ -117,7 +117,12 @@ class MessageTracer {
117
117
  cache.instance.setJSON( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), traceRoot, "$", 1 ).then( () => {
118
118
  resolve();
119
119
  } ).catch( ( error ) => {
120
- reject( exceptions.raise( error ) );
120
+ // If JSON is unsupported in Redis server, the trace will use a Set later, so we can resolve the promise:
121
+ if ( error.code === exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ) {
122
+ resolve();
123
+ } else {
124
+ reject( exceptions.raise( error ) );
125
+ }
121
126
  } );
122
127
  } );
123
128
  }
@@ -125,7 +130,7 @@ class MessageTracer {
125
130
  /**
126
131
  * Used to create a trace entry for the provided {@link Message} and parameters.
127
132
  * <br/>
128
- * NOTE: By default all trace events are stored in the memory cache for further processing and analysis. The
133
+ * NOTE: By default, all trace events are stored in the memory cache for further processing and analysis. The
129
134
  * location is configured in the MESSAGE_EXCHANGE_TRACE_REPOSITORY setting.
130
135
  * <br/>
131
136
  * NOTE: Trace events are logged with severity level NOTICE or ERROR for failed dispatches. They still might be
@@ -139,7 +144,7 @@ class MessageTracer {
139
144
  * @public
140
145
  */
141
146
  recordTraceEntry( message, messageType, dispatchEvent, messageState ) {
142
- // Depending on whether the message comes as request or response, the from and to addresses will be opposite:
147
+ // Depending on whether the message comes as a request or response, the from and to addresses will be opposite:
143
148
  let source = message.source.route + "." + message.source.instanceID;
144
149
  let destination = message.destination.route + ( ( message.destination.instanceID != null ) ? "." + message.destination.instanceID : "" );
145
150
  let messageSnapshot = MessageTracer.#obscureSensitiveData( message );
@@ -58,8 +58,6 @@ class ServiceExecutor extends MessageObserver {
58
58
  #serviceInterface = {};
59
59
  /** @type VerifyAccessMethod */
60
60
  #verifyAccess;
61
- #registrationTasks = {};
62
- #registrationRetryInterval = 500;
63
61
 
64
62
  /**
65
63
  * @constructor
@@ -67,7 +65,7 @@ class ServiceExecutor extends MessageObserver {
67
65
  constructor() {
68
66
  super();
69
67
 
70
- // Setup a default empty verify access method:
68
+ // Set up a default empty verify access method:
71
69
  this.#verifyAccess = () => {
72
70
  return Promise.resolve();
73
71
  };
@@ -127,17 +125,6 @@ class ServiceExecutor extends MessageObserver {
127
125
  */
128
126
  onConnectionRecovered( identifier ) {
129
127
  super.onConnectionRecovered( identifier );
130
-
131
- if ( identifier !== cache.instance.connectionIdentifier ) {
132
- let serviceCatalog = config.getSetting( config.setting.SERVICE_REGISTRY_ADDRESS ) + ServiceInstance.serviceDomainName;
133
- _.forOwn( this.#serviceInterface, ( versions, serviceAlias ) => {
134
- if ( !this.#registrationTasks[ serviceAlias ] ) {
135
- this.#registrationTasks[ serviceAlias ] = setInterval( () => {
136
- this.#registerServiceToCatalog( serviceCatalog, serviceAlias );
137
- }, this.#registrationRetryInterval );
138
- }
139
- } );
140
- }
141
128
  }
142
129
 
143
130
  /**
@@ -164,16 +151,26 @@ class ServiceExecutor extends MessageObserver {
164
151
  * @param {ServiceHandlerMethod} serviceHandler
165
152
  * @param {ServiceDefinition} serviceDefinition
166
153
  * @param {ServiceInstance} serviceInstance This will be used as context to bind all business services.
154
+ * @returns {Promise}
167
155
  * @public
168
156
  */
169
157
  addServiceHandler( serviceHandler, serviceDefinition, serviceInstance ) {
170
- if ( !this.#serviceInterface[ serviceDefinition.serviceAlias ] ) {
171
- this.#serviceInterface[ serviceDefinition.serviceAlias ] = {};
172
- }
173
- if ( this.#serviceInterface[ serviceDefinition.serviceAlias ][ serviceDefinition.serviceVersion ] ) {
174
- logger.log( `Service handler for '${ serviceDefinition.serviceAlias }' version '${ serviceDefinition.serviceVersion }' already existed and will be overridden.`, logger.logSeverity.WARNING );
175
- }
176
- this.#serviceInterface[ serviceDefinition.serviceAlias ][ serviceDefinition.serviceVersion ] = serviceHandler.bind( serviceInstance, _.cloneDeep( serviceDefinition ) );
158
+ return new Promise( ( resolve, reject ) => {
159
+ this.#registerServiceToCatalog( serviceDefinition.serviceAlias ).then( () => {
160
+ // Bind the service handler to the service alias and version:
161
+ if ( !this.#serviceInterface[ serviceDefinition.serviceAlias ] ) {
162
+ this.#serviceInterface[ serviceDefinition.serviceAlias ] = {};
163
+ }
164
+ if ( this.#serviceInterface[ serviceDefinition.serviceAlias ][ serviceDefinition.serviceVersion ] ) {
165
+ logger.log( `Service handler for '${ serviceDefinition.serviceAlias }' version '${ serviceDefinition.serviceVersion }' already existed and will be overridden.`, logger.logSeverity.WARNING );
166
+ }
167
+ this.#serviceInterface[ serviceDefinition.serviceAlias ][ serviceDefinition.serviceVersion ] = serviceHandler.bind( serviceInstance, _.cloneDeep( serviceDefinition ) );
168
+
169
+ resolve();
170
+ } ).catch( ( error ) => {
171
+ reject( exceptions.raise( error ) );
172
+ } );
173
+ } );
177
174
  }
178
175
 
179
176
  /* Private interface */
@@ -207,19 +204,20 @@ class ServiceExecutor extends MessageObserver {
207
204
  * Used to register a service in the service registry.
208
205
  *
209
206
  * @method
210
- * @param {string} serviceCatalog
211
207
  * @param {string} serviceAlias
208
+ * @returns {Promise}
212
209
  * @private
213
210
  */
214
- #registerServiceToCatalog( serviceCatalog, serviceAlias ) {
215
- if ( cache.instance.isOperational ) {
211
+ #registerServiceToCatalog( serviceAlias ) {
212
+ return new Promise( ( resolve, reject ) => {
213
+ let serviceCatalog = config.getSetting( config.setting.SERVICE_REGISTRY_ADDRESS ) + ServiceInstance.serviceDomainName;
216
214
  cache.instance.addToSet( serviceCatalog, serviceAlias ).then( () => {
217
- clearTimeout( this.#registrationTasks[ serviceAlias ] );
218
- delete this.#registrationTasks[ serviceAlias ];
215
+ resolve();
219
216
  } ).catch( ( error ) => {
220
- logger.log( `Record for service '${ serviceAlias }' could not be added to the service registry. Will retry in '${ this.#registrationRetryInterval }' milliseconds.`, logger.logSeverity.ERROR, error );
217
+ logger.log( `Record for service '${ serviceAlias }' could not be added to the service registry. Service will not be visible to Service Consumers!`, logger.logSeverity.ERROR, error );
218
+ reject( exceptions.raise( error ) );
221
219
  } );
222
- }
220
+ } );
223
221
  }
224
222
 
225
223
  /**
@@ -56,7 +56,7 @@ class ServiceInstance {
56
56
  } );
57
57
  }
58
58
 
59
- // Ensure uniform 'ti-' prefix even if env is missing or custom starter script is used:
59
+ // Ensure a uniform 'ti-' prefix even if env is missing or a custom starter script is used:
60
60
  const envID = process.env.TI_INSTANCE_ID;
61
61
  ServiceInstance.#instanceID = ( envID && String( envID ).startsWith( "ti-" ) ) ? envID : ( "ti-" + ( envID || tools.getUUID() ) );
62
62
 
@@ -217,7 +217,7 @@ class ServiceInstance {
217
217
  * Used to report health status of the service instance for external monitoring.
218
218
  * This is a scheduled job that will be executed at SERVICE_HEALTH_CHECK_INTERVAL time.
219
219
  * <br/>
220
- * NOTE: By default this method will update a Redis key with an expiration timer. You can override this
220
+ * NOTE: By default, this method will update a Redis key with an expiration timer. You can override this
221
221
  * functionality with something custom like calling an HTTP endpoint.
222
222
  *
223
223
  * @method
@@ -140,10 +140,10 @@ class ServiceProvider extends ServiceConsumer {
140
140
  /**
141
141
  * Used to register a single service to the service provider's API. One service can have multiple versions accessible at the same time.
142
142
  * <br/>
143
- * NOTE: This will actually bind the serviceDefinition as first parameter of the service handler function. When creating default service handlers,
143
+ * NOTE: This will actually bind the serviceDefinition as the first parameter of the service handler function. When creating default service handlers,
144
144
  * keep in mind that your first param must always be the 'serviceDefinition' and the second one will be the general 'serviceParams' object.
145
145
  * <br/>
146
- * NOTE: Additionally, if you intend to call another service inside the service handler, then you have to use normal function for the handler and not
146
+ * NOTE: Additionally, if you intend to call another service inside the service handler, then you have to use a normal function for the handler and not
147
147
  * an arrow function! Arrow functions cannot bind the scope of the parent class to themselves, and you won't have access to it and its methods.
148
148
  *
149
149
  * @method
@@ -171,12 +171,15 @@ class ServiceProvider extends ServiceConsumer {
171
171
  }
172
172
  }
173
173
 
174
- // if we have a valid service handler proceed with the registration:
174
+ // if we have a valid service handler, proceed with the registration:
175
175
  if ( typeof ( serviceHandler ) === "function" ) {
176
176
  // make sure we have a version and parent service provider specified:
177
177
  serviceDefinition.serviceVersion = serviceDefinition.serviceVersion || 1;
178
- this.#serviceExecutor.addServiceHandler( serviceHandler, serviceDefinition, this );
179
- resolve();
178
+ this.#serviceExecutor.addServiceHandler( serviceHandler, serviceDefinition, this ).then( () => {
179
+ resolve();
180
+ } ).catch( ( error ) => {
181
+ reject( exceptions.raise( error ) );
182
+ } );
180
183
  } else {
181
184
  reject( exceptions.raise( exceptions.exceptionCode.E_GEN_BAD_SERVICE_HANDLER ) );
182
185
  }
@@ -201,7 +204,7 @@ class ServiceProvider extends ServiceConsumer {
201
204
  _.forEach( serviceDefinitions, ( serviceDefinition ) => {
202
205
  // NOTE: we are not going to interrupt the service interface loading if one of the services fails to load or is not found!
203
206
  // If this happens, a corresponding log entry will be created but the loading process will continue. Therefore, the following
204
- // promise will always resolve (unless a programming error occurs in it of course).
207
+ // promise will always resolve (unless a programming error occurs in it, of course).
205
208
  let registrationPromise = ( serviceDefinition, defaultServiceHandler ) => {
206
209
  return new Promise( ( resolve ) => {
207
210
  this.registerService( serviceDefinition, defaultServiceHandler ).then( () => {