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 +431 -161
- package/package.json +5 -3
- package/srv/CachingService.js +19 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 it
|
|
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
|
-
```
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
99
|
-
#### Cache Events
|
|
123
|
+
#### 2. CQN Query Caching
|
|
100
124
|
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
113
|
-
cache.
|
|
114
|
-
|
|
115
|
-
})
|
|
143
|
+
// Runs the query internally and caches the result
|
|
144
|
+
const result = await cache.run(query, db)
|
|
145
|
+
```
|
|
116
146
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
-
|
|
174
|
+
#### 4. Declarative Caching with Annotations
|
|
134
175
|
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
#### Compression
|
|
200
|
+
#### 5. Function Caching
|
|
150
201
|
|
|
151
|
-
|
|
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
|
-
```
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
#### Caching CQN queries
|
|
238
|
+
The caching service provides different strategies to invalidate cached values.
|
|
174
239
|
|
|
175
|
-
|
|
240
|
+
**IMPORTANT: You should not use cds-caching without a proper invalidation strategy.**
|
|
176
241
|
|
|
177
|
-
|
|
242
|
+
#### 1. Time-Based (TTL)
|
|
178
243
|
|
|
179
|
-
|
|
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
|
-
//
|
|
184
|
-
const
|
|
251
|
+
// Run with 30 seconds TTL
|
|
252
|
+
const result = await cache.run(query, db, { ttl: 30000 })
|
|
185
253
|
|
|
186
|
-
//
|
|
187
|
-
const result = await
|
|
254
|
+
// Send with 10 seconds TTL
|
|
255
|
+
const result = await cache.send(request, service, { ttl: 10000 })
|
|
188
256
|
|
|
189
|
-
//
|
|
190
|
-
|
|
257
|
+
// Wrap with 10 seconds TTL
|
|
258
|
+
const cachedOperation = cache.wrap("key", expensiveOperation, { ttl: 10000 })
|
|
191
259
|
|
|
192
|
-
//
|
|
193
|
-
const
|
|
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
|
-
|
|
196
|
-
const key = cache.createKey(query)
|
|
269
|
+
#### 2. Tag-Based
|
|
197
270
|
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
286
|
+
```javascript
|
|
207
287
|
|
|
208
|
-
|
|
288
|
+
// Store with tags
|
|
289
|
+
await cache.set("key", "value", {
|
|
290
|
+
tags: [{ value: "user-123" }]
|
|
291
|
+
})
|
|
209
292
|
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
303
|
+
|
|
304
|
+
const businessPartners = [
|
|
305
|
+
{
|
|
306
|
+
businessPartner: 1,
|
|
307
|
+
name: 'John Doe'
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
businessPartner: 2,
|
|
311
|
+
name: 'Jane Doe'
|
|
225
312
|
}
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
+
```javascript
|
|
334
|
+
const result = await cache.run(query, db, {
|
|
335
|
+
tags: [{ data: 'businessPartner', prefix: 'bp-' }]
|
|
336
|
+
})
|
|
337
|
+
```
|
|
233
338
|
|
|
234
|
-
|
|
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
|
-
|
|
341
|
+
### Cache Iteration
|
|
237
342
|
|
|
238
|
-
|
|
343
|
+
The caching service provides an iterator interface to traverse all cache entries:
|
|
239
344
|
|
|
240
|
-
|
|
345
|
+
```javascript
|
|
346
|
+
const iterator = await cache.iterator()
|
|
241
347
|
|
|
242
|
-
|
|
348
|
+
for await (const entry of iterator) {
|
|
349
|
+
console.log(entry)
|
|
350
|
+
}
|
|
351
|
+
```
|
|
243
352
|
|
|
244
|
-
|
|
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
|
-
|
|
355
|
+
### OData Service Caching Considerations
|
|
247
356
|
|
|
248
|
-
|
|
357
|
+
While caching individual requests can improve performance, **caching an entire OData service is generally not recommended**. Here's why:
|
|
249
358
|
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
371
|
+
### Best Practices
|
|
266
372
|
|
|
267
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
284
|
-
|
|
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
|
-
|
|
387
|
+
4. **Monitor Cache Performance**: Regularly check cache statistics:
|
|
388
|
+
- Hit rates
|
|
389
|
+
- Memory usage
|
|
390
|
+
- Response times
|
|
391
|
+
- Error rates
|
|
287
392
|
|
|
288
|
-
|
|
289
|
-
const expensiveFunction = async (param) => { /* Do something complex */ }
|
|
393
|
+
### Limitations and Considerations
|
|
290
394
|
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
result = await cachedExpensiveFunction("someParam"); // No cache hit
|
|
400
|
+
## Full API
|
|
296
401
|
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
577
|
+
Returns an iterator over all cache entries.
|
|
302
578
|
|
|
579
|
+
#### Returns
|
|
303
580
|
|
|
304
|
-
|
|
581
|
+
An iterator over all cache entries.
|
|
305
582
|
|
|
306
|
-
|
|
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
|
-
|
|
586
|
+
### Contributing
|
|
317
587
|
|
|
318
|
-
Contributions are welcome! Please
|
|
588
|
+
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
|
|
319
589
|
|
|
320
|
-
|
|
590
|
+
### License
|
|
321
591
|
|
|
322
|
-
This project is licensed under the
|
|
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.
|
|
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"
|
package/srv/CachingService.js
CHANGED
|
@@ -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
|
|
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;
|