cds-caching 1.0.0 → 1.2.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.
@@ -7,6 +7,36 @@ class AsyncOperations {
7
7
  this.keyManager = keyManager;
8
8
  this.statistics = statistics;
9
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
+ }
10
40
  }
11
41
 
12
42
  /**
@@ -85,25 +115,91 @@ class AsyncOperations {
85
115
  cacheOptions: JSON.stringify(options)
86
116
  };
87
117
 
88
- if (await this.cache.has(cacheKey)) {
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) {
89
129
  const latency = this.getElapsedMs(startTime);
90
- this.statistics.recordHit(latency, cacheKey, metadata);
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
+ }
91
157
 
92
- const result = await this.cache.send("GET", { key: cacheKey });
93
- return { result: result?.value || result, cacheKey, metadata: { hit: true, latency } };
94
- } else {
158
+ // Cache miss or cache error - delegate to underlying function
159
+ try {
95
160
  const response = await asyncFunction(...args);
96
161
  const latency = this.getElapsedMs(startTime);
97
- this.statistics.recordMiss(latency, cacheKey, metadata);
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
+ }
98
172
 
173
+ // Safely store in cache
99
174
  const wrappedValue = {
100
175
  value: response,
101
176
  tags: options.tags || [],
102
177
  timestamp: Date.now()
103
178
  };
104
- await this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 });
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
+ }
105
188
 
106
- return { result: response, cacheKey, metadata: { hit: false, latency } };
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;
107
203
  }
108
204
  }
109
205
  }
@@ -137,24 +233,91 @@ class AsyncOperations {
137
233
  cacheOptions: JSON.stringify(options)
138
234
  };
139
235
 
140
- if (await this.cache.has(cacheKey)) {
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) {
141
247
  const latency = this.getElapsedMs(startTime);
142
- this.statistics.recordHit(latency, cacheKey, metadata);
143
- const result = await this.cache.send("GET", { key: cacheKey });
144
- return { result: result?.value || result, cacheKey, metadata: { hit: true, latency } };
145
- } else {
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 {
146
278
  const response = await asyncFunction(...args);
147
279
  const latency = this.getElapsedMs(startTime);
148
- this.statistics.recordMiss(latency, cacheKey, metadata);
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
+ }
149
290
 
291
+ // Safely store in cache
150
292
  const wrappedValue = {
151
293
  value: response,
152
294
  tags: options.tags || [],
153
295
  timestamp: Date.now()
154
296
  };
155
- await this.cache.send("SET", { key: cacheKey, value: wrappedValue, ttl: options.ttl || 0 });
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
+ }
156
306
 
157
- return { result: response, cacheKey, metadata: { hit: false, latency } };
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;
158
321
  }
159
322
  }
160
323
 
@@ -9,6 +9,16 @@ class BasicOperations {
9
9
  this.statistics = statistics;
10
10
  }
11
11
 
12
+ _keyv() {
13
+ // The underlying Keyv instance is stored on the CachingService as `this.cache`
14
+ // (see `CachingService.init()` where `this.cache = new Keyv(...)`).
15
+ const keyv = this.cache?.cache;
16
+ if (!keyv) {
17
+ throw new Error('cds-caching: Keyv store not initialized on caching service');
18
+ }
19
+ return keyv;
20
+ }
21
+
12
22
  /**
13
23
  * Set a value in the cache
14
24
  * @param {string|object} key - the key to set
@@ -16,14 +26,17 @@ class BasicOperations {
16
26
  * @param {object} options - cache options
17
27
  * @returns {Promise<void>}
18
28
  */
