@cap-js-community/common 0.3.0 → 0.3.2

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 CHANGED
@@ -5,6 +5,18 @@ 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.2 - 2025-11-05
9
+
10
+ ### Fixed
11
+
12
+ - Rework redis client
13
+
14
+ ## Version 0.3.1 - 2025-11-05
15
+
16
+ ### Fixed
17
+
18
+ - Rework redis client
19
+
8
20
  ## Version 0.3.0 - 2025-11-03
9
21
 
10
22
  ### Fixed
package/README.md CHANGED
@@ -328,14 +328,14 @@ A Redis Client broker is provided to connect to Redis service.
328
328
 
329
329
  ```js
330
330
  const { RedisClient } = require("@cap-js-community/common");
331
- const mainClient = await RedisClient.default().createMainClientAndConnect(options);
331
+ const mainClient = await RedisClient.create().createMainClientAndConnect(options);
332
332
  ```
333
333
 
334
334
  #### Main named singleton
335
335
 
336
336
  ```js
337
337
  const { RedisClient } = require("@cap-js-community/common");
338
- const mainClient = await RedisClient.default("name").createMainClientAndConnect(options);
338
+ const mainClient = await RedisClient.create("name").createMainClientAndConnect(options);
339
339
  ```
340
340
 
341
341
  #### Custom named
@@ -384,14 +384,14 @@ Specific Redis options for a custom name can be established as follows:
384
384
 
385
385
  ```js
386
386
  const { RedisClient } = require("@cap-js-community/common");
387
- const mainClient = await RedisClient.default("customName").createMainClientAndConnect(options);
387
+ const mainClient = await RedisClient.create("customName").createMainClientAndConnect(options);
388
388
  ```
389
389
 
390
390
  In addition, options can be passed to Redis client during creation via `options` parameter:
391
391
 
392
392
  ```js
393
393
  const { RedisClient } = require("@cap-js-community/common");
394
- const mainClient = await RedisClient.default().createMainClientAndConnect(options);
394
+ const mainClient = await RedisClient.create().createMainClientAndConnect(options);
395
395
  ```
396
396
 
