runmq 0.0.1 → 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 RunMQ
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,470 @@
1
+ # RunMQ
2
+
3
+ RunMQ is a robust message queue library for Node.js built on RabbitMQ. Enables async background processing, or event-driven architectures via message bus — both with automatic retries, schema validation and DLQ.
4
+
5
+ RunMQ can be used to implement two main patterns:
6
+ - **Event Bus** for event-driven microservices architectures, where multiple services independently react to the same events
7
+ - **Job Queue** for async background task processing, with retries and dead letter queues
8
+
9
+ ## Features
10
+
11
+ - **Automatic Connection Management**: Built-in retry logic with configurable attempts and delays
12
+ - **Message Processing with Retries**: Automatic retry mechanism for failed messages with configurable retry delays
13
+ - **Dead Letter Queue (DLQ) Support**: Failed messages automatically move to DLQ after exhausting retry attempts
14
+ - **Isolated Queues**: Each processor maintains its own queue and DLQ, ensuring complete isolation between services
15
+ - **Schema Validation**: Optional message validation using JSON Schema (AJV)
16
+ - **Concurrent Processing**: Support for multiple concurrent consumers per queue
17
+ - **Correlation ID Support**: Built-in correlation ID generation and tracking for distributed tracing
18
+ - **Custom Logging**: Pluggable logging interface with default console logger
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install runmq
24
+ ```
25
+
26
+ ## Architecture Overview
27
+
28
+ RunMQ can be used to implement various messaging patterns. Here are two common architectures:
29
+
30
+ ### 1. Event-Driven Architecture (Event Bus Pattern)
31
+
32
+ In this pattern, multiple processors (or services) subscribe to the same event topic. Each processor gets its own isolated queue and DLQ, enabling true microservices autonomy.
33
+
34
+ ```
35
+ Publisher → Topic (user.created)
36
+ ├→ Queue: emailService → DLQ: emailService_dlq
37
+ ├→ Queue: analyticsService → DLQ: analyticsService_dlq
38
+ └→ Queue: notificationService → DLQ: notificationService_dlq
39
+ ```
40
+
41
+ **Key Benefits:**
42
+ - Services remain independent and isolated
43
+ - Each service can fail/retry without affecting others
44
+ - Easy to add new services by subscribing to existing events
45
+ - Natural implementation of CQRS and event sourcing patterns
46
+
47
+ ### 2. Background Processing Pattern
48
+
49
+ RunMQ can also be used as a job queue for background processing tasks. A single worker service processes jobs from a dedicated queue with retries and DLQ support.
50
+
51
+ ```
52
+ Publisher → Topic (email.send) → Queue: emailWorker → DLQ: emailWorker_dlq
53
+ ```
54
+
55
+ **Key Benefits:**
56
+ - Simple async job processing
57
+ - Automatic retries for failed jobs
58
+ - Scalable with multiple concurrent workers
59
+ - Dead letter queue for failed job analysis
60
+
61
+ ## Quick Start
62
+
63
+ ### Basic Setup
64
+
65
+ ```typescript
66
+ import { RunMQ } from 'runmq';
67
+
68
+ // 1. Initialize RunMQ
69
+ const runMQ = await RunMQ.start({
70
+ url: 'amqp://localhost:5672',
71
+ reconnectDelay: 5000, // optional, default: 5000ms
72
+ maxReconnectAttempts: 5 // optional, default: 5
73
+ });
74
+
75
+ // 2. Process messages (create a consumer)
76
+ await runMQ.process('user.created', {
77
+ name: 'emailService', // Unique processor name (creates isolated queue)
78
+ consumersCount: 2, // Number of concurrent workers
79
+ attempts: 3, // Try processing a message up to 3 times
80
+ attemptsDelay: 2000 // Wait 2 seconds between retries
81
+ }, async (message) => {
82
+ // Your processing logic here
83
+ console.log('Received:', message.message);
84
+ await sendEmail(message.message);
85
+ });
86
+
87
+ // 3. Publish messages
88
+ runMQ.publish('user.created', {
89
+ userId: '123',
90
+ email: 'user@example.com',
91
+ name: 'John Doe'
92
+ });
93
+
94
+ // That's it! The message will be delivered to all processors subscribed to 'user.created'
95
+ ```
96
+
97
+ ## Event-Driven Architecture Example
98
+
99
+ One of the most powerful patterns with RunMQ is the Event Bus pattern, where multiple services independently react to the same events.
100
+ The main advantage is that each service has its own isolated queue and dead letter queue, allowing for true microservices autonomy
101
+ Publishing a single message (event) results in multiple services receiving and processing it independently.
102
+
103
+ ### Scenario: User Registration System
104
+
105
+ When a user registers, multiple services need to react independently.
106
+
107
+ ```typescript
108
+ import { RunMQ, RunMQMessage } from 'runmq';
109
+
110
+ interface UserCreatedEvent {
111
+ userId: string;
112
+ email: string;
113
+ name: string;
114
+ createdAt: string;
115
+ }
116
+
117
+ // Initialize RunMQ in each service
118
+ const runMQ = await RunMQ.start({
119
+ url: 'amqp://localhost:5672'
120
+ });
121
+
122
+ // ============================================
123
+ // SERVICE 1: Email Service
124
+ // ============================================
125
+ await runMQ.process<UserCreatedEvent>('user.created', {
126
+ name: 'emailService', // Creates queue: emailService
127
+ consumersCount: 2,
128
+ attempts: 3,
129
+ attemptsDelay: 2000
130
+ }, async (message: RunMQMessage<UserCreatedEvent>) => {
131
+ console.log(`[Email Service] Sending welcome email to ${message.message.email}`);
132
+ await sendWelcomeEmail(message.message);
133
+ });
134
+
135
+ // ============================================
136
+ // SERVICE 2: Analytics Service
137
+ // ============================================
138
+ await runMQ.process<UserCreatedEvent>('user.created', {
139
+ name: 'analyticsService', // Creates queue: analyticsService
140
+ consumersCount: 1,
141
+ attempts: 3
142
+ }, async (message: RunMQMessage<UserCreatedEvent>) => {
143
+ console.log(`[Analytics] Recording user registration for ${message.message.userId}`);
144
+ await trackUserRegistration(message.message);
145
+ });
146
+
147
+ // ============================================
148
+ // SERVICE 3: Notification Service
149
+ // ============================================
150
+ await runMQ.process<UserCreatedEvent>('user.created', {
151
+ name: 'notificationService', // Creates queue: notificationService
152
+ consumersCount: 3,
153
+ attempts: 5,
154
+ attemptsDelay: 1000
155
+ }, async (message: RunMQMessage<UserCreatedEvent>) => {
156
+ console.log(`[Notifications] Sending push notification to ${message.message.userId}`);
157
+ await sendPushNotification(message.message);
158
+ });
159
+
160
+ // ============================================
161
+ // PUBLISHER: User Registration Handler
162
+ // ============================================
163
+ // When a user registers, publish one event
164
+ runMQ.publish('user.created', {
165
+ userId: 'user-123',
166
+ email: 'john@example.com',
167
+ name: 'John Doe',
168
+ createdAt: new Date().toISOString()
169
+ });
170
+
171
+ // All three services receive the event independently!
172
+ ```
173
+
174
+ ### Adding a New Processor
175
+
176
+ Want to add a new service? Just subscribe to existing events:
177
+
178
+ ```typescript
179
+ // NEW SERVICE 4: CRM Sync Service
180
+ await runMQ.process<UserCreatedEvent>('user.created', {
181
+ name: 'crmSyncService', // Creates new isolated queue
182
+ consumersCount: 1,
183
+ attempts: 3
184
+ }, async (message: RunMQMessage<UserCreatedEvent>) => {
185
+ console.log(`[CRM] Syncing user to CRM: ${message.message.userId}`);
186
+ await syncToCRM(message.message);
187
+ });
188
+
189
+ // This new service automatically receives all future user.created events
190
+ // No changes needed to existing services!
191
+ ```
192
+
193
+ ## Job Queue Pattern Example
194
+
195
+ ### Scenario: Background Email Processing
196
+
197
+ Use RunMQ for async job processing with a single worker service.
198
+
199
+ ```typescript
200
+ import { RunMQ, RunMQMessage } from 'runmq';
201
+
202
+ interface EmailJob {
203
+ to: string;
204
+ subject: string;
205
+ body: string;
206
+ attachments?: string[];
207
+ }
208
+
209
+ const runMQ = await RunMQ.start({
210
+ url: 'amqp://localhost:5672'
211
+ });
212
+
213
+ // ============================================
214
+ // WORKER: Email Processing Service
215
+ // ============================================
216
+ await runMQ.process<EmailJob>('email.send', {
217
+ name: 'emailWorker', // Single queue for job processing
218
+ consumersCount: 5, // 5 concurrent workers
219
+ attempts: 3,
220
+ attemptsDelay: 5000,
221
+ messageSchema: {
222
+ type: 'ajv',
223
+ schema: {
224
+ type: 'object',
225
+ properties: {
226
+ to: { type: 'string', format: 'email' },
227
+ subject: { type: 'string' },
228
+ body: { type: 'string' },
229
+ attachments: {
230
+ type: 'array',
231
+ items: { type: 'string' }
232
+ }
233
+ },
234
+ required: ['to', 'subject', 'body']
235
+ },
236
+ failureStrategy: 'dlq'
237
+ }
238
+ }, async (message: RunMQMessage<EmailJob>) => {
239
+ console.log(`[Worker] Sending email to ${message.message.to}`);
240
+
241
+ await sendEmail({
242
+ to: message.message.to,
243
+ subject: message.message.subject,
244
+ body: message.message.body,
245
+ attachments: message.message.attachments
246
+ });
247
+
248
+ console.log(`[Worker] Email sent successfully to ${message.message.to}`);
249
+ });
250
+
251
+ // ============================================
252
+ // PUBLISHER: API Endpoint
253
+ // ============================================
254
+ // Your API can now queue emails for background processing
255
+ app.post('/api/send-email', async (req, res) => {
256
+ const { to, subject, body } = req.body;
257
+
258
+ // Queue the job - returns immediately
259
+ runMQ.publish('email.send', {
260
+ to,
261
+ subject,
262
+ body,
263
+ attachments: []
264
+ });
265
+
266
+ res.json({ status: 'queued' });
267
+ });
268
+ ```
269
+
270
+ ### Job Processing Flow
271
+
272
+ ```
273
+ API Request → Publish Job → Queue (emailWorker)
274
+
275
+ 5 Concurrent Workers
276
+
277
+ [Success] or [Try processing for 3 times]
278
+
279
+ [Final Failure] → DLQ (emailWorker_dlq)
280
+ ```
281
+
282
+ ## Advanced Examples
283
+
284
+ ### Event Choreography with Multiple Events
285
+
286
+ Build complex workflows by publishing new events from processors:
287
+
288
+ ```typescript
289
+ // Order Service - publishes order.placed
290
+ await runMQ.process('order.placed', {
291
+ name: 'paymentService',
292
+ consumersCount: 2,
293
+ attempts: 3
294
+ }, async (message) => {
295
+ const payment = await processPayment(message.message);
296
+
297
+ if (payment.success) {
298
+ // Trigger next event in the workflow
299
+ runMQ.publish('payment.completed', {
300
+ orderId: message.message.orderId,
301
+ paymentId: payment.id,
302
+ amount: payment.amount
303
+ }, message.meta.correlationId); // Preserve correlation ID
304
+ }
305
+ });
306
+
307
+ // Inventory Service - reacts to payment.completed
308
+ await runMQ.process('payment.completed', {
309
+ name: 'inventoryService',
310
+ consumersCount: 3,
311
+ attempts: 5
312
+ }, async (message) => {
313
+ await reserveInventory(message.message.orderId);
314
+
315
+ // Trigger next step
316
+ runMQ.publish('inventory.reserved', {
317
+ orderId: message.message.orderId
318
+ }, message.meta.correlationId);
319
+ });
320
+
321
+ // Shipping Service - reacts to inventory.reserved
322
+ await runMQ.process('inventory.reserved', {
323
+ name: 'shippingService',
324
+ consumersCount: 2,
325
+ attempts: 3
326
+ }, async (message) => {
327
+ await scheduleShipment(message.message.orderId);
328
+
329
+ runMQ.publish('order.fulfilled', {
330
+ orderId: message.message.orderId,
331
+ fulfilledAt: new Date().toISOString()
332
+ }, message.meta.correlationId);
333
+ });
334
+ ```
335
+
336
+ ### Schema Validation
337
+
338
+ RunMQ supports JSON schema validation to ensure message integrity, so only valid messages are passed to your processors
339
+ Currently, only AJV is supported for schema validation, with a single failure strategy of sending invalid messages to the DLQ in the meantime.
340
+ if the schema validation fails, the message is sent directly to the DLQ without being processed.
341
+
342
+ ```typescript
343
+ const orderSchema = {
344
+ type: 'object',
345
+ properties: {
346
+ orderId: { type: 'string', pattern: '^ORD-[0-9]+$' },
347
+ customerId: { type: 'string' },
348
+ items: {
349
+ type: 'array',
350
+ minItems: 1,
351
+ items: {
352
+ type: 'object',
353
+ properties: {
354
+ sku: { type: 'string' },
355
+ quantity: { type: 'number', minimum: 1 },
356
+ price: { type: 'number', minimum: 0 }
357
+ },
358
+ required: ['sku', 'quantity', 'price']
359
+ }
360
+ },
361
+ total: { type: 'number', minimum: 0 }
362
+ },
363
+ required: ['orderId', 'customerId', 'items', 'total']
364
+ };
365
+
366
+ await runMQ.process('order.placed', {
367
+ name: 'orderProcessor',
368
+ consumersCount: 3,
369
+ attempts: 3,
370
+ messageSchema: {
371
+ type: 'ajv',
372
+ schema: orderSchema,
373
+ failureStrategy: 'dlq' // Invalid messages go straight to DLQ
374
+ }
375
+ }, async (message) => {
376
+ // Message is guaranteed to be valid
377
+ await processOrder(message.message);
378
+ });
379
+ ```
380
+
381
+ ## Configuration
382
+
383
+ ### Connection Configuration
384
+
385
+ ```typescript
386
+ interface RunMQConnectionConfig {
387
+ url: string; // The URL of the RabbitMQ server.
388
+ reconnectDelay?: number; // The delay in milliseconds before attempting to reconnect after a disconnection (default: 5000)
389
+ maxReconnectAttempts?: number; // Maximum reconnection attempts (default: 5)
390
+ }
391
+ ```
392
+
393
+ ### Processor Configuration
394
+
395
+ ```typescript
396
+ interface RunMQProcessorConfiguration {
397
+ name: string; // The name of the processor, used to create isolated queues for each processor.
398
+ consumersCount: number; // The number of concurrent consumers to run for this processor.
399
+ attempts?: number; // The maximum number attempts processing a message, default is 1 attempt.
400
+ attemptsDelay?: number; // The delay in milliseconds between attempts.
401
+ messageSchema?: MessageSchema; // The schema configuration for message validation.
402
+ }
403
+ ```
404
+
405
+ ### Message Schema Configuration
406
+
407
+ ```typescript
408
+ interface MessageSchema {
409
+ type: 'ajv'; // The type of schema used for validation (Currently only 'ajv').
410
+ schema: any; // The schema definition of the chosen schemaType, used for validating messages.
411
+ failureStrategy: 'dlq'; // The strategy to apply when schema validation fails (e.g., 'dlq').
412
+ }
413
+ ```
414
+
415
+ ## Message Structure
416
+
417
+ ```typescript
418
+ interface RunMQMessageContent<T> {
419
+ message: T; // Your message payload
420
+ meta: {
421
+ id: string; // The unique identifier of the message.
422
+ publishedAt: number; // The timestamp when the message was published.
423
+ correlationId: string; // The correlation identifier.
424
+ }
425
+ }
426
+ ```
427
+
428
+ ## Queue Isolation and Naming
429
+
430
+ **Important:** Each processor creates an isolated queue based on its `name` parameter:
431
+
432
+ - Queue name: `{processor.name}`
433
+ - DLQ name: `{processor.name}_dlq`
434
+
435
+ This ensures:
436
+ - ✅ Processors can't interfere with each other
437
+ - ✅ Each processor controls its own retry logic
438
+ - ✅ Failed messages are isolated per processor
439
+ - ✅ Easy to monitor and debug per-processor queues
440
+
441
+ Example:
442
+ ```typescript
443
+ // Creates queue: userEmailService and userEmailService_dlq
444
+ await runMQ.process('user.created', { name: 'userEmailService', ... }, handler);
445
+
446
+ // Creates queue: userAnalytics and userAnalytics_dlq
447
+ await runMQ.process('user.created', { name: 'userAnalytics', ... }, handler);
448
+ ```
449
+
450
+ ## Custom Logger
451
+
452
+ ```typescript
453
+ import { RunMQLogger } from 'runmq';
454
+
455
+ class CustomLogger implements RunMQLogger {
456
+ log(message: string): void {
457
+ // Your logging implementation
458
+ }
459
+
460
+ error(message: string, error?: any): void {
461
+ // Your error logging implementation
462
+ }
463
+ }
464
+
465
+ const runMQ = await RunMQ.start(config, new CustomLogger());
466
+ ```
467
+
468
+ ## License
469
+
470
+ MIT