cds-caching 1.2.1 → 1.3.1

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.
Files changed (80) hide show
  1. package/README.md +195 -1114
  2. package/app/dashboard/Component-dbg.js +59 -0
  3. package/app/dashboard/Component-dbg.js.map +1 -0
  4. package/app/dashboard/Component.js +2 -0
  5. package/app/dashboard/Component.js.map +1 -0
  6. package/app/dashboard/Component.ts +59 -0
  7. package/app/dashboard/controller/App-dbg.controller.js +132 -0
  8. package/app/dashboard/controller/App-dbg.controller.js.map +1 -0
  9. package/app/dashboard/controller/App.controller.js +2 -0
  10. package/app/dashboard/controller/App.controller.js.map +1 -0
  11. package/app/dashboard/controller/App.controller.ts +147 -0
  12. package/app/dashboard/controller/BaseController-dbg.js +75 -0
  13. package/app/dashboard/controller/BaseController-dbg.js.map +1 -0
  14. package/app/dashboard/controller/BaseController.js +2 -0
  15. package/app/dashboard/controller/BaseController.js.map +1 -0
  16. package/app/dashboard/controller/BaseController.ts +88 -0
  17. package/app/dashboard/controller/Cache-dbg.controller.js +421 -0
  18. package/app/dashboard/controller/Cache-dbg.controller.js.map +1 -0
  19. package/app/dashboard/controller/Cache.controller.js +2 -0
  20. package/app/dashboard/controller/Cache.controller.js.map +1 -0
  21. package/app/dashboard/controller/Cache.controller.ts +490 -0
  22. package/app/dashboard/controller/Main-dbg.controller.js +32 -0
  23. package/app/dashboard/controller/Main-dbg.controller.js.map +1 -0
  24. package/app/dashboard/controller/Main.controller.js +2 -0
  25. package/app/dashboard/controller/Main.controller.js.map +1 -0
  26. package/app/dashboard/controller/Main.controller.ts +31 -0
  27. package/app/dashboard/controller/SingleMetric-dbg.controller.js +306 -0
  28. package/app/dashboard/controller/SingleMetric-dbg.controller.js.map +1 -0
  29. package/app/dashboard/controller/SingleMetric.controller.js +2 -0
  30. package/app/dashboard/controller/SingleMetric.controller.js.map +1 -0
  31. package/app/dashboard/controller/SingleMetric.controller.ts +335 -0
  32. package/app/dashboard/i18n/i18n.properties +61 -0
  33. package/app/dashboard/i18n/i18n_de.properties +3 -0
  34. package/app/dashboard/i18n/i18n_en.properties +3 -0
  35. package/app/dashboard/index-cdn.html +28 -0
  36. package/app/dashboard/index.html +19 -0
  37. package/app/dashboard/manifest.json +145 -0
  38. package/app/dashboard/model/formatter-dbg.js +253 -0
  39. package/app/dashboard/model/formatter-dbg.js.map +1 -0
  40. package/app/dashboard/model/formatter.js +2 -0
  41. package/app/dashboard/model/formatter.js.map +1 -0
  42. package/app/dashboard/model/formatter.ts +275 -0
  43. package/app/dashboard/model/models-dbg.js +13 -0
  44. package/app/dashboard/model/models-dbg.js.map +1 -0
  45. package/app/dashboard/model/models.js +2 -0
  46. package/app/dashboard/model/models.js.map +1 -0
  47. package/app/dashboard/model/models.ts +13 -0
  48. package/app/dashboard/resources/sap-ui-custom-dbg.js +4695 -0
  49. package/app/dashboard/resources/sap-ui-custom-dbg.js.map +1 -0
  50. package/app/dashboard/resources/sap-ui-custom.js +5794 -0
  51. package/app/dashboard/resources/sap-ui-custom.js.map +1 -0
  52. package/app/dashboard/service/CacheStatisticsService-dbg.js +267 -0
  53. package/app/dashboard/service/CacheStatisticsService-dbg.js.map +1 -0
  54. package/app/dashboard/service/CacheStatisticsService.js +2 -0
  55. package/app/dashboard/service/CacheStatisticsService.js.map +1 -0
  56. package/app/dashboard/service/CacheStatisticsService.ts +397 -0
  57. package/app/dashboard/view/App.view.xml +18 -0
  58. package/app/dashboard/view/Cache.view.xml +777 -0
  59. package/app/dashboard/view/Main.view.xml +57 -0
  60. package/app/dashboard/view/SingleMetric.view.xml +337 -0
  61. package/cds-plugin.js +52 -0
  62. package/db/cache-store.cds +12 -0
  63. package/db/statistics.cds +128 -0
  64. package/index.cds +2 -128
  65. package/lib/CachingService.js +24 -10
  66. package/lib/add.js +17 -0
  67. package/lib/operations/AsyncOperations.js +162 -111
  68. package/lib/operations/BasicOperations.js +42 -4
  69. package/lib/operations/CapOperations.js +92 -40
  70. package/lib/support/CacheStatisticsHandler.js +35 -10
  71. package/lib/support/CacheStoreManager.js +29 -0
  72. package/lib/support/KeyManager.js +40 -2
  73. package/lib/support/KeyvCDS.js +149 -0
  74. package/lib/support/MultitenancyDetector.js +25 -0
  75. package/lib/support/RuntimeConfigurationManager.js +69 -37
  76. package/lib/support/StatisticsPersistenceManager.js +18 -2
  77. package/lib/support/Telemetry.js +137 -0
  78. package/lib/util.js +29 -11
  79. package/package.json +43 -11
  80. package/srv/caching-api-service.js +47 -2
package/README.md CHANGED
@@ -4,233 +4,119 @@
4
4
 
5
5
  ## Overview
6
6
 
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.
7
+ A caching plugin for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap/docs/) that improves performance by caching slow remote service calls, complex operations, and queries.
8
8
 
9
- While CAP in general performs well for most use cases, caching can help with:
10
- - Slow remote service calls
11
- - Complex operations
12
- - Slow queries
13
- - Other performance bottle necks
14
-
15
- While cds-caching can be a big helper, an additional caching layer also adds complexity and should be used judiciously.
16
-
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).
9
+ Please also read the introduction blog post: [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).
18
10
 
19
11
  ### Key Features
20
12
 
21
- * **Flexible Key-Value Store** – Store and retrieve data using simple key-based access.
22
- * **CachingService** – A cds.Service implementation with an intuitive API for seamless integration into CAP.
23
- * **Read-Through Capabilities** – Let the caching service handle the cache set and get operatios for you
24
- * **CAP-specific Caching** – Effortlessly cache CQN queries or CAP cds.Requests using code or the @cache annotation.
25
- * **TTL Support** – Automatically manage data expiration with configurable time-to-live (TTL) settings.
26
- * **Tag Support** – Use dynamic tags for flexible cache invalidation options.
27
- * **Pluggable Storage Options** – Choose between in-memory caching, SQLite or Redis.
28
- * **Compression** – Compress cached data to save memory using LZ4 or GZIP.
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 cds-caching
41
-
42
- > **⚠️ Important:** Version 1.x contains breaking changes. Please review the migration guide below.
43
-
44
- ## Upgrading from 1.1.0 to 1.2.0
13
+ * **Read-Through Caching** – Transparently cache CQN queries, CAP requests, or function calls
14
+ * **Pluggable Storage** – In-memory, SQLite, Redis, PostgreSQL, SAP HANA, or CDS database
15
+ * **Multi-Tenancy** – Automatic tenant isolation for SAP BTP MTX deployments
16
+ * **TTL & Tag Support** – Time-based expiry and tag-based invalidation
17
+ * **Compression** – LZ4 or GZIP compression for cached data
18
+ * **Metrics & Monitoring** – Hit rates, latencies, key-level tracking, and an OData API
19
+ * **Annotations** – Declarative caching via `@cache` annotations on entities and functions
20
+
21
+ ### Documentation
22
+
23
+ | Guide | Description |
24
+ |-------|-------------|
25
+ | [Programmatic API](docs/programmatic-api.md) | Full API reference for cache operations |
26
+ | [Key Management](docs/key-management.md) | Key templates, context awareness, custom keys |
27
+ | [Metrics Guide](docs/metrics-guide.md) | Statistics, monitoring, and performance tracking |
28
+ | [OpenTelemetry Integration](docs/telemetry.md) | Distributed tracing and metrics export |
29
+ | [OData API](docs/odata-api.md) | REST endpoints for management and monitoring |
30
+ | [Dashboard](docs/dashboard.md) | Setup and usage of the monitoring dashboard |
31
+ | [Deployment Guide](docs/deployment-guide.md) | SAP BTP deployment for Redis, PostgreSQL, HANA, CDS |
32
+ | [Migration Guide](docs/migration-guide.md) | Upgrading from 0.x to 1.x, 1.1 to 1.2, and 1.2.x to 1.3.0 |
33
+ | [Example Application](docs/example-app.md) | Sample app with caching patterns |
34
+
35
+ ## Getting Started
45
36
 
