@ti-engine/core 1.1.5 → 1.1.9
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 +39 -0
- package/README.md +45 -11
- package/bin/start-instance.js +40 -16
- package/components/auditing.js +3 -3
- package/components/exchange/default/default-message-receiver.js +6 -2
- package/components/exchange/default/default-message-sender.js +6 -2
- package/components/exchange/message-dispatcher.js +13 -9
- package/components/exchange/message-exchange.js +57 -21
- package/components/exchange/message-memory-cache.js +31 -4
- package/components/exchange/message-tracer.js +138 -92
- package/components/service-caller.js +6 -6
- package/components/service-consumer.js +3 -3
- package/components/service-executor.js +38 -16
- package/components/service-instance.js +46 -28
- package/components/service-provider.js +5 -5
- package/integrations/gcloud-integration.js +3 -2
- package/integrations/redis-integration.js +257 -138
- package/package.json +6 -6
- package/testRedis.js +22 -0
- package/utils/cache.js +44 -14
- package/utils/config.js +9 -1
- package/utils/localization.js +2 -2
- package/utils/logger.js +3 -4
|
@@ -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-
|
|
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 config = require( "#config" );
|
|
10
|
+
const tools = require( "#tools" );
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Used to verify if the GCloud integration is enabled.
|
|
@@ -16,7 +17,7 @@ const config = require( "#config" );
|
|
|
16
17
|
* @public
|
|
17
18
|
*/
|
|
18
19
|
module.exports.isEnabled = () => {
|
|
19
|
-
return process.env.TI_GCLOUD_ENABLED
|
|
20
|
+
return tools.toBool( process.env.TI_GCLOUD_ENABLED );
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -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-
|
|
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/>.
|
|
@@ -20,27 +20,27 @@ const _ = require( "lodash" );
|
|
|
20
20
|
* @enum {string}
|
|
21
21
|
*/
|
|
22
22
|
let cacheCommandsEnum = tools.enum( {
|
|
23
|
-
ADD_TO_SET: [ "sadd", "add to set", "https://redis.io/commands/sadd" ],
|
|
24
|
-
DELETE_VALUE: [ "del", "delete value", "https://redis.io/commands/del" ],
|
|
25
|
-
EXPIRE: [ "expire", "expire", "https://redis.io/commands/expire" ],
|
|
26
|
-
GET_ALL_FROM_SET: [ "smembers", "get all set members", "https://redis.io/commands/smembers" ],
|
|
27
|
-
GET_VALUE: [ "get", "get value", "https://redis.io/commands/get" ],
|
|
28
|
-
HASH_GET: [ "hget", "hash get", "https://redis.io/commands/hget" ],
|
|
29
|
-
HASH_GET_ALL: [ "hgetall", "hash get all", "https://redis.io/commands/hgetall" ],
|
|
30
|
-
HASH_REMOVE: [ "hdel", "hash remove", "https://redis.io/commands/hdel" ],
|
|
31
|
-
HASH_SET: [ "hset", "", "https://redis.io/commands/hset" ],
|
|
32
|
-
HASH_SET_MANY: [ "hmset", "", "https://redis.io/commands/hmset" ],
|
|
33
|
-
IS_SET_MEMBER: [ "sismember", "", "https://redis.io/commands/sismember" ],
|
|
34
|
-
JSON_ARRAY_APPEND: [ "json.arrappend", "", "https://redis.io/commands/json.arrappend" ],
|
|
35
|
-
JSON_GET: [ "json.get", "", "https://redis.io/commands/json.get" ],
|
|
36
|
-
JSON_SET: [ "json.set", "", "https://redis.io/commands/json.set" ],
|
|
37
|
-
KEYS: [ "keys", "", "https://redis.io/commands/keys" ],
|
|
38
|
-
LIST_PUSH: [ "lpush", "list push", "https://redis.io/commands/lpush" ],
|
|
39
|
-
LIST_POP_TAIL_BLOCKING: [ "brpop", "list pop tail blocking", "https://redis.io/commands/brpop" ],
|
|
40
|
-
LIST_POP_TAIL_PUSH_HEAD_BLOCKING: [ "brpoplpush", "list pop tail push head blocking", "https://redis.io/commands/brpoplpush" ],
|
|
41
|
-
LIST_REMOVE: [ "lrem", "list remove", "https://redis.io/commands/lrem" ],
|
|
42
|
-
SET_VALUE: [ "set", "set value", "https://redis.io/commands/set" ],
|
|
43
|
-
UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/commands/sunion" ]
|
|
23
|
+
ADD_TO_SET: [ "sadd", "add to set", "https://redis.io/docs/latest/commands/sadd/" ],
|
|
24
|
+
DELETE_VALUE: [ "del", "delete value", "https://redis.io/docs/latest/commands/del/" ],
|
|
25
|
+
EXPIRE: [ "expire", "expire", "https://redis.io/docs/latest/commands/expire/" ],
|
|
26
|
+
GET_ALL_FROM_SET: [ "smembers", "get all set members", "https://redis.io/docs/latest/commands/smembers/" ],
|
|
27
|
+
GET_VALUE: [ "get", "get value", "https://redis.io/docs/latest/commands/get/" ],
|
|
28
|
+
HASH_GET: [ "hget", "hash get", "https://redis.io/docs/latest/commands/hget/" ],
|
|
29
|
+
HASH_GET_ALL: [ "hgetall", "hash get all", "https://redis.io/docs/latest/commands/hgetall/" ],
|
|
30
|
+
HASH_REMOVE: [ "hdel", "hash remove", "https://redis.io/docs/latest/commands/hdel/" ],
|
|
31
|
+
HASH_SET: [ "hset", "", "https://redis.io/docs/latest/commands/hset/" ],
|
|
32
|
+
HASH_SET_MANY: [ "hmset", "(deprecated) use HSET with multiple fields", "https://redis.io/docs/latest/commands/hmset/" ],
|
|
33
|
+
IS_SET_MEMBER: [ "sismember", "", "https://redis.io/docs/latest/commands/sismember/" ],
|
|
34
|
+
JSON_ARRAY_APPEND: [ "json.arrappend", "", "https://redis.io/docs/latest/commands/json.arrappend/" ],
|
|
35
|
+
JSON_GET: [ "json.get", "", "https://redis.io/docs/latest/commands/json.get/" ],
|
|
36
|
+
JSON_SET: [ "json.set", "", "https://redis.io/docs/latest/commands/json.set/" ],
|
|
37
|
+
KEYS: [ "keys", "(warning: O(N), use SCAN where possible)", "https://redis.io/docs/latest/commands/keys/" ],
|
|
38
|
+
LIST_PUSH: [ "lpush", "list push", "https://redis.io/docs/latest/commands/lpush/" ],
|
|
39
|
+
LIST_POP_TAIL_BLOCKING: [ "brpop", "list pop tail blocking", "https://redis.io/docs/latest/commands/brpop/" ],
|
|
40
|
+
LIST_POP_TAIL_PUSH_HEAD_BLOCKING: [ "brpoplpush", "list pop tail push head blocking", "https://redis.io/docs/latest/commands/brpoplpush/" ],
|
|
41
|
+
LIST_REMOVE: [ "lrem", "list remove", "https://redis.io/docs/latest/commands/lrem/" ],
|
|
42
|
+
SET_VALUE: [ "set", "set value", "https://redis.io/docs/latest/commands/set/" ],
|
|
43
|
+
UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/docs/latest/commands/sunion/" ]
|
|
44
44
|
} );
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -66,6 +66,9 @@ module.exports.cacheOverrideMode = cacheOverrideModeEnum;
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* Used to create a Redis Cache client.
|
|
69
|
+
* <br/>
|
|
70
|
+
* NOTE: This client is set to automatically resend all pending commands on connection recovery with no limit on the retry attempts.
|
|
71
|
+
* This is done to avoid losing any pending commands in case of a connection failure. For a different behavior, use custom implementation.
|
|
69
72
|
*
|
|
70
73
|
* @class RedisClient
|
|
71
74
|
* @public
|
|
@@ -79,101 +82,15 @@ class RedisClient {
|
|
|
79
82
|
#serverInfo = {};
|
|
80
83
|
#serverFeatures = {};
|
|
81
84
|
#connectionObservers = [];
|
|
85
|
+
#messageHandlersByChannel = new Map();
|
|
82
86
|
|
|
83
87
|
/**
|
|
84
88
|
* @constructor
|
|
85
89
|
* @param {string} identifier
|
|
86
|
-
* @
|
|
87
|
-
* @param {number} port
|
|
88
|
-
* @param {string} authKey
|
|
89
|
-
* @param {string} user
|
|
90
|
-
* @param {number} defaultDB
|
|
91
|
-
* @param {boolean} autoRetryUnfulfilled
|
|
92
|
-
* @param {number} maxRetries
|
|
90
|
+
* @returns {RedisClient}
|
|
93
91
|
*/
|
|
94
|
-
constructor( identifier
|
|
92
|
+
constructor( identifier ) {
|
|
95
93
|
this.#clientIdentifier = identifier || "redis-client-" + tools.getUUID();
|
|
96
|
-
|
|
97
|
-
let retryStrategy = ( attempt ) => {
|
|
98
|
-
let result = Math.min( attempt * 50, this.#retryMaxInterval );
|
|
99
|
-
|
|
100
|
-
if ( this.#retryMaxAttempts != null && attempt > this.#retryMaxAttempts ) {
|
|
101
|
-
logger.log( "In Redis retry strategy: reached max attempts for command retry. Aborting...", logger.logSeverity.WARNING, { attempts: attempt } );
|
|
102
|
-
result = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return result;
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
let reconnectOnError = ( error ) => {
|
|
109
|
-
logger.log( `In Redis reconnect on error strategy: ${ error.message }`, logger.logSeverity.ERROR, error );
|
|
110
|
-
return !!error.message.includes( "READONLY" );
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
let options = {
|
|
114
|
-
port: port,
|
|
115
|
-
host: host,
|
|
116
|
-
username: user,
|
|
117
|
-
password: authKey,
|
|
118
|
-
db: defaultDB,
|
|
119
|
-
autoResendUnfulfilledCommands: autoRetryUnfulfilled,
|
|
120
|
-
maxRetriesPerRequest: maxRetries,
|
|
121
|
-
retryStrategy: retryStrategy,
|
|
122
|
-
reconnectOnError: reconnectOnError
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
/** @type Redis */
|
|
126
|
-
this.#redisClient = new Redis( options );
|
|
127
|
-
|
|
128
|
-
this.#redisClient.on( "ready", () => {
|
|
129
|
-
logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
130
|
-
|
|
131
|
-
// notify all connection observers about this event:
|
|
132
|
-
_.forEach( this.#connectionObservers, ( connectionObservers ) => {
|
|
133
|
-
connectionObservers.onConnectionRecovered( this.#clientIdentifier );
|
|
134
|
-
} );
|
|
135
|
-
|
|
136
|
-
// fetch the server information and store it:
|
|
137
|
-
this.#redisClient.info().then( ( result ) => {
|
|
138
|
-
this.#serverInfo = {};
|
|
139
|
-
if ( _.isString( result ) ) {
|
|
140
|
-
let rawData = _.split( result, "\r\n" );
|
|
141
|
-
_.forEach( rawData, ( entry ) => {
|
|
142
|
-
let details = _.split( entry, ":" );
|
|
143
|
-
if ( !_.startsWith( details[ 0 ], "#" ) && details[ 0 ] !== "" && details[ 0 ] ) {
|
|
144
|
-
if ( _.isNaN( _.toNumber( details[ 1 ] ) ) ) {
|
|
145
|
-
this.#serverInfo[ details[ 0 ] ] = details[ 1 ];
|
|
146
|
-
} else {
|
|
147
|
-
this.#serverInfo[ details[ 0 ] ] = _.toNumber( details[ 1 ] );
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} );
|
|
151
|
-
}
|
|
152
|
-
return this.#redisClient.module( "LIST" );
|
|
153
|
-
} ).then( ( result ) => {
|
|
154
|
-
this.#serverFeatures = {};
|
|
155
|
-
if ( _.isArray( result ) ) {
|
|
156
|
-
_.forEach( result, ( entry ) => {
|
|
157
|
-
this.#serverFeatures[ entry[ 1 ] ] = entry[ 3 ];
|
|
158
|
-
} );
|
|
159
|
-
}
|
|
160
|
-
} ).catch( ( error ) => {
|
|
161
|
-
logger.log( `Failed to fetch server information by client '${ this.identifier }'!`, logger.logSeverity.WARNING, error );
|
|
162
|
-
} );
|
|
163
|
-
} );
|
|
164
|
-
this.#redisClient.on( "error", ( error ) => {
|
|
165
|
-
logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
|
|
166
|
-
|
|
167
|
-
// notify all connection observers about this event:
|
|
168
|
-
_.forEach( this.#connectionObservers, ( connectionObservers ) => {
|
|
169
|
-
connectionObservers.onConnectionDisrupted( this.#clientIdentifier );
|
|
170
|
-
} );
|
|
171
|
-
} );
|
|
172
|
-
this.#redisClient.on( "reconnecting", ( info ) => {
|
|
173
|
-
if ( info.attempt > 1 ) {
|
|
174
|
-
logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ info.delay } ms. This is attempt ${ info.attempt }.`, logger.logSeverity.DEBUG );
|
|
175
|
-
}
|
|
176
|
-
} );
|
|
177
94
|
}
|
|
178
95
|
|
|
179
96
|
/* Public interface */
|
|
@@ -182,7 +99,7 @@ class RedisClient {
|
|
|
182
99
|
* Used to return the Redis client identifier.
|
|
183
100
|
*
|
|
184
101
|
* @property
|
|
185
|
-
* @
|
|
102
|
+
* @returns {string}
|
|
186
103
|
* @public
|
|
187
104
|
*/
|
|
188
105
|
get identifier() {
|
|
@@ -193,7 +110,7 @@ class RedisClient {
|
|
|
193
110
|
* Used to return the Redis server version.
|
|
194
111
|
*
|
|
195
112
|
* @property
|
|
196
|
-
* @
|
|
113
|
+
* @returns {number}
|
|
197
114
|
* @public
|
|
198
115
|
*/
|
|
199
116
|
get serverVersion() {
|
|
@@ -208,7 +125,94 @@ class RedisClient {
|
|
|
208
125
|
* @public
|
|
209
126
|
*/
|
|
210
127
|
get isJSONSupported() {
|
|
211
|
-
|
|
128
|
+
// Detect RedisJSON module variants (e.g., ReJSON, ReJSON2):
|
|
129
|
+
return !_.isNil( this.#serverFeatures[ "ReJSON" ] ) || !_.isNil( this.#serverFeatures[ "ReJSON2" ] );
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Used to initialize the Redis client.
|
|
134
|
+
*
|
|
135
|
+
* @method
|
|
136
|
+
* @param {string} host
|
|
137
|
+
* @param {number} port
|
|
138
|
+
* @param {string} authKey
|
|
139
|
+
* @param {string} user
|
|
140
|
+
* @param {number} defaultDB
|
|
141
|
+
* @param {number} [retryMaxIntervalMs=1000] Optional max backoff interval.
|
|
142
|
+
* @param {number|undefined} [retryMaxAttempts=undefined] Optional max (re)connection attempts before abort.
|
|
143
|
+
* @public
|
|
144
|
+
*/
|
|
145
|
+
initialize( host, port, authKey, user, defaultDB, retryMaxIntervalMs = 1000, retryMaxAttempts = undefined ) {
|
|
146
|
+
return new Promise( ( resolve, reject ) => {
|
|
147
|
+
if ( this.#redisClient ) {
|
|
148
|
+
resolve();
|
|
149
|
+
} else {
|
|
150
|
+
try {
|
|
151
|
+
this.#retryMaxInterval = retryMaxIntervalMs;
|
|
152
|
+
this.#retryMaxAttempts = retryMaxAttempts;
|
|
153
|
+
|
|
154
|
+
let retryStrategy = ( attempt ) => {
|
|
155
|
+
let result = Math.min( attempt * 50, this.#retryMaxInterval );
|
|
156
|
+
|
|
157
|
+
if ( this.#retryMaxAttempts != null && attempt > this.#retryMaxAttempts ) {
|
|
158
|
+
logger.log( "In Redis retry strategy: reached max attempts for command retry. Aborting...", logger.logSeverity.WARNING, { attempts: attempt } );
|
|
159
|
+
result = exceptions.raise( exceptions.exceptionCode.E_COM_RETRY_ATTEMPTS_EXCEEDED );
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return result;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let reconnectOnError = ( error ) => {
|
|
166
|
+
logger.log( `In Redis reconnect on error strategy: ${ error.message }`, logger.logSeverity.ERROR, error );
|
|
167
|
+
return !!error.message.includes( "READONLY" );
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let options = {
|
|
171
|
+
port: port,
|
|
172
|
+
host: host,
|
|
173
|
+
username: user,
|
|
174
|
+
password: authKey,
|
|
175
|
+
db: defaultDB,
|
|
176
|
+
autoResendUnfulfilledCommands: true,
|
|
177
|
+
maxRetriesPerRequest: null,
|
|
178
|
+
retryStrategy: retryStrategy,
|
|
179
|
+
reconnectOnError: reconnectOnError
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** @type Redis */
|
|
183
|
+
this.#redisClient = new Redis( options );
|
|
184
|
+
|
|
185
|
+
this.#redisClient.on( "ready", () => {
|
|
186
|
+
logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
|
|
187
|
+
|
|
188
|
+
// Notify all connection observers about this event:
|
|
189
|
+
_.forEach( this.#connectionObservers, ( connectionObservers ) => {
|
|
190
|
+
connectionObservers.onConnectionRecovered( this.#clientIdentifier );
|
|
191
|
+
} );
|
|
192
|
+
|
|
193
|
+
// Fetch the server information and store it:
|
|
194
|
+
this.#fetchServerInfo();
|
|
195
|
+
|
|
196
|
+
resolve();
|
|
197
|
+
} );
|
|
198
|
+
this.#redisClient.on( "error", ( error ) => {
|
|
199
|
+
logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
|
|
200
|
+
|
|
201
|
+
// Notify all connection observers about this event:
|
|
202
|
+
_.forEach( this.#connectionObservers, ( connectionObservers ) => {
|
|
203
|
+
connectionObservers.onConnectionDisrupted( this.#clientIdentifier );
|
|
204
|
+
} );
|
|
205
|
+
} );
|
|
206
|
+
this.#redisClient.on( "reconnecting", ( info ) => {
|
|
207
|
+
if ( info.attempt > 1 ) {
|
|
208
|
+
logger.log( `Client '${ this.identifier }' reconnecting to Redis server after ${ info.delay } ms. This is attempt ${ info.attempt }.`, logger.logSeverity.DEBUG );
|
|
209
|
+
}
|
|
210
|
+
} );
|
|
211
|
+
} catch ( error ) {
|
|
212
|
+
reject( exceptions.raise( error ) );
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} );
|
|
212
216
|
}
|
|
213
217
|
|
|
214
218
|
/**
|
|
@@ -231,7 +235,7 @@ class RedisClient {
|
|
|
231
235
|
*
|
|
232
236
|
* @method
|
|
233
237
|
* @param {Array[]} commands
|
|
234
|
-
* @
|
|
238
|
+
* @returns {Promise<*>}
|
|
235
239
|
* @public
|
|
236
240
|
*/
|
|
237
241
|
executeCommands( commands ) {
|
|
@@ -252,7 +256,7 @@ class RedisClient {
|
|
|
252
256
|
* @method
|
|
253
257
|
* @param {string} command
|
|
254
258
|
* @param {Array} commandArguments
|
|
255
|
-
* @
|
|
259
|
+
* @returns {Promise<*>}
|
|
256
260
|
* @public
|
|
257
261
|
*/
|
|
258
262
|
blockingCommand( command, commandArguments ) {
|
|
@@ -271,7 +275,7 @@ class RedisClient {
|
|
|
271
275
|
* @method
|
|
272
276
|
* @param {string} channel
|
|
273
277
|
* @param {(Object|string)} message
|
|
274
|
-
* @
|
|
278
|
+
* @returns {Promise<number>}
|
|
275
279
|
* @public
|
|
276
280
|
*/
|
|
277
281
|
publishCommand( channel, message ) {
|
|
@@ -286,25 +290,69 @@ class RedisClient {
|
|
|
286
290
|
|
|
287
291
|
/**
|
|
288
292
|
* Used to subscribe to the specified channel for messages.
|
|
293
|
+
* <br/>
|
|
294
|
+
* NOTE: Call unsubscribeCommand(channel) to detach later.
|
|
289
295
|
*
|
|
290
296
|
* @method
|
|
291
|
-
* @param {string} channel
|
|
297
|
+
* @param {string} channel Unique identifier of the channel to subscribe to.
|
|
292
298
|
* @param {function( Object )} messageHandler Will execute this handler every time a new message is received.
|
|
293
|
-
* @
|
|
299
|
+
* @returns {Promise}
|
|
294
300
|
* @public
|
|
295
301
|
*/
|
|
296
302
|
subscribeCommand( channel, messageHandler ) {
|
|
297
303
|
return new Promise( ( resolve, reject ) => {
|
|
298
|
-
|
|
299
|
-
|
|
304
|
+
// Avoid attaching multiple listeners for the same channel:
|
|
305
|
+
if ( this.#messageHandlersByChannel.has( channel ) ) {
|
|
306
|
+
logger.log( "Attempting to subscribe to message channel '" + channel + "' while already subscribed to it.", logger.logSeverity.WARNING );
|
|
300
307
|
resolve();
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
+
} else {
|
|
309
|
+
const onMessage = ( subscribedChannel, message ) => {
|
|
310
|
+
if ( subscribedChannel === channel && typeof messageHandler === "function" ) {
|
|
311
|
+
messageHandler( tools.parseJSON( message ) );
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
this.#redisClient.once( "subscribe", ( subscribedChannel ) => {
|
|
316
|
+
if ( subscribedChannel === channel ) {
|
|
317
|
+
logger.log( "Subscription to message channel '" + channel + "' successful.", logger.logSeverity.DEBUG );
|
|
318
|
+
resolve();
|
|
319
|
+
}
|
|
320
|
+
} );
|
|
321
|
+
|
|
322
|
+
this.#redisClient.on( "message", onMessage );
|
|
323
|
+
this.#messageHandlersByChannel.set( channel, onMessage );
|
|
324
|
+
|
|
325
|
+
this.#redisClient.subscribe( channel ).catch( ( error ) => {
|
|
326
|
+
// Cleanup partial state if subscribe fails:
|
|
327
|
+
const handler = this.#messageHandlersByChannel.get( channel );
|
|
328
|
+
if ( handler ) {
|
|
329
|
+
this.#redisClient.off( "message", handler );
|
|
330
|
+
this.#messageHandlersByChannel.delete( channel );
|
|
331
|
+
}
|
|
332
|
+
reject( exceptions.raise( error ) );
|
|
333
|
+
} );
|
|
334
|
+
}
|
|
335
|
+
} );
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Used to unsubscribe from a channel and remove its message handler.
|
|
340
|
+
*
|
|
341
|
+
* @method
|
|
342
|
+
* @param {string} channel Unique identifier of the channel to unsubscribe from.
|
|
343
|
+
* @returns {Promise}
|
|
344
|
+
* @public
|
|
345
|
+
*/
|
|
346
|
+
unsubscribeCommand( channel ) {
|
|
347
|
+
return new Promise( ( resolve, reject ) => {
|
|
348
|
+
const handler = this.#messageHandlersByChannel.get( channel );
|
|
349
|
+
if ( handler ) {
|
|
350
|
+
this.#redisClient.off( "message", handler );
|
|
351
|
+
this.#messageHandlersByChannel.delete( channel );
|
|
352
|
+
}
|
|
353
|
+
this.#redisClient.unsubscribe( channel ).then( () => {
|
|
354
|
+
resolve();
|
|
355
|
+
} ).catch( ( error ) => {
|
|
308
356
|
reject( exceptions.raise( error ) );
|
|
309
357
|
} );
|
|
310
358
|
} );
|
|
@@ -332,6 +380,84 @@ class RedisClient {
|
|
|
332
380
|
} );
|
|
333
381
|
}
|
|
334
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Used to gracefully close the Redis connection.
|
|
385
|
+
* Attempts to quit(), then falls back to disconnect() on timeout.
|
|
386
|
+
*
|
|
387
|
+
* @method
|
|
388
|
+
* @param {number} [timeoutMs=1000]
|
|
389
|
+
* @returns {Promise<Object>}
|
|
390
|
+
* @public
|
|
391
|
+
*/
|
|
392
|
+
shutDown( timeoutMs = 1000 ) {
|
|
393
|
+
return new Promise( ( resolve ) => {
|
|
394
|
+
let finished = false;
|
|
395
|
+
const done = () => {
|
|
396
|
+
if ( !finished ) {
|
|
397
|
+
finished = true;
|
|
398
|
+
resolve();
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const timeout = setTimeout( () => {
|
|
403
|
+
try {
|
|
404
|
+
this.#redisClient.disconnect();
|
|
405
|
+
} catch ( error ) {
|
|
406
|
+
}
|
|
407
|
+
done();
|
|
408
|
+
}, timeoutMs );
|
|
409
|
+
|
|
410
|
+
this.#redisClient.quit().then( () => {
|
|
411
|
+
clearTimeout( timeout );
|
|
412
|
+
done();
|
|
413
|
+
} ).catch( () => {
|
|
414
|
+
clearTimeout( timeout );
|
|
415
|
+
try {
|
|
416
|
+
this.#redisClient.disconnect();
|
|
417
|
+
} catch ( error ) {
|
|
418
|
+
}
|
|
419
|
+
done();
|
|
420
|
+
} );
|
|
421
|
+
} );
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/* Private interface */
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Used to fetch and store Redis server information.
|
|
428
|
+
*
|
|
429
|
+
* @method
|
|
430
|
+
* @private
|
|
431
|
+
*/
|
|
432
|
+
#fetchServerInfo() {
|
|
433
|
+
this.#redisClient.info().then( ( result ) => {
|
|
434
|
+
this.#serverInfo = {};
|
|
435
|
+
if ( _.isString( result ) ) {
|
|
436
|
+
let rawData = _.split( result, "\r\n" );
|
|
437
|
+
_.forEach( rawData, ( entry ) => {
|
|
438
|
+
let details = _.split( entry, ":" );
|
|
439
|
+
if ( !_.startsWith( details[ 0 ], "#" ) && details[ 0 ] !== "" && details[ 0 ] ) {
|
|
440
|
+
if ( _.isNaN( _.toNumber( details[ 1 ] ) ) ) {
|
|
441
|
+
this.#serverInfo[ details[ 0 ] ] = details[ 1 ];
|
|
442
|
+
} else {
|
|
443
|
+
this.#serverInfo[ details[ 0 ] ] = _.toNumber( details[ 1 ] );
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} );
|
|
447
|
+
}
|
|
448
|
+
return this.#redisClient.module( "LIST" );
|
|
449
|
+
} ).then( ( result ) => {
|
|
450
|
+
this.#serverFeatures = {};
|
|
451
|
+
if ( _.isArray( result ) ) {
|
|
452
|
+
_.forEach( result, ( entry ) => {
|
|
453
|
+
this.#serverFeatures[ entry[ 1 ] ] = entry[ 3 ];
|
|
454
|
+
} );
|
|
455
|
+
}
|
|
456
|
+
} ).catch( ( error ) => {
|
|
457
|
+
logger.log( `Failed to fetch server information by client '${ this.identifier }'!`, logger.logSeverity.WARNING, error );
|
|
458
|
+
} );
|
|
459
|
+
}
|
|
460
|
+
|
|
335
461
|
}
|
|
336
462
|
|
|
337
463
|
/**
|
|
@@ -339,16 +465,9 @@ class RedisClient {
|
|
|
339
465
|
*
|
|
340
466
|
* @method
|
|
341
467
|
* @param {string} identifier
|
|
342
|
-
* @param {string} host
|
|
343
|
-
* @param {number} [port=6379]
|
|
344
|
-
* @param {string} [authKey=undefined]
|
|
345
|
-
* @param {string} [user="default"]
|
|
346
|
-
* @param {number} [defaultDB=0]
|
|
347
|
-
* @param {boolean} [autoRetryUnfulfilled=true]
|
|
348
|
-
* @param {number} [maxRetries=20]
|
|
349
468
|
* @return {RedisClient}
|
|
350
469
|
* @public
|
|
351
470
|
*/
|
|
352
|
-
module.exports.createRedisClient = ( identifier
|
|
353
|
-
return Object.freeze( new RedisClient( identifier
|
|
354
|
-
};
|
|
471
|
+
module.exports.createRedisClient = ( identifier ) => {
|
|
472
|
+
return Object.freeze( new RedisClient( identifier ) );
|
|
473
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ti-engine/core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
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
5
|
"author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -44,15 +44,15 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"blake2": "^5.0.0",
|
|
47
|
-
"dotenv": "^
|
|
48
|
-
"fs-extra": "^11.
|
|
47
|
+
"dotenv": "^17.2.1",
|
|
48
|
+
"fs-extra": "^11.3.1",
|
|
49
49
|
"lodash": "^4.17.21",
|
|
50
50
|
"node-schedule": "^2.1.1",
|
|
51
|
-
"ioredis": "^5.
|
|
51
|
+
"ioredis": "^5.7.0"
|
|
52
52
|
},
|
|
53
53
|
"optionalDependencies": {
|
|
54
54
|
"@google-cloud/error-reporting": "^3.0.5",
|
|
55
|
-
"zeromq": "^6.
|
|
55
|
+
"zeromq": "^6.5.0"
|
|
56
56
|
},
|
|
57
57
|
"repository": {
|
|
58
58
|
"type": "git",
|
|
@@ -61,4 +61,4 @@
|
|
|
61
61
|
"engines": {
|
|
62
62
|
"node": ">=14.17.0"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|
package/testRedis.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Redis from "ioredis";
|
|
2
|
+
|
|
3
|
+
const redisClient = new Redis( {
|
|
4
|
+
port: 17467,
|
|
5
|
+
host: "redis-17467.c300.eu-central-1-1.ec2.redns.redis-cloud.com",
|
|
6
|
+
username: "default",
|
|
7
|
+
password: "o49Iyj2JYnmD7NSFWa32x2iYqulWkIM1",
|
|
8
|
+
db: 0,
|
|
9
|
+
autoResendUnfulfilledCommands: true,
|
|
10
|
+
maxRetriesPerRequest: null,
|
|
11
|
+
retryStrategy: ( attempt ) => {
|
|
12
|
+
let result = Math.min( attempt * 50, 1000 );
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
redisClient.on('error', err => console.log('Redis Client Error', err));
|
|
17
|
+
|
|
18
|
+
//await redisClient.connect();
|
|
19
|
+
|
|
20
|
+
await redisClient.set('foo', 'bar');
|
|
21
|
+
const result = await redisClient.get('foo');
|
|
22
|
+
console.log(result);
|