cds-caching 3.0.0 → 3.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 CHANGED
@@ -77,7 +77,7 @@ This uses the in-memory store — no additional setup needed for development.
77
77
 
78
78
  The plugin ships CDS entity definitions for database-backed features. These load **conditionally** based on your configuration — no manual `model` property needed.
79
79
 
80
- > **Full guide:** [Feature Activation](docs/feature-activation.md) — decision tree, BTP/MTX best practices, and rules for avoiding duplicate model loading.
80
+ > **Full guide:** [Feature Activation](docs/feature-activation.md) — decision tree, BTP/MTX guidance, and rules for avoiding duplicate model loading.
81
81
 
82
82
  | Option | Entities loaded | Purpose |
83
83
  |--------|-----------------|---------|
@@ -258,7 +258,7 @@ For detailed key configuration and deployment instructions, see [Key Management]
258
258
 
259
259
  The plugin includes `CachingApiService`, an OData service for managing caches, browsing entries, and viewing metrics. It powers the [dashboard](docs/dashboard.md) and can be consumed by any OData client.
260
260
 
261
- See **[Feature Activation](docs/feature-activation.md)** for reuse vs own activation, BTP/MTX best practices, and API authorization.
261
+ See **[Feature Activation](docs/feature-activation.md)** for reuse vs own activation, BTP/MTX guidance, and API authorization.
262
262
 
263
263
  **Quick reference:**
264
264
 
package/db/statistics.cds CHANGED
@@ -5,10 +5,13 @@ entity Caches {
5
5
  config : String;
6
6
  metricsEnabled : Boolean default false;
7
7
  keyMetricsEnabled : Boolean default false;
8
+ tagMetricsEnabled : Boolean default false;
8
9
  metrics : Composition of many Metrics
9
10
  on metrics.cache = $self.name;
10
11
  keyMetrics : Composition of many KeyMetrics
11
12
  on keyMetrics.cache = $self.name;
13
+ tagMetrics : Composition of many TagMetrics
14
+ on tagMetrics.cache = $self.name;
12
15
  }
13
16
 
14
17
  entity Metrics {
@@ -126,3 +129,56 @@ entity KeyMetrics {
126
129
  timestamp : DateTime; // When this key was first accessed
127
130
  cacheOptions : String; // JSON string with cache options
128
131
  }
132
+
133
+ /**
134
+ * Per-tag metrics. Opt-in via `tagMetricsEnabled` / `metrics.tagMetricsEnabled`.
135
+ *
136
+ * An entry can carry several tags, so a single hit increments every matching tag
137
+ * row. Tag totals can therefore exceed cache-level `Metrics` totals — that is
138
+ * expected, not a bug. Query by the resolved tag string (exact match), e.g.
139
+ * `tag eq 'federation:Airports'`.
140
+ */
141
+ entity TagMetrics {
142
+ key ID : String;
143
+ key cache : String;
144
+ key tag : String;
145
+ lastAccess : DateTime;
146
+ period : String enum {
147
+ current;
148
+ hourly;
149
+ daily;
150
+ };
151
+
152
+ // Read-through metrics (hits and misses only)
153
+ hits : Integer default 0;
154
+ misses : Integer default 0;
155
+ errors : Integer default 0;
156
+ totalRequests : Integer default 0;
157
+ hitRatio : Double;
158
+ cacheEfficiency : Double;
159
+
160
+ // Read-through latency metrics
161
+ avgHitLatency : Double;
162
+ minHitLatency : Double;
163
+ maxHitLatency : Double;
164
+ avgMissLatency : Double;
165
+ minMissLatency : Double;
166
+ maxMissLatency : Double;
167
+ avgReadThroughLatency : Double;
168
+
169
+ // Read-through performance metrics
170
+ throughput : Double;
171
+ errorRate : Double;
172
+
173
+ // Native function metrics (counts only)
174
+ nativeHits : Integer default 0;
175
+ nativeMisses : Integer default 0;
176
+ nativeSets : Integer default 0;
177
+ nativeDeletes : Integer default 0;
178
+ nativeErrors : Integer default 0;
179
+ totalNativeOperations : Integer default 0;
180
+ nativeThroughput : Double;
181
+ nativeErrorRate : Double;
182
+
183
+ timestamp : DateTime;
184
+ }
package/index.cds CHANGED
@@ -39,8 +39,10 @@ context plugin.cds_caching {
39
39
  action clear() returns Boolean;
40
40
  action clearMetrics() returns Boolean;
41
41
  action clearKeyMetrics() returns Boolean;
42
+ action clearTagMetrics() returns Boolean;
42
43
  action setMetricsEnabled(enabled : Boolean) returns Boolean;
43
44
  action setKeyMetricsEnabled(enabled : Boolean) returns Boolean;
45
+ action setTagMetricsEnabled(enabled : Boolean) returns Boolean;
44
46
  };
45
47
 
46
48
  @readonly
@@ -49,5 +51,8 @@ context plugin.cds_caching {
49
51
  @readonly
50
52
  entity KeyMetrics as projection on plugin.cds_caching.KeyMetrics;
51
53
 
54
+ @readonly
55
+ entity TagMetrics as projection on plugin.cds_caching.TagMetrics;
56
+
52
57
  }
53
58
  }