46
- From **1.2.0** onwards, storage/compression adapters are treated as **optional peer dependencies** and must be installed **explicitly in your consuming CAP project** (i.e. *your app*, not `cds-caching`). This avoids relying on transitive dependencies and makes adapter usage deterministic.
47
-
48
- ### Required adapter packages (add to your app's `package.json`)
49
-
50
- Install the package(s) matching your configured `store` / `compression`:
51
-
52
- | Config | Value | Install in your app |
53
- |---|---|---|
54
- | `store` | `"redis"` | `@keyv/redis` |
55
- | `store` | `"sqlite"` | `@resolid/keyv-sqlite` (recommended) **or** `@keyv/sqlite` |
56
- | `store` | `"postgres"` | `@keyv/postgres` |
57
- | `compression` | `"lz4"` | `@keyv/compress-lz4` |
58
- | `compression` | `"gzip"` | `@keyv/compress-gzip` |
59
-
60
- Example:
37
+ ### Installation
61
38
 
62
39
  ```bash
63
- npm i @keyv/redis
64
- # or: npm i @resolid/keyv-sqlite
65
- # and optionally: npm i @keyv/compress-gzip
66
- ```
67
-
68
- ## Upgrading from 0.x to 1.x
69
-
70
- ### 🔄 API Changes for read-through methods
71
-
72
- 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.
73
-
74
- | **Old Method** | **New Method** | **Key Differences** |
75
- |----------------|----------------|---------------------|
76
- | `cache.run()` | `cache.rt.run()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
77
- | `cache.send()` | `cache.rt.send()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
78
- | `cache.wrap()` | `cache.rt.wrap()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
79
- | `cache.exec()` | `cache.rt.exec()` | Returns `{result, cacheKey, metadata}` instead of just `result` |
80
-
81
- ### 🔑 Key Template Changes
82
-
83
- **Before (0.x):**
84
- ```javascript
85
- // Old syntax - object with template property
86
- await cache.set(query, result, {
87
- key: { template: "user:{user}:{hash}" }
88
- })
89
- ```
90
-
91
- **After (1.x):**
92
- ```javascript
93
- // New syntax - direct string template
94
- await cache.set(query, result, {
95
- key: "user:{user}:{hash}"
96
- })
40
+ npm install cds-caching
97
41
  ```
98
42
 
99
- ### 🌍 Context Awareness Changes
100
-
101
- **Default Behavior Changed:**
102
- - **0.x:** Context (user, tenant, locale) was automatically included in some cache keys (ODataRequests)
103
- - **1.x:** Context is **disabled by default** and can be enabled for **ALL** keys (unless overwritten)
43
+ ### Minimal Configuration
104
44
 
105
- **To Enable Context Awareness:**
106
45
  ```json
107
46
  {
108
47
  "cds": {
109
48
  "requires": {
110
49
  "caching": {
111
- ...
112
- "keyManagement": {
113
- "isUserAware": true, // Include user context in cache keys
114
- "isTenantAware": true, // Include tenant context in cache keys
115
- "isLocaleAware": false // Include locale context in cache keys
116
- }
50
+ "impl": "cds-caching"
117
51
  }
118
52
  }
119
53
  }
120
- }
121
- ```
122
-
123
- ### 📚 Migration Examples
124
-
125
- **Example 1: Basic Caching**
126
- ```javascript
127
- // ❌ Old way (deprecated, but will still work)
128
- const result = await cache.run(query, db)
129
-
130
- // ✅ New way
131
- const { result, cacheKey, metadata } = await cache.rt.run(query, db)
132
- ```
133
-
134
- **Example 2: Function Wrapping**
135
- ```javascript
136
- // ❌ Old way (deprecated, but will still work)
137
- const cachedFn = cache.wrap("key", expensiveOperation)
138
- const result = await cachedFn("param1", "param2")
139
-
140
- // ✅ New way
141
- const cachedFn = cache.rt.wrap("key", expensiveOperation)
142
- const { result, cacheKey, metadata } = await cachedFn("param1", "param2")
143
- ```
144
-
145
- **Example 3: Custom Key Templates**
146
- ```javascript
147
- // ❌ Old way (will not work anymore)
148
- await cache.set(data, value, {
149
- key: { template: "user:{user}:{hash}" }
150
- })
151
-
152
- // ✅ New way
153
- await cache.set(data, value, {
154
- key: "user:{user}:{hash}"
155
- })
54
+ }
156
55
  ```
157
56
 
158
- ### 🔍 What's New
159
-
160
- - **Enhanced Metadata:** All read-through operations now return cache keys and performance metadata
161
- - **Better Performance:** Context awareness is opt-in, reducing unnecessary key complexity
162
- - **Improved Debugging:** Access to generated cache keys for troubleshooting
163
- - **Flexible Configuration:** Global and per-operation key template control
57
+ This uses the in-memory store — no additional setup needed for development.
164
58
 
165
- For detailed API documentation, see [Programmatic API Reference](docs/programmatic-api.md).
59
+ ### Data Model
166
60
 
167
- ### Example Application
61
+ The plugin ships CDS entity definitions for database-backed features. These are **auto-loaded conditionally** based on your configuration — no manual `model` property or `using from` needed:
168
62
 
169
- The cds-caching plugin includes a comprehensive example application demonstrating various caching use cases and a UI5-based dashboard for monitoring cache performance.
63
+ | Condition | Entities loaded | Purpose |
64
+ |-----------|----------------|---------|
65
+ | `statistics` block present | `Caches`, `Metrics`, `KeyMetrics` | Persist metrics and runtime config to the database |
66
+ | `store: 'cds'` | `CacheStore` | Key-value table used by the CDS store adapter |
67
+ | `using from 'cds-caching/index.cds'` | `CachingApiService` + statistics entities | OData API for the [dashboard](docs/dashboard.md) |
68
+ | None of the above | Nothing | Plugin works with external stores only |
170
69
 
171
- ![Cache Dashboard](./docs/dashboard.jpg)
70
+ The auto-loading works by injecting the relevant CDS files into `cds.env.roots` at plugin load time, before CAP compiles the model. This means `cds deploy` and `cds build` automatically pick up the required tables.
172
71
 
173
- The example consists of:
174
- - **Backend Application** (`examples/app/`) - A CAP application showing annotation-based and programmatic caching patterns
175
- - **Dashboard** (`examples/dashboard/`) - A UI5-based monitoring interface with real-time metrics, key-level analytics, and historical data
72
+ ### Basic Usage
176
73
 
177
- [See the full Example Application Guide →](docs/example-app.md)
74
+ ```javascript
75
+ const cache = await cds.connect.to("caching")
178
76
 
77
+ // Key-value operations
78
+ await cache.set("bp:1000001", businessPartnerData, { ttl: 60000 })
79
+ const data = await cache.get("bp:1000001")
179
80
 
180
- ### Installation
81
+ // Read-through caching for CQN queries
82
+ const { result } = await cache.rt.run(query, db, { ttl: 30000 })
181
83
 
182
- Installing and using cds-caching is straightforward since it's a CAP plugin. Simply run:
84
+ // Read-through caching for remote services
85
+ const { result } = await cache.rt.send(request, remoteService, { ttl: 10000 })
183
86
 
184
- ```bash
185
- npm install cds-caching
87
+ // Function caching
88
+ const cachedFn = cache.rt.wrap("expensive-op", expensiveFunction, { ttl: 3600 })
89
+ const { result } = await cachedFn("param1")
186
90
  ```
187
91
 
188
- #### Adapter packages (Redis / SQLite / Compression)
189
-
190
- `cds-caching` only ships with the in-memory store. If you configure a different store or compression, you must install the corresponding adapter package **in your consuming CAP project**:
92
+ ### Annotation-Based Caching
191
93
 
