amqp-resilient 1.0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/dist/connection/ConnectionManager.d.ts +84 -0
  4. package/dist/connection/ConnectionManager.d.ts.map +1 -0
  5. package/dist/connection/ConnectionManager.js +312 -0
  6. package/dist/connection/ConnectionManager.js.map +1 -0
  7. package/dist/connection/index.d.ts +2 -0
  8. package/dist/connection/index.d.ts.map +1 -0
  9. package/dist/connection/index.js +2 -0
  10. package/dist/connection/index.js.map +1 -0
  11. package/dist/consumer/BaseConsumer.d.ts +131 -0
  12. package/dist/consumer/BaseConsumer.d.ts.map +1 -0
  13. package/dist/consumer/BaseConsumer.js +398 -0
  14. package/dist/consumer/BaseConsumer.js.map +1 -0
  15. package/dist/consumer/index.d.ts +2 -0
  16. package/dist/consumer/index.d.ts.map +1 -0
  17. package/dist/consumer/index.js +2 -0
  18. package/dist/consumer/index.js.map +1 -0
  19. package/dist/health/HealthService.d.ts +46 -0
  20. package/dist/health/HealthService.d.ts.map +1 -0
  21. package/dist/health/HealthService.js +85 -0
  22. package/dist/health/HealthService.js.map +1 -0
  23. package/dist/health/index.d.ts +2 -0
  24. package/dist/health/index.d.ts.map +1 -0
  25. package/dist/health/index.js +2 -0
  26. package/dist/health/index.js.map +1 -0
  27. package/dist/index.d.ts +11 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +17 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/patterns/CircuitBreaker.d.ts +76 -0
  32. package/dist/patterns/CircuitBreaker.d.ts.map +1 -0
  33. package/dist/patterns/CircuitBreaker.js +156 -0
  34. package/dist/patterns/CircuitBreaker.js.map +1 -0
  35. package/dist/patterns/index.d.ts +2 -0
  36. package/dist/patterns/index.d.ts.map +1 -0
  37. package/dist/patterns/index.js +2 -0
  38. package/dist/patterns/index.js.map +1 -0
  39. package/dist/publisher/BasePublisher.d.ts +87 -0
  40. package/dist/publisher/BasePublisher.d.ts.map +1 -0
  41. package/dist/publisher/BasePublisher.js +275 -0
  42. package/dist/publisher/BasePublisher.js.map +1 -0
  43. package/dist/publisher/index.d.ts +2 -0
  44. package/dist/publisher/index.d.ts.map +1 -0
  45. package/dist/publisher/index.js +2 -0
  46. package/dist/publisher/index.js.map +1 -0
  47. package/dist/types.d.ts +184 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +35 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +81 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 berkeerdo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # amqp-resilient
