@discover-cloud/shared 1.4.0 → 1.5.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.
@@ -34,7 +34,7 @@ class PermissionCacheService {
34
34
  */
35
35
  constructor() {
36
36
  this.client = (0, redis_1.createClient)({
37
- url: (0, env_util_1.getEnvOptional)('REDIS_URL') ?? 'redis://localhost:6379',
37
+ url: (0, env_util_1.getEnv)('REDIS_URL'),
38
38
  });
39
39
  this.client.on('error', (err) => {
40
40
  console.error('[PermissionCache] Redis error', err);
@@ -2,14 +2,24 @@ import { ILogger } from '../utils/logger.util';
2
2
  export declare class RabbitMQClient {
3
3
  private connection;
4
4
  private channel;
5
- private url;
6
- private exchangeName;
7
- private logger;
5
+ private readonly url;
6
+ private readonly exchangeName;
7
+ private readonly logger;
8
+ private reconnecting;
8
9
  constructor(url: string, logger?: ILogger);
10
+ /**
11
+ * Establishes a connection to RabbitMQ.
12
+ * Blocks until a connection is successfully established.
13
+ */
9
14
  connect(): Promise<void>;
15
+ /**
16
+ * Background reconnect loop.
17
+ */
18
+ private reconnect;
10
19
  publishEvent(routingKey: string, payload: unknown): Promise<void>;
11
20
  subscribe(queueName: string, routingKey: string, handler: (payload: unknown) => Promise<void>, options?: {
12
21
  prefetch?: number;
13
22
  }): Promise<void>;
14
23
  close(): Promise<void>;
24
+ private delay;
15
25
  }
@@ -41,35 +41,60 @@ class RabbitMQClient {
41
41
  this.connection = null;
42
42
  this.channel = null;
43
43
  this.exchangeName = 'discovercloud.topic';
44
+ this.reconnecting = false;
44
45
  this.url = url;
45
46
  this.logger = logger;
46
47
  }
48
+ /**
49
+ * Establishes a connection to RabbitMQ.
50
+ * Blocks until a connection is successfully established.
51
+ */
47
52
  async connect() {
48
53
  if (this.connection && this.channel) {
49
54
  return;
50
55
  }
51
- try {
52
- this.connection = await amqplib.connect(this.url);
53
- this.connection.on('error', (err) => {
54
- this.logger.error(err, 'RabbitMQ Connection Error');
55
- });
56
- this.connection.on('close', () => {
57
- this.logger.warn('RabbitMQ Connection Closed. Reconnecting...');
58
- this.connection = null;
59
- this.channel = null;
60
- setTimeout(() => this.connect(), 5000);
61
- });
62
- this.channel = await this.connection.createChannel();
63
- // Ensure the main topic exchange exists
64
- await this.channel.assertExchange(this.exchangeName, 'topic', {
65
- durable: true,
66
- });
67
- this.logger.info(' Connected to RabbitMQ');
56
+ while (!this.channel) {
57
+ try {
58
+ this.logger.info('Connecting to RabbitMQ...');
59
+ this.connection = await amqplib.connect(this.url);
60
+ this.connection.on('error', (err) => {
61
+ this.logger.error(err, 'RabbitMQ Connection Error');
62
+ });
63
+ this.connection.on('close', () => {
64
+ this.logger.warn('RabbitMQ Connection Closed');
65
+ this.connection = null;
66
+ this.channel = null;
67
+ if (!this.reconnecting) {
68
+ void this.reconnect();
69
+ }
70
+ });
71
+ this.channel = await this.connection.createChannel();
72
+ await this.channel.assertExchange(this.exchangeName, 'topic', {
73
+ durable: true,
74
+ });
75
+ this.logger.info('✅ RabbitMQ connected');
76
+ return;
77
+ }
78
+ catch (err) {
79
+ this.logger.error(err, 'RabbitMQ unavailable. Retrying in 5 seconds...');
80
+ await this.delay(5000);
81
+ }
68
82
  }
69
- catch (error) {
70
- this.logger.error(error, '❌ Failed to connect to RabbitMQ');
71
- setTimeout(() => this.connect(), 5000);
83
+ }
84
+ /**
85
+ * Background reconnect loop.
86
+ */
87
+ async reconnect() {
88
+ this.reconnecting = true;
89
+ while (!this.channel) {
90
+ try {
91
+ await this.connect();
92
+ }
93
+ catch {
94
+ await this.delay(5000);
95
+ }
72
96
  }
97
+ this.reconnecting = false;
73
98
  }
74
99
  async publishEvent(routingKey, payload) {
75
100
  if (!this.channel) {
@@ -89,45 +114,61 @@ class RabbitMQClient {
89
114
  if (options.prefetch) {
90
115
  await this.channel.prefetch(options.prefetch);
91
116
  }
92
- // Dead Letter configuration
93
117
  const dlxName = `${this.exchangeName}.dlx`;
94
118
  const dlqName = `${queueName}.dlq`;
95
- await this.channel.assertExchange(dlxName, 'topic', { durable: true });
96
- await this.channel.assertQueue(dlqName, { durable: true });
119
+ await this.channel.assertExchange(dlxName, 'topic', {
120
+ durable: true,
121
+ });
122
+ await this.channel.assertQueue(dlqName, {
123
+ durable: true,
124
+ });
97
125
  await this.channel.bindQueue(dlqName, dlxName, routingKey);
98
- // Assert main queue with DLX bindings
99
126
  await this.channel.assertQueue(queueName, {
100
127
  durable: true,
101
128
  deadLetterExchange: dlxName,
102
129
  deadLetterRoutingKey: routingKey,
103
130
  });
104
131
  await this.channel.bindQueue(queueName, this.exchangeName, routingKey);
105
- this.channel.consume(queueName, async (msg) => {
106
- if (!msg)
132
+ await this.channel.consume(queueName, async (msg) => {
133
+ if (!msg) {
107
134
  return;
135
+ }
108
136
  try {
109
137
  const payload = JSON.parse(msg.content.toString());
110
138
  await handler(payload);
111
- // Fix: Use optional chaining instead of non-null assertion (!)
112
139
  this.channel?.ack(msg);
113
140
  }
114
- catch (error) {
115
- this.logger.error(error, `Error processing message from ${queueName}`);
116
- // Fix: Use optional chaining instead of non-null assertion (!)
117
- // Nack the message and don't requeue, so it goes to the DLX
141
+ catch (err) {
142
+ this.logger.error(err, `Error processing ${queueName}`);
118
143
  this.channel?.nack(msg, false, false);
119
144
  }
120
145
  });
121
- this.logger.info(`📡 Subscribed to queue [${queueName}] with routing key [${routingKey}]`);
146
+ this.logger.info(`📡 Subscribed to ${queueName} (${routingKey})`);
122
147
  }
123
148
  async close() {
124
- if (this.channel) {
125
- await this.channel.close();
149
+ try {
150
+ this.reconnecting = true; // Prevents triggering reconnect during manual close
151
+ if (this.connection) {
152
+ this.connection.removeAllListeners('close');
153
+ }
154
+ if (this.channel) {
155
+ await this.channel.close();
156
+ }
157
+ if (this.connection) {
158
+ await this.connection.close();
159
+ }
126
160
  }
127
- if (this.connection) {
128
- await this.connection.close();
161
+ finally {
162
+ this.channel = null;
163
+ this.connection = null;
164
+ this.reconnecting = false;
129
165
  }
130
- this.logger.info('RabbitMQ connection closed.');
166
+ this.logger.info('RabbitMQ disconnected');
167
+ }
168
+ delay(ms) {
169
+ return new Promise((resolve) => {
170
+ setTimeout(resolve, ms);
171
+ });
131
172
  }
132
173
  }
133
174
  exports.RabbitMQClient = RabbitMQClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@discover-cloud/shared",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",