@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,261 @@
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 ioredis = require( "ioredis" );
8
+ const tools = require( "#tools" );
9
+ const logger = require( "#logger" );
10
+ const exceptions = require( "#exceptions" );
11
+ const _ = require( "lodash" );
12
+
13
+ /**
14
+ * Enum for listing all used Redis cache commands.
15
+ *
16
+ * @readonly
17
+ * @enum {string}
18
+ */
19
+ let cacheCommandsEnum = tools.enum( {
20
+ ADD_TO_SET: [ "sadd", "add to set", "https://redis.io/commands/sadd" ],
21
+ DELETE_VALUE: [ "del", "delete value", "https://redis.io/commands/del" ],
22
+ GET_ALL_FROM_SET: [ "smembers", "get all set members", "https://redis.io/commands/smembers" ],
23
+ GET_VALUE: [ "get", "get value", "https://redis.io/commands/get" ],
24
+ HASH_GET: [ "hget", "hash get", "https://redis.io/commands/hget" ],
25
+ HASH_GET_ALL: [ "hgetall", "hash get all", "https://redis.io/commands/hgetall" ],
26
+ HASH_REMOVE: [ "hdel", "hash remove", "https://redis.io/commands/hdel" ],
27
+ HASH_SET: [ "hset", "", "https://redis.io/commands/hset" ],
28
+ HASH_SET_MANY: [ "hmset", "", "https://redis.io/commands/hmset" ],
29
+ IS_SET_MEMBER: [ "sismember", "", "https://redis.io/commands/sismember" ],
30
+ KEYS: [ "keys", "", "https://redis.io/commands/keys" ],
31
+ LIST_PUSH: [ "lpush", "list push", "https://redis.io/commands/lpush" ],
32
+ LIST_POP_TAIL_BLOCKING: [ "brpop", "list pop tail blocking", "https://redis.io/commands/brpop" ],
33
+ LIST_POP_TAIL_PUSH_HEAD_BLOCKING: [ "brpoplpush", "list pop tail push head blocking", "https://redis.io/commands/brpoplpush" ],
34
+ LIST_REMOVE: [ "lrem", "list remove", "https://redis.io/commands/lrem" ],
35
+ SET_VALUE: [ "set", "set value", "https://redis.io/commands/set" ],
36
+ UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/commands/sunion" ]
37
+ } );
38
+
39
+ /**
40
+ * @typedef {string} TiRedisCommand
41
+ */
42
+ module.exports.cacheCommands = cacheCommandsEnum;
43
+
44
+ /**
45
+ * Used to create a Redis Cache client.
46
+ *
47
+ * @class RedisClient
48
+ * @public
49
+ */
50
+ class RedisClient {
51
+
52
+ #clientIdentifier = "default";
53
+ #retryMaxInterval = 1000;
54
+ #retryMaxAttempts = undefined;
55
+ #redisClient = undefined;
56
+ #connectionObservers = [];
57
+
58
+ /**
59
+ * @constructor
60
+ * @param {string} identifier
61
+ * @param {string} host
62
+ * @param {number} port
63
+ * @param {string} authKey
64
+ * @param {number} defaultDB
65
+ * @param {boolean} autoRetryUnfulfilled
66
+ * @param {number} maxRetries
67
+ */
68
+ constructor( identifier, host, port, authKey, defaultDB, autoRetryUnfulfilled, maxRetries ) {
69
+ this.#clientIdentifier = identifier || this.#clientIdentifier;
70
+
71
+ let retryStrategy = ( attempt ) => {
72
+ let result = Math.min( attempt * 50, this.#retryMaxInterval );
73
+
74
+ if ( this.#retryMaxAttempts != null && attempt > this.#retryMaxAttempts ) {
75
+ logger.log( "In Redis retry strategy: reached max attempts for command retry. Aborting...", logger.logSeverity.WARNING, { attempts: attempt } );
76
+ result = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
77
+ }
78
+
79
+ return result;
80
+ };
81
+
82
+ let reconnectOnError = ( error ) => {
83
+ logger.log( `In Redis reconnect on error strategy: ${ error.message }`, logger.logSeverity.ERROR, error );
84
+ return !!error.message.includes( "READONLY" );
85
+ };
86
+
87
+ let options = {
88
+ port: port,
89
+ host: host,
90
+ password: authKey,
91
+ db: defaultDB,
92
+ autoResendUnfulfilledCommands: autoRetryUnfulfilled,
93
+ maxRetriesPerRequest: maxRetries,
94
+ retryStrategy: retryStrategy,
95
+ reconnectOnError: reconnectOnError
96
+ };
97
+
98
+ /** @type Redis */
99
+ this.#redisClient = new ioredis( options );
100
+
101
+ this.#redisClient.on( "ready", () => {
102
+ logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO, {
103
+ redis_version: this.#redisClient.serverInfo.redis_version,
104
+ redis_mode: this.#redisClient.serverInfo.redis_mode,
105
+ os: this.#redisClient.serverInfo.os,
106
+ uptime_in_days: this.#redisClient.serverInfo.uptime_in_days,
107
+ connected_clients: this.#redisClient.serverInfo.connected_clients,
108
+ role: this.#redisClient.serverInfo.role,
109
+ connected_slaves: this.#redisClient.serverInfo.connected_slaves
110
+ } );
111
+
112
+ // notify all connection observers about this event:
113
+ _.forEach( this.#connectionObservers, ( connectionObservers ) => {
114
+ connectionObservers.onConnectionRecovered( this.#clientIdentifier );
115
+ } );
116
+ } );
117
+ this.#redisClient.on( "error", ( error ) => {
118
+ logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
119
+
120
+ // notify all connection observers about this event:
121
+ _.forEach( this.#connectionObservers, ( connectionObservers ) => {
122
+ connectionObservers.onConnectionDisrupted( this.#clientIdentifier );
123
+ } );
124
+ } );
125
+ this.#redisClient.on( "reconnecting", ( info ) => {
126
+ if ( info.attempt > 1 ) {
127
+ logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ info.delay } ms. This is attempt ${ info.attempt }.`, logger.logSeverity.DEBUG );
128
+ }
129
+ } );
130
+ }
131
+
132
+ /* Public interface */
133
+
134
+ /**
135
+ * Used to return the Redis client identifier.
136
+ *
137
+ * @property
138
+ * @return {string}
139
+ * @public
140
+ */
141
+ get identifier() {
142
+ return this.#clientIdentifier;
143
+ }
144
+
145
+ /**
146
+ * Used to register a new {@link ConnectionObserver} for events related to the Redis connection state.
147
+ *
148
+ * @method
149
+ * @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
150
+ * @public
151
+ */
152
+ addConnectionObserver( connectionObserver ) {
153
+ if ( connectionObserver instanceof ConnectionObserver ) {
154
+ this.#connectionObservers.push( connectionObserver );
155
+ } else {
156
+ logger.log( `Attempting to add '${ connectionObserver.constructor.name }' as connection observer but it's not a child-class of 'ConnectionObserver'!`, logger.logSeverity.WARNING );
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Used to execute multiple commands within a Redis transaction.
162
+ *
163
+ * @method
164
+ * @param {Array[]} commands
165
+ * @return {Promise<*>}
166
+ * @public
167
+ */
168
+ executeCommands( commands ) {
169
+ return new Promise( ( resolve, reject ) => {
170
+ this.#redisClient.multi( commands ).exec().then( ( results ) => {
171
+ resolve( results );
172
+ } ).catch( ( error ) => {
173
+ reject( exceptions.raise( error ) );
174
+ } );
175
+ } );
176
+ }
177
+
178
+ /**
179
+ * Used to send a new blocking command to Redis.
180
+ * WARNING: This will reserve the client connection until a result is received.
181
+ *
182
+ * @method
183
+ * @param {string} command
184
+ * @param {Array} commandArguments
185
+ * @return {Promise<*>}
186
+ * @public
187
+ */
188
+ blockingCommand( command, commandArguments ) {
189
+ return new Promise( ( resolve, reject ) => {
190
+ this.#redisClient[ command ].apply( this.#redisClient, commandArguments ).then( ( results ) => {
191
+ resolve( results );
192
+ } ).catch( ( error ) => {
193
+ reject( exceptions.raise( error ) );
194
+ } );
195
+ } );
196
+ }
197
+
198
+ /**
199
+ * Used to publish a message to the specified channel.
200
+ *
201
+ * @method
202
+ * @param {string} channel
203
+ * @param {(Object|string)} message
204
+ * @return {Promise<number>}
205
+ * @public
206
+ */
207
+ publishCommand( channel, message ) {
208
+ return new Promise( ( resolve, reject ) => {
209
+ this.#redisClient.publish( channel, tools.stringifyJSON( message ) ).then( ( receivedBy ) => {
210
+ resolve( receivedBy );
211
+ } ).catch( ( error ) => {
212
+ reject( exceptions.raise( error ) );
213
+ } );
214
+ } );
215
+ }
216
+
217
+ /**
218
+ * Used to subscribe to the specified channel for messages.
219
+ *
220
+ * @method
221
+ * @param {string} channel
222
+ * @param {function( Object )} messageHandler Will execute this handler every time a new message is received.
223
+ * @return {Promise}
224
+ * @public
225
+ */
226
+ subscribeCommand( channel, messageHandler ) {
227
+ return new Promise( ( resolve, reject ) => {
228
+ this.#redisClient.on( "subscribe", ( channel, count ) => {
229
+ logger.log( "Subscription to message channel '" + channel + "' successful.", logger.logSeverity.DEBUG );
230
+ resolve();
231
+ } );
232
+ this.#redisClient.on( "message", ( channel, message ) => {
233
+ if ( typeof ( messageHandler ) === "function" ) {
234
+ messageHandler( tools.parseJSON( message ) );
235
+ }
236
+ } );
237
+ this.#redisClient.subscribe( channel ).catch( ( error ) => {
238
+ reject( exceptions.raise( error ) );
239
+ } );
240
+ } );
241
+ }
242
+
243
+ }
244
+
245
+ /**
246
+ * Create and return a new Redis client.
247
+ *
248
+ * @method
249
+ * @param {string} identifier
250
+ * @param {string} host
251
+ * @param {number} [port=6379]
252
+ * @param {string} [authKey=undefined]
253
+ * @param {number} [defaultDB=0]
254
+ * @param {boolean} [autoRetryUnfulfilled=true]
255
+ * @param {number} [maxRetries=20]
256
+ * @return {RedisClient}
257
+ * @public
258
+ */
259
+ module.exports.createRedisClient = ( identifier, host, port = 6379, authKey = undefined, defaultDB = 0, autoRetryUnfulfilled = true, maxRetries = 20 ) => {
260
+ return Object.freeze( new RedisClient( identifier, host, port, authKey, defaultDB, autoRetryUnfulfilled, maxRetries ) );
261
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@ti-engine/core",
3
+ "version": "1.0.0",
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
+ "author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
6
+ "license": "ISC",
7
+ "exports": {
8
+ "./service-consumer": "./components/service-consumer.js",
9
+ "./service-instance": "./components/service-instance.js",
10
+ "./service-provider": "./components/service-provider.js",
11
+ "./exceptions": "./utils/exceptions.js",
12
+ "./tools": "./utils/tools.js"
13
+ },
14
+ "imports": {
15
+ "#auditing": "./components/auditing.js",
16
+ "#cache": "./utils/cache.js",
17
+ "#config": "./utils/config.js",
18
+ "#connection-observer": "./components/connection-observer.js",
19
+ "#default-message-exchange": "./components/exchange/default/default-message-exchange.js",
20
+ "#default-message-receiver": "./components/exchange/default/default-message-receiver.js",
21
+ "#default-message-sender": "./components/exchange/default/default-message-sender.js",
22
+ "#exceptions": "./utils/exceptions.js",
23
+ "#gcloud-integration": "./integrations/gcloud-integration.js",
24
+ "#logger": "./utils/logger.js",
25
+ "#message-dispatcher": "./components/exchange/message-dispatcher.js",
26
+ "#message-exchange": "./components/exchange/message-exchange.js",
27
+ "#message-handler": "./components/exchange/message-handler.js",
28
+ "#message-memory-cache": "./components/exchange/message-memory-cache.js",
29
+ "#message-observer": "./components/exchange/message-observer.js",
30
+ "#message-receiver": "./components/exchange/message-receiver.js",
31
+ "#message-sender": "./components/exchange/message-sender.js",
32
+ "#message-tracer": "./components/exchange/message-tracer.js",
33
+ "#redis-integration": "./integrations/redis-integration.js",
34
+ "#service-caller": "./components/service-caller.js",
35
+ "#service-consumer": "./components/service-consumer.js",
36
+ "#service-executor": "./components/service-executor.js",
37
+ "#service-instance": "./components/service-instance.js",
38
+ "#service-provider": "./components/service-provider.js",
39
+ "#settings": "./settings.json",
40
+ "#tools": "./utils/tools.js"
41
+ },
42
+ "dependencies": {
43
+ "fs-extra": "^10.0.0",
44
+ "lodash": "^4.17.21",
45
+ "node-schedule": "^2.0.0",
46
+ "ioredis": "^4.27.7"
47
+ },
48
+ "optionalDependencies": {
49
+ "@google-cloud/error-reporting": "^2.0.2"
50
+ },
51
+ "engines": {
52
+ "node": ">=14.17.0"
53
+ }
54
+ }
package/settings.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "auditing": {
3
+ "logConsoleEnabled": true,
4
+ "logDetails": false,
5
+ "logMinLevel": 0,
6
+ "logUsesJSON": false
7
+ },
8
+ "gcloudIntegration": {
9
+ "apiKey": "",
10
+ "loggingEnabled": false,
11
+ "logName": "ti-engine",
12
+ "projectID": ""
13
+ },
14
+ "memoryCache": {
15
+ "authKey": null,
16
+ "redisDB": 0,
17
+ "redisHost": "127.0.0.1",
18
+ "redisPort": 6379
19
+ },
20
+ "messageExchange": {
21
+ "messageQueuePrefix": "ti:messages:",
22
+ "messageStore": "ti:messages:store",
23
+ "traceLogEnabled": true
24
+ },
25
+ "serviceConfig": {
26
+ "executionTimeout": 180000,
27
+ "healthCheckAddress": "ti:services:registry:health:",
28
+ "healthCheckInterval": "*/1 * * * * *",
29
+ "healthCheckTimeout": 3,
30
+ "serviceRegistryAddress": "ti:services:registry:catalog:"
31
+ },
32
+ "operationMode": "production"
33
+ }