2
+
3
+ [![npm version](https://img.shields.io/npm/v/amqp-resilient.svg)](https://www.npmjs.com/package/amqp-resilient)
4
+ [![npm downloads](https://img.shields.io/npm/dm/amqp-resilient.svg)](https://www.npmjs.com/package/amqp-resilient)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
+ [![Node.js](https://img.shields.io/badge/Node.js-18+-green.svg)](https://nodejs.org/)
8
+
9
+ Production-ready AMQP client for Node.js with built-in resilience patterns.
10
+
11
+ ## Features
12
+
13
+ - **Auto-Reconnection** - Exponential backoff with jitter for stable reconnects
14
+ - **Circuit Breaker** - Prevent cascading failures with configurable thresholds
15
+ - **Retry with DLQ** - Failed messages retry with backoff, then route to Dead Letter Queue
16
+ - **Publisher Confirms** - Guaranteed message delivery with confirmation
17
+ - **Health Monitoring** - Built-in health checks for all connections
18
+ - **TypeScript** - Full type safety with generics support
19
+ - **ESM** - Native ES modules support
20
+ - **Zero Dependencies** - Only amqplib as peer dependency
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install amqp-resilient amqplib
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ### Connection
31
+
32
+ ```typescript
33
+ import { ConnectionManager } from 'amqp-resilient';
34
+
35
+ const connection = new ConnectionManager({
36
+ url: 'amqp://localhost:5672',
37
+ connectionName: 'my-service',
38
+ logger: console, // optional
39
+ });
40
+
41
+ await connection.connect();
42
+ ```
43
+
44
+ ### Consumer
45
+
46
+ ```typescript
47
+ import { BaseConsumer, type MessageContext } from 'amqp-resilient';
48
+
49
+ interface OrderMessage {
50
+ orderId: string;
51
+ amount: number;
52
+ }
53
+
54
+ class OrderConsumer extends BaseConsumer<OrderMessage> {
55
+ constructor(connection: ConnectionManager) {
56
+ super(connection, {
57
+ queue: 'orders',
58
+ exchange: 'orders.exchange',
59
+ routingKeys: ['order.created', 'order.updated'],
60
+ prefetch: 10,
61
+ maxRetries: 3,
62
+ });
63
+ }
64
+
65
+ protected async handle(message: OrderMessage, context: MessageContext): Promise<void> {
66
+ console.log(`Processing order ${message.orderId}`);
67
+ // Throw an error to trigger retry
68
+ }
69
+ }
70
+
71
+ const consumer = new OrderConsumer(connection);
72
+ await consumer.start();
73
+ ```
74
+
75
+ ### Publisher
76
+
77
+ ```typescript
78
+ import { BasePublisher } from 'amqp-resilient';
79
+
80
+ const publisher = new BasePublisher(connection, {
81
+ exchange: 'orders.exchange',
82
+ confirm: true,
83
+ });
84
+
85
+ const result = await publisher.publish('order.created', {
86
+ orderId: '123',
87
+ amount: 99.99,
88
+ });
89
+
90
+ if (result.success) {
91
+ console.log(`Published message ${result.messageId}`);
92
+ }
93
+
94
+ // Or throw on failure
95
+ await publisher.publishOrThrow('order.created', { orderId: '456' });
96
+ ```
97
+
98
+ ## Configuration
99
+
100
+ ### ConnectionManager
101
+
102
+ | Option | Type | Default | Description |
103
+ |--------|------|---------|-------------|
104
+ | `url` | string | - | Full AMQP URL (amqp://host:port) |
105
+ | `host` | string | - | RabbitMQ host (alternative to url) |
106
+ | `port` | number | 5672 | RabbitMQ port |
107
+ | `username` | string | guest | Username |
108
+ | `password` | string | guest | Password |
109
+ | `vhost` | string | / | Virtual host |
110
+ | `connectionName` | string | **required** | Name for logging and health checks |
111
+ | `prefetch` | number | 10 | Default channel prefetch count |
112
+ | `heartbeat` | number | 60 | Heartbeat interval in seconds |
113
+ | `maxReconnectAttempts` | number | 0 | Max reconnect attempts (0 = unlimited) |
114
+ | `initialReconnectDelay` | number | 1000 | Initial reconnect delay (ms) |
115
+ | `maxReconnectDelay` | number | 30000 | Max reconnect delay (ms) |
116
+ | `logger` | AmqpLogger | noop | Logger instance |
117
+
118
+ ### BaseConsumer
119
+
120
+ | Option | Type | Default | Description |
121
+ |--------|------|---------|-------------|
122
+ | `queue` | string | **required** | Queue name |
123
+ | `exchange` | string | **required** | Exchange to bind |
124
+ | `routingKeys` | string[] | **required** | Routing keys to bind |
125
+ | `prefetch` | number | 10 | Consumer prefetch count |
126
+ | `maxRetries` | number | 3 | Max retries before DLQ |
127
+ | `initialRetryDelay` | number | 1000 | Initial retry delay (ms) |
128
+ | `maxRetryDelay` | number | 30000 | Max retry delay (ms) |
129
+ | `useCircuitBreaker` | boolean | true | Enable circuit breaker |
130
+ | `exchangeType` | ExchangeType | topic | Exchange type |
131
+
132
+ ### BasePublisher
133
+
134
+ | Option | Type | Default | Description |
135
+ |--------|------|---------|-------------|
136
+ | `exchange` | string | **required** | Exchange name |
137
+ | `exchangeType` | ExchangeType | topic | Exchange type |
138
+ | `confirm` | boolean | true | Use publisher confirms |
139
+ | `maxRetries` | number | 3 | Max publish retries |
140
+ | `initialRetryDelay` | number | 100 | Initial retry delay (ms) |
141
+ | `maxRetryDelay` | number | 5000 | Max retry delay (ms) |
142
+ | `useCircuitBreaker` | boolean | true | Enable circuit breaker |
143
+
144
+ ## Health & Metrics
145
+
146
+ ```typescript
147
+ import { HealthService, ConnectionStatus } from 'amqp-resilient';
148
+
149
+ // Get overall health status
150
+ const status = HealthService.getOverallStatus();
151
+ // 'healthy' | 'degraded' | 'dead' | 'not_configured'
152
+
153
+ // Get specific connection status
154
+ const connStatus = HealthService.getStatus('my-connection');
155
+ // ConnectionStatus.CONNECTED | CONNECTING | DISCONNECTED | RECONNECTING | CLOSED
156
+
157
+ // Get all connection statuses
158
+ const allStatuses = HealthService.getAllStatuses();
159
+ // { 'my-connection': 'connected', 'other-connection': 'reconnecting' }
160
+
161
+ // Get connection stats
162
+ const stats = connection.getStats();
163
+ // {
164
+ // connected: true,
165
+ // reconnectAttempts: 0,
166
+ // channelCount: 2,
167
+ // lastConnectedAt: Date,
168
+ // }
169
+ ```
170
+
171
+ ## Events
172
+
173
+ ```typescript
174
+ connection.on('connected', () => {
175
+ console.log('AMQP connected');
176
+ });
177
+
178
+ connection.on('disconnected', () => {
179
+ console.log('AMQP disconnected');
180
+ });
181
+
182
+ connection.on('reconnecting', (attempt) => {
183
+ console.log(`Reconnecting attempt ${attempt}`);
184
+ });
185
+
186
+ connection.on('error', (error) => {
187
+ console.error('Connection error:', error);
188
+ });
189
+ ```
190
+
191
+ ## Circuit Breaker
192
+
193
+ Standalone circuit breaker that can be used independently:
194
+
195
+ ```typescript
196
+ import { CircuitBreaker, CircuitBreakerOpenError } from 'amqp-resilient';
197
+
198
+ const breaker = new CircuitBreaker({
199
+ name: 'external-api',
200
+ failureThreshold: 5, // Open after 5 failures
201
+ resetTimeout: 30000, // Try half-open after 30s
202
+ successThreshold: 3, // Close after 3 successes in half-open
203
+ });
204
+
205
+ try {
206
+ const result = await breaker.execute(async () => {
207
+ return await externalApiCall();
208
+ });
209
+ } catch (error) {
210
+ if (error instanceof CircuitBreakerOpenError) {
211
+ console.log(`Circuit open, retry in ${error.remainingResetTime}ms`);
212
+ }
213
+ }
214
+
215
+ // Get circuit state
216
+ const state = breaker.getState();
217
+ // 'CLOSED' | 'OPEN' | 'HALF_OPEN'
218
+ ```
219
+
220
+ ## Logger Interface
221
+
222
+ Any logger that implements this interface works:
223
+
224
+ ```typescript
225
+ interface AmqpLogger {
226
+ info(obj: object, msg?: string): void;
227
+ warn(obj: object, msg?: string): void;
228
+ error(obj: object, msg?: string): void;
229
+ debug(obj: object, msg?: string): void;
230
+ }
231
+
232
+ // Examples: pino, winston, bunyan, console
233
+ ```
234
+
235
+ ## Dead Letter Queue (DLQ)
236
+
237
+ Messages that fail after max retries are automatically sent to a DLQ:
238
+
239
+ ```typescript
240
+ // Queue: orders
241
+ // DLQ: orders.dlq (auto-created)
242
+
243
+ // Message headers in DLQ:
244
+ // x-death-reason: 'max-retries-exceeded'
245
+ // x-death-count: 3
246
+ // x-original-routing-key: 'order.created'
247
+ // x-first-death-queue: 'orders'
248
+ ```
249
+
250
+ ## Requirements
251
+
252
+ - Node.js >= 18.0.0
253
+ - amqplib >= 0.10.0
254
+
255
+ ## License
256
+
257
+ MIT © [berkeerdo](https://github.com/berkeerdo)
@@ -0,0 +1,84 @@
1
+ import type { Channel, ConfirmChannel } from 'amqplib';
2
+ import { type AmqpLogger, type ConnectionOptions } from '../types.js';
3
+ /**
4
+ * ConnectionManager - Manages AMQP connection with auto-reconnect
5
+ * Use one instance per logical connection purpose
6
+ */
7
+ export declare class ConnectionManager {
8
+ private connection;
9
+ private sharedChannel;
10
+ private confirmChannel;
11
+ private reconnectAttempts;
12
+ private isShuttingDown;
13
+ private reconnectTimer;
14
+ private readonly createdChannels;
15
+ private readonly logger;
16
+ private readonly connectionUrl;
17
+ private readonly connectionName;
18
+ private readonly prefetch;
19
+ private readonly heartbeat;
20
+ private readonly maxReconnectAttempts;
21
+ private readonly initialReconnectDelay;
22
+ private readonly maxReconnectDelay;
23
+ constructor(options: ConnectionOptions);
24
+ /**
25
+ * Setup connection event handlers
26
+ */
27
+ private setupConnectionHandlers;
28
+ /**
29
+ * Reset connection state on disconnect
30
+ */
31
+ private resetConnectionState;
32
+ /**
33
+ * Connect to AMQP server
34
+ */
35
+ connect(): Promise<void>;
36
+ /**
37
+ * Create a managed channel with error handlers
38
+ */
39
+ private createManagedChannel;
40
+ /**
41
+ * Schedule reconnection with exponential backoff and jitter
42
+ */
43
+ private scheduleReconnect;
44
+ /**
45
+ * Get the shared channel (for simple operations)
46
+ * Note: For consumers, use createChannel() instead
47
+ */
48
+ getChannel(): Promise<Channel>;
49
+ /**
50
+ * Create a dedicated channel for a consumer
51
+ * Best Practice: Each consumer should have its own channel
52
+ */
53
+ createChannel(): Promise<Channel>;
54
+ /**
55
+ * Get confirm channel for guaranteed delivery
56
+ */
57
+ getConfirmChannel(): Promise<ConfirmChannel>;
58
+ /**
59
+ * Close connection gracefully
60
+ */
61
+ close(): Promise<void>;
62
+ /**
63
+ * Check if connected
64
+ */
65
+ isConnected(): boolean;
66
+ /**
67
+ * Get connection name
68
+ */
69
+ getConnectionName(): string;
70
+ /**
71
+ * Get logger instance (for consumers/publishers to inherit)
72
+ */
73
+ getLogger(): AmqpLogger;
74
+ /**
75
+ * Get connection stats
76
+ */
77
+ getStats(): {
78
+ connected: boolean;
79
+ reconnectAttempts: number;
80
+ channelCount: number;
81
+ hasConfirmChannel: boolean;
82
+ };
83
+ }
84
+ //# sourceMappingURL=ConnectionManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConnectionManager.d.ts","sourceRoot":"","sources":["../../src/connection/ConnectionManager.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACvD,OAAO,EAAgC,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAepG;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,UAAU,CAA+B;IACjD,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsB;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;IAEpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAE/B,OAAO,EAAE,iBAAiB;IAwBtC;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAkC/B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAO5B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B9B;;OAEG;YACW,oBAAoB;IAwBlC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA2CzB;;;OAGG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAUpC;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC;IAiBvC;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,cAAc,CAAC;IA8BlD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0D5B;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,iBAAiB,IAAI,MAAM;IAI3B;;OAEG;IACH,SAAS,IAAI,UAAU;IAIvB;;OAEG;IACH,QAAQ,IAAI;QACV,SAAS,EAAE,OAAO,CAAC;QACnB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,YAAY,EAAE,MAAM,CAAC;QACrB,iBAAiB,EAAE,OAAO,CAAC;KAC5B;CAQF"}
@@ -0,0 +1,312 @@
1
+ /**
2
+ * AMQP Connection Manager
3
+ * Production-ready implementation with:
4
+ * - Automatic reconnection with exponential backoff and jitter
5
+ * - Connection heartbeat for health monitoring
6
+ * - Dedicated channels for consumers (one channel per consumer)
7
+ * - Confirm channels for publishers (guaranteed delivery)
8
+ * - Graceful shutdown handling
9
+ *
10
+ * Best Practices:
11
+ * 1. Use separate channels for publishing and consuming
12
+ * 2. Never share channels between consumers
13
+ * 3. Use confirm channels for critical messages
14
+ * 4. Implement proper error handling and reconnection
15
+ * 5. Use heartbeats to detect dead connections
16
+ */
17
+ import amqplib from 'amqplib';
18
+ import { ConnectionStatus, noopLogger } from '../types.js';
19
+ import { HealthService } from '../health/HealthService.js';
20
+ /** Default configuration values */
21
+ const DEFAULTS = {
22
+ INITIAL_RECONNECT_DELAY: 1000,
23
+ MAX_RECONNECT_DELAY: 60000,
24
+ MAX_RECONNECT_ATTEMPTS: 0, // unlimited
25
+ HEARTBEAT_SECONDS: 60,
26
+ PREFETCH: 10,
27
+ };
28
+ /**
29
+ * ConnectionManager - Manages AMQP connection with auto-reconnect
30
+ * Use one instance per logical connection purpose
31
+ */
32
+ export class ConnectionManager {
33
+ connection = null;
34
+ sharedChannel = null;
35
+ confirmChannel = null;
36
+ reconnectAttempts = 0;
37
+ isShuttingDown = false;
38
+ reconnectTimer = null;
39
+ createdChannels = new Set();
40
+ logger;
41
+ connectionUrl;
42
+ connectionName;
43
+ prefetch;
44
+ heartbeat;
45
+ maxReconnectAttempts;
46
+ initialReconnectDelay;
47
+ maxReconnectDelay;
48
+ constructor(options) {
49
+ this.connectionName = options.connectionName ?? 'default';
50
+ this.prefetch = options.prefetch ?? DEFAULTS.PREFETCH;
51
+ this.heartbeat = options.heartbeat ?? DEFAULTS.HEARTBEAT_SECONDS;
52
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? DEFAULTS.MAX_RECONNECT_ATTEMPTS;
53
+ this.initialReconnectDelay = options.initialReconnectDelay ?? DEFAULTS.INITIAL_RECONNECT_DELAY;
54
+ this.maxReconnectDelay = options.maxReconnectDelay ?? DEFAULTS.MAX_RECONNECT_DELAY;
55
+ this.logger = options.logger ?? noopLogger;
56
+ // Build connection URL from individual params or use provided URL
57
+ if (options.url) {
58
+ this.connectionUrl = options.url;
59
+ }
60
+ else if (options.host) {
61
+ const username = encodeURIComponent(options.username ?? 'guest');
62
+ const password = encodeURIComponent(options.password ?? 'guest');
63
+ const host = options.host;
64
+ const port = options.port ?? 5672;
65
+ const vhost = encodeURIComponent(options.vhost ?? '/');
66
+ this.connectionUrl = `amqp://${username}:${password}@${host}:${port}/${vhost}`;
67
+ }
68
+ else {
69
+ throw new Error('ConnectionManager requires either url or host parameter');
70
+ }
71
+ }
72
+ /**
73
+ * Setup connection event handlers
74
+ */
75
+ setupConnectionHandlers() {
76
+ if (!this.connection) {
77
+ return;
78
+ }
79
+ this.connection.on('error', (err) => {
80
+ this.logger.error({ err, connectionName: this.connectionName }, 'AMQP connection error');
81
+ });
82
+ this.connection.on('close', () => {
83
+ if (this.isShuttingDown) {
84
+ return;
85
+ }
86
+ this.logger.warn({ connectionName: this.connectionName }, 'AMQP connection closed unexpectedly, scheduling reconnect...');
87
+ this.resetConnectionState();
88
+ HealthService.registerStatus(this.connectionName, ConnectionStatus.RECONNECTING);
89
+ this.scheduleReconnect();
90
+ });
91
+ this.connection.on('blocked', (reason) => {
92
+ this.logger.warn({ connectionName: this.connectionName, reason }, 'AMQP connection blocked by broker');
93
+ });
94
+ this.connection.on('unblocked', () => {
95
+ this.logger.info({ connectionName: this.connectionName }, 'AMQP connection unblocked');
96
+ });
97
+ }
98
+ /**
99
+ * Reset connection state on disconnect
100
+ */
101
+ resetConnectionState() {
102
+ this.connection = null;
103
+ this.sharedChannel = null;
104
+ this.confirmChannel = null;
105
+ this.createdChannels.clear();
106
+ }
107
+ /**
108
+ * Connect to AMQP server
109
+ */
110
+ async connect() {
111
+ if (this.connection) {
112
+ return;
113
+ }
114
+ HealthService.registerStatus(this.connectionName, ConnectionStatus.CONNECTING);
115
+ try {
116
+ this.logger.info({ connectionName: this.connectionName }, 'Connecting to AMQP server...');
117
+ this.connection = await amqplib.connect(this.connectionUrl, { heartbeat: this.heartbeat });
118
+ this.reconnectAttempts = 0;
119
+ this.setupConnectionHandlers();
120
+ this.sharedChannel = await this.createManagedChannel();
121
+ HealthService.registerStatus(this.connectionName, ConnectionStatus.CONNECTED);
122
+ this.logger.info({ connectionName: this.connectionName }, 'AMQP connected successfully');
123
+ }
124
+ catch (error) {
125
+ this.logger.error({ err: error, connectionName: this.connectionName }, 'Failed to connect to AMQP server');
126
+ HealthService.registerStatus(this.connectionName, ConnectionStatus.DISCONNECTED);
127
+ this.scheduleReconnect();
128
+ throw error;
129
+ }
130
+ }
131
+ /**
132
+ * Create a managed channel with error handlers
133
+ */
134
+ async createManagedChannel() {
135
+ if (!this.connection) {
136
+ throw new Error('Not connected to AMQP server');
137
+ }
138
+ const channel = await this.connection.createChannel();
139
+ await channel.prefetch(this.prefetch);
140
+ channel.on('error', (err) => {
141
+ this.logger.error({ err, connectionName: this.connectionName }, 'AMQP channel error');
142
+ this.createdChannels.delete(channel);
143
+ });
144
+ channel.on('close', () => {
145
+ if (!this.isShuttingDown) {
146
+ this.logger.warn({ connectionName: this.connectionName }, 'AMQP channel closed');
147
+ }
148
+ this.createdChannels.delete(channel);
149
+ });
150
+ this.createdChannels.add(channel);
151
+ return channel;
152
+ }
153
+ /**
154
+ * Schedule reconnection with exponential backoff and jitter
155
+ */
156
+ scheduleReconnect() {
157
+ if (this.isShuttingDown || this.reconnectTimer) {
158
+ return;
159
+ }
160
+ this.reconnectAttempts++;
161
+ // Check max attempts (0 = unlimited)
162
+ if (this.maxReconnectAttempts > 0 && this.reconnectAttempts > this.maxReconnectAttempts) {
163
+ this.logger.error({ connectionName: this.connectionName, attempts: this.reconnectAttempts }, 'Max reconnection attempts reached, marking connection as dead');
164
+ HealthService.registerStatus(this.connectionName, ConnectionStatus.DEAD);
165
+ return;
166
+ }
167
+ // Calculate delay with exponential backoff and jitter
168
+ const exponentialDelay = Math.min(this.initialReconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
169
+ // Add jitter (0-25% of delay) to prevent thundering herd
170
+ const jitter = Math.random() * exponentialDelay * 0.25;
171
+ const delay = Math.floor(exponentialDelay + jitter);
172
+ this.logger.info({
173
+ connectionName: this.connectionName,
174
+ attempt: this.reconnectAttempts,
175
+ delayMs: delay,
176
+ }, 'Scheduling AMQP reconnection...');
177
+ this.reconnectTimer = setTimeout(() => {
178
+ this.reconnectTimer = null;
179
+ this.connect().catch(() => {
180
+ // Error already logged in connect()
181
+ });
182
+ }, delay);
183
+ }
184
+ /**
185
+ * Get the shared channel (for simple operations)
186
+ * Note: For consumers, use createChannel() instead
187
+ */
188
+ async getChannel() {
189
+ if (!this.sharedChannel) {
190
+ await this.connect();
191
+ }
192
+ if (!this.sharedChannel) {
193
+ throw new Error('Failed to get AMQP channel');
194
+ }
195
+ return this.sharedChannel;
196
+ }
197
+ /**
198
+ * Create a dedicated channel for a consumer
199
+ * Best Practice: Each consumer should have its own channel
200
+ */
201
+ async createChannel() {
202
+ if (!this.connection) {
203
+ await this.connect();
204
+ }
205
+ if (!this.connection) {
206
+ throw new Error('Failed to get AMQP connection');
207
+ }
208
+ const channel = await this.createManagedChannel();
209
+ this.logger.debug({ connectionName: this.connectionName, channelCount: this.createdChannels.size }, 'Created dedicated channel');
210
+ return channel;
211
+ }
212
+ /**
213
+ * Get confirm channel for guaranteed delivery
214
+ */
215
+ async getConfirmChannel() {
216
+ if (!this.confirmChannel) {
217
+ if (!this.connection) {
218
+ await this.connect();
219
+ }
220
+ if (!this.connection) {
221
+ throw new Error('Failed to get AMQP connection');
222
+ }
223
+ this.confirmChannel = await this.connection.createConfirmChannel();
224
+ await this.confirmChannel.prefetch(this.prefetch);
225
+ this.confirmChannel.on('error', (err) => {
226
+ this.logger.error({ err, connectionName: this.connectionName }, 'AMQP confirm channel error');
227
+ this.confirmChannel = null;
228
+ });
229
+ this.confirmChannel.on('close', () => {
230
+ if (!this.isShuttingDown) {
231
+ this.logger.warn({ connectionName: this.connectionName }, 'AMQP confirm channel closed');
232
+ }
233
+ this.confirmChannel = null;
234
+ });
235
+ this.logger.debug({ connectionName: this.connectionName }, 'Created confirm channel');
236
+ }
237
+ return this.confirmChannel;
238
+ }
239
+ /**
240
+ * Close connection gracefully
241
+ */
242
+ async close() {
243
+ this.isShuttingDown = true;
244
+ // Cancel reconnection timer
245
+ if (this.reconnectTimer) {
246
+ clearTimeout(this.reconnectTimer);
247
+ this.reconnectTimer = null;
248
+ }
249
+ try {
250
+ // Close all created channels
251
+ const closePromises = [];
252
+ for (const channel of this.createdChannels) {
253
+ closePromises.push(channel.close().catch((err) => {
254
+ this.logger.debug({ err }, 'Error closing channel');
255
+ }));
256
+ }
257
+ if (this.sharedChannel && !this.createdChannels.has(this.sharedChannel)) {
258
+ closePromises.push(this.sharedChannel.close().catch((err) => {
259
+ this.logger.debug({ err }, 'Error closing shared channel');
260
+ }));
261
+ }
262
+ if (this.confirmChannel) {
263
+ closePromises.push(this.confirmChannel.close().catch((err) => {
264
+ this.logger.debug({ err }, 'Error closing confirm channel');
265
+ }));
266
+ }
267
+ await Promise.all(closePromises);
268
+ if (this.connection) {
269
+ await this.connection.close();
270
+ }
271
+ this.connection = null;
272
+ this.sharedChannel = null;
273
+ this.confirmChannel = null;
274
+ this.createdChannels.clear();
275
+ HealthService.unregisterConnection(this.connectionName);
276
+ this.logger.info({ connectionName: this.connectionName }, 'AMQP connection closed gracefully');
277
+ }
278
+ catch (error) {
279
+ this.logger.error({ err: error, connectionName: this.connectionName }, 'Error closing AMQP connection');
280
+ }
281
+ }
282
+ /**
283
+ * Check if connected
284
+ */
285
+ isConnected() {
286
+ return this.connection !== null;
287
+ }
288
+ /**
289
+ * Get connection name
290
+ */
291
+ getConnectionName() {
292
+ return this.connectionName;
293
+ }
294
+ /**
295
+ * Get logger instance (for consumers/publishers to inherit)
296
+ */
297
+ getLogger() {
298
+ return this.logger;
299
+ }
300
+ /**
301
+ * Get connection stats
302
+ */
303
+ getStats() {
304
+ return {
305
+ connected: this.isConnected(),
306
+ reconnectAttempts: this.reconnectAttempts,
307
+ channelCount: this.createdChannels.size,
308
+ hasConfirmChannel: this.confirmChannel !== null,
309
+ };
310
+ }
311
+ }
312
+ //# sourceMappingURL=ConnectionManager.js.map