@ti-engine/core 1.12.2 → 1.13.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.
- package/CHANGELOG.md +11 -0
- package/bin/settings.json +1 -0
- package/components/cache/cache-capability.js +42 -0
- package/components/cache/cache-provider.js +432 -0
- package/components/cache/redis-cache-provider.js +749 -0
- package/package.json +17 -5
- package/types/components/cache/cache-capability.d.ts +23 -0
- package/types/components/cache/cache-provider.d.ts +326 -0
- package/types/components/cache/redis-cache-provider.d.ts +327 -0
- package/types/utils/cache.d.ts +40 -32
- package/types/utils/config.d.ts +1 -0
- package/utils/cache.js +126 -388
- package/utils/config.js +7 -0
|
@@ -0,0 +1,749 @@
|
|
|
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
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const CacheProvider = require( "#cache-provider" );
|
|
19
|
+
const _ = require( "lodash" );
|
|
20
|
+
const config = require( "#config" );
|
|
21
|
+
const tools = require( "#tools" );
|
|
22
|
+
const redis = require( "#redis-integration" );
|
|
23
|
+
const exceptions = require( "#exceptions" );
|
|
24
|
+
|
|
25
|
+
/** @import ConnectionObserver from "#connection-observer" */
|
|
26
|
+
|
|
27
|
+
const cacheCapability = require( "#cache-capability" ).cacheCapability;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decodes one entry of a `multi(...).exec()` result into the value it carries.
|
|
31
|
+
* <br/>
|
|
32
|
+
* Each entry is an ioredis `[ error, value ]` pair, so the value sits at index 1 and is a string when the key existed.
|
|
33
|
+
* Returns `undefined` for a miss, an error entry, or a malformed entry.
|
|
34
|
+
* <br/>
|
|
35
|
+
* This lives outside the class, and is shared by {@link CommonMemoryCache#getValue} and
|
|
36
|
+
* {@link CommonMemoryCache#getValues}, because it previously existed as two near-identical inline expressions and one
|
|
37
|
+
* of them drifted: `getValues` inspected its own accumulator instead of the per-key entry, so `.length` was
|
|
38
|
+
* `undefined`, the comparison was always false, and **every key resolved to `null`** whatever Redis returned.
|
|
39
|
+
*
|
|
40
|
+
* @method
|
|
41
|
+
* @param {Array} [result] One `[ error, value ]` entry.
|
|
42
|
+
* @returns {*} The parsed value, or `undefined` when there is none.
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
function decodeCommandValue( result ) {
|
|
46
|
+
// `Array.isArray` rather than a bare truthy-and-length test: a string also has a `length` and an indexable
|
|
47
|
+
// character at 1, so a malformed non-array entry would otherwise be parsed as if it were a value.
|
|
48
|
+
return ( Array.isArray( result ) && result.length > 1 && _.isString( result[ 1 ] ) ) ? tools.parseJSON( result[ 1 ] ) : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Maps a set of requested keys onto the values a `multi(...).exec()` returned for them, using `null` for a miss.
|
|
53
|
+
* <br/>
|
|
54
|
+
* Iterates the requested `keys` rather than the raw results, so a short or absent response still yields one entry per
|
|
55
|
+
* requested key instead of silently omitting some — the caller's map always has the shape it asked for.
|
|
56
|
+
* <br/>
|
|
57
|
+
* The accumulator has no prototype on purpose: the key names come from the caller, and a cache key named `__proto__`
|
|
58
|
+
* written by bracket assignment onto an ordinary `{}` would repoint the accumulator's prototype instead of creating
|
|
59
|
+
* the entry. Same class as the `decycle` defect fixed in `tools.js`; see the 1.11.0 changelog entry.
|
|
60
|
+
*
|
|
61
|
+
* @method
|
|
62
|
+
* @param {string[]} keys The keys that were requested, in command order.
|
|
63
|
+
* @param {Array} [rawResults] The `multi(...).exec()` result.
|
|
64
|
+
* @returns {Object} A null-prototype map of key to value, `null` where the key was absent.
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
function mapCommandValues( keys, rawResults ) {
|
|
68
|
+
let values = Object.create( null );
|
|
69
|
+
|
|
70
|
+
_.forEach( keys, ( key, idx ) => {
|
|
71
|
+
let decoded = decodeCommandValue( rawResults ? rawResults[ idx ] : undefined );
|
|
72
|
+
values[ key ] = ( decoded === undefined ) ? null : decoded;
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
return values;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A {@link CacheProvider} backed by Redis, optionally with the RedisJSON module.
|
|
80
|
+
* <br/>
|
|
81
|
+
* NOTE: This holds every Redis-specific detail in the engine's cache path — the command names, the
|
|
82
|
+
* '[ error, value ]' result shape, and the JSONPath encoding. Nothing above it should know that Redis is what is
|
|
83
|
+
* storing the values.
|
|
84
|
+
*
|
|
85
|
+
* @class RedisCacheProvider
|
|
86
|
+
* @extends CacheProvider
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
class RedisCacheProvider extends CacheProvider {
|
|
90
|
+
|
|
91
|
+
#redisClient;
|
|
92
|
+
#connectionIdentifier;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @constructor
|
|
96
|
+
* @param {string} connectionIdentifier The identifier under which this backend's connection is observed.
|
|
97
|
+
*/
|
|
98
|
+
constructor( connectionIdentifier ) {
|
|
99
|
+
super();
|
|
100
|
+
|
|
101
|
+
this.#connectionIdentifier = connectionIdentifier;
|
|
102
|
+
this.#redisClient = redis.createRedisClient( connectionIdentifier );
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Public interface */
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Decodes one entry of a `multi(...).exec()` result into the value it carries.
|
|
109
|
+
* <br/>
|
|
110
|
+
* NOTE: Exposed for testing. The provider builds its own Redis client in its constructor, so `getValues` cannot be
|
|
111
|
+
* driven without a live server — this is one of the pure halves of it, and it is where the defect was.
|
|
112
|
+
*
|
|
113
|
+
* @method
|
|
114
|
+
* @param {Array} [result] One `[ error, value ]` entry.
|
|
115
|
+
* @returns {*} The parsed value, or `undefined` when there is none.
|
|
116
|
+
* @public
|
|
117
|
+
*/
|
|
118
|
+
static decodeCommandValue( result ) {
|
|
119
|
+
return decodeCommandValue( result );
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Maps a set of requested keys onto the values a `multi(...).exec()` returned for them, using `null` for a miss.
|
|
124
|
+
* <br/>
|
|
125
|
+
* NOTE: Exposed for testing, for the same reason as {@link RedisCacheProvider.decodeCommandValue}.
|
|
126
|
+
*
|
|
127
|
+
* @method
|
|
128
|
+
* @param {string[]} keys The keys that were requested, in command order.
|
|
129
|
+
* @param {Array} [rawResults] The `multi(...).exec()` result.
|
|
130
|
+
* @returns {Object} A null-prototype map of key to value, `null` where the key was absent.
|
|
131
|
+
* @public
|
|
132
|
+
*/
|
|
133
|
+
static mapCommandValues( keys, rawResults ) {
|
|
134
|
+
return mapCommandValues( keys, rawResults );
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Property returning the connection identifier of this backend.
|
|
139
|
+
*
|
|
140
|
+
* @property
|
|
141
|
+
* @returns {string}
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
get connectionIdentifier() {
|
|
145
|
+
return this.#connectionIdentifier;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Property returning the optional behaviors this backend provides.
|
|
150
|
+
* <br/>
|
|
151
|
+
* NOTE: The JSON capabilities depend on the RedisJSON module being installed on the server, which is only known
|
|
152
|
+
* after the client has connected — so this is accurate from {@link RedisCacheProvider#initialize} onward and
|
|
153
|
+
* reports no JSON support before that.
|
|
154
|
+
*
|
|
155
|
+
* @property
|
|
156
|
+
* @returns {string[]}
|
|
157
|
+
* @override
|
|
158
|
+
* @public
|
|
159
|
+
*/
|
|
160
|
+
get capabilities() {
|
|
161
|
+
let capabilities = [
|
|
162
|
+
cacheCapability.KEY_EXPIRY,
|
|
163
|
+
cacheCapability.KEY_PATTERN_MATCH,
|
|
164
|
+
cacheCapability.LISTS,
|
|
165
|
+
cacheCapability.SETS,
|
|
166
|
+
cacheCapability.HASH_FIELDS
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// RedisJSON applies a JSONPath write server-side, so a concurrent edit to a different path of the same
|
|
170
|
+
// document is not lost. That is what ATOMIC_JSON_EDIT promises, and why it is declared together with
|
|
171
|
+
// JSON_DOCUMENTS rather than separately - for this backend the two always arrive together.
|
|
172
|
+
if ( this.#redisClient.isJSONSupported === true ) {
|
|
173
|
+
capabilities.push( cacheCapability.JSON_DOCUMENTS );
|
|
174
|
+
capabilities.push( cacheCapability.ATOMIC_JSON_EDIT );
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return capabilities;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Used to initialize the backend and connect to the Redis server.
|
|
182
|
+
*
|
|
183
|
+
* @method
|
|
184
|
+
* @returns {Promise}
|
|
185
|
+
* @override
|
|
186
|
+
* @public
|
|
187
|
+
*/
|
|
188
|
+
initialize() {
|
|
189
|
+
let host = config.getSetting( config.setting.MEMORY_CACHE_REDIS_HOST );
|
|
190
|
+
let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
|
|
191
|
+
let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
|
|
192
|
+
let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
|
|
193
|
+
let user = config.getSetting( config.setting.MEMORY_CACHE_USER );
|
|
194
|
+
|
|
195
|
+
return this.#redisClient.initialize( host, port, authKey, user, db );
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Used to gracefully shut the Redis connection down.
|
|
200
|
+
*
|
|
201
|
+
* @method
|
|
202
|
+
* @returns {Promise}
|
|
203
|
+
* @override
|
|
204
|
+
* @public
|
|
205
|
+
*/
|
|
206
|
+
shutDown() {
|
|
207
|
+
return this.#redisClient.shutDown( 250 );
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Used to register a new {@link ConnectionObserver} for events related to the underlying Redis connection state.
|
|
212
|
+
*
|
|
213
|
+
* @method
|
|
214
|
+
* @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
|
|
215
|
+
* @override
|
|
216
|
+
* @public
|
|
217
|
+
*/
|
|
218
|
+
addConnectionObserver( connectionObserver ) {
|
|
219
|
+
this.#redisClient.addConnectionObserver( connectionObserver );
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Used to search for keys by a given pattern.
|
|
224
|
+
*
|
|
225
|
+
* @method
|
|
226
|
+
* @param {string} pattern
|
|
227
|
+
* @returns {Promise<Array>}
|
|
228
|
+
* @public
|
|
229
|
+
*/
|
|
230
|
+
matchKeys( pattern ) {
|
|
231
|
+
return new Promise( ( resolve, reject ) => {
|
|
232
|
+
let commandKeys = [ redis.cacheCommands.KEYS, pattern ];
|
|
233
|
+
this.#redisClient.executeCommands( [ commandKeys ] ).then( ( results ) => {
|
|
234
|
+
results = results[ 0 ];
|
|
235
|
+
resolve( ( results && results.length > 1 ) ? results[ 1 ] : [] );
|
|
236
|
+
} ).catch( ( error ) => {
|
|
237
|
+
reject( error );
|
|
238
|
+
} );
|
|
239
|
+
} );
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Used to set a specific string value.
|
|
244
|
+
*
|
|
245
|
+
* @method
|
|
246
|
+
* @param {string} key
|
|
247
|
+
* @param {string} value
|
|
248
|
+
* @param {number} [expiration] Expiration value is in seconds.
|
|
249
|
+
* @return {Promise<string>}
|
|
250
|
+
* @public
|
|
251
|
+
*/
|
|
252
|
+
setValue( key, value, expiration ) {
|
|
253
|
+
return new Promise( ( resolve, reject ) => {
|
|
254
|
+
if ( value ) {
|
|
255
|
+
let commandSetValue = [ redis.cacheCommands.SET_VALUE, key, tools.stringifyJSON( value ) ];
|
|
256
|
+
if ( expiration ) {
|
|
257
|
+
commandSetValue.push( "EX" );
|
|
258
|
+
commandSetValue.push( expiration );
|
|
259
|
+
}
|
|
260
|
+
this.#redisClient.executeCommands( [ commandSetValue ] ).then( () => {
|
|
261
|
+
resolve( value );
|
|
262
|
+
} ).catch( ( error ) => {
|
|
263
|
+
reject( error );
|
|
264
|
+
} );
|
|
265
|
+
} else {
|
|
266
|
+
resolve( value );
|
|
267
|
+
}
|
|
268
|
+
} );
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Used to set multiple string values.
|
|
273
|
+
*
|
|
274
|
+
* @method
|
|
275
|
+
* @param {Object} keyValues
|
|
276
|
+
* @param {string} [prefix]
|
|
277
|
+
* @param {number} [expiration]
|
|
278
|
+
* @return {Promise}
|
|
279
|
+
* @public
|
|
280
|
+
*/
|
|
281
|
+
setValues( keyValues, prefix, expiration ) {
|
|
282
|
+
return new Promise( ( resolve, reject ) => {
|
|
283
|
+
if ( keyValues ) {
|
|
284
|
+
let commands = [];
|
|
285
|
+
_.forEach( keyValues, ( value, key ) => {
|
|
286
|
+
let commandSetValue = [ redis.cacheCommands.SET_VALUE, ( ( prefix ) ? prefix : "" ) + key, tools.stringifyJSON( value ) ];
|
|
287
|
+
if ( expiration ) {
|
|
288
|
+
commandSetValue.push( "EX" );
|
|
289
|
+
commandSetValue.push( expiration );
|
|
290
|
+
}
|
|
291
|
+
commands.push( commandSetValue );
|
|
292
|
+
} );
|
|
293
|
+
|
|
294
|
+
this.#redisClient.executeCommands( commands ).then( () => {
|
|
295
|
+
resolve( keyValues );
|
|
296
|
+
} ).catch( ( error ) => {
|
|
297
|
+
reject( error );
|
|
298
|
+
} );
|
|
299
|
+
} else {
|
|
300
|
+
resolve( keyValues );
|
|
301
|
+
}
|
|
302
|
+
} );
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Used to get a string value.
|
|
307
|
+
*
|
|
308
|
+
* @method
|
|
309
|
+
* @param {string} key
|
|
310
|
+
* @return {Promise}
|
|
311
|
+
* @public
|
|
312
|
+
*/
|
|
313
|
+
getValue( key ) {
|
|
314
|
+
return new Promise( ( resolve, reject ) => {
|
|
315
|
+
let commandGetValue = [ redis.cacheCommands.GET_VALUE, key ];
|
|
316
|
+
this.#redisClient.executeCommands( [ commandGetValue ] ).then( ( results ) => {
|
|
317
|
+
resolve( decodeCommandValue( results ? results[ 0 ] : undefined ) );
|
|
318
|
+
} ).catch( ( error ) => {
|
|
319
|
+
reject( error );
|
|
320
|
+
} );
|
|
321
|
+
} );
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Used to get multiple string values.
|
|
326
|
+
*
|
|
327
|
+
* @method
|
|
328
|
+
* @param {string[]} keys
|
|
329
|
+
* @param {string} [prefix]
|
|
330
|
+
* @return {Promise}
|
|
331
|
+
* @public
|
|
332
|
+
*/
|
|
333
|
+
getValues( keys, prefix ) {
|
|
334
|
+
return new Promise( ( resolve, reject ) => {
|
|
335
|
+
let commands = [];
|
|
336
|
+
_.forEach( keys, ( key ) => {
|
|
337
|
+
commands.push( [ redis.cacheCommands.GET_VALUE, ( ( prefix ) ? prefix : "" ) + key ] );
|
|
338
|
+
} );
|
|
339
|
+
this.#redisClient.executeCommands( commands ).then( ( rawResults ) => {
|
|
340
|
+
resolve( mapCommandValues( keys, rawResults ) );
|
|
341
|
+
} ).catch( ( error ) => {
|
|
342
|
+
reject( error );
|
|
343
|
+
} );
|
|
344
|
+
} );
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Used to delete a value / item.
|
|
349
|
+
*
|
|
350
|
+
* @method
|
|
351
|
+
* @param {string} key
|
|
352
|
+
* @returns {Promise<boolean>}
|
|
353
|
+
* @public
|
|
354
|
+
*/
|
|
355
|
+
deleteValue( key ) {
|
|
356
|
+
return new Promise( ( resolve, reject ) => {
|
|
357
|
+
let commandDeleteValue = [ redis.cacheCommands.DELETE_VALUE, key ];
|
|
358
|
+
this.#redisClient.executeCommands( [ commandDeleteValue ] ).then( ( results ) => {
|
|
359
|
+
results = results[ 0 ];
|
|
360
|
+
resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
|
|
361
|
+
} ).catch( ( error ) => {
|
|
362
|
+
reject( error );
|
|
363
|
+
} );
|
|
364
|
+
} );
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Used to set expiration in seconds to an existing key.
|
|
369
|
+
* <br/>
|
|
370
|
+
* NOTE: For performance optimization reasons, only use this only if the Redis command does not itself support the 'EX' argument.
|
|
371
|
+
*
|
|
372
|
+
* @method
|
|
373
|
+
* @param {string} key
|
|
374
|
+
* @param {number} seconds
|
|
375
|
+
* @param {string} [name] If you need to expire a field in a hash set instead, provide the name of the set here.
|
|
376
|
+
* @returns {Promise<number>} This will resolve with the seconds as provided initially by the caller.
|
|
377
|
+
* @public
|
|
378
|
+
*/
|
|
379
|
+
expireValue( key, seconds, name ) {
|
|
380
|
+
return new Promise( ( resolve, reject ) => {
|
|
381
|
+
let commandExpire = ( name ) ? [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ] : [ redis.cacheCommands.EXPIRE, key, seconds ];
|
|
382
|
+
this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
|
|
383
|
+
resolve( seconds );
|
|
384
|
+
} ).catch( ( error ) => {
|
|
385
|
+
reject( error );
|
|
386
|
+
} );
|
|
387
|
+
} );
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Used to add the specified values to a list.
|
|
392
|
+
*
|
|
393
|
+
* @method
|
|
394
|
+
* @param {string} listName
|
|
395
|
+
* @param {Object[]} values
|
|
396
|
+
* @returns {Promise<number>}
|
|
397
|
+
* @public
|
|
398
|
+
*/
|
|
399
|
+
listPushValue( listName, values ) {
|
|
400
|
+
return new Promise( ( resolve, reject ) => {
|
|
401
|
+
let commandPushValues = [ redis.cacheCommands.LIST_PUSH, listName ];
|
|
402
|
+
_.forEach( values, ( value ) => {
|
|
403
|
+
if ( value ) {
|
|
404
|
+
commandPushValues.push( tools.stringifyJSON( value ) );
|
|
405
|
+
}
|
|
406
|
+
} );
|
|
407
|
+
this.#redisClient.executeCommands( [ commandPushValues ] ).then( ( results ) => {
|
|
408
|
+
results = results[ 0 ];
|
|
409
|
+
resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
|
|
410
|
+
} ).catch( ( error ) => {
|
|
411
|
+
reject( error );
|
|
412
|
+
} );
|
|
413
|
+
} );
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Used to add the specified value to a set.
|
|
418
|
+
*
|
|
419
|
+
* @method
|
|
420
|
+
* @param {string} key
|
|
421
|
+
* @param {string|Object} value
|
|
422
|
+
* @returns {Promise}
|
|
423
|
+
* @public
|
|
424
|
+
*/
|
|
425
|
+
addToSet( key, value ) {
|
|
426
|
+
return new Promise( ( resolve, reject ) => {
|
|
427
|
+
let commandAddToSet = [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( value ) ];
|
|
428
|
+
this.#redisClient.executeCommands( [ commandAddToSet ] ).then( () => {
|
|
429
|
+
resolve();
|
|
430
|
+
} ).catch( ( error ) => {
|
|
431
|
+
reject( error );
|
|
432
|
+
} );
|
|
433
|
+
} );
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Used to add multiple values to multiple sets in one transactional request.
|
|
438
|
+
* <br/>
|
|
439
|
+
* 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)!
|
|
440
|
+
*
|
|
441
|
+
* @method
|
|
442
|
+
* @param {string[]} keys
|
|
443
|
+
* @param {string[]} values
|
|
444
|
+
* @returns {Promise}
|
|
445
|
+
* @public
|
|
446
|
+
*/
|
|
447
|
+
addToSetMulti( keys, values ) {
|
|
448
|
+
return new Promise( ( resolve, reject ) => {
|
|
449
|
+
let commands = [];
|
|
450
|
+
_.forEach( keys, ( key, idx ) => {
|
|
451
|
+
commands.push( [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( values[ idx ] ) ] );
|
|
452
|
+
} );
|
|
453
|
+
this.#redisClient.executeCommands( commands ).then( () => {
|
|
454
|
+
resolve();
|
|
455
|
+
} ).catch( ( error ) => {
|
|
456
|
+
reject( error );
|
|
457
|
+
} );
|
|
458
|
+
} );
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Used to check if the provided value is a member of the specified set.
|
|
463
|
+
*
|
|
464
|
+
* @method
|
|
465
|
+
* @param {string} setName
|
|
466
|
+
* @param {string} value
|
|
467
|
+
* @returns {Promise<boolean>}
|
|
468
|
+
* @public
|
|
469
|
+
*/
|
|
470
|
+
isSetMember( setName, value ) {
|
|
471
|
+
return new Promise( ( resolve, reject ) => {
|
|
472
|
+
let commandIsSetMember = [ redis.cacheCommands.IS_SET_MEMBER, setName, value ];
|
|
473
|
+
this.#redisClient.executeCommands( [ commandIsSetMember ] ).then( ( results ) => {
|
|
474
|
+
results = results[ 0 ];
|
|
475
|
+
let result = !!( results && results.length > 1 && results[ 1 ] === 1 );
|
|
476
|
+
resolve( result );
|
|
477
|
+
} ).catch( ( error ) => {
|
|
478
|
+
reject( error );
|
|
479
|
+
} );
|
|
480
|
+
} );
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Used to get all elements of a set.
|
|
485
|
+
*
|
|
486
|
+
* @method
|
|
487
|
+
* @param {string} key
|
|
488
|
+
* @returns {Promise<Object[]>}
|
|
489
|
+
* @public
|
|
490
|
+
*/
|
|
491
|
+
membersOfSet( key ) {
|
|
492
|
+
return new Promise( ( resolve, reject ) => {
|
|
493
|
+
let commandMembersOfSet = [ redis.cacheCommands.GET_ALL_FROM_SET, key ];
|
|
494
|
+
this.#redisClient.executeCommands( [ commandMembersOfSet ] ).then( ( results ) => {
|
|
495
|
+
results = results[ 0 ];
|
|
496
|
+
let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
|
|
497
|
+
resolve( parsedResults );
|
|
498
|
+
} ).catch( ( error ) => {
|
|
499
|
+
reject( error );
|
|
500
|
+
} );
|
|
501
|
+
} );
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Used to get a union of all elements in the list of sets.
|
|
506
|
+
*
|
|
507
|
+
* @method
|
|
508
|
+
* @param {string[]} keys
|
|
509
|
+
* @returns {Promise<Object[]>}
|
|
510
|
+
* @public
|
|
511
|
+
*/
|
|
512
|
+
unionOfSets( keys ) {
|
|
513
|
+
return new Promise( ( resolve, reject ) => {
|
|
514
|
+
let commandUnionOfSets = _.concat( [ redis.cacheCommands.UNION_OF_SETS ], keys );
|
|
515
|
+
this.#redisClient.executeCommands( [ commandUnionOfSets ] ).then( ( results ) => {
|
|
516
|
+
results = results[ 0 ];
|
|
517
|
+
let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
|
|
518
|
+
resolve( parsedResults );
|
|
519
|
+
} ).catch( ( error ) => {
|
|
520
|
+
reject( error );
|
|
521
|
+
} );
|
|
522
|
+
} );
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Used to set a single hash field.
|
|
527
|
+
*
|
|
528
|
+
* @method
|
|
529
|
+
* @deprecated
|
|
530
|
+
* @param {string} key
|
|
531
|
+
* @param {string} name
|
|
532
|
+
* @param {*} value
|
|
533
|
+
* @returns {Promise}
|
|
534
|
+
* @public
|
|
535
|
+
*/
|
|
536
|
+
hashSetField( key, name, value ) {
|
|
537
|
+
return new Promise( ( resolve, reject ) => {
|
|
538
|
+
let commandHashSetField = [ redis.cacheCommands.HASH_SET, key, name, tools.stringifyJSON( value ) ];
|
|
539
|
+
this.#redisClient.executeCommands( [ commandHashSetField ] ).then( () => {
|
|
540
|
+
resolve();
|
|
541
|
+
} ).catch( ( error ) => {
|
|
542
|
+
reject( error );
|
|
543
|
+
} );
|
|
544
|
+
} );
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Used to set multiple hash fields.
|
|
549
|
+
*
|
|
550
|
+
* @method
|
|
551
|
+
* @deprecated
|
|
552
|
+
* @param {string} key
|
|
553
|
+
* @param {Object[]} fields
|
|
554
|
+
* @param {string} fields[].name
|
|
555
|
+
* @param {*} fields[].value
|
|
556
|
+
* @returns {Promise}
|
|
557
|
+
* @public
|
|
558
|
+
*/
|
|
559
|
+
hashSetFields( key, fields ) {
|
|
560
|
+
return new Promise( ( resolve, reject ) => {
|
|
561
|
+
let commandHashSetFields = [ redis.cacheCommands.HASH_SET, key ];
|
|
562
|
+
_.forEach( fields, ( field ) => {
|
|
563
|
+
commandHashSetFields.push( field.name );
|
|
564
|
+
commandHashSetFields.push( tools.stringifyJSON( field.value ) );
|
|
565
|
+
} );
|
|
566
|
+
this.#redisClient.executeCommands( [ commandHashSetFields ] ).then( () => {
|
|
567
|
+
resolve();
|
|
568
|
+
} ).catch( ( error ) => {
|
|
569
|
+
reject( error );
|
|
570
|
+
} );
|
|
571
|
+
} );
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Used to get a single field from a hash.
|
|
576
|
+
*
|
|
577
|
+
* @method
|
|
578
|
+
* @param {string} key
|
|
579
|
+
* @param {string} field
|
|
580
|
+
* @return {Promise}
|
|
581
|
+
* @public
|
|
582
|
+
*/
|
|
583
|
+
hashGetField( key, field ) {
|
|
584
|
+
return new Promise( ( resolve, reject ) => {
|
|
585
|
+
let commandHashGetField = [ redis.cacheCommands.HASH_GET, key, field ];
|
|
586
|
+
this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
|
|
587
|
+
results = results[ 0 ];
|
|
588
|
+
resolve( ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : null );
|
|
589
|
+
} ).catch( ( error ) => {
|
|
590
|
+
reject( error );
|
|
591
|
+
} );
|
|
592
|
+
} );
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Used to remove a single field from a hash.
|
|
597
|
+
*
|
|
598
|
+
* @method
|
|
599
|
+
* @param {string} key
|
|
600
|
+
* @param {string} field
|
|
601
|
+
* @return {Promise<boolean>} Will return 'true' if the field was removed, 'false' otherwise.
|
|
602
|
+
* @public
|
|
603
|
+
*/
|
|
604
|
+
hashDeleteField( key, field ) {
|
|
605
|
+
return new Promise( ( resolve, reject ) => {
|
|
606
|
+
let commandHashGetField = [ redis.cacheCommands.HASH_REMOVE, key, field ];
|
|
607
|
+
this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
|
|
608
|
+
results = results[ 0 ];
|
|
609
|
+
resolve( ( results && results.length > 1 ) ? tools.toBool( results[ 1 ] ) : false );
|
|
610
|
+
} ).catch( ( error ) => {
|
|
611
|
+
reject( error );
|
|
612
|
+
} );
|
|
613
|
+
} );
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Used to store a JSON variable.
|
|
618
|
+
* <br/>
|
|
619
|
+
* NOTE: Requires ReJSON module installed on server to work.
|
|
620
|
+
*
|
|
621
|
+
* @method
|
|
622
|
+
* @param {string} key
|
|
623
|
+
* @param {Object} value
|
|
624
|
+
* @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).
|
|
625
|
+
* @param {number} [overrideMode=0] By default this allows full override for existing keys.
|
|
626
|
+
* Option 1 will set the key only if it doesn't already exist. Option 2 will set it only if it already exists.
|
|
627
|
+
* @returns {Promise}
|
|
628
|
+
* @public
|
|
629
|
+
*/
|
|
630
|
+
setJSON( key, value, path = "$", overrideMode = 0 ) {
|
|
631
|
+
return new Promise( ( resolve, reject ) => {
|
|
632
|
+
if ( this.#redisClient.isJSONSupported ) {
|
|
633
|
+
let commandArguments = [ redis.cacheCommands.JSON_SET, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
|
|
634
|
+
if ( overrideMode !== 0 ) {
|
|
635
|
+
commandArguments.push( overrideMode === 1 ? redis.cacheOverrideMode.NX : redis.cacheOverrideMode.XX );
|
|
636
|
+
}
|
|
637
|
+
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
638
|
+
resolve();
|
|
639
|
+
} ).catch( ( error ) => {
|
|
640
|
+
reject( error );
|
|
641
|
+
} );
|
|
642
|
+
} else {
|
|
643
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
644
|
+
}
|
|
645
|
+
} );
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Used to fetch a JSON variable.
|
|
650
|
+
* <br/>
|
|
651
|
+
* NOTE: Requires ReJSON module installed on server to work.
|
|
652
|
+
*
|
|
653
|
+
* @method
|
|
654
|
+
* @param {string} key
|
|
655
|
+
* @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).
|
|
656
|
+
* @returns {Promise<Object>}
|
|
657
|
+
* @public
|
|
658
|
+
*/
|
|
659
|
+
getJSON( key, path = "$" ) {
|
|
660
|
+
return new Promise( ( resolve, reject ) => {
|
|
661
|
+
if ( this.#redisClient.isJSONSupported ) {
|
|
662
|
+
let commandArguments = [ redis.cacheCommands.JSON_GET, key, this.#normalizeJSONPath( path ) ];
|
|
663
|
+
this.#redisClient.callCommand( commandArguments ).then( ( result ) => {
|
|
664
|
+
resolve( result != null ? tools.parseJSON( String( result ) ) : null );
|
|
665
|
+
} ).catch( ( error ) => {
|
|
666
|
+
reject( error );
|
|
667
|
+
} );
|
|
668
|
+
} else {
|
|
669
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
670
|
+
}
|
|
671
|
+
} );
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Used to update/edit an existing JSON variable.
|
|
676
|
+
* <br/>
|
|
677
|
+
* NOTE: Requires ReJSON module installed on server to work.
|
|
678
|
+
*
|
|
679
|
+
* @method
|
|
680
|
+
* @param {string} key
|
|
681
|
+
* @param {Object} value
|
|
682
|
+
* @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).
|
|
683
|
+
* @returns {Promise}
|
|
684
|
+
* @public
|
|
685
|
+
*/
|
|
686
|
+
editJSON( key, value, path = "$" ) {
|
|
687
|
+
return new Promise( ( resolve, reject ) => {
|
|
688
|
+
if ( this.#redisClient.isJSONSupported ) {
|
|
689
|
+
let commandArguments = [ redis.cacheCommands.JSON_MERGE, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
|
|
690
|
+
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
691
|
+
resolve();
|
|
692
|
+
} ).catch( ( error ) => {
|
|
693
|
+
reject( error );
|
|
694
|
+
} );
|
|
695
|
+
} else {
|
|
696
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
697
|
+
}
|
|
698
|
+
} );
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Used to add an item to a JSON array. That array needs to exist already.
|
|
703
|
+
* <br/>
|
|
704
|
+
* NOTE: Requires ReJSON module installed on server to work.
|
|
705
|
+
*
|
|
706
|
+
* @method
|
|
707
|
+
* @param {string} key
|
|
708
|
+
* @param {Object} value
|
|
709
|
+
* @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).
|
|
710
|
+
* @returns {Promise}
|
|
711
|
+
* @public
|
|
712
|
+
*/
|
|
713
|
+
arrayAppendJSON( key, value, path = "$" ) {
|
|
714
|
+
return new Promise( ( resolve, reject ) => {
|
|
715
|
+
if ( this.#redisClient.isJSONSupported ) {
|
|
716
|
+
let commandArguments = [ redis.cacheCommands.JSON_ARRAY_APPEND, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
|
|
717
|
+
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
718
|
+
resolve();
|
|
719
|
+
} ).catch( ( error ) => {
|
|
720
|
+
reject( error );
|
|
721
|
+
} );
|
|
722
|
+
} else {
|
|
723
|
+
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
724
|
+
}
|
|
725
|
+
} );
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/* Private interface */
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Used to normalize a JSON path.
|
|
732
|
+
* <br/>
|
|
733
|
+
* NOTE: If "path" is an array, each element is treated as a literal key name and encoded with bracket notation,
|
|
734
|
+
* which correctly handles key names that contain dots or other JSONPath special characters.
|
|
735
|
+
*
|
|
736
|
+
* @method
|
|
737
|
+
* @param {string|string[]} path
|
|
738
|
+
* @returns {string}
|
|
739
|
+
*/
|
|
740
|
+
#normalizeJSONPath( path ) {
|
|
741
|
+
if ( Array.isArray( path ) ) {
|
|
742
|
+
return "$" + path.map( ( segment ) => `["${ String( segment ).replace( /\\/g, "\\\\" ).replace( /"/g, '\\"' ) }"]` ).join( "" );
|
|
743
|
+
}
|
|
744
|
+
return ( path.startsWith( "$" ) === false ) ? ( "$." + path ) : path;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
module.exports = RedisCacheProvider;
|