@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,150 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const config = require( "#config" );
7
+ const tools = require( "#tools" );
8
+ const exceptions = require( "#exceptions" );
9
+ const redis = require( "#redis-integration" );
10
+
11
+ /**
12
+ * Used to create a Redis Cache client wrapped in a specialized message memory cache interface.
13
+ *
14
+ * @class MessageMemoryCache
15
+ * @public
16
+ */
17
+ class MessageMemoryCache {
18
+
19
+ #redisClient = null;
20
+
21
+ /**
22
+ * @constructor
23
+ * @param {string} identifier The connection identifier for the Redis connection.
24
+ */
25
+ constructor( identifier ) {
26
+ let host = config.getSetting( config.setting.MEMORY_CACHE_REDIS_HOST );
27
+ let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
28
+ let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
29
+ let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
30
+ this.#redisClient = redis.createRedisClient( identifier, host, port, authKey, db );
31
+ }
32
+
33
+ /* Public interface */
34
+
35
+ /**
36
+ * Used to register a new {@link ConnectionObserver} for events related to the Redis connection state.
37
+ *
38
+ * @method
39
+ * @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
40
+ * @public
41
+ */
42
+ addConnectionObserver( connectionObserver ) {
43
+ this.#redisClient.addConnectionObserver( connectionObserver );
44
+ }
45
+
46
+ /**
47
+ * Used to send a message to the specified route.
48
+ *
49
+ * @method
50
+ * @param {Message} message The message to send.
51
+ * @param {string} queue The destination queue for the message as recognized by the {@link MessageExchange} implementation.
52
+ * @returns {Promise<number>} Will return the destination queue length after adding the current message to it.
53
+ * @public
54
+ */
55
+ sendMessage( message, queue ) {
56
+ return new Promise( ( resolve, reject ) => {
57
+ let command = [ redis.cacheCommands.LIST_PUSH, queue, tools.stringifyJSON( message ) ];
58
+ this.#redisClient.executeCommands( [ command ] ).then( ( results ) => {
59
+ results = results[ 0 ];
60
+ resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
61
+ } ).catch( ( error ) => {
62
+ reject( exceptions.raise( error ) );
63
+ } );
64
+ } );
65
+ }
66
+
67
+ /**
68
+ * Used to store a message payload.
69
+ *
70
+ * @method
71
+ * @param {Object} payload
72
+ * @param {string} storeLocation
73
+ * @returns {Promise<string>} Will return an unique ID of the storage location for the payload.
74
+ * @public
75
+ */
76
+ storeMessagePayload( payload, storeLocation ) {
77
+ return new Promise( ( resolve, reject ) => {
78
+ if ( payload ) {
79
+ let storeID = tools.getUUID();
80
+ let command = [ redis.cacheCommands.HASH_SET, storeLocation, storeID, tools.stringifyJSON( payload ) ];
81
+ this.#redisClient.executeCommands( [ command ] ).then( () => {
82
+ resolve( storeID );
83
+ } ).catch( ( error ) => {
84
+ reject( exceptions.raise( error ) );
85
+ } );
86
+ } else {
87
+ resolve();
88
+ }
89
+ } );
90
+ }
91
+
92
+ /**
93
+ * Used to receive a message from the specified queue.
94
+ *
95
+ * @method
96
+ * @param {string} queue
97
+ * @returns {Promise<Message>}
98
+ * @public
99
+ */
100
+ receiveMessage( queue ) {
101
+ return new Promise( ( resolve, reject ) => {
102
+ this.#redisClient.blockingCommand( redis.cacheCommands.LIST_POP_TAIL_BLOCKING, [ queue, 0 ] ).then( ( results ) => {
103
+ results = ( results && results.length > 1 ) ? results[ 1 ] : undefined;
104
+ resolve( tools.parseJSON( results ) );
105
+ } ).catch( ( error ) => {
106
+ reject( exceptions.raise( error ) );
107
+ } );
108
+ } );
109
+ }
110
+
111
+ /**
112
+ * Used to retrieve a message payload by its store ID.
113
+ *
114
+ * @method
115
+ * @param {Message} message
116
+ * @param {string} storeLocation
117
+ * @returns {Promise<Message>} Will return the message with its payload populated if such is found.
118
+ * @public
119
+ */
120
+ retrieveMessagePayload( message, storeLocation ) {
121
+ return new Promise( ( resolve, reject ) => {
122
+ if ( message.payload ) {
123
+ let command1 = [ redis.cacheCommands.HASH_GET, storeLocation, message.payload ];
124
+ let command2 = [ redis.cacheCommands.HASH_REMOVE, storeLocation, message.payload ];
125
+ this.#redisClient.executeCommands( [ command1, command2 ] ).then( ( results ) => {
126
+ results = results[ 0 ];
127
+ message.payload = ( results && results.length > 1 ) ? tools.parseJSON( results[ 1 ] ) : undefined;
128
+ resolve( message );
129
+ } ).catch( ( error ) => {
130
+ reject( exceptions.raise( error ) );
131
+ } );
132
+ } else {
133
+ resolve( message );
134
+ }
135
+ } );
136
+ }
137
+
138
+ }
139
+
140
+ /**
141
+ * Used to create a new message memory cache.
142
+ *
143
+ * @method
144
+ * @param {string} identifier The connection identifier for the Redis connection.
145
+ * @returns {MessageMemoryCache}
146
+ * @public
147
+ */
148
+ module.exports.create = ( identifier ) => {
149
+ return Object.freeze( new MessageMemoryCache( identifier ) );
150
+ };
@@ -0,0 +1,76 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const ConnectionObserver = require( "#connection-observer" );
7
+ const exceptions = require( "#exceptions" );
8
+
9
+ /**
10
+ * An abstract class that allows the child class to observe and take action on message events.
11
+ * <br/>
12
+ * NOTE: This class inherits {@link ConnectionObserver} so it can also act in that capacity.
13
+ *
14
+ * @class MessageObserver
15
+ * @extends ConnectionObserver
16
+ * @abstract
17
+ * @public
18
+ */
19
+ class MessageObserver extends ConnectionObserver {
20
+
21
+ /**
22
+ * @constructor
23
+ */
24
+ constructor() {
25
+ super();
26
+
27
+ // make sure this abstract class cannot be instantiated:
28
+ if ( new.target === MessageObserver ) {
29
+ throw exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_CLASS_INIT, { name: this.constructor.name } );
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Needs to be invoked by the message handler once a message enters its logic for processing.
35
+ * <br/>
36
+ * NOTE: Override this to add custom functionality.
37
+ *
38
+ * @method
39
+ * @param {string} identifier The identifier of the observed connection.
40
+ * @param {Message} message The message for processing.
41
+ * @virtual
42
+ * @public
43
+ */
44
+ onMessage( identifier, message ) { }
45
+
46
+ /**
47
+ * Needs to be invoked by the connection handler when the connection is disrupted.
48
+ * <br/>
49
+ * NOTE: Override this to add custom functionality.
50
+ *
51
+ * @method
52
+ * @param {string} identifier The identifier of the observed connection.
53
+ * @virtual
54
+ * @public
55
+ */
56
+ onConnectionDisrupted( identifier ) {
57
+ super.onConnectionDisrupted( identifier );
58
+ }
59
+
60
+ /**
61
+ * Needs to be invoked by the connection handler when the connection is recovered.
62
+ * <br/>
63
+ * NOTE: Override this to add custom functionality.
64
+ *
65
+ * @method
66
+ * @param {string} identifier The identifier of the observed connection.
67
+ * @virtual
68
+ * @public
69
+ */
70
+ onConnectionRecovered( identifier ) {
71
+ super.onConnectionRecovered( identifier );
72
+ }
73
+
74
+ }
75
+
76
+ module.exports = MessageObserver;
@@ -0,0 +1,113 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const MessageHandler = require( "#message-handler" );
7
+ const logger = require( "#logger" );
8
+ const exceptions = require( "#exceptions" );
9
+
10
+ /**
11
+ * An abstract class that defines a basic message receiver behavior.
12
+ *
13
+ * @class MessageReceiver
14
+ * @extends MessageHandler
15
+ * @abstract
16
+ * @public
17
+ */
18
+ class MessageReceiver extends MessageHandler {
19
+
20
+ #receiveQueue;
21
+
22
+ /**
23
+ * @constructor
24
+ * @param {string} identifier An identifier for this message handler. Should be unique in the context of the message exchange.
25
+ * @param {string} receiveQueue The queue from which the messages will be received.
26
+ */
27
+ constructor( identifier, receiveQueue ) {
28
+ super( identifier );
29
+
30
+ // make sure this abstract class cannot be instantiated:
31
+ if ( new.target === MessageReceiver ) {
32
+ throw exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_CLASS_INIT, { name: this.constructor.name } );
33
+ }
34
+
35
+ this.#receiveQueue = receiveQueue;
36
+ }
37
+
38
+ /* Public interface */
39
+
40
+ /**
41
+ * Property returning the configured receive queue.
42
+ *
43
+ * @property
44
+ * @returns {string}
45
+ * @public
46
+ */
47
+ get receiveQueue() { return this.#receiveQueue; }
48
+
49
+ /**
50
+ * Used to initialize and enable the communication capabilities of the handler.
51
+ * <br/>
52
+ * NOTE: Override this to add functionality.
53
+ *
54
+ * @method
55
+ * @returns {Promise}
56
+ * @abstract
57
+ * @public
58
+ */
59
+ enable() {
60
+ return super.enable();
61
+ }
62
+
63
+ /**
64
+ * Used to shutdown and disable the communication behavior of the handler.
65
+ * <br/>
66
+ * NOTE: Override this to add functionality.
67
+ *
68
+ * @method
69
+ * @returns {Promise}
70
+ * @abstract
71
+ * @public
72
+ */
73
+ disable() {
74
+ return super.disable();
75
+ }
76
+
77
+ /**
78
+ * Used to receive messages.
79
+ * <br/>
80
+ * NOTE: This method will start a recursion of subsequent receives that will continue even if an individual message fetch fails.
81
+ *
82
+ * @method
83
+ * @public
84
+ */
85
+ receive() {
86
+ this.onReceive().then( ( message ) => {
87
+ this.onMessage( message );
88
+ } ).catch( ( error ) => {
89
+ logger.log( `Error while trying to receive the next pending message from memory cache in receiver '${ this.connectionIdentifier }'! Resuming operation...`, logger.logSeverity.ERROR, error );
90
+ } ).finally( () => {
91
+ this.receive();
92
+ } );
93
+ }
94
+
95
+ /**
96
+ * Used to receive messages.
97
+ * <br/>
98
+ * NOTE: This method will be called automatically even if overridden.
99
+ * <br/>
100
+ * NOTE: Override this to add functionality.
101
+ *
102
+ * @method
103
+ * @returns {Promise<Message>}
104
+ * @abstract
105
+ * @public
106
+ */
107
+ onReceive() {
108
+ return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_METHOD_CALL, { name: this.constructor.name + "." + this.onReceive.name } ) );
109
+ }
110
+
111
+ }
112
+
113
+ module.exports = MessageReceiver;
@@ -0,0 +1,133 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const MessageHandler = require( "#message-handler" );
7
+ const exceptions = require( "#exceptions" );
8
+
9
+ /**
10
+ * An abstract class that defines a basic message sender behavior.
11
+ *
12
+ * @class MessageSender
13
+ * @extends MessageHandler
14
+ * @abstract
15
+ * @public
16
+ */
17
+ class MessageSender extends MessageHandler {
18
+
19
+ /**
20
+ * @constructor
21
+ * @param {string} identifier An identifier for this message handler. Should be unique in the context of the message exchange.
22
+ */
23
+ constructor( identifier ) {
24
+ super( identifier );
25
+
26
+ // make sure this abstract class cannot be instantiated:
27
+ if ( new.target === MessageSender ) {
28
+ throw exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_CLASS_INIT, { name: this.constructor.name } );
29
+ }
30
+ }
31
+
32
+ /* Public interface */
33
+
34
+ /**
35
+ * Used to initialize and enable the communication capabilities of the handler.
36
+ * <br/>
37
+ * NOTE: Override this to add functionality.
38
+ *
39
+ * @method
40
+ * @returns {Promise}
41
+ * @abstract
42
+ * @public
43
+ */
44
+ enable() {
45
+ return super.enable();
46
+ }
47
+
48
+ /**
49
+ * Used to shutdown and disable the communication behavior of the handler.
50
+ * <br/>
51
+ * NOTE: Override this to add functionality.
52
+ *
53
+ * @method
54
+ * @returns {Promise}
55
+ * @abstract
56
+ * @public
57
+ */
58
+ disable() {
59
+ return super.disable();
60
+ }
61
+
62
+ /**
63
+ * Used to send a {@link Message} via this message handler.
64
+ *
65
+ * @method
66
+ * @param {Message} message The message to send.
67
+ * @param {string} queue The route to destination (queue) for the message as recognized by the {@link MessageExchange} implementation.
68
+ * @returns {Promise}
69
+ * @public
70
+ */
71
+ send( message, queue ) {
72
+ return new Promise( ( resolve, reject ) => {
73
+ this.#preSend().then( () => {
74
+ return this.onSend( message, queue );
75
+ } ).then( () => {
76
+ return this.#postSend();
77
+ } ).then( () => {
78
+ resolve();
79
+ } ).catch( ( error ) => {
80
+ reject( exceptions.raise( error ) );
81
+ } );
82
+ } );
83
+ }
84
+
85
+ /**
86
+ * Used to perform the actual sending of a message.
87
+ * <br/>
88
+ * NOTE: This method will be called automatically even if overridden.
89
+ * <br/>
90
+ * NOTE: Override this to add functionality.
91
+ *
92
+ * @method
93
+ * @param {Message} message The message to send.
94
+ * @param {string} queue The route to destination (queue) for the message as recognized by the {@link MessageExchange} implementation.
95
+ * @returns {Promise<*>}
96
+ * @abstract
97
+ * @public
98
+ */
99
+ onSend( message, queue ) {
100
+ return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_METHOD_CALL, { name: this.constructor.name + "." + this.onSend.name } ) );
101
+ }
102
+
103
+ /* Private interface */
104
+
105
+ /**
106
+ * Used to do pre-send verifications and checks.
107
+ *
108
+ * @method
109
+ * @returns {Promise}
110
+ * @private
111
+ */
112
+ #preSend() {
113
+ if ( this.isAvailable === true ) {
114
+ return Promise.resolve();
115
+ } else {
116
+ return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_COM_MESSAGE_SENDER_UNAVAILABLE ) );
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Used to execute post- successful send logic.
122
+ *
123
+ * @method
124
+ * @returns {Promise}
125
+ * @private
126
+ */
127
+ #postSend() {
128
+ return Promise.resolve();
129
+ }
130
+
131
+ }
132
+
133
+ module.exports = MessageSender;
@@ -0,0 +1,146 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
3
+ * SPDX-License-Identifier: ICU
4
+ */
5
+
6
+ const _ = require( "lodash" );
7
+ const tools = require( "#tools" );
8
+ const config = require( "#config" );
9
+ const logger = require( "#logger" );
10
+
11
+ /**
12
+ * @typedef {Object} TiTraceEntry
13
+ * @property {string} chainID
14
+ * @property {string} dispatchEvent
15
+ * @property {string} fromAddress
16
+ * @property {string} messageID
17
+ * @property {string} messageSnapshot
18
+ * @property {string} messageState
19
+ * @property {string} messageType
20
+ * @property {string} toAddress
21
+ * @property {string} traceID
22
+ */
23
+
24
+ /**
25
+ * Enum for listing message types.
26
+ *
27
+ * @readonly
28
+ * @enum {number}
29
+ */
30
+ let messageTypeEnum = tools.enum( {
31
+ MESSAGE_REQUEST: [ 1000, "REQUEST", "" ],
32
+ MESSAGE_RESPONSE: [ 1001, "RESPONSE", "" ]
33
+ } );
34
+
35
+ /**
36
+ * @typedef {number} TiMessageType
37
+ */
38
+ module.exports.messageType = messageTypeEnum;
39
+
40
+ /**
41
+ * Enum for listing dispatch events.
42
+ *
43
+ * @readonly
44
+ * @enum {number}
45
+ */
46
+ let dispatchEventEnum = tools.enum( {
47
+ DELIVERED: [ 1100, "DELIVERED", "When message delivery is confirmed." ],
48
+ FAILED: [ 1101, "FAILED", "When message delivery has failed." ],
49
+ RECEIVED: [ 1102, "RECEIVED", "When message was received." ],
50
+ SENT: [ 1103, "SENT", "When message was sent." ]
51
+ } );
52
+
53
+ /**
54
+ * @typedef {number} TiDispatchEvent
55
+ */
56
+ module.exports.dispatchEvent = dispatchEventEnum;
57
+
58
+ /**
59
+ * Enum for listing message states.
60
+ *
61
+ * @readonly
62
+ * @enum {number}
63
+ */
64
+ let messageStateEnum = tools.enum( {
65
+ PENDING: [ 1200, "PENDING", "" ],
66
+ PROCESSED: [ 1201, "PROCESSED", "" ]
67
+ } );
68
+
69
+ /**
70
+ * @typedef {number} TiMessageState
71
+ */
72
+ module.exports.messageState = messageStateEnum;
73
+
74
+ /**
75
+ * Used to create a log entry from the trace entry.
76
+ *
77
+ * @method
78
+ * @param {TiTraceEntry} traceEntry The trace entry to log.
79
+ * @param {TiLogSeverity} severity The log entry severity.
80
+ * @private
81
+ */
82
+ let createLogEntry = ( traceEntry, severity ) => {
83
+ logger.log( formatLogEntry( traceEntry ), severity, traceEntry );
84
+ };
85
+
86
+ /**
87
+ * Used to format trace entry into log-suitable string.
88
+ *
89
+ * @method
90
+ * @param {TiTraceEntry} traceEntry
91
+ * @return {string} Prepared trace info.
92
+ * @private
93
+ */
94
+ let formatLogEntry = ( traceEntry ) => {
95
+ return `Message(${ traceEntry.chainID || traceEntry.messageID }) Trace: '${ traceEntry.messageType } ${ traceEntry.dispatchEvent } ${ traceEntry.messageState }' From: '${ traceEntry.fromAddress }' To: '${ traceEntry.toAddress }'`;
96
+ };
97
+
98
+ /**
99
+ * Used to obscure sensitive data in the message snapshot and convert it to string.
100
+ *
101
+ * @method
102
+ * @param {Message} message
103
+ * @returns {string}
104
+ * @private
105
+ */
106
+ let obscureSensitiveData = ( message ) => {
107
+ let messageSnapshot = tools.stringifyJSON( message );
108
+ return _.replace( messageSnapshot, /("\w*?pin\w*?"|"\w*?pass\w*?"|"\w*?otp\w*?"):"(.*?)"/gmi, "\"SENSITIVE_PROPERTY\":\"OBSCURED_BY_SYSTEM\"" );
109
+ };
110
+
111
+ /**
112
+ * Used to create a trace entry for the provided {@link Message} and parameters.
113
+ * <br/>
114
+ * NOTE: With the exception of failed message delivery, trace events are logged with severity level DEBUG.
115
+ *
116
+ * @method
117
+ * @param {Message} message The message to trace.
118
+ * @param {TiMessageType} messageType The type of the message.
119
+ * @param {TiDispatchEvent} dispatchEvent The event in the dispatch system that triggered the trace entry.
120
+ * @param {TiMessageState} messageState The state of the processing of the message.
121
+ * @public
122
+ */
123
+ module.exports.recordTraceEntry = ( message, messageType, dispatchEvent, messageState ) => {
124
+ // depending on whether the message comes as request or response, the from and to addresses will be opposite:
125
+ let source = message.source.route + "." + message.source.instanceID;
126
+ let destination = message.destination.route + ( ( message.destination.instanceID != null ) ? "." + message.destination.instanceID : "" );
127
+
128
+ /** @type TiTraceEntry */
129
+ let traceEntry = {
130
+ chainID: message.chainID,
131
+ dispatchEvent: tools.getEnumName( dispatchEventEnum, dispatchEvent ),
132
+ fromAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? source : destination,
133
+ messageID: message.messageID,
134
+ messageSnapshot: obscureSensitiveData( message ),
135
+ messageState: tools.getEnumName( messageStateEnum, messageState ),
136
+ messageType: tools.getEnumName( messageTypeEnum, messageType ),
137
+ toAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? destination : source,
138
+ traceID: tools.getUUID()
139
+ };
140
+
141
+ if ( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_LOG_ENABLED ) === true ) {
142
+ createLogEntry( traceEntry, ( dispatchEvent === dispatchEventEnum.FAILED ) ? logger.logSeverity.ERROR : logger.logSeverity.DEBUG );
143
+ }
144
+
145
+ // TODO Feature: Functionality that can dispatch the trace entry to a configurable database and/or monitoring system.
146
+ };