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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cds-caching",
3
- "version": "0.3.3",
4
- "description": "A caching plugin for SAP CAP applications supporting Redis",
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": ">=9"
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.0",
44
- "@keyv/redis": "^4.5.0",
47
+ "@keyv/compress-lz4": "^1.0.1",
48
+ "@keyv/redis": "^5.0.0",
45
49
  "@keyv/sqlite": "^4.0.5",
46
- "keyv": "^5.3.4"
50
+ "keyv": "^5.4.0"
47
51
  },
48
52
  "devDependencies": {
49
- "eslint": "^9.30.1",
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.4",
52
- "release-it": "^19.0.3",
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": ">=16"
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;