cds-caching 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,33 +2,50 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- This plugin for the [SAP Cloud Application Programming Model (CAP)](https://cap.cloud.sap/docs/) provides a robust caching service for CAP applications, addressing common performance challenges in distributed systems.
5
+ 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.
6
+ While CAP in general performs well for most use cases, caching can help with:
7
+ - Slow remote service calls
8
+ - Complex calculations
9
+ - Heavy queries
10
+ - External API integration
6
11
 
7
- By reducing database requests and accelerating response times, caching is ideal for handling expensive operations like complex queries/calculations or external API calls. However, it introduces another layer of complexity and should be used with caution.
12
+ While caching can help with these, it also adds complexity and should be used judiciously.
13
+
14
+ 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
+
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.
8
25
 
9
26
  ### Key Features
10
27
 
11
28
  * **Flexible Key-Value Store** – Store and retrieve data using simple key-based access.
12
- * **CachingService** – A cds.Service-based implementation with an intuitive API for seamless integration.
29
+ * **CachingService** – A CALESI-compliant cds.Service implementation with an intuitive API for seamless integration.
13
30
  * **Event Handling** – Monitor and react to cache events, such as before/after storage and retrieval.
14
31
  * **CAP-specific Caching** – Effortlessly cache CQN queries or CAP cds.Requests using code or the @cache annotation.
15
32
  * **TTL Support** – Automatically manage data expiration with configurable time-to-live (TTL) settings.
16
33
  * **Tag Support** – Use dynamic tags for flexible cache invalidation options.
17
34
  * **Pluggable Storage Options** – Choose between in-memory caching or Redis.
18
- * **Compression** – Compress cached data to save memory.
19
- * **Integrated Statistics (WIP)** – Integrated statistics on cache hits, etc.
35
+ * **Compression** – Compress cached data to save memory using LZ4 or GZIP.
36
+ * **Integrated Statistics** – Monitor cache performance with hit rates, latencies, and more.
37
+
20
38
  ### Installation
21
39
 
22
- Installing and using cds-caching is straightforward since its a CAP plugin. Simply run:
40
+ Installing and using cds-caching is straightforward since it's a CAP plugin. Simply run:
23
41
 
24
- ```
42
+ ```bash
25
43
  npm install cds-caching
26
44
  ```
27
45
 
28
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.
29
47
 
30
- ```json
31
-
48
+ ```javascript
32
49
  {
33
50
  "cds": {
34
51
  "requires": {
@@ -36,6 +53,7 @@ Next, add a caching service configuration to your package.json. You can even def
36
53
  "impl": "cds-caching",
37
54
  "namespace": "my::app::caching"
38
55
  },
56
+ // Optional: Define a specific caching service for Business Partner API
39
57
  "bp-caching": {
40
58
  "impl": "cds-caching",
41
59
  "namespace": "my::app::bp-caching"
@@ -43,14 +61,13 @@ Next, add a caching service configuration to your package.json. You can even def
43
61
  }
44
62
  }
45
63
  }
46
-
47
64
  ```
65
+
48
66
  ### Advanced Configuration
49
67
 
50
- For more control, you can specify additional options. Some of those will be explained later:
68
+ For more control, you can specify additional options:
51
69
 
52
70
  ```javascript
53
-
54
71
  {
55
72
  "cds": {
56
73
  "requires": {
@@ -63,23 +80,31 @@ For more control, you can specify additional options. Some of those will be expl
63
80
  "host": "localhost",
64
81
  "port": 6379,
65
82
  "password": "optional",
83
+ "url": "redis://..." // Alternative: Redis connection URI
84
+ },
85
+ "statistics": {
86
+ "enabled": true,
87
+ "persistenceInterval": 60000, // Optional: Interval for statistics persistence
88
+ "maxLatencies": 1000 // Optional: Maximum number of latencies to track
66
89
  }
67
90
  }
68
91
  }
69
92
  }
70
93
  }
71
-
72
94
  ```
73
- ### Low level usage
74
95
 
75
- The **cds-caching** plugin provides direct **key-value storage**, allowing fine-grained caching control. Its API follows a familiar pattern, making it easy to use if you have ever worked with other caching solutions and frameworks.
76
- Here’s how you can interact with the cache:
96
+ ### Usage Patterns
97
+
98
+ The caching service provides a flexible API for caching data in CAP applications. Here are the key usage patterns:
99
+ #### 1. Low-Level Key-Value API
100
+
101
+ The most basic way to use cds-caching is through its key-value API:
77
102
 
78
103
  ```javascript
79
104
  // Connect to the caching service
80
105
  const cache = await cds.connect.to("caching")
81
106
 
82
- // Store a value
107
+ // Store a value (can be any object)
83
108
  await cache.set("key", "value")
84
109
 
85
110
  // Retrieve the value
@@ -95,228 +120,473 @@ await cache.delete("key")
95
120
  await cache.clear()
96
121
  ```
97
122
 
98
- This **low-level API** is useful when you need direct access to cached data, such as storing configuration values, or precomputed results. While the core API is simple, **cds-caching** also provides **higher-level caching strategies** that are more integrated with CAP. We will look at those in a minute. Before, let's focus on cache events.
99
- #### Cache Events
123
+ #### 2. CQN Query Caching
100
124
 
101
- The caching service follows the same event-driven principles as cds.Service instances. This means you can hook into events **before** and **after** storing or retrieving data. This is useful for logging, debugging, or performing additional actions when cache operations occur.
125
+ 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.
102
126
 
103
- Each cache event (event.data.value) contains the following structure:
104
- * **value** The cached data.
105
- * **tags** Tags assigned to the cached entry.
106
- * **timestamp** The timestamp when the cache entry was created.
127
+ ```javascript
128
+ // Create and execute a CQN query
129
+ const query = SELECT.from(Foo)
130
+ const result = await db.run(query)
131
+
132
+ // Cache the result
133
+ await cache.set(query, result)
107
134
 
108
- Here’s how you can listen for and react to cache events:
135
+ // Retrieve from cache using the same query
136
+ const cachedResult = await cache.get(query)
137
+ ```
138
+ 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.
109
139
 
110
140
  ```javascript
141
+ const query = SELECT.from(Foo)
111
142
 
112
- // Log before the cache is cleared
113
- cache.before("CLEAR", () => {
114
- console.log("Cache is about to be cleared")
115
- })
143
+ // Runs the query internally and caches the result
144
+ const result = await cache.run(query, db)
145
+ ```
116
146
 
117
- // Log before storing data
118
- cache.before("SET", (event) => {
119
- console.log(`Storing key: ${event.data.key} with value: ${event.data.value}`)
120
- })
147
+ This will transparently cache the result of the query and return the cached result if available for all further requests.
148
+
149
+ #### 3. Request-Level Caching
121
150
 
122
- // Log after retrieving data
123
- cache.after("GET", (event) => {
124
- console.log(`Retrieved key: ${event.data.key} with value: ${event.data.value}`)
151
+ Cache entire CAP requests with context awareness (e.g. user, tenant, locale, etc.), which is useful for caching slow remote service calls. The caching service will automatically generate a key for the request based on the request object and the current user, tenant and locale.
152
+
153
+ ```javascript
154
+ this.on('READ', BusinessPartners, async (req, next) => {
155
+ const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
156
+ let value = await cache.get(req)
157
+ if(!value) {
158
+ value = await bupa.run(req)
159
+ await cache.set(req, value, { ttl: 3600 })
160
+ }
161
+ return value
125
162
  })
163
+ ```
126
164
 
165
+ Alternatively use read-through caching via the `run` method to let the caching service handle the caching transparently:
127
166
 
167
+ ```javascript
168
+ const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
169
+ const result = await cache.run(req, bupa)
128
170
  ```
129
171
 
130
- This approach allows you to monitor cache activity and, if needed, manipulate data before storage or retrieval.
131
- #### Invalidation via Time-To-Live (TTL)
172
+ This will transparently cache the result of the request and return the cached result if available for all further requests.
132
173
 
133
- **cds-caching** supports automatic cache invalidation via **TTL (Time-To-Live)**. Cached values will expire after the specified TTL, preventing stale data from lingering in memory.
174
+ #### 4. Declarative Caching with Annotations
134
175
 
135
- ```javascript
136
- // Store a value with a ttl
137
- await cache.set("key", "value", { ttl: 6000 }) // 60 seconds
176
+ 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.
138
177
 
139
- // Retrieve the value in time
140
- await cache.get("key") // => value
178
+ **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.**
179
+
180
+ ```
181
+ service MyService {
182
+ @cache: {
183
+ ttl: 3600
184
+ }
185
+ entity BusinessPartners as projection on BusinessPartner {
186
+ // ... entity definition
187
+ }
141
188
 
142
- await new Promise((resolve) => setTimeout(resolve, 6100)) // wait 61 seonds
143
189
 
144
- // Now the value is not available anymore
145
- await cache.get("key") // => undefined
190
+ @cache: {
191
+ ttl: 1800,
192
+ tags: [{
193
+ template: 'user-{user}'
194
+ }]
195
+ }
196
+ function getUserPreferences() returns array of Preferences;
197
+ }
146
198
  ```
147
199
 
148
- TTL-based invalidation is useful for **temporary data, rate-limiting mechanisms, and frequently updated information**. However, **cds-caching** also supports other invalidation strategies, which will be covered in the advanced section.
149
- #### Compression
200
+ #### 5. Function Caching
150
201
 
151
- To optimize storage, **cds-caching** supports **data compression**. This reduces cache size and can improve performance, especially when caching large datasets. Compression is **applied only when storing data**, meaning applications interact with uncompressed values. Available compression methods include:
152
- * **lz4** – Faster compression and decompression, ideal for performance-critical applications.
153
- * **gzip** – Higher compression ratio, reducing storage footprint at the cost of slightly increased CPU usage.
154
-
155
- Compression can be configured via package.json:
202
+ While not directly related to CAP functionality, the caching service provides two methods for read-through caching of JavaScript functions:
156
203
 
157
- ```json
158
- {
159
- "cds": {
160
- "requires": {
161
- "caching": {
162
- "impl": "cds-caching",
163
- "compression": "lz4"
164
- }
165
- }
166
- }
204
+ ```javascript
205
+ // Using wrap() to create a cached version of a function
206
+ const expensiveOperation = async (value) => {
207
+ // ... some expensive computation
208
+ return result
167
209
  }
210
+
211
+ // Creates a cached version of the function
212
+ const cachedOperation = cache.wrap("key", expensiveOperation, {
213
+ ttl: 3600,
214
+ tags: ['computation']
215
+ })
216
+
217
+ // Each call checks cache first, only executes if cache miss
218
+ const result = await cachedOperation("input")
219
+
220
+ // Using exec() for immediate execution with caching
221
+ const result = await cache.exec("key", async () => {
222
+ // ... some expensive computation
223
+ return result
224
+ }, {
225
+ ttl: 3600,
226
+ tags: ['computation']
227
+ })
168
228
  ```
169
229
 
170
- ### Medium level usage
230
+ The key differences between `wrap()` and `exec()`:
231
+ - `wrap()` returns a new function that includes caching logic
232
+ - `exec()` immediately executes the function and caches the result
233
+ - Use `wrap()` when you need to reuse the cached function multiple times
234
+ - Use `exec()` for one-off executions with caching
235
+
236
+ ### Cache Invalidation Strategies
171
237
 
172
- One of the core principles of **cds-caching** is **seamless integration with CAP**, aligning with CAP’s design and query execution model. As a result, **cds-caching** provides **native support** for **CQN (Core Query Notation) queries** and **cds.Requests**.
173
- #### Caching CQN queries
238
+ The caching service provides different strategies to invalidate cached values.
174
239
 
175
- CQN queries are widely used in CAP for **database operations** and **remote service calls (e.g., OData requests)**. Since these queries often involve **repeated data retrieval**, caching them can **significantly reduce response times** and **offload system resources**.
240
+ **IMPORTANT: You should not use cds-caching without a proper invalidation strategy.**
176
241
 
177
- **cds-caching** treats **CQN queries as first-class objects**, allowing them to be passed directly into the caching API. Internally, it generates a unique key based on the CQN query structure, ensuring **consistent retrieval** and **cache integrity**.
242
+ #### 1. Time-Based (TTL)
178
243
 
179
- **Example: Caching a CQN Query**
244
+ 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.
245
+ The TTL can be specified for individually through all cache methods (e.g. `set`, `run`, `send`, `wrap`, `exec`).
180
246
 
181
247
  ```javascript
248
+ // Store with 60 seconds TTL
249
+ await cache.set("key", "value", { ttl: 60000 })
182
250
 
183
- // Create the CQN object
184
- const query = SELECT.from(Foo)
251
+ // Run with 30 seconds TTL
252
+ const result = await cache.run(query, db, { ttl: 30000 })
185
253
 
186
- // Execute to fetch the result
187
- const result = await cds.run(query) // => [{...}, {...}]
254
+ // Send with 10 seconds TTL
255
+ const result = await cache.send(request, service, { ttl: 10000 })
188
256
 
189
- // Store value in the cache
190
- await cache.set(query, result)
257
+ // Wrap with 10 seconds TTL
258
+ const cachedOperation = cache.wrap("key", expensiveOperation, { ttl: 10000 })
191
259
 
192
- // Retrieve the value from the cache using the same CQN object
193
- const cachedResult = await cache.get(query) // => [{...}, {...}]
260
+ // Exec with 10 seconds TTL
261
+ const result = await cache.exec("key", async () => {
262
+ // ... some expensive computation
263
+ return result
264
+ }, {
265
+ ttl: 10000
266
+ })
267
+ ```
194
268
 
195
- // Create the key that is used internally
196
- const key = cache.createKey(query)
269
+ #### 2. Tag-Based
197
270
 
198
- // Delete the value from the cache
199
- await cache.delete(query)
271
+ 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`).
272
+ Tags can be provided as an array of strings or as an array of objects with the following properties:
273
+ - `value`: The value to use for the tag.
274
+ - `data`: A field from the value to use for the tag. This is working for objects and arrays of objects.
275
+ - `prefix`: A prefix that will be added to the tag.
276
+ - `suffix`: A suffix that will be added to the tag.
277
+ - `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.
278
+ Templates support the following properties:
279
+ - `{user}`: The current user
280
+ - `{tenant}`: The current tenant
281
+ - `{locale}`: The current locale
282
+ - `{hash}`: The hash of the current query
200
283
 
201
- ```
202
284
 
203
- While **CQN queries** can be cached directly, **cds-caching** also supports **caching full** cds.Requests, including their **request context**.
204
- #### Caching cds.Requests
205
285
 
206
- Caching requests is particularly useful when exposing **remote services** through **local CAP services**. For example, if your CAP application proxies an **external API**, caching can significantly reduce redundant requests and improve response times.
286
+ ```javascript
207
287
 
208
- 👉 **Use Case:** [Exposing Remote Services](https://cap.cloud.sap/docs/guides/using-services#expose-remote-services)
288
+ // Store with tags
289
+ await cache.set("key", "value", {
290
+ tags: [{ value: "user-123" }]
291
+ })
209
292
 
210
- **cds-caching** automatically includes contextual information in the cache key, making request caching **tenant- and user-aware**. The cache key incorporates:
293
+ // Invalidate by tag
294
+ await cache.deleteByTag('user-123')
295
+ ```
296
+ This is really useful for invalidating cache entries based on a specific attribute or context.
211
297
 
212
- * req.tenant Ensures data is scoped per tenant in multi-tenant environments.
213
- * req.user – Allows user-specific caching when necessary.
214
- * req.locale – Supports localized responses when caching multilingual content.
298
+ #### 3. Dynamic Tags
215
299
 
216
- **Example: Caching a cds.Request**
300
+ 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.
217
301
 
218
302
  ```javascript
219
- this.on('READ', BusinessPartners, async (req, next) => {
220
- const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
221
- let value = await cache.get(req)
222
- if(!value) {
223
- value = await bupa.run(req)
224
- await cache.set(req, next, { ttl: 3600 })
303
+
304
+ const businessPartners = [
305
+ {
306
+ businessPartner: 1,
307
+ name: 'John Doe'
308
+ },
309
+ {
310
+ businessPartner: 2,
311
+ name: 'Jane Doe'
225
312
  }
226
- return value;
313
+ ]
314
+
315
+ // Store with dynamic tags
316
+ await cache.set("key", businessPartners, {
317
+ tags: [
318
+ { data: 'businessPartner', prefix: 'bp-' },
319
+ { value: "businessPartner" }
320
+ ]
227
321
  })
322
+
323
+ // Introspect the tags
324
+ const tags = await cache.tags("key") // => ["bp-1", "bp-2", "businessPartner"]
325
+
326
+ // Invalidate by tag
327
+ await cache.deleteByTag('bp-1')
328
+ await cache.deleteByTag('bp-2')
228
329
  ```
229
330
 
230
- **When Not to Cache Full OData Services**
331
+ 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.
231
332
 
232
- While caching individual **requests** can improve performance, **caching an entire OData service is generally not recommended**. Here’s why:
333
+ ```javascript
334
+ const result = await cache.run(query, db, {
335
+ tags: [{ data: 'businessPartner', prefix: 'bp-' }]
336
+ })
337
+ ```
233
338
 
234
- 1. **Data Inconsistency** OData services expose **live business data**, which frequently changes. Caching responses without an **appropriate invalidation strategy** can lead to outdated or incorrect data being served.
339
+ 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`.
235
340
 
236
- 2. **Complex Query Variations** – OData allows **dynamic query parameters** ($filter, $expand, $orderby, etc.), making it difficult to cache efficiently without storing excessive variations.
341
+ ### Cache Iteration
237
342
 
238
- 3. **Large Payloads** Full OData responses can be **significantly large**, consuming cache memory inefficiently compared to caching targeted **CQN queries** or specific **request results**.
343
+ The caching service provides an iterator interface to traverse all cache entries:
239
344
 
240
- 👉 **Best Practice:** Instead of caching entire OData service responses, cache **specific queries or request results** where the data is frequently accessed and doesn’t change often.
345
+ ```javascript
346
+ const iterator = await cache.iterator()
241
347
 
242
- For example, focussing on remote services, **static master data**, or **computed results** is much safer and more efficient than blindly caching full OData responses.
348
+ for await (const entry of iterator) {
349
+ console.log(entry)
350
+ }
351
+ ```
243
352
 
244
- ### Read-through CQN queries and cds.Requests
353
+ 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).
245
354
 
246
- While this API is useful, it follows the **read-aside cache pattern**, meaning **manual cache checks** are required before fetching and storing data. In scenarios where caching logic becomes repetitive, **higher-level caching strategies** can help streamline this process and thanks to the already available CAP API, those are also available in cds-caching.
355
+ ### OData Service Caching Considerations
247
356
 
248
- In **read-through caching**, queries and requests are executed **through the caching service itself**, reducing the need for manual cache handling. **cds-caching** extends CAP’s built-in service methods with two key functions:
357
+ While caching individual requests can improve performance, **caching an entire OData service is generally not recommended**. Here's why:
249
358
 
250
- * run Executes **CQN queries** or **requests** against a database or remote OData service.
251
- [CAP Documentation: srv.run(query)](https://cap.cloud.sap/docs/node.js/core-services#srv-run-query)
359
+ 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.
252
360
 
253
- * send Sends **custom synchronous requests** (e.g., to REST APIs) with configurable paths and headers.
254
- [CAP Documentation: srv.send(request)](https://cap.cloud.sap/docs/node.js/core-services#srv-send-request)
361
+ 2. **Query Complexity**: OData allows dynamic query parameters ($filter, $expand, $orderby, etc.), making it difficult to cache efficiently without storing excessive variations.
255
362
 
256
- Using **read-through caching**, the previous read-aside pattern can be reduced to a **single line of code**:
363
+ 3. **Payload Size**: Full OData responses can be significantly large, consuming cache memory inefficiently compared to caching targeted CQN queries or specific request results.
257
364
 
258
- ```javascript
259
- this.on('READ', BusinessPartners, async (req, next) => {
260
- const bupa = await cds.connect.to('API_BUSINESS_PARTNER')
261
- return cache.run(req, bupa, { ttl: 3600 })
262
- })
263
- ```
365
+ Instead of caching entire OData service responses, focus on:
366
+ - Specific queries or request results
367
+ - Static master data
368
+ - Computed results
369
+ - Remote service calls with stable data
264
370
 
265
- With this approach, **all requests to the external service will be automatically executed and cached**, eliminating the need for manual cache handling.
371
+ ### Best Practices
266
372
 
267
- Some other examples :
373
+ 1. **Cache Selectively**: Not all data benefits from caching. Focus on:
374
+ - Frequently accessed, rarely changed data
375
+ - Computationally expensive operations
376
+ - Remote service calls with stable data
268
377
 
269
- ```javascript
270
- // Read-through for a CQN query
271
- const queryResult = await cache.run(SELECT.from("Foo"), db, { ttl: 3600 })
272
-
273
- // Read-through for a custom REST request
274
- const restService = await cds.connect.to({
275
- "kind": "rest",
276
- "credentials": {
277
- "url": "https://services.odata.org/V3/Northwind/Northwind.svc/"
278
- }
279
- });
280
- const restResult = await cache.send({ method: "GET", path: "Products" }, restService, { ttl: 3600 });
281
- ```
378
+ 2. **Use Appropriate TTLs**: Set TTLs based on data volatility:
379
+ - Short TTLs (seconds/minutes) for frequently changing data
380
+ - Longer TTLs (hours/days) for stable reference data
282
381
 
283
- The **read-through strategy** makes caching **cleaner and more maintainable**, as it abstracts cache management entirely. However, **the first request is still slow** (since there’s no cached value yet), but all subsequent requests will be **served instantly from the cache**.
284
- #### Wrapping async complex code
382
+ 3. **Implement Cache Tags**: Use tags for granular cache invalidation:
383
+ - Group related cache entries
384
+ - Enable targeted invalidation
385
+ - Use dynamic tags for user/tenant-specific caching
285
386
 
286
- The **read-through approach** can also be applied to **non-CAP-specific** operations. **cds-caching** provides a wrap function that caches the result of **any asynchronous function**.
387
+ 4. **Monitor Cache Performance**: Regularly check cache statistics:
388
+ - Hit rates
389
+ - Memory usage
390
+ - Response times
391
+ - Error rates
287
392
 
288
- ```javascript
289
- const expensiveFunction = async (param) => { /* Do something complex */ }
393
+ ### Limitations and Considerations
290
394
 
291
- // Wrap the function with caching
292
- const cachedExpensiveFunction = await cache.wrap("key", expensiveFunction, { ttl: 3600 })
395
+ 1. **Memory Usage**: Monitor cache size, especially with in-memory storage
396
+ 2. **Consistency**: Consider data freshness requirements when setting TTLs
397
+ 3. **Multi-Tenant**: Use appropriate namespacing and key strategies
398
+ 4. **Redis Setup**: Ensure proper configuration for production use
293
399
 
294
- // First call executes the function
295
- result = await cachedExpensiveFunction("someParam"); // No cache hit
400
+ ## Full API
296
401
 
297
- // Subsequent calls retrieve the result from cache
298
- result = await cachedExpensiveFunction("someParam"); // Cache hit
299
- ```
402
+ ### `cache.createKey(key: any)` : `string`
403
+
404
+ 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.
405
+
406
+ #### `key: any`
407
+
408
+ 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.
409
+
410
+ #### Returns
411
+
412
+ A string key.
413
+
414
+ ---
415
+
416
+ ### `await cache.set(key: any, value: any[, options: object])`
417
+
418
+ Sets a value in the cache.
419
+
420
+ #### `key: any`
421
+
422
+ The key to store the value under. The key handling is the same as for the `≈` method.
423
+
424
+ #### `value: any`
425
+
426
+ 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).
427
+
428
+ #### `options: object`
429
+
430
+ Object literal containing cache options.
431
+
432
+ The following properties are accepted:
433
+
434
+ | Property | Description | Example |
435
+ | ------------- | ------------- | ----------
436
+ | ttl | Time-to-live in milliseconds. | `1000`
437
+ | tags | Array of tags to associate with the value. Tags can be dynamic based on the given value (see chapter Cache Invalidation Strategies) | `[{template: 'user-{user}', value: '123'}]`
438
+
439
+ ---
440
+
441
+ ### `await cache.get(key: any)`
442
+
443
+ Gets a value from the cache.
444
+
445
+ #### `key: any`
446
+
447
+ The key to retrieve the value from. The key handling is the same as for the `createKey` method.
448
+
449
+ #### Returns
450
+
451
+ The deserialized value from the cache or `undefined` if the value does not exist.
452
+
453
+ ---
454
+
455
+ ### `await cache.has(key: any)`
456
+
457
+ Checks if a value exists in the cache.
458
+
459
+ #### `key: any`
460
+
461
+ The key to check for existence. The key handling is the same as for the `createKey` method.
462
+
463
+ #### Returns
464
+
465
+ `true` if the value exists in the cache, `false` otherwise.
466
+
467
+ ---
468
+
469
+ ### `await cache.delete(key: any)`
470
+
471
+ Deletes a value from the cache.
472
+
473
+ #### `key: any`
474
+
475
+ The key to delete the value from. The key handling is the same as for the `createKey` method.
476
+
477
+ ---
478
+
479
+ ### `await cache.clear()`
480
+
481
+ Clears the whole cache.
482
+
483
+ ---
484
+
485
+ ### `await cache.deleteByTag(tag: string)`
486
+
487
+ Deletes all values from the cache that are associated with the given tag.
488
+
489
+ #### `tag: string`
490
+
491
+ The tag to delete the values from.
492
+
493
+ ---
494
+
495
+ ### `await cache.run(query: cds.CQN , service: cds.Service)`
496
+
497
+ 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)
498
+
499
+ #### `object: cds.CQN`
500
+
501
+ The CQN query to run.
502
+
503
+ #### `service: cds.Service`
504
+
505
+ The service to run the query on.
506
+
507
+ #### Returns
508
+
509
+ The result of the query, either from the cache or the service.
510
+
511
+ ---
512
+
513
+ ### `await cache.send(request: cds.Request, service: cds.Service)`
514
+
515
+ 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.
516
+
517
+ #### `request: cds.Request`
518
+
519
+ The request to send.
520
+
521
+ #### `service: cds.Service `
522
+
523
+ The service to send the request to.
524
+
525
+ #### Returns
526
+
527
+ The result of the request, either from the cache or the service.
528
+
529
+ ---
530
+
531
+ ### `await cache.wrap(key: any, fn: async function, options: object)`
532
+
533
+ Wraps a function in a cache.
534
+
535
+ #### `key: any`
536
+
537
+ The key to store the cached function under. The key handling is the same as for the `createKey` method.
538
+
539
+ #### `fn: async function`
540
+
541
+ The async function to wrap in a cache.
542
+
543
+ #### `options: object`
544
+
545
+ The options to use for the cache.
546
+
547
+ #### Returns
548
+
549
+ A cached version of the function. The cached function will check the cache first and only execute the function if the cache miss.
550
+
551
+ ---
552
+
553
+ ### `await cache.exec(key: any, fn: async function, options: object)`
554
+
555
+ Executes a function and caches the result. This method is useful for one-off executions with caching.
556
+
557
+ #### `key: any`
558
+
559
+ The key to store the cached function under. The key handling is the same as for the `createKey` method.
560
+
561
+ #### `fn: async function`
562
+
563
+ The async function to execute.
564
+
565
+ #### `options: object`
566
+
567
+ The options to use for the cache.
568
+
569
+ #### Returns
570
+
571
+ The result of the function.
572
+
573
+ ---
574
+
575
+ ### `await cache.iterator()`
300
576
 
301
- This is particularly useful for **heavy computations**, ensuring they only need to be executed **once per TTL period**.
577
+ Returns an iterator over all cache entries.
302
578
 
579
+ #### Returns
303
580
 
304
- ## TODO:
581
+ An iterator over all cache entries.
305
582
 
306
- - [ ] Add documentation for cache iteration
307
- - [ ] Add documentation for cache invalidation
308
- - [ ] Add documentation for cache annotations
309
- - [ ] Add documentation for cache key generation
310
- - [ ] Add documentation for cache tags
311
- - [ ] Add documentation for cache statistics
312
- - [ ] Add documentation for running locally with redis on docker
313
- - [ ] Add documentation for running with redis on BTP
583
+ ---
314
584
 
315
585
 
316
- ## Contributing
586
+ ### Contributing
317
587
 
318
- Contributions are welcome! Please feel free to submit a Pull Request.
588
+ Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
319
589
 
320
- ## License
590
+ ### License
321
591
 
322
- This project is licensed under the MIT License - see the LICENSE file for detail
592
+ This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cds-caching",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A caching plugin for SAP CAP applications supporting Redis",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,8 @@
28
28
  "scripts": {
29
29
  "test": "jest --runInBand --silent",
30
30
  "test:watch": "jest --watch",
31
- "lint": "eslint ."
31
+ "lint": "eslint .",
32
+ "release": "release-it"
32
33
  },
33
34
  "peerDependencies": {
34
35
  "@sap/cds": ">=8"
@@ -49,7 +50,8 @@
49
50
  "chai-subset": "^1.6.0",
50
51
  "eslint": "^8.57.0",
51
52
  "husky": "^9.0.11",
52
- "jest": "^29.7.0"
53
+ "jest": "^29.7.0",
54
+ "release-it": "^18.1.2"
53
55
  },
54
56
  "engines": {
55
57
  "node": ">=16"
@@ -29,7 +29,7 @@ class CachingService extends cds.Service {
29
29
  ...(this.options.store === "redis" ? { store: new KeyvRedis({
30
30
  ...this.options.credentials,
31
31
  // Redis, Hyperscaler Option on BTP provides a URI
32
- ...(this.options.credentials.uri ? { url: this.options.credentials.uri } : {}),
32
+ ...(this.options.credentials?.uri ? { url: this.options.credentials?.uri } : {}),
33
33
  }) } : {}),
34
34
  compression: this.options.compression === "lz4" ? new KeyvLz4() : this.options.compression === "gzip" ? new KeyvGzip() : undefined
35
35
  }
@@ -523,6 +523,24 @@ class CachingService extends cds.Service {
523
523
  ].filter(Boolean).join('');
524
524
  }
525
525
 
526
+ /**
527
+ * Executes an async function and caches its result
528
+ *
529
+ * @param {string} key - the key to cache
530
+ * @param {function} asyncFunction - the async function to execute
531
+ * @param {object} options - additional options
532
+ * @returns {Promise<any>} - the result
533
+ */
534
+ async exec(key, asyncFunction, options = {}) {
535
+ const cacheKey = this.createKey(key, options.key);
536
+ if (await this.has(cacheKey)) {
537
+ return this.get(cacheKey);
538
+ }
539
+ const response = await asyncFunction();
540
+ await this.set(cacheKey, response, options);
541
+ return response;
542
+ }
543
+
526
544
  }
527
545
 
528
546
  module.exports = CachingService;