@onlineapps/service-common 1.0.1 → 1.0.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/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@onlineapps/service-common",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Common utilities for both infrastructure services and business services",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
+ "prepublishOnly": "../../scripts/pre-publish-compatibility-check.sh",
8
+ "postpublish": "../../scripts/update-manifest-from-npm.sh && ../../scripts/update-all-services.sh",
7
9
  "test": "jest",
8
10
  "test:unit": "jest tests/unit",
9
11
  "test:integration": "jest tests/integration"
@@ -27,4 +29,3 @@
27
29
  "node": ">=14.0.0"
28
30
  }
29
31
  }
30
-
package/src/index.js CHANGED
@@ -2,22 +2,34 @@
2
2
 
3
3
  /**
4
4
  * @onlineapps/service-common
5
- *
5
+ *
6
6
  * Common utilities for both infrastructure services and business services.
7
7
  * Provides shared functionality that is used across the entire system.
8
- *
8
+ *
9
9
  * This library is for ALL services (infrastructure and business).
10
10
  */
11
11
 
12
12
  const { waitForInfrastructureReady } = require('./infrastructure/waitForInfrastructureReady');
13
13
  const { waitForHealthCheckQueueReady } = require('./infrastructure/waitForHealthCheckQueueReady');
14
14
  const { sendMonitoringFailFallbackEmail } = require('./reporting/monitoringFallbackEmail');
15
+ const {
16
+ buildRedisUrl,
17
+ createRedisClient,
18
+ connectRedis
19
+ } = require('./redisClient');
15
20
 
16
21
  module.exports = {
17
22
  // Infrastructure readiness utilities (used by both infrastructure and business services)
18
23
  waitForInfrastructureReady,
19
24
  waitForHealthCheckQueueReady,
25
+
26
+ // Redis utilities (shared between services and tests)
27
+ buildRedisUrl,
28
+ createRedisClient,
29
+ connectRedis,
30
+
20
31
  // Reporting utilities
21
32
  sendMonitoringFailFallbackEmail
22
33
  };
23
34
 
