@ti-engine/core 1.7.1 → 1.7.2

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +383 -377
  2. package/LICENSE.md +321 -321
  3. package/README.md +597 -597
  4. package/bin/localization/labels.json +122 -122
  5. package/bin/settings.json +41 -41
  6. package/bin/start-instance.js +164 -164
  7. package/components/auditing.js +191 -191
  8. package/components/connection-observer.js +72 -72
  9. package/components/definitions.types.js +248 -248
  10. package/components/exchange/default/default-message-exchange.js +136 -136
  11. package/components/exchange/default/default-message-receiver.js +101 -101
  12. package/components/exchange/default/default-message-sender.js +100 -100
  13. package/components/exchange/message-dispatcher.js +168 -168
  14. package/components/exchange/message-exchange.js +449 -449
  15. package/components/exchange/message-handler.js +235 -235
  16. package/components/exchange/message-memory-cache.js +190 -190
  17. package/components/exchange/message-observer.js +126 -126
  18. package/components/exchange/message-receiver.js +181 -181
  19. package/components/exchange/message-sender.js +143 -143
  20. package/components/exchange/message-tracer.js +212 -212
  21. package/components/service-caller.js +370 -370
  22. package/components/service-consumer.js +131 -131
  23. package/components/service-executor.js +278 -278
  24. package/components/service-instance.js +316 -316
  25. package/components/service-provider.js +251 -251
  26. package/integrations/redis-integration.js +591 -591
  27. package/package.json +89 -89
  28. package/utils/cache.js +772 -772
  29. package/utils/config.js +103 -103
  30. package/utils/exceptions.js +368 -368
  31. package/utils/localization.js +298 -298
  32. package/utils/logger.js +82 -82
  33. package/utils/tools.js +632 -632
