@a_jackie_z/event-bus 1.0.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/LICENSE.txt +21 -0
- package/README.md +1011 -0
- package/dist/chunk-5DMUVWFR.js +151 -0
- package/dist/chunk-5DMUVWFR.js.map +1 -0
- package/dist/chunk-JBE5KWYZ.js +131 -0
- package/dist/chunk-JBE5KWYZ.js.map +1 -0
- package/dist/chunk-YP7YVSEN.js +88 -0
- package/dist/chunk-YP7YVSEN.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tests/broadcast-consumer.d.ts +2 -0
- package/dist/lib/tests/broadcast-consumer.js +112 -0
- package/dist/lib/tests/broadcast-consumer.js.map +1 -0
- package/dist/lib/tests/broadcast-producer.d.ts +2 -0
- package/dist/lib/tests/broadcast-producer.js +57 -0
- package/dist/lib/tests/broadcast-producer.js.map +1 -0
- package/dist/lib/tests/consumer.d.ts +2 -0
- package/dist/lib/tests/consumer.js +62 -0
- package/dist/lib/tests/consumer.js.map +1 -0
- package/dist/lib/tests/mixed-consumer.d.ts +2 -0
- package/dist/lib/tests/mixed-consumer.js +117 -0
- package/dist/lib/tests/mixed-consumer.js.map +1 -0
- package/dist/lib/tests/mixed-producer.d.ts +2 -0
- package/dist/lib/tests/mixed-producer.js +59 -0
- package/dist/lib/tests/mixed-producer.js.map +1 -0
- package/dist/lib/tests/producer.d.ts +2 -0
- package/dist/lib/tests/producer.js +60 -0
- package/dist/lib/tests/producer.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,1011 @@
|
|
|
1
|
+
# @a_jackie_z/event-bus
|
|
2
|
+
|
|
3
|
+
A robust, TypeScript-first event bus implementation using RabbitMQ. Supports both point-to-point (queue-based) and broadcast (fanout exchange) messaging patterns with automatic reconnection and reliable message delivery.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dual Messaging Patterns**: Point-to-point (load-balanced queues) and broadcast (fanout exchanges)
|
|
8
|
+
- **TypeScript Support**: Full type safety with TypeScript definitions
|
|
9
|
+
- **Auto-Reconnection**: Automatic reconnection with 15-second intervals
|
|
10
|
+
- **Message Persistence**: Durable queues and persistent messages for reliability
|
|
11
|
+
- **Fair Distribution**: Prefetch set to 1 for even load balancing across consumers
|
|
12
|
+
- **Connection State Monitoring**: Track connection state changes with callbacks
|
|
13
|
+
- **Graceful Shutdown**: Proper cleanup of consumers and connections
|
|
14
|
+
- **Error Handling**: Fail-safe message acknowledgment strategies
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @a_jackie_z/event-bus
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Architecture](#architecture)
|
|
25
|
+
- [Pattern Comparison](#pattern-comparison)
|
|
26
|
+
- [Quick Start](#quick-start)
|
|
27
|
+
- [Point-to-Point Messaging](#point-to-point-messaging)
|
|
28
|
+
- [Broadcast Messaging](#broadcast-messaging)
|
|
29
|
+
- [Mixed Mode](#mixed-mode)
|
|
30
|
+
- [Connection State Management](#connection-state-management)
|
|
31
|
+
- [Scaling Strategies](#scaling-strategies)
|
|
32
|
+
- [Performance Tips](#performance-tips)
|
|
33
|
+
- [API Reference](#api-reference)
|
|
34
|
+
- [Best Practices](#best-practices)
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
### Point-to-Point Pattern (Queue-Based)
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
┌──────────┐ ┌───────────┐
|
|
42
|
+
│ Producer │───── publish() ───>│ Queue │
|
|
43
|
+
└──────────┘ │ (durable) │
|
|
44
|
+
└─────┬─────┘
|
|
45
|
+
│
|
|
46
|
+
┌─────────────┴─────────────┐
|
|
47
|
+
│ (Load Balanced) │
|
|
48
|
+
▼ ▼
|
|
49
|
+
┌───────────┐ ┌───────────┐
|
|
50
|
+
│ Consumer1 │ │ Consumer2 │
|
|
51
|
+
└───────────┘ └───────────┘
|
|
52
|
+
|
|
53
|
+
Each message is delivered to ONE consumer (round-robin)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Broadcast Pattern (Fanout Exchange)
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
┌─────────────────┐
|
|
60
|
+
│ Fanout Exchange │
|
|
61
|
+
┌──────────┐ └────────┬────────┘
|
|
62
|
+
│ Producer │─── broadcast() ───────────>│
|
|
63
|
+
└──────────┘ │
|
|
64
|
+
┌───────────────┴───────────────┐
|
|
65
|
+
│ │
|
|
66
|
+
▼ ▼
|
|
67
|
+
┌─────────┐ ┌─────────┐
|
|
68
|
+
│ Queue1 │ │ Queue2 │
|
|
69
|
+
│(exclusive) │(exclusive)
|
|
70
|
+
└────┬────┘ └────┬────┘
|
|
71
|
+
│ │
|
|
72
|
+
▼ ▼
|
|
73
|
+
┌───────────┐ ┌───────────┐
|
|
74
|
+
│ Consumer1 │ │ Consumer2 │
|
|
75
|
+
└───────────┘ └───────────┘
|
|
76
|
+
|
|
77
|
+
Each message is delivered to ALL consumers
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Pattern Comparison
|
|
81
|
+
|
|
82
|
+
| Feature | Point-to-Point (Queue) | Broadcast (Fanout Exchange) |
|
|
83
|
+
|---------|------------------------|------------------------------|
|
|
84
|
+
| **Use Case** | Task distribution, work queues | Event notifications, announcements |
|
|
85
|
+
| **Delivery** | One consumer receives each message | All consumers receive every message |
|
|
86
|
+
| **Message Persistence** | Durable queues, persistent messages | Transient messages, non-durable exchange |
|
|
87
|
+
| **Queue Type** | Shared, durable queue | Exclusive, auto-delete queues per consumer |
|
|
88
|
+
| **Consumer Behavior** | Load-balanced (round-robin) | All consumers process independently |
|
|
89
|
+
| **Example Scenarios** | Order processing, email sending, image processing | System alerts, cache invalidation, real-time updates |
|
|
90
|
+
| **Scaling** | Add consumers for parallel processing | Add consumers for redundancy/availability |
|
|
91
|
+
| **Message Guarantee** | At-least-once delivery | Best-effort delivery (no persistence) |
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { EventBusProducer, EventBusConsumer, type QueueHandlers, type EventHandler } from '@a_jackie_z/event-bus';
|
|
97
|
+
|
|
98
|
+
// Producer: Send messages
|
|
99
|
+
const producer = new EventBusProducer({
|
|
100
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672'
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await producer.connect();
|
|
104
|
+
await producer.publish('tasks', { taskId: 1, action: 'process' });
|
|
105
|
+
await producer.disconnect();
|
|
106
|
+
|
|
107
|
+
// Consumer: Process messages
|
|
108
|
+
const taskHandler: EventHandler = async (data) => {
|
|
109
|
+
console.log('Processing task:', data);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const queueHandlers: QueueHandlers = new Map([
|
|
113
|
+
['tasks', [taskHandler]]
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const consumer = new EventBusConsumer({
|
|
117
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
118
|
+
queueHandlers
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await consumer.connect();
|
|
122
|
+
// Consumer runs until disconnect() is called
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Point-to-Point Messaging
|
|
126
|
+
|
|
127
|
+
Use point-to-point messaging for task distribution where each message should be processed by exactly one consumer.
|
|
128
|
+
|
|
129
|
+
### Producer Example
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { EventBusProducer, ConnectionState } from '@a_jackie_z/event-bus';
|
|
133
|
+
|
|
134
|
+
const producer = new EventBusProducer({
|
|
135
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
136
|
+
onStateChange: (state, reconnectCount) => {
|
|
137
|
+
console.log(`Producer state: ${state}`, { reconnectCount });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await producer.connect();
|
|
143
|
+
|
|
144
|
+
// Publish to durable queue with persistent messages
|
|
145
|
+
await producer.publish('order_events', {
|
|
146
|
+
orderId: 1001,
|
|
147
|
+
userId: 5001,
|
|
148
|
+
items: ['laptop', 'mouse'],
|
|
149
|
+
total: 1299.99,
|
|
150
|
+
status: 'pending',
|
|
151
|
+
timestamp: new Date().toISOString()
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await producer.publish('order_events', {
|
|
155
|
+
orderId: 1002,
|
|
156
|
+
userId: 5002,
|
|
157
|
+
items: ['keyboard'],
|
|
158
|
+
total: 89.99,
|
|
159
|
+
status: 'pending',
|
|
160
|
+
timestamp: new Date().toISOString()
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
console.log('Messages published successfully');
|
|
164
|
+
|
|
165
|
+
// Graceful shutdown
|
|
166
|
+
await producer.disconnect();
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Failed to publish messages:', error);
|
|
169
|
+
await producer.disconnect();
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Consumer Example
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { EventBusConsumer, type QueueHandlers, type EventHandler } from '@a_jackie_z/event-bus';
|
|
177
|
+
|
|
178
|
+
// Define handlers for the queue
|
|
179
|
+
const processOrderHandler: EventHandler = async (data) => {
|
|
180
|
+
console.log('Processing order:', data.orderId);
|
|
181
|
+
// Process order logic here
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const notifyUserHandler: EventHandler = async (data) => {
|
|
185
|
+
console.log('Notifying user:', data.userId);
|
|
186
|
+
// Send notification logic here
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Map queue names to handlers (multiple handlers per queue)
|
|
190
|
+
const queueHandlers: QueueHandlers = new Map([
|
|
191
|
+
['order_events', [processOrderHandler, notifyUserHandler]]
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const consumer = new EventBusConsumer({
|
|
195
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
196
|
+
queueHandlers,
|
|
197
|
+
onStateChange: (state, reconnectCount) => {
|
|
198
|
+
console.log(`Consumer state: ${state}`, { reconnectCount });
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await consumer.connect();
|
|
204
|
+
console.log('Consumer connected and listening for messages');
|
|
205
|
+
|
|
206
|
+
// Handle graceful shutdown
|
|
207
|
+
process.on('SIGINT', async () => {
|
|
208
|
+
console.log('Shutting down gracefully...');
|
|
209
|
+
await consumer.disconnect();
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
process.on('SIGTERM', async () => {
|
|
214
|
+
console.log('Shutting down gracefully...');
|
|
215
|
+
await consumer.disconnect();
|
|
216
|
+
process.exit(0);
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('Failed to start consumer:', error);
|
|
220
|
+
await consumer.disconnect();
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Broadcast Messaging
|
|
226
|
+
|
|
227
|
+
Use broadcast messaging when all consumers need to receive every message (e.g., cache invalidation, system-wide notifications).
|
|
228
|
+
|
|
229
|
+
### Producer Example
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
import { EventBusProducer } from '@a_jackie_z/event-bus';
|
|
233
|
+
|
|
234
|
+
const producer = new EventBusProducer({
|
|
235
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672'
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await producer.connect();
|
|
239
|
+
|
|
240
|
+
// Broadcast to fanout exchange - all consumers receive this
|
|
241
|
+
await producer.broadcast('system_notifications', {
|
|
242
|
+
type: 'maintenance',
|
|
243
|
+
message: 'System maintenance scheduled for tonight',
|
|
244
|
+
priority: 'high',
|
|
245
|
+
timestamp: new Date().toISOString()
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await producer.broadcast('system_notifications', {
|
|
249
|
+
type: 'update',
|
|
250
|
+
message: 'New features available in version 2.0',
|
|
251
|
+
priority: 'medium',
|
|
252
|
+
timestamp: new Date().toISOString()
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await producer.disconnect();
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Consumer Example
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { EventBusConsumer, type QueueHandlers, type ExchangeBindings, type EventHandler } from '@a_jackie_z/event-bus';
|
|
262
|
+
|
|
263
|
+
// Handler for broadcast messages
|
|
264
|
+
const notificationHandler: EventHandler = async (data) => {
|
|
265
|
+
console.log('Received system notification:', data);
|
|
266
|
+
// Each consumer instance processes the notification independently
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Define queue handlers
|
|
270
|
+
const queueHandlers: QueueHandlers = new Map([
|
|
271
|
+
['notifications', [notificationHandler]]
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
// Bind queues to exchanges for broadcast
|
|
275
|
+
const exchangeBindings: ExchangeBindings = new Map([
|
|
276
|
+
['notifications', 'system_notifications'] // Queue -> Exchange mapping
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
const consumer = new EventBusConsumer({
|
|
280
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
281
|
+
queueHandlers,
|
|
282
|
+
exchangeBindings // This enables broadcast mode for 'notifications' queue
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
await consumer.connect();
|
|
286
|
+
console.log('Consumer listening for broadcasts');
|
|
287
|
+
|
|
288
|
+
// Each consumer instance will receive ALL broadcast messages
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Note**: When using `exchangeBindings`, the consumer creates an **exclusive queue** that is automatically deleted when the consumer disconnects. This ensures each consumer instance receives all broadcast messages.
|
|
292
|
+
|
|
293
|
+
## Mixed Mode
|
|
294
|
+
|
|
295
|
+
Combine both point-to-point and broadcast patterns in a single consumer.
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { EventBusConsumer, type QueueHandlers, type ExchangeBindings, type EventHandler } from '@a_jackie_z/event-bus';
|
|
299
|
+
|
|
300
|
+
// Point-to-point handler (only one consumer processes each message)
|
|
301
|
+
const processOrderHandler: EventHandler = async (data) => {
|
|
302
|
+
console.log('Processing order (load balanced):', data.orderId);
|
|
303
|
+
// Heavy processing - distributed across consumers
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Broadcast handler (all consumers receive each message)
|
|
307
|
+
const cacheInvalidationHandler: EventHandler = async (data) => {
|
|
308
|
+
console.log('Invalidating cache (all consumers):', data.cacheKey);
|
|
309
|
+
// Cache invalidation - every instance must process
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const queueHandlers: QueueHandlers = new Map([
|
|
313
|
+
['orders', [processOrderHandler]], // Point-to-point queue
|
|
314
|
+
['cache_invalidation', [cacheInvalidationHandler]] // Broadcast queue
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const exchangeBindings: ExchangeBindings = new Map([
|
|
318
|
+
['cache_invalidation', 'cache_invalidation_broadcast'] // Only bind broadcast queues
|
|
319
|
+
// 'orders' is NOT bound, so it remains a point-to-point queue
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
const consumer = new EventBusConsumer({
|
|
323
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
324
|
+
queueHandlers,
|
|
325
|
+
exchangeBindings
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await consumer.connect();
|
|
329
|
+
// Now handles both patterns:
|
|
330
|
+
// - orders: Load-balanced across consumers
|
|
331
|
+
// - cache_invalidation: All consumers receive every message
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Producer for Mixed Mode
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { EventBusProducer } from '@a_jackie_z/event-bus';
|
|
338
|
+
|
|
339
|
+
const producer = new EventBusProducer({
|
|
340
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672'
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await producer.connect();
|
|
344
|
+
|
|
345
|
+
// Point-to-point: Only one consumer processes this
|
|
346
|
+
await producer.publish('orders', {
|
|
347
|
+
orderId: 1001,
|
|
348
|
+
customerId: 5001,
|
|
349
|
+
total: 99.99
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Broadcast: All consumers receive this
|
|
353
|
+
await producer.broadcast('cache_invalidation_broadcast', {
|
|
354
|
+
cacheKey: 'user:5001',
|
|
355
|
+
action: 'invalidate'
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await producer.disconnect();
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
## Connection State Management
|
|
362
|
+
|
|
363
|
+
Monitor connection state changes and handle reconnection events.
|
|
364
|
+
|
|
365
|
+
### Connection States
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { ConnectionState } from '@a_jackie_z/event-bus';
|
|
369
|
+
|
|
370
|
+
// Available states:
|
|
371
|
+
ConnectionState.CONNECTED // Successfully connected to RabbitMQ
|
|
372
|
+
ConnectionState.DISCONNECTED // Intentionally disconnected
|
|
373
|
+
ConnectionState.RECONNECTING // Attempting to reconnect after connection loss
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### State Change Callback
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { EventBusProducer, ConnectionState } from '@a_jackie_z/event-bus';
|
|
380
|
+
|
|
381
|
+
const producer = new EventBusProducer({
|
|
382
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
383
|
+
onStateChange: (state, reconnectCount) => {
|
|
384
|
+
switch (state) {
|
|
385
|
+
case ConnectionState.CONNECTED:
|
|
386
|
+
console.log('✓ Connected to RabbitMQ');
|
|
387
|
+
break;
|
|
388
|
+
case ConnectionState.DISCONNECTED:
|
|
389
|
+
console.log('✗ Disconnected from RabbitMQ');
|
|
390
|
+
break;
|
|
391
|
+
case ConnectionState.RECONNECTING:
|
|
392
|
+
console.log(`⟳ Reconnecting... (attempt ${reconnectCount})`);
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
await producer.connect();
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Automatic Reconnection
|
|
402
|
+
|
|
403
|
+
The event bus automatically attempts to reconnect when:
|
|
404
|
+
- Connection is lost
|
|
405
|
+
- Channel errors occur
|
|
406
|
+
- Network issues arise
|
|
407
|
+
|
|
408
|
+
**Reconnection Behavior**:
|
|
409
|
+
- Initial retry after 15 seconds
|
|
410
|
+
- Continues retrying indefinitely with 15-second intervals
|
|
411
|
+
- Resets retry counter on successful connection
|
|
412
|
+
- Preserves queue/exchange configurations on reconnection
|
|
413
|
+
|
|
414
|
+
## Scaling Strategies
|
|
415
|
+
|
|
416
|
+
### Horizontal Consumer Scaling
|
|
417
|
+
|
|
418
|
+
**Point-to-Point Queues** (Load Balancing):
|
|
419
|
+
```typescript
|
|
420
|
+
// Run multiple consumer instances - messages are distributed
|
|
421
|
+
// Consumer 1
|
|
422
|
+
const consumer1 = new EventBusConsumer({
|
|
423
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
424
|
+
queueHandlers: new Map([['tasks', [handler]]])
|
|
425
|
+
});
|
|
426
|
+
await consumer1.connect();
|
|
427
|
+
|
|
428
|
+
// Consumer 2 (same configuration)
|
|
429
|
+
const consumer2 = new EventBusConsumer({
|
|
430
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
431
|
+
queueHandlers: new Map([['tasks', [handler]]])
|
|
432
|
+
});
|
|
433
|
+
await consumer2.connect();
|
|
434
|
+
|
|
435
|
+
// Messages in 'tasks' queue are distributed round-robin between consumers
|
|
436
|
+
// If Consumer 1 is processing, Consumer 2 gets the next message
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Broadcast Exchanges** (Redundancy):
|
|
440
|
+
```typescript
|
|
441
|
+
// All consumer instances receive ALL broadcast messages
|
|
442
|
+
// Useful for cache invalidation, configuration updates, etc.
|
|
443
|
+
|
|
444
|
+
// Service Instance 1
|
|
445
|
+
const consumer1 = new EventBusConsumer({
|
|
446
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
447
|
+
queueHandlers: new Map([['updates', [handler]]]),
|
|
448
|
+
exchangeBindings: new Map([['updates', 'system_updates']])
|
|
449
|
+
});
|
|
450
|
+
await consumer1.connect();
|
|
451
|
+
|
|
452
|
+
// Service Instance 2 (receives same broadcasts)
|
|
453
|
+
const consumer2 = new EventBusConsumer({
|
|
454
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
455
|
+
queueHandlers: new Map([['updates', [handler]]]),
|
|
456
|
+
exchangeBindings: new Map([['updates', 'system_updates']])
|
|
457
|
+
});
|
|
458
|
+
await consumer2.connect();
|
|
459
|
+
|
|
460
|
+
// Both instances receive every broadcast message
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Queue Partitioning Strategies
|
|
464
|
+
|
|
465
|
+
**Option 1: Multiple Queues by Category**
|
|
466
|
+
```typescript
|
|
467
|
+
// Producer distributes by category
|
|
468
|
+
await producer.publish('orders_electronics', { category: 'electronics', ... });
|
|
469
|
+
await producer.publish('orders_clothing', { category: 'clothing', ... });
|
|
470
|
+
|
|
471
|
+
// Consumer specializes by category
|
|
472
|
+
const consumer = new EventBusConsumer({
|
|
473
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
474
|
+
queueHandlers: new Map([
|
|
475
|
+
['orders_electronics', [electronicsHandler]],
|
|
476
|
+
['orders_clothing', [clothingHandler]]
|
|
477
|
+
])
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Option 2: Consistent Hashing for Partitioning**
|
|
482
|
+
```typescript
|
|
483
|
+
// Hash-based queue assignment
|
|
484
|
+
function getQueueForUser(userId: number, partitionCount: number): string {
|
|
485
|
+
const partition = userId % partitionCount;
|
|
486
|
+
return `user_events_partition_${partition}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Producer
|
|
490
|
+
const queueName = getQueueForUser(userId, 4); // 4 partitions
|
|
491
|
+
await producer.publish(queueName, userData);
|
|
492
|
+
|
|
493
|
+
// Consumer handles specific partitions
|
|
494
|
+
const consumer = new EventBusConsumer({
|
|
495
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
496
|
+
queueHandlers: new Map([
|
|
497
|
+
['user_events_partition_0', [handler]],
|
|
498
|
+
['user_events_partition_1', [handler]]
|
|
499
|
+
])
|
|
500
|
+
});
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Option 3: Priority Queues**
|
|
504
|
+
```typescript
|
|
505
|
+
// Separate queues by priority
|
|
506
|
+
await producer.publish('tasks_high_priority', { priority: 'high', ... });
|
|
507
|
+
await producer.publish('tasks_low_priority', { priority: 'low', ... });
|
|
508
|
+
|
|
509
|
+
// Run more consumers on high-priority queue
|
|
510
|
+
// 3 consumers for high priority
|
|
511
|
+
// 1 consumer for low priority
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Consumer Groups Pattern
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// Consumer Group 1: Order Processing (3 instances)
|
|
518
|
+
// All share the same 'orders' queue for load balancing
|
|
519
|
+
for (let i = 1; i <= 3; i++) {
|
|
520
|
+
const consumer = new EventBusConsumer({
|
|
521
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
522
|
+
queueHandlers: new Map([['orders', [orderHandler]]])
|
|
523
|
+
});
|
|
524
|
+
await consumer.connect();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Consumer Group 2: Notifications (2 instances)
|
|
528
|
+
// All share the same 'notifications' queue
|
|
529
|
+
for (let i = 1; i <= 2; i++) {
|
|
530
|
+
const consumer = new EventBusConsumer({
|
|
531
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
532
|
+
queueHandlers: new Map([['notifications', [notificationHandler]]])
|
|
533
|
+
});
|
|
534
|
+
await consumer.connect();
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Monitoring for Scaling Decisions
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// Monitor queue depth to make scaling decisions
|
|
542
|
+
// Use RabbitMQ Management API or CLI
|
|
543
|
+
|
|
544
|
+
// Example: Check queue depth
|
|
545
|
+
// rabbitmqadmin list queues name messages
|
|
546
|
+
|
|
547
|
+
// Scale up when:
|
|
548
|
+
// - Queue depth consistently > 1000 messages
|
|
549
|
+
// - Consumer processing time increases
|
|
550
|
+
// - Message age increases
|
|
551
|
+
|
|
552
|
+
// Scale down when:
|
|
553
|
+
// - Queue depth consistently near 0
|
|
554
|
+
// - Multiple consumers idle
|
|
555
|
+
// - Processing capacity exceeds demand
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Performance Tips
|
|
559
|
+
|
|
560
|
+
### 1. Connection Reuse
|
|
561
|
+
|
|
562
|
+
**DO**: Create one producer/consumer per service instance
|
|
563
|
+
```typescript
|
|
564
|
+
// Good: Single producer for the entire service
|
|
565
|
+
class OrderService {
|
|
566
|
+
private producer: EventBusProducer;
|
|
567
|
+
|
|
568
|
+
async init() {
|
|
569
|
+
this.producer = new EventBusProducer({
|
|
570
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672'
|
|
571
|
+
});
|
|
572
|
+
await this.producer.connect();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async createOrder(order: Order) {
|
|
576
|
+
await this.producer.publish('orders', order);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**DON'T**: Create new connections per operation
|
|
582
|
+
```typescript
|
|
583
|
+
// Bad: Creates new connection for each publish
|
|
584
|
+
async function sendMessage(data: any) {
|
|
585
|
+
const producer = new EventBusProducer({ rabbitMqUrl: '...' });
|
|
586
|
+
await producer.connect();
|
|
587
|
+
await producer.publish('queue', data);
|
|
588
|
+
await producer.disconnect(); // Expensive!
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### 2. Message Batching
|
|
593
|
+
|
|
594
|
+
For high-throughput scenarios, batch multiple operations:
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
// Batch publishing
|
|
598
|
+
const messages = [
|
|
599
|
+
{ orderId: 1, ... },
|
|
600
|
+
{ orderId: 2, ... },
|
|
601
|
+
{ orderId: 3, ... }
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
await producer.connect();
|
|
605
|
+
for (const message of messages) {
|
|
606
|
+
await producer.publish('orders', message); // Uses same connection
|
|
607
|
+
}
|
|
608
|
+
// Confirms are awaited per message, ensuring reliability
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### 3. Non-Blocking Handlers
|
|
612
|
+
|
|
613
|
+
Keep handlers async and non-blocking:
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// Good: Non-blocking handler
|
|
617
|
+
const handler: EventHandler = async (data) => {
|
|
618
|
+
// Quick processing
|
|
619
|
+
await database.insert(data);
|
|
620
|
+
|
|
621
|
+
// Offload heavy work to another queue
|
|
622
|
+
await producer.publish('heavy_processing', data);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Bad: Blocking handler
|
|
626
|
+
const slowHandler: EventHandler = async (data) => {
|
|
627
|
+
// Blocks other messages for 10 seconds
|
|
628
|
+
await heavyComputation(data); // 10 seconds
|
|
629
|
+
await database.insert(data);
|
|
630
|
+
};
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### 4. Prefetch Optimization
|
|
634
|
+
|
|
635
|
+
The library sets `prefetch=1` by default for fair distribution. This means:
|
|
636
|
+
- Each consumer gets one message at a time
|
|
637
|
+
- Fast consumers get more messages
|
|
638
|
+
- Slow consumers don't get overwhelmed
|
|
639
|
+
|
|
640
|
+
For specialized scenarios:
|
|
641
|
+
```typescript
|
|
642
|
+
// Current behavior (prefetch=1):
|
|
643
|
+
// - Fair distribution across consumers
|
|
644
|
+
// - Prevents consumer overload
|
|
645
|
+
// - Ideal for most use cases
|
|
646
|
+
|
|
647
|
+
// If you need different prefetch values, you would need to
|
|
648
|
+
// modify the consumer.ts source code (line: channel.prefetch(1))
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### 5. Connection Pooling
|
|
652
|
+
|
|
653
|
+
For microservices with multiple queues:
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
// Single consumer handles multiple queues
|
|
657
|
+
const consumer = new EventBusConsumer({
|
|
658
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
659
|
+
queueHandlers: new Map([
|
|
660
|
+
['queue1', [handler1]],
|
|
661
|
+
['queue2', [handler2]],
|
|
662
|
+
['queue3', [handler3]]
|
|
663
|
+
])
|
|
664
|
+
});
|
|
665
|
+
// One connection, multiple queues - efficient!
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### 6. Monitor Queue Depths
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
// Implement monitoring to track performance
|
|
672
|
+
const producer = new EventBusProducer({
|
|
673
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
674
|
+
onStateChange: (state) => {
|
|
675
|
+
// Log state changes for monitoring
|
|
676
|
+
metrics.recordConnectionState(state);
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// External monitoring with RabbitMQ Management API:
|
|
681
|
+
// - Queue depth (messages ready)
|
|
682
|
+
// - Consumer count
|
|
683
|
+
// - Message rate (in/out)
|
|
684
|
+
// - Consumer utilization
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
### 7. Message Size Optimization
|
|
688
|
+
|
|
689
|
+
```typescript
|
|
690
|
+
// Keep messages small for better throughput
|
|
691
|
+
// Good: Reference to data
|
|
692
|
+
await producer.publish('image_processing', {
|
|
693
|
+
imageId: 12345,
|
|
694
|
+
bucket: 's3://images',
|
|
695
|
+
key: 'photo.jpg'
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Bad: Embedding large data
|
|
699
|
+
await producer.publish('image_processing', {
|
|
700
|
+
imageData: base64Image // Could be MBs!
|
|
701
|
+
});
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### 8. Batch Acknowledgments
|
|
705
|
+
|
|
706
|
+
The library handles acknowledgments efficiently:
|
|
707
|
+
- Messages are acknowledged individually
|
|
708
|
+
- Failed messages are not requeued (nack without requeue)
|
|
709
|
+
- At least one handler must succeed for acknowledgment
|
|
710
|
+
|
|
711
|
+
## API Reference
|
|
712
|
+
|
|
713
|
+
### EventBusProducer
|
|
714
|
+
|
|
715
|
+
Producer for publishing messages to queues or broadcasting to exchanges.
|
|
716
|
+
|
|
717
|
+
#### Constructor
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
new EventBusProducer(options: EventBusProducerOptions)
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
**Options**:
|
|
724
|
+
- `rabbitMqUrl: string` - RabbitMQ connection URL (e.g., `amqp://user:pass@host:5672`)
|
|
725
|
+
- `onStateChange?: (state: ConnectionState, reconnectCount?: number) => void` - Callback for connection state changes
|
|
726
|
+
|
|
727
|
+
#### Methods
|
|
728
|
+
|
|
729
|
+
##### `connect(): Promise<void>`
|
|
730
|
+
|
|
731
|
+
Establishes connection to RabbitMQ. Must be called before publishing.
|
|
732
|
+
|
|
733
|
+
```typescript
|
|
734
|
+
await producer.connect();
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
##### `publish(queueName: string, data: any): Promise<void>`
|
|
738
|
+
|
|
739
|
+
Publishes a message to a durable queue with persistence enabled. Message is delivered to one consumer (load-balanced).
|
|
740
|
+
|
|
741
|
+
**Parameters**:
|
|
742
|
+
- `queueName: string` - Name of the queue
|
|
743
|
+
- `data: any` - Message payload (will be JSON serialized)
|
|
744
|
+
|
|
745
|
+
**Throws**: Error if channel is not available or publish fails
|
|
746
|
+
|
|
747
|
+
```typescript
|
|
748
|
+
await producer.publish('orders', { orderId: 123, amount: 99.99 });
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
##### `broadcast(exchangeName: string, data: any): Promise<void>`
|
|
752
|
+
|
|
753
|
+
Broadcasts a message to all consumers listening on the fanout exchange. All consumers receive the message.
|
|
754
|
+
|
|
755
|
+
**Parameters**:
|
|
756
|
+
- `exchangeName: string` - Name of the fanout exchange
|
|
757
|
+
- `data: any` - Message payload (will be JSON serialized)
|
|
758
|
+
|
|
759
|
+
**Throws**: Error if channel is not available or broadcast fails
|
|
760
|
+
|
|
761
|
+
```typescript
|
|
762
|
+
await producer.broadcast('notifications', { type: 'alert', message: 'System update' });
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
##### `disconnect(): Promise<void>`
|
|
766
|
+
|
|
767
|
+
Gracefully closes the connection and stops reconnection attempts.
|
|
768
|
+
|
|
769
|
+
```typescript
|
|
770
|
+
await producer.disconnect();
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### EventBusConsumer
|
|
774
|
+
|
|
775
|
+
Consumer for processing messages from queues and broadcast exchanges.
|
|
776
|
+
|
|
777
|
+
#### Constructor
|
|
778
|
+
|
|
779
|
+
```typescript
|
|
780
|
+
new EventBusConsumer(options: EventBusConsumerOptions)
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**Options**:
|
|
784
|
+
- `rabbitMqUrl: string` - RabbitMQ connection URL
|
|
785
|
+
- `queueHandlers: QueueHandlers` - Map of queue names to handler arrays
|
|
786
|
+
- `onStateChange?: (state: ConnectionState, reconnectCount?: number) => void` - State change callback
|
|
787
|
+
- `exchangeBindings?: ExchangeBindings` - Map of queue names to exchange names (for broadcast mode)
|
|
788
|
+
|
|
789
|
+
#### Types
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
type EventHandler<T = any> = (data: T) => Promise<void>;
|
|
793
|
+
type QueueHandlers = Map<string, EventHandler[]>;
|
|
794
|
+
type ExchangeBindings = Map<string, string>; // Map<queueName, exchangeName>
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
#### Methods
|
|
798
|
+
|
|
799
|
+
##### `connect(): Promise<void>`
|
|
800
|
+
|
|
801
|
+
Connects to RabbitMQ and starts consuming messages from configured queues.
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
await consumer.connect();
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
##### `disconnect(): Promise<void>`
|
|
808
|
+
|
|
809
|
+
Gracefully cancels all consumers and closes the connection.
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
await consumer.disconnect();
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### ConnectionState
|
|
816
|
+
|
|
817
|
+
Enum representing connection states:
|
|
818
|
+
|
|
819
|
+
```typescript
|
|
820
|
+
enum ConnectionState {
|
|
821
|
+
CONNECTED = 'CONNECTED', // Successfully connected
|
|
822
|
+
DISCONNECTED = 'DISCONNECTED', // Intentionally disconnected
|
|
823
|
+
RECONNECTING = 'RECONNECTING' // Attempting reconnection
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
## Best Practices
|
|
828
|
+
|
|
829
|
+
### Error Handling in Handlers
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
// Good: Handler with error handling
|
|
833
|
+
const orderHandler: EventHandler = async (data) => {
|
|
834
|
+
try {
|
|
835
|
+
await processOrder(data);
|
|
836
|
+
await updateInventory(data);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
console.error('Failed to process order:', error);
|
|
839
|
+
// Log error for monitoring
|
|
840
|
+
// Handler failure is caught by the library
|
|
841
|
+
// Message is nacked if all handlers fail
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// Multiple handlers: At least one must succeed
|
|
846
|
+
const queueHandlers: QueueHandlers = new Map([
|
|
847
|
+
['orders', [
|
|
848
|
+
orderHandler, // If this fails...
|
|
849
|
+
notificationHandler // ...but this succeeds, message is acknowledged
|
|
850
|
+
]]
|
|
851
|
+
]);
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
### Graceful Shutdown
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
// Always handle graceful shutdown
|
|
858
|
+
const consumer = new EventBusConsumer({
|
|
859
|
+
rabbitMqUrl: 'amqp://username:password@localhost:5672',
|
|
860
|
+
queueHandlers
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
await consumer.connect();
|
|
864
|
+
|
|
865
|
+
// Handle termination signals
|
|
866
|
+
const shutdown = async (signal: string) => {
|
|
867
|
+
console.log(`${signal} received, shutting down gracefully...`);
|
|
868
|
+
await consumer.disconnect();
|
|
869
|
+
process.exit(0);
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
873
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
874
|
+
|
|
875
|
+
// For producers
|
|
876
|
+
process.on('beforeExit', async () => {
|
|
877
|
+
await producer.disconnect();
|
|
878
|
+
});
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
### Message Persistence Configuration
|
|
882
|
+
|
|
883
|
+
**Point-to-Point** (Durable and Persistent):
|
|
884
|
+
```typescript
|
|
885
|
+
// Producer automatically creates durable queues
|
|
886
|
+
await producer.publish('tasks', data);
|
|
887
|
+
// - Queue survives broker restart (durable: true)
|
|
888
|
+
// - Messages survive broker restart (persistent: true)
|
|
889
|
+
// - Use for critical tasks that must not be lost
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
**Broadcast** (Transient):
|
|
893
|
+
```typescript
|
|
894
|
+
// Broadcast messages are transient
|
|
895
|
+
await producer.broadcast('notifications', data);
|
|
896
|
+
// - Exchange is non-durable
|
|
897
|
+
// - Messages are non-persistent
|
|
898
|
+
// - Queues are exclusive and auto-delete
|
|
899
|
+
// - Use for real-time updates that don't need persistence
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### Handler Execution
|
|
903
|
+
|
|
904
|
+
Handlers execute **sequentially** within each message:
|
|
905
|
+
|
|
906
|
+
```typescript
|
|
907
|
+
const queueHandlers: QueueHandlers = new Map([
|
|
908
|
+
['orders', [
|
|
909
|
+
handler1, // Executes first
|
|
910
|
+
handler2, // Executes after handler1 completes
|
|
911
|
+
handler3 // Executes after handler2 completes
|
|
912
|
+
]]
|
|
913
|
+
]);
|
|
914
|
+
|
|
915
|
+
// Acknowledgment rules:
|
|
916
|
+
// - If at least one handler succeeds -> message acknowledged
|
|
917
|
+
// - If all handlers fail -> message nacked (not requeued)
|
|
918
|
+
// - Failed handlers log errors but don't block subsequent handlers
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### Connection Management
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
// DO: Initialize once, use throughout application lifecycle
|
|
925
|
+
class MessageService {
|
|
926
|
+
private producer: EventBusProducer;
|
|
927
|
+
private consumer: EventBusConsumer;
|
|
928
|
+
|
|
929
|
+
async initialize() {
|
|
930
|
+
this.producer = new EventBusProducer({
|
|
931
|
+
rabbitMqUrl: process.env.RABBITMQ_URL!,
|
|
932
|
+
onStateChange: this.handleStateChange
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
this.consumer = new EventBusConsumer({
|
|
936
|
+
rabbitMqUrl: process.env.RABBITMQ_URL!,
|
|
937
|
+
queueHandlers: this.getHandlers()
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
await Promise.all([
|
|
941
|
+
this.producer.connect(),
|
|
942
|
+
this.consumer.connect()
|
|
943
|
+
]);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async shutdown() {
|
|
947
|
+
await Promise.all([
|
|
948
|
+
this.producer.disconnect(),
|
|
949
|
+
this.consumer.disconnect()
|
|
950
|
+
]);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private handleStateChange(state: ConnectionState, reconnectCount?: number) {
|
|
954
|
+
// Log state changes for monitoring/alerting
|
|
955
|
+
logger.info({ state, reconnectCount }, 'RabbitMQ state change');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
### Testing
|
|
961
|
+
|
|
962
|
+
```typescript
|
|
963
|
+
// Use test containers or local RabbitMQ for testing
|
|
964
|
+
describe('EventBus', () => {
|
|
965
|
+
let producer: EventBusProducer;
|
|
966
|
+
let consumer: EventBusConsumer;
|
|
967
|
+
|
|
968
|
+
beforeAll(async () => {
|
|
969
|
+
producer = new EventBusProducer({
|
|
970
|
+
rabbitMqUrl: 'amqp://guest:guest@localhost:5672'
|
|
971
|
+
});
|
|
972
|
+
await producer.connect();
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
afterAll(async () => {
|
|
976
|
+
await producer.disconnect();
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('should process messages', async () => {
|
|
980
|
+
const received: any[] = [];
|
|
981
|
+
|
|
982
|
+
const handler: EventHandler = async (data) => {
|
|
983
|
+
received.push(data);
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
consumer = new EventBusConsumer({
|
|
987
|
+
rabbitMqUrl: 'amqp://guest:guest@localhost:5672',
|
|
988
|
+
queueHandlers: new Map([['test_queue', [handler]]])
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
await consumer.connect();
|
|
992
|
+
await producer.publish('test_queue', { test: 'data' });
|
|
993
|
+
|
|
994
|
+
// Wait for processing
|
|
995
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
996
|
+
|
|
997
|
+
expect(received).toHaveLength(1);
|
|
998
|
+
expect(received[0]).toEqual({ test: 'data' });
|
|
999
|
+
|
|
1000
|
+
await consumer.disconnect();
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
## License
|
|
1006
|
+
|
|
1007
|
+
MIT © Sang Lu
|
|
1008
|
+
|
|
1009
|
+
## Author
|
|
1010
|
+
|
|
1011
|
+
Sang Lu <connect.with.sang@gmail.com>
|