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 CHANGED
@@ -1,39 +1,155 @@
1
1
  # Welcome to cds-caching
2
+ [![npm version](https://img.shields.io/npm/v/cds-caching)](https://www.npmjs.com/package/cds-caching/common)
3
+ [![monthly downloads](https://img.shields.io/npm/dm/cds-caching)](https://www.npmjs.com/package/cds-caching)
2
4
 
3
5
  ## Overview
4
6
 
5
7
  This plugin for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap/docs/) provides a caching service to improve performance in CAP applications.
8
+
6
9
  While CAP in general performs well for most use cases, caching can help with:
7
10
  - Slow remote service calls
8
- - Complex calculations
9
- - Heavy queries
10
- - External API integration
11
+ - Complex operations
12
+ - Slow queries
13
+ - Other performance bottle necks
11
14
 
12
- While caching can help with these, it also adds complexity and should be used judiciously.
15
+ While cds-caching can be a big helper, an additional caching layer also adds complexity and should be used judiciously.
13
16
 
14
17
  Please also read the introduction blog post in the SAP Community: [Boosting performance in SAP Cloud Application Programming Model (CAP) applications with cds-caching](https://community.sap.com/t5/technology-blogs-by-members/boosting-performance-in-sap-cloud-application-programming-model-cap/ba-p/14002015).
15
18
 
16
- ### Caching vs. Replication
17
-
18
- It's important to understand the difference between **caching** and **data replication**:
19
-
20
- * **Caching** temporarily stores data to reduce latency and improve response times. It's ideal for read-heavy workloads but does not maintain data integrity or understand data semantics.
21
-
22
- * **Replication** creates full, persistent copies of remote data within your application to ensure availability and enable seamless data sharing across systems. It focuses on resilience rather than performance optimization.
23
-
24
- cds-caching is specifically designed for efficient caching, not data replication.
25
-
26
19
  ### Key Features
27
20
 
28
21
  * **Flexible Key-Value Store** – Store and retrieve data using simple key-based access.
29
22
  * **CachingService** – A cds.Service implementation with an intuitive API for seamless integration into CAP.
30
- * **Event Handling** – Monitor and react to cache events, such as before/after storage and retrieval.
23
+ * **Read-Through Capabilities** – Let the caching service handle the cache set and get operatios for you
31
24
  * **CAP-specific Caching** – Effortlessly cache CQN queries or CAP cds.Requests using code or the @cache annotation.
32
25
  * **TTL Support** – Automatically manage data expiration with configurable time-to-live (TTL) settings.
33
26
  * **Tag Support** – Use dynamic tags for flexible cache invalidation options.
34
27
  * **Pluggable Storage Options** – Choose between in-memory caching, SQLite or Redis.
35
28
  * **Compression** – Compress cached data to save memory using LZ4 or GZIP.
36
- * **Integrated Statistics** – Monitor cache performance with hit rates, latencies, and more.
29
+ * **Integrated Metrics** – Monitor cache performance with hit rates, latencies, and more.
30
+ * **API** – Access basic cache operations and metrics via API
31
+ * **Event Handling** – Monitor and react to cache events, such as before/after storage and retrieval.
32
+
33
+ ### Checkout detailed information on how to use cds-caching
34
+
35
+ > - [Programmatic API](docs/programmatic-api.md)
36
+ > - [Key Management](docs/key-management.md)
37
+ > - [Metrics Guide](docs/metrics-guide.md)
38
+ > - [OData API Reference](docs/odata-api.md)
39
+
40
+ ## 🚨 Breaking Changes: Migrating from cds-caching 0.x
41
+
42
+ > **⚠️ Important:** Version 1.x contains breaking changes. Please review the migration guide below.
43
+
44
+ ### 🔄 API Changes for read-through methods
45
+
46
+ Version 1.x introduces new methods that provide more insights into the read-through caching as they also directly return the genrated cache `key` and some caching `metadata`. The should be preferrably used instead of the old methods.
47
+
48
+ | **Old Method** | **New Method** | **Key Differences** |
49
+ |----------------|----------------|---------------------|
50
+ | `cache.run()` | `cache.rt.run()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
51
+ | `cache.send()` | `cache.rt.send()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
52
+ | `cache.wrap()` | `cache.rt.wrap()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
53
+ | `cache.exec()` | `cache.rt.exec()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
54
+
55
+ ### 🔑 Key Template Changes
56
+
57
+ **Before (0.x):**
58
+ ```javascript
59
+ // Old syntax - object with template property
60
+ await cache.set(query, result, {
61
+ key: { template: "user:{user}:{hash}" }
62
+ })
63
+ ```
64
+
65
+ **After (1.x):**
66
+ ```javascript
67
+ // New syntax - direct string template
68
+ await cache.set(query, result, {
69
+ key: "user:{user}:{hash}"
70
+ })
71
+ ```
72
+
73
+ ### 🌍 Context Awareness Changes
74
+
75
+ **Default Behavior Changed:**
76
+ - **0.x:** Context (user, tenant, locale) was automatically included in some cache keys (ODataRequests)
77
+ - **1.x:** Context is **disabled by default** and can be enabled for **ALL** keys (unless overwritten)
78
+
79
+ **To Enable Context Awareness:**
80
+ ```json
81
+ {
82
+ "cds": {
83
+ "requires": {
84
+ "caching": {
85
+ ...
86
+ "keyManagement": {
87
+ "isUserAware": true, // Include user context in cache keys
88
+ "isTenantAware": true, // Include tenant context in cache keys
89
+ "isLocaleAware": false // Include locale context in cache keys
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### 📚 Migration Examples
98
+
99
+ **Example 1: Basic Caching**
100
+ ```javascript
101
+ // ❌ Old way (deprecated, but will still work)
102
+ const result = await cache.run(query, db)
103
+
104
+ // ✅ New way
105
+ const { result, cacheKey, metadata } = await cache.rt.run(query, db)
106
+ ```
107
+
108
+ **Example 2: Function Wrapping**
109
+ ```javascript
110
+ // ❌ Old way (deprecated, but will still work)
111
+ const cachedFn = cache.wrap("key", expensiveOperation)
112
+ const result = await cachedFn("param1", "param2")
113
+
114
+ // ✅ New way
115
+ const cachedFn = cache.rt.wrap("key", expensiveOperation)
116
+ const { result, cacheKey, metadata } = await cachedFn("param1", "param2")
117
+ ```
118
+
119
+ **Example 3: Custom Key Templates**
120
+ ```javascript
121
+ // ❌ Old way (will not work anymore)
122
+ await cache.set(data, value, {
123
+ key: { template: "user:{user}:{hash}" }
124
+ })
125
+
126
+ // ✅ New way
127
+ await cache.set(data, value, {
128
+ key: "user:{user}:{hash}"
129
+ })
130
+ ```
131
+
132
+ ### 🔍 What's New
133
+
134
+ - **Enhanced Metadata:** All read-through operations now return cache keys and performance metadata
135
+ - **Better Performance:** Context awareness is opt-in, reducing unnecessary key complexity
136
+ - **Improved Debugging:** Access to generated cache keys for troubleshooting
137
+ - **Flexible Configuration:** Global and per-operation key template control
138
+
139
+ For detailed API documentation, see [Programmatic API Reference](docs/programmatic-api.md).
140
+
141
+ ### Example Application
142
+
143
+ The cds-caching plugin includes a comprehensive example application demonstrating various caching use cases and a UI5-based dashboard for monitoring cache performance.
144
+
145
+ ![Cache Dashboard](./docs/dashboard.jpg)
146
+
147
+ The example consists of:
148
+ - **Backend Application** (`examples/app/`) - A CAP application showing annotation-based and programmatic caching patterns
149
+ - **Dashboard** (`examples/dashboard/`) - A UI5-based monitoring interface with real-time metrics, key-level analytics, and historical data
150
+
151
+ [See the full Example Application Guide →](docs/example-app.md)
152
+
37
153
 
38
154
  ### Installation
39
155
 
@@ -43,56 +159,136 @@ Installing and using cds-caching is straightforward since it's a CAP plugin. Sim
43
159
  npm install cds-caching
44
160
  ```
45
161
 
46
- Next, add a caching service configuration to your package.json. You can even define **multiple caching services**, which is recommended if you need to cache different types of data within your application.
162
+ ### Configuration
47
163
 
48
- ```javascript
164
+ The cds-caching plugin supports comprehensive configuration through `package.json`. Here are all available configuration options:
165
+
166
+ #### Basic Service Configuration
167
+
168
+ **Minimal setup** (in-memory cache for development):
169
+
170
+ ```json
49
171
  {
50
172
  "cds": {
51
173
  "requires": {
52
174
  "caching": {
53
175
  "impl": "cds-caching",
54
- "namespace": "my::app::caching"
176
+ "namespace": "caching"
55
177
  },
56
- // Optional: Define a specific caching service for Business Partner API
178
+ // Recommended: Define a specific caching service for different caching requirements
57
179
  "bp-caching": {
58
180
  "impl": "cds-caching",
59
- "namespace": "my::app::bp-caching"
181
+ "namespace": "bp-caching"
60
182
  }
61
183
  }
62
184
  }
63
185
  }
64
186
  ```
65
187
 
66
- ### Advanced Configuration
188
+ **Advanced configuration** with all options:
67
189
 
68
- For more control, you can specify additional options:
69
-
70
- ```javascript
190
+ ```json
71
191
  {
72
192
  "cds": {
73
193
  "requires": {
74
194
  "caching": {
75
195
  "impl": "cds-caching",
76
- "namespace": "my::app::caching",
77
- "store": "in-memory", // "in-memory" or "sqlite" or "redis"
196
+ "namespace": "caching",
197
+ "store": "in-memory", // "in-memory", "sqlite", or "redis"
78
198
  "compression": "lz4", // "lz4" or "gzip"
79
- "credentials": { // if store is redis or sqlite
80
-
81
- // Redis specific
199
+ "throwOnErrors": false, // Whether basic operations should throw errors (default: false)
200
+ "credentials": {
201
+ // Redis configuration
82
202
  "host": "localhost",
83
203
  "port": 6379,
84
204
  "password": "optional",
85
- "uri": "redis://..." // Alternative: Redis connection URI
86
-
87
- // SQLite specific
88
- "url": "sqlite://./cache.sqlite"
205
+ "url": "redis://..." // Alternative: Redis connection URI
206
+
207
+ // SQLite configuration
208
+ "url": "sqlite://./cache.sqlite",
89
209
  "table": "cache",
90
210
  "busyTimeout": 10000
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ #### Read-Through (RT) Key Configuration
219
+
220
+ Configure default key templates for read-through operations:
221
+
222
+ ```json
223
+ {
224
+ "cds": {
225
+ "requires": {
226
+ "caching": {
227
+ ...
228
+ "keyManagement": {
229
+ "isUserAware": true, // Include user context in cache keys
230
+ "isTenantAware": true, // Include tenant context in cache keys
231
+ "isLocaleAware": false // Include locale context in cache keys
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ ```
238
+
239
+ **Default behavior** (if not configured): All context elements are disabled by default.
240
+
241
+ #### Error Handling Configuration
242
+
243
+ Configure how the caching service handles errors:
244
+
245
+ ```json
246
+ {
247
+ "cds": {
248
+ "requires": {
249
+ "caching": {
250
+ ...
251
+ "throwOnErrors": true, // Basic operations (set, get, delete, has) throw errors
252
+ // Default: false - operations return undefined/null instead of throwing
253
+ }
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ **Error Handling Behavior:**
260
+
261
+ - **Basic Operations** (`set`, `get`, `delete`, `has`):
262
+ - When `throwOnErrors: false` (default): Operations return `undefined`/`null` on errors
263
+ - When `throwOnErrors: true`: Operations throw errors for connection issues, etc.
264
+
265
+ - **Read-Through Operations** (`rt.run`, `rt.send`, `rt.wrap`, `rt.exec`):
266
+ - Never throw errors, regardless of `throwOnErrors` setting
267
+ - Include `cacheErrors` array in response when errors occur
268
+ - Always fetch from remote service when cache operations fail
269
+ - Log errors for monitoring and debugging
270
+
271
+ #### Environment-Specific Configuration
272
+
273
+ You can override settings for different environments:
274
+
275
+ ```json
276
+ {
277
+ "cds": {
278
+ "requires": {
279
+ "caching": {
280
+ "impl": "cds-caching",
281
+ "store": "redis",
282
+ "[development]": {
283
+ "credentials": {
284
+ "host": "localhost",
285
+ "port": 6379
286
+ }
91
287
  },
92
- "statistics": {
93
- "enabled": true,
94
- "persistenceInterval": 60000, // Optional: Interval for statistics persistence
95
- "maxLatencies": 1000 // Optional: Maximum number of latencies to track
288
+ "[production]": {
289
+ "credentials": {
290
+ "url": "redis://production-redis:6379"
291
+ }
96
292
  }
97
293
  }
98
294
  }
@@ -100,6 +296,19 @@ For more control, you can specify additional options:
100
296
  }
101
297
  ```
102
298
 
299
+ For detailed information on RT key generation and advanced configuration options, see [Key Management](docs/key-management.md) and [Programmatic API Reference](docs/programmatic-api.md).
300
+
301
+ ### Service Definition
302
+
303
+ Add the following cds definition to your data model:
304
+
305
+ ```
306
+ using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';
307
+
308
+ // Don't forget to protect the service, e.g.
309
+ annotate CachingApiService with @requires: 'authenticated-user';
310
+ ```
311
+
103
312
  ### Real-World Usage and Deployment
104
313
 
105
314
 
@@ -124,6 +333,7 @@ cds-caching provides 3 storage options:
124
333
  - Works across multiple app instances, making it ideal for scalable applications
125
334
  - Available on SAP BTP via hyperscaler options (e.g., AWS, Azure, Google Cloud)
126
335
  - Even trial accounts provide Redis access
336
+ - Redis will be non-blocking
127
337
 
128
338
  #### Redis Development Setup
129
339
 
@@ -200,8 +410,20 @@ resources:
200
410
 
201
411
  ### Usage Patterns
202
412
 
203
- The caching service provides a flexible API for caching data in CAP applications. Here are the key usage patterns:
204
- #### 1. Low-Level Key-Value API
413
+ > ⚠️ **Deprecation Notice**: The following methods are deprecated since version 1.0 and will be removed in a future version:
414
+ > - `cache.run()` - use `cache.rt.run()` instead
415
+ > - `cache.exec()` - use `cache.rt.exec()` instead
416
+ > - `cache.wrap()` - use `cache.rt.wrap()` instead
417
+ > - `cache.send()` - use `cache.rt.send()` instead
418
+ >
419
+ > The `rt.xxx` methods provide enhanced functionality including:
420
+ > - **Read-through metadata**: Information about cache hits/misses and latency
421
+ > - **Consistent return format**: All methods return `{ result, cacheKey, metadata }` by default
422
+ >
423
+ > **Migration**: Simply replace `cache.method()` with `cache.rt.method()` and access the result via `.result` property if needed.
424
+
425
+ The caching service provides a flexible API for caching data in CAP applications ([full API](docs/programmatic-api.md)). Here are the key usage patterns:
426
+ #### 1. Low-Level Key-Value API for Read-Aside Caching
205
427
 
206
428
  The most basic way to use cds-caching is through its key-value API:
207
429
 
@@ -210,28 +432,53 @@ The most basic way to use cds-caching is through its key-value API:
210
432
  const cache = await cds.connect.to("caching")
211
433
 
212
434
  // Store a value (can be any object)
213
- await cache.set("key", "value")
435
+ await cache.set("bp:1000001", businessPartnerData)
214
436
 
215
437
  // Retrieve the value
216
- await cache.get("key") // => value
438
+ await cache.get("bp:1000001") // => businessPartnerData
217
439
 
218
440
  // Check if the key exists
219
- await cache.has("key") // => true/false
441
+ await cache.has("bp:1000001") // => true/false
220
442
 
221
443
  // Delete the key
222
- await cache.delete("key")
444
+ await cache.delete("bp:1000001")
223
445
 
224
446
  // Clear the whole cache
225
447
  await cache.clear()
226
448
  ```
227
449
 
450
+ **Error Handling for Basic Operations:**
451
+
452
+ ```javascript
453
+ // With throwOnErrors: false (default)
454
+ try {
455
+ const value = await cache.get("bp:1000001")
456
+ if (value === undefined) {
457
+ // Handle cache miss or error
458
+ console.log("Value not found or cache error occurred")
459
+ }
460
+ } catch (error) {
461
+ // Only thrown for non-cache related errors
462
+ console.error("Unexpected error:", error)
463
+ }
464
+
465
+ // With throwOnErrors: true
466
+ try {
467
+ const value = await cache.get("bp:1000001")
468
+ // Value will be undefined if not found, but errors will be thrown
469
+ } catch (error) {
470
+ // Errors thrown for connection issues, etc.
471
+ console.error("Cache error:", error)
472
+ }
473
+ ```
474
+
228
475
  #### 2. CQN Query Caching
229
476
 
230
477
  For more advanced CAP integration, cache CAP's CQN queries directly. By passing in the query, a dynamic key is generated based on the CQN structure of the query. Note, that passing in queries with dynamic parameters (e.g. `SELECT.from(Foo).where({id: 1})`) will result in a different key for each query execution.
231
478
 
232
479
  ```javascript
233
480
  // Create and execute a CQN query
234
- const query = SELECT.from(Foo)
481
+ const query = SELECT.from(BusinessPartners).where({ businessPartnerType: '2' })
235
482
  const result = await db.run(query)
236
483
 
237
484
  // Cache the result
@@ -240,20 +487,43 @@ await cache.set(query, result)
240
487
  // Retrieve from cache using the same query
241
488
  const cachedResult = await cache.get(query)
242
489
  ```
243
- Handling the cache manually via read-aside pattern is possible, but the caching service provides a more convenient way to cache and retrieve CQN queries. By using the `run` method, the caching service will transparently cache the result of the query and return the cached result if available for all further requests.
490
+ Handling the cache manually via read-aside pattern is possible, but the caching service provides a more convenient way to cache and retrieve CQN queries. By using the `rt.run` method, the caching service will transparently cache the result of the query and return the cached result if available for all further requests.
244
491
 
245
492
  ```javascript
246
- const query = SELECT.from(Foo)
493
+ const query = SELECT.from(BusinessPartners).where({ businessPartnerType: '2' })
247
494
 
248
495
  // Runs the query internally and caches the result
249
- const result = await cache.run(query, db)
496
+ const { result } = await cache.rt.run(query, db)
497
+ ```
498
+
499
+
500
+ Because the cache key has been dynamically created at runtime, it will also be returned:
501
+
502
+ ```javascript
503
+ // Access the cacheKey for later usage
504
+ const { result, cacheKey } = await cache.rt.run(query, db)
250
505
  ```
251
506
 
252
- This will transparently cache the result of the query and return the cached result if available for all further requests.
507
+ **Error Handling for Read-Through Operations:**
508
+
509
+ Read-through operations never throw errors, even when cache operations fail. Instead, they include error information in the response:
510
+
511
+ ```javascript
512
+ // Read-through operations always return a result, even on cache errors
513
+ const { result, cacheKey, metadata, cacheErrors } = await cache.rt.run(query, db)
514
+
515
+ if (cacheErrors && cacheErrors.length > 0) {
516
+ console.log("Cache errors occurred:", cacheErrors)
517
+ // Result will be fetched from remote service despite cache errors
518
+ }
519
+
520
+ // The result is always available, regardless of cache errors
521
+ return result
522
+ ```
253
523
 
254
524
  #### 3. RemoteService Request-Level Caching
255
525
 
256
- Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls or even application services. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
526
+ Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls or even application services. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale (if not configured otherwise).
257
527
 
258
528
  ```javascript
259
529
  // Cache the requests to an exposed external entity
@@ -262,18 +532,19 @@ this.on('READ', BusinessPartners, async (req, next) => {
262
532
  let value = await cache.get(req)
263
533
  if(!value) {
264
534
  value = await bupa.run(req)
265
- await cache.set(req, value, { ttl: 3600 })
535
+ await cache.set(req, value, { ttl: 30000 })
266
536
  }
267
537
  return value
268
538
  })
269
539
  ```
270
540
 
271
- Alternatively use read-through caching via the `run` method to let the caching service handle the caching transparently:
541
+ Alternatively use read-through caching via the `rt.run` method to let the caching service handle the caching transparently:
272
542
 
273
543
  ```javascript
274
544
  this.on('READ', BusinessPartners, async (req, next) => {
275
545
  const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
276
- return await cache.run(req, bupa)
546
+ const { result } = await cache.rt.run(req, bupa)
547
+ return result
277
548
  })
278
549
 
279
550
  ```
@@ -283,7 +554,7 @@ This will transparently cache the result of the request and return the cached re
283
554
  ### 4. ApplicationService Request-Level Caching
284
555
 
285
556
 
286
- > Caching an entire entity should be used with caution, as it will cache all permutations of requests ($filter, $expand, $orderby, etc.) on the entity, which will lead to a huge number of cache entries. Use this only for entities where you can guarantee a low number of different queries.
557
+ > Caching an entire entity should be used with caution, as it will cache all permutations of requests ($select, $filter, $expand, $orderby, etc.) on the entity, which may lead to a huge number of cache entries. Use this only for entities where you can guarantee a low number of different queries.
287
558
 
288
559
 
289
560
  But not only external services can be cached, it's also possible to cache requests against an ApplicationService.
@@ -299,7 +570,8 @@ class MyService extends cds.ApplicationService {
299
570
  const { MyEntity } = this.entities;
300
571
  this.on('READ', MyEntity, async (req, next) => {
301
572
  const cache = cds.connect.to("caching");
302
- return cache.run(req, next);
573
+ const { result } = await cache.rt.run(req, next)
574
+ return result;
303
575
  });
304
576
  });
305
577
  return super.init()
@@ -314,7 +586,7 @@ Alternatively to doing this via code, you can use annotations to enable caching
314
586
  ```
315
587
  service MyService {
316
588
  @cache: {
317
- ttl: 3600
589
+ ttl: 10000 // 10 seconds
318
590
  }
319
591
  entity BusinessPartners as projection on BusinessPartner {
320
592
  // ... entity definition
@@ -322,7 +594,7 @@ service MyService {
322
594
 
323
595
 
324
596
  @cache: {
325
- ttl: 1800,
597
+ ttl: 100000, // 10 seconds
326
598
  tags: [{
327
599
  template: 'user-{user}'
328
600
  }]
@@ -337,35 +609,55 @@ While not directly related to CAP functionality, the caching service provides tw
337
609
 
338
610
  ```javascript
339
611
  // Using wrap() to create a cached version of a function
340
- const expensiveOperation = async (value) => {
341
- // ... some expensive computation
342
- return result
612
+ const fetchBusinessPartnerData = async (businessPartnerId, includeAddresses) => {
613
+ // ... some expensive computation to fetch BP data
614
+ return businessPartnerData
343
615
  }
344
616
 
345
- // Creates a cached version of the function
346
- const cachedOperation = cache.wrap("key", expensiveOperation, {
617
+ // Creates a cached version of the function.
618
+ const cachedBpOperation = cache.rt.wrap("bp-data", fetchBusinessPartnerData, {
347
619
  ttl: 3600,
348
- tags: ['computation']
620
+ tags: ['business-partner']
349
621
  })
350
622
 
351
623
  // Each call checks cache first, only executes if cache miss
352
- const result = await cachedOperation("input")
624
+ const result = await cachedBpOperation("1000001", true)
353
625
 
354
626
  // Using exec() for immediate execution with caching
355
- const result = await cache.exec("key", async () => {
356
- // ... some expensive computation
357
- return result
358
- }, {
627
+ const result = await cache.rt.exec("product-data", async (productId) => {
628
+ // ... some expensive computation to fetch product data
629
+ return productData
630
+ }, ["1000001"], {
359
631
  ttl: 3600,
360
- tags: ['computation']
632
+ tags: ['product']
361
633
  })
362
634
  ```
363
635
 
364
- The key differences between `wrap()` and `exec()`:
365
- - `wrap()` returns a new function that includes caching logic
366
- - `exec()` immediately executes the function and caches the result
367
- - Use `wrap()` when you need to reuse the cached function multiple times
368
- - Use `exec()` for one-off executions with caching
636
+ The key differences between `rt.wrap()` and `rt.exec()`:
637
+ - `rt.wrap()` returns a new function that includes caching logic
638
+ - `rt.exec()` immediately executes the function and caches the result
639
+ - Use `rt.wrap()` when you need to reuse the cached function multiple times
640
+ - Use `rt.exec()` for one-off executions with caching
641
+
642
+ #### Dynamic Key Generation
643
+
644
+ All `rt.xxx` methods automatically generate dynamic cache keys based on function arguments (`wrap`, `exec`) and request/query parameters. This ensures that different function calls with different arguments are cached separately.
645
+
646
+ ```javascript
647
+ // Different arguments = different cache keys
648
+ const result1 = await cachedBpOperation("1000001", true) // Cache key: "bp-data:1000001:true"
649
+ const result2 = await cachedBpOperation("1000002", false) // Cache key: "bp-data:1000002:false"
650
+ ```
651
+
652
+ You can override this behavior by providing a custom key template:
653
+
654
+ ```javascript
655
+ const cachedOperation = cache.rt.wrap("bp-profile", fetchBusinessPartnerData, {
656
+ key: "profile:{args[0]}:{args[1]}"
657
+ })
658
+ ```
659
+
660
+ For detailed information on how read-through keys are generated and configured, see [Key Management](docs/key-management.md).
369
661
 
370
662
  ### Cache Invalidation Strategies
371
663
 
@@ -383,19 +675,19 @@ The TTL can be specified for individually through all cache methods (e.g. `set`,
383
675
  await cache.set("key", "value", { ttl: 60000 })
384
676
 
385
677
  // Run with 30 seconds TTL
386
- const result = await cache.run(query, db, { ttl: 30000 })
678
+ const { result } = await cache.rt.run(query, db, { ttl: 30000 })
387
679
 
388
680
  // Send with 10 seconds TTL
389
- const result = await cache.send(request, service, { ttl: 10000 })
681
+ const { result } = await cache.rt.send(request, service, { ttl: 10000 })
390
682
 
391
683
  // Wrap with 10 seconds TTL
392
- const cachedOperation = cache.wrap("key", expensiveOperation, { ttl: 10000 })
684
+ const cachedOperation = cache.rt.wrap("key", expensiveOperation, { ttl: 10000 })
393
685
 
394
686
  // Exec with 10 seconds TTL
395
- const result = await cache.exec("key", async () => {
687
+ const { result } = await cache.rt.exec("key", async () => {
396
688
  // ... some expensive computation
397
689
  return result
398
- }, {
690
+ }, [] {
399
691
  ttl: 10000
400
692
  })
401
693
  ```
@@ -408,43 +700,42 @@ Key-based invalidation is a way to invalidate cache entries based on a specific
408
700
  await cache.delete("key")
409
701
  ```
410
702
 
411
- Keys are critical for cache invalidation. To allow custom key management, you can override the auto-generated key. This option is available for all essential methods (e.g cache.set, cache.run, cache.send, cache.createKey) and for the annotations.
703
+ Keys are critical for cache invalidation. To allow custom key management, you can override the auto-generated key. This option is available for all essential methods (e.g cache.set, cache.rt.run, cache.rt.send, cache.createKey) and for the annotations.
704
+
705
+ **Read-Through (RT) Methods**: All `rt.xxx` methods automatically generate dynamic cache keys and return them in the response. The generated keys include configurable context (user, tenant, locale) and a content hash. For detailed information on RT key generation, see [Key Management](docs/key-management.md).
412
706
 
413
707
  ```javascript
414
708
  // No key override given, string will just be used as keys
415
- await cache.set('key', 'value') // key: key
709
+ await cache.set('bp:1000001', businessPartnerData) // key: bp:1000001
416
710
 
417
711
  // No key override given, objects will be smartly hashed
418
- await cache.set(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
712
+ await cache.set(SELECT.from(BusinessPartners)) // key: bd3f3690d3e96a569bd89d9e207a89af
419
713
 
420
714
  // Automatically build the key for retrieval/deletion
421
- cache.createKey(SELECT.from(Foo)) // key: bd3f3690d3e96a569bd89d9e207a89af
715
+ cache.createKey(SELECT.from(BusinessPartners)) // key: bd3f3690d3e96a569bd89d9e207a89af
422
716
 
423
717
  // Override and use your own key based on a fixed value
424
- await cache.set(SELECT.from(Foo, 1), { key: { value: "foo:1" } })
718
+ await cache.set(SELECT.from(BusinessPartners, 1000001), { key: "bp:1000001" })
425
719
 
426
- // Override and only for requests, use request context information
427
- await cache.run(req, remoteService, { key: { template: "mykey:{tenant}:{user}:{locale}:{hash}" } })
720
+ // RT methods return the generated cache key
721
+ const { result, cacheKey } = await cache.rt.run(query, db)
722
+ console.log('Generated key:', cacheKey) // e.g., "tenant-acme:user-john:locale-en:hash-abc123"
428
723
 
429
- // This requests will be cached for all users and for each locale
430
- await cache.set(req, remoteService, { key: { template: "mykey:{user}:{locale}:{hash}" } })
431
- ```
724
+ // Override RT key template for requests
725
+ await cache.rt.run(req, remoteService, { key: "mykey:{tenant}:{user}:{locale}:{hash}" })
432
726
 
433
- Overriding keys support the following configuration options:
434
- - `value` generates a static value
435
- - `prefix` – will add this piece at the beginning
436
- - `suffix` - will ad this piece at the end
437
- - `template` - will set a value filled with placeholders, available placeholders are (only relevant for cds.Requests):
438
- - `{user}`: The current user
439
- - `{tenant}`: The current tenant
440
- - `{locale}`: The current locale
441
- - `{hash}`: The hash of the request query/params/data/path/etc.
727
+ // This requests will be cached for all users and for each locale
728
+ await cache.rt.run(req, remoteService, { key: "mykey:{user}:{locale}:{hash}" })
442
729
 
443
- With well-structured keys, invalidating cache entries becomes a lot easier. However, for more complex scenarios tags provide an even more effective solution, as tags can automatically be created based on the cached data.
730
+ // Function wrapping with custom key template
731
+ const cachedFunction = cache.rt.wrap("user-data", expensiveOperation, {
732
+ key: "user:{user}:{args[0]}"
733
+ })
734
+ ```
444
735
 
445
736
  #### 3. Tag-Based
446
737
 
447
- Tags are a way to invalidate cache entries based on a specific tag. Tags need to be provided explicitly when storing a value in the cache and are supported for all cache methods (e.g. `set`, `run`, `send`, `wrap`, `exec`).
738
+ Tags are a way to invalidate cache entries based on a specific tag. Tags need to be provided explicitly when storing a value in the cache and are supported for all cache methods (e.g. `set`, `rt.run`, `rt.send`, `rt.wrap`, `rt.exec`).
448
739
  Tags can be provided as an array of strings or as an array of objects with the following properties:
449
740
  - `value`: The value to use for the tag.
450
741
  - `data`: A field from the value to use for the tag. This is working for objects and arrays of objects.
@@ -461,22 +752,22 @@ Templates support the following properties:
461
752
 
462
753
  ```javascript
463
754
  // Store with static tag
464
- await cache.set("key", "value", {
465
- tags: [{ value: "user-123" }]
755
+ await cache.set("bp:1000001", businessPartnerData, {
756
+ tags: [{ value: "bp-1000001" }]
466
757
  })
467
758
 
468
759
  // Store with template tag (will generate a tag like "tenant-global-user-anonymous")
469
- await cache.set("key", "value", {
760
+ await cache.set("bp:1000001", businessPartnerData, {
470
761
  tags: [{ template: "tenant-{tenant}-user-{user}" }]
471
762
  })
472
763
 
473
764
  // Store with data-based tag
474
- await cache.set("key", { id: 123, name: "Product" }, {
475
- tags: [{ data: "id", prefix: "product-" }]
765
+ await cache.set("product:1000001", { productId: 1000001, name: "Laptop Computer" }, {
766
+ tags: [{ data: "productId", prefix: "product-" }]
476
767
  })
477
768
 
478
769
  // Invalidate by tag
479
- await cache.deleteByTag('user-123')
770
+ await cache.deleteByTag('bp-1000001')
480
771
  ```
481
772
  This is really useful for invalidating cache entries based on a specific attribute or context.
482
773
 
@@ -488,17 +779,17 @@ Dynamic tags using data `data` property are a way to invalidate cache entries ba
488
779
 
489
780
  const businessPartners = [
490
781
  {
491
- businessPartner: 1,
492
- name: 'John Doe'
782
+ businessPartner: 1000001,
783
+ name: 'Acme Corporation'
493
784
  },
494
785
  {
495
- businessPartner: 2,
496
- name: 'Jane Doe'
786
+ businessPartner: 1000002,
787
+ name: 'Tech Solutions Ltd'
497
788
  }
498
789
  ]
499
790
 
500
791
  // Store with dynamic tags
501
- await cache.set("key", businessPartners, {
792
+ await cache.set("bp-list", businessPartners, {
502
793
  tags: [
503
794
  { data: 'businessPartner', prefix: 'bp-' },
504
795
  { value: "businessPartner" }
@@ -506,17 +797,17 @@ await cache.set("key", businessPartners, {
506
797
  })
507
798
 
508
799
  // Introspect the tags
509
- const tags = await cache.tags("key") // => ["bp-1", "bp-2", "businessPartner"]
800
+ const tags = await cache.tags("bp-list") // => ["bp-1000001", "bp-1000002", "businessPartner"]
510
801
 
511
802
  // Invalidate by tag
512
- await cache.deleteByTag('bp-1')
513
- await cache.deleteByTag('bp-2')
803
+ await cache.deleteByTag('bp-1000001')
804
+ await cache.deleteByTag('bp-1000002')
514
805
  ```
515
806
 
516
- This is really usefull for caching results with multiple rows where you can't predict the tags beforehand or when you want to invalidate cache entries based on the data itself. This is also possible for the `run` method.
807
+ This is really usefull for caching results with multiple rows where you can't predict the tags beforehand or when you want to invalidate cache entries based on the data itself. This is also possible for the `rt.run` method.
517
808
 
518
809
  ```javascript
519
- const result = await cache.run(query, db, {
810
+ const result = await cache.rt.run(query, db, {
520
811
  tags: [{ data: 'businessPartner', prefix: 'bp-' }]
521
812
  })
522
813
  ```
@@ -537,6 +828,36 @@ for await (const entry of iterator) {
537
828
 
538
829
  This will return an iterator over all cache entries. You can use this to traverse all cache entries and invalidate them based on a specific condition. You should only use this for small caches (e.g. by using multiple caching services with different namespaces).
539
830
 
831
+ ### TypeScript Support
832
+
833
+ cds-caching includes comprehensive TypeScript definitions. The library is written in JavaScript but provides full TypeScript support for better development experience.
834
+
835
+ #### Basic Usage with TypeScript
836
+
837
+ ```typescript
838
+ import { CachingService, CacheOptions, ReadThroughResult } from 'cds-caching';
839
+
840
+ const cache = await cds.connect.to('caching') as CachingService;
841
+
842
+ // Basic cache operations
843
+ await cache.set('my-key', { data: 'value' }, { ttl: 3600 });
844
+ const value = await cache.get('my-key');
845
+
846
+ // Read-through operations with full type safety
847
+ const { result, cacheKey, metadata } = await cache.rt.send(request, service, {
848
+ ttl: 1800,
849
+ tags: ['user-data']
850
+ });
851
+
852
+ // Function wrapping with type inference
853
+ const cachedFunction = cache.rt.wrap('expensive-operation', async (id: string) => {
854
+ return await this.performExpensiveOperation(id);
855
+ });
856
+
857
+ const { result: operationResult } = await cachedFunction('user-123');
858
+
859
+ ```
860
+
540
861
  ### OData Service Caching Considerations
541
862
 
542
863
  While caching individual requests can improve performance, **caching an entire OData service is generally not recommended**. Here's why:
@@ -582,222 +903,314 @@ Instead of caching entire OData service responses, focus on:
582
903
  3. **Multi-Tenant**: Use appropriate namespacing and key strategies
583
904
  4. **Redis Setup**: Ensure proper configuration for production use
584
905
 
585
- ## Full API
586
-
587
- ### `cache.createKey(key: any)` : `string`
588
-
589
- Creates a key from a string or an object. This method is used internally when passing keys to the cache methods, so you don't need to call it directly other then to retrieve the dynamic generated key for a given object.
590
-
591
- #### `key: any`
592
-
593
- The key to create the key from. The key can be a string or an object. If an object is used, it will be hashed to a string key using MD5. cds.Requests are handled explicitly as the dynamic generated key includes the user, tenant and locale and query hash.
594
-
595
- #### Returns
596
-
597
- A string key.
598
-
599
- ---
600
-
601
- ### `await cache.set(key: any, value: any[, options: object])`
602
-
603
- Sets a value in the cache.
604
-
605
- #### `key: any`
606
-
607
- The key to store the value under. The key handling is the same as for the `≈` method.
608
-
609
- #### `value: any`
610
-
611
- The value to store in the cache. The value will be serialized to a string using `JSON.stringify` (unless the value is already a string).
612
-
613
- #### `options: object`
614
-
615
- Object literal containing cache options.
616
-
617
- The following properties are accepted:
906
+ ## Enhanced Statistics & Monitoring
618
907
 
619
- | Property | Description | Example |
620
- | ------------- | ------------- | ----------
621
- | ttl | Time-to-live in milliseconds. | `1000`
622
- | key | Key override for the cache for full control over the key management (see chapter Cache Invalidation Strategies) | `{template: 'user-{user}', value: '123'}`
623
- | tags | Array of tags to associate with the value. Tags can be dynamic based on the stored cache data (see chapter Cache Invalidation Strategies) | `[{template: 'user-{user}', value: '123'}]`
908
+ The plugin now includes comprehensive statistics and monitoring capabilities that provide deep insights into cache performance and help optimize cache usage.
624
909
 
625
- ---
910
+ [See the full Metrics Guide →](docs/metrics-guide.md)
626
911
 
627
- ### `await cache.get(key: any)`
628
-
629
- Gets a value from the cache.
630
-
631
- #### `key: any`
632
-
633
- The key to retrieve the value from. The key handling is the same as for the `createKey` method.
634
-
635
- #### Returns
636
-
637
- The deserialized value from the cache or `undefined` if the value does not exist.
638
-
639
- ---
640
-
641
- ### `await cache.has(key: any)`
642
-
643
- Checks if a value exists in the cache.
644
-
645
- #### `key: any`
646
-
647
- The key to check for existence. The key handling is the same as for the `createKey` method.
648
-
649
- #### Returns
650
-
651
- `true` if the value exists in the cache, `false` otherwise.
652
-
653
- ---
654
-
655
- ### `await cache.delete(key: any)`
656
-
657
- Deletes a value from the cache.
658
-
659
- #### `key: any`
660
-
661
- The key to delete the value from. The key handling is the same as for the `createKey` method.
662
-
663
- ---
664
-
665
- ### `await cache.clear()`
666
-
667
- Clears the whole cache.
668
-
669
- ---
670
-
671
- ### `await cache.deleteByTag(tag: string)`
672
-
673
- Deletes all values from the cache that are associated with the given tag.
674
-
675
- #### `tag: string`
676
-
677
- The tag to delete the values from.
678
-
679
- ---
680
-
681
- ### `await cache.run(query: cds.CQN , service: cds.Service)`
682
-
683
- Runs a query against the provided service and caches the result for all further requests. This method is useful for read-through caching. (see Usage Patterns and [CAP docs](https://cap.cloud.sap/docs/node.js/core-services#srv-run-query) for more information)
684
-
685
- #### `object: cds.CQN`
686
-
687
- The CQN query to run.
688
-
689
- #### `service: cds.Service`
690
-
691
- The service to run the query on.
692
-
693
- #### Returns
694
-
695
- The result of the query, either from the cache or the service.
696
-
697
- ---
912
+ ### Key Features
698
913
 
699
- ### `await cache.send(request: cds.Request, service: cds.Service)`
914
+ - **Real-time Metrics**: Monitor cache performance with detailed hit rates, latencies, and throughput
915
+ - **Key-level Tracking**: Track performance metrics for individual cache keys
916
+ - **Historical Data**: Store and analyze metrics over time (hourly/daily periods)
917
+ - **Performance Analytics**: Calculate cache efficiency, error rates, and response times
918
+ - **Runtime Configuration**: Enable/disable metrics at runtime without restart
919
+ - **API Access**: Access metrics programmatically or via OData service
700
920
 
701
- Sends a request to a cds.Service and caches the result. In contrast to the `run` method, this method is useful for caching full cds.Requests.
921
+ ### Metrics Overview
702
922
 
703
- #### `request: cds.Request`
923
+ cds-caching provides two types of metrics:
704
924
 
705
- The request to send.
925
+ #### 1. General Cache Metrics
926
+ Track overall cache performance including:
927
+ - **Hit/Miss Statistics**: Total hits, misses, and hit ratios
928
+ - **Latency Metrics**: Average, min, max, and percentile latencies for hits and misses
929
+ - **Performance Metrics**: Throughput (requests/second), error rates, cache efficiency
930
+ - **Memory Usage**: Current memory consumption and item count
931
+ - **Native Operations**: Counts of direct cache operations (set, get, delete, etc.)
706
932
 
707
- #### `service: cds.Service `
933
+ #### 2. Key-level Metrics
934
+ Track performance for individual cache keys including:
935
+ - **Key-specific Statistics**: Hits, misses, and hit ratios per key
936
+ - **Context Information**: Data type, service name, entity name, operation type
937
+ - **Enhanced Metadata**: Query text, request info, function names, user/tenant context
938
+ - **Performance Tracking**: Latency and throughput metrics per key
708
939
 
709
- The service to send the request to.
940
+ ### Enabling Metrics
710
941
 
711
- #### Returns
942
+ Metrics are disabled by default to minimize performance impact. They can only be enabled/disabled via the programmatic API or OData API at runtime, not through package.json configuration.
712
943
 
713
- The result of the request, either from the cache or the service.
944
+ To enable metrics programmatically:
714
945
 
715
- ---
946
+ ```javascript
947
+ // Connect to the caching service
948
+ const cache = await cds.connect.to("caching")
716
949
 
717
- ### `await cache.wrap(key: any, fn: async function, options: object)`
950
+ // Enable metrics at runtime
951
+ await cache.setMetricsEnabled(true)
952
+ await cache.setKeyMetricsEnabled(true)
953
+ ```
718
954
 
719
- Wraps a function in a cache.
955
+ Or via OData API:
720
956
 
721
- #### `key: any`
957
+ ```http
958
+ ### Enable general metrics
959
+ POST http://localhost:4004/odata/v4/caching-api/Caches('caching')/setMetricsEnabled
960
+ Content-Type: application/json
722
961
 
723
- The key to store the cached function under. The key handling is the same as for the `createKey` method.
962
+ {
963
+ "enabled": true
964
+ }
724
965
 
725
- #### `fn: async function`
966
+ ### Enable key-level metrics
967
+ POST http://localhost:4004/odata/v4/caching-api/Caches('caching')/setKeyMetricsEnabled
968
+ Content-Type: application/json
726
969
 
727
- The async function to wrap in a cache.
970
+ {
971
+ "enabled": true
972
+ }
973
+ ```
728
974
 
729
- #### `options: object`
975
+ ### Accessing Metrics via Caching Service
730
976
 
731
- The options to use for the cache.
977
+ The caching service provides comprehensive metrics collection and persistence capabilities. Metrics are automatically collected during cache operations and can be accessed both in real-time and from historical data.
732
978
 
733
- #### Returns
979
+ #### Metrics Persistence
734
980
 
735
- A cached version of the function. The cached function will check the cache first and only execute the function if the cache miss.
981
+ **Transient Metrics**: Current statistics are kept in memory and provide real-time insights into cache performance:
982
+ - Hit/miss ratios
983
+ - Current latency statistics
984
+ - Active cache entries
985
+ - Key-level performance data
736
986
 
737
- ---
987
+ **Persisted Metrics**: Historical data is automatically stored in the database for long-term analysis:
988
+ - Hourly aggregated statistics
989
+ - Key-level metrics over time
990
+ - Performance trends and patterns
991
+ - Cache efficiency analysis
738
992
 
739
- ### `await cache.exec(key: any, fn: async function, options: object)`
993
+ #### Current Statistics
740
994
 
741
- Executes a function and caches the result. This method is useful for one-off executions with caching.
995
+ ```javascript
996
+ // Connect to the caching service
997
+ const cache = await cds.connect.to("caching")
742
998
 
743
- #### `key: any`
999
+ // Get current statistics
1000
+ const stats = await cache.getCurrentStats()
1001
+ console.log('Hit ratio:', stats.hitRatio)
1002
+ console.log('Average hit latency:', stats.avgHitLatency)
1003
+ console.log('Throughput:', stats.throughput)
1004
+
1005
+ // Get current key metrics
1006
+ const keyMetrics = await cache.getCurrentKeyMetrics()
1007
+ for (const [key, metrics] of keyMetrics) {
1008
+ console.log(`Key ${key}:`, {
1009
+ hits: metrics.hits,
1010
+ misses: metrics.misses,
1011
+ hitRatio: metrics.hitRatio,
1012
+ avgHitLatency: metrics.avgHitLatency
1013
+ })
1014
+ }
1015
+ ```
744
1016
 
745
- The key to store the cached function under. The key handling is the same as for the `createKey` method.
1017
+ #### Historical Metrics
746
1018
 
747
- #### `fn: async function`
1019
+ ```javascript
1020
+ // Get metrics for a specific time period
1021
+ const from = new Date('2024-01-01')
1022
+ const to = new Date('2024-01-31')
1023
+ const historicalStats = await cache.getMetrics(from, to)
748
1024
 
749
- The async function to execute.
1025
+ // Get key-specific metrics
1026
+ const keyStats = await cache.getKeyMetrics('my-cache-key', from, to)
1027
+ ```
750
1028
 
751
- #### `options: object`
1029
+ #### Runtime Configuration
752
1030
 
753
- The options to use for the cache.
1031
+ ```javascript
1032
+ // Enable/disable metrics at runtime
1033
+ await cache.setMetricsEnabled(true)
1034
+ await cache.setKeyMetricsEnabled(true)
1035
+
1036
+ // Get current configuration
1037
+ const config = await cache.getRuntimeConfiguration()
1038
+ console.log('Metrics enabled:', config.metricsEnabled)
1039
+ console.log('Key metrics enabled:', config.keyMetricsEnabled)
1040
+
1041
+ // Clear metrics
1042
+ await cache.clearMetrics()
1043
+ await cache.clearKeyMetrics()
1044
+ ```
754
1045
 
755
- #### Returns
1046
+ ### Metrics Data Structure
756
1047
 
757
- The result of the function.
1048
+ #### General Cache Statistics (Metrics Entity)
758
1049
 
759
- ---
1050
+ ```javascript
1051
+ {
1052
+ // Entity identification
1053
+ ID: "daily:2024-01-15", // Unique identifier (period:date)
1054
+ cache: "caching", // Cache name
1055
+ timestamp: "2024-01-15T10:30:00Z", // When metrics were recorded
1056
+ period: "daily", // Aggregation period (hourly/daily/monthly)
1057
+
1058
+ // Read-through metrics
1059
+ hits: 1500, // Number of cache hits
1060
+ misses: 300, // Number of cache misses
1061
+ errors: 5, // Number of errors
1062
+ totalRequests: 1800, // Total read-through requests
1063
+
1064
+ // Read-through latency metrics (milliseconds)
1065
+ avgHitLatency: 2.5, // Average hit latency
1066
+ minHitLatency: 0.1, // Minimum hit latency
1067
+ maxHitLatency: 15.2, // Maximum hit latency
1068
+ avgMissLatency: 45.8, // Average miss latency
1069
+ minMissLatency: 12.3, // Minimum miss latency
1070
+ maxMissLatency: 120.5, // Maximum miss latency
1071
+ avgReadThroughLatency: 8.9, // Average read-through latency
1072
+
1073
+ // Read-through performance metrics
1074
+ hitRatio: 0.833, // Hit ratio as percentage (83.3%)
1075
+ throughput: 25.5, // Requests per second
1076
+ errorRate: 0.003, // Error rate as percentage (0.3%)
1077
+ cacheEfficiency: 18.3, // Miss latency / hit latency ratio
1078
+
1079
+ // Native operation metrics
1080
+ nativeSets: 200, // Number of direct set operations
1081
+ nativeGets: 800, // Number of direct get operations
1082
+ nativeDeletes: 50, // Number of direct delete operations
1083
+ nativeClears: 2, // Number of clear operations
1084
+ nativeDeleteByTags: 10, // Number of delete-by-tag operations
1085
+ nativeErrors: 1, // Number of native operation errors
1086
+ totalNativeOperations: 1063, // Total native operations
1087
+ nativeThroughput: 17.7, // Native operations per second
1088
+ nativeErrorRate: 0.001, // Native operation error rate (0.1%)
1089
+
1090
+ // System metrics
1091
+ memoryUsage: 52428800, // Memory usage in bytes
1092
+ itemCount: 150, // Number of items in cache
1093
+ uptimeMs: 7200000 // Cache uptime in milliseconds
1094
+ }
1095
+ ```
760
1096
 
761
- ### `await cache.iterator() : AsyncIterator<{ key: string, value: { value: any, tags: string[], timestamp: number } }>`
1097
+ #### Key-level Metrics (KeyMetrics Entity)
762
1098
 
763
- Returns an iterator over all cache entries.
1099
+ ```javascript
1100
+ {
1101
+ // Entity identification
1102
+ ID: "key:user-preferences:123", // Unique identifier
1103
+ cache: "caching", // Cache name
1104
+ keyName: "user-preferences:123", // Cache key name
1105
+ lastAccess: "2024-01-15T10:30:00Z", // Last access time
1106
+ period: "current", // Period type (current/hourly/daily)
1107
+ operationType: "read_through", // Operation category (read_through/native/mixed)
1108
+
1109
+ // Read-through metrics
1110
+ hits: 45, // Number of hits for this key
1111
+ misses: 5, // Number of misses for this key
1112
+ errors: 0, // Number of errors for this key
1113
+ totalRequests: 50, // Total requests for this key
1114
+ hitRatio: 0.9, // Hit ratio for this key (90%)
1115
+ cacheEfficiency: 21.2, // Cache efficiency for this key
1116
+
1117
+ // Read-through latency metrics (milliseconds)
1118
+ avgHitLatency: 1.2, // Average hit latency for this key
1119
+ minHitLatency: 0.5, // Minimum hit latency for this key
1120
+ maxHitLatency: 3.1, // Maximum hit latency for this key
1121
+ avgMissLatency: 25.4, // Average miss latency for this key
1122
+ minMissLatency: 15.2, // Minimum miss latency for this key
1123
+ maxMissLatency: 45.8, // Maximum miss latency for this key
1124
+ avgReadThroughLatency: 3.8, // Average read-through latency for this key
1125
+
1126
+ // Read-through performance metrics
1127
+ throughput: 2.5, // Requests per second for this key
1128
+ errorRate: 0.0, // Error rate for this key (0%)
1129
+
1130
+ // Native operation metrics for this key
1131
+ nativeHits: 10, // Native hits for this key
1132
+ nativeMisses: 2, // Native misses for this key
1133
+ nativeSets: 5, // Native sets for this key
1134
+ nativeDeletes: 1, // Native deletes for this key
1135
+ nativeClears: 0, // Native clears for this key
1136
+ nativeDeleteByTags: 0, // Native delete-by-tags for this key
1137
+ nativeErrors: 0, // Native errors for this key
1138
+ totalNativeOperations: 18, // Total native operations for this key
1139
+ nativeThroughput: 0.5, // Native operations per second for this key
1140
+ nativeErrorRate: 0.0, // Native error rate for this key
1141
+
1142
+ // Context and metadata
1143
+ dataType: "request", // Type of data (query/request/function/custom)
1144
+ operation: "READ", // Cache operation type
1145
+ metadata: '{"ttl":3600}', // JSON string with additional metadata
1146
+ context: '{"user":"john.doe","tenant":"acme"}', // JSON string with context
1147
+ query: "SELECT * FROM UserPreferences WHERE userId = '123'", // CQL query text
1148
+ subject: '{"entity":"UserPreferences"}', // JSON string with subject info
1149
+ target: "UserService", // Target service name
1150
+ tenant: "acme", // Tenant information
1151
+ user: "john.doe", // User information
1152
+ locale: "en-US", // Locale information
1153
+ cacheOptions: '{"ttl":3600}', // JSON string with cache options
1154
+ timestamp: "2024-01-15T09:00:00Z" // When this key was first accessed
1155
+ }
1156
+ ```
764
1157
 
765
- #### Returns
1158
+ ### Best Practices for Metrics
766
1159
 
767
- An iterator over all cache entries.
1160
+ 1. **Enable Selectively**: Only enable metrics when needed for monitoring or debugging
1161
+ 2. **Monitor Memory Usage**: Key metrics can consume significant memory for large caches
1162
+ 3. **Set Appropriate Intervals**: Balance persistence frequency with performance impact
1163
+ 4. **Use Historical Data**: Analyze trends over time to optimize cache configuration
1164
+ 5. **Monitor Error Rates**: High error rates may indicate configuration issues
1165
+ 6. **Track Cache Efficiency**: Aim for high cache efficiency (miss latency >> hit latency)
768
1166
 
769
- ---
1167
+ ## API Reference
770
1168
 
771
- ### `await cache.tags(key: any) : string[]`
1169
+ The cds-caching plugin provides two APIs for managing cache operations:
772
1170
 
773
- Returns the tags for a given key.
1171
+ - **Programmatic API** - JavaScript methods for use within your CAP application code
1172
+ - **OData API** - REST endpoints for external applications and monitoring tools
774
1173
 
775
- #### `key: any`
1174
+ ### Programmatic API
776
1175
 
777
- The key to get the tags for. The key handling is the same as for the `createKey` method.
1176
+ The programmatic API provides methods for direct cache operations within your CAP application:
778
1177
 
779
- #### Returns
1178
+ ```javascript
1179
+ // Connect to the caching service
1180
+ const cache = await cds.connect.to("caching")
780
1181
 
781
- An array of tags. If the key does not exist, an empty array is returned.
1182
+ // Basic operations
1183
+ await cache.set("key", "value")
1184
+ const value = await cache.get("key")
1185
+ await cache.delete("key")
782
1186
 
783
- ---
1187
+ // Read-through operations
1188
+ const result = await cache.rt.run(query, db)
1189
+ const result = await cache.rt.send(request, service)
784
1190
 
785
- ### `await cache.metadata(key: any) : { tags: string[], timestamp: number } | undefined`
1191
+ // Metrics and statistics
1192
+ const stats = await cache.getCurrentStats()
1193
+ const keyMetrics = await cache.getCurrentKeyMetrics()
1194
+ ```
786
1195
 
787
- Returns the metadata for a given key.
1196
+ [See the full Programmatic API Reference →](docs/programmatic-api.md)
788
1197
 
789
- #### `key: any`
1198
+ ### OData API
790
1199
 
791
- The key to get the metadata for. The key handling is the same as for the `createKey` method.
1200
+ The OData API provides REST endpoints for external applications, monitoring tools, and administrative interfaces:
792
1201
 
793
- #### Returns
1202
+ ```http
1203
+ ### Get cache statistics
1204
+ GET /odata/v4/caching-api/Metrics?$filter=cache eq 'mycache'
794
1205
 
795
- An object containing the metadata for the given key or `undefined` if the key does not exist. The metadata object contains the following properties:
1206
+ ### Get cache entries
1207
+ GET /odata/v4/caching-api/Caches('mycache')/getEntries()
796
1208
 
797
- - `tags`: An array of tags.
798
- - `timestamp`: The timestamp of the cache entry.
1209
+ ### Clear cache
1210
+ POST /odata/v4/caching-api/Caches('mycache')/clear()
1211
+ ```
799
1212
 
800
- ---
1213
+ [See the full OData API Reference →](docs/odata-api.md)
801
1214
 
802
1215
  ### Contributing
803
1216