@@ -58,6 +58,15 @@ export interface CacheKeyMetrics {
58
58
  lastAccessed: Date;
59
59
  }
60
60
 
61
+ export interface CacheTagMetrics {
62
+ tag: string;
63
+ hits: number;
64
+ misses: number;
65
+ totalRequests: number;
66
+ hitRatio: number;
67
+ lastAccess: Date;
68
+ }
69
+
61
70
  export interface CachableFunctionOptions {
62
71
  '@cache.ttl'?: number;
63
72
  '@cache.key'?: string | object;
@@ -280,6 +289,16 @@ export declare class CachingService extends Service {
280
289
  */
281
290
  getCurrentKeyMetrics(): Promise<CacheKeyMetrics>;
282
291
 
292
+ /**
293
+ * Get tag-specific metrics for a specific period
294
+ */
295
+ getTagMetrics(tag: string, from: Date, to: Date): Promise<CacheTagMetrics>;
296
+
297
+ /**
298
+ * Get current tag metrics
299
+ */
300
+ getCurrentTagMetrics(): Promise<Map<string, CacheTagMetrics> | null>;
301
+
283
302
  /**
284
303
  * Clear all metrics
285
304
  */
@@ -290,6 +309,11 @@ export declare class CachingService extends Service {
290
309
  */
291
310
  clearKeyMetrics(): Promise<void>;
292
311
 
312
+ /**
313
+ * Clear tag metrics
314
+ */
315
+ clearTagMetrics(): Promise<void>;
316
+
293
317
  /**
294
318
  * Enable or disable statistics at runtime
295
319
  */
@@ -300,6 +324,11 @@ export declare class CachingService extends Service {
300
324
  */
301
325
  setKeyMetricsEnabled(enabled: boolean): Promise<void>;
302
326
 
327
+ /**
328
+ * Enable or disable tag tracking at runtime
329
+ */
330
+ setTagMetricsEnabled(enabled: boolean): Promise<void>;
331
+
303
332
  /**
304
333
  * Persist metrics to database
305
334
  */
@@ -129,12 +129,16 @@ class CachingService extends cds.Service {
129
129
 
130
130
  let metricsEnabled = config.metricsEnabled;
131
131
  let keyMetricsEnabled = config.keyMetricsEnabled;
132
+ let tagMetricsEnabled = config.tagMetricsEnabled;
132
133
  if (normalized.metrics?.enabled === true) {
133
134
  metricsEnabled = true;
134
135
  }
135
136
  if (normalized.metrics?.keyMetricsEnabled === true) {
136
137
  keyMetricsEnabled = true;
137
138
  }
139
+ if (normalized.metrics?.tagMetricsEnabled === true) {
140
+ tagMetricsEnabled = true;
141
+ }
138
142
 
139
143
  // Initialize statistics with runtime configuration support
140
144
  this.statistics = new CacheStatisticsHandler({
@@ -142,6 +146,7 @@ class CachingService extends cds.Service {
142
146
  ...statsOpts,
143
147
  });
144
148
  this.statistics.enableKeyMetrics(keyMetricsEnabled);
149
+ this.statistics.enableTagMetrics(tagMetricsEnabled);
145
150
  this.statistics.enableMetrics(metricsEnabled);
146
151
 
147
152
  if (normalized.metrics?.enabled === true && isPluginModelAvailable()) {
@@ -158,6 +163,13 @@ class CachingService extends cds.Service {
158
163
  this.log.warn(`Failed to persist keyMetricsEnabled from config for ${this.name}:`, error);
159
164
  }
160
165
  }
166
+ if (normalized.metrics?.tagMetricsEnabled === true && isPluginModelAvailable()) {
167
+ try {
168
+ await this.runtimeConfigManager.setTagMetricsEnabled(true);
169
+ } catch (error) {
170
+ this.log.warn(`Failed to persist tagMetricsEnabled from config for ${this.name}:`, error);
171
+ }
172
+ }
161
173
 
162
174
  // Initialize operation managers
163
175
  this.basicOperations = new BasicOperations(
@@ -402,6 +414,25 @@ class CachingService extends cds.Service {
402
414
  return this.statistics.getCurrentKeyMetrics();
403
415
  }
404
416
 
417
+ /**
418
+ * Get tag-specific metrics for a specific period
419
+ * @param {string} tag - resolved tag string
420
+ * @param {Date} from
421
+ * @param {Date} to
422
+ * @returns {Promise<object>}
423
+ */
424
+ async getTagMetrics(tag, from, to) {
425
+ return this.statistics.getTagMetrics(tag, from, to);
426
+ }
427
+
428
+ /**
429
+ * Get current tag metrics
430
+ * @returns {Promise<Map|null>}
431
+ */
432
+ async getCurrentTagMetrics() {
433
+ return this.statistics.getCurrentTagMetrics();
434
+ }
435
+
405
436
 
406
437
 
407
438
  /**
@@ -420,6 +451,14 @@ class CachingService extends cds.Service {
420
451
  return this.statistics.clearKeyMetrics();
421
452
  }
422
453
 
454
+ /**
455
+ * Clear tag metrics
456
+ * @returns {Promise<void>}
457
+ */
458
+ async clearTagMetrics() {
459
+ return this.statistics.clearTagMetrics();
460
+ }
461
+
423
462
  /**
424
463
  * Enable or disable statistics at runtime
425
464
  * @param {boolean} enabled - whether to enable statistics
@@ -440,6 +479,16 @@ class CachingService extends cds.Service {
440
479
  this.statistics.enableKeyMetrics(enabled);
441
480
  }
442
481
 
482
+ /**
483
+ * Enable or disable tag tracking at runtime
484
+ * @param {boolean} enabled - whether to enable tag tracking
485
+ * @returns {Promise<void>}
486
+ */
487
+ async setTagMetricsEnabled(enabled) {
488
+ await this.runtimeConfigManager.setTagMetricsEnabled(enabled);
489
+ this.statistics.enableTagMetrics(enabled);
490
+ }
491
+
443
492
  /**
444
493
  * Persist metrics to database
445
494
  * @returns {Promise<void>}
@@ -97,9 +97,11 @@ function getStatisticsHandlerOptions(metrics) {
97
97
  if (metrics.persistenceInterval != null) opts.persistenceInterval = metrics.persistenceInterval
98
98
  if (metrics.maxLatencies != null) opts.maxLatencies = metrics.maxLatencies
99
99
  if (metrics.maxKeyMetrics != null) opts.maxKeyMetrics = metrics.maxKeyMetrics
100
+ if (metrics.maxTagMetrics != null) opts.maxTagMetrics = metrics.maxTagMetrics
100
101
  if (metrics.maxMetricFieldLength != null) opts.maxMetricFieldLength = metrics.maxMetricFieldLength
101
102
  if (metrics.enabled === true) opts.metricsEnabled = true
102
103
  if (metrics.keyMetricsEnabled === true) opts.keyMetricsEnabled = true
104
+ if (metrics.tagMetricsEnabled === true) opts.tagMetricsEnabled = true
103
105
  return opts
104
106
  }
105
107
 
@@ -122,6 +122,22 @@ class AsyncOperations {
122
122
  };
123
123
  }
124
124
 
125
+ /** @private */
126
+ _resolveTags(options, value) {
127
+ const resolver = this.cache?.tagResolver;
128
+ if (resolver) {
129
+ return resolver.resolveTags(options?.tags, value, {
130
+ user: cds.context?.user?.id,
131
+ tenant: cds.context?.tenant,
132
+ locale: cds.context?.locale,
133
+ }) || [];
134
+ }
135
+ if (!Array.isArray(options?.tags)) return [];
136
+ return options.tags
137
+ .map((t) => (typeof t === 'string' ? t : t?.value))
138
+ .filter((t) => typeof t === 'string' && t);
139
+ }
140
+
125
141
  /** @private */
126
142
  async _wrap(key, asyncFunction, options, args) {
127
143
  // Create dynamic key based on function arguments
@@ -140,7 +156,8 @@ class AsyncOperations {
140
156
  args: args,
141
157
  cacheKey: cacheKey
142
158
  }),
143
- cacheOptions: JSON.stringify(options)
159
+ cacheOptions: JSON.stringify(options),
160
+ tags: this._resolveTags(options, undefined),
144
161
  };
145
162
 
146
163
  // Safely check if key exists in cache
@@ -156,16 +173,6 @@ class AsyncOperations {
156
173
  if (hasKey) {
157
174
  const latency = this.getElapsedMs(startTime);
158
175
 
159
- // Safely record hit statistics
160
- const hitStatsResult = await this.safeCacheOperation(
161
- () => this.statistics.recordHit(latency, cacheKey, metadata),
162
- 'recordHit',
163
- { key: cacheKey, latency }
164
- );
165
- if (!hitStatsResult.success) {
166
- cacheErrors.push(hitStatsResult.error);
167
- }
168
-
169
176
  // Safely get value from cache
170
177
  const getResult = await this.safeCacheOperation(
171
178
  () => this.cache.send("GET", { key: cacheKey }),
@@ -174,6 +181,19 @@ class AsyncOperations {
174
181
  );
175
182
 
176
183
  if (getResult.success && getResult.result?.value !== undefined) {
184
+ if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
185
+ metadata.tags = getResult.result.tags;
186
+ }
187
+
188
+ const hitStatsResult = await this.safeCacheOperation(
189
+ () => this.statistics.recordHit(latency, cacheKey, metadata),
190
+ 'recordHit',
191
+ { key: cacheKey, latency }
192
+ );
193
+ if (!hitStatsResult.success) {
194
+ cacheErrors.push(hitStatsResult.error);
195
+ }
196
+
177
197
  return {
178
198
  result: getResult.result.value,
179
199
  cacheKey,
@@ -187,6 +207,8 @@ class AsyncOperations {
187
207
  try {
188
208
  const response = await asyncFunction(...args);
189
209
  const latency = this.getElapsedMs(startTime);
210
+ const tags = this._resolveTags(options, response);
211
+ metadata.tags = tags;
190
212
 
191
213
  // Safely record miss statistics
192
214
  const missStatsResult = await this.safeCacheOperation(
@@ -201,7 +223,7 @@ class AsyncOperations {
201
223
  // Safely store in cache
202
224
  const wrappedValue = {
203
225
  value: response,
204
- tags: options.tags || [],
226
+ tags,
205
227
  timestamp: Date.now()
206
228
  };
207
229
 
@@ -281,7 +303,8 @@ class AsyncOperations {
281
303
  args: args,
282
304
  cacheKey: cacheKey
283
305
  }),
284
- cacheOptions: JSON.stringify(options)
306
+ cacheOptions: JSON.stringify(options),
307
+ tags: this._resolveTags(options, undefined),
285
308
  };
286
309
 
287
310
  // Safely check if key exists in cache
@@ -297,16 +320,6 @@ class AsyncOperations {
297
320
  if (hasKey) {
298
321
  const latency = this.getElapsedMs(startTime);
299
322
 
300
- // Safely record hit statistics
301
- const hitStatsResult = await this.safeCacheOperation(
302
- () => this.statistics.recordHit(latency, cacheKey, metadata),
303
- 'recordHit',
304
- { key: cacheKey, latency }
305
- );
306
- if (!hitStatsResult.success) {
307
- cacheErrors.push(hitStatsResult.error);
308
- }
309
-
310
323
  // Safely get value from cache
311
324
  const getResult = await this.safeCacheOperation(
312
325
  () => this.cache.send("GET", { key: cacheKey }),
@@ -315,6 +328,19 @@ class AsyncOperations {
315
328
  );
316
329
 
317
330
  if (getResult.success && getResult.result?.value !== undefined) {
331
+ if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
332
+ metadata.tags = getResult.result.tags;
333
+ }
334
+
335
+ const hitStatsResult = await this.safeCacheOperation(
336
+ () => this.statistics.recordHit(latency, cacheKey, metadata),
337
+ 'recordHit',
338
+ { key: cacheKey, latency }
339
+ );
340
+ if (!hitStatsResult.success) {
341
+ cacheErrors.push(hitStatsResult.error);
342
+ }
343
+
318
344
  return {
319
345
  result: getResult.result.value,
320
346
  cacheKey,
@@ -328,6 +354,8 @@ class AsyncOperations {
328
354
  try {
329
355
  const response = await asyncFunction(...args);
330
356
  const latency = this.getElapsedMs(startTime);
357
+ const tags = this._resolveTags(options, response);
358
+ metadata.tags = tags;
331
359
 
332
360
  // Safely record miss statistics
333
361
  const missStatsResult = await this.safeCacheOperation(
@@ -342,7 +370,7 @@ class AsyncOperations {
342
370
  // Safely store in cache
343
371
  const wrappedValue = {
344
372
  value: response,
345
- tags: options.tags || [],
373
+ tags,
346
374
  timestamp: Date.now()
347
375
  };
348
376
 
@@ -28,9 +28,10 @@ class BasicOperations {
28
28
  * @returns {Promise<void>}
29
29
  */
30
30
  async set(key, value, options = {}, tx = null) {
31
+ const tags = this.tagResolver.resolveTags(options.tags, value, options.params) || [];
31
32
  const wrappedValue = {
32
33
  value,
33
- tags: this.tagResolver.resolveTags(options.tags, value, options.params) || [],
34
+ tags,
34
35
  timestamp: Date.now()
35
36
  };
36
37
  const createdKey = this.keyManager.createKey(key, {}, options.key);
@@ -48,7 +49,8 @@ class BasicOperations {
48
49
  operation: 'SET',
49
50
  operationType: 'BASIC',
50
51
  metadata: JSON.stringify({ key: createdKey, ttl: options.ttl || 0 }),
51
- cacheOptions: JSON.stringify(options)
52
+ cacheOptions: JSON.stringify(options),
53
+ tags,
52
54
  };
53
55
  this.statistics.recordNativeSet(createdKey, metadata);
54
56
 
@@ -84,7 +86,8 @@ class BasicOperations {
84
86
  dataType: 'Operation',
85
87
  operation: 'GET',
86
88
  operationType: 'BASIC',
87
- metadata: JSON.stringify({ key: createdKey })
89
+ metadata: JSON.stringify({ key: createdKey }),
90
+ tags: wrappedValue?.tags || [],
88
91
  };
89
92
  const isHit = wrappedValue !== undefined && wrappedValue !== null;
90
93
  this.statistics.recordNativeGet(createdKey, isHit, metadata);
@@ -129,6 +129,13 @@ class CapOperations {
129
129
 
130
130
  const key = this.keyManager.createKey(request, keyParts, requestOptions.key);
131
131
  const startTime = process.hrtime();
132
+ const tagParams = {
133
+ ...request.params,
134
+ user: request.user?.id,
135
+ tenant: request.tenant,
136
+ locale: request.locale,
137
+ hash: this.keyManager.createContentHash(request),
138
+ };
132
139
 
133
140
  // Track the miss with total latency (cache lookup + backend operation)
134
141
  const metadata = {
@@ -153,7 +160,9 @@ class CapOperations {
153
160
  event: request.event,
154
161
  headers: redactHeaders(request.headers),
155
162
  }),
156
- cacheOptions: JSON.stringify(requestOptions)
163
+ cacheOptions: JSON.stringify(requestOptions),
164
+ // Static tags resolve without a payload; data tags fill in on miss after the response.
165
+ tags: this.tagResolver.resolveTags(requestOptions.tags, undefined, tagParams) || [],
157
166
  };
158
167
 
159
168
  // Safely check if key exists in cache
@@ -169,17 +178,7 @@ class CapOperations {
169
178
  if (hasKey) {
170
179
  const latency = this.getElapsedMs(startTime);
171
180
 
172
- // Safely record hit statistics
173
- const hitStatsResult = await this.safeCacheOperation(
174
- () => this.statistics.recordHit(latency, key, metadata),
175
- 'recordHit',
176
- { key, latency }
177
- );
178
- if (!hitStatsResult.success) {
179
- cacheErrors.push(hitStatsResult.error);
180
- }
181
-
182
- // Safely get value from cache
181
+ // Safely get value from cache (before recording so entry tags are available)
183
182
  const getResult = await this.safeCacheOperation(
184
183
  () => this.cache.send("GET", { key }),
185
184
  'get',
@@ -187,6 +186,19 @@ class CapOperations {
187
186
  );
188
187
 
189
188
  if (getResult.success && getResult.result?.value !== undefined) {
189
+ if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
190
+ metadata.tags = getResult.result.tags;
191
+ }
192
+
193
+ const hitStatsResult = await this.safeCacheOperation(
194
+ () => this.statistics.recordHit(latency, key, metadata),
195
+ 'recordHit',
196
+ { key, latency }
197
+ );
198
+ if (!hitStatsResult.success) {
199
+ cacheErrors.push(hitStatsResult.error);
200
+ }
201
+
190
202
  return {
191
203
  result: getResult.result.value,
192
204
  cacheKey: key,
@@ -200,6 +212,8 @@ class CapOperations {
200
212
  try {
201
213
  const response = await service.send(request);
202
214
  const totalLatency = this.getElapsedMs(startTime);
215
+ const tags = this.tagResolver.resolveTags(requestOptions.tags, response, tagParams) || [];
216
+ metadata.tags = tags;
203
217
 
204
218
  // Safely record miss statistics
205
219
  const missStatsResult = await this.safeCacheOperation(
@@ -214,7 +228,7 @@ class CapOperations {
214
228
  // Safely store in cache
215
229
  const wrappedValue = {
216
230
  value: response,
217
- tags: this.tagResolver.resolveTags(requestOptions.tags, response, { ...request.params, user: request.user?.id, tenant: request.tenant, locale: request.locale, hash: this.keyManager.createContentHash(request) }),
231
+ tags,
218
232
  timestamp: Date.now()
219
233
  };
220
234
 
@@ -311,9 +325,14 @@ class CapOperations {
311
325
  const cacheHit = getResult.success && getResult.result?.value !== undefined;
312
326
  const cacheLatency = this.getElapsedMs(startTime);
313
327
  const metadata = this.extractMetadataFromRequest(req);
328
+ const tagParams = { ...req.params, hash: this.keyManager.createContentHash(req) };
314
329
  const cacheErrors = [];
315
330
 
316
331
  if (cacheHit) {
332
+ metadata.tags = (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0)
333
+ ? getResult.result.tags
334
+ : (this.tagResolver.resolveTags(req.cacheOptions.tags, getResult.result.value, tagParams) || []);
335
+
317
336
  // Cache hit
318
337
  const hitStatsResult = await this.safeCacheOperation(
319
338
  () => this.statistics.recordHit(cacheLatency, req.cacheKey, metadata),
@@ -336,6 +355,8 @@ class CapOperations {
336
355
  try {
337
356
  const response = await next();
338
357
  const totalLatency = this.getElapsedMs(startTime);
358
+ const tags = this.tagResolver.resolveTags(req.cacheOptions.tags, response, tagParams) || [];
359
+ metadata.tags = tags;
339
360
 
340
361
  // Safely record miss statistics
341
362
  const missStatsResult = await this.safeCacheOperation(
@@ -352,7 +373,7 @@ class CapOperations {
352
373
  // Safely store in cache
353
374
  const wrappedValue = {
354
375
  value: response,
355
- tags: this.tagResolver.resolveTags(req.cacheOptions.tags, response, { ...req.params, hash: this.keyManager.createContentHash(req) }),
376
+ tags,
356
377
  timestamp: Date.now()
357
378
  };
358
379
 
@@ -407,6 +428,7 @@ class CapOperations {
407
428
 
408
429
  const hasCachedValue = hasCachedValueResult.success && hasCachedValueResult.result;
409
430
  const cacheLatency = this.getElapsedMs(startTime);
431
+ const tagParams = { ...query.params, hash: this.keyManager.createContentHash(query) };
410
432
  const metadata = {
411
433
  dataType: 'Query',
412
434
  operation: 'SELECT',
@@ -423,21 +445,12 @@ class CapOperations {
423
445
  serviceName: srv?.name,
424
446
  path: query.path,
425
447
  }),
426
- cacheOptions: JSON.stringify(options)
448
+ cacheOptions: JSON.stringify(options),
449
+ tags: this.tagResolver.resolveTags(options.tags, undefined, tagParams) || [],
427
450
  };
428
451
  const cacheErrors = [];
429
452
 
430
453
  if (hasCachedValue) {
431
- // Cache hit
432
- const hitStatsResult = await this.safeCacheOperation(
433
- () => this.statistics.recordHit(cacheLatency, query.cacheKey, metadata),
434
- 'recordHit',
435
- { key: query.cacheKey, latency: cacheLatency }
436
- );
437
- if (!hitStatsResult.success) {
438
- cacheErrors.push(hitStatsResult.error);
439
- }
440
-
441
454
  const getResult = await this.safeCacheOperation(
442
455
  () => this.cache.send("GET", { key: query.cacheKey }),
443
456
  'get',
@@ -445,6 +458,19 @@ class CapOperations {
445
458
  );
446
459
 
447
460
  if (getResult.success && getResult.result?.value !== undefined) {
461
+ if (Array.isArray(getResult.result.tags) && getResult.result.tags.length > 0) {
462
+ metadata.tags = getResult.result.tags;
463
+ }
464
+
465
+ const hitStatsResult = await this.safeCacheOperation(
466
+ () => this.statistics.recordHit(cacheLatency, query.cacheKey, metadata),
467
+ 'recordHit',
468
+ { key: query.cacheKey, latency: cacheLatency }
469
+ );
470
+ if (!hitStatsResult.success) {
471
+ cacheErrors.push(hitStatsResult.error);
472
+ }
473
+
448
474
  return {
449
475
  result: getResult.result.value,
450
476
  cacheKey: query.cacheKey,
@@ -458,6 +484,8 @@ class CapOperations {
458
484
  try {
459
485
  const data = await srv.run(query);
460
486
  const totalLatency = this.getElapsedMs(startTime);
487
+ const tags = this.tagResolver.resolveTags(options.tags, data, tagParams) || [];
488
+ metadata.tags = tags;
461
489
 
462
490
  // Safely record miss statistics
463
491
  const missStatsResult = await this.safeCacheOperation(
@@ -472,7 +500,7 @@ class CapOperations {
472
500
  // Safely store in cache
473
501
  const wrappedValue = {
474
502
  value: data,
475
- tags: this.tagResolver.resolveTags(options.tags, data, { ...query.params, hash: this.keyManager.createContentHash(query) }),
503
+ tags,
476
504
  timestamp: Date.now()
477
505
  };
478
506
 
@@ -590,7 +618,11 @@ class CapOperations {
590
618
  path: req.http?.req?.path,
591
619
  url: req.http?.req?.url
592
620
  }),
593
- cacheOptions: JSON.stringify(req.cacheOptions)
621
+ cacheOptions: JSON.stringify(req.cacheOptions),
622
+ tags: this.tagResolver.resolveTags(req.cacheOptions?.tags, undefined, {
623
+ ...req.params,
624
+ hash: this.keyManager.createContentHash(req),
625
+ }) || [],
594
626
  };
595
627
 
596
628
  return metadata;