cds-caching 0.3.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +691 -278
- package/cds-plugin.js +2 -2
- package/index.cds +165 -20
- package/lib/CachingService.d.ts +328 -0
- package/lib/CachingService.js +373 -0
- package/lib/operations/AsyncOperations.js +335 -0
- package/lib/operations/BasicOperations.js +211 -0
- package/lib/operations/CapOperations.js +540 -0
- package/lib/support/CacheStatisticsHandler.js +1124 -0
- package/lib/support/CacheStoreManager.js +120 -0
- package/lib/support/KeyManager.js +132 -0
- package/lib/support/RuntimeConfigurationManager.js +148 -0
- package/lib/support/StatisticsPersistenceManager.js +375 -0
- package/lib/support/TagResolver.js +125 -0
- package/lib/util.js +201 -0
- package/package.json +18 -13
- package/srv/caching-api-service.js +124 -0
- package/srv/CacheStatisticsHandler.js +0 -213
- package/srv/CachingService.js +0 -587
- package/srv/statistics-service.cds +0 -37
- package/srv/statistics-service.js +0 -72
- package/srv/util.js +0 -101
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cds-caching",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A caching plugin for SAP CAP applications
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "A caching plugin for SAP CAP applications",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/mikezaschka/cds-caching.git"
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
},
|
|
22
22
|
"homepage": "https://github.com/mikezaschka/cds-caching#readme",
|
|
23
23
|
"main": "cds-plugin.js",
|
|
24
|
+
"types": "lib/CachingService.d.ts",
|
|
24
25
|
"files": [
|
|
25
26
|
"srv/**",
|
|
26
|
-
"index.cds"
|
|
27
|
+
"index.cds",
|
|
28
|
+
"lib/**"
|
|
27
29
|
],
|
|
28
30
|
"scripts": {
|
|
29
31
|
"test": "jest --runInBand --silent",
|
|
@@ -32,27 +34,30 @@
|
|
|
32
34
|
"release": "release-it"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
|
-
"@sap/cds": ">=
|
|
37
|
+
"@sap/cds": ">=8"
|
|
36
38
|
},
|
|
37
39
|
"workspaces": [
|
|
38
40
|
".",
|
|
39
|
-
"test
|
|
41
|
+
"test/app",
|
|
42
|
+
"examples/dashboard",
|
|
43
|
+
"examples/app"
|
|
40
44
|
],
|
|
41
45
|
"dependencies": {
|
|
42
46
|
"@keyv/compress-gzip": "^2.0.3",
|
|
43
|
-
"@keyv/compress-lz4": "^1.0.
|
|
44
|
-
"@keyv/redis": "^
|
|
47
|
+
"@keyv/compress-lz4": "^1.0.1",
|
|
48
|
+
"@keyv/redis": "^5.0.0",
|
|
45
49
|
"@keyv/sqlite": "^4.0.5",
|
|
46
|
-
"keyv": "^5.
|
|
50
|
+
"keyv": "^5.4.0"
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
|
-
"
|
|
53
|
+
"@cap-js/cds-test": "^0.4.0",
|
|
54
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
55
|
+
"eslint": "^9.32.0",
|
|
50
56
|
"husky": "^9.1.7",
|
|
51
|
-
"jest": "^30.0.
|
|
52
|
-
"release-it": "^19.0.
|
|
53
|
-
"@cap-js/cds-test": "^0.4.0"
|
|
57
|
+
"jest": "^30.0.5",
|
|
58
|
+
"release-it": "^19.0.4"
|
|
54
59
|
},
|
|
55
60
|
"engines": {
|
|
56
|
-
"node": ">=
|
|
61
|
+
"node": ">=20"
|
|
57
62
|
}
|
|
58
63
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const cds = require('@sap/cds')
|
|
2
|
+
|
|
3
|
+
class CachingApiService extends cds.ApplicationService {
|
|
4
|
+
log = cds.log('cds-caching');
|
|
5
|
+
|
|
6
|
+
async init() {
|
|
7
|
+
|
|
8
|
+
// Handle setMetricsEnabled action
|
|
9
|
+
this.on('setMetricsEnabled', async (req) => {
|
|
10
|
+
const { enabled } = req.data
|
|
11
|
+
const cache = req.params[0].name;
|
|
12
|
+
const cacheService = await cds.connect.to(cache);
|
|
13
|
+
try {
|
|
14
|
+
await cacheService.setMetricsEnabled(enabled)
|
|
15
|
+
req.info(`Metrics ${enabled ? 'enabled' : 'disabled'} for cache ${cache}`);
|
|
16
|
+
return true;
|
|
17
|
+
} catch (error) {
|
|
18
|
+
req.error(`Failed to set metrics enabled: ${error.message}`);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Handle setKeyMetricsEnabled action
|
|
24
|
+
this.on('setKeyMetricsEnabled', async (req) => {
|
|
25
|
+
const { enabled } = req.data
|
|
26
|
+
const cache = req.params[0].name;
|
|
27
|
+
const cacheService = await cds.connect.to(cache);
|
|
28
|
+
try {
|
|
29
|
+
await cacheService.setKeyMetricsEnabled(enabled)
|
|
30
|
+
req.info(`Key metrics ${enabled ? 'enabled' : 'disabled'} for cache ${cache}`);
|
|
31
|
+
return true;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
req.error(`Failed to set key metrics enabled: ${error.message}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Handle getCacheEntries function
|
|
39
|
+
this.on('getEntries', async (req) => {
|
|
40
|
+
const cache = req.params[0].name;
|
|
41
|
+
try {
|
|
42
|
+
const cacheService = await cds.connect.to(cache);
|
|
43
|
+
const entries = [];
|
|
44
|
+
for await (const [key, value] of cacheService.iterator()) {
|
|
45
|
+
entries.push({
|
|
46
|
+
entryKey: key,
|
|
47
|
+
value: JSON.stringify(value.value),
|
|
48
|
+
timestamp: value.timestamp,
|
|
49
|
+
tags: value.tags,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return entries;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
req.error(`Failed to get cache entries: ${error.message}`);
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Handle getCacheEntry function
|
|
60
|
+
this.on('getEntry', async (req) => {
|
|
61
|
+
const { key } = req.data;
|
|
62
|
+
const cache = req.params[0].name;
|
|
63
|
+
const cacheService = await cds.connect.to(cache);
|
|
64
|
+
const value = await cacheService.get(key);
|
|
65
|
+
return {
|
|
66
|
+
value: value,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Handle setCacheEntry action
|
|
71
|
+
this.on('setEntry', async (req) => {
|
|
72
|
+
const { key, value, ttl } = req.data;
|
|
73
|
+
const cache = req.params[0].name;
|
|
74
|
+
const cacheService = await cds.connect.to(cache);
|
|
75
|
+
await cacheService.set(key, value, { ttl: ttl });
|
|
76
|
+
req.info(`Cache entry set successfully: ${key}`);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Handle deleteCacheEntry action
|
|
81
|
+
this.on('deleteEntry', async (req) => {
|
|
82
|
+
const { key } = req.data;
|
|
83
|
+
const cache = req.params[0].name;
|
|
84
|
+
const cacheService = await cds.connect.to(cache);
|
|
85
|
+
await cacheService.delete(key);
|
|
86
|
+
req.info(`Cache entry deleted successfully: ${key}`);
|
|
87
|
+
return true;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Handle clearCache action
|
|
91
|
+
this.on('clear', async (req) => {
|
|
92
|
+
const cache = req.params[0].name;
|
|
93
|
+
const cacheService = await cds.connect.to(cache);
|
|
94
|
+
await cacheService.clear();
|
|
95
|
+
req.info(`Cache cleared successfully: ${cache}`);
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Handle clearKeyMetrics action
|
|
100
|
+
this.on('clearKeyMetrics', async (req) => {
|
|
101
|
+
const cache = req.params[0].name;
|
|
102
|
+
const cacheService = await cds.connect.to(cache);
|
|
103
|
+
await cacheService.clearKeyMetrics();
|
|
104
|
+
req.info(`Key metrics cleared successfully: ${cache}`);
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Handle clearMetrics action
|
|
109
|
+
this.on('clearMetrics', async (req) => {
|
|
110
|
+
const cache = req.params[0].name;
|
|
111
|
+
const cacheService = await cds.connect.to(cache);
|
|
112
|
+
await cacheService.clearMetrics();
|
|
113
|
+
req.info(`Metrics cleared successfully: ${cache}`);
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await super.init()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = CachingApiService
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
const cds = require('@sap/cds');
|
|
2
|
-
|
|
3
|
-
class CacheStatisticsHandler {
|
|
4
|
-
constructor(options = {}) {
|
|
5
|
-
console.log(options);
|
|
6
|
-
this.options = {
|
|
7
|
-
persistenceInterval: 5 * 60 * 60 * 1000, // 5 minutes
|
|
8
|
-
maxLatencies: 1000,
|
|
9
|
-
...options
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
this.stats = {
|
|
13
|
-
current: {
|
|
14
|
-
hits: 0,
|
|
15
|
-
misses: 0,
|
|
16
|
-
sets: 0,
|
|
17
|
-
deletes: 0,
|
|
18
|
-
errors: 0,
|
|
19
|
-
latencies: []
|
|
20
|
-
},
|
|
21
|
-
lastPersisted: Date.now()
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
if (this.options.enabled) {
|
|
25
|
-
cds.once('served', () => {
|
|
26
|
-
this.persistInterval = setInterval(
|
|
27
|
-
() => this.persistStats(),
|
|
28
|
-
this.options.persistenceInterval
|
|
29
|
-
);
|
|
30
|
-
})
|
|
31
|
-
cds.on('shutdown', () => {
|
|
32
|
-
clearInterval(this.persistInterval)
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
recordHit(latencyMs) {
|
|
38
|
-
this.stats.current.hits++;
|
|
39
|
-
this.recordLatency(latencyMs);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
recordMiss() {
|
|
43
|
-
this.stats.current.misses++;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
recordSet() {
|
|
47
|
-
this.stats.current.sets++;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
recordDelete() {
|
|
51
|
-
this.stats.current.deletes++;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
recordError() {
|
|
55
|
-
this.stats.current.errors++;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
recordLatency(ms) {
|
|
59
|
-
this.stats.current.latencies.push(ms);
|
|
60
|
-
if (this.stats.current.latencies.length > this.options.maxLatencies) {
|
|
61
|
-
this.stats.current.latencies.shift();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async persistStats() {
|
|
66
|
-
if (!this.options.enabled) return;
|
|
67
|
-
|
|
68
|
-
const now = new Date().toISOString();
|
|
69
|
-
const hourlyId = `hourly:${now.slice(0, 13)}`;
|
|
70
|
-
const dailyId = `daily:${now.slice(0, 10)}`;
|
|
71
|
-
|
|
72
|
-
const stats = await this.calculateStats();
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
|
|
76
|
-
// Persist hourly stats
|
|
77
|
-
const existingHourly = await SELECT.one.from("cds_caching_Statistics")
|
|
78
|
-
.where({ ID: hourlyId, cache: this.options.cache });
|
|
79
|
-
|
|
80
|
-
if (!existingHourly) {
|
|
81
|
-
await INSERT.into('cds_caching_Statistics').entries([{
|
|
82
|
-
ID: hourlyId,
|
|
83
|
-
cache: this.options.cache,
|
|
84
|
-
timestamp: now,
|
|
85
|
-
period: 'hourly',
|
|
86
|
-
...stats
|
|
87
|
-
}]);
|
|
88
|
-
} else {
|
|
89
|
-
await UPDATE('cds_caching_Statistics')
|
|
90
|
-
.set({
|
|
91
|
-
hits: { '+=': stats.hits },
|
|
92
|
-
misses: { '+=': stats.misses },
|
|
93
|
-
sets: { '+=': stats.sets },
|
|
94
|
-
deletes: { '+=': stats.deletes },
|
|
95
|
-
errors: { '+=': stats.errors },
|
|
96
|
-
avgLatency: (existingHourly.avgLatency + stats.avgLatency) / 2,
|
|
97
|
-
p95Latency: Math.max(existingHourly.p95Latency, stats.p95Latency),
|
|
98
|
-
memoryUsage: stats.memoryUsage,
|
|
99
|
-
itemCount: stats.itemCount
|
|
100
|
-
})
|
|
101
|
-
.where({ ID: hourlyId, cache: this.options.cache });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Update or insert daily stats
|
|
105
|
-
const existingDaily = await SELECT.one.from("cds_caching_Statistics")
|
|
106
|
-
.where({ ID: dailyId, cache: this.options.cache });
|
|
107
|
-
|
|
108
|
-
if (existingDaily) {
|
|
109
|
-
await UPDATE('cds_caching_Statistics')
|
|
110
|
-
.set({
|
|
111
|
-
hits: { '+=': stats.hits },
|
|
112
|
-
misses: { '+=': stats.misses },
|
|
113
|
-
sets: { '+=': stats.sets },
|
|
114
|
-
deletes: { '+=': stats.deletes },
|
|
115
|
-
errors: { '+=': stats.errors },
|
|
116
|
-
avgLatency: (existingDaily.avgLatency + stats.avgLatency) / 2,
|
|
117
|
-
p95Latency: Math.max(existingDaily.p95Latency, stats.p95Latency),
|
|
118
|
-
memoryUsage: stats.memoryUsage,
|
|
119
|
-
itemCount: stats.itemCount
|
|
120
|
-
})
|
|
121
|
-
.where({ ID: dailyId, cache: this.options.cache });
|
|
122
|
-
} else {
|
|
123
|
-
await INSERT.into("cds_caching_Statistics").entries({
|
|
124
|
-
ID: dailyId,
|
|
125
|
-
cache: this.options.cache,
|
|
126
|
-
timestamp: now,
|
|
127
|
-
period: 'daily',
|
|
128
|
-
...stats
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
this.resetCurrentStats();
|
|
133
|
-
|
|
134
|
-
} catch (error) {
|
|
135
|
-
cds.log('caching').error('Error persisting cache statistics:', error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async calculateStats() {
|
|
140
|
-
const latencies = this.stats.current.latencies;
|
|
141
|
-
const avgLatency = latencies.length > 0
|
|
142
|
-
? latencies.reduce((a, b) => a + b, 0) / latencies.length
|
|
143
|
-
: 0;
|
|
144
|
-
const p95Latency = latencies.length > 0
|
|
145
|
-
? latencies.sort((a, b) => a - b)[Math.floor(latencies.length * 0.95)]
|
|
146
|
-
: 0;
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
hits: this.stats.current.hits,
|
|
150
|
-
misses: this.stats.current.misses,
|
|
151
|
-
sets: this.stats.current.sets,
|
|
152
|
-
deletes: this.stats.current.deletes,
|
|
153
|
-
errors: this.stats.current.errors,
|
|
154
|
-
avgLatency,
|
|
155
|
-
p95Latency,
|
|
156
|
-
memoryUsage: process.memoryUsage().heapUsed,
|
|
157
|
-
itemCount: await this.options.getItemCount?.() || 0
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
resetCurrentStats() {
|
|
162
|
-
this.stats.current = {
|
|
163
|
-
hits: 0,
|
|
164
|
-
misses: 0,
|
|
165
|
-
sets: 0,
|
|
166
|
-
deletes: 0,
|
|
167
|
-
errors: 0,
|
|
168
|
-
latencies: []
|
|
169
|
-
};
|
|
170
|
-
this.stats.lastPersisted = Date.now();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async getStats(period = 'hourly', from, to) {
|
|
174
|
-
if (!this.options.enabled) return null;
|
|
175
|
-
|
|
176
|
-
const query = SELECT.from("cds_caching_Statistics")
|
|
177
|
-
.where({ period: period });
|
|
178
|
-
|
|
179
|
-
if (from) query.and({ timestamp: { '>=': from } });
|
|
180
|
-
if (to) query.and({ timestamp: { '<=': to } });
|
|
181
|
-
|
|
182
|
-
query.orderBy({ timestamp: 'desc' });
|
|
183
|
-
|
|
184
|
-
return await query;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async getCurrentStats() {
|
|
188
|
-
if (!this.options.enabled) return null;
|
|
189
|
-
|
|
190
|
-
const { current, lastPersisted } = this.stats;
|
|
191
|
-
const latencies = current.latencies;
|
|
192
|
-
|
|
193
|
-
return {
|
|
194
|
-
...current,
|
|
195
|
-
avgLatency: latencies.length > 0
|
|
196
|
-
? latencies.reduce((a, b) => a + b, 0) / latencies.length
|
|
197
|
-
: 0,
|
|
198
|
-
p95Latency: latencies.length > 0
|
|
199
|
-
? latencies.sort((a, b) => a - b)[Math.floor(latencies.length * 0.95)]
|
|
200
|
-
: 0,
|
|
201
|
-
hitRatio: (current.hits / (current.hits + current.misses)) || 0,
|
|
202
|
-
lastPersisted: new Date(lastPersisted)
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
dispose() {
|
|
207
|
-
if (this.persistInterval) {
|
|
208
|
-
clearInterval(this.persistInterval);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
module.exports = CacheStatisticsHandler;
|