397
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, RedisClient } = require("./src");
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.0",
3
+ "version": "0.3.2",
4
4
  "description": "CAP Node.js Community Common",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "engines": {
@@ -56,7 +56,7 @@
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.36.0",
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.default(COMPONENT_NAME).connectionCheck();
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.default(COMPONENT_NAME).createMainClientAndConnect());
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,30 @@
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 {
9
- constructor(name) {
11
+ #clusterClient = false;
12
+ #beforeCloseHandler;
13
+ constructor(name, env) {
10
14
  this.name = name;
15
+ this.env = env || this.name;
11
16
  this.log = cds.log(COMPONENT_NAME);
12
17
  this.mainClientPromise = null;
13
- this.additionalClientPromise = null;
14
18
  this.subscriberClientPromise = null;
15
19
  this.subscribedChannels = {};
16
20
  this.lastErrorLog = Date.now();
21
+
22
+ if (!RedisClient._shutdownRegistered) {
23
+ RedisClient._shutdownRegistered = true;
24
+ cds.on("shutdown", async () => {
25
+ await this.closeRedisClients();
26
+ });
27
+ }
17
28
  }
18
29
 
19
30
  createMainClientAndConnect(options) {
@@ -33,19 +44,8 @@ class RedisClient {
33
44
  }
34
45
 
35
46
  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;
47
+ const redisClient = RedisClient.create(this.name + "-2", this.env);
48
+ return redisClient.createMainClientAndConnect(options);
49
49
  }
50
50
 
51
51
  async createClientAndConnect(options, errorHandlerCreateClient, isConnectionCheck) {
@@ -92,7 +92,8 @@ class RedisClient {
92
92
  throw error;
93
93
  }
94
94
  if (client) {
95
- await this.resilientClientClose(client);
95
+ // NOTE: ignore promise: client should not wait + fn can't throw
96
+ this.resilientClientClose(client);
96
97
  return true;
97
98
  }
98
99
  } catch (err) {
@@ -103,7 +104,7 @@ class RedisClient {
103
104
 
104
105
  createClientBase(redisOptions = {}) {
105
106
  const { credentials, options } =
106
- (this.name ? cds.env.requires[`redis-${this.name}`] : undefined) || cds.env.requires["redis"] || {};
107
+ (this.env ? cds.env.requires[`redis-${this.env}`] : undefined) || cds.env.requires["redis"] || {};
107
108
  const socket = {
108
109
  host: credentials?.hostname ?? "127.0.0.1",
109
110
  tls: !!credentials?.tls,
@@ -119,6 +120,7 @@ class RedisClient {
119
120
  };
120
121
  try {
121
122
  if (credentials?.cluster_mode) {
123
+ this.#clusterClient = true;
122
124
  return redis.createCluster({
123
125
  rootNodes: [socketOptions],
124
126
  defaults: socketOptions,
@@ -141,14 +143,14 @@ class RedisClient {
141
143
  LOG_AFTER_SEC * 1000,
142
144
  ).unref();
143
145
  };
144
- this.subscribeChannels(options, { [channel]: subscribeHandler }, errorHandlerCreateClient);
146
+ this.subscribeChannels(options, errorHandlerCreateClient);
145
147
  }
146
148
 
147
- subscribeChannels(options, subscribedChannels, errorHandlerCreateClient) {
149
+ subscribeChannels(options, errorHandlerCreateClient) {
148
150
  this.subscriberClientPromise = this.createClientAndConnect(options, errorHandlerCreateClient)
149
151
  .then((client) => {
150
152
  for (const channel in this.subscribedChannels) {
151
- const fn = subscribedChannels[channel];
153
+ const fn = this.subscribedChannels[channel];
152
154
  client._subscribedChannels ??= {};
153
155
  if (client._subscribedChannels[channel]) {
154
156
  continue;
@@ -193,16 +195,6 @@ class RedisClient {
193
195
  this.log.info("Main redis client closed!");
194
196
  }
195
197
 
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
198
  async closeSubscribeClient() {
207
199
  if (!this.subscriberClientPromise) {
208
200
  return;
@@ -214,9 +206,10 @@ class RedisClient {
214
206
  }
215
207
 
216
208
  async closeClients() {
217
- await this.closeMainClient();
218
- await this.closeAdditionalClient();
219
- await this.closeSubscribeClient();
209
+ if (this.#beforeCloseHandler) {
210
+ await this.#beforeCloseHandler();
211
+ }
212
+ await Promise.allSettled([this.closeMainClient(), this.closeSubscribeClient()]);
220
213
  }
221
214
 
222
215
  async resilientClientClose(client) {
@@ -229,16 +222,47 @@ class RedisClient {
229
222
  }
230
223
  }
231
224
 
232
- static default(name = "default") {
233
- RedisClient._default ??= {};
234
- if (!RedisClient._default[name]) {
235
- RedisClient._default[name] = new RedisClient(name);
225
+ async closeRedisClients() {
226
+ return await new Promise((resolve, reject) => {
227
+ const timeoutRef = setTimeout(() => {
228
+ clearTimeout(timeoutRef);
229
+ resolve();
230
+ }, TIMEOUT_SHUTDOWN);
231
+ RedisClient.closeAllClients()
232
+ .then((result) => {
233
+ clearTimeout(timeoutRef);
234
+ resolve(result);
235
+ })
236
+ .catch((err) => {
237
+ clearTimeout(timeoutRef);
238
+ reject(err);
239
+ });
240
+ });
241
+ }
242
+
243
+ set beforeCloseHandler(cb) {
244
+ this.#beforeCloseHandler = cb;
245
+ }
246
+
247
+ get isCluster() {
248
+ return this.#clusterClient;
249
+ }
250
+
251
+ static create(name = "default", env) {
252
+ env ??= name;
253
+ RedisClient._create ??= {};
254
+ if (!RedisClient._create[name]) {
255
+ RedisClient._create[name] = new RedisClient(name, env);
236
256
  }
237
- return RedisClient._default[name];
257
+ return RedisClient._create[name];
258
+ }
259
+
260
+ static default(name) {
261
+ return RedisClient.create(name);
238
262
  }
239
263
 
240
264
  static async closeAllClients() {
241
- for (const entry of Object.values(RedisClient._default || {})) {
265
+ for (const entry of Object.values(RedisClient._create || {})) {
242
266
  await entry.closeClients();
243
267
  }
244
268
  }