19
- async set(key, value, options = {}) {
29
+ async set(key, value, options = {}, tx = null) {
20
30
  const wrappedValue = {
21
31
  value,
22
32
  tags: this.tagResolver.resolveTags(options.tags, value, options.params) || [],
23
33
  timestamp: Date.now()
24
34
  };
25
35
  const createdKey = this.keyManager.createKey(key, {}, options.key);
26
- await this.cache.send('SET', {
36
+ // If no key was created, the input should not be cached (e.g. non-SELECT CQN)
37
+ if (!createdKey) return;
38
+ const srv = tx || this.cache;
39
+ await srv.send('SET', {
27
40
  key: createdKey,
28
41
  value: wrappedValue,
29
42
  ttl: options.ttl || 0
@@ -44,9 +57,16 @@ class BasicOperations {
44
57
  * @param {string|object} key - the key to get
45
58
  * @returns {Promise<any>} - the cached value
46
59
  */
47
- async get(key) {
60
+ async get(key, tx = null) {
48
61
  const createdKey = this.keyManager.createKey(key);
49
- const wrappedValue = await this.cache.send('GET', {
62
+
63
+ // If not key was created, return undefined
64
+ if(!createdKey) {
65
+ return undefined;
66
+ }
67
+
68
+ const srv = tx || this.cache;
69
+ const wrappedValue = await srv.send('GET', {
50
70
  key: createdKey
51
71
  });
52
72
 
@@ -68,9 +88,21 @@ class BasicOperations {
68
88
  * @param {string|object} key - the key to check
69
89
  * @returns {Promise<boolean>} - whether the key exists
70
90
  */
71
- async has(key) {
91
+ async has(key, tx = null) {
72
92
  const createdKey = this.keyManager.createKey(key);
73
- return this.cache.cache.has(createdKey);
93
+ if (!createdKey) return false;
94
+ try {
95
+ // Intentionally bypass CAP service/transaction handling here.
96
+ // This avoids coupling to the caller's request tx (which might already be rolled back)
97
+ // and makes `has()` safe to call even in concurrent BEFORE handlers.
98
+ return await this._keyv().has(createdKey);
99
+ } catch (error) {
100
+ if (this.cache.options.throwOnErrors) {
101
+ throw error;
102
+ } else {
103
+ return false; // We don't want to throw errors here
104
+ }
105
+ }
74
106
  }
75
107
 
76
108
  /**
@@ -78,9 +110,11 @@ class BasicOperations {
78
110
  * @param {string|object} key - the key to delete
79
111
  * @returns {Promise<boolean>} - whether the key was deleted
80
112
  */
81
- async delete(key) {
113
+ async delete(key, tx = null) {
82
114
  const createdKey = this.keyManager.createKey(key);
83
- const result = await this.cache.send('DELETE', {
115
+ if (!createdKey) return false;
116
+ const srv = tx || this.cache;
117
+ const result = await srv.send('DELETE', {
84
118
  key: createdKey
85
119
  });
86
120
 
@@ -100,8 +134,11 @@ class BasicOperations {
100
134
  * Clear all cache entries
101
135
  * @returns {Promise<void>}
102
136
  */
103
- async clear() {
104
- await this.cache.send('CLEAR');
137
+ async clear(tx = null) {
138
+ const srv = tx || this.cache;
139
+ await srv.send('CLEAR', {
140
+ deleteAll: true
141
+ });
105
142
 
106
143
  // Record the native clear operation
107
144
  const metadata = {
@@ -119,10 +156,11 @@ class BasicOperations {
119
156
  * @param {string} tag - the tag to match
120
157
  * @returns {Promise<void>}
121
158
  */
122
- async deleteByTag(tag) {
159
+ async deleteByTag(tag, tx = null) {
160
+ // Iterate directly on the underlying store to avoid recursion via CachingService.iterator()
123
161
  for await (const [key, wrappedValue] of this.iterator()) {
124
162
  if (wrappedValue?.tags?.includes(tag)) {
125
- await this.delete(key);
163
+ await this.delete(key, tx);
126
164
  }
127
165
  }
128
166
 
@@ -142,9 +180,11 @@ class BasicOperations {
142
180
  * @param {string|object} key - the key to get metadata for
143
181
  * @returns {Promise<object|null>} - the metadata or null if not found
144
182
  */
145
- async metadata(key) {
183
+ async metadata(key, tx = null) {
146
184
  const createdKey = this.keyManager.createKey(key);
147
- const wrappedValue = await this.cache.send('GET', {
185
+ if (!createdKey) return null;
186
+ const srv = tx || this.cache;
187
+ const wrappedValue = await srv.send('GET', {
148
188
  key: createdKey
149
189
  });
150
190
  if (!wrappedValue) return null;
@@ -158,9 +198,11 @@ class BasicOperations {
158
198
  * @param {string|object} key - the key to get tags for
159
199
  * @returns {Promise<string[]>} - the tags
160
200
  */
161
- async tags(key) {
201
+ async tags(key, tx = null) {
162
202
  const createdKey = this.keyManager.createKey(key);
163
- const wrappedValue = await this.cache.send('GET', {
203
+ if (!createdKey) return [];
204
+ const srv = tx || this.cache;
205
+ const wrappedValue = await srv.send('GET', {
164
206
  key: createdKey
165
207
  });
166
208
  return wrappedValue?.tags || [];
@@ -171,9 +213,11 @@ class BasicOperations {
171
213
  * @param {string|object} key - the key to get
172
214
  * @returns {Promise<any>} - the raw cached value
173
215
  */
174
- async getRaw(key) {
216
+ async getRaw(key, tx = null) {
175
217
  const createdKey = this.keyManager.createKey(key);
176
- const wrappedValue = await this.cache.send('GET', {
218
+ if (!createdKey) return undefined;
219
+ const srv = tx || this.cache;
220
+ const wrappedValue = await srv.send('GET', {
177
221
  key: createdKey
178
222
  });
179
223
  return wrappedValue?.value;
@@ -183,8 +227,9 @@ class BasicOperations {
183
227
  * Iterator for all cache entries
184
228
  * @returns {AsyncIterator} - iterator for cache entries
185
229
  */
186
- async *iterator() {
187
- for await (const [key, value] of this.cache.cache.iterator()) {
230
+ async *iterator(tx = null) {
231
+ // Iterate directly on Keyv to avoid recursion via CachingService.iterator()
232
+ for await (const [key, value] of this._keyv().iterator()) {
188
233
  if (typeof value === "string") {
189
234
  try {
190
235
  yield [key, JSON.parse(value)];
@@ -196,6 +241,107 @@ class BasicOperations {
196
241
  }
197
242
  }
198
243
  }
244
+
245
+ async setInTx(key, value, options = {}) {
246
+ const tx = await this.cache.tx();
247
+ try {
248
+ await this.set(key, value, options, tx);
249
+ await tx.commit();
250
+ return;
251
+ } catch (error) {
252
+ await tx.rollback();
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ async getInTx(key) {
258
+ const tx = await this.cache.tx();
259
+ try {
260
+ const value = await this.get(key, tx);
261
+ await tx.commit();
262
+ return value;
263
+ } catch (error) {
264
+ await tx.rollback();
265
+ throw error;
266
+ }
267
+ }
268
+
269
+ async hasInTx(key) {
270
+ // `has()` bypasses CAP tx handling on purpose, so opening a new transaction is unnecessary.
271
+ return this.has(key);
272
+ }
273
+
274
+ async deleteInTx(key) {
275
+ const tx = await this.cache.tx();
276
+ try {
277
+ const value = await this.delete(key, tx);
278
+ await tx.commit();
279
+ return value;
280
+ } catch (error) {
281
+ await tx.rollback();
282
+ throw error;
283
+ }
284
+ }
285
+
286
+ async clearInTx() {
287
+ const tx = await this.cache.tx();
288
+ try {
289
+ await this.clear(tx);
290
+ await tx.commit();
291
+ return;
292
+ } catch (error) {
293
+ await tx.rollback();
294
+ throw error;
295
+ }
296
+ }
297
+
298
+ async deleteByTagInTx(tag) {
299
+ const tx = await this.cache.tx();
300
+ try {
301
+ await this.deleteByTag(tag, tx);
302
+ await tx.commit();
303
+ return;
304
+ } catch (error) {
305
+ await tx.rollback();
306
+ throw error;
307
+ }
308
+ }
309
+
310
+ async metadataInTx(key) {
311
+ const tx = await this.cache.tx();
312
+ try {
313
+ const value = await this.metadata(key, tx);
314
+ await tx.commit();
315
+ return value;
316
+ } catch (error) {
317
+ await tx.rollback();
318
+ throw error;
319
+ }
320
+ }
321
+
322
+ async tagsInTx(key) {
323
+ const tx = await this.cache.tx();
324
+ try {
325
+ const value = await this.tags(key, tx);
326
+ await tx.commit();
327
+ return value;
328
+ } catch (error) {
329
+ await tx.rollback();
330
+ throw error;
331
+ }
332
+ }
333
+
334
+ async getRawInTx(key) {
335
+ const tx = await this.cache.tx();
336
+ try {
337
+ const value = await this.getRaw(key, tx);
338
+ await tx.commit();
339
+ return value;
340
+ } catch (error) {
341
+ await tx.rollback();
342
+ throw error;
343
+ }
344
+ }
199
345
  }
200
346
 
201
347
  module.exports = BasicOperations;