package/utils/cache.js CHANGED
@@ -1,773 +1,773 @@
1
- /*
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-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
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
- * 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
- * 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
- */
8
-
9
- const ConnectionObserver = require( "#connection-observer" );
10
- const _ = require( "lodash" );
11
- const config = require( "#config" );
12
- const tools = require( "#tools" );
13
- const redis = require( "#redis-integration" );
14
- const exceptions = require( "#exceptions" );
15
-
16
- /**
17
- * Used to create and/or return a Common Memory Cache singleton instance.
18
- *
19
- * @class CommonMemoryCache
20
- * @extends ConnectionObserver
21
- * @singleton
22
- * @public
23
- */
24
- class CommonMemoryCache extends ConnectionObserver {
25
-
26
- static #instance = null;
27
- #redisClient = null;
28
- #isOperational = false;
29
- #connectionIdentifier = "system-cache";
30
-
31
- /**
32
- * @constructor
33
- * @return {CommonMemoryCache}
34
- */
35
- constructor() {
36
- super();
37
-
38
- if ( !CommonMemoryCache.#instance ) {
39
- this.#redisClient = redis.createRedisClient( this.#connectionIdentifier );
40
- this.#redisClient.addConnectionObserver( this );
41
-
42
- CommonMemoryCache.#instance = this;
43
- }
44
- return CommonMemoryCache.#instance;
45
- }
46
-
47
- /* Public interface */
48
-
49
- /**
50
- * Property returning the operational state of the cache.
51
- *
52
- * @property
53
- * @returns {boolean}
54
- * @public
55
- */
56
- get isOperational() {
57
- return this.#isOperational;
58
- }
59
-
60
- /**
61
- * Property returning the connection identifier of the cache service.
62
- *
63
- * @property
64
- * @returns {string}
65
- * @public
66
- */
67
- get connectionIdentifier() {
68
- return this.#connectionIdentifier;
69
- }
70
-
71
- /**
72
- * Used to initialize the cache service.
73
- *
74
- * @method
75
- * @returns {Promise}
76
- * @public
77
- */
78
- initialize() {
79
- let host = config.getSetting( config.setting.MEMORY_CACHE_REDIS_HOST );
80
- let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
81
- let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
82
- let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
83
- let user = config.getSetting( config.setting.MEMORY_CACHE_USER );
84
-
85
- return this.#redisClient.initialize( host, port, authKey, user, db );
86
- }
87
-
88
- /**
89
- * Used to gracefully shut down the cache service.
90
- *
91
- * @method
92
- * @return {Promise}
93
- * @public
94
- */
95
- shutDown() {
96
- return this.#redisClient.shutDown( 250 );
97
- }
98
-
99
- /**
100
- * Needs to be invoked by the connection handler when the connection is disrupted.
101
- *
102
- * @method
103
- * @param {string} identifier The identifier of the observed connection.
104
- * @override
105
- * @public
106
- */
107
- onConnectionDisrupted( identifier ) {
108
- if ( identifier === this.#connectionIdentifier ) {
109
- this.#isOperational = false;
110
- }
111
- }
112
-
113
- /**
114
- * Needs to be invoked by the connection handler when the connection is recovered.
115
- *
116
- * @method
117
- * @param {string} identifier The identifier of the observed connection.
118
- * @override
119
- * @public
120
- */
121
- onConnectionRecovered( identifier ) {
122
- if ( identifier === this.#connectionIdentifier ) {
123
- this.#isOperational = true;
124
- }
125
- }
126
-
127
- /**
128
- * Needs to be invoked by the connection handler when the connection is irrevocably lost.
129
- *
130
- * @method
131
- * @param {string} identifier The identifier of the observed connection.
132
- * @throws {TiException.E_GEN_SYSTEM_CACHE_UNAVAILABLE} If the cache service is no longer available.
133
- * @override
134
- * @public
135
- */
136
- onConnectionLost( identifier ) {
137
- if ( identifier === this.#connectionIdentifier ) {
138
- this.#isOperational = false;
139
- // TODO: implement forced shut down of the service instance instead of crashing it outright
140
- throw exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE );
141
- }
142
- }
143
-
144
- /**
145
- * Used to register a new {@link ConnectionObserver} for events related to the underlying Redis connection state.
146
- *
147
- * @method
148
- * @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
149
- * @public
150
- */
151
- addConnectionObserver( connectionObserver ) {
152
- this.#redisClient.addConnectionObserver( connectionObserver );
153
- }
154
-
155
- /**
156
- * Used to search for keys by a given pattern.
157
- *
158
- * @method
159
- * @param {string} pattern
160
- * @returns {Promise<Array>}
161
- * @public
162
- */
163
- matchKeys( pattern ) {
164
- return new Promise( ( resolve, reject ) => {
165
- if ( this.#isOperational === true ) {
166
- let commandKeys = [ redis.cacheCommands.KEYS, pattern ];
167
- this.#redisClient.executeCommands( [ commandKeys ] ).then( ( results ) => {
168
- results = results[ 0 ];
169
- resolve( ( results && results.length > 1 ) ? results[ 1 ] : [] );
170
- } ).catch( ( error ) => {
171
- reject( error );
172
- } );
173
- } else {
174
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
175
- }
176
- } );
177
- }
178
-
179
- /**
180
- * Used to set a specific string value.
181
- *
182
- * @method
183
- * @param {string} key
184
- * @param {string} value
185
- * @param {number} [expiration] Expiration value is in seconds.
186
- * @return {Promise<string>}
187
- * @public
188
- */
189
- setValue( key, value, expiration ) {
190
- return new Promise( ( resolve, reject ) => {
191
- if ( this.#isOperational === true ) {
192
- if ( value ) {
193
- let commandSetValue = [ redis.cacheCommands.SET_VALUE, key, tools.stringifyJSON( value ) ];
194
- if ( expiration ) {
195
- commandSetValue.push( "EX" );
196
- commandSetValue.push( expiration );
197
- }
198
- this.#redisClient.executeCommands( [ commandSetValue ] ).then( () => {
199
- resolve( value );
200
- } ).catch( ( error ) => {
201
- reject( error );
202
- } );
203
- } else {
204
- resolve( value );
205
- }
206
- } else {
207
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
208
- }
209
- } );
210
- }
211
-
212
- /**
213
- * Used to set multiple string values.
214
- *
215
- * @method
216
- * @param {Object} keyValues
217
- * @param {string} [prefix]
218
- * @param {number} [expiration]
219
- * @return {Promise}
220
- * @public
221
- */
222
- setValues( keyValues, prefix, expiration ) {
223
- return new Promise( ( resolve, reject ) => {
224
- if ( this.#isOperational === true ) {
225
- if ( keyValues ) {
226
- let commands = [];
227
- _.forEach( keyValues, ( value, key ) => {
228
- let commandSetValue = [ redis.cacheCommands.SET_VALUE, ( ( prefix ) ? prefix : "" ) + key, tools.stringifyJSON( value ) ];
229
- if ( expiration ) {
230
- commandSetValue.push( "EX" );
231
- commandSetValue.push( expiration );
232
- }
233
- commands.push( commandSetValue );
234
- } );
235
-
236
- this.#redisClient.executeCommands( commands ).then( () => {
237
- resolve( keyValues );
238
- } ).catch( ( error ) => {
239
- reject( error );
240
- } );
241
- } else {
242
- resolve( keyValues );
243
- }
244
- } else {
245
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
246
- }
247
- } );
248
- }
249
-
250
- /**
251
- * Used to get a string value.
252
- *
253
- * @method
254
- * @param {string} key
255
- * @return {Promise}
256
- * @public
257
- */
258
- getValue( key ) {
259
- return new Promise( ( resolve, reject ) => {
260
- if ( this.#isOperational === true ) {
261
- let commandGetValue = [ redis.cacheCommands.GET_VALUE, key ];
262
- this.#redisClient.executeCommands( [ commandGetValue ] ).then( ( results ) => {
263
- results = results[ 0 ];
264
- resolve( ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : undefined );
265
- } ).catch( ( error ) => {
266
- reject( error );
267
- } );
268
- } else {
269
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
270
- }
271
- } );
272
- }
273
-
274
- /**
275
- * Used to get multiple string values.
276
- *
277
- * @method
278
- * @param {string[]} keys
279
- * @param {string} [prefix]
280
- * @return {Promise}
281
- * @public
282
- */
283
- getValues( keys, prefix ) {
284
- return new Promise( ( resolve, reject ) => {
285
- if ( this.#isOperational === true ) {
286
- let commands = [];
287
- _.forEach( keys, ( key ) => {
288
- commands.push( [ redis.cacheCommands.GET_VALUE, ( ( prefix ) ? prefix : "" ) + key ] );
289
- } );
290
- this.#redisClient.executeCommands( commands ).then( ( rawResults ) => {
291
- let results = {};
292
- _.forEach( rawResults, ( result, idx ) => {
293
- results[ keys[ idx ] ] = ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : null;
294
- } );
295
- resolve( results );
296
- } ).catch( ( error ) => {
297
- reject( error );
298
- } );
299
- } else {
300
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
301
- }
302
- } );
303
- }
304
-
305
- /**
306
- * Used to delete a value / item.
307
- *
308
- * @method
309
- * @param {string} key
310
- * @returns {Promise<boolean>}
311
- * @public
312
- */
313
- deleteValue( key ) {
314
- return new Promise( ( resolve, reject ) => {
315
- if ( this.#isOperational === true ) {
316
- let commandDeleteValue = [ redis.cacheCommands.DELETE_VALUE, key ];
317
- this.#redisClient.executeCommands( [ commandDeleteValue ] ).then( ( results ) => {
318
- results = results[ 0 ];
319
- resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
320
- } ).catch( ( error ) => {
321
- reject( error );
322
- } );
323
- } else {
324
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
325
- }
326
- } );
327
- }
328
-
329
- /**
330
- * Used to set expiration in seconds to an existing key.
331
- * <br/>
332
- * NOTE: For performance optimization reasons, only use this only if the Redis command does not itself support the 'EX' argument.
333
- *
334
- * @method
335
- * @param {string} key
336
- * @param {number} seconds
337
- * @param {string} [name] If you need to expire a field in a hash set instead, provide the name of the set here.
338
- * @returns {Promise<number>} This will resolve with the seconds as provided initially by the caller.
339
- * @public
340
- */
341
- expireValue( key, seconds, name ) {
342
- return new Promise( ( resolve, reject ) => {
343
- if ( this.#isOperational === true ) {
344
- let commandExpire = ( name ) ? [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ] : [ redis.cacheCommands.EXPIRE, key, seconds ];
345
- this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
346
- resolve( seconds );
347
- } ).catch( ( error ) => {
348
- reject( error );
349
- } );
350
- } else {
351
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
352
- }
353
- } );
354
- }
355
-
356
- /**
357
- * Used to add the specified values to a list.
358
- *
359
- * @method
360
- * @param {string} listName
361
- * @param {Object[]} values
362
- * @returns {Promise<number>}
363
- * @public
364
- */
365
- listPushValue( listName, values ) {
366
- return new Promise( ( resolve, reject ) => {
367
- if ( this.#isOperational === true ) {
368
- let commandPushValues = [ redis.cacheCommands.LIST_PUSH, listName ];
369
- _.forEach( values, ( value ) => {
370
- if ( value ) {
371
- commandPushValues.push( tools.stringifyJSON( value ) );
372
- }
373
- } );
374
- this.#redisClient.executeCommands( [ commandPushValues ] ).then( ( results ) => {
375
- results = results[ 0 ];
376
- resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
377
- } ).catch( ( error ) => {
378
- reject( error );
379
- } );
380
- } else {
381
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
382
- }
383
- } );
384
- }
385
-
386
- /**
387
- * Used to add the specified value to a set.
388
- *
389
- * @method
390
- * @param {string} key
391
- * @param {string|Object} value
392
- * @returns {Promise}
393
- * @public
394
- */
395
- addToSet( key, value ) {
396
- return new Promise( ( resolve, reject ) => {
397
- if ( this.#isOperational === true ) {
398
- let commandAddToSet = [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( value ) ];
399
- this.#redisClient.executeCommands( [ commandAddToSet ] ).then( () => {
400
- resolve();
401
- } ).catch( ( error ) => {
402
- reject( error );
403
- } );
404
- } else {
405
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
406
- }
407
- } );
408
- }
409
-
410
- /**
411
- * Used to add multiple values to multiple sets in one transactional request.
412
- * <br/>
413
- * NOTE: The two arrays of keys and values must have correct index relations (i.e., first pair on keys[0] and values[0] and so on)!
414
- *
415
- * @method
416
- * @param {string[]} keys
417
- * @param {string[]} values
418
- * @returns {Promise}
419
- * @public
420
- */
421
- addToSetMulti( keys, values ) {
422
- return new Promise( ( resolve, reject ) => {
423
- if ( this.#isOperational === true ) {
424
- let commands = [];
425
- _.forEach( keys, ( key, idx ) => {
426
- commands.push( [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( values[ idx ] ) ] );
427
- } );
428
- this.#redisClient.executeCommands( commands ).then( () => {
429
- resolve();
430
- } ).catch( ( error ) => {
431
- reject( error );
432
- } );
433
- } else {
434
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
435
- }
436
- } );
437
- }
438
-
439
- /**
440
- * Used to check if the provided value is a member of the specified set.
441
- *
442
- * @method
443
- * @param {string} setName
444
- * @param {string} value
445
- * @returns {Promise<boolean>}
446
- * @public
447
- */
448
- isSetMember( setName, value ) {
449
- return new Promise( ( resolve, reject ) => {
450
- if ( this.#isOperational === true ) {
451
- let commandIsSetMember = [ redis.cacheCommands.IS_SET_MEMBER, setName, value ];
452
- this.#redisClient.executeCommands( [ commandIsSetMember ] ).then( ( results ) => {
453
- results = results[ 0 ];
454
- let result = !!( results && results.length > 1 && results[ 1 ] === 1 );
455
- resolve( result );
456
- } ).catch( ( error ) => {
457
- reject( error );
458
- } );
459
- } else {
460
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
461
- }
462
- } );
463
- }
464
-
465
- /**
466
- * Used to get all elements of a set.
467
- *
468
- * @method
469
- * @param {string} key
470
- * @returns {Promise<Object[]>}
471
- * @public
472
- */
473
- membersOfSet( key ) {
474
- return new Promise( ( resolve, reject ) => {
475
- if ( this.#isOperational === true ) {
476
- let commandMembersOfSet = [ redis.cacheCommands.GET_ALL_FROM_SET, key ];
477
- this.#redisClient.executeCommands( [ commandMembersOfSet ] ).then( ( results ) => {
478
- results = results[ 0 ];
479
- let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
480
- resolve( parsedResults );
481
- } ).catch( ( error ) => {
482
- reject( error );
483
- } );
484
- } else {
485
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
486
- }
487
- } );
488
- }
489
-
490
- /**
491
- * Used to get a union of all elements in the list of sets.
492
- *
493
- * @method
494
- * @param {string[]} keys
495
- * @returns {Promise<Object[]>}
496
- * @public
497
- */
498
- unionOfSets( keys ) {
499
- return new Promise( ( resolve, reject ) => {
500
- if ( this.#isOperational === true ) {
501
- let commandUnionOfSets = _.concat( [ redis.cacheCommands.UNION_OF_SETS ], keys );
502
- this.#redisClient.executeCommands( [ commandUnionOfSets ] ).then( ( results ) => {
503
- results = results[ 0 ];
504
- let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
505
- resolve( parsedResults );
506
- } ).catch( ( error ) => {
507
- reject( error );
508
- } );
509
- } else {
510
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
511
- }
512
- } );
513
- }
514
-
515
- /**
516
- * Used to set a single hash field.
517
- *
518
- * @method
519
- * @deprecated
520
- * @param {string} key
521
- * @param {string} name
522
- * @param {*} value
523
- * @returns {Promise}
524
- * @public
525
- */
526
- hashSetField( key, name, value ) {
527
- return new Promise( ( resolve, reject ) => {
528
- if ( this.#isOperational === true ) {
529
- let commandHashSetField = [ redis.cacheCommands.HASH_SET, key, name, tools.stringifyJSON( value ) ];
530
- this.#redisClient.executeCommands( [ commandHashSetField ] ).then( () => {
531
- resolve();
532
- } ).catch( ( error ) => {
533
- reject( error );
534
- } );
535
- } else {
536
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
537
- }
538
- } );
539
- }
540
-
541
- /**
542
- * Used to set multiple hash fields.
543
- *
544
- * @method
545
- * @deprecated
546
- * @param {string} key
547
- * @param {Object[]} fields
548
- * @param {string} fields[].name
549
- * @param {*} fields[].value
550
- * @returns {Promise}
551
- * @public
552
- */
553
- hashSetFields( key, fields ) {
554
- return new Promise( ( resolve, reject ) => {
555
- if ( this.#isOperational === true ) {
556
- let commandHashSetFields = [ redis.cacheCommands.HASH_SET, key ];
557
- _.forEach( fields, ( field ) => {
558
- commandHashSetFields.push( field.name );
559
- commandHashSetFields.push( tools.stringifyJSON( field.value ) );
560
- } );
561
- this.#redisClient.executeCommands( [ commandHashSetFields ] ).then( () => {
562
- resolve();
563
- } ).catch( ( error ) => {
564
- reject( error );
565
- } );
566
- } else {
567
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
568
- }
569
- } );
570
- }
571
-
572
- /**
573
- * Used to get a single field from a hash.
574
- *
575
- * @method
576
- * @param {string} key
577
- * @param {string} field
578
- * @return {Promise}
579
- * @public
580
- */
581
- hashGetField( key, field ) {
582
- return new Promise( ( resolve, reject ) => {
583
- if ( this.#isOperational === true ) {
584
- let commandHashGetField = [ redis.cacheCommands.HASH_GET, key, field ];
585
- this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
586
- results = results[ 0 ];
587
- resolve( ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : null );
588
- } ).catch( ( error ) => {
589
- reject( error );
590
- } );
591
- } else {
592
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
593
- }
594
- } );
595
- }
596
-
597
- /**
598
- * Used to remove a single field from a hash.
599
- *
600
- * @method
601
- * @param {string} key
602
- * @param {string} field
603
- * @return {Promise<boolean>} Will return 'true' if the field was removed, 'false' otherwise.
604
- * @public
605
- */
606
- hashDeleteField( key, field ) {
607
- return new Promise( ( resolve, reject ) => {
608
- if ( this.#isOperational === true ) {
609
- let commandHashGetField = [ redis.cacheCommands.HASH_REMOVE, key, field ];
610
- this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
611
- results = results[ 0 ];
612
- resolve( ( results && results.length > 1 ) ? tools.toBool( results[ 1 ] ) : false );
613
- } ).catch( ( error ) => {
614
- reject( error );
615
- } );
616
- } else {
617
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
618
- }
619
- } );
620
- }
621
-
622
- /**
623
- * Used to store a JSON variable.
624
- * <br/>
625
- * NOTE: Requires ReJSON module installed on server to work.
626
- *
627
- * @method
628
- * @param {string} key
629
- * @param {Object} value
630
- * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
631
- * @param {number} [overrideMode=0] By default this allows full override for existing keys.
632
- * Option 1 will set the key only if it doesn't already exist. Option 2 will set it only if it already exists.
633
- * @returns {Promise}
634
- * @public
635
- */
636
- setJSON( key, value, path = "$", overrideMode = 0 ) {
637
- return new Promise( ( resolve, reject ) => {
638
- if ( this.#isOperational === true ) {
639
- if ( this.#redisClient.isJSONSupported ) {
640
- let commandArguments = [ redis.cacheCommands.JSON_SET, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
641
- if ( overrideMode !== 0 ) {
642
- commandArguments.push( overrideMode === 1 ? redis.cacheOverrideMode.NX : redis.cacheOverrideMode.XX );
643
- }
644
- this.#redisClient.callCommand( commandArguments ).then( () => {
645
- resolve();
646
- } ).catch( ( error ) => {
647
- reject( error );
648
- } );
649
- } else {
650
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
651
- }
652
- } else {
653
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
654
- }
655
- } );
656
- }
657
-
658
- /**
659
- * Used to fetch a JSON variable.
660
- * <br/>
661
- * NOTE: Requires ReJSON module installed on server to work.
662
- *
663
- * @method
664
- * @param {string} key
665
- * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
666
- * @returns {Promise<Object>}
667
- * @public
668
- */
669
- getJSON( key, path = "$" ) {
670
- return new Promise( ( resolve, reject ) => {
671
- if ( this.#isOperational === true ) {
672
- if ( this.#redisClient.isJSONSupported ) {
673
- let commandArguments = [ redis.cacheCommands.JSON_GET, key, this.#normalizeJSONPath( path ) ];
674
- this.#redisClient.callCommand( commandArguments ).then( ( result ) => {
675
- resolve( result != null ? tools.parseJSON( String( result ) ) : null );
676
- } ).catch( ( error ) => {
677
- reject( error );
678
- } );
679
- } else {
680
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
681
- }
682
- } else {
683
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
684
- }
685
- } );
686
- }
687
-
688
- /**
689
- * Used to update/edit an existing JSON variable.
690
- * <br/>
691
- * NOTE: Requires ReJSON module installed on server to work.
692
- *
693
- * @method
694
- * @param {string} key
695
- * @param {Object} value
696
- * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
697
- * @returns {Promise}
698
- * @public
699
- */
700
- editJSON( key, value, path = "$" ) {
701
- return new Promise( ( resolve, reject ) => {
702
- if ( this.#isOperational === true ) {
703
- if ( this.#redisClient.isJSONSupported ) {
704
- let commandArguments = [ redis.cacheCommands.JSON_MERGE, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
705
- this.#redisClient.callCommand( commandArguments ).then( () => {
706
- resolve();
707
- } ).catch( ( error ) => {
708
- reject( error );
709
- } );
710
- } else {
711
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
712
- }
713
- } else {
714
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
715
- }
716
- } );
717
- }
718
-
719
- /**
720
- * Used to add an item to a JSON array. That array needs to exist already.
721
- * <br/>
722
- * NOTE: Requires ReJSON module installed on server to work.
723
- *
724
- * @method
725
- * @param {string} key
726
- * @param {Object} value
727
- * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
728
- * @returns {Promise}
729
- * @public
730
- */
731
- arrayAppendJSON( key, value, path = "$" ) {
732
- return new Promise( ( resolve, reject ) => {
733
- if ( this.#isOperational === true ) {
734
- if ( this.#redisClient.isJSONSupported ) {
735
- let commandArguments = [ redis.cacheCommands.JSON_ARRAY_APPEND, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
736
- this.#redisClient.callCommand( commandArguments ).then( () => {
737
- resolve();
738
- } ).catch( ( error ) => {
739
- reject( error );
740
- } );
741
- } else {
742
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
743
- }
744
- } else {
745
- reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
746
- }
747
- } );
748
- }
749
-
750
- /* Private interface */
751
-
752
- /**
753
- * Used to normalize a JSON path.
754
- * <br/>
755
- * NOTE: If "path" is an array, each element is treated as a literal key name and encoded with bracket notation,
756
- * which correctly handles key names that contain dots or other JSONPath special characters.
757
- *
758
- * @method
759
- * @param {string|string[]} path
760
- * @returns {string}
761
- * @private
762
- */
763
- #normalizeJSONPath( path ) {
764
- if ( Array.isArray( path ) ) {
765
- return "$" + path.map( ( segment ) => `["${ String( segment ).replace( /\\/g, "\\\\" ).replace( /"/g, '\\"' ) }"]` ).join( "" );
766
- }
767
- return ( path.startsWith( "$" ) === false ) ? ( "$." + path ) : path;
768
- }
769
-
770
- }
771
-
772
- const instance = new CommonMemoryCache();
1
+ /*
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-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
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
+ * 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
+ * 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
+ */
8
+
9
+ const ConnectionObserver = require( "#connection-observer" );
10
+ const _ = require( "lodash" );
11
+ const config = require( "#config" );
12
+ const tools = require( "#tools" );
13
+ const redis = require( "#redis-integration" );
14
+ const exceptions = require( "#exceptions" );
15
+
16
+ /**
17
+ * Used to create and/or return a Common Memory Cache singleton instance.
18
+ *
19
+ * @class CommonMemoryCache
20
+ * @extends ConnectionObserver
21
+ * @singleton
22
+ * @public
23
+ */
24
+ class CommonMemoryCache extends ConnectionObserver {
25
+
26
+ static #instance = null;
27
+ #redisClient = null;
28
+ #isOperational = false;
29
+ #connectionIdentifier = "system-cache";
30
+
31
+ /**
32
+ * @constructor
33
+ * @return {CommonMemoryCache}
34
+ */
35
+ constructor() {
36
+ super();
37
+
38
+ if ( !CommonMemoryCache.#instance ) {
39
+ this.#redisClient = redis.createRedisClient( this.#connectionIdentifier );
40
+ this.#redisClient.addConnectionObserver( this );
41
+
42
+ CommonMemoryCache.#instance = this;
43
+ }
44
+ return CommonMemoryCache.#instance;
45
+ }
46
+
47
+ /* Public interface */
48
+
49
+ /**
50
+ * Property returning the operational state of the cache.
51
+ *
52
+ * @property
53
+ * @returns {boolean}
54
+ * @public
55
+ */
56
+ get isOperational() {
57
+ return this.#isOperational;
58
+ }
59
+
60
+ /**
61
+ * Property returning the connection identifier of the cache service.
62
+ *
63
+ * @property
64
+ * @returns {string}
65
+ * @public
66
+ */
67
+ get connectionIdentifier() {
68
+ return this.#connectionIdentifier;
69
+ }
70
+
71
+ /**
72
+ * Used to initialize the cache service.
73
+ *
74
+ * @method
75
+ * @returns {Promise}
76
+ * @public
77
+ */
78
+ initialize() {
79
+ let host = config.getSetting( config.setting.MEMORY_CACHE_REDIS_HOST );
80
+ let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
81
+ let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
82
+ let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
83
+ let user = config.getSetting( config.setting.MEMORY_CACHE_USER );
84
+
85
+ return this.#redisClient.initialize( host, port, authKey, user, db );
86
+ }
87
+
88
+ /**
89
+ * Used to gracefully shut down the cache service.
90
+ *
91
+ * @method
92
+ * @return {Promise}
93
+ * @public
94
+ */
95
+ shutDown() {
96
+ return this.#redisClient.shutDown( 250 );
97
+ }
98
+
99
+ /**
100
+ * Needs to be invoked by the connection handler when the connection is disrupted.
101
+ *
102
+ * @method
103
+ * @param {string} identifier The identifier of the observed connection.
104
+ * @override
105
+ * @public
106
+ */
107
+ onConnectionDisrupted( identifier ) {
108
+ if ( identifier === this.#connectionIdentifier ) {
109
+ this.#isOperational = false;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Needs to be invoked by the connection handler when the connection is recovered.
115
+ *
116
+ * @method
117
+ * @param {string} identifier The identifier of the observed connection.
118
+ * @override
119
+ * @public
120
+ */
121
+ onConnectionRecovered( identifier ) {
122
+ if ( identifier === this.#connectionIdentifier ) {
123
+ this.#isOperational = true;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Needs to be invoked by the connection handler when the connection is irrevocably lost.
129
+ *
130
+ * @method
131
+ * @param {string} identifier The identifier of the observed connection.
132
+ * @throws {TiException.E_GEN_SYSTEM_CACHE_UNAVAILABLE} If the cache service is no longer available.
133
+ * @override
134
+ * @public
135
+ */
136
+ onConnectionLost( identifier ) {
137
+ if ( identifier === this.#connectionIdentifier ) {
138
+ this.#isOperational = false;
139
+ // TODO: implement forced shut down of the service instance instead of crashing it outright
140
+ throw exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE );
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Used to register a new {@link ConnectionObserver} for events related to the underlying Redis connection state.
146
+ *
147
+ * @method
148
+ * @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
149
+ * @public
150
+ */
151
+ addConnectionObserver( connectionObserver ) {
152
+ this.#redisClient.addConnectionObserver( connectionObserver );
153
+ }
154
+
155
+ /**
156
+ * Used to search for keys by a given pattern.
157
+ *
158
+ * @method
159
+ * @param {string} pattern
160
+ * @returns {Promise<Array>}
161
+ * @public
162
+ */
163
+ matchKeys( pattern ) {
164
+ return new Promise( ( resolve, reject ) => {
165
+ if ( this.#isOperational === true ) {
166
+ let commandKeys = [ redis.cacheCommands.KEYS, pattern ];
167
+ this.#redisClient.executeCommands( [ commandKeys ] ).then( ( results ) => {
168
+ results = results[ 0 ];
169
+ resolve( ( results && results.length > 1 ) ? results[ 1 ] : [] );
170
+ } ).catch( ( error ) => {
171
+ reject( error );
172
+ } );
173
+ } else {
174
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
175
+ }
176
+ } );
177
+ }
178
+
179
+ /**
180
+ * Used to set a specific string value.
181
+ *
182
+ * @method
183
+ * @param {string} key
184
+ * @param {string} value
185
+ * @param {number} [expiration] Expiration value is in seconds.
186
+ * @return {Promise<string>}
187
+ * @public
188
+ */
189
+ setValue( key, value, expiration ) {
190
+ return new Promise( ( resolve, reject ) => {
191
+ if ( this.#isOperational === true ) {
192
+ if ( value ) {
193
+ let commandSetValue = [ redis.cacheCommands.SET_VALUE, key, tools.stringifyJSON( value ) ];
194
+ if ( expiration ) {
195
+ commandSetValue.push( "EX" );
196
+ commandSetValue.push( expiration );
197
+ }
198
+ this.#redisClient.executeCommands( [ commandSetValue ] ).then( () => {
199
+ resolve( value );
200
+ } ).catch( ( error ) => {
201
+ reject( error );
202
+ } );
203
+ } else {
204
+ resolve( value );
205
+ }
206
+ } else {
207
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
208
+ }
209
+ } );
210
+ }
211
+
212
+ /**
213
+ * Used to set multiple string values.
214
+ *
215
+ * @method
216
+ * @param {Object} keyValues
217
+ * @param {string} [prefix]
218
+ * @param {number} [expiration]
219
+ * @return {Promise}
220
+ * @public
221
+ */
222
+ setValues( keyValues, prefix, expiration ) {
223
+ return new Promise( ( resolve, reject ) => {
224
+ if ( this.#isOperational === true ) {
225
+ if ( keyValues ) {
226
+ let commands = [];
227
+ _.forEach( keyValues, ( value, key ) => {
228
+ let commandSetValue = [ redis.cacheCommands.SET_VALUE, ( ( prefix ) ? prefix : "" ) + key, tools.stringifyJSON( value ) ];
229
+ if ( expiration ) {
230
+ commandSetValue.push( "EX" );
231
+ commandSetValue.push( expiration );
232
+ }
233
+ commands.push( commandSetValue );
234
+ } );
235
+
236
+ this.#redisClient.executeCommands( commands ).then( () => {
237
+ resolve( keyValues );
238
+ } ).catch( ( error ) => {
239
+ reject( error );
240
+ } );
241
+ } else {
242
+ resolve( keyValues );
243
+ }
244
+ } else {
245
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
246
+ }
247
+ } );
248
+ }
249
+
250
+ /**
251
+ * Used to get a string value.
252
+ *
253
+ * @method
254
+ * @param {string} key
255
+ * @return {Promise}
256
+ * @public
257
+ */
258
+ getValue( key ) {
259
+ return new Promise( ( resolve, reject ) => {
260
+ if ( this.#isOperational === true ) {
261
+ let commandGetValue = [ redis.cacheCommands.GET_VALUE, key ];
262
+ this.#redisClient.executeCommands( [ commandGetValue ] ).then( ( results ) => {
263
+ results = results[ 0 ];
264
+ resolve( ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : undefined );
265
+ } ).catch( ( error ) => {
266
+ reject( error );
267
+ } );
268
+ } else {
269
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
270
+ }
271
+ } );
272
+ }
273
+
274
+ /**
275
+ * Used to get multiple string values.
276
+ *
277
+ * @method
278
+ * @param {string[]} keys
279
+ * @param {string} [prefix]
280
+ * @return {Promise}
281
+ * @public
282
+ */
283
+ getValues( keys, prefix ) {
284
+ return new Promise( ( resolve, reject ) => {
285
+ if ( this.#isOperational === true ) {
286
+ let commands = [];
287
+ _.forEach( keys, ( key ) => {
288
+ commands.push( [ redis.cacheCommands.GET_VALUE, ( ( prefix ) ? prefix : "" ) + key ] );
289
+ } );
290
+ this.#redisClient.executeCommands( commands ).then( ( rawResults ) => {
291
+ let results = {};
292
+ _.forEach( rawResults, ( result, idx ) => {
293
+ results[ keys[ idx ] ] = ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : null;
294
+ } );
295
+ resolve( results );
296
+ } ).catch( ( error ) => {
297
+ reject( error );
298
+ } );
299
+ } else {
300
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
301
+ }
302
+ } );
303
+ }
304
+
305
+ /**
306
+ * Used to delete a value / item.
307
+ *
308
+ * @method
309
+ * @param {string} key
310
+ * @returns {Promise<boolean>}
311
+ * @public
312
+ */
313
+ deleteValue( key ) {
314
+ return new Promise( ( resolve, reject ) => {
315
+ if ( this.#isOperational === true ) {
316
+ let commandDeleteValue = [ redis.cacheCommands.DELETE_VALUE, key ];
317
+ this.#redisClient.executeCommands( [ commandDeleteValue ] ).then( ( results ) => {
318
+ results = results[ 0 ];
319
+ resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
320
+ } ).catch( ( error ) => {
321
+ reject( error );
322
+ } );
323
+ } else {
324
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
325
+ }
326
+ } );
327
+ }
328
+
329
+ /**
330
+ * Used to set expiration in seconds to an existing key.
331
+ * <br/>
332
+ * NOTE: For performance optimization reasons, only use this only if the Redis command does not itself support the 'EX' argument.
333
+ *
334
+ * @method
335
+ * @param {string} key
336
+ * @param {number} seconds
337
+ * @param {string} [name] If you need to expire a field in a hash set instead, provide the name of the set here.
338
+ * @returns {Promise<number>} This will resolve with the seconds as provided initially by the caller.
339
+ * @public
340
+ */
341
+ expireValue( key, seconds, name ) {
342
+ return new Promise( ( resolve, reject ) => {
343
+ if ( this.#isOperational === true ) {
344
+ let commandExpire = ( name ) ? [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ] : [ redis.cacheCommands.EXPIRE, key, seconds ];
345
+ this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
346
+ resolve( seconds );
347
+ } ).catch( ( error ) => {
348
+ reject( error );
349
+ } );
350
+ } else {
351
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
352
+ }
353
+ } );
354
+ }
355
+
356
+ /**
357
+ * Used to add the specified values to a list.
358
+ *
359
+ * @method
360
+ * @param {string} listName
361
+ * @param {Object[]} values
362
+ * @returns {Promise<number>}
363
+ * @public
364
+ */
365
+ listPushValue( listName, values ) {
366
+ return new Promise( ( resolve, reject ) => {
367
+ if ( this.#isOperational === true ) {
368
+ let commandPushValues = [ redis.cacheCommands.LIST_PUSH, listName ];
369
+ _.forEach( values, ( value ) => {
370
+ if ( value ) {
371
+ commandPushValues.push( tools.stringifyJSON( value ) );
372
+ }
373
+ } );
374
+ this.#redisClient.executeCommands( [ commandPushValues ] ).then( ( results ) => {
375
+ results = results[ 0 ];
376
+ resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
377
+ } ).catch( ( error ) => {
378
+ reject( error );
379
+ } );
380
+ } else {
381
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
382
+ }
383
+ } );
384
+ }
385
+
386
+ /**
387
+ * Used to add the specified value to a set.
388
+ *
389
+ * @method
390
+ * @param {string} key
391
+ * @param {string|Object} value
392
+ * @returns {Promise}
393
+ * @public
394
+ */
395
+ addToSet( key, value ) {
396
+ return new Promise( ( resolve, reject ) => {
397
+ if ( this.#isOperational === true ) {
398
+ let commandAddToSet = [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( value ) ];
399
+ this.#redisClient.executeCommands( [ commandAddToSet ] ).then( () => {
400
+ resolve();
401
+ } ).catch( ( error ) => {
402
+ reject( error );
403
+ } );
404
+ } else {
405
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
406
+ }
407
+ } );
408
+ }
409
+
410
+ /**
411
+ * Used to add multiple values to multiple sets in one transactional request.
412
+ * <br/>
413
+ * NOTE: The two arrays of keys and values must have correct index relations (i.e., first pair on keys[0] and values[0] and so on)!
414
+ *
415
+ * @method
416
+ * @param {string[]} keys
417
+ * @param {string[]} values
418
+ * @returns {Promise}
419
+ * @public
420
+ */
421
+ addToSetMulti( keys, values ) {
422
+ return new Promise( ( resolve, reject ) => {
423
+ if ( this.#isOperational === true ) {
424
+ let commands = [];
425
+ _.forEach( keys, ( key, idx ) => {
426
+ commands.push( [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( values[ idx ] ) ] );
427
+ } );
428
+ this.#redisClient.executeCommands( commands ).then( () => {
429
+ resolve();
430
+ } ).catch( ( error ) => {
431
+ reject( error );
432
+ } );
433
+ } else {
434
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
435
+ }
436
+ } );
437
+ }
438
+
439
+ /**
440
+ * Used to check if the provided value is a member of the specified set.
441
+ *
442
+ * @method
443
+ * @param {string} setName
444
+ * @param {string} value
445
+ * @returns {Promise<boolean>}
446
+ * @public
447
+ */
448
+ isSetMember( setName, value ) {
449
+ return new Promise( ( resolve, reject ) => {
450
+ if ( this.#isOperational === true ) {
451
+ let commandIsSetMember = [ redis.cacheCommands.IS_SET_MEMBER, setName, value ];
452
+ this.#redisClient.executeCommands( [ commandIsSetMember ] ).then( ( results ) => {
453
+ results = results[ 0 ];
454
+ let result = !!( results && results.length > 1 && results[ 1 ] === 1 );
455
+ resolve( result );
456
+ } ).catch( ( error ) => {
457
+ reject( error );
458
+ } );
459
+ } else {
460
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
461
+ }
462
+ } );
463
+ }
464
+
465
+ /**
466
+ * Used to get all elements of a set.
467
+ *
468
+ * @method
469
+ * @param {string} key
470
+ * @returns {Promise<Object[]>}
471
+ * @public
472
+ */
473
+ membersOfSet( key ) {
474
+ return new Promise( ( resolve, reject ) => {
475
+ if ( this.#isOperational === true ) {
476
+ let commandMembersOfSet = [ redis.cacheCommands.GET_ALL_FROM_SET, key ];
477
+ this.#redisClient.executeCommands( [ commandMembersOfSet ] ).then( ( results ) => {
478
+ results = results[ 0 ];
479
+ let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
480
+ resolve( parsedResults );
481
+ } ).catch( ( error ) => {
482
+ reject( error );
483
+ } );
484
+ } else {
485
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
486
+ }
487
+ } );
488
+ }
489
+
490
+ /**
491
+ * Used to get a union of all elements in the list of sets.
492
+ *
493
+ * @method
494
+ * @param {string[]} keys
495
+ * @returns {Promise<Object[]>}
496
+ * @public
497
+ */
498
+ unionOfSets( keys ) {
499
+ return new Promise( ( resolve, reject ) => {
500
+ if ( this.#isOperational === true ) {
501
+ let commandUnionOfSets = _.concat( [ redis.cacheCommands.UNION_OF_SETS ], keys );
502
+ this.#redisClient.executeCommands( [ commandUnionOfSets ] ).then( ( results ) => {
503
+ results = results[ 0 ];
504
+ let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
505
+ resolve( parsedResults );
506
+ } ).catch( ( error ) => {
507
+ reject( error );
508
+ } );
509
+ } else {
510
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
511
+ }
512
+ } );
513
+ }
514
+
515
+ /**
516
+ * Used to set a single hash field.
517
+ *
518
+ * @method
519
+ * @deprecated
520
+ * @param {string} key
521
+ * @param {string} name
522
+ * @param {*} value
523
+ * @returns {Promise}
524
+ * @public
525
+ */
526
+ hashSetField( key, name, value ) {
527
+ return new Promise( ( resolve, reject ) => {
528
+ if ( this.#isOperational === true ) {
529
+ let commandHashSetField = [ redis.cacheCommands.HASH_SET, key, name, tools.stringifyJSON( value ) ];
530
+ this.#redisClient.executeCommands( [ commandHashSetField ] ).then( () => {
531
+ resolve();
532
+ } ).catch( ( error ) => {
533
+ reject( error );
534
+ } );
535
+ } else {
536
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
537
+ }
538
+ } );
539
+ }
540
+
541
+ /**
542
+ * Used to set multiple hash fields.
543
+ *
544
+ * @method
545
+ * @deprecated
546
+ * @param {string} key
547
+ * @param {Object[]} fields
548
+ * @param {string} fields[].name
549
+ * @param {*} fields[].value
550
+ * @returns {Promise}
551
+ * @public
552
+ */
553
+ hashSetFields( key, fields ) {
554
+ return new Promise( ( resolve, reject ) => {
555
+ if ( this.#isOperational === true ) {
556
+ let commandHashSetFields = [ redis.cacheCommands.HASH_SET, key ];
557
+ _.forEach( fields, ( field ) => {
558
+ commandHashSetFields.push( field.name );
559
+ commandHashSetFields.push( tools.stringifyJSON( field.value ) );
560
+ } );
561
+ this.#redisClient.executeCommands( [ commandHashSetFields ] ).then( () => {
562
+ resolve();
563
+ } ).catch( ( error ) => {
564
+ reject( error );
565
+ } );
566
+ } else {
567
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
568
+ }
569
+ } );
570
+ }
571
+
572
+ /**
573
+ * Used to get a single field from a hash.
574
+ *
575
+ * @method
576
+ * @param {string} key
577
+ * @param {string} field
578
+ * @return {Promise}
579
+ * @public
580
+ */
581
+ hashGetField( key, field ) {
582
+ return new Promise( ( resolve, reject ) => {
583
+ if ( this.#isOperational === true ) {
584
+ let commandHashGetField = [ redis.cacheCommands.HASH_GET, key, field ];
585
+ this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
586
+ results = results[ 0 ];
587
+ resolve( ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : null );
588
+ } ).catch( ( error ) => {
589
+ reject( error );
590
+ } );
591
+ } else {
592
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
593
+ }
594
+ } );
595
+ }
596
+
597
+ /**
598
+ * Used to remove a single field from a hash.
599
+ *
600
+ * @method
601
+ * @param {string} key
602
+ * @param {string} field
603
+ * @return {Promise<boolean>} Will return 'true' if the field was removed, 'false' otherwise.
604
+ * @public
605
+ */
606
+ hashDeleteField( key, field ) {
607
+ return new Promise( ( resolve, reject ) => {
608
+ if ( this.#isOperational === true ) {
609
+ let commandHashGetField = [ redis.cacheCommands.HASH_REMOVE, key, field ];
610
+ this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
611
+ results = results[ 0 ];
612
+ resolve( ( results && results.length > 1 ) ? tools.toBool( results[ 1 ] ) : false );
613
+ } ).catch( ( error ) => {
614
+ reject( error );
615
+ } );
616
+ } else {
617
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
618
+ }
619
+ } );
620
+ }
621
+
622
+ /**
623
+ * Used to store a JSON variable.
624
+ * <br/>
625
+ * NOTE: Requires ReJSON module installed on server to work.
626
+ *
627
+ * @method
628
+ * @param {string} key
629
+ * @param {Object} value
630
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
631
+ * @param {number} [overrideMode=0] By default this allows full override for existing keys.
632
+ * Option 1 will set the key only if it doesn't already exist. Option 2 will set it only if it already exists.
633
+ * @returns {Promise}
634
+ * @public
635
+ */
636
+ setJSON( key, value, path = "$", overrideMode = 0 ) {
637
+ return new Promise( ( resolve, reject ) => {
638
+ if ( this.#isOperational === true ) {
639
+ if ( this.#redisClient.isJSONSupported ) {
640
+ let commandArguments = [ redis.cacheCommands.JSON_SET, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
641
+ if ( overrideMode !== 0 ) {
642
+ commandArguments.push( overrideMode === 1 ? redis.cacheOverrideMode.NX : redis.cacheOverrideMode.XX );
643
+ }
644
+ this.#redisClient.callCommand( commandArguments ).then( () => {
645
+ resolve();
646
+ } ).catch( ( error ) => {
647
+ reject( error );
648
+ } );
649
+ } else {
650
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
651
+ }
652
+ } else {
653
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
654
+ }
655
+ } );
656
+ }
657
+
658
+ /**
659
+ * Used to fetch a JSON variable.
660
+ * <br/>
661
+ * NOTE: Requires ReJSON module installed on server to work.
662
+ *
663
+ * @method
664
+ * @param {string} key
665
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
666
+ * @returns {Promise<Object>}
667
+ * @public
668
+ */
669
+ getJSON( key, path = "$" ) {
670
+ return new Promise( ( resolve, reject ) => {
671
+ if ( this.#isOperational === true ) {
672
+ if ( this.#redisClient.isJSONSupported ) {
673
+ let commandArguments = [ redis.cacheCommands.JSON_GET, key, this.#normalizeJSONPath( path ) ];
674
+ this.#redisClient.callCommand( commandArguments ).then( ( result ) => {
675
+ resolve( result != null ? tools.parseJSON( String( result ) ) : null );
676
+ } ).catch( ( error ) => {
677
+ reject( error );
678
+ } );
679
+ } else {
680
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
681
+ }
682
+ } else {
683
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
684
+ }
685
+ } );
686
+ }
687
+
688
+ /**
689
+ * Used to update/edit an existing JSON variable.
690
+ * <br/>
691
+ * NOTE: Requires ReJSON module installed on server to work.
692
+ *
693
+ * @method
694
+ * @param {string} key
695
+ * @param {Object} value
696
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
697
+ * @returns {Promise}
698
+ * @public
699
+ */
700
+ editJSON( key, value, path = "$" ) {
701
+ return new Promise( ( resolve, reject ) => {
702
+ if ( this.#isOperational === true ) {
703
+ if ( this.#redisClient.isJSONSupported ) {
704
+ let commandArguments = [ redis.cacheCommands.JSON_MERGE, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
705
+ this.#redisClient.callCommand( commandArguments ).then( () => {
706
+ resolve();
707
+ } ).catch( ( error ) => {
708
+ reject( error );
709
+ } );
710
+ } else {
711
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
712
+ }
713
+ } else {
714
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
715
+ }
716
+ } );
717
+ }
718
+
719
+ /**
720
+ * Used to add an item to a JSON array. That array needs to exist already.
721
+ * <br/>
722
+ * NOTE: Requires ReJSON module installed on server to work.
723
+ *
724
+ * @method
725
+ * @param {string} key
726
+ * @param {Object} value
727
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
728
+ * @returns {Promise}
729
+ * @public
730
+ */
731
+ arrayAppendJSON( key, value, path = "$" ) {
732
+ return new Promise( ( resolve, reject ) => {
733
+ if ( this.#isOperational === true ) {
734
+ if ( this.#redisClient.isJSONSupported ) {
735
+ let commandArguments = [ redis.cacheCommands.JSON_ARRAY_APPEND, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
736
+ this.#redisClient.callCommand( commandArguments ).then( () => {
737
+ resolve();
738
+ } ).catch( ( error ) => {
739
+ reject( error );
740
+ } );
741
+ } else {
742
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
743
+ }
744
+ } else {
745
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
746
+ }
747
+ } );
748
+ }
749
+
750
+ /* Private interface */
751
+
752
+ /**
753
+ * Used to normalize a JSON path.
754
+ * <br/>
755
+ * NOTE: If "path" is an array, each element is treated as a literal key name and encoded with bracket notation,
756
+ * which correctly handles key names that contain dots or other JSONPath special characters.
757
+ *
758
+ * @method
759
+ * @param {string|string[]} path
760
+ * @returns {string}
761
+ * @private
762
+ */
763
+ #normalizeJSONPath( path ) {
764
+ if ( Array.isArray( path ) ) {
765
+ return "$" + path.map( ( segment ) => `["${ String( segment ).replace( /\\/g, "\\\\" ).replace( /"/g, '\\"' ) }"]` ).join( "" );
766
+ }
767
+ return ( path.startsWith( "$" ) === false ) ? ( "$." + path ) : path;
768
+ }
769
+
770
+ }
771
+
772
+ const instance = new CommonMemoryCache();
773
773
  module.exports.instance = Object.freeze( instance );