192
- ```bash
193
- # Redis store
194
- npm install @keyv/redis
195
-
196
- # SQLite store
197
- npm install @resolid/keyv-sqlite # Preferred in CAP because of better-sqlite3 usage
198
- npm install @keyv/sqlite # Alternative if you want to rely on the official adapter
94
+ ```cds
95
+ service MyService {
96
+ @cache: { ttl: 10000 }
97
+ entity Products as projection on db.Products;
199
98
 
200
- # Compression
201
- npm install @keyv/compress-lz4 # for "lz4"
202
- npm install @keyv/compress-gzip # for "gzip"
99
+ @cache: { ttl: 60000 }
100
+ function getRecommendations() returns array of Products;
101
+ }
203
102
  ```
204
103
 
205
- If you configure an adapter but don’t have its package installed, `cds-caching` will fail fast with a clear error telling you what to install.
104
+ ## Configuration
206
105
 
207
- ### Configuration
106
+ ### Store Types
208
107
 
209
- The cds-caching plugin supports comprehensive configuration through `package.json`. Here are all available configuration options:
108
+ | Store | Config | Use Case | Adapter Package |
109
+ |-------|--------|----------|-----------------|
110
+ | In-Memory | `"memory"` | Development, small-scale | Built-in |
111
+ | SQLite | `"sqlite"` | Medium-size, single instance | `@resolid/keyv-sqlite` or `@keyv/sqlite` |
112
+ | Redis | `"redis"` | Production, distributed | `@keyv/redis` |
113
+ | PostgreSQL | `"postgres"` | Production, when Redis unavailable | `@keyv/postgres` |
114
+ | CDS Database | `"cds"` | Production, HANA, multi-tenant | None (uses app's DB) |
115
+ | SAP HANA | `"hana"` | Direct HANA connection | `keyv-hana` |
210
116
 
211
- #### Basic Service Configuration
117
+ > **Recommendation**: Use `store: 'cds'` for CAP applications on SAP HANA — it reuses your app's DB connection, requires no extra packages, and supports multi-tenancy automatically. Use `store: 'redis'` for best performance in distributed setups.
212
118
 
213
- **Minimal setup** (in-memory cache for development):
214
-
215
- ```json
216
- {
217
- "cds": {
218
- "requires": {
219
- "caching": {
220
- "impl": "cds-caching",
221
- "namespace": "caching"
222
- },
223
- // Recommended: Define a specific caching service for different caching requirements
224
- "bp-caching": {
225
- "impl": "cds-caching",
226
- "namespace": "bp-caching"
227
- }
228
- }
229
- }
230
- }
231
- ```
232
-
233
- **Advanced configuration** with all options:
119
+ ### Full Configuration Options
234
120
 
235
121
  ```json
236
122
  {
@@ -239,21 +125,19 @@ The cds-caching plugin supports comprehensive configuration through `package.jso
239
125
  "caching": {
240
126
  "impl": "cds-caching",
241
127
  "namespace": "caching",
242
- "store": "in-memory", // "in-memory", "sqlite", or "redis"
243
- "compression": "lz4", // "lz4" or "gzip"
244
- "throwOnErrors": false, // Whether basic operations should throw errors (default: false)
245
- "transactionalOperations": false, // When true, basic ops run in a dedicated cache tx (cache.tx())
246
- "credentials": {
247
- // Redis configuration
248
- "host": "localhost",
249
- "port": 6379,
250
- "password": "optional",
251
- "url": "redis://..." // Alternative: Redis connection URI
252
-
253
- // SQLite configuration
254
- "url": "sqlite://./cache.sqlite",
255
- "table": "cache",
256
- "busyTimeout": 10000
128
+ "store": "redis",
129
+ "compression": "lz4",
130
+ "throwOnErrors": false,
131
+ "transactionalOperations": false,
132
+ "credentials": { },
133
+ "statistics": {
134
+ "enabled": true,
135
+ "persistenceInterval": 60000
136
+ },
137
+ "keyManagement": {
138
+ "isUserAware": false,
139
+ "isTenantAware": false,
140
+ "isLocaleAware": false
257
141
  }
258
142
  }
259
143
  }
@@ -261,11 +145,21 @@ The cds-caching plugin supports comprehensive configuration through `package.jso
261
145
  }
262
146
  ```
263
147
 
264
- #### Transaction isolation for basic operations (`transactionalOperations`)
148
+ | Option | Default | Description |
149
+ |--------|---------|-------------|
150
+ | `store` | `"memory"` | Storage backend (`memory`, `sqlite`, `redis`, `postgres`, `hana`, `cds`) |
151
+ | `namespace` | service name | Key prefix for store isolation |
152
+ | `compression` | none | `"lz4"` or `"gzip"` |
153
+ | `throwOnErrors` | `false` | Whether basic operations throw on cache errors |
154
+ | `transactionalOperations` | `false` | Isolate basic ops in dedicated cache transactions |
155
+ | `statistics` | none | When present, auto-loads the statistics data model and enables persistence (see [Statistics & Monitoring](#statistics--monitoring)) |
156
+ | `statistics.enabled` | `false` | Enable metrics collection |
157
+ | `statistics.persistenceInterval` | `60000` | Interval (ms) for persisting hourly stats to the database |
158
+ | `keyManagement.isTenantAware` | `false` (auto `true` in MTX) | Include tenant in cache keys |
159
+ | `keyManagement.isUserAware` | `false` | Include user in cache keys |
160
+ | `keyManagement.isLocaleAware` | `false` | Include locale in cache keys |
265
161
 
266
- CAP can run multiple `before` handlers concurrently. If one handler fails and rolls back the request transaction, other concurrent handlers may still be running and can fail when they access the cache (typical error: “Transaction is rolled back, no subsequent .run allowed…”).
267
-
268
- To isolate **basic cache operations** (`get`, `set`, `delete`, `clear`, `deleteByTag`, `metadata`, `tags`, `getRaw`) from the request transaction, enable:
162
+ ### Environment-Specific Configuration
269
163
 
270
164
  ```json
