@ti-engine/core 1.0.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.
@@ -0,0 +1,280 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const _ = require( "lodash" );
7
+ const schedule = require( "node-schedule" );
8
+ const tools = require( "#tools" );
9
+ const config = require( "#config" );
10
+ const logger = require( "#logger" );
11
+ const exceptions = require( "#exceptions" );
12
+ const cache = require( "#cache" );
13
+ const messageDispatcher = require( "#message-dispatcher" );
14
+
15
+ /**
16
+ * Abstract class used to define a Service Instance behavior.
17
+ * <br/>
18
+ * NOTE: Inherit this to create an a module that can be started as a microservice instance.
19
+ * <br/>
20
+ * NOTE: This class does not
21
+ *
22
+ * @class ServiceInstance
23
+ * @abstract
24
+ * @public
25
+ */
26
+ class ServiceInstance {
27
+
28
+ static #instanceID;
29
+ static #serviceDomainName;
30
+ #serviceConfig;
31
+ #serviceHealthCheck;
32
+ #reportHealthyJob;
33
+
34
+ /**
35
+ * @constructor
36
+ * @param {string} serviceDomainName The service domain name for this service instance.
37
+ * @param {Object} [serviceConfig={}] The JSON configuration for this service.
38
+ */
39
+ constructor( serviceDomainName, serviceConfig = {} ) {
40
+ // make sure this abstract class cannot be instantiated:
41
+ if ( new.target === ServiceInstance ) {
42
+ throw exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_CLASS_INIT, { name: this.constructor.name } );
43
+ }
44
+
45
+ ServiceInstance.#instanceID = process.env.TI_INSTANCE_ID || tools.getUUID();
46
+ ServiceInstance.#serviceDomainName = serviceDomainName;
47
+ this.#serviceConfig = ( _.isObjectLike( serviceConfig ) ) ? serviceConfig : {};
48
+ }
49
+
50
+ /* Public interface */
51
+
52
+ /**
53
+ * Property returning the current service instance ID.
54
+ *
55
+ * @property
56
+ * @returns {string}
57
+ * @public
58
+ */
59
+ static get instanceID() { return ServiceInstance.#instanceID; }
60
+
61
+ /**
62
+ * Property returning the current service domain name.
63
+ *
64
+ * @property
65
+ * @returns {string}
66
+ * @public
67
+ */
68
+ static get serviceDomainName() { return ServiceInstance.#serviceDomainName; }
69
+
70
+ /**
71
+ * Property to indicate that this and every child class is a {@link ServiceInstance}.
72
+ *
73
+ * @property
74
+ * @returns {boolean}
75
+ * @public
76
+ */
77
+ get isServiceInstance() { return true; }
78
+
79
+ /**
80
+ * Property returning the service configuration JSON.
81
+ *
82
+ * @property
83
+ * @returns {Object}
84
+ * @public
85
+ */
86
+ get serviceConfig() { return this.#serviceConfig; }
87
+
88
+ /**
89
+ * Initializes the instance.
90
+ *
91
+ * @method
92
+ * @returns {Promise}
93
+ * @public
94
+ */
95
+ start() {
96
+ return new Promise( ( resolve, reject ) => {
97
+ if ( !ServiceInstance.#serviceDomainName ) {
98
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_INVALID_SERVICE_DOMAIN_NAME ) );
99
+ } else {
100
+ this.#preStart().then( () => {
101
+ return this.onStart();
102
+ } ).then( () => {
103
+ return this.#postStart();
104
+ } ).then( () => {
105
+ resolve();
106
+ } ).catch( ( error ) => {
107
+ reject( exceptions.raise( error ) );
108
+ } );
109
+ }
110
+ } );
111
+ }
112
+
113
+ /**
114
+ * Executes custom logic on instance start.
115
+ * <br/>
116
+ * NOTE: This method will be invoked automatically.
117
+ * <br/>
118
+ * NOTE: If you need to add more onStart logic you can override this method but make sure to call it in the
119
+ * overriding method using: super.onStart()
120
+ *
121
+ * @method
122
+ * @returns {Promise}
123
+ * @virtual
124
+ * @public
125
+ */
126
+ onStart() {
127
+ return new Promise( ( resolve, reject ) => {
128
+ const DefaultMessageExchange = require( "#default-message-exchange" );
129
+ const ServiceProvider = require( "#service-provider" );
130
+ const ServiceConsumer = require( "#service-consumer" );
131
+
132
+ let configureInbound = ( this instanceof ServiceProvider );
133
+ let configureOutbound = ( this instanceof ServiceConsumer );
134
+
135
+ messageDispatcher.initialize( new DefaultMessageExchange( ServiceInstance.instanceID, ServiceInstance.serviceDomainName ), configureInbound, configureOutbound ).then( () => {
136
+ resolve();
137
+ } ).catch( ( error ) => {
138
+ reject( exceptions.raise( error ) );
139
+ } );
140
+ } );
141
+ }
142
+
143
+ /**
144
+ * Shuts down the instance.
145
+ *
146
+ * @method
147
+ * @returns {Promise}
148
+ * @public
149
+ */
150
+ stop() {
151
+ return new Promise( ( resolve, reject ) => {
152
+ this.#preStop().then( () => {
153
+ return this.onStop();
154
+ } ).then( () => {
155
+ return this.#postStop();
156
+ } ).then( () => {
157
+ resolve();
158
+ } ).catch( ( error ) => {
159
+ reject( exceptions.raise( error ) );
160
+ } );
161
+ } );
162
+ }
163
+
164
+ /**
165
+ * Executes custom logic on instance stop.
166
+ * <br/>
167
+ * NOTE: This method will be invoked automatically.
168
+ * <br/>
169
+ * NOTE: If you need to add more onStop logic you can override this method but make sure to call it in the
170
+ * overriding method using: super.onStop()
171
+ *
172
+ * @method
173
+ * @returns {Promise}
174
+ * @virtual
175
+ * @public
176
+ */
177
+ onStop() {
178
+ return new Promise( ( resolve, reject ) => {
179
+ messageDispatcher.shutDown().then( () => {
180
+ resolve();
181
+ } ).catch( ( error ) => {
182
+ reject( exceptions.raise( error ) );
183
+ } );
184
+ } );
185
+ }
186
+
187
+ /* Private interface */
188
+
189
+ /**
190
+ * Used to run internal pre-start logic.
191
+ * <br/>
192
+ * NOTE: This will be executed before any user's custom logic in {@link ServiceInstance.onStart}.
193
+ *
194
+ * @method
195
+ * @returns {Promise}
196
+ * @private
197
+ */
198
+ #preStart() {
199
+ return new Promise( ( resolve, reject ) => {
200
+ this.#serviceHealthCheck = config.getSetting( config.setting.SERVICE_HEALTH_CHECK_ADDRESS ) + process.env.TI_INSTANCE_NAME + ":" + ServiceInstance.instanceID;
201
+ resolve();
202
+ } );
203
+ }
204
+
205
+ /**
206
+ * Used to run internal post-start logic.
207
+ * <br/>
208
+ * NOTE: This will be executed only after the user's custom logic in {@link ServiceInstance.onStart} has been successfully executed.
209
+ *
210
+ * @method
211
+ * @returns {Promise}
212
+ * @private
213
+ */
214
+ #postStart() {
215
+ return new Promise( ( resolve, reject ) => {
216
+ // schedule regular health check:
217
+ this.#reportHealthyJob = schedule.scheduleJob( config.getSetting( config.setting.SERVICE_HEALTH_CHECK_INTERVAL ), () => {
218
+ this.#reportHealthy();
219
+ } );
220
+
221
+ logger.log( `Instance '${ ServiceInstance.instanceID }' started successfully.`, logger.logSeverity.NOTICE, {
222
+ nodeVersion: process.version
223
+ } );
224
+
225
+ resolve();
226
+ } );
227
+ }
228
+
229
+ /**
230
+ * Used to run internal pre-start logic.
231
+ * <br/>
232
+ * NOTE: This will be executed before any user's custom logic in {@link ServiceInstance.onStop}.
233
+ *
234
+ * @method
235
+ * @returns {Promise}
236
+ * @private
237
+ */
238
+ #preStop() {
239
+ return new Promise( ( resolve, reject ) => {
240
+ if ( this.#reportHealthyJob ) {
241
+ this.#reportHealthyJob.cancel();
242
+ }
243
+
244
+ resolve();
245
+ } );
246
+ }
247
+
248
+ /**
249
+ * Used to run internal post-stop logic.
250
+ * <br/>
251
+ * NOTE: This will be executed only after the user's custom logic in {@link ServiceInstance.onStop} has been successfully executed.
252
+ *
253
+ * @method
254
+ * @returns {Promise}
255
+ * @private
256
+ */
257
+ #postStop() {
258
+ return new Promise( ( resolve, reject ) => {
259
+ logger.log( `Instance '${ ServiceInstance.instanceID }' shut down successfully.`, logger.logSeverity.NOTICE );
260
+
261
+ resolve();
262
+ } );
263
+ }
264
+
265
+ /**
266
+ * Scheduled job used to report for service instance health checks.
267
+ *
268
+ * @method
269
+ * @private
270
+ */
271
+ #reportHealthy() {
272
+ let timestamp = new Date();
273
+ cache.setValue( this.#serviceHealthCheck, timestamp.toISOString(), config.getSetting( config.setting.SERVICE_HEALTH_CHECK_TIMEOUT ) ).catch( ( error ) => {
274
+ logger.log( `Error while trying to report for health check from '${ ServiceInstance.instanceID }'!`, logger.logSeverity.WARNING, error );
275
+ } );
276
+ }
277
+
278
+ }
279
+
280
+ module.exports = ServiceInstance;
@@ -0,0 +1,221 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const ServiceConsumer = require( "#service-consumer" );
7
+ const _ = require( "lodash" );
8
+ const fs = require( "fs-extra" );
9
+ const exceptions = require( "#exceptions" );
10
+ const logger = require( "#logger" );
11
+ const messageDispatcher = require( "#message-dispatcher" );
12
+
13
+ /**
14
+ * Abstract class used to define a Service Provider behavior.
15
+ * <br/>
16
+ * NOTE: Inherit this to create an a module that can be started as a microservice provider instance.
17
+ * <br/>
18
+ * NOTE: A service provider is a microservice that offers an API of named business services that can be invoked by other
19
+ * microservices using {@link ServiceCall} objects. The provider will take care of the actual execution of that service and
20
+ * therefore acts as a "black box". The only necessary items are the service address and optional inbound parameters to be
21
+ * used in that service's logic. The result of the service's execution will be bundled in an {@link ServiceCallResult}
22
+ * object and returned to the caller.
23
+ *
24
+ * @class ServiceProvider
25
+ * @extends ServiceConsumer
26
+ * @abstract
27
+ * @public
28
+ */
29
+ class ServiceProvider extends ServiceConsumer {
30
+
31
+ #serviceExecutor;
32
+
33
+ /**
34
+ * @constructor
35
+ * @param {string} serviceDomainName The service domain name for this service instance.
36
+ * @param {Object} [serviceConfig={}] The JSON configuration for this service.
37
+ */
38
+ constructor( serviceDomainName, serviceConfig = {} ) {
39
+ super( serviceDomainName, serviceConfig );
40
+
41
+ // make sure this abstract class cannot be instantiated:
42
+ if ( new.target === ServiceProvider ) {
43
+ throw exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_CLASS_INIT, { name: this.constructor.name } );
44
+ }
45
+ }
46
+
47
+ /* Public interface */
48
+
49
+ /**
50
+ * Perform initialization tasks when the service provider starts.
51
+ * <br/>
52
+ * NOTE: This method will be invoked automatically.
53
+ * <br/>
54
+ * NOTE: If you need to add more onStart logic you can override this method but make sure to call it in the
55
+ * overriding method using: super.onStart()
56
+ *
57
+ * @method
58
+ * @returns {Promise}
59
+ * @override
60
+ * @public
61
+ */
62
+ onStart() {
63
+ return new Promise( ( resolve, reject ) => {
64
+ const ServiceExecutor = require( "#service-executor" );
65
+
66
+ this.#serviceExecutor = new ServiceExecutor();
67
+ this.#serviceExecutor.configureVerifyAccess( this.verifyAccess );
68
+
69
+ super.onStart().then( () => {
70
+ let serviceDefinitions = this.serviceConfig.services;
71
+ return this.registerServices( serviceDefinitions );
72
+ } ).then( () => {
73
+ messageDispatcher.addMessageObserverRequestsIn( this.#serviceExecutor );
74
+ resolve();
75
+ } ).catch( ( error ) => {
76
+ reject( exceptions.raise( error ) );
77
+ } );
78
+ } );
79
+ }
80
+
81
+ /**
82
+ * Perform shut down and cleanup tasks when the service provider stops.
83
+ * <br/>
84
+ * NOTE: This method will be invoked automatically.
85
+ * <br/>
86
+ * NOTE: If you need to add more onStop logic you can override this method but make sure to call it in the
87
+ * overriding method using: super.onStop()
88
+ *
89
+ * @method
90
+ * @returns {Promise}
91
+ * @override
92
+ * @public
93
+ */
94
+ onStop() {
95
+ return new Promise( ( resolve, reject ) => {
96
+ super.onStop().then( () => {
97
+ resolve();
98
+ } ).catch( ( error ) => {
99
+ reject( exceptions.raise( error ) );
100
+ } );
101
+ } );
102
+ }
103
+
104
+ /**
105
+ * Used to verify whether the service caller has authorization to access the service.
106
+ * <br/>
107
+ * NOTE: Override this to implement authorization check. By default this method simply returns.
108
+ *
109
+ * @method
110
+ * @param {string} authToken
111
+ * @param {ServiceAddress} serviceAddress
112
+ * @return {Promise}
113
+ * @virtual
114
+ * @public
115
+ */
116
+ verifyAccess( authToken, serviceAddress ) {
117
+ return Promise.resolve();
118
+ }
119
+
120
+ /**
121
+ * Used to register a single service to the service provider's API. One service can have multiple versions accessible at the same time.
122
+ * <br/>
123
+ * NOTE: This will actually bind the serviceDefinition as first parameter of the service handler function. When creating default service handlers,
124
+ * keep in mind that your first param must always be the 'serviceDefinition' and the second one will be the general 'serviceParams' object.
125
+ * <br/>
126
+ * 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
127
+ * 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.
128
+ *
129
+ * @method
130
+ * @param {ServiceDefinition} serviceDefinition Full service definition object.
131
+ * @param {ServiceHandler} [defaultServiceHandler=undefined] A default service handler in case there is one.
132
+ * @return {Promise}
133
+ * @public
134
+ */
135
+ registerService( serviceDefinition, defaultServiceHandler = undefined ) {
136
+ return new Promise( ( resolve, reject ) => {
137
+ /** @type {ServiceHandler} */
138
+ let serviceHandler = null;
139
+ if ( serviceDefinition.serviceFile ) {
140
+ let filePath = process.cwd() + serviceDefinition.serviceFile + ( ( _.endsWith( serviceDefinition.serviceFile, ".js" ) ) ? "" : ".js" );
141
+ if ( fs.existsSync( filePath ) ) {
142
+ // try to load the service handler dynamically from the file:
143
+ try {
144
+ serviceHandler = require( filePath ).service;
145
+ } catch ( error ) {
146
+ logger.log( `Specified service handler file '${ serviceDefinition.serviceFile }' could not be loaded!`, logger.logSeverity.ERROR, error );
147
+ }
148
+ } else {
149
+ logger.log( `Specified service handler file '${ serviceDefinition.serviceFile }' was not found in the specified location!`, logger.logSeverity.WARNING );
150
+ }
151
+ } else {
152
+ if ( typeof ( defaultServiceHandler ) === "function" ) {
153
+ serviceHandler = defaultServiceHandler;
154
+ } else {
155
+ logger.log( "A service cannot be registered without provided default service handler at the very least!", logger.logSeverity.WARNING, serviceDefinition );
156
+ }
157
+ }
158
+
159
+ // if we have a valid service handler proceed with the registration:
160
+ if ( typeof ( serviceHandler ) === "function" ) {
161
+ // make sure we have a version and parent service provider specified:
162
+ serviceDefinition.serviceVersion = serviceDefinition.serviceVersion || 1;
163
+ this.#serviceExecutor.addServiceHandler( serviceHandler, serviceDefinition );
164
+ resolve();
165
+ } else {
166
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_BAD_SERVICE_HANDLER ) );
167
+ }
168
+ } );
169
+ }
170
+
171
+ /**
172
+ * Used to register multiple services from the provided service definitions.
173
+ *
174
+ * @method
175
+ * @param {ServiceDefinition[]} serviceDefinitions
176
+ * @param {ServiceHandler} [defaultServiceHandler=undefined]
177
+ * @return {Promise}
178
+ * @public
179
+ */
180
+ registerServices( serviceDefinitions, defaultServiceHandler = undefined ) {
181
+ return new Promise( ( resolve, reject ) => {
182
+ if ( serviceDefinitions ) {
183
+ logger.log( `Starting service registration process. There is ${ ( ( defaultServiceHandler ) ? "" : "NO" ) } default service handler provided.`, logger.logSeverity.INFO );
184
+
185
+ let promises = [];
186
+ _.forEach( serviceDefinitions, ( serviceDefinition ) => {
187
+ // NOTE: we are not going to interrupt the service interface loading if one of the services fails to load or is not found!
188
+ // If this happens, a corresponding log entry will be created but the loading process will continue. Therefore the following
189
+ // promise will always resolve (unless a programming error occurs in it of course).
190
+ let registrationPromise = ( serviceDefinition, defaultServiceHandler ) => {
191
+ return new Promise( ( resolve, reject ) => {
192
+ this.registerService( serviceDefinition, defaultServiceHandler ).then( () => {
193
+ resolve( true );
194
+ } ).catch( ( error ) => {
195
+ resolve( false );
196
+ } );
197
+ } );
198
+ };
199
+ promises.push( registrationPromise( serviceDefinition, defaultServiceHandler ) );
200
+ } );
201
+
202
+ Promise.all( promises ).then( ( result ) => {
203
+ let registrationResults = _.countBy( result, ( value ) => {
204
+ return value === true;
205
+ } );
206
+ logger.log( `Registration of defined services completed with ${ registrationResults[ "true" ] || 0 } successful out of ${ serviceDefinitions.length } total.`, logger.logSeverity.INFO );
207
+
208
+ resolve();
209
+ } ).catch( ( error ) => {
210
+ reject( exceptions.raise( error ) );
211
+ } );
212
+ } else {
213
+ logger.log( `Service registration process skipped as there are no service definitions provided.`, logger.logSeverity.NOTICE );
214
+ resolve();
215
+ }
216
+ } );
217
+ }
218
+
219
+ }
220
+
221
+ module.exports = ServiceProvider;
@@ -0,0 +1,61 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const config = require( "#config" );
7
+
8
+ /**
9
+ * Used to verify if the GCloud integration is enabled.
10
+ *
11
+ * @method
12
+ * @returns {boolean}
13
+ * @public
14
+ */
15
+ module.exports.isEnabled = () => {
16
+ return process.env.TI_GCLOUD_ENABLED === true;
17
+ };
18
+
19
+ /**
20
+ * Will be used to gracefully shut down the instance.
21
+ * <br/>
22
+ * NOTE: Will be overridden below after the instance creation.
23
+ *
24
+ * @method
25
+ * @param {Error} error
26
+ * @abstract
27
+ * @public
28
+ */
29
+ module.exports.reportError = ( error ) => {
30
+ console.error( "Attempting to report error to GCloud while integration to it is disabled!" );
31
+ };
32
+
33
+ if ( process.env.TI_GCLOUD_ENABLED === true ) {
34
+ const { ErrorReporting } = require( "@google-cloud/error-reporting" );
35
+
36
+ const errorReporting = new ErrorReporting( {
37
+ projectId: config.getSetting( config.setting.GCLOUD_PROJECT_ID ),
38
+ key: config.getSetting( config.setting.GCLOUD_API_KEY ),
39
+ reportMode: "production",
40
+ logLevel: 2,
41
+ reportUnhandledRejections: true
42
+ } );
43
+
44
+ /**
45
+ * Reports an error to the GCloud error reporting system.
46
+ *
47
+ * @method
48
+ * @param {Error} error
49
+ * @override
50
+ * @public
51
+ */
52
+ module.exports.reportError = ( error ) => {
53
+ errorReporting.report( {
54
+ eventTime: ( new Date() ).toISOString(),
55
+ message: error.stack,
56
+ serviceContext: {
57
+ service: process.env.TI_INSTANCE_ID
58
+ }
59
+ } );
60
+ };
61
+ }