@cap-js-community/common 0.3.0 → 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 CHANGED
@@ -5,6 +5,12 @@ 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
+
8
14
  ## Version 0.3.0 - 2025-11-03
9
15
 
10
16
  ### 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.1",
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,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
- await this.resilientClientClose(client);
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, { [channel]: subscribeHandler }, errorHandlerCreateClient);
140
+ this.subscribeChannels(options, errorHandlerCreateClient);
145
141
  }
146
142
 
147
- subscribeChannels(options, subscribedChannels, errorHandlerCreateClient) {
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
- await this.closeMainClient();
218
- await this.closeAdditionalClient();
219
- await this.closeSubscribeClient();
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
- static default(name = "default") {
233
- RedisClient._default ??= {};
234
- if (!RedisClient._default[name]) {
235
- RedisClient._default[name] = new RedisClient(name);
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._default[name];
250
+ return RedisClient._create[name];
238
251
  }
239
252
 
240
253
  static async closeAllClients() {
241
- for (const entry of Object.values(RedisClient._default || {})) {
254
+ for (const entry of Object.values(RedisClient._create || {})) {
242
255
  await entry.closeClients();
243
256
  }
244
257
  }