@cap-js-community/common 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +7 -4
- package/cds-plugin.js +1 -25
- package/package.json +3 -3
- package/src/migration-check/MigrationCheck.js +1 -1
- package/src/rate-limiting/RateLimiting.js +1 -1
- package/src/rate-limiting/redis/common.js +3 -3
- package/src/redis-client/RedisClient.js +54 -41
- package/src/replication-cache/ReplicationCache.js +36 -35
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## Version 0.3.1 - 2025-11-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Rework redis client
|
|
13
|
+
|
|
14
|
+
## Version 0.3.0 - 2025-11-03
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Refactor replication cache
|
|
19
|
+
- Change `@cap-js/sqlite` to dev dependency
|
|
20
|
+
- Replication cache requires `@cap-js/sqlite` as project dependency (no dev dependency)
|
|
21
|
+
|
|
8
22
|
## Version 0.2.8 - 2025-10-13
|
|
9
23
|
|
|
10
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -30,6 +30,9 @@ Local replicated SQLite database can be queried with same query as the original
|
|
|
30
30
|
|
|
31
31
|
### Usage
|
|
32
32
|
|
|
33
|
+
> Replication cache uses SQLite as local database for productive usage.
|
|
34
|
+
> Ensure `@cap-js/sqlite` is installed as dependency (not as dev dependency) in your project.
|
|
35
|
+
|
|
33
36
|
```cds
|
|
34
37
|
@cds.replicate
|
|
35
38
|
entity Books {
|
|
@@ -325,14 +328,14 @@ A Redis Client broker is provided to connect to Redis service.
|
|
|
325
328
|
|
|
326
329
|
```js
|
|
327
330
|
const { RedisClient } = require("@cap-js-community/common");
|
|
328
|
-
const mainClient = await RedisClient.
|
|
331
|
+
const mainClient = await RedisClient.create().createMainClientAndConnect(options);
|
|
329
332
|
```
|
|
330
333
|
|
|
331
334
|
#### Main named singleton
|
|
332
335
|
|
|
333
336
|
```js
|
|
334
337
|
const { RedisClient } = require("@cap-js-community/common");
|
|
335
|
-
const mainClient = await RedisClient.
|
|
338
|
+
const mainClient = await RedisClient.create("name").createMainClientAndConnect(options);
|
|
336
339
|
```
|
|
337
340
|
|
|
338
341
|
#### Custom named
|
|
@@ -381,14 +384,14 @@ Specific Redis options for a custom name can be established as follows:
|
|
|
381
384
|
|
|
382
385
|
```js
|
|
383
386
|
const { RedisClient } = require("@cap-js-community/common");
|
|
384
|
-
const mainClient = await RedisClient.
|
|
387
|
+
const mainClient = await RedisClient.create("customName").createMainClientAndConnect(options);
|
|
385
388
|
```
|
|
386
389
|
|
|
387
390
|
In addition, options can be passed to Redis client during creation via `options` parameter:
|
|
388
391
|
|
|
389
392
|
```js
|
|
390
393
|
const { RedisClient } = require("@cap-js-community/common");
|
|
391
|
-
const mainClient = await RedisClient.
|
|
394
|
+
const mainClient = await RedisClient.create().createMainClientAndConnect(options);
|
|
392
395
|
```
|
|
393
396
|
|
|
394
397
|
For details on Redis `createClient` configuration options see [Redis Client Configuration](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md).
|
package/cds-plugin.js
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
const cds = require("@sap/cds");
|
|
4
4
|
require("./src/common/promise");
|
|
5
5
|
|
|
6
|
-
const { ReplicationCache, RateLimiting
|
|
7
|
-
|
|
8
|
-
const TIMEOUT_SHUTDOWN = 2500;
|
|
6
|
+
const { ReplicationCache, RateLimiting } = require("./src");
|
|
9
7
|
|
|
10
8
|
if (cds.env.rateLimiting.plugin) {
|
|
11
9
|
cds.on("serving", async (service) => {
|
|
@@ -24,25 +22,3 @@ if (cds.env.rateLimiting.plugin) {
|
|
|
24
22
|
if (cds.env.replicationCache.plugin) {
|
|
25
23
|
cds.replicationCache = new ReplicationCache();
|
|
26
24
|
}
|
|
27
|
-
|
|
28
|
-
cds.on("shutdown", async () => {
|
|
29
|
-
await shutdownWebSocketServer();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
async function shutdownWebSocketServer() {
|
|
33
|
-
return await new Promise((resolve, reject) => {
|
|
34
|
-
const timeoutRef = setTimeout(() => {
|
|
35
|
-
clearTimeout(timeoutRef);
|
|
36
|
-
resolve();
|
|
37
|
-
}, TIMEOUT_SHUTDOWN);
|
|
38
|
-
RedisClient.closeAllClients()
|
|
39
|
-
.then((result) => {
|
|
40
|
-
clearTimeout(timeoutRef);
|
|
41
|
-
resolve(result);
|
|
42
|
-
})
|
|
43
|
-
.catch((err) => {
|
|
44
|
-
clearTimeout(timeoutRef);
|
|
45
|
-
reject(err);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js-community/common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "CAP Node.js Community Common",
|
|
5
5
|
"homepage": "https://cap.cloud.sap/",
|
|
6
6
|
"engines": {
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"audit": "npm audit --only=prod"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@cap-js/sqlite": "^2.0.3",
|
|
49
48
|
"commander": "^14.0.1",
|
|
50
49
|
"redis": "^4.7.1",
|
|
51
50
|
"verror": "^1.10.1"
|
|
@@ -53,10 +52,11 @@
|
|
|
53
52
|
"devDependencies": {
|
|
54
53
|
"@cap-js-community/common": "./",
|
|
55
54
|
"@cap-js/cds-test": "^0.4.0",
|
|
55
|
+
"@cap-js/sqlite": "^2.0.3",
|
|
56
56
|
"@sap/cds": "^9.4.3",
|
|
57
57
|
"@sap/cds-common-content": "^3.0.1",
|
|
58
58
|
"@sap/cds-dk": "^9.4.1",
|
|
59
|
-
"eslint": "^9.
|
|
59
|
+
"eslint": "^9.39.1",
|
|
60
60
|
"eslint-config-prettier": "^10.1.8",
|
|
61
61
|
"eslint-plugin-jest": "^29.0.1",
|
|
62
62
|
"eslint-plugin-n": "^17.23.1",
|
|
@@ -5,7 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
|
|
8
|
-
const COMPONENT_NAME = "migrationCheck";
|
|
8
|
+
const COMPONENT_NAME = "/cap-js-community-common/migrationCheck";
|
|
9
9
|
const STRING_DEFAULT_LENGTH = 5000;
|
|
10
10
|
|
|
11
11
|
const Checks = [releasedEntityCheck, newEntityCheck, uniqueIndexCheck, journalModeCheck];
|
|
@@ -7,7 +7,7 @@ const redisResetTime = require("./redis/resetTime");
|
|
|
7
7
|
|
|
8
8
|
const { connectionCheck } = require("./redis/common");
|
|
9
9
|
|
|
10
|
-
const COMPONENT_NAME = "rateLimiting";
|
|
10
|
+
const COMPONENT_NAME = "/cap-js-community-common/rateLimiting";
|
|
11
11
|
|
|
12
12
|
class RateLimiting {
|
|
13
13
|
constructor(service, { maxConcurrent, maxInWindow, window } = {}) {
|
|
@@ -4,14 +4,14 @@ const cds = require("@sap/cds");
|
|
|
4
4
|
|
|
5
5
|
const { RedisClient } = require("../../redis-client");
|
|
6
6
|
|
|
7
|
-
const COMPONENT_NAME = "rateLimiting";
|
|
7
|
+
const COMPONENT_NAME = "/cap-js-community-common/rateLimiting";
|
|
8
8
|
|
|
9
9
|
async function connectionCheck() {
|
|
10
|
-
return await RedisClient.
|
|
10
|
+
return await RedisClient.create(COMPONENT_NAME).connectionCheck();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
async function perform(key, cb, cbFallback, retry = cds.env.rateLimiting.retry) {
|
|
14
|
-
const client = cds.env.rateLimiting.redis && (await RedisClient.
|
|
14
|
+
const client = cds.env.rateLimiting.redis && (await RedisClient.create(COMPONENT_NAME).createMainClientAndConnect());
|
|
15
15
|
if (client) {
|
|
16
16
|
const value = await cb(client, key);
|
|
17
17
|
if (value === undefined) {
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const redis = require("redis");
|
|
4
|
+
const cds = require("@sap/cds");
|
|
4
5
|
|
|
5
|
-
const COMPONENT_NAME = "redisClient";
|
|
6
|
+
const COMPONENT_NAME = "/cap-js-community-common/redisClient";
|
|
6
7
|
const LOG_AFTER_SEC = 5;
|
|
8
|
+
const TIMEOUT_SHUTDOWN = 2500;
|
|
7
9
|
|
|
8
10
|
class RedisClient {
|
|
11
|
+
#clusterClient = false;
|
|
12
|
+
#beforeCloseHandler;
|
|
9
13
|
constructor(name) {
|
|
10
14
|
this.name = name;
|
|
11
15
|
this.log = cds.log(COMPONENT_NAME);
|
|
12
16
|
this.mainClientPromise = null;
|
|
13
|
-
this.additionalClientPromise = null;
|
|
14
17
|
this.subscriberClientPromise = null;
|
|
15
18
|
this.subscribedChannels = {};
|
|
16
19
|
this.lastErrorLog = Date.now();
|
|
20
|
+
|
|
21
|
+
if (!RedisClient._shutdownRegistered) {
|
|
22
|
+
RedisClient._shutdownRegistered = true;
|
|
23
|
+
cds.on("shutdown", async () => {
|
|
24
|
+
await this.closeRedisClients();
|
|
25
|
+
});
|
|
26
|
+
}
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
createMainClientAndConnect(options) {
|
|
@@ -32,22 +42,6 @@ class RedisClient {
|
|
|
32
42
|
return this.mainClientPromise;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
|
-
createAdditionalClientAndConnect(options) {
|
|
36
|
-
if (this.additionalClientPromise) {
|
|
37
|
-
return this.additionalClientPromise;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const errorHandlerCreateClient = (err) => {
|
|
41
|
-
this.additionalClientPromise?.then?.(this.resilientClientClose);
|
|
42
|
-
this.log.error("Error from additional redis client", err);
|
|
43
|
-
this.additionalClientPromise = null;
|
|
44
|
-
setTimeout(() => this.createAdditionalClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
this.additionalClientPromise = this.createClientAndConnect(options, errorHandlerCreateClient);
|
|
48
|
-
return this.additionalClientPromise;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
45
|
async createClientAndConnect(options, errorHandlerCreateClient, isConnectionCheck) {
|
|
52
46
|
try {
|
|
53
47
|
const client = this.createClientBase(options);
|
|
@@ -92,7 +86,8 @@ class RedisClient {
|
|
|
92
86
|
throw error;
|
|
93
87
|
}
|
|
94
88
|
if (client) {
|
|
95
|
-
|
|
89
|
+
// NOTE: ignore promise: client should not wait + fn can't throw
|
|
90
|
+
this.resilientClientClose(client);
|
|
96
91
|
return true;
|
|
97
92
|
}
|
|
98
93
|
} catch (err) {
|
|
@@ -119,6 +114,7 @@ class RedisClient {
|
|
|
119
114
|
};
|
|
120
115
|
try {
|
|
121
116
|
if (credentials?.cluster_mode) {
|
|
117
|
+
this.#clusterClient = true;
|
|
122
118
|
return redis.createCluster({
|
|
123
119
|
rootNodes: [socketOptions],
|
|
124
120
|
defaults: socketOptions,
|
|
@@ -141,14 +137,14 @@ class RedisClient {
|
|
|
141
137
|
LOG_AFTER_SEC * 1000,
|
|
142
138
|
).unref();
|
|
143
139
|
};
|
|
144
|
-
this.subscribeChannels(options,
|
|
140
|
+
this.subscribeChannels(options, errorHandlerCreateClient);
|
|
145
141
|
}
|
|
146
142
|
|
|
147
|
-
subscribeChannels(options,
|
|
143
|
+
subscribeChannels(options, errorHandlerCreateClient) {
|
|
148
144
|
this.subscriberClientPromise = this.createClientAndConnect(options, errorHandlerCreateClient)
|
|
149
145
|
.then((client) => {
|
|
150
146
|
for (const channel in this.subscribedChannels) {
|
|
151
|
-
const fn = subscribedChannels[channel];
|
|
147
|
+
const fn = this.subscribedChannels[channel];
|
|
152
148
|
client._subscribedChannels ??= {};
|
|
153
149
|
if (client._subscribedChannels[channel]) {
|
|
154
150
|
continue;
|
|
@@ -193,16 +189,6 @@ class RedisClient {
|
|
|
193
189
|
this.log.info("Main redis client closed!");
|
|
194
190
|
}
|
|
195
191
|
|
|
196
|
-
async closeAdditionalClient() {
|
|
197
|
-
if (!this.additionalClientPromise) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const client = this.additionalClientPromise;
|
|
201
|
-
this.additionalClientPromise = null;
|
|
202
|
-
await this.resilientClientClose(await client);
|
|
203
|
-
this.log.info("Additional redis client closed!");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
192
|
async closeSubscribeClient() {
|
|
207
193
|
if (!this.subscriberClientPromise) {
|
|
208
194
|
return;
|
|
@@ -214,9 +200,10 @@ class RedisClient {
|
|
|
214
200
|
}
|
|
215
201
|
|
|
216
202
|
async closeClients() {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
203
|
+
if (this.#beforeCloseHandler) {
|
|
204
|
+
await this.#beforeCloseHandler();
|
|
205
|
+
}
|
|
206
|
+
await Promise.allSettled([this.closeMainClient(), this.closeSubscribeClient()]);
|
|
220
207
|
}
|
|
221
208
|
|
|
222
209
|
async resilientClientClose(client) {
|
|
@@ -229,16 +216,42 @@ class RedisClient {
|
|
|
229
216
|
}
|
|
230
217
|
}
|
|
231
218
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
219
|
+
async closeRedisClients() {
|
|
220
|
+
return await new Promise((resolve, reject) => {
|
|
221
|
+
const timeoutRef = setTimeout(() => {
|
|
222
|
+
clearTimeout(timeoutRef);
|
|
223
|
+
resolve();
|
|
224
|
+
}, TIMEOUT_SHUTDOWN);
|
|
225
|
+
RedisClient.closeAllClients()
|
|
226
|
+
.then((result) => {
|
|
227
|
+
clearTimeout(timeoutRef);
|
|
228
|
+
resolve(result);
|
|
229
|
+
})
|
|
230
|
+
.catch((err) => {
|
|
231
|
+
clearTimeout(timeoutRef);
|
|
232
|
+
reject(err);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
set beforeCloseHandler(cb) {
|
|
238
|
+
this.#beforeCloseHandler = cb;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
get isCluster() {
|
|
242
|
+
return this.#clusterClient;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static create(name = "default") {
|
|
246
|
+
RedisClient._create ??= {};
|
|
247
|
+
if (!RedisClient._create[name]) {
|
|
248
|
+
RedisClient._create[name] = new RedisClient(name);
|
|
236
249
|
}
|
|
237
|
-
return RedisClient.
|
|
250
|
+
return RedisClient._create[name];
|
|
238
251
|
}
|
|
239
252
|
|
|
240
253
|
static async closeAllClients() {
|
|
241
|
-
for (const entry of Object.values(RedisClient.
|
|
254
|
+
for (const entry of Object.values(RedisClient._create || {})) {
|
|
242
255
|
await entry.closeClients();
|
|
243
256
|
}
|
|
244
257
|
}
|
|
@@ -5,8 +5,6 @@ const path = require("path");
|
|
|
5
5
|
const fs = require("fs").promises;
|
|
6
6
|
|
|
7
7
|
const cds = require("@sap/cds");
|
|
8
|
-
const SQLiteService = require("@cap-js/sqlite");
|
|
9
|
-
|
|
10
8
|
require("../common/promise");
|
|
11
9
|
|
|
12
10
|
const Component = "replicationCache";
|
|
@@ -51,7 +49,7 @@ class ReplicationCache {
|
|
|
51
49
|
this.group = this.options.group;
|
|
52
50
|
this.log = cds.log(Component);
|
|
53
51
|
this.template = null;
|
|
54
|
-
this.
|
|
52
|
+
this.entries = new Map();
|
|
55
53
|
this.initStats();
|
|
56
54
|
this.attach();
|
|
57
55
|
}
|
|
@@ -64,6 +62,8 @@ class ReplicationCache {
|
|
|
64
62
|
if (service.name === this.name) {
|
|
65
63
|
const refs = ReplicationCache.replicationRefs(this.model, service, this.options.deploy);
|
|
66
64
|
if (refs.length > 0) {
|
|
65
|
+
// @cap-js/sqlite dependency is required for replication cache
|
|
66
|
+
this.SQLiteService = require("@cap-js/sqlite");
|
|
67
67
|
this.setup(service, refs);
|
|
68
68
|
this.log.info("using replication cache", {
|
|
69
69
|
service: service.name,
|
|
@@ -74,7 +74,7 @@ class ReplicationCache {
|
|
|
74
74
|
this.options?.credentials?.database !== Constants.InMemory
|
|
75
75
|
) {
|
|
76
76
|
this.log.info("Preparing replication cache template database");
|
|
77
|
-
this.template = createDB(Tenant.Template, this.model, this.options)
|
|
77
|
+
this.template = createDB(this.SQLiteService, Tenant.Template, this.model, this.options)
|
|
78
78
|
.then(() => {
|
|
79
79
|
this.log.info("Prepared replication cache template database");
|
|
80
80
|
})
|
|
@@ -220,7 +220,7 @@ class ReplicationCache {
|
|
|
220
220
|
this.stats.used++;
|
|
221
221
|
this.stats.ratio = Math.round(this.stats.used / this.stats.hits);
|
|
222
222
|
this.log.debug("Replication cache was used");
|
|
223
|
-
const db = this.
|
|
223
|
+
const db = this.entries.get(tenant).db;
|
|
224
224
|
if (this.options.measure) {
|
|
225
225
|
return this.measure(
|
|
226
226
|
async () => {
|
|
@@ -316,15 +316,15 @@ class ReplicationCache {
|
|
|
316
316
|
if (refs.length === 0) {
|
|
317
317
|
return;
|
|
318
318
|
}
|
|
319
|
-
let tenantCache = cached(this.
|
|
320
|
-
return new ReplicationCacheTenant(tenant, model, this.options).prepare();
|
|
319
|
+
let tenantCache = cached(this.entries, tenant, async () => {
|
|
320
|
+
return new ReplicationCacheTenant(this, tenant, model, this.options).prepare();
|
|
321
321
|
});
|
|
322
322
|
return (async () => {
|
|
323
323
|
try {
|
|
324
324
|
const prepared = Promise.resolve(tenantCache).then(async (tenantCache) => {
|
|
325
325
|
const prepares = [];
|
|
326
326
|
for (const ref of refs) {
|
|
327
|
-
const entry = cached(tenantCache.
|
|
327
|
+
const entry = cached(tenantCache.entries, ref, () => {
|
|
328
328
|
return new ReplicationCacheEntry(this, tenantCache, ref);
|
|
329
329
|
});
|
|
330
330
|
entry.touched = Date.now();
|
|
@@ -346,7 +346,7 @@ class ReplicationCache {
|
|
|
346
346
|
return Status.NotReady;
|
|
347
347
|
}
|
|
348
348
|
for (const ref of refs) {
|
|
349
|
-
const entry = tenantCache.
|
|
349
|
+
const entry = tenantCache.entries.get(ref);
|
|
350
350
|
if (!entry || entry.status !== Status.Ready) {
|
|
351
351
|
return Status.NotReady;
|
|
352
352
|
}
|
|
@@ -361,13 +361,13 @@ class ReplicationCache {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
async prepared(tenant, ref) {
|
|
364
|
-
const tenants = tenant ? [tenant] : this.
|
|
364
|
+
const tenants = tenant ? [tenant] : this.entries.keys();
|
|
365
365
|
for (const id of tenants) {
|
|
366
|
-
const tenant = await this.
|
|
366
|
+
const tenant = await this.entries.get(id);
|
|
367
367
|
if (tenant) {
|
|
368
|
-
const refs = ref ? [ref] : tenant.
|
|
368
|
+
const refs = ref ? [ref] : tenant.entries.keys();
|
|
369
369
|
for (const ref of refs) {
|
|
370
|
-
const entry = tenant.
|
|
370
|
+
const entry = tenant.entries.get(ref);
|
|
371
371
|
if (entry) {
|
|
372
372
|
await entry.prepared;
|
|
373
373
|
}
|
|
@@ -377,13 +377,13 @@ class ReplicationCache {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
async clear(tenant, ref) {
|
|
380
|
-
const tenants = tenant ? [tenant] : this.
|
|
380
|
+
const tenants = tenant ? [tenant] : this.entries.keys();
|
|
381
381
|
for (const id of tenants) {
|
|
382
|
-
const tenant = await this.
|
|
382
|
+
const tenant = await this.entries.get(id);
|
|
383
383
|
if (tenant) {
|
|
384
|
-
const refs = ref ? [ref] : tenant.
|
|
384
|
+
const refs = ref ? [ref] : tenant.entries.keys();
|
|
385
385
|
for (const ref of refs) {
|
|
386
|
-
const entry = tenant.
|
|
386
|
+
const entry = tenant.entries.get(ref);
|
|
387
387
|
if (entry) {
|
|
388
388
|
await entry.clear();
|
|
389
389
|
this.log.debug("Replication cache cleared", {
|
|
@@ -404,10 +404,10 @@ class ReplicationCache {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
async prune(tenant) {
|
|
407
|
-
const maxSize = this.options.size / this.
|
|
408
|
-
const tenants = tenant ? [tenant] : this.
|
|
407
|
+
const maxSize = this.options.size / this.entries.size;
|
|
408
|
+
const tenants = tenant ? [tenant] : this.entries.keys();
|
|
409
409
|
for (const id of tenants) {
|
|
410
|
-
const tenant = await this.
|
|
410
|
+
const tenant = await this.entries.get(id);
|
|
411
411
|
const size = await this.size(tenant.id);
|
|
412
412
|
let diff = size - maxSize;
|
|
413
413
|
if (diff > 0) {
|
|
@@ -415,12 +415,12 @@ class ReplicationCache {
|
|
|
415
415
|
tenant,
|
|
416
416
|
diff,
|
|
417
417
|
});
|
|
418
|
-
const refs = [...tenant.
|
|
419
|
-
refs.sort((ref1, ref2) => tenant.
|
|
418
|
+
const refs = [...tenant.entries.keys()];
|
|
419
|
+
refs.sort((ref1, ref2) => tenant.entries.get(ref1).touched - tenant.entries.get(ref2).touched);
|
|
420
420
|
const pruneRefs = [];
|
|
421
421
|
for (const ref of refs) {
|
|
422
422
|
pruneRefs.push(ref);
|
|
423
|
-
const entry = tenant.
|
|
423
|
+
const entry = tenant.entries.get(ref);
|
|
424
424
|
if (entry) {
|
|
425
425
|
diff -= entry.size;
|
|
426
426
|
if (diff <= 0) {
|
|
@@ -429,7 +429,7 @@ class ReplicationCache {
|
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
for (const ref of pruneRefs) {
|
|
432
|
-
const entry = tenant.
|
|
432
|
+
const entry = tenant.entries.get(ref);
|
|
433
433
|
this.log.debug("Replication cache prunes ref for tenant", {
|
|
434
434
|
tenant,
|
|
435
435
|
ref,
|
|
@@ -444,13 +444,13 @@ class ReplicationCache {
|
|
|
444
444
|
|
|
445
445
|
async size(tenant, ref) {
|
|
446
446
|
let size = 0;
|
|
447
|
-
const tenants = tenant ? [tenant] : this.
|
|
447
|
+
const tenants = tenant ? [tenant] : this.entries.keys();
|
|
448
448
|
for (const id of tenants) {
|
|
449
|
-
const tenant = await this.
|
|
449
|
+
const tenant = await this.entries.get(id);
|
|
450
450
|
if (tenant) {
|
|
451
|
-
const refs = ref ? [ref] : tenant.
|
|
451
|
+
const refs = ref ? [ref] : tenant.entries.keys();
|
|
452
452
|
for (const ref of refs) {
|
|
453
|
-
const entry = tenant.
|
|
453
|
+
const entry = tenant.entries.get(ref);
|
|
454
454
|
if (entry) {
|
|
455
455
|
size += entry.size;
|
|
456
456
|
}
|
|
@@ -461,7 +461,7 @@ class ReplicationCache {
|
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
async tenantSize(id) {
|
|
464
|
-
const tenant = await this.
|
|
464
|
+
const tenant = await this.entries.get(id);
|
|
465
465
|
if (tenant) {
|
|
466
466
|
return await tenant.db.tx(async (tx) => {
|
|
467
467
|
const result = await tx.run(
|
|
@@ -578,16 +578,17 @@ class ReplicationCache {
|
|
|
578
578
|
}
|
|
579
579
|
|
|
580
580
|
class ReplicationCacheTenant {
|
|
581
|
-
constructor(tenant, model, options) {
|
|
581
|
+
constructor(cache, tenant, model, options) {
|
|
582
|
+
this.cache = cache;
|
|
582
583
|
this.id = tenant;
|
|
583
584
|
this.model = model;
|
|
584
585
|
this.options = options;
|
|
585
586
|
this.csn = model.definitions;
|
|
586
|
-
this.
|
|
587
|
+
this.entries = new Map();
|
|
587
588
|
}
|
|
588
589
|
|
|
589
590
|
async prepare() {
|
|
590
|
-
this.db = await createDB(this.id, this.model, this.options);
|
|
591
|
+
this.db = await createDB(this.cache.SQLiteService, this.id, this.model, this.options);
|
|
591
592
|
return this;
|
|
592
593
|
}
|
|
593
594
|
}
|
|
@@ -697,7 +698,7 @@ class ReplicationCacheEntry {
|
|
|
697
698
|
async load(thread) {
|
|
698
699
|
this.timestamp = Date.now();
|
|
699
700
|
await this.clear();
|
|
700
|
-
if (thread && cds.context && this.service instanceof SQLiteService) {
|
|
701
|
+
if (thread && cds.context && this.service instanceof this.cache.SQLiteService) {
|
|
701
702
|
const srcTx = this.service.tx(cds.context);
|
|
702
703
|
await this.db.tx({ tenant: this.tenant.id }, async (destTx) => {
|
|
703
704
|
await this.loadRecords(srcTx, destTx);
|
|
@@ -803,7 +804,7 @@ class ReplicationCacheEntry {
|
|
|
803
804
|
|
|
804
805
|
module.exports = ReplicationCache;
|
|
805
806
|
|
|
806
|
-
async function createDB(tenant, model, options) {
|
|
807
|
+
async function createDB(DBService, tenant, model, options) {
|
|
807
808
|
const filePath = await dbPath(tenant, options);
|
|
808
809
|
cds.log(Component).debug("Preparing replication cache database", {
|
|
809
810
|
tenant,
|
|
@@ -813,7 +814,7 @@ async function createDB(tenant, model, options) {
|
|
|
813
814
|
const templateDatabase = await dbPath(Tenant.Template, options);
|
|
814
815
|
await fs.copyFile(templateDatabase, filePath);
|
|
815
816
|
}
|
|
816
|
-
const db = new
|
|
817
|
+
const db = new DBService(tenant ?? Tenant.Default, model, {
|
|
817
818
|
kind: "sqlite",
|
|
818
819
|
impl: "@cap-js/sqlite",
|
|
819
820
|
multiTenant: false,
|