cds-caching 0.3.3 → 1.1.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/README.md +691 -278
- package/cds-plugin.js +2 -2
- package/index.cds +165 -20
- package/lib/CachingService.d.ts +328 -0
- package/lib/CachingService.js +373 -0
- package/lib/operations/AsyncOperations.js +335 -0
- package/lib/operations/BasicOperations.js +211 -0
- package/lib/operations/CapOperations.js +540 -0
- package/lib/support/CacheStatisticsHandler.js +1124 -0
- package/lib/support/CacheStoreManager.js +120 -0
- package/lib/support/KeyManager.js +132 -0
- package/lib/support/RuntimeConfigurationManager.js +148 -0
- package/lib/support/StatisticsPersistenceManager.js +375 -0
- package/lib/support/TagResolver.js +125 -0
- package/lib/util.js +201 -0
- package/package.json +18 -13
- package/srv/caching-api-service.js +124 -0
- package/srv/CacheStatisticsHandler.js +0 -213
- package/srv/CachingService.js +0 -587
- package/srv/statistics-service.cds +0 -37
- package/srv/statistics-service.js +0 -72
- package/srv/util.js +0 -101
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const CacheStoreManager = require('./support/CacheStoreManager');
|
|
3
|
+
const KeyManager = require('./support/KeyManager');
|
|
4
|
+
const TagResolver = require('./support/TagResolver');
|
|
5
|
+
const RuntimeConfigurationManager = require('./support/RuntimeConfigurationManager');
|
|
6
|
+
const CacheStatisticsHandler = require('./support/CacheStatisticsHandler');
|
|
7
|
+
const BasicOperations = require('./operations/BasicOperations');
|
|
8
|
+
const CapOperations = require('./operations/CapOperations');
|
|
9
|
+
const AsyncOperations = require('./operations/AsyncOperations');
|
|
10
|
+
|
|
11
|
+
class CachingService extends cds.Service {
|
|
12
|
+
|
|
13
|
+
async init() {
|
|
14
|
+
super.init()
|
|
15
|
+
this.log = cds.log('cds-caching')
|
|
16
|
+
this.options = this.options || {
|
|
17
|
+
store: null,
|
|
18
|
+
compression: null,
|
|
19
|
+
credentials: {},
|
|
20
|
+
namespace: null,
|
|
21
|
+
throwOnErrors: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Initialize managers
|
|
25
|
+
this.storeManager = new CacheStoreManager();
|
|
26
|
+
this.runtimeConfigManager = new RuntimeConfigurationManager(this.name, this.log, this.options);
|
|
27
|
+
this.keyManager = new KeyManager(this.runtimeConfigManager);
|
|
28
|
+
this.tagResolver = new TagResolver();
|
|
29
|
+
|
|
30
|
+
// Create cache store
|
|
31
|
+
const { cache, cleanup } = this.storeManager.createStore(this.options, this.name);
|
|
32
|
+
this.cache = cache;
|
|
33
|
+
this.log.info(`Caching service ${this.name} initialized with namespace ${this.options.namespace || this.name}`);
|
|
34
|
+
|
|
35
|
+
// Set up cleanup on shutdown
|
|
36
|
+
cds.once("shutdown", cleanup);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Internal event handlers
|
|
40
|
+
*/
|
|
41
|
+
const handleSet = async (event) => {
|
|
42
|
+
this.log.debug(`SET ${event.data.key}`);
|
|
43
|
+
if (typeof event.data.value === "object") {
|
|
44
|
+
event.data.value = JSON.stringify(event.data.value);
|
|
45
|
+
}
|
|
46
|
+
await this.cache.set(event.data.key, event.data.value, (event.data.ttl || 0))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleGet = async (event) => {
|
|
50
|
+
const value = await this.cache.get(event.data.key);
|
|
51
|
+
this.log.debug(`GET ${event.data.key}`);
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(value);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleDelete = async (event) => {
|
|
63
|
+
this.log.debug(`DELETE ${event.data.key}`);
|
|
64
|
+
await this.cache.delete(event.data.key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const handleClear = async (event) => {
|
|
68
|
+
this.log.debug(`CLEAR`);
|
|
69
|
+
await this.cache.clear();
|
|
70
|
+
|
|
71
|
+
// Also clear statistics
|
|
72
|
+
if (this.statistics) {
|
|
73
|
+
await this.statistics.resetCurrentStats();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await this.statistics.deleteMetrics();
|
|
77
|
+
await this.statistics.deleteKeyMetrics();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
this.log.warn(`Failed to clear statistics for cache ${this.name}:`, error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Those support before/after hooks
|
|
85
|
+
this.on('SET', handleSet.bind(this));
|
|
86
|
+
this.on('GET', handleGet.bind(this));
|
|
87
|
+
this.on('DELETE', handleDelete.bind(this));
|
|
88
|
+
this.on('CLEAR', handleClear.bind(this));
|
|
89
|
+
|
|
90
|
+
const config = await this.runtimeConfigManager.getRuntimeConfiguration();
|
|
91
|
+
|
|
92
|
+
// Initialize statistics with runtime configuration support
|
|
93
|
+
this.statistics = new CacheStatisticsHandler({
|
|
94
|
+
cache: this.name
|
|
95
|
+
});
|
|
96
|
+
this.statistics.enableKeyMetrics(config.keyMetricsEnabled);
|
|
97
|
+
this.statistics.enableMetrics(config.metricsEnabled);
|
|
98
|
+
|
|
99
|
+
// Initialize operation managers
|
|
100
|
+
this.basicOperations = new BasicOperations(
|
|
101
|
+
this,
|
|
102
|
+
this.keyManager,
|
|
103
|
+
this.tagResolver,
|
|
104
|
+
this.statistics,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
this.capOperations = new CapOperations(
|
|
108
|
+
this,
|
|
109
|
+
this.keyManager,
|
|
110
|
+
this.statistics,
|
|
111
|
+
this.log,
|
|
112
|
+
this.runtimeConfigManager
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
this.asyncOperations = new AsyncOperations(
|
|
116
|
+
this,
|
|
117
|
+
this.keyManager,
|
|
118
|
+
this.statistics,
|
|
119
|
+
this.runtimeConfigManager
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
// Always set up statistics hooks, but they will be controlled by the enabled flag
|
|
124
|
+
this.setupStatisticsHooks();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// PUBLIC API - Core Cache Operations
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
createKey(...args) { return this.keyManager.createKey(...args); }
|
|
132
|
+
|
|
133
|
+
async set(...args) { return this.basicOperations.set(...args); }
|
|
134
|
+
async get(...args) { return this.basicOperations.get(...args); }
|
|
135
|
+
async has(...args) { return this.basicOperations.has(...args); }
|
|
136
|
+
async delete(...args) { return this.basicOperations.delete(...args); }
|
|
137
|
+
async clear(...args) { return this.basicOperations.clear(...args); }
|
|
138
|
+
async deleteByTag(...args) { return this.basicOperations.deleteByTag(...args); }
|
|
139
|
+
async metadata(...args) { return this.basicOperations.metadata(...args); }
|
|
140
|
+
async tags(...args) { return this.basicOperations.tags(...args); }
|
|
141
|
+
async *iterator(...args) { return yield* this.basicOperations.iterator(...args); }
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// PUBLIC API - Read Through Operations
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
get rt() {
|
|
148
|
+
return {
|
|
149
|
+
send: (...args) => this.capOperations.send(...args),
|
|
150
|
+
run: (...args) => this.capOperations.run(...args),
|
|
151
|
+
wrap: (...args) => this.asyncOperations.wrap(...args),
|
|
152
|
+
exec: (...args) => this.asyncOperations.exec(...args),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// PUBLIC API - Deprecated Shortcut Read Through Methods
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @deprecated Use cache.rt.send() instead. The rt.send() method provides enhanced functionality including read-through metadata, dynamic cache keys, and detailed mode options.
|
|
162
|
+
* @param {...any} args
|
|
163
|
+
* @returns {Promise<object>} - the result of the request
|
|
164
|
+
*/
|
|
165
|
+
async send(...args) {
|
|
166
|
+
if (typeof args[0] !== "object" || !args[1].send || typeof args[1] !== "object" || args[0].method !== "GET") {
|
|
167
|
+
return super.send(...args);
|
|
168
|
+
} else {
|
|
169
|
+
const result = await this.rt.send(...args);
|
|
170
|
+
return result.result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @deprecated Use cache.rt.run() instead. The rt.run() method provides enhanced functionality including read-through metadata, dynamic cache keys, and detailed mode options.
|
|
176
|
+
* @param {...any} args
|
|
177
|
+
* @returns {Promise<object>} - the result of the request
|
|
178
|
+
*/
|
|
179
|
+
async run(...args) {
|
|
180
|
+
if (typeof args[0] !== "object" && !["Request", "NoaRequest", "ODataRequest", "cds.ql"].includes(args[0].constructor.name)) {
|
|
181
|
+
return super.run(...args);
|
|
182
|
+
} else {
|
|
183
|
+
const result = await this.rt.run(...args);
|
|
184
|
+
return result.result;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @deprecated Use cache.rt.wrap() instead. The rt.wrap() method provides enhanced functionality including read-through metadata, dynamic cache keys, and detailed mode options.
|
|
190
|
+
* @param {...any} args
|
|
191
|
+
* @returns {Promise<object>} - the result of the request
|
|
192
|
+
*/
|
|
193
|
+
wrap(...args) {
|
|
194
|
+
const wrappedFunction = this.rt.wrap(...args);
|
|
195
|
+
return async (...args) => {
|
|
196
|
+
const result = await wrappedFunction(...args);
|
|
197
|
+
return result.result;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @deprecated Use cache.rt.exec() instead. The rt.exec() method provides enhanced functionality including read-through metadata, dynamic cache keys, and detailed mode options.
|
|
203
|
+
* @param {...any} args
|
|
204
|
+
* @returns {Promise<object>} - the result of the request
|
|
205
|
+
*/
|
|
206
|
+
async exec(...args) {
|
|
207
|
+
const result = await this.rt.exec(...args);
|
|
208
|
+
return result.result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// PUBLIC API - Statistics and Configuration
|
|
213
|
+
// ============================================================================
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolve tags for a given query
|
|
218
|
+
* @param {object} query - the query to resolve tags for
|
|
219
|
+
* @returns {Promise<object>} - the resolved tags
|
|
220
|
+
*/
|
|
221
|
+
async resolveTags(...args) { return this.tagResolver.resolveTags(...args); }
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get statistics for a specific period
|
|
225
|
+
* @param {string} period - the period to get stats for
|
|
226
|
+
* @param {Date} from - start date
|
|
227
|
+
* @param {Date} to - end date
|
|
228
|
+
* @returns {Promise<object>} - the statistics
|
|
229
|
+
*/
|
|
230
|
+
async getMetrics(from, to) {
|
|
231
|
+
return this.statistics.getMetrics("hourly", from, to);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async getKeyMetrics(key, from, to) {
|
|
235
|
+
return this.statistics.getKeyMetrics(key, from, to);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get current statistics
|
|
240
|
+
* @returns {Promise<object>} - the current statistics
|
|
241
|
+
*/
|
|
242
|
+
async getCurrentMetrics() {
|
|
243
|
+
return this.statistics.getCurrentStats();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get current key metrics
|
|
248
|
+
* @returns {Promise<object>} - the current key metrics
|
|
249
|
+
*/
|
|
250
|
+
async getCurrentKeyMetrics() {
|
|
251
|
+
return this.statistics.getCurrentKeyMetrics();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Clear all metrics
|
|
258
|
+
* @returns {Promise<void>}
|
|
259
|
+
*/
|
|
260
|
+
async clearMetrics() {
|
|
261
|
+
return this.statistics.clearMetrics();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Clear key metrics
|
|
266
|
+
* @returns {Promise<void>}
|
|
267
|
+
*/
|
|
268
|
+
async clearKeyMetrics() {
|
|
269
|
+
return this.statistics.clearKeyMetrics();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Enable or disable statistics at runtime
|
|
274
|
+
* @param {boolean} enabled - whether to enable statistics
|
|
275
|
+
* @returns {Promise<void>}
|
|
276
|
+
*/
|
|
277
|
+
async setMetricsEnabled(enabled) {
|
|
278
|
+
await this.runtimeConfigManager.setMetricsEnabled(enabled);
|
|
279
|
+
this.statistics.enableMetrics(enabled);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Enable or disable key tracking at runtime
|
|
284
|
+
* @param {boolean} enabled - whether to enable key tracking
|
|
285
|
+
* @returns {Promise<void>}
|
|
286
|
+
*/
|
|
287
|
+
async setKeyMetricsEnabled(enabled) {
|
|
288
|
+
await this.runtimeConfigManager.setKeyMetricsEnabled(enabled);
|
|
289
|
+
this.statistics.enableKeyMetrics(enabled);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Persist metrics to database
|
|
294
|
+
* @returns {Promise<void>}
|
|
295
|
+
*/
|
|
296
|
+
async persistMetrics() {
|
|
297
|
+
return this.statistics.persistMetrics();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Get current runtime configuration
|
|
302
|
+
* @returns {Promise<object>} - the runtime configuration
|
|
303
|
+
*/
|
|
304
|
+
async getRuntimeConfiguration() {
|
|
305
|
+
return await this.runtimeConfigManager.getRuntimeConfiguration();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Reload runtime configuration from database
|
|
310
|
+
* @returns {Promise<void>}
|
|
311
|
+
*/
|
|
312
|
+
async reloadRuntimeConfiguration() {
|
|
313
|
+
await this.loadRuntimeConfiguration();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Add a cachable function
|
|
318
|
+
* @param {string} name - the function name
|
|
319
|
+
* @param {object} options - the cache options
|
|
320
|
+
* @param {boolean} isBound - whether the function is bound
|
|
321
|
+
*/
|
|
322
|
+
addCachableFunction(name, options, isBound = false) {
|
|
323
|
+
this.capOperations.cacheAnnotatedFunctions[isBound ? 'bound' : 'unbound'].push({ name, options });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Dispose of the service
|
|
328
|
+
* @returns {Promise<void>}
|
|
329
|
+
*/
|
|
330
|
+
async dispose() {
|
|
331
|
+
await this.statistics.dispose();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// PRIVATE METHODS
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Set up statistics hooks for all cache operations
|
|
340
|
+
* @private
|
|
341
|
+
*/
|
|
342
|
+
setupStatisticsHooks() {
|
|
343
|
+
// Enhance methods with statistics
|
|
344
|
+
this.before('GET', (req) => {
|
|
345
|
+
req.startTime = process.hrtime();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
this.before('SET', (req) => {
|
|
349
|
+
req.startTime = process.hrtime();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
this.before('DELETE', (req) => {
|
|
353
|
+
req.startTime = process.hrtime();
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Load runtime configuration from database
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
async loadRuntimeConfiguration() {
|
|
362
|
+
try {
|
|
363
|
+
const config = await this.runtimeConfigManager.loadRuntimeConfiguration();
|
|
364
|
+
this.statistics.enableKeyTracking(config.enableKeyTracking);
|
|
365
|
+
this.statistics.enableStatistics(config.enableStatistics);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
this.log.warn(`Failed to load runtime configuration for cache ${this.name}:`, error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
module.exports = CachingService;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manages async cache operations for function wrapping and execution
|
|
3
|
+
*/
|
|
4
|
+
class AsyncOperations {
|
|
5
|
+
constructor(cache, keyManager, statistics, runtimeConfigManager) {
|
|
6
|
+
this.cache = cache;
|
|
7
|
+
this.keyManager = keyManager;
|
|
8
|
+
this.statistics = statistics;
|
|
9
|
+
this.runtimeConfigManager = runtimeConfigManager;
|
|
10
|
+
this.log = console; // Default logger
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safely execute cache operations with error handling
|
|
15
|
+
* @param {Function} operation - The cache operation to execute
|
|
16
|
+
* @param {string} operationName - Name of the operation for logging
|
|
17
|
+
* @param {object} context - Context information for logging
|
|
18
|
+
* @returns {Promise<object>} - The result with error information
|
|
19
|
+
*/
|
|
20
|
+
async safeCacheOperation(operation, operationName, context = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const result = await operation();
|
|
23
|
+
return { success: true, result, error: null };
|
|
24
|
+
} catch (error) {
|
|
25
|
+
this.log.warn(`Cache ${operationName} failed:`, {
|
|
26
|
+
error: error.message,
|
|
27
|
+
stack: error.stack,
|
|
28
|
+
context: context
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
result: null,
|
|
33
|
+
error: {
|
|
34
|
+
message: error.message,
|
|
35
|
+
operation: operationName,
|
|
36
|
+
context: context
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a dynamic key for async function caching
|
|
44
|
+
* @param {string} baseKey - the base cache key
|
|
45
|
+
* @param {Array} args - function arguments
|
|
46
|
+
* @param {object} options - cache options
|
|
47
|
+
* @param {string} functionName - name of the function
|
|
48
|
+
* @returns {string} - generated cache key
|
|
49
|
+
*/
|
|
50
|
+
createDynamicKey(baseKey, args, options, functionName) {
|
|
51
|
+
// If explicit key is provided, use it
|
|
52
|
+
if (options.key) {
|
|
53
|
+
return this.keyManager.createKey(
|
|
54
|
+
{ baseKey, args, functionName },
|
|
55
|
+
{
|
|
56
|
+
baseKey,
|
|
57
|
+
args,
|
|
58
|
+
functionName,
|
|
59
|
+
user: cds.context?.user?.id,
|
|
60
|
+
tenant: cds.context?.tenant,
|
|
61
|
+
locale: cds.context?.locale
|
|
62
|
+
},
|
|
63
|
+
options.key
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Auto-generate template based on arguments
|
|
68
|
+
if (args.length === 0) {
|
|
69
|
+
return this.keyManager.createKey(baseKey);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create argument placeholders
|
|
73
|
+
const argPlaceholders = args.map((_, index) => `{args[${index}]}`).join(':');
|
|
74
|
+
const template = `${baseKey}:${argPlaceholders}`;
|
|
75
|
+
|
|
76
|
+
return this.keyManager.createKey(
|
|
77
|
+
{ baseKey, args, functionName },
|
|
78
|
+
{
|
|
79
|
+
baseKey,
|
|
80
|
+
args,
|
|
81
|
+
functionName,
|
|
82
|
+
user: cds.context?.user?.id,
|
|
83
|
+
tenant: cds.context?.tenant,
|
|
84
|
+
locale: cds.context?.locale
|
|
85
|
+
},
|
|
86
|
+
template
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wrap an async function with caching
|
|
92
|
+
* @param {string} key - the cache key
|
|
93
|
+
* @param {function} asyncFunction - the async function to wrap
|
|
94
|
+
* @param {object} options - cache options
|
|
95
|
+
* @returns {function} - the wrapped function
|
|
96
|
+
*/
|
|
97
|
+
wrap(key, asyncFunction, options = {}) {
|
|
98
|
+
return async (...args) => {
|
|
99
|
+
// Create dynamic key based on function arguments
|
|
100
|
+
const cacheKey = this.createDynamicKey(key, args, options, asyncFunction.name || 'anonymous');
|
|
101
|
+
|
|
102
|
+
const startTime = process.hrtime();
|
|
103
|
+
const metadata = {
|
|
104
|
+
dataType: 'Function',
|
|
105
|
+
operation: 'WRAP',
|
|
106
|
+
operationType: 'READ_THROUGH',
|
|
107
|
+
user: cds.context?.user?.id,
|
|
108
|
+
tenant: cds.context?.tenant,
|
|
109
|
+
locale: cds.context?.locale,
|
|
110
|
+
metadata: JSON.stringify({
|
|
111
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
112
|
+
args: args,
|
|
113
|
+
cacheKey: cacheKey
|
|
114
|
+
}),
|
|
115
|
+
cacheOptions: JSON.stringify(options)
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Safely check if key exists in cache
|
|
119
|
+
const hasKeyResult = await this.safeCacheOperation(
|
|
120
|
+
() => this.cache.has(cacheKey),
|
|
121
|
+
'has',
|
|
122
|
+
{ key: cacheKey, functionName: asyncFunction.name || 'anonymous' }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const hasKey = hasKeyResult.success && hasKeyResult.result;
|
|
126
|
+
const cacheErrors = [];
|
|
127
|
+
|
|
128
|
+
if (hasKey) {
|
|
129
|
+
const latency = this.getElapsedMs(startTime);
|
|
130
|
+
|
|
131
|
+
// Safely record hit statistics
|
|
132
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
133
|
+
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
134
|
+
'recordHit',
|
|
135
|
+
{ key: cacheKey, latency }
|
|
136
|
+
);
|
|
137
|
+
if (!hitStatsResult.success) {
|
|
138
|
+
cacheErrors.push(hitStatsResult.error);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Safely get value from cache
|
|
142
|
+
const getResult = await this.safeCacheOperation(
|
|
143
|
+
() => this.cache.send("GET", { key: cacheKey }),
|
|
144
|
+
'get',
|
|
145
|
+
{ key: cacheKey }
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (getResult.success && getResult.result?.value !== undefined) {
|
|
149
|
+
return {
|
|
150
|
+
result: getResult.result.value,
|
|
151
|
+
cacheKey,
|
|
152
|
+
metadata: { hit: true, latency },
|
|
153
|
+
cacheErrors: cacheErrors
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Cache miss or cache error - delegate to underlying function
|
|
159
|
+
try {
|
|
160
|
+
const response = await asyncFunction(...args);
|
|
161
|
+
const latency = this.getElapsedMs(startTime);
|
|
162
|
+
|
|
163
|
+
// Safely record miss statistics
|
|
164
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
165
|
+
() => this.statistics.recordMiss(latency, cacheKey, metadata),
|
|
166
|
+
'recordMiss',
|
|
167
|
+
{ key: cacheKey, latency }
|
|
168
|
+
);
|
|
169
|
+
if (!missStatsResult.success) {
|
|
170
|
+
cacheErrors.push(missStatsResult.error);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Safely store in cache
|
|
174
|
+
const wrappedValue = {
|
|
175
|
+
value: response,
|
|
176
|
+
tags: options.tags || [],
|
|
177
|
+
timestamp: Date.now()
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const setResult = await this.safeCacheOperation(
|
|
181
|
+
() => this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 }),
|
|
182
|
+
'set',
|
|
183
|
+
{ key: cacheKey, ttl: options.ttl }
|
|
184
|
+
);
|
|
185
|
+
if (!setResult.success) {
|
|
186
|
+
cacheErrors.push(setResult.error);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
result: response,
|
|
191
|
+
cacheKey,
|
|
192
|
+
metadata: { hit: false, latency },
|
|
193
|
+
cacheErrors: cacheErrors
|
|
194
|
+
};
|
|
195
|
+
} catch (functionError) {
|
|
196
|
+
// If the underlying function fails, throw the error
|
|
197
|
+
this.log.error('Function execution failed:', {
|
|
198
|
+
error: functionError.message,
|
|
199
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
200
|
+
key: cacheKey
|
|
201
|
+
});
|
|
202
|
+
throw functionError;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Executes an async function and caches its result
|
|
209
|
+
* @param {string} key - the key to cache
|
|
210
|
+
* @param {function} asyncFunction - the async function to execute
|
|
211
|
+
* @param {Array} args - function arguments
|
|
212
|
+
* @param {object} options - additional options
|
|
213
|
+
* @returns {Promise<any>} - the result
|
|
214
|
+
*/
|
|
215
|
+
async exec(key, asyncFunction, args = [], options = {}) {
|
|
216
|
+
// Create dynamic key based on function arguments
|
|
217
|
+
const cacheKey = this.createDynamicKey(key, args, options, asyncFunction.name || 'anonymous');
|
|
218
|
+
|
|
219
|
+
const startTime = process.hrtime();
|
|
220
|
+
|
|
221
|
+
const metadata = {
|
|
222
|
+
dataType: 'Function',
|
|
223
|
+
operation: 'EXEC',
|
|
224
|
+
operationType: 'READ_THROUGH',
|
|
225
|
+
user: cds.context?.user?.id,
|
|
226
|
+
tenant: cds.context?.tenant,
|
|
227
|
+
locale: cds.context?.locale,
|
|
228
|
+
metadata: JSON.stringify({
|
|
229
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
230
|
+
args: args,
|
|
231
|
+
cacheKey: cacheKey
|
|
232
|
+
}),
|
|
233
|
+
cacheOptions: JSON.stringify(options)
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// Safely check if key exists in cache
|
|
237
|
+
const hasKeyResult = await this.safeCacheOperation(
|
|
238
|
+
() => this.cache.has(cacheKey),
|
|
239
|
+
'has',
|
|
240
|
+
{ key: cacheKey, functionName: asyncFunction.name || 'anonymous' }
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const hasKey = hasKeyResult.success && hasKeyResult.result;
|
|
244
|
+
const cacheErrors = [];
|
|
245
|
+
|
|
246
|
+
if (hasKey) {
|
|
247
|
+
const latency = this.getElapsedMs(startTime);
|
|
248
|
+
|
|
249
|
+
// Safely record hit statistics
|
|
250
|
+
const hitStatsResult = await this.safeCacheOperation(
|
|
251
|
+
() => this.statistics.recordHit(latency, cacheKey, metadata),
|
|
252
|
+
'recordHit',
|
|
253
|
+
{ key: cacheKey, latency }
|
|
254
|
+
);
|
|
255
|
+
if (!hitStatsResult.success) {
|
|
256
|
+
cacheErrors.push(hitStatsResult.error);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Safely get value from cache
|
|
260
|
+
const getResult = await this.safeCacheOperation(
|
|
261
|
+
() => this.cache.send("GET", { key: cacheKey }),
|
|
262
|
+
'get',
|
|
263
|
+
{ key: cacheKey }
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (getResult.success && getResult.result?.value !== undefined) {
|
|
267
|
+
return {
|
|
268
|
+
result: getResult.result.value,
|
|
269
|
+
cacheKey,
|
|
270
|
+
metadata: { hit: true, latency },
|
|
271
|
+
cacheErrors: cacheErrors
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Cache miss or cache error - delegate to underlying function
|
|
277
|
+
try {
|
|
278
|
+
const response = await asyncFunction(...args);
|
|
279
|
+
const latency = this.getElapsedMs(startTime);
|
|
280
|
+
|
|
281
|
+
// Safely record miss statistics
|
|
282
|
+
const missStatsResult = await this.safeCacheOperation(
|
|
283
|
+
() => this.statistics.recordMiss(latency, cacheKey, metadata),
|
|
284
|
+
'recordMiss',
|
|
285
|
+
{ key: cacheKey, latency }
|
|
286
|
+
);
|
|
287
|
+
if (!missStatsResult.success) {
|
|
288
|
+
cacheErrors.push(missStatsResult.error);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Safely store in cache
|
|
292
|
+
const wrappedValue = {
|
|
293
|
+
value: response,
|
|
294
|
+
tags: options.tags || [],
|
|
295
|
+
timestamp: Date.now()
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const setResult = await this.safeCacheOperation(
|
|
299
|
+
() => this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 }),
|
|
300
|
+
'set',
|
|
301
|
+
{ key: cacheKey, ttl: options.ttl }
|
|
302
|
+
);
|
|
303
|
+
if (!setResult.success) {
|
|
304
|
+
cacheErrors.push(setResult.error);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
result: response,
|
|
309
|
+
cacheKey,
|
|
310
|
+
metadata: { hit: false, latency },
|
|
311
|
+
cacheErrors: cacheErrors
|
|
312
|
+
};
|
|
313
|
+
} catch (functionError) {
|
|
314
|
+
// If the underlying function fails, throw the error
|
|
315
|
+
this.log.error('Function execution failed:', {
|
|
316
|
+
error: functionError.message,
|
|
317
|
+
functionName: asyncFunction.name || 'anonymous',
|
|
318
|
+
key: cacheKey
|
|
319
|
+
});
|
|
320
|
+
throw functionError;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get elapsed time in milliseconds
|
|
326
|
+
* @param {[number, number]} startTime - start time from process.hrtime()
|
|
327
|
+
* @returns {number} - elapsed time in milliseconds
|
|
328
|
+
*/
|
|
329
|
+
getElapsedMs(startTime) {
|
|
330
|
+
const [seconds, nanoseconds] = process.hrtime(startTime);
|
|
331
|
+
return (seconds * 1000) + (nanoseconds / 1000000);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = AsyncOperations;
|