35
+
@@ -72,23 +72,50 @@ async function waitForInfrastructureReady(options = {}) {
72
72
  log(`[InfrastructureReady] Max wait: ${maxWait}ms, Check interval: ${checkInterval}ms`);
73
73
 
74
74
  try {
75
- // Connect to Redis
75
+ // Connect to Redis with timeout
76
76
  const { createClient } = require('redis');
77
- redis = createClient({ url: redisUrl });
77
+ redis = createClient({
78
+ url: redisUrl,
79
+ socket: {
80
+ connectTimeout: 5000, // 5 seconds timeout for connection
81
+ reconnectStrategy: false // Don't auto-reconnect - fail fast
82
+ }
83
+ });
78
84
 
79
85
  redis.on('error', (err) => {
80
86
  log(`[InfrastructureReady] Redis error: ${err.message}`);
81
87
  });
82
88
 
83
- await redis.connect();
84
- log('[InfrastructureReady] Connected to Redis');
89
+ // Add timeout for Redis connection
90
+ const CONNECT_TIMEOUT = 10000; // 10 seconds
91
+ const connectPromise = redis.connect();
92
+ const connectTimeoutPromise = new Promise((_, reject) => {
93
+ setTimeout(() => {
94
+ reject(new Error(`Redis connection timeout after ${CONNECT_TIMEOUT}ms. Redis may be unavailable at ${redisUrl}`));
95
+ }, CONNECT_TIMEOUT);
96
+ });
97
+
98
+ try {
99
+ await Promise.race([connectPromise, connectTimeoutPromise]);
100
+ log('[InfrastructureReady] Connected to Redis');
101
+ } catch (connectError) {
102
+ throw new Error(`[InfrastructureReady] Failed to connect to Redis: ${connectError.message}`);
103
+ }
85
104
 
86
105
  while (Date.now() - startTime < maxWait) {
87
106
  attemptCount++;
88
107
 
89
108
  try {
90
- // Check Redis key: infrastructure:health:all
91
- const allHealthy = await redis.get('infrastructure:health:all');
109
+ // Check Redis key: infrastructure:health:all with timeout to prevent hanging
110
+ const REDIS_OPERATION_TIMEOUT = 5000; // 5 seconds per operation
111
+ const getPromise = redis.get('infrastructure:health:all');
112
+ const getTimeoutPromise = new Promise((_, reject) => {
113
+ setTimeout(() => {
114
+ reject(new Error(`Redis get() operation timeout after ${REDIS_OPERATION_TIMEOUT}ms`));
115
+ }, REDIS_OPERATION_TIMEOUT);
116
+ });
117
+
118
+ const allHealthy = await Promise.race([getPromise, getTimeoutPromise]);
92
119
 
93
120
  if (allHealthy === 'true') {
94
121
  // All services are UP, we can proceed
@@ -100,9 +127,18 @@ async function waitForInfrastructureReady(options = {}) {
100
127
  const serviceKeys = ['gateway', 'registry', 'validator', 'delivery', 'monitoring'];
101
128
  const statuses = {};
102
129
  for (const serviceName of serviceKeys) {
103
- const status = await redis.get(`infrastructure:health:${serviceName}`);
104
- if (status) {
105
- statuses[serviceName] = JSON.parse(status);
130
+ try {
131
+ const statusPromise = redis.get(`infrastructure:health:${serviceName}`);
132
+ const statusTimeoutPromise = new Promise((_, reject) => {
133
+ setTimeout(() => reject(new Error('timeout')), REDIS_OPERATION_TIMEOUT);
134
+ });
135
+ const status = await Promise.race([statusPromise, statusTimeoutPromise]);
136
+ if (status) {
137
+ statuses[serviceName] = JSON.parse(status);
138
+ }
139
+ } catch (statusError) {
140
+ // Ignore individual service status errors
141
+ log(`[InfrastructureReady] Could not get status for ${serviceName}: ${statusError.message}`);
106
142
  }
107
143
  }
108
144
  if (Object.keys(statuses).length > 0) {
@@ -115,7 +151,13 @@ async function waitForInfrastructureReady(options = {}) {
115
151
  log(`[InfrastructureReady] Attempt ${attemptCount}: Not all services ready (current status: ${allHealthy || 'unknown'}). Waiting ${checkInterval}ms...`);
116
152
 
117
153
  } catch (error) {
118
- log(`[InfrastructureReady] Attempt ${attemptCount}: Failed to get infrastructure health from Redis (${error.message}). Waiting ${checkInterval}ms...`);
154
+ const errorMsg = error.message || String(error);
155
+ log(`[InfrastructureReady] Attempt ${attemptCount}: Failed to get infrastructure health from Redis (${errorMsg}). Waiting ${checkInterval}ms...`);
156
+
157
+ // If Redis connection is lost, throw error immediately instead of retrying
158
+ if (errorMsg.includes('timeout') || errorMsg.includes('ECONNREFUSED') || errorMsg.includes('ENOTFOUND')) {
159
+ throw new Error(`[InfrastructureReady] Redis connection lost: ${errorMsg}. Cannot continue waiting for infrastructure ready.`);
160
+ }
119
161
  }
120
162
 
121
163
  await new Promise(resolve => setTimeout(resolve, checkInterval));
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared Redis client utilities for services and tests.
5
+ *
6
+ * Responsibility:
7
+ * - Build Redis connection URL from environment in a consistent way
8
+ * - Create a configured redis client with unified error logging
9
+ * - (Optionally) connect with a hard timeout for tests / tools
10
+ *
11
+ * This module is intentionally small and framework-agnostic so it can be used
12
+ * both in infrastructure services, business services and in test utilities.
13
+ */
14
+
15
+ const { createClient } = require('redis');
16
+
17
+ /**
18
+ * Build a Redis connection URL from environment.
19
+ *
20
+ * Precedence:
21
+ * 1. REDIS_URL (full URL, e.g. redis://host:port/db)
22
+ * 2. REDIS_HOST + REDIS_PORT
23
+ * 3. Defaults (host/port) or localhost:6379
24
+ */
25
+ function buildRedisUrl(options = {}) {
26
+ const env = options.env || process.env || {};
27
+ const defaults = options.defaults || {};
28
+
29
+ if (env.REDIS_URL && typeof env.REDIS_URL === 'string' && env.REDIS_URL.length > 0) {
30
+ return env.REDIS_URL;
31
+ }
32
+
33
+ const host = env.REDIS_HOST || defaults.host || 'localhost';
34
+ const port = env.REDIS_PORT || defaults.port || '6379';
35
+
36
+ return `redis://${host}:${port}`;
37
+ }
38
+
39
+ /**
40
+ * Create a Redis client with unified error logging and optional test settings.
41
+ *
42
+ * @param {Object} options
43
+ * @param {string} [options.purpose] - Logical purpose (for log context)
44
+ * @param {boolean} [options.forTests=false] - If true, disables automatic reconnect loops
45
+ * @param {Object} [options.logger=console] - Logger with .error/.info methods
46
+ * @param {Object} [options.env=process.env] - Optional env override (for tests)
47
+ * @param {Object} [options.defaults] - Optional default host/port overrides
48
+ * @returns {{ client: import('redis').RedisClientType, url: string }}
49
+ */
50
+ function createRedisClient(options = {}) {
51
+ const {
52
+ purpose = 'default',
53
+ forTests = false,
54
+ logger = console,
55
+ env,
56
+ defaults
57
+ } = options;
58
+
59
+ const url = buildRedisUrl({ env, defaults });
60
+
61
+ const socketOptions = forTests
62
+ ? {
63
+ // In tests we typically want fail-fast behaviour, not infinite reconnect loops
64
+ reconnectStrategy: false
65
+ }
66
+ : {};
67
+
68
+ const client = createClient({
69
+ url,
70
+ socket: socketOptions
71
+ });
72
+
73
+ const log = logger && typeof logger.error === 'function' ? logger : console;
74
+
75
+ client.on('error', (err) => {
76
+ try {
77
+ log.error('[Redis] Error', {
78
+ purpose,
79
+ url,
80
+ message: err && err.message ? err.message : String(err)
81
+ });
82
+ } catch {
83
+ // Last-resort fallback to avoid throwing in error handler
84
+ // eslint-disable-next-line no-console
85
+ console.error('[Redis] Error', err);
86
+ }
87
+ });
88
+
89
+ return { client, url };
90
+ }
91
+
92
+ /**
93
+ * Create and connect a Redis client with a hard timeout (useful for tests/tools).
94
+ *
95
+ * @param {Object} options - Same as createRedisClient plus:
96
+ * @param {number} [options.timeoutMs=15000] - Max time to wait for connect()
97
+ * @returns {Promise<import('redis').RedisClientType>}
98
+ */
99
+ async function connectRedis(options = {}) {
100
+ const { timeoutMs = 15000, logger = console } = options;
101
+ const { client, url } = createRedisClient(options);
102
+
103
+ if (!timeoutMs || timeoutMs <= 0) {
104
+ await client.connect();
105
+ if (logger && typeof logger.info === 'function') {
106
+ logger.info('[Redis] Connected', { url });
107
+ }
108
+ return client;
109
+ }
110
+
111
+ const connectPromise = client.connect();
112
+ const timeoutPromise = new Promise((_, reject) => {
113
+ setTimeout(
114
+ () => reject(new Error(`Redis connection timeout after ${timeoutMs}ms to ${url}`)),
115
+ timeoutMs
116
+ );
117
+ });
118
+
119
+ await Promise.race([connectPromise, timeoutPromise]);
120
+
121
+ if (logger && typeof logger.info === 'function') {
122
+ logger.info('[Redis] Connected', { url });
123
+ }
124
+
125
+ return client;
126
+ }
127
+
128
+ module.exports = {
129
+ buildRedisUrl,
130
+ createRedisClient,
131
+ connectRedis
132
+ };
133
+
134
+