271
165
  {
@@ -273,1018 +167,205 @@ To isolate **basic cache operations** (`get`, `set`, `delete`, `clear`, `deleteB
273
167
  "requires": {
274
168
  "caching": {
275
169
  "impl": "cds-caching",
276
- "transactionalOperations": true
170
+ "store": "redis",
171
+ "[development]": {
172
+ "credentials": { "host": "localhost", "port": 6379 }
173
+ },
174
+ "[production]": {
175
+ "credentials": { "url": "redis://production-redis:6379" }
176
+ }
277
177
  }
278
178
  }
279
179
  }
280
180
  }
281
181
  ```
282
182
 
283
- In the background, the caching service opens a dedicated cache transaction via `cache.tx()`, executes the operation via `tx.send(...)`, and commits/rolls back the cache transaction per operation. This keeps cache calls working even if the surrounding request transaction is already rolled back.
183
+ For detailed key configuration and deployment instructions, see [Key Management](docs/key-management.md) and [Deployment Guide](docs/deployment-guide.md).
284
184
 
285
- #### Read-Through (RT) Key Configuration
286
-
287
- Configure default key templates for read-through operations:
288
-
289
- ```json
290
- {
291
- "cds": {
292
- "requires": {
293
- "caching": {
294
- ...
295
- "keyManagement": {
296
- "isUserAware": true, // Include user context in cache keys
297
- "isTenantAware": true, // Include tenant context in cache keys
298
- "isLocaleAware": false // Include locale context in cache keys
299
- }
300
- }
301
- }
302
- }
303
- }
304
- ```
185
+ ### Service Integration
305
186
 
306
- **Default behavior** (if not configured): All context elements are disabled by default.
187
+ 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.
307
188
 
308
- #### Error Handling Configuration
189
+ The easiest way to set this up is `cds add caching-dashboard`, which creates both the service exposure and the dashboard UI. To expose only the service without the dashboard, reference it in one of your `.cds` files:
309
190
 
310
- Configure how the caching service handles errors:
191
+ ```cds
192
+ using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';
311
193
 
312
- ```json
313
- {
314
- "cds": {
315
- "requires": {
316
- "caching": {
317
- ...
318
- "throwOnErrors": true, // Basic operations (set, get, delete, has) throw errors
319
- // Default: false - operations return undefined/null instead of throwing
320
- }
321
- }
322
- }
323
- }
194
+ annotate CachingApiService with @requires: 'authenticated-user';
324
195
  ```
325
196
 
326
- **Error Handling Behavior:**
197
+ This automatically loads the required database entities (`Caches`, `Metrics`, `KeyMetrics`) via a transitive `using from` dependency — no additional configuration needed. Without this step, the service won't be served by CAP and the dashboard won't work.
327
198
 
328
- - **Basic Operations** (`set`, `get`, `delete`, `has`):
329
- - When `throwOnErrors: false` (default): Operations return `undefined`/`null` on errors
330
- - When `throwOnErrors: true`: Operations throw errors for connection issues, etc.
199
+ ## Multi-Tenancy (MTX)
331
200
 
332
- - **Read-Through Operations** (`rt.run`, `rt.send`, `rt.wrap`, `rt.exec`):
333
- - Never throw errors, regardless of `throwOnErrors` setting
334
- - Include `cacheErrors` array in response when errors occur
335
- - Always fetch from remote service when cache operations fail
336
- - Log errors for monitoring and debugging
201
+ cds-caching supports SAP BTP multi-tenant applications using `@sap/cds-mtxs`. When multitenancy is detected, the plugin automatically:
337
202
 
338
- #### Environment-Specific Configuration
203
+ - Enables **tenant-aware cache keys** (`{tenant}:{hash}`)
204
+ - **Defers database operations** to request-time (avoids startup crashes without tenant context)
205
+ - **Guards statistics persistence** to only run within a tenant request context
339
206
 
340
- You can override settings for different environments:
207
+ ### Recommended Setup
341
208
 
342
209
  ```json
343
210
  {
344
211
  "cds": {
345
212
  "requires": {
213
+ "multitenancy": true,
346
214
  "caching": {
347
215
  "impl": "cds-caching",
348
- "store": "redis",
349
- "[development]": {
350
- "credentials": {
351
- "host": "localhost",
352
- "port": 6379
353
- }
354
- },
355
- "[production]": {
356
- "credentials": {
357
- "url": "redis://production-redis:6379"
358
- }
359
- }
216
+ "store": "cds"
360
217
  }
361
218
  }
362
219
  }
363
220
  }
364
221
  ```
365
222
 
366
- 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).
367
-
368
- ### Service Definition
369
-
370
- Add the following cds definition to your data model:
371
-
372
- ```
373
- using {plugin.cds_caching.CachingApiService} from 'cds-caching/index.cds';
374
-
375
- // Don't forget to protect the service, e.g.
376
- annotate CachingApiService with @requires: 'authenticated-user';
377
- ```
378
-
379
- ### Real-World Usage and Deployment
380
-
381
-
382
- #### Storage Options
383
-
384
- cds-caching provides 3 storage options:
385
-
386
- ##### In-Memory Cache (for development / small-scale uses)
387
- - Simple and fast, but not persistent
388
- - Not suitable for production since Node.js runtime memory is limited
389
- - Data is lost when the application restarts
390
- - Memory on SAP BTP Cloud Foundry is limited (up to 16 GB) and produces costs
391
-
392
- ##### SQLite (for medium-size use uses)
393
- - Requires installing the adapter package: `@keyv/sqlite` (in your project)
394
- - Data is stored in local SQLite database
395
- - Data is persited next to SAP BTP application with disk-quota up to 10 GB
396
- - Cache will be removed after each deployment to SAP BTP
397
- - No distributed cache between application instances (horizontal scaling)
223
+ With `store: 'cds'`, each tenant's cache data lives in its own HDI container fully isolated by CAP's Service Manager.
398
224
 
399
- ##### Redis Cache (recommended for production)
400
- - Requires installing the adapter package: `@keyv/redis` (in your project)
401
- - Persistent and supports distributed caching
402
- - Works across multiple app instances, making it ideal for scalable applications
403
- - Available on SAP BTP via hyperscaler options (e.g., AWS, Azure, Google Cloud)
404
- - Even trial accounts provide Redis access
405
- - Redis will be non-blocking
225
+ Alternatively, use `store: 'redis'` for shared Redis with automatic tenant-prefixed keys:
406
226
 
407
- #### Redis Development Setup
408
-
409
- ##### Running Redis Locally via Docker
410
- For local development, Redis can be quickly set up using Docker. A simple docker-compose configuration provides a lightweight caching environment:
411
-
412
- 1. Create a `docker-compose.yml` file:
413
- ```yaml
414
- services:
415
- redis:
416
- image: redis:latest
417
- container_name: local-redis
418
- ports:
419
- - "6379:6379"
420
- ```
421
-
422
- 2. Run Redis with:
423
- ```bash
424
- docker compose up -d
425
- ```
426
-
427
- 3. Modify the `package.json` configuration to connect to the local Redis instance:
428
227
  ```json
429
228
  {
430
229
  "cds": {
431
230
  "requires": {
231
+ "multitenancy": true,
432
232
  "caching": {
433
233
  "impl": "cds-caching",
434
- "namespace": "myCache",
435
234
  "store": "redis",
436
- "[development]": {
437
- "credentials": {
438
- "host": "localhost",
439
- "port": 6379
440
- }
441
- }
235
+ "credentials": { "socket": { "host": "localhost", "port": 6379 } }
442
236
  }
443
237
  }
444
238
  }
445
239
  }
446
240
  ```
447
241
 
448
- Now, caching will be handled by Redis instead of in-memory storage during development.
449
-
450
- #### Production Deployment on SAP BTP
242
+ > `isTenantAware` is automatically set to `true` in MTX mode. Set `"isTenantAware": false` in `keyManagement` to explicitly opt out.
451
243
 
452
- ![Redis on SAP BTP](./docs/caching-btp.png)
244
+ ## Usage Patterns
453
245
 
454
- For production deployments on SAP BTP, Redis can be provisioned as a managed service through the Redis on SAP BTP hyperscaler option. An instance can be provisioned via trial or even as a Free Tier to explore the service. However, for production scenarios the size of the Redis instance should match your caching requirements.
246
+ > **Deprecation Notice**: `cache.run()`, `cache.send()`, `cache.wrap()`, `cache.exec()` are deprecated since v1.0. Use `cache.rt.run()`, `cache.rt.send()`, `cache.rt.wrap()`, `cache.rt.exec()` instead. See [Migration Guide](docs/migration-guide.md).
455
247
 
456
- To bind Redis to your CAP application on SAP BTP, add the following configuration in `mta.yaml`. This will automatically create the service instance and bind your application to it. Since the credentials will automatically be fetched by CAP, make sure to maintain the service-tags to match the kind property of your cds-caching service(s) in the package.json:
457
-
458
- ```yaml
459
- modules:
460
- - name: cap-app-srv
461
- # ... other module configuration ...
462
- requires:
463
- - name: redis-cache
464
-
465
- resources:
466
- - name: redis-cache
467
- type: org.cloudfoundry.managed-service
468
- parameters:
469
- service: redis-cache
470
- service-plan: trial
471
- service-tags:
472
- # Must match the kind property in the package.json
473
- - cds-caching
474
- ```
475
-
476
- > 👉 **Tip**: There is a detailed [blog series on Redis in SAP BTP](https://community.sap.com/t5/technology-blogs-by-sap/redis-on-sap-btp-understanding-service-entitlements-and-metrics/ba-p/13738371) explaining how to set up Redis and connect via SSH for local/hybrid testing, as this is by default not possible.
477
-
478
-
479
-
480
- ### Usage Patterns
481
-
482
- > ⚠️ **Deprecation Notice**: The following methods are deprecated since version 1.0 and will be removed in a future version:
483
- > - `cache.run()` - use `cache.rt.run()` instead
484
- > - `cache.exec()` - use `cache.rt.exec()` instead
485
- > - `cache.wrap()` - use `cache.rt.wrap()` instead
486
- > - `cache.send()` - use `cache.rt.send()` instead
487
- >
488
- > The `rt.xxx` methods provide enhanced functionality including:
489
- > - **Read-through metadata**: Information about cache hits/misses and latency
490
- > - **Consistent return format**: All methods return `{ result, cacheKey, metadata }` by default
491
- >
492
- > **Migration**: Simply replace `cache.method()` with `cache.rt.method()` and access the result via `.result` property if needed.
493
-
494
- 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:
495
- #### 1. Low-Level Key-Value API for Read-Aside Caching
496
-
497
- The most basic way to use cds-caching is through its key-value API:
248
+ ### Read-Through Query Caching
498
249
 
499
250
  ```javascript
500
- // Connect to the caching service
501
- const cache = await cds.connect.to("caching")
502
-
503
- // Store a value (can be any object)
504
- await cache.set("bp:1000001", businessPartnerData)
505
-
506
- // Retrieve the value
507
- await cache.get("bp:1000001") // => businessPartnerData
508
-
509
- // Check if the key exists
510
- await cache.has("bp:1000001") // => true/false
511
-
512
- // Delete the key
513
- await cache.delete("bp:1000001")
514
-
515
- // Clear the whole cache
516
- await cache.clear()
251
+ const { result } = await cache.rt.run(
252
+ SELECT.from(BusinessPartners).where({ type: '2' }),
253
+ db,
254
+ { ttl: 30000 }
255
+ )
517
256
  ```
518
257
 
519
- **Error Handling for Basic Operations:**
520
-
521
- ```javascript
522
- // With throwOnErrors: false (default)
523
- try {
524
- const value = await cache.get("bp:1000001")
525
- if (value === undefined) {
526
- // Handle cache miss or error
527
- console.log("Value not found or cache error occurred")
528
- }
529
- } catch (error) {
530
- // Only thrown for non-cache related errors
531
- console.error("Unexpected error:", error)
532
- }
533
-
534
- // With throwOnErrors: true
535
- try {
536
- const value = await cache.get("bp:1000001")
537
- // Value will be undefined if not found, but errors will be thrown
538
- } catch (error) {
539
- // Errors thrown for connection issues, etc.
540
- console.error("Cache error:", error)
541
- }
542
- ```
543
-
544
- #### 2. CQN Query Caching
545
-
546
- 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.
547
-
548
- ```javascript
549
- // Create and execute a CQN query
550
- const query = SELECT.from(BusinessPartners).where({ businessPartnerType: '2' })
551
- const result = await db.run(query)
552
-
553
- // Cache the result
554
- await cache.set(query, result)
555
-
556
- // Retrieve from cache using the same query
557
- const cachedResult = await cache.get(query)
558
- ```
559
- 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.
560
-
561
- ```javascript
562
- const query = SELECT.from(BusinessPartners).where({ businessPartnerType: '2' })
563
-
564
- // Runs the query internally and caches the result
565
- const { result } = await cache.rt.run(query, db)
566
- ```
567
-
568
-
569
- Because the cache key has been dynamically created at runtime, it will also be returned:
570
-
571
- ```javascript
572
- // Access the cacheKey for later usage
573
- const { result, cacheKey } = await cache.rt.run(query, db)
574
- ```
575
-
576
- **Error Handling for Read-Through Operations:**
577
-
578
- Read-through operations never throw errors, even when cache operations fail. Instead, they include error information in the response:
579
-
580
- ```javascript
581
- // Read-through operations always return a result, even on cache errors
582
- const { result, cacheKey, metadata, cacheErrors } = await cache.rt.run(query, db)
583
-
584
- if (cacheErrors && cacheErrors.length > 0) {
585
- console.log("Cache errors occurred:", cacheErrors)
586
- // Result will be fetched from remote service despite cache errors
587
- }
588
-
589
- // The result is always available, regardless of cache errors
590
- return result
591
- ```
592
-
593
- #### 3. RemoteService Request-Level Caching
594
-
595
- 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).
596
-
597
- ```javascript
598
- // Cache the requests to an exposed external entity
599
- this.on('READ', BusinessPartners, async (req, next) => {
600
- const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
601
- let value = await cache.get(req)
602
- if(!value) {
603
- value = await bupa.run(req)
604
- await cache.set(req, value, { ttl: 30000 })
605
- }
606
- return value
607
- })
608
- ```
609
-
610
- Alternatively use read-through caching via the `rt.run` method to let the caching service handle the caching transparently:
258
+ ### Read-Through Remote Service Caching
611
259
 
612
260
  ```javascript
613
261
  this.on('READ', BusinessPartners, async (req, next) => {
614
262
  const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
615
- const { result } = await cache.rt.run(req, bupa)
616
- return result
617
- })
618
-
619
- ```
620
-
621
- This will transparently cache the result of the request and return the cached result if available for all further requests.
622
-
623
- ### 4. ApplicationService Request-Level Caching
624
-
625
-
626
- > 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.
627
-
628
-
629
- But not only external services can be cached, it's also possible to cache requests against an ApplicationService.
630
- Here, you should make use of the [`prepend`](https://cap.cloud.sap/docs/node.js/core-services#srv-prepend) function, to register the `on` handler before the default handler. Thus, it is possible to first check for the cache entries and only execute the default behavior if necessary.
631
-
632
-
633
- ```javascript
634
- class MyService extends cds.ApplicationService {
635
- async init() {
636
-
637
- // Read-through caching for the full entity
638
- this.prepend(() => {
639
- const { MyEntity } = this.entities;
640
- this.on('READ', MyEntity, async (req, next) => {
641
- const cache = cds.connect.to("caching");
642
- const { result } = await cache.rt.run(req, next)
643
- return result;
644
- });
645
- });
646
- return super.init()
647
- }
648
- }
649
- ```
650
-
651
- #### 5. ApplicationService Request-Level Caching with Annotations
652
-
653
- Alternatively to doing this via code, you can use annotations to enable caching on service entities or OData functions. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
654
-
655
- ```
656
- service MyService {
657
- @cache: {
658
- ttl: 10000 // 10 seconds
659
- }
660
- entity BusinessPartners as projection on BusinessPartner {
661
- // ... entity definition
662
- }
663
-
664
-
665
- @cache: {
666
- ttl: 100000, // 10 seconds
667
- tags: [{
668
- template: 'user-{user}'
669
- }]
670
- }
671
- function getUserPreferences() returns array of Preferences;
672
- }
673
- ```
674
-
675
- #### 5. Function Caching
676
-
677
- While not directly related to CAP functionality, the caching service provides two methods for read-through caching of JavaScript functions:
678
-
679
- ```javascript
680
- // Using wrap() to create a cached version of a function
681
- const fetchBusinessPartnerData = async (businessPartnerId, includeAddresses) => {
682
- // ... some expensive computation to fetch BP data
683
- return businessPartnerData
684
- }
685
-
686
- // Creates a cached version of the function.
687
- const cachedBpOperation = cache.rt.wrap("bp-data", fetchBusinessPartnerData, {
688
- ttl: 3600,
689
- tags: ['business-partner']
690
- })
691
-
692
- // Each call checks cache first, only executes if cache miss
693
- const result = await cachedBpOperation("1000001", true)
694
-
695
- // Using exec() for immediate execution with caching
696
- const result = await cache.rt.exec("product-data", async (productId) => {
697
- // ... some expensive computation to fetch product data
698
- return productData
699
- }, ["1000001"], {
700
- ttl: 3600,
701
- tags: ['product']
702
- })
703
- ```
704
-
705
- The key differences between `rt.wrap()` and `rt.exec()`:
706
- - `rt.wrap()` returns a new function that includes caching logic
707
- - `rt.exec()` immediately executes the function and caches the result
708
- - Use `rt.wrap()` when you need to reuse the cached function multiple times
709
- - Use `rt.exec()` for one-off executions with caching
710
-
711
- #### Dynamic Key Generation
712
-
713
- 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.
714
-
715
- ```javascript
716
- // Different arguments = different cache keys
717
- const result1 = await cachedBpOperation("1000001", true) // Cache key: "bp-data:1000001:true"
718
- const result2 = await cachedBpOperation("1000002", false) // Cache key: "bp-data:1000002:false"
719
- ```
720
-
721
- You can override this behavior by providing a custom key template:
722
-
723
- ```javascript
724
- const cachedOperation = cache.rt.wrap("bp-profile", fetchBusinessPartnerData, {
725
- key: "profile:{args[0]}:{args[1]}"
726
- })
727
- ```
728
-
729
- For detailed information on how read-through keys are generated and configured, see [Key Management](docs/key-management.md).
730
-
731
- ### Cache Invalidation Strategies
732
-
733
- The caching service provides different strategies to invalidate cached values.
734
-
735
- **IMPORTANT: You should not use cds-caching without a proper invalidation strategy.**
736
-
737
- #### 1. Time-Based (TTL)
738
-
739
- The most basic strategy is to use a time-to-live (TTL) for the cache. The caching service will automatically delete the value from the cache after the specified TTL has expired.
740
- The TTL can be specified for individually through all cache methods (e.g. `set`, `run`, `send`, `wrap`, `exec`).
741
-
742
- ```javascript
743
- // Store with 60 seconds TTL
744
- await cache.set("key", "value", { ttl: 60000 })
745
-
746
- // Run with 30 seconds TTL
747
- const { result } = await cache.rt.run(query, db, { ttl: 30000 })
748
-
749
- // Send with 10 seconds TTL
750
- const { result } = await cache.rt.send(request, service, { ttl: 10000 })
751
-
752
- // Wrap with 10 seconds TTL
753
- const cachedOperation = cache.rt.wrap("key", expensiveOperation, { ttl: 10000 })
754
-
755
- // Exec with 10 seconds TTL
756
- const { result } = await cache.rt.exec("key", async () => {
757
- // ... some expensive computation
263
+ const { result } = await cache.rt.run(req, bupa, { ttl: 30000 })
758
264
  return result
759
- }, [] {
760
- ttl: 10000
761
265
  })
762
266
  ```
763
267
 
764
- #### 2. Key-Based
268
+ ### ApplicationService Caching with `prepend`
765
269
 
766
- Key-based invalidation is a way to invalidate cache entries based on a specific key.
767
-
768
- ```javascript
769
- await cache.delete("key")
770
- ```
771
-
772
- 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.
773
-
774
- **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).
775
-
776
270
  ```javascript
777
- // No key override given, string will just be used as keys
778
- await cache.set('bp:1000001', businessPartnerData) // key: bp:1000001
779
-
780
- // No key override given, objects will be smartly hashed
781
- await cache.set(SELECT.from(BusinessPartners)) // key: bd3f3690d3e96a569bd89d9e207a89af
782
-
783
- // Automatically build the key for retrieval/deletion
784
- cache.createKey(SELECT.from(BusinessPartners)) // key: bd3f3690d3e96a569bd89d9e207a89af
785
-
786
- // Override and use your own key based on a fixed value
787
- await cache.set(SELECT.from(BusinessPartners, 1000001), { key: "bp:1000001" })
788
-
789
- // RT methods return the generated cache key
790
- const { result, cacheKey } = await cache.rt.run(query, db)
791
- console.log('Generated key:', cacheKey) // e.g., "tenant-acme:user-john:locale-en:hash-abc123"
792
-
793
- // Override RT key template for requests
794
- await cache.rt.run(req, remoteService, { key: "mykey:{tenant}:{user}:{locale}:{hash}" })
795
-
796
- // This requests will be cached for all users and for each locale
797
- await cache.rt.run(req, remoteService, { key: "mykey:{user}:{locale}:{hash}" })
798
-
799
- // Function wrapping with custom key template
800
- const cachedFunction = cache.rt.wrap("user-data", expensiveOperation, {
801
- key: "user:{user}:{args[0]}"
271
+ this.prepend(() => {
272
+ this.on('READ', MyEntity, async (req, next) => {
273
+ const cache = await cds.connect.to("caching")
274
+ const { result } = await cache.rt.run(req, next)
275
+ return result
276
+ })
802
277
  })
803
278
  ```
804
279
 
805
- #### 3. Tag-Based
806
-
807
- 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`).
808
- Tags can be provided as an array of strings or as an array of objects with the following properties:
809
- - `value`: The value to use for the tag.
810
- - `data`: A field from the value to use for the tag. This is working for objects and arrays of objects.
811
- - `prefix`: A prefix that will be added to the tag.
812
- - `suffix`: A suffix that will be added to the tag.
813
- - `template`: A template string that will be used to generate the tag (e.g. `{tenant}-{locale}-{user}-{hash}`). This is useful for dynamic tags based on cds.Requests.
814
- Templates support the following properties:
815
- - `{user}`: The current user
816
- - `{tenant}`: The current tenant
817
- - `{locale}`: The current locale
818
- - `{hash}`: The hash of the current query
819
-
820
-
280
+ ### Function Caching
821
281
 
822
282
  ```javascript
823
- // Store with static tag
824
- await cache.set("bp:1000001", businessPartnerData, {
825
- tags: [{ value: "bp-1000001" }]
826
- })
827
-
828
- // Store with template tag (will generate a tag like "tenant-global-user-anonymous")
829
- await cache.set("bp:1000001", businessPartnerData, {
830
- tags: [{ template: "tenant-{tenant}-user-{user}" }]
831
- })
283
+ // Wrap: create a cached version of a function
284
+ const cachedFn = cache.rt.wrap("bp-data", fetchBPData, { ttl: 3600, tags: ['bp'] })
285
+ const { result } = await cachedFn("1000001", true)
832
286
 
833
- // Store with data-based tag
834
- await cache.set("product:1000001", { productId: 1000001, name: "Laptop Computer" }, {
835
- tags: [{ data: "productId", prefix: "product-" }]
836
- })
837
-
838
- // Invalidate by tag
839
- await cache.deleteByTag('bp-1000001')
287
+ // Exec: immediate one-off execution with caching
288
+ const { result } = await cache.rt.exec("product", fetchProduct, ["1000001"], { ttl: 3600 })
840
289
  ```
841
- This is really useful for invalidating cache entries based on a specific attribute or context.
842
-
843
- #### 3. Dynamic Tags
844
290
 
845
- Dynamic tags using data `data` property are a way to invalidate cache entries based on the data itself. The caching service will automatically generate a tag for the value and invalidate the cache entry when the value changes.
291
+ ### Cache Invalidation
846
292
 
847
293
  ```javascript
294
+ // Time-based (TTL)
295
+ await cache.set("key", value, { ttl: 60000 })
848
296
 
849
- const businessPartners = [
850
- {
851
- businessPartner: 1000001,
852
- name: 'Acme Corporation'
853
- },
854
- {
855
- businessPartner: 1000002,
856
- name: 'Tech Solutions Ltd'
857
- }
858
- ]
859
-
860
- // Store with dynamic tags
861
- await cache.set("bp-list", businessPartners, {
862
- tags: [
863
- { data: 'businessPartner', prefix: 'bp-' },
864
- { value: "businessPartner" }
865
- ]
866
- })
867
-
868
- // Introspect the tags
869
- const tags = await cache.tags("bp-list") // => ["bp-1000001", "bp-1000002", "businessPartner"]
870
-
871
- // Invalidate by tag
872
- await cache.deleteByTag('bp-1000001')
873
- await cache.deleteByTag('bp-1000002')
874
- ```
297
+ // Key-based
298
+ await cache.delete("bp:1000001")
875
299
 
876
- 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.
300
+ // Tag-based
301
+ await cache.set("bp:1000001", data, { tags: [{ value: "bp-list" }] })
302
+ await cache.set("bp:1000002", data, { tags: [{ value: "bp-list" }] })
303
+ await cache.deleteByTag("bp-list")
877
304
 
878
- ```javascript
879
- const result = await cache.rt.run(query, db, {
880
- tags: [{ data: 'businessPartner', prefix: 'bp-' }]
305
+ // Dynamic tags from data
306
+ await cache.set("bp-list", bpArray, {
307
+ tags: [{ data: "businessPartner", prefix: "bp-" }]
881
308
  })
882
309
  ```
883
310
 
884
- This will transparently cache the result of the query and create a tag for each business partner in the result. If you use the same technique in other places and you want to invalidate the cache entries for a specific business partner, you can do this by simply invalidating the tag `bp-1`.
885
-
886
- ### Cache Iteration
887
-
888
- The caching service provides an iterator interface to traverse all cache entries:
889
-
890
- ```javascript
891
- const iterator = await cache.iterator()
892
-
893
- for await (const entry of iterator) {
894
- console.log(entry)
895
- }
896
- ```
897
-
898
- 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).
899
-
900
- ### TypeScript Support
901
-
902
- cds-caching includes comprehensive TypeScript definitions. The library is written in JavaScript but provides full TypeScript support for better development experience.
903
-
904
- #### Basic Usage with TypeScript
905
-
906
- ```typescript
907
- import { CachingService, CacheOptions, ReadThroughResult } from 'cds-caching';
908
-
909
- const cache = await cds.connect.to('caching') as CachingService;
910
-
911
- // Basic cache operations
912
- await cache.set('my-key', { data: 'value' }, { ttl: 3600 });
913
- const value = await cache.get('my-key');
914
-
915
- // Read-through operations with full type safety
916
- const { result, cacheKey, metadata } = await cache.rt.send(request, service, {
917
- ttl: 1800,
918
- tags: ['user-data']
919
- });
920
-
921
- // Function wrapping with type inference
922
- const cachedFunction = cache.rt.wrap('expensive-operation', async (id: string) => {
923
- return await this.performExpensiveOperation(id);
924
- });
925
-
926
- const { result: operationResult } = await cachedFunction('user-123');
927
-
928
- ```
929
-
930
- ### OData Service Caching Considerations
931
-
932
- While caching individual requests can improve performance, **caching an entire OData service is generally not recommended**. Here's why:
933
-
934
- 1. **Data Consistency**: OData services expose live business data that frequently changes. Caching responses without an appropriate invalidation strategy can lead to outdated or incorrect data being served.
935
-
936
- 2. **Query Complexity**: OData allows dynamic query parameters ($filter, $expand, $orderby, etc.), making it difficult to cache efficiently without storing excessive variations.
937
-
938
- 3. **Payload Size**: Full OData responses can be significantly large, consuming cache memory inefficiently compared to caching targeted CQN queries or specific request results.
939
-
940
- Instead of caching entire OData service responses, focus on:
941
- - Specific queries or request results
942
- - Static master data
943
- - Computed results
944
- - Remote service calls with stable data
945
-
946
- ### Best Practices
947
-
948
- 1. **Cache Selectively**: Not all data benefits from caching. Focus on:
949
- - Frequently accessed, rarely changed data
950
- - Computationally expensive operations
951
- - Remote service calls with stable data
952
-
953
- 2. **Use Appropriate TTLs**: Set TTLs based on data volatility:
954
- - Short TTLs (seconds/minutes) for frequently changing data
955
- - Longer TTLs (hours/days) for stable reference data
956
-
957
- 3. **Implement Cache Tags**: Use tags for granular cache invalidation:
958
- - Group related cache entries
959
- - Enable targeted invalidation
960
- - Use dynamic tags for user/tenant-specific caching
961
-
962
- 4. **Monitor Cache Performance**: Regularly check cache statistics:
963
- - Hit rates
964
- - Memory usage
965
- - Response times
966
- - Error rates
967
-
968
- ### Limitations and Considerations
969
-
970
- 1. **Memory Usage**: Monitor cache size, especially with in-memory storage
971
- 2. **Consistency**: Consider data freshness requirements when setting TTLs
972
- 3. **Multi-Tenant**: Use appropriate namespacing and key strategies
973
- 4. **Redis Setup**: Ensure proper configuration for production use
974
-
975
- ## Enhanced Statistics & Monitoring
976
-
977
- The plugin now includes comprehensive statistics and monitoring capabilities that provide deep insights into cache performance and help optimize cache usage.
978
-
979
- [See the full Metrics Guide →](docs/metrics-guide.md)
980
-
981
- ### Key Features
982
-
983
- - **Real-time Metrics**: Monitor cache performance with detailed hit rates, latencies, and throughput
984
- - **Key-level Tracking**: Track performance metrics for individual cache keys
985
- - **Historical Data**: Store and analyze metrics over time (hourly/daily periods)
986
- - **Performance Analytics**: Calculate cache efficiency, error rates, and response times
987
- - **Runtime Configuration**: Enable/disable metrics at runtime without restart
988
- - **API Access**: Access metrics programmatically or via OData service
989
-
990
- ### Metrics Overview
991
-
992
- cds-caching provides two types of metrics:
993
-
994
- #### 1. General Cache Metrics
995
- Track overall cache performance including:
996
- - **Hit/Miss Statistics**: Total hits, misses, and hit ratios
997
- - **Latency Metrics**: Average, min, max, and percentile latencies for hits and misses
998
- - **Performance Metrics**: Throughput (requests/second), error rates, cache efficiency
999
- - **Memory Usage**: Current memory consumption and item count
1000
- - **Native Operations**: Counts of direct cache operations (set, get, delete, etc.)
1001
-
1002
- #### 2. Key-level Metrics
1003
- Track performance for individual cache keys including:
1004
- - **Key-specific Statistics**: Hits, misses, and hit ratios per key
1005
- - **Context Information**: Data type, service name, entity name, operation type
1006
- - **Enhanced Metadata**: Query text, request info, function names, user/tenant context
1007
- - **Performance Tracking**: Latency and throughput metrics per key
1008
-
1009
- ### Enabling Metrics
1010
-
1011
- 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.
1012
-
1013
- To enable metrics programmatically:
1014
-
1015
- ```javascript
1016
- // Connect to the caching service
1017
- const cache = await cds.connect.to("caching")
1018
-
1019
- // Enable metrics at runtime
1020
- await cache.setMetricsEnabled(true)
1021
- await cache.setKeyMetricsEnabled(true)
1022
- ```
1023
-
1024
- Or via OData API:
1025
-
1026
- ```http
1027
- ### Enable general metrics
1028
- POST http://localhost:4004/odata/v4/caching-api/Caches('caching')/setMetricsEnabled
1029
- Content-Type: application/json
311
+ For more usage patterns, error handling details, and TypeScript support, see [Programmatic API](docs/programmatic-api.md).
1030
312
 
1031
- {
1032
- "enabled": true
1033
- }
313
+ ## Statistics & Monitoring
1034
314
 
1035
- ### Enable key-level metrics
1036
- POST http://localhost:4004/odata/v4/caching-api/Caches('caching')/setKeyMetricsEnabled
1037
- Content-Type: application/json
315
+ To persist metrics to the database, add a `statistics` block to your configuration. This automatically loads the required data model (`Caches`, `Metrics`, `KeyMetrics` tables):
1038
316
 
317
+ ```json
1039
318
  {
1040
- "enabled": true
319
+ "cds": {
320
+ "requires": {
321
+ "caching": {
322
+ "impl": "cds-caching",
323
+ "store": "redis",
324
+ "statistics": {
325
+ "enabled": true,
326
+ "persistenceInterval": 60000
327
+ }
328
+ }
329
+ }
330
+ }
1041
331
  }
1042
332
  ```
1043
333
 
1044
- ### Accessing Metrics via Caching Service
1045
-
1046
- 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.
1047
-
1048
- #### Metrics Persistence
1049
-
1050
- **Transient Metrics**: Current statistics are kept in memory and provide real-time insights into cache performance:
1051
- - Hit/miss ratios
1052
- - Current latency statistics
1053
- - Active cache entries
1054
- - Key-level performance data
1055
-
1056
- **Persisted Metrics**: Historical data is automatically stored in the database for long-term analysis:
1057
- - Hourly aggregated statistics
1058
- - Key-level metrics over time
1059
- - Performance trends and patterns
1060
- - Cache efficiency analysis
1061
-
1062
- #### Current Statistics
334
+ You can also enable metrics at runtime:
1063
335
 
1064
336
  ```javascript
1065
- // Connect to the caching service
1066
337
  const cache = await cds.connect.to("caching")
1067
-
1068
- // Get current statistics
1069
- const stats = await cache.getCurrentStats()
1070
- console.log('Hit ratio:', stats.hitRatio)
1071
- console.log('Average hit latency:', stats.avgHitLatency)
1072
- console.log('Throughput:', stats.throughput)
1073
-
1074
- // Get current key metrics
1075
- const keyMetrics = await cache.getCurrentKeyMetrics()
1076
- for (const [key, metrics] of keyMetrics) {
1077
- console.log(`Key ${key}:`, {
1078
- hits: metrics.hits,
1079
- misses: metrics.misses,
1080
- hitRatio: metrics.hitRatio,
1081
- avgHitLatency: metrics.avgHitLatency
1082
- })
1083
- }
1084
- ```
1085
-
1086
- #### Historical Metrics
1087
-
1088
- ```javascript
1089
- // Get metrics for a specific time period
1090
- const from = new Date('2024-01-01')
1091
- const to = new Date('2024-01-31')
1092
- const historicalStats = await cache.getMetrics(from, to)
1093
-
1094
- // Get key-specific metrics
1095
- const keyStats = await cache.getKeyMetrics('my-cache-key', from, to)
1096
- ```
1097
-
1098
- #### Runtime Configuration
1099
-
1100
- ```javascript
1101
- // Enable/disable metrics at runtime
1102
338
  await cache.setMetricsEnabled(true)
1103
339
  await cache.setKeyMetricsEnabled(true)
1104
340
 
1105
- // Get current configuration
1106
- const config = await cache.getRuntimeConfiguration()
1107
- console.log('Metrics enabled:', config.metricsEnabled)
1108
- console.log('Key metrics enabled:', config.keyMetricsEnabled)
1109
-
1110
- // Clear metrics
1111
- await cache.clearMetrics()
1112
- await cache.clearKeyMetrics()
341
+ const stats = await cache.getCurrentMetrics()
1113
342
  ```
1114
343
 
1115
- ### Metrics Data Structure
1116
-
1117
- #### General Cache Statistics (Metrics Entity)
344
+ To add the monitoring dashboard to your project, run:
1118
345
 
1119
- ```javascript
1120
- {
1121
- // Entity identification
1122
- ID: "daily:2024-01-15", // Unique identifier (period:date)
1123
- cache: "caching", // Cache name
1124
- timestamp: "2024-01-15T10:30:00Z", // When metrics were recorded
1125
- period: "daily", // Aggregation period (hourly/daily/monthly)
1126
-
1127
- // Read-through metrics
1128
- hits: 1500, // Number of cache hits
1129
- misses: 300, // Number of cache misses
1130
- errors: 5, // Number of errors
1131
- totalRequests: 1800, // Total read-through requests
1132
-
1133
- // Read-through latency metrics (milliseconds)
1134
- avgHitLatency: 2.5, // Average hit latency
1135
- minHitLatency: 0.1, // Minimum hit latency
1136
- maxHitLatency: 15.2, // Maximum hit latency
1137
- avgMissLatency: 45.8, // Average miss latency
1138
- minMissLatency: 12.3, // Minimum miss latency
1139
- maxMissLatency: 120.5, // Maximum miss latency
1140
- avgReadThroughLatency: 8.9, // Average read-through latency
1141
-
1142
- // Read-through performance metrics
1143
- hitRatio: 0.833, // Hit ratio as percentage (83.3%)
1144
- throughput: 25.5, // Requests per second
1145
- errorRate: 0.003, // Error rate as percentage (0.3%)
1146
- cacheEfficiency: 18.3, // Miss latency / hit latency ratio
1147
-
1148
- // Native operation metrics
1149
- nativeSets: 200, // Number of direct set operations
1150
- nativeGets: 800, // Number of direct get operations
1151
- nativeDeletes: 50, // Number of direct delete operations
1152
- nativeClears: 2, // Number of clear operations
1153
- nativeDeleteByTags: 10, // Number of delete-by-tag operations
1154
- nativeErrors: 1, // Number of native operation errors
1155
- totalNativeOperations: 1063, // Total native operations
1156
- nativeThroughput: 17.7, // Native operations per second
1157
- nativeErrorRate: 0.001, // Native operation error rate (0.1%)
1158
-
1159
- // System metrics
1160
- memoryUsage: 52428800, // Memory usage in bytes
1161
- itemCount: 150, // Number of items in cache
1162
- uptimeMs: 7200000 // Cache uptime in milliseconds
1163
- }
346
+ ```bash
347
+ cds add caching-dashboard
1164
348
  ```
1165
349
 
1166
- #### Key-level Metrics (KeyMetrics Entity)
350
+ This copies a pre-built UI5 dashboard into your `app/` folder and exposes the `CachingApiService`. After running `cds watch`, the dashboard is available at `/caching-dashboard/index.html`.
1167
351
 
1168
- ```javascript
1169
- {
1170
- // Entity identification
1171
- ID: "key:user-preferences:123", // Unique identifier
1172
- cache: "caching", // Cache name
1173
- keyName: "user-preferences:123", // Cache key name
1174
- lastAccess: "2024-01-15T10:30:00Z", // Last access time
1175
- period: "current", // Period type (current/hourly/daily)
1176
- operationType: "read_through", // Operation category (read_through/native/mixed)
1177
-
1178
- // Read-through metrics
1179
- hits: 45, // Number of hits for this key
1180
- misses: 5, // Number of misses for this key
1181
- errors: 0, // Number of errors for this key
1182
- totalRequests: 50, // Total requests for this key
1183
- hitRatio: 0.9, // Hit ratio for this key (90%)
1184
- cacheEfficiency: 21.2, // Cache efficiency for this key
1185
-
1186
- // Read-through latency metrics (milliseconds)
1187
- avgHitLatency: 1.2, // Average hit latency for this key
1188
- minHitLatency: 0.5, // Minimum hit latency for this key
1189
- maxHitLatency: 3.1, // Maximum hit latency for this key
1190
- avgMissLatency: 25.4, // Average miss latency for this key
1191
- minMissLatency: 15.2, // Minimum miss latency for this key
1192
- maxMissLatency: 45.8, // Maximum miss latency for this key
1193
- avgReadThroughLatency: 3.8, // Average read-through latency for this key
1194
-
1195
- // Read-through performance metrics
1196
- throughput: 2.5, // Requests per second for this key
1197
- errorRate: 0.0, // Error rate for this key (0%)
1198
-
1199
- // Native operation metrics for this key
1200
- nativeHits: 10, // Native hits for this key
1201
- nativeMisses: 2, // Native misses for this key
1202
- nativeSets: 5, // Native sets for this key
1203
- nativeDeletes: 1, // Native deletes for this key
1204
- nativeClears: 0, // Native clears for this key
1205
- nativeDeleteByTags: 0, // Native delete-by-tags for this key
1206
- nativeErrors: 0, // Native errors for this key
1207
- totalNativeOperations: 18, // Total native operations for this key
1208
- nativeThroughput: 0.5, // Native operations per second for this key
1209
- nativeErrorRate: 0.0, // Native error rate for this key
1210
-
1211
- // Context and metadata
1212
- dataType: "request", // Type of data (query/request/function/custom)
1213
- operation: "READ", // Cache operation type
1214
- metadata: '{"ttl":3600}', // JSON string with additional metadata
1215
- context: '{"user":"john.doe","tenant":"acme"}', // JSON string with context
1216
- query: "SELECT * FROM UserPreferences WHERE userId = '123'", // CQL query text
1217
- subject: '{"entity":"UserPreferences"}', // JSON string with subject info
1218
- target: "UserService", // Target service name
1219
- tenant: "acme", // Tenant information
1220
- user: "john.doe", // User information
1221
- locale: "en-US", // Locale information
1222
- cacheOptions: '{"ttl":3600}', // JSON string with cache options
1223
- timestamp: "2024-01-15T09:00:00Z" // When this key was first accessed
1224
- }
1225
- ```
352
+ ![Cache Dashboard](./docs/dashboard.jpg)
1226
353
 
1227
- ### Best Practices for Metrics
354
+ See the [Dashboard Guide](docs/dashboard.md) for details on features, security, and customization.
1228
355
 
1229
- 1. **Enable Selectively**: Only enable metrics when needed for monitoring or debugging
1230
- 2. **Monitor Memory Usage**: Key metrics can consume significant memory for large caches
1231
- 3. **Set Appropriate Intervals**: Balance persistence frequency with performance impact
1232
- 4. **Use Historical Data**: Analyze trends over time to optimize cache configuration
1233
- 5. **Monitor Error Rates**: High error rates may indicate configuration issues
1234
- 6. **Track Cache Efficiency**: Aim for high cache efficiency (miss latency >> hit latency)
356
+ [See the full Metrics Guide →](docs/metrics-guide.md)
1235
357
 
1236
358
  ## API Reference
1237
359
 
1238
- The cds-caching plugin provides two APIs for managing cache operations:
1239
-
1240
- - **Programmatic API** - JavaScript methods for use within your CAP application code
1241
- - **OData API** - REST endpoints for external applications and monitoring tools
1242
-
1243
- ### Programmatic API
1244
-
1245
- The programmatic API provides methods for direct cache operations within your CAP application:
1246
-
1247
- ```javascript
1248
- // Connect to the caching service
1249
- const cache = await cds.connect.to("caching")
1250
-
1251
- // Basic operations
1252
- await cache.set("key", "value")
1253
- const value = await cache.get("key")
1254
- await cache.delete("key")
1255
-
1256
- // Read-through operations
1257
- const result = await cache.rt.run(query, db)
1258
- const result = await cache.rt.send(request, service)
1259
-
1260
- // Metrics and statistics
1261
- const stats = await cache.getCurrentStats()
1262
- const keyMetrics = await cache.getCurrentKeyMetrics()
1263
- ```
1264
-
1265
- [See the full Programmatic API Reference →](docs/programmatic-api.md)
1266
-
1267
- ### OData API
1268
-
1269
- The OData API provides REST endpoints for external applications, monitoring tools, and administrative interfaces:
1270
-
1271
- ```http
1272
- ### Get cache statistics
1273
- GET /odata/v4/caching-api/Metrics?$filter=cache eq 'mycache'
1274
-
1275
- ### Get cache entries
1276
- GET /odata/v4/caching-api/Caches('mycache')/getEntries()
1277
-
1278
- ### Clear cache
1279
- POST /odata/v4/caching-api/Caches('mycache')/clear()
1280
- ```
1281
-
1282
- [See the full OData API Reference →](docs/odata-api.md)
360
+ | API | Description |
361
+ |-----|-------------|
362
+ | [Programmatic API](docs/programmatic-api.md) | JavaScript methods for cache operations |
363
+ | [OData API](docs/odata-api.md) | REST endpoints for monitoring and management |
1283
364
 
1284
- ### Contributing
365
+ ## Contributing
1285
366
 
1286
- Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
367
+ Contributions are welcome! Please submit pull requests to the [repository](https://github.com/mikezaschka/cds-caching).
1287
368
 
1288
- ### License
369
+ ## License
1289
370
 
1290
371
  This project is licensed under the MIT License - see the LICENSE file for details.