@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/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>