UNPKG

@message-in-the-middle/rabbitmq

Version:

RabbitMQ integration for message-in-the-middle - Production-ready AMQP messaging with zero boilerplate

544 lines (413 loc) 13.6 kB
# @message-in-the-middle/rabbitmq > ⚠️ **Work in Progress** > **Is this library production-ready?** No. > **Is this library safe?** No. > **When will it be ready?** Soon™ (maybe tomorrow, maybe never). > **Why is it public?** Experiment message-in-the-middle is to Express.js what your message queue processing is to HTTP request processing. Just as Express provides a middleware pattern for HTTP requests, this library provides a middleware pattern for processing queue messages. ## Why This Exists Processing queue messages usually means copy-pasting the same boilerplate: parse JSON, validate, log, retry, deduplicate, route to handlers. This library lets you compose that logic as middlewares. --- **Production-ready RabbitMQ integration for message-in-the-middle** Eliminate 50-100 lines of RabbitMQ boilerplate with zero dependencies beyond `amqplib`. ## Features - ✅ **RabbitMQPoller** - Production-ready message consumption (50+ lines → 5 lines) - ✅ **RabbitMQPublisher** - Middleware-aware publishing - ✅ **Auto-reconnection** - Automatic reconnection with exponential backoff - ✅ **Multi-consumer** - Manage multiple consumers from single poller - ✅ **Graceful shutdown** - Wait for in-flight messages before closing - ✅ **Per-consumer events** - No if-else chains for consumer-specific logic - ✅ **QoS/Prefetch** - Full control over message prefetch - ✅ **TypeScript-first** - Full type safety with generics - ✅ **Zero overhead** - Thin wrapper over amqplib ## Installation ```bash npm install @message-in-the-middle/rabbitmq @message-in-the-middle/core amqplib ``` ## Quick Start ### Before (Manual Setup - 50+ lines) ```typescript // Manual connection, channel management, error handling, etc. const connection = await amqp.connect('amqp://localhost'); const channel = await connection.createChannel(); await channel.assertQueue('orders', { durable: true }); channel.prefetch(10); await channel.consume('orders', async (msg) => { if (msg) { try { const content = msg.content.toString(); const parsed = JSON.parse(content); // ... validation, retry logic, error handling await processOrder(parsed); channel.ack(msg); } catch (error) { channel.nack(msg, false, true); } } }); // Reconnection logic, graceful shutdown, etc. (30+ more lines) ``` ### After (With RabbitMQPoller - 5 lines) ```typescript import { RabbitMQPoller } from '@message-in-the-middle/rabbitmq'; import { createQueuePipeline } from '@message-in-the-middle/core'; // 1. Create pipeline with all middleware const ordersManager = createQueuePipeline({ queueName: 'orders', validation: OrderSchema, handler: async (ctx) => await processOrder(ctx.message), logger: console, }); // 2. Create poller and start consuming const poller = new RabbitMQPoller('amqp://localhost', { logger: console }); const ordersConsumer = await poller.start({ queue: 'orders-queue', manager: ordersManager, name: 'orders', prefetch: 10, }); // 3. Graceful shutdown await poller.stopAll(); ``` **Result:** 50+ lines → 5 lines. Production-ready with auto-reconnection, graceful shutdown, and full middleware support. --- ## API Documentation ### RabbitMQPoller Production-ready message poller with connection management. #### Constructor ```typescript new RabbitMQPoller( connectionConfig: string | Options.Connect, options?: RabbitMQPollerOptions ) ``` **Options:** ```typescript interface RabbitMQPollerOptions { logger?: Logger; // Logger instance defaultPrefetch?: number; // Default: 10 defaultNoAck?: boolean; // Default: false reconnectDelayMs?: number; // Default: 5000 maxReconnectAttempts?: number; // Default: Infinity heartbeat?: number; // Default: 30 seconds } ``` #### start(options) → ConsumerController Start consuming from a queue. ```typescript const consumer = await poller.start({ queue: 'orders-queue', // Queue name (required) manager: ordersManager, // Middleware manager (required) name: 'orders', // Consumer name (required) prefetch: 10, // QoS prefetch count noAck: false, // Auto-ack (default: false) queueOptions: { // Queue assertion options durable: true, autoDelete: false, }, requeueOnError: true, // Requeue on error (default: true) }); ``` **Returns:** `ConsumerController` for per-consumer control #### stopAll() → Promise<void> Stop all consumers gracefully and close connection. ```typescript await poller.stopAll(); ``` #### Global Events ```typescript // Connection lifecycle poller.on('connection:connected', (connection) => {}); poller.on('connection:error', (error) => {}); poller.on('connection:closed', () => {}); poller.on('connection:reconnecting', (attempt) => {}); // Consumer lifecycle poller.on('consumer:started', (name, queue) => {}); poller.on('consumer:stopped', (name) => {}); poller.on('consumer:error', (name, error) => {}); // Message processing (cross-consumer) poller.on('message:received', (name, message) => {}); poller.on('message:processed', (name, message, duration) => {}); poller.on('message:failed', (name, message, error) => {}); ``` --- ### ConsumerController Per-consumer control and events. #### Methods ```typescript consumer.pause() // Pause consuming consumer.resume() // Resume consuming await consumer.stop() // Stop consumer gracefully consumer.getStatus() // Get consumer status consumer.getName() // Get consumer name consumer.getQueue() // Get queue name consumer.isPaused() // Check if paused ``` #### Per-Consumer Events ```typescript // No if-else chains! Each consumer has its own events ordersConsumer.on('message:processed', (message, duration) => { logger.info('Order processed', { duration }); }); ordersConsumer.on('message:failed', (message, error) => { alerting.notify('Order processing failed', { error }); }); notificationsConsumer.on('message:processed', (message, duration) => { logger.info('Notification sent', { duration }); }); ``` --- ### RabbitMQPublisher Middleware-aware message publishing. #### Constructor ```typescript new RabbitMQPublisher( connectionConfig: string | Options.Connect, options?: RabbitMQPublisherOptions ) ``` **Options:** ```typescript interface RabbitMQPublisherOptions { logger?: Logger; confirmChannel?: boolean; // Use confirm channel (default: false) defaultExchange?: string; // Default: '' (direct) defaultRoutingKey?: string; } ``` #### use(middleware) → this Add outbound middleware to pipeline. ```typescript publisher .use(new StringifyJsonOutboundMiddleware()) .use(new EncryptOutboundMiddleware(key)) .use(new MetricsOutboundMiddleware(collector)); ``` #### publish(message, options) → Promise<void> Publish message through middleware pipeline. ```typescript await publisher.publish( { orderId: '123', amount: 99.99 }, { exchange: 'orders', routingKey: 'order.created', options: { persistent: true }, } ); ``` #### destroy() → Promise<void> Close connections and cleanup. ```typescript await publisher.destroy(); ``` --- ### RabbitMQMetadataMiddleware Extract RabbitMQ-specific metadata. ```typescript import { RabbitMQMetadataMiddleware } from '@message-in-the-middle/rabbitmq'; manager.addInboundMiddleware(new RabbitMQMetadataMiddleware()); // Access in handler const handler = async (ctx) => { const rabbitmq = ctx.metadata.rabbitmq; // RabbitMQMessageMetadata console.log('Exchange:', rabbitmq.exchange); console.log('Routing Key:', rabbitmq.routingKey); console.log('Redelivered:', rabbitmq.redelivered); console.log('Correlation ID:', rabbitmq.correlationId); }; ``` **Extracted metadata:** - `exchange` - Exchange name - `routingKey` - Routing key - `redelivered` - Redelivery flag - `messageId` - Message ID - `correlationId` - Correlation ID - `timestamp` - Timestamp - `headers` - Custom headers - `deliveryTag` - Delivery tag --- ## Complete Example ```typescript import { RabbitMQPoller, RabbitMQMetadataMiddleware } from '@message-in-the-middle/rabbitmq'; import { createQueuePipeline } from '@message-in-the-middle/core'; import { z } from 'zod'; // Define schema const OrderSchema = z.object({ orderId: z.string().uuid(), amount: z.number().positive(), }); // Create pipeline const ordersManager = createQueuePipeline({ queueName: 'orders', validation: OrderSchema, maxRetries: 3, handler: async (ctx) => { // Access RabbitMQ metadata const rabbitmq = ctx.metadata.rabbitmq; console.log('Routing key:', rabbitmq.routingKey); // Process order await processOrder(ctx.message); }, logger: console, }); // Add RabbitMQ metadata extraction ordersManager.addInboundMiddleware(new RabbitMQMetadataMiddleware()); // Create poller const poller = new RabbitMQPoller('amqp://localhost', { logger: console, defaultPrefetch: 10, }); // Start consumer const ordersConsumer = await poller.start({ queue: 'orders-queue', manager: ordersManager, name: 'orders', queueOptions: { durable: true }, }); // Per-consumer events ordersConsumer.on('message:processed', (message, duration) => { console.log(`✅ Processed in ${duration}ms`); }); // Graceful shutdown process.on('SIGTERM', async () => { await poller.stopAll(); await ordersManager.destroy(); }); ``` --- ## Multi-Consumer Example Manage multiple consumers from a single poller: ```typescript const poller = new RabbitMQPoller('amqp://localhost', { logger: console }); // Start multiple consumers const ordersConsumer = await poller.start({ queue: 'orders', manager: ordersManager, name: 'orders', prefetch: 10, }); const notificationsConsumer = await poller.start({ queue: 'notifications', manager: notificationsManager, name: 'notifications', prefetch: 20, }); const analyticsConsumer = await poller.start({ queue: 'analytics', manager: analyticsManager, name: 'analytics', prefetch: 50, }); // Per-consumer events (no if-else chains!) ordersConsumer.on('message:processed', (msg, duration) => { metrics.timing('orders.duration', duration); }); notificationsConsumer.on('message:processed', (msg, duration) => { metrics.timing('notifications.duration', duration); }); // Stop all at once await poller.stopAll(); ``` --- ## Publishing Example ```typescript import { RabbitMQPublisher } from '@message-in-the-middle/rabbitmq'; import { StringifyJsonOutboundMiddleware } from '@message-in-the-middle/core'; const publisher = new RabbitMQPublisher('amqp://localhost', { confirmChannel: true, logger: console, }); // Add middlewares publisher.use(new StringifyJsonOutboundMiddleware()); // Publish with middleware processing await publisher.publish( { orderId: '123', amount: 99.99 }, { exchange: 'orders', routingKey: 'order.created', options: { persistent: true, headers: { source: 'api' }, }, } ); // Cleanup await publisher.destroy(); ``` --- ## Comparison ### vs Manual RabbitMQ | Feature | Manual | @message-in-the-middle/rabbitmq | |---------|--------|-------------------------------| | **Setup code** | 50-100 lines | 5 lines | | **Connection management** | Manual | Automatic | | **Reconnection** | Manual (30+ lines) | Automatic | | **Graceful shutdown** | Manual (20+ lines) | Built-in | | **Multi-consumer** | Duplicate code | Single poller | | **Middleware** | None | Full support | | **Type safety** | Basic | Full generics | ### vs SQS Package Both packages follow the **same API pattern** for consistency: ```typescript // SQS const sqsPoller = new SQSPoller(sqsClient, { logger }); sqsPoller.start({ queueUrl, manager, name }); // RabbitMQ (same pattern!) const rabbitPoller = new RabbitMQPoller('amqp://localhost', { logger }); rabbitPoller.start({ queue, manager, name }); ``` --- ## Best Practices ### 1. Use Per-Consumer Events ```typescript // ✅ Good - Per-consumer events ordersConsumer.on('message:failed', async (msg, error) => { await notifyTeam('Orders queue failing', { error }); }); // ❌ Bad - Global events with if-else poller.on('message:failed', (name, msg, error) => { if (name === 'orders') { await notifyTeam('Orders queue failing', { error }); } }); ``` ### 2. Graceful Shutdown ```typescript // Always implement graceful shutdown process.on('SIGTERM', async () => { await poller.stopAll(); await manager.destroy(); process.exit(0); }); ``` ### 3. Queue-Native Features Use RabbitMQ's native features for infrastructure concerns: ```typescript // ✅ Use RabbitMQ DLX for dead letters await channel.assertQueue('orders', { arguments: { 'x-dead-letter-exchange': 'dlx', 'x-dead-letter-routing-key': 'failed.orders', }, }); // ✅ Use prefetch for rate limiting poller.start({ queue: 'orders', prefetch: 10 }); // ✅ Use middleware for message processing manager .use(parseJson()) .use(validate(schema)) .use(retry({ maxRetries: 3 })); ``` --- ## Examples - [Basic RabbitMQ Example](../../examples/queues/rabbitmq-with-poller.ts) - [Multi-Consumer Example](../../examples/queues/rabbitmq-multi-consumer.ts) - [Real-World Express + RabbitMQ](../../examples/real-world-express-rabbitmq/) --- ## Links - [Main Repository](https://github.com/gorannovosel/message-in-the-middle) - [Core Package](../core) - [Documentation](../../docs) - [Examples](../../examples) --- ## License MIT