@ti-engine/core 1.12.3 → 1.14.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 +15 -0
- package/bin/settings.json +4 -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/components/service-instance.js +40 -3
- package/package.json +13 -1
- 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/components/service-instance.d.ts +14 -0
- package/types/utils/cache.d.ts +70 -26
- package/types/utils/config.d.ts +4 -0
- package/utils/cache.js +180 -378
- package/utils/config.js +12 -0
package/utils/cache.js
CHANGED
|
@@ -15,64 +15,99 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
const CacheProvider = require( "#cache-provider" );
|
|
18
19
|
const ConnectionObserver = require( "#connection-observer" );
|
|
20
|
+
const RedisCacheProvider = require( "#redis-cache-provider" );
|
|
19
21
|
const _ = require( "lodash" );
|
|
20
22
|
const config = require( "#config" );
|
|
21
|
-
const tools = require( "#tools" );
|
|
22
|
-
const redis = require( "#redis-integration" );
|
|
23
23
|
const exceptions = require( "#exceptions" );
|
|
24
|
+
const path = require( "path" );
|
|
25
|
+
const { cacheCapability } = require( "#cache-capability" );
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
*
|
|
28
|
+
* Determines whether a value is a class extending {@link CacheProvider}, without constructing it.
|
|
27
29
|
* <br/>
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* of them drifted: `getValues` inspected its own accumulator instead of the per-key entry, so `.length` was
|
|
34
|
-
* `undefined`, the comparison was always false, and **every key resolved to `null`** whatever Redis returned.
|
|
30
|
+
* NOTE: A `typeof === "function"` test is not enough, and constructing first to ask `instanceof` afterwards is worse.
|
|
31
|
+
* An arrow function passes the `typeof` test but is not a constructor, so `new` on it throws a raw TypeError instead
|
|
32
|
+
* of the documented exception; and an unrelated class would have its constructor RUN - arbitrary code from a
|
|
33
|
+
* misconfigured path - before anything rejected it. Walking the prototype chain answers the question without
|
|
34
|
+
* executing anything.
|
|
35
35
|
*
|
|
36
36
|
* @method
|
|
37
|
-
* @param {
|
|
38
|
-
* @returns {
|
|
39
|
-
* @
|
|
37
|
+
* @param {*} candidate The value exported by the configured provider module.
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
* @public
|
|
40
40
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
// character at 1, so a malformed non-array entry would otherwise be parsed as if it were a value.
|
|
44
|
-
return ( Array.isArray( result ) && result.length > 1 && _.isString( result[ 1 ] ) ) ? tools.parseJSON( result[ 1 ] ) : undefined;
|
|
41
|
+
function isCacheProviderClass( candidate ) {
|
|
42
|
+
return typeof candidate === "function" && candidate.prototype instanceof CacheProvider;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
/**
|
|
48
|
-
*
|
|
46
|
+
* Creates the cache backend named by the 'memoryCache.provider' setting.
|
|
49
47
|
* <br/>
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* NOTE: The built-in name "redis" selects {@link RedisCacheProvider}. Any other value is treated as a module path
|
|
49
|
+
* resolved against the process working directory, much as 'TI_INSTANCE_CLASS' already is, and must export a class
|
|
50
|
+
* extending {@link CacheProvider}. Resolution uses `path.resolve` rather than `path.join` so that an absolute path is
|
|
51
|
+
* taken as given instead of being appended to the working directory.
|
|
52
52
|
* <br/>
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* NOTE: This runs while the singleton is being constructed, which is to say at require time. A bad provider name
|
|
54
|
+
* therefore fails the process immediately rather than at the first cache call - which is the point: a deployment
|
|
55
|
+
* pointed at a backend that does not exist should not reach the code that assumes one.
|
|
56
56
|
*
|
|
57
57
|
* @method
|
|
58
|
-
* @param {string
|
|
59
|
-
* @
|
|
60
|
-
* @
|
|
61
|
-
* @
|
|
58
|
+
* @param {string} connectionIdentifier The identifier under which the backend's connection is observed.
|
|
59
|
+
* @returns {CacheProvider}
|
|
60
|
+
* @throws {TiException.E_GEN_INVALID_ARGUMENT_TYPE} If the configured module does not export a {@link CacheProvider}.
|
|
61
|
+
* @public
|
|
62
62
|
*/
|
|
63
|
-
function
|
|
64
|
-
let
|
|
63
|
+
function createConfiguredProvider( connectionIdentifier ) {
|
|
64
|
+
let selected = config.getSetting( config.setting.MEMORY_CACHE_PROVIDER, "redis" );
|
|
65
|
+
|
|
66
|
+
if ( selected === "redis" ) {
|
|
67
|
+
return new RedisCacheProvider( connectionIdentifier );
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let ProviderClass;
|
|
71
|
+
try {
|
|
72
|
+
ProviderClass = require( path.resolve( process.cwd(), selected ) );
|
|
73
|
+
} catch ( error ) {
|
|
74
|
+
throw exceptions.raise( exceptions.exceptionCode.E_GEN_INVALID_ARGUMENT_TYPE, {
|
|
75
|
+
details: `Could not load the cache provider configured as '${ selected }': ${ error.message }`
|
|
76
|
+
} );
|
|
77
|
+
}
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
79
|
+
if ( isCacheProviderClass( ProviderClass ) === false ) {
|
|
80
|
+
throw exceptions.raise( exceptions.exceptionCode.E_GEN_INVALID_ARGUMENT_TYPE, {
|
|
81
|
+
details: `The cache provider configured as '${ selected }' does not export a class extending CacheProvider.`
|
|
82
|
+
} );
|
|
83
|
+
}
|
|
70
84
|
|
|
71
|
-
return
|
|
85
|
+
return new ProviderClass( connectionIdentifier );
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Determines which of the required capabilities a backend does not provide.
|
|
90
|
+
* <br/>
|
|
91
|
+
* NOTE: This lives outside the class, and is exported, for the same reason the Redis decoders are: it is the pure half
|
|
92
|
+
* of the reconciliation, and the half that decides whether an instance starts.
|
|
93
|
+
*
|
|
94
|
+
* @method
|
|
95
|
+
* @param {string[]} [required] Capabilities the application declared it needs.
|
|
96
|
+
* @param {string[]} [available] Capabilities the backend reports it provides.
|
|
97
|
+
* @returns {string[]} The required capabilities that are absent, in the order they were required.
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
function findMissingCapabilities( required, available ) {
|
|
101
|
+
let provided = new Set( Array.isArray( available ) ? available : [] );
|
|
102
|
+
return _.filter( Array.isArray( required ) ? required : [], ( capability ) => provided.has( capability ) === false );
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
/**
|
|
75
106
|
* Used to create and/or return a Common Memory Cache singleton instance.
|
|
107
|
+
* <br/>
|
|
108
|
+
* NOTE: This owns the cache's operational state and the connection observation around it; where the values actually
|
|
109
|
+
* live is the {@link CacheProvider}'s business. Every method here checks that the cache is usable and then delegates,
|
|
110
|
+
* which is why no provider repeats that check.
|
|
76
111
|
*
|
|
77
112
|
* @class CommonMemoryCache
|
|
78
113
|
* @extends ConnectionObserver
|
|
@@ -82,7 +117,8 @@ function mapCommandValues( keys, rawResults ) {
|
|
|
82
117
|
class CommonMemoryCache extends ConnectionObserver {
|
|
83
118
|
|
|
84
119
|
static #instance = null;
|
|
85
|
-
|
|
120
|
+
/** @type CacheProvider */
|
|
121
|
+
#provider = null;
|
|
86
122
|
#isOperational = false;
|
|
87
123
|
#connectionIdentifier = "system-cache";
|
|
88
124
|
|
|
@@ -94,8 +130,8 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
94
130
|
super();
|
|
95
131
|
|
|
96
132
|
if ( !CommonMemoryCache.#instance ) {
|
|
97
|
-
this.#
|
|
98
|
-
this.#
|
|
133
|
+
this.#provider = createConfiguredProvider( this.#connectionIdentifier );
|
|
134
|
+
this.#provider.addConnectionObserver( this );
|
|
99
135
|
|
|
100
136
|
CommonMemoryCache.#instance = this;
|
|
101
137
|
}
|
|
@@ -126,21 +162,56 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
126
162
|
return this.#connectionIdentifier;
|
|
127
163
|
}
|
|
128
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Property returning the optional behaviors the configured backend provides.
|
|
167
|
+
* <br/>
|
|
168
|
+
* NOTE: Accurate only once {@link CommonMemoryCache#initialize} has resolved — some capabilities cannot be
|
|
169
|
+
* established until the backend has connected.
|
|
170
|
+
*
|
|
171
|
+
* @property
|
|
172
|
+
* @returns {string[]} Values drawn from {@link TiCacheCapability}.
|
|
173
|
+
* @public
|
|
174
|
+
*/
|
|
175
|
+
get capabilities() {
|
|
176
|
+
return this.#provider.capabilities;
|
|
177
|
+
}
|
|
178
|
+
|
|
129
179
|
/**
|
|
130
180
|
* Used to initialize the cache service.
|
|
181
|
+
* <br/>
|
|
182
|
+
* NOTE: Once the backend is connected, the capabilities it reports are reconciled against the
|
|
183
|
+
* 'memoryCache.requiredCapabilities' setting, and startup fails if any of them is missing. That is deliberate: a
|
|
184
|
+
* backend silently lacking a behavior the application depends on is otherwise discovered from inside a request,
|
|
185
|
+
* long after the deployment that introduced it.
|
|
186
|
+
* <br/>
|
|
187
|
+
* NOTE: A failed reconciliation rolls the cache back to non-operational and shuts the backend down before it
|
|
188
|
+
* rejects, so a refused startup never leaves a usable cache behind.
|
|
131
189
|
*
|
|
132
190
|
* @method
|
|
133
191
|
* @returns {Promise}
|
|
192
|
+
* @throws {TiException.E_GEN_FEATURE_UNSUPPORTED} If the backend does not provide every required capability.
|
|
134
193
|
* @public
|
|
135
194
|
*/
|
|
136
195
|
initialize() {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
196
|
+
return this.#provider.initialize().then( () => {
|
|
197
|
+
try {
|
|
198
|
+
this.#verifyRequiredCapabilities();
|
|
199
|
+
} catch ( error ) {
|
|
200
|
+
// The backend is already connected and this cache already operational by the time the check runs:
|
|
201
|
+
// the Redis client notifies its connection observers from inside its "ready" handler, before
|
|
202
|
+
// `initialize()` resolves, and `onConnectionRecovered` sets `#isOperational` to true. Rejecting
|
|
203
|
+
// without undoing that would leave the singleton reporting an operational cache over a live
|
|
204
|
+
// connection while its caller has been told that startup failed - `ServiceInstance.onStart` only
|
|
205
|
+
// propagates the rejection, and `shutDown()` is reached from `stop()`, which a failed start never
|
|
206
|
+
// gets to. So the rollback belongs here, where the failure is raised.
|
|
207
|
+
this.#isOperational = false;
|
|
208
|
+
return this.#provider.shutDown().catch( () => {
|
|
209
|
+
// A backend that cannot close cleanly must not replace the reason startup was refused.
|
|
210
|
+
} ).then( () => {
|
|
211
|
+
throw error;
|
|
212
|
+
} );
|
|
213
|
+
}
|
|
214
|
+
} );
|
|
144
215
|
}
|
|
145
216
|
|
|
146
217
|
/**
|
|
@@ -151,7 +222,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
151
222
|
* @public
|
|
152
223
|
*/
|
|
153
224
|
shutDown() {
|
|
154
|
-
return this.#
|
|
225
|
+
return this.#provider.shutDown();
|
|
155
226
|
}
|
|
156
227
|
|
|
157
228
|
/**
|
|
@@ -200,14 +271,14 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
200
271
|
}
|
|
201
272
|
|
|
202
273
|
/**
|
|
203
|
-
* Used to register a new {@link ConnectionObserver} for events related to the underlying
|
|
274
|
+
* Used to register a new {@link ConnectionObserver} for events related to the underlying backend connection state.
|
|
204
275
|
*
|
|
205
276
|
* @method
|
|
206
277
|
* @param {ConnectionObserver} connectionObserver The {@link ConnectionObserver} that will be notified of any changes.
|
|
207
278
|
* @public
|
|
208
279
|
*/
|
|
209
280
|
addConnectionObserver( connectionObserver ) {
|
|
210
|
-
this.#
|
|
281
|
+
this.#provider.addConnectionObserver( connectionObserver );
|
|
211
282
|
}
|
|
212
283
|
|
|
213
284
|
/**
|
|
@@ -219,19 +290,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
219
290
|
* @public
|
|
220
291
|
*/
|
|
221
292
|
matchKeys( pattern ) {
|
|
222
|
-
return
|
|
223
|
-
if ( this.#isOperational === true ) {
|
|
224
|
-
let commandKeys = [ redis.cacheCommands.KEYS, pattern ];
|
|
225
|
-
this.#redisClient.executeCommands( [ commandKeys ] ).then( ( results ) => {
|
|
226
|
-
results = results[ 0 ];
|
|
227
|
-
resolve( ( results && results.length > 1 ) ? results[ 1 ] : [] );
|
|
228
|
-
} ).catch( ( error ) => {
|
|
229
|
-
reject( error );
|
|
230
|
-
} );
|
|
231
|
-
} else {
|
|
232
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
233
|
-
}
|
|
234
|
-
} );
|
|
293
|
+
return this.#guarded( () => this.#provider.matchKeys( pattern ) );
|
|
235
294
|
}
|
|
236
295
|
|
|
237
296
|
/**
|
|
@@ -245,26 +304,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
245
304
|
* @public
|
|
246
305
|
*/
|
|
247
306
|
setValue( key, value, expiration ) {
|
|
248
|
-
return
|
|
249
|
-
if ( this.#isOperational === true ) {
|
|
250
|
-
if ( value ) {
|
|
251
|
-
let commandSetValue = [ redis.cacheCommands.SET_VALUE, key, tools.stringifyJSON( value ) ];
|
|
252
|
-
if ( expiration ) {
|
|
253
|
-
commandSetValue.push( "EX" );
|
|
254
|
-
commandSetValue.push( expiration );
|
|
255
|
-
}
|
|
256
|
-
this.#redisClient.executeCommands( [ commandSetValue ] ).then( () => {
|
|
257
|
-
resolve( value );
|
|
258
|
-
} ).catch( ( error ) => {
|
|
259
|
-
reject( error );
|
|
260
|
-
} );
|
|
261
|
-
} else {
|
|
262
|
-
resolve( value );
|
|
263
|
-
}
|
|
264
|
-
} else {
|
|
265
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
266
|
-
}
|
|
267
|
-
} );
|
|
307
|
+
return this.#guarded( () => this.#provider.setValue( key, value, expiration ) );
|
|
268
308
|
}
|
|
269
309
|
|
|
270
310
|
/**
|
|
@@ -278,31 +318,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
278
318
|
* @public
|
|
279
319
|
*/
|
|
280
320
|
setValues( keyValues, prefix, expiration ) {
|
|
281
|
-
return
|
|
282
|
-
if ( this.#isOperational === true ) {
|
|
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
|
-
} else {
|
|
303
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
304
|
-
}
|
|
305
|
-
} );
|
|
321
|
+
return this.#guarded( () => this.#provider.setValues( keyValues, prefix, expiration ) );
|
|
306
322
|
}
|
|
307
323
|
|
|
308
324
|
/**
|
|
@@ -314,18 +330,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
314
330
|
* @public
|
|
315
331
|
*/
|
|
316
332
|
getValue( key ) {
|
|
317
|
-
return
|
|
318
|
-
if ( this.#isOperational === true ) {
|
|
319
|
-
let commandGetValue = [ redis.cacheCommands.GET_VALUE, key ];
|
|
320
|
-
this.#redisClient.executeCommands( [ commandGetValue ] ).then( ( results ) => {
|
|
321
|
-
resolve( decodeCommandValue( results ? results[ 0 ] : undefined ) );
|
|
322
|
-
} ).catch( ( error ) => {
|
|
323
|
-
reject( error );
|
|
324
|
-
} );
|
|
325
|
-
} else {
|
|
326
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
327
|
-
}
|
|
328
|
-
} );
|
|
333
|
+
return this.#guarded( () => this.#provider.getValue( key ) );
|
|
329
334
|
}
|
|
330
335
|
|
|
331
336
|
/**
|
|
@@ -338,21 +343,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
338
343
|
* @public
|
|
339
344
|
*/
|
|
340
345
|
getValues( keys, prefix ) {
|
|
341
|
-
return
|
|
342
|
-
if ( this.#isOperational === true ) {
|
|
343
|
-
let commands = [];
|
|
344
|
-
_.forEach( keys, ( key ) => {
|
|
345
|
-
commands.push( [ redis.cacheCommands.GET_VALUE, ( ( prefix ) ? prefix : "" ) + key ] );
|
|
346
|
-
} );
|
|
347
|
-
this.#redisClient.executeCommands( commands ).then( ( rawResults ) => {
|
|
348
|
-
resolve( mapCommandValues( keys, rawResults ) );
|
|
349
|
-
} ).catch( ( error ) => {
|
|
350
|
-
reject( error );
|
|
351
|
-
} );
|
|
352
|
-
} else {
|
|
353
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
354
|
-
}
|
|
355
|
-
} );
|
|
346
|
+
return this.#guarded( () => this.#provider.getValues( keys, prefix ) );
|
|
356
347
|
}
|
|
357
348
|
|
|
358
349
|
/**
|
|
@@ -364,19 +355,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
364
355
|
* @public
|
|
365
356
|
*/
|
|
366
357
|
deleteValue( key ) {
|
|
367
|
-
return
|
|
368
|
-
if ( this.#isOperational === true ) {
|
|
369
|
-
let commandDeleteValue = [ redis.cacheCommands.DELETE_VALUE, key ];
|
|
370
|
-
this.#redisClient.executeCommands( [ commandDeleteValue ] ).then( ( results ) => {
|
|
371
|
-
results = results[ 0 ];
|
|
372
|
-
resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
|
|
373
|
-
} ).catch( ( error ) => {
|
|
374
|
-
reject( error );
|
|
375
|
-
} );
|
|
376
|
-
} else {
|
|
377
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
378
|
-
}
|
|
379
|
-
} );
|
|
358
|
+
return this.#guarded( () => this.#provider.deleteValue( key ) );
|
|
380
359
|
}
|
|
381
360
|
|
|
382
361
|
/**
|
|
@@ -392,18 +371,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
392
371
|
* @public
|
|
393
372
|
*/
|
|
394
373
|
expireValue( key, seconds, name ) {
|
|
395
|
-
return
|
|
396
|
-
if ( this.#isOperational === true ) {
|
|
397
|
-
let commandExpire = ( name ) ? [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ] : [ redis.cacheCommands.EXPIRE, key, seconds ];
|
|
398
|
-
this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
|
|
399
|
-
resolve( seconds );
|
|
400
|
-
} ).catch( ( error ) => {
|
|
401
|
-
reject( error );
|
|
402
|
-
} );
|
|
403
|
-
} else {
|
|
404
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
405
|
-
}
|
|
406
|
-
} );
|
|
374
|
+
return this.#guarded( () => this.#provider.expireValue( key, seconds, name ) );
|
|
407
375
|
}
|
|
408
376
|
|
|
409
377
|
/**
|
|
@@ -416,24 +384,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
416
384
|
* @public
|
|
417
385
|
*/
|
|
418
386
|
listPushValue( listName, values ) {
|
|
419
|
-
return
|
|
420
|
-
if ( this.#isOperational === true ) {
|
|
421
|
-
let commandPushValues = [ redis.cacheCommands.LIST_PUSH, listName ];
|
|
422
|
-
_.forEach( values, ( value ) => {
|
|
423
|
-
if ( value ) {
|
|
424
|
-
commandPushValues.push( tools.stringifyJSON( value ) );
|
|
425
|
-
}
|
|
426
|
-
} );
|
|
427
|
-
this.#redisClient.executeCommands( [ commandPushValues ] ).then( ( results ) => {
|
|
428
|
-
results = results[ 0 ];
|
|
429
|
-
resolve( ( results && results.length > 1 ) ? results[ 1 ] : undefined );
|
|
430
|
-
} ).catch( ( error ) => {
|
|
431
|
-
reject( error );
|
|
432
|
-
} );
|
|
433
|
-
} else {
|
|
434
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
435
|
-
}
|
|
436
|
-
} );
|
|
387
|
+
return this.#guarded( () => this.#provider.listPushValue( listName, values ) );
|
|
437
388
|
}
|
|
438
389
|
|
|
439
390
|
/**
|
|
@@ -446,18 +397,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
446
397
|
* @public
|
|
447
398
|
*/
|
|
448
399
|
addToSet( key, value ) {
|
|
449
|
-
return
|
|
450
|
-
if ( this.#isOperational === true ) {
|
|
451
|
-
let commandAddToSet = [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( value ) ];
|
|
452
|
-
this.#redisClient.executeCommands( [ commandAddToSet ] ).then( () => {
|
|
453
|
-
resolve();
|
|
454
|
-
} ).catch( ( error ) => {
|
|
455
|
-
reject( error );
|
|
456
|
-
} );
|
|
457
|
-
} else {
|
|
458
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
459
|
-
}
|
|
460
|
-
} );
|
|
400
|
+
return this.#guarded( () => this.#provider.addToSet( key, value ) );
|
|
461
401
|
}
|
|
462
402
|
|
|
463
403
|
/**
|
|
@@ -472,21 +412,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
472
412
|
* @public
|
|
473
413
|
*/
|
|
474
414
|
addToSetMulti( keys, values ) {
|
|
475
|
-
return
|
|
476
|
-
if ( this.#isOperational === true ) {
|
|
477
|
-
let commands = [];
|
|
478
|
-
_.forEach( keys, ( key, idx ) => {
|
|
479
|
-
commands.push( [ redis.cacheCommands.ADD_TO_SET, key, tools.stringifyJSON( values[ idx ] ) ] );
|
|
480
|
-
} );
|
|
481
|
-
this.#redisClient.executeCommands( commands ).then( () => {
|
|
482
|
-
resolve();
|
|
483
|
-
} ).catch( ( error ) => {
|
|
484
|
-
reject( error );
|
|
485
|
-
} );
|
|
486
|
-
} else {
|
|
487
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
488
|
-
}
|
|
489
|
-
} );
|
|
415
|
+
return this.#guarded( () => this.#provider.addToSetMulti( keys, values ) );
|
|
490
416
|
}
|
|
491
417
|
|
|
492
418
|
/**
|
|
@@ -499,20 +425,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
499
425
|
* @public
|
|
500
426
|
*/
|
|
501
427
|
isSetMember( setName, value ) {
|
|
502
|
-
return
|
|
503
|
-
if ( this.#isOperational === true ) {
|
|
504
|
-
let commandIsSetMember = [ redis.cacheCommands.IS_SET_MEMBER, setName, value ];
|
|
505
|
-
this.#redisClient.executeCommands( [ commandIsSetMember ] ).then( ( results ) => {
|
|
506
|
-
results = results[ 0 ];
|
|
507
|
-
let result = !!( results && results.length > 1 && results[ 1 ] === 1 );
|
|
508
|
-
resolve( result );
|
|
509
|
-
} ).catch( ( error ) => {
|
|
510
|
-
reject( error );
|
|
511
|
-
} );
|
|
512
|
-
} else {
|
|
513
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
514
|
-
}
|
|
515
|
-
} );
|
|
428
|
+
return this.#guarded( () => this.#provider.isSetMember( setName, value ) );
|
|
516
429
|
}
|
|
517
430
|
|
|
518
431
|
/**
|
|
@@ -524,20 +437,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
524
437
|
* @public
|
|
525
438
|
*/
|
|
526
439
|
membersOfSet( key ) {
|
|
527
|
-
return
|
|
528
|
-
if ( this.#isOperational === true ) {
|
|
529
|
-
let commandMembersOfSet = [ redis.cacheCommands.GET_ALL_FROM_SET, key ];
|
|
530
|
-
this.#redisClient.executeCommands( [ commandMembersOfSet ] ).then( ( results ) => {
|
|
531
|
-
results = results[ 0 ];
|
|
532
|
-
let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
|
|
533
|
-
resolve( parsedResults );
|
|
534
|
-
} ).catch( ( error ) => {
|
|
535
|
-
reject( error );
|
|
536
|
-
} );
|
|
537
|
-
} else {
|
|
538
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
539
|
-
}
|
|
540
|
-
} );
|
|
440
|
+
return this.#guarded( () => this.#provider.membersOfSet( key ) );
|
|
541
441
|
}
|
|
542
442
|
|
|
543
443
|
/**
|
|
@@ -549,20 +449,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
549
449
|
* @public
|
|
550
450
|
*/
|
|
551
451
|
unionOfSets( keys ) {
|
|
552
|
-
return
|
|
553
|
-
if ( this.#isOperational === true ) {
|
|
554
|
-
let commandUnionOfSets = _.concat( [ redis.cacheCommands.UNION_OF_SETS ], keys );
|
|
555
|
-
this.#redisClient.executeCommands( [ commandUnionOfSets ] ).then( ( results ) => {
|
|
556
|
-
results = results[ 0 ];
|
|
557
|
-
let parsedResults = ( results && results.length > 1 && results[ 1 ] ) ? results[ 1 ] : [];
|
|
558
|
-
resolve( parsedResults );
|
|
559
|
-
} ).catch( ( error ) => {
|
|
560
|
-
reject( error );
|
|
561
|
-
} );
|
|
562
|
-
} else {
|
|
563
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
564
|
-
}
|
|
565
|
-
} );
|
|
452
|
+
return this.#guarded( () => this.#provider.unionOfSets( keys ) );
|
|
566
453
|
}
|
|
567
454
|
|
|
568
455
|
/**
|
|
@@ -577,18 +464,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
577
464
|
* @public
|
|
578
465
|
*/
|
|
579
466
|
hashSetField( key, name, value ) {
|
|
580
|
-
return
|
|
581
|
-
if ( this.#isOperational === true ) {
|
|
582
|
-
let commandHashSetField = [ redis.cacheCommands.HASH_SET, key, name, tools.stringifyJSON( value ) ];
|
|
583
|
-
this.#redisClient.executeCommands( [ commandHashSetField ] ).then( () => {
|
|
584
|
-
resolve();
|
|
585
|
-
} ).catch( ( error ) => {
|
|
586
|
-
reject( error );
|
|
587
|
-
} );
|
|
588
|
-
} else {
|
|
589
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
590
|
-
}
|
|
591
|
-
} );
|
|
467
|
+
return this.#guarded( () => this.#provider.hashSetField( key, name, value ) );
|
|
592
468
|
}
|
|
593
469
|
|
|
594
470
|
/**
|
|
@@ -604,22 +480,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
604
480
|
* @public
|
|
605
481
|
*/
|
|
606
482
|
hashSetFields( key, fields ) {
|
|
607
|
-
return
|
|
608
|
-
if ( this.#isOperational === true ) {
|
|
609
|
-
let commandHashSetFields = [ redis.cacheCommands.HASH_SET, key ];
|
|
610
|
-
_.forEach( fields, ( field ) => {
|
|
611
|
-
commandHashSetFields.push( field.name );
|
|
612
|
-
commandHashSetFields.push( tools.stringifyJSON( field.value ) );
|
|
613
|
-
} );
|
|
614
|
-
this.#redisClient.executeCommands( [ commandHashSetFields ] ).then( () => {
|
|
615
|
-
resolve();
|
|
616
|
-
} ).catch( ( error ) => {
|
|
617
|
-
reject( error );
|
|
618
|
-
} );
|
|
619
|
-
} else {
|
|
620
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
621
|
-
}
|
|
622
|
-
} );
|
|
483
|
+
return this.#guarded( () => this.#provider.hashSetFields( key, fields ) );
|
|
623
484
|
}
|
|
624
485
|
|
|
625
486
|
/**
|
|
@@ -632,19 +493,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
632
493
|
* @public
|
|
633
494
|
*/
|
|
634
495
|
hashGetField( key, field ) {
|
|
635
|
-
return
|
|
636
|
-
if ( this.#isOperational === true ) {
|
|
637
|
-
let commandHashGetField = [ redis.cacheCommands.HASH_GET, key, field ];
|
|
638
|
-
this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
|
|
639
|
-
results = results[ 0 ];
|
|
640
|
-
resolve( ( results && results.length > 1 && _.isString( results[ 1 ] ) ) ? tools.parseJSON( results[ 1 ] ) : null );
|
|
641
|
-
} ).catch( ( error ) => {
|
|
642
|
-
reject( error );
|
|
643
|
-
} );
|
|
644
|
-
} else {
|
|
645
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
646
|
-
}
|
|
647
|
-
} );
|
|
496
|
+
return this.#guarded( () => this.#provider.hashGetField( key, field ) );
|
|
648
497
|
}
|
|
649
498
|
|
|
650
499
|
/**
|
|
@@ -657,19 +506,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
657
506
|
* @public
|
|
658
507
|
*/
|
|
659
508
|
hashDeleteField( key, field ) {
|
|
660
|
-
return
|
|
661
|
-
if ( this.#isOperational === true ) {
|
|
662
|
-
let commandHashGetField = [ redis.cacheCommands.HASH_REMOVE, key, field ];
|
|
663
|
-
this.#redisClient.executeCommands( [ commandHashGetField ] ).then( ( results ) => {
|
|
664
|
-
results = results[ 0 ];
|
|
665
|
-
resolve( ( results && results.length > 1 ) ? tools.toBool( results[ 1 ] ) : false );
|
|
666
|
-
} ).catch( ( error ) => {
|
|
667
|
-
reject( error );
|
|
668
|
-
} );
|
|
669
|
-
} else {
|
|
670
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
671
|
-
}
|
|
672
|
-
} );
|
|
509
|
+
return this.#guarded( () => this.#provider.hashDeleteField( key, field ) );
|
|
673
510
|
}
|
|
674
511
|
|
|
675
512
|
/**
|
|
@@ -687,25 +524,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
687
524
|
* @public
|
|
688
525
|
*/
|
|
689
526
|
setJSON( key, value, path = "$", overrideMode = 0 ) {
|
|
690
|
-
return
|
|
691
|
-
if ( this.#isOperational === true ) {
|
|
692
|
-
if ( this.#redisClient.isJSONSupported ) {
|
|
693
|
-
let commandArguments = [ redis.cacheCommands.JSON_SET, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
|
|
694
|
-
if ( overrideMode !== 0 ) {
|
|
695
|
-
commandArguments.push( overrideMode === 1 ? redis.cacheOverrideMode.NX : redis.cacheOverrideMode.XX );
|
|
696
|
-
}
|
|
697
|
-
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
698
|
-
resolve();
|
|
699
|
-
} ).catch( ( error ) => {
|
|
700
|
-
reject( error );
|
|
701
|
-
} );
|
|
702
|
-
} else {
|
|
703
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
704
|
-
}
|
|
705
|
-
} else {
|
|
706
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
707
|
-
}
|
|
708
|
-
} );
|
|
527
|
+
return this.#guarded( () => this.#provider.setJSON( key, value, path, overrideMode ) );
|
|
709
528
|
}
|
|
710
529
|
|
|
711
530
|
/**
|
|
@@ -720,22 +539,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
720
539
|
* @public
|
|
721
540
|
*/
|
|
722
541
|
getJSON( key, path = "$" ) {
|
|
723
|
-
return
|
|
724
|
-
if ( this.#isOperational === true ) {
|
|
725
|
-
if ( this.#redisClient.isJSONSupported ) {
|
|
726
|
-
let commandArguments = [ redis.cacheCommands.JSON_GET, key, this.#normalizeJSONPath( path ) ];
|
|
727
|
-
this.#redisClient.callCommand( commandArguments ).then( ( result ) => {
|
|
728
|
-
resolve( result != null ? tools.parseJSON( String( result ) ) : null );
|
|
729
|
-
} ).catch( ( error ) => {
|
|
730
|
-
reject( error );
|
|
731
|
-
} );
|
|
732
|
-
} else {
|
|
733
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
734
|
-
}
|
|
735
|
-
} else {
|
|
736
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
737
|
-
}
|
|
738
|
-
} );
|
|
542
|
+
return this.#guarded( () => this.#provider.getJSON( key, path ) );
|
|
739
543
|
}
|
|
740
544
|
|
|
741
545
|
/**
|
|
@@ -751,22 +555,7 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
751
555
|
* @public
|
|
752
556
|
*/
|
|
753
557
|
editJSON( key, value, path = "$" ) {
|
|
754
|
-
return
|
|
755
|
-
if ( this.#isOperational === true ) {
|
|
756
|
-
if ( this.#redisClient.isJSONSupported ) {
|
|
757
|
-
let commandArguments = [ redis.cacheCommands.JSON_MERGE, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
|
|
758
|
-
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
759
|
-
resolve();
|
|
760
|
-
} ).catch( ( error ) => {
|
|
761
|
-
reject( error );
|
|
762
|
-
} );
|
|
763
|
-
} else {
|
|
764
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
765
|
-
}
|
|
766
|
-
} else {
|
|
767
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
768
|
-
}
|
|
769
|
-
} );
|
|
558
|
+
return this.#guarded( () => this.#provider.editJSON( key, value, path ) );
|
|
770
559
|
}
|
|
771
560
|
|
|
772
561
|
/**
|
|
@@ -782,41 +571,45 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
782
571
|
* @public
|
|
783
572
|
*/
|
|
784
573
|
arrayAppendJSON( key, value, path = "$" ) {
|
|
785
|
-
return
|
|
786
|
-
if ( this.#isOperational === true ) {
|
|
787
|
-
if ( this.#redisClient.isJSONSupported ) {
|
|
788
|
-
let commandArguments = [ redis.cacheCommands.JSON_ARRAY_APPEND, key, this.#normalizeJSONPath( path ), tools.stringifyJSON( value ) ];
|
|
789
|
-
this.#redisClient.callCommand( commandArguments ).then( () => {
|
|
790
|
-
resolve();
|
|
791
|
-
} ).catch( ( error ) => {
|
|
792
|
-
reject( error );
|
|
793
|
-
} );
|
|
794
|
-
} else {
|
|
795
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, { details: "No RedisJSON module installed on server." } ) );
|
|
796
|
-
}
|
|
797
|
-
} else {
|
|
798
|
-
reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
799
|
-
}
|
|
800
|
-
} );
|
|
574
|
+
return this.#guarded( () => this.#provider.arrayAppendJSON( key, value, path ) );
|
|
801
575
|
}
|
|
802
576
|
|
|
803
577
|
/* Private interface */
|
|
804
578
|
|
|
805
579
|
/**
|
|
806
|
-
* Used to
|
|
580
|
+
* Used to reject an operation when the cache is not usable, and to delegate it to the backend when it is.
|
|
807
581
|
* <br/>
|
|
808
|
-
* NOTE:
|
|
809
|
-
* which
|
|
582
|
+
* NOTE: This exists so the check lives in exactly one place. It previously stood at the head of all twenty-one
|
|
583
|
+
* data methods, which is twenty-one chances for a new method to be added without it.
|
|
810
584
|
*
|
|
811
585
|
* @method
|
|
812
|
-
* @param {
|
|
813
|
-
* @returns {
|
|
586
|
+
* @param {function(): Promise} operation
|
|
587
|
+
* @returns {Promise}
|
|
814
588
|
*/
|
|
815
|
-
#
|
|
816
|
-
|
|
817
|
-
|
|
589
|
+
#guarded( operation ) {
|
|
590
|
+
return ( this.#isOperational === true )
|
|
591
|
+
? operation()
|
|
592
|
+
: Promise.reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Used to reconcile what the application requires against what the backend reports it can do.
|
|
597
|
+
*
|
|
598
|
+
* @method
|
|
599
|
+
* @throws {TiException.E_GEN_FEATURE_UNSUPPORTED} If any required capability is absent.
|
|
600
|
+
*/
|
|
601
|
+
#verifyRequiredCapabilities() {
|
|
602
|
+
let required = config.getSetting( config.setting.MEMORY_CACHE_REQUIRED_CAPABILITIES, [] );
|
|
603
|
+
if ( _.isEmpty( required ) === true ) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let missing = findMissingCapabilities( required, this.#provider.capabilities );
|
|
608
|
+
if ( missing.length > 0 ) {
|
|
609
|
+
throw exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED, {
|
|
610
|
+
details: `The configured cache backend '${ this.#provider.constructor.name }' does not provide the required ${ ( missing.length === 1 ) ? "capability" : "capabilities" }: ${ missing.join( ", " ) }.`
|
|
611
|
+
} );
|
|
818
612
|
}
|
|
819
|
-
return ( path.startsWith( "$" ) === false ) ? ( "$." + path ) : path;
|
|
820
613
|
}
|
|
821
614
|
|
|
822
615
|
}
|
|
@@ -824,7 +617,16 @@ class CommonMemoryCache extends ConnectionObserver {
|
|
|
824
617
|
const instance = new CommonMemoryCache();
|
|
825
618
|
module.exports.instance = Object.freeze( instance );
|
|
826
619
|
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
module.exports.
|
|
620
|
+
// Re-exported from the Redis backend, where these now live along with the rest of the Redis-specific decoding. They
|
|
621
|
+
// stay on this module because it is the published entry point and removing them would break any consumer that reaches
|
|
622
|
+
// for them - including this package's own `cache-get-values` suite.
|
|
623
|
+
module.exports.decodeCommandValue = RedisCacheProvider.decodeCommandValue;
|
|
624
|
+
module.exports.mapCommandValues = RedisCacheProvider.mapCommandValues;
|
|
625
|
+
|
|
626
|
+
// Re-exported so a consumer can name a capability without reaching past this module's exports map.
|
|
627
|
+
module.exports.cacheCapability = cacheCapability;
|
|
628
|
+
|
|
629
|
+
// Exported for testing, per the notes on the functions themselves.
|
|
630
|
+
module.exports.findMissingCapabilities = findMissingCapabilities;
|
|
631
|
+
module.exports.createConfiguredProvider = createConfiguredProvider;
|
|
632
|
+
module.exports.isCacheProviderClass = isCacheProviderClass;
|