@linagora/rabbitmq-client 0.2.0 → 0.2.2
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 +11 -1
- package/dist/index.cjs +167 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +167 -60
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,8 @@ Only `url` is required. Everything else has defaults.
|
|
|
37
37
|
| `connectionRetryDelay` | `5000` | Milliseconds between reconnection attempts |
|
|
38
38
|
| `initMaxAttempts` | `5` | Connection attempts on startup before giving up |
|
|
39
39
|
| `publishMaxAttempts` | `5` | Publish retries (exponential backoff, capped at 60s) |
|
|
40
|
-
| `prefetch` | `10` | Channel prefetch
|
|
40
|
+
| `prefetch` | `10` | Channel prefetch: how many messages the broker delivers into consumer memory before waiting for acks |
|
|
41
|
+
| `concurrency` | = `prefetch` | How many handlers run at once per subscription. Prefetch buffers messages in memory; concurrency bounds how many are processed simultaneously. Only throttles when set below `prefetch` (the broker never delivers more than `prefetch` unacked at a time); `0` means no limit. Override per queue via `subscribe(..., { concurrency })` |
|
|
41
42
|
| `closeTimeout` | `5000` | Milliseconds to wait for in-flight messages when closing |
|
|
42
43
|
| `logger` | console (warn/error) | Any `ILogger` (`console`, Winston, pino, Bunyan). Omitted: logs warn/error to the console, stays quiet otherwise. Pass `silentLogger` for no output. |
|
|
43
44
|
| `hooks` | -- | Observability callbacks (see [Hooks](#hooks)) |
|
|
@@ -79,6 +80,15 @@ await client.subscribe('events', 'order.placed', 'order-queue', handler, {
|
|
|
79
80
|
|
|
80
81
|
Custom arguments are merged with the DLQ wiring defaults, so you can swap the queue type or add a max-length without losing dead-letter routing.
|
|
81
82
|
|
|
83
|
+
To limit how many messages this queue processes at once (for example, to protect a downstream database from a burst), set `concurrency` on the subscription. It defaults to the client-level `concurrency`, which in turn defaults to `prefetch`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Buffer up to 50 messages in memory, but process at most 4 at a time.
|
|
87
|
+
await client.subscribe('events', 'order.placed', 'order-queue', handler, {
|
|
88
|
+
concurrency: 4,
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
82
92
|
### Unsubscribing
|
|
83
93
|
|
|
84
94
|
Cancel a consumer and remove it from the auto-restoration list:
|
package/dist/index.cjs
CHANGED
|
@@ -59,6 +59,52 @@ var silentLogger = {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
// src/semaphore.ts
|
|
63
|
+
var Semaphore = class {
|
|
64
|
+
permits;
|
|
65
|
+
waiters = [];
|
|
66
|
+
constructor(permits) {
|
|
67
|
+
if (!Number.isInteger(permits) || permits < 1) {
|
|
68
|
+
throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`);
|
|
69
|
+
}
|
|
70
|
+
this.permits = permits;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Acquires a permit, resolving immediately when one is free or queueing
|
|
74
|
+
* (FIFO) until another holder releases. Always pair with `release()` in a
|
|
75
|
+
* `finally` so a throwing handler never leaks a permit.
|
|
76
|
+
*/
|
|
77
|
+
acquire() {
|
|
78
|
+
if (this.permits > 0) {
|
|
79
|
+
this.permits--;
|
|
80
|
+
return Promise.resolve();
|
|
81
|
+
}
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
this.waiters.push(resolve);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Releases a permit. Hands it directly to the next waiter if any are queued,
|
|
88
|
+
* otherwise returns it to the pool.
|
|
89
|
+
*/
|
|
90
|
+
release() {
|
|
91
|
+
const next = this.waiters.shift();
|
|
92
|
+
if (next) {
|
|
93
|
+
next();
|
|
94
|
+
} else {
|
|
95
|
+
this.permits++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Permits currently available. Exposed for tests and introspection. */
|
|
99
|
+
get available() {
|
|
100
|
+
return this.permits;
|
|
101
|
+
}
|
|
102
|
+
/** Number of callers currently queued waiting for a permit. */
|
|
103
|
+
get waiting() {
|
|
104
|
+
return this.waiters.length;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
62
108
|
// src/client.ts
|
|
63
109
|
var DEFAULT_PREFETCH = 10;
|
|
64
110
|
var MAX_PUBLISH_RETRY_DELAY_MS = 6e4;
|
|
@@ -83,6 +129,11 @@ var RabbitMQClient = class {
|
|
|
83
129
|
reconnectionPromise = null;
|
|
84
130
|
assertedExchanges = /* @__PURE__ */ new Set();
|
|
85
131
|
consumerTags = /* @__PURE__ */ new Map();
|
|
132
|
+
// Per-queue concurrency limiter, keyed by queue name. Persisted across
|
|
133
|
+
// reconnects (unlike consumerTags) so handlers still running when a
|
|
134
|
+
// connection drops keep holding their permits, and the new consumer cannot
|
|
135
|
+
// exceed the ceiling while the old pipeline drains. `null` = no limit.
|
|
136
|
+
subscriptionSemaphores = /* @__PURE__ */ new Map();
|
|
86
137
|
inflightCount = 0;
|
|
87
138
|
drainResolve = null;
|
|
88
139
|
constructor(options) {
|
|
@@ -94,6 +145,11 @@ var RabbitMQClient = class {
|
|
|
94
145
|
initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,
|
|
95
146
|
publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,
|
|
96
147
|
prefetch: options.prefetch ?? DEFAULTS.prefetch,
|
|
148
|
+
// Default concurrency tracks prefetch, preserving the previous behaviour
|
|
149
|
+
// (handlers ran fire-and-forget, so up to `prefetch` ran at once) while
|
|
150
|
+
// making the ceiling explicit and independently tunable. A value <= 0
|
|
151
|
+
// (including an unlimited `prefetch: 0`) means "no concurrency limit".
|
|
152
|
+
concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,
|
|
97
153
|
closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout
|
|
98
154
|
};
|
|
99
155
|
this.logger = options.logger ?? defaultLogger;
|
|
@@ -260,6 +316,7 @@ var RabbitMQClient = class {
|
|
|
260
316
|
this.consumerTags.clear();
|
|
261
317
|
if (clearSubscriptions) {
|
|
262
318
|
this.subscriptions = [];
|
|
319
|
+
this.subscriptionSemaphores.clear();
|
|
263
320
|
}
|
|
264
321
|
this.logger.info("Connection closed");
|
|
265
322
|
} catch (error) {
|
|
@@ -332,6 +389,7 @@ var RabbitMQClient = class {
|
|
|
332
389
|
this.subscriptions.push(sub);
|
|
333
390
|
} else {
|
|
334
391
|
this.subscriptions[existingIndex] = sub;
|
|
392
|
+
this.subscriptionSemaphores.delete(queue);
|
|
335
393
|
}
|
|
336
394
|
await this.setupSubscription(sub);
|
|
337
395
|
}
|
|
@@ -344,6 +402,7 @@ var RabbitMQClient = class {
|
|
|
344
402
|
await this.channel.cancel(tag);
|
|
345
403
|
}
|
|
346
404
|
this.consumerTags.delete(queue);
|
|
405
|
+
this.subscriptionSemaphores.delete(queue);
|
|
347
406
|
this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue);
|
|
348
407
|
this.logger.info("Unsubscribed from queue", { queue });
|
|
349
408
|
}
|
|
@@ -351,15 +410,16 @@ var RabbitMQClient = class {
|
|
|
351
410
|
if (!this.channel) {
|
|
352
411
|
throw new Error("Channel not available");
|
|
353
412
|
}
|
|
413
|
+
const channel = this.channel;
|
|
354
414
|
const { exchange, routingKey, queue, handler, options } = sub;
|
|
355
415
|
const dlxExchange = `${exchange}.dlx`;
|
|
356
416
|
const dlqQueue = `${queue}.dlq`;
|
|
357
417
|
const dlqRoutingKey = `${routingKey}.dead`;
|
|
358
|
-
await
|
|
359
|
-
await
|
|
360
|
-
await
|
|
418
|
+
await channel.assertExchange(dlxExchange, "topic", { durable: true });
|
|
419
|
+
await channel.assertQueue(dlqQueue, { durable: true });
|
|
420
|
+
await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
|
|
361
421
|
if (!this.assertedExchanges.has(exchange)) {
|
|
362
|
-
await
|
|
422
|
+
await channel.assertExchange(exchange, "topic", { durable: true });
|
|
363
423
|
this.assertedExchanges.add(exchange);
|
|
364
424
|
}
|
|
365
425
|
const queueType = options?.queueArguments?.["x-queue-type"] ?? "quorum";
|
|
@@ -370,20 +430,23 @@ var RabbitMQClient = class {
|
|
|
370
430
|
if (queueType === "quorum") {
|
|
371
431
|
queueArgs["x-dead-letter-strategy"] = "at-least-once";
|
|
372
432
|
}
|
|
373
|
-
await
|
|
433
|
+
await channel.assertQueue(queue, {
|
|
374
434
|
durable: true,
|
|
375
435
|
deadLetterExchange: dlxExchange,
|
|
376
436
|
deadLetterRoutingKey: dlqRoutingKey,
|
|
377
437
|
arguments: { ...queueArgs, ...options?.queueArguments }
|
|
378
438
|
});
|
|
379
|
-
await
|
|
380
|
-
|
|
439
|
+
await channel.bindQueue(queue, exchange, routingKey);
|
|
440
|
+
if (!this.subscriptionSemaphores.has(queue)) {
|
|
441
|
+
const concurrency = options?.concurrency ?? this.options.concurrency;
|
|
442
|
+
this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null);
|
|
443
|
+
}
|
|
444
|
+
const semaphore = this.subscriptionSemaphores.get(queue) ?? null;
|
|
445
|
+
const { consumerTag } = await channel.consume(
|
|
381
446
|
queue,
|
|
382
447
|
(message) => {
|
|
383
448
|
if (message) {
|
|
384
|
-
this.
|
|
385
|
-
this.logger.error("Unhandled error in message handler", { error });
|
|
386
|
-
});
|
|
449
|
+
this.dispatch(message, handler, semaphore, channel);
|
|
387
450
|
}
|
|
388
451
|
},
|
|
389
452
|
{ noAck: false }
|
|
@@ -391,67 +454,111 @@ var RabbitMQClient = class {
|
|
|
391
454
|
this.consumerTags.set(queue, consumerTag);
|
|
392
455
|
this.logger.info("Subscribed to queue", { exchange, routingKey, queue });
|
|
393
456
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
459
|
+
* processes it. A message counts as in-flight from delivery until its handler
|
|
460
|
+
* settles (including time spent waiting for a permit), so graceful `close()`
|
|
461
|
+
* drains queued messages as well as actively-processing ones.
|
|
462
|
+
*/
|
|
463
|
+
dispatch(message, handler, semaphore, channel) {
|
|
464
|
+
this.inflightCount++;
|
|
465
|
+
const acquire = semaphore ? semaphore.acquire() : Promise.resolve();
|
|
466
|
+
acquire.then(() => this.handleWithRetry(message, handler, channel)).catch((error) => {
|
|
467
|
+
this.logger.error("Unhandled error in message handler", { error });
|
|
468
|
+
}).finally(() => {
|
|
469
|
+
semaphore?.release();
|
|
470
|
+
this.inflightCount--;
|
|
471
|
+
if (this.inflightCount === 0 && this.drainResolve) {
|
|
472
|
+
this.drainResolve();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async handleWithRetry(message, handler, channel) {
|
|
477
|
+
if (channel !== this.channel) {
|
|
478
|
+
this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
|
|
398
479
|
return;
|
|
399
480
|
}
|
|
400
|
-
|
|
481
|
+
const startTime = Date.now();
|
|
482
|
+
let attempts = 0;
|
|
483
|
+
const routingKey = message.fields.routingKey;
|
|
484
|
+
const exchange = message.fields.exchange;
|
|
485
|
+
let content;
|
|
401
486
|
try {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
|
|
487
|
+
content = JSON.parse(message.content.toString());
|
|
488
|
+
} catch (parseError) {
|
|
489
|
+
const rawContent = message.content.toString();
|
|
490
|
+
const rawPreview = rawContent.substring(0, 100);
|
|
491
|
+
this.logger.error("Failed to parse message JSON, sending to DLQ", {
|
|
492
|
+
error: parseError,
|
|
493
|
+
exchange,
|
|
494
|
+
routingKey,
|
|
495
|
+
rawContentPreview: rawPreview + (rawContent.length > 100 ? "..." : "")
|
|
496
|
+
});
|
|
497
|
+
if (this.settle(channel, message, "nack")) {
|
|
498
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: "invalid_json" });
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
|
|
503
|
+
while (attempts < this.options.maxRetries) {
|
|
407
504
|
try {
|
|
408
|
-
|
|
409
|
-
} catch (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
error:
|
|
505
|
+
await handler(content);
|
|
506
|
+
} catch (error) {
|
|
507
|
+
attempts++;
|
|
508
|
+
this.logger.error("Handler failed", {
|
|
509
|
+
error: error instanceof Error ? error.message : error,
|
|
510
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
414
511
|
exchange,
|
|
415
512
|
routingKey,
|
|
416
|
-
|
|
513
|
+
attempt: attempts,
|
|
514
|
+
maxRetries: this.options.maxRetries
|
|
417
515
|
});
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
|
|
423
|
-
while (attempts < this.options.maxRetries) {
|
|
424
|
-
try {
|
|
425
|
-
await handler(content);
|
|
426
|
-
const duration2 = Date.now() - startTime;
|
|
427
|
-
this.logger.info("Message processed successfully", { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
428
|
-
channel.ack(message);
|
|
429
|
-
this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
430
|
-
return;
|
|
431
|
-
} catch (error) {
|
|
432
|
-
attempts++;
|
|
433
|
-
this.logger.error("Handler failed", {
|
|
434
|
-
error: error instanceof Error ? error.message : error,
|
|
435
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
436
|
-
exchange,
|
|
437
|
-
routingKey,
|
|
438
|
-
attempt: attempts,
|
|
439
|
-
maxRetries: this.options.maxRetries
|
|
440
|
-
});
|
|
441
|
-
if (attempts < this.options.maxRetries) {
|
|
442
|
-
await this.sleep(this.options.retryDelay);
|
|
443
|
-
}
|
|
516
|
+
if (attempts < this.options.maxRetries) {
|
|
517
|
+
await this.sleep(this.options.retryDelay);
|
|
444
518
|
}
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const duration2 = Date.now() - startTime;
|
|
522
|
+
this.logger.info("Message processed successfully", { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
523
|
+
if (this.settle(channel, message, "ack")) {
|
|
524
|
+
this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
445
525
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const duration = Date.now() - startTime;
|
|
529
|
+
this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries: this.options.maxRetries, duration });
|
|
530
|
+
if (this.settle(channel, message, "nack")) {
|
|
449
531
|
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Acks or nacks a delivery, reporting whether the broker accepted it.
|
|
536
|
+
*
|
|
537
|
+
* A settle failure is a transport error, never a handler failure: by this
|
|
538
|
+
* point the handler has already run, so it must not be retried just because
|
|
539
|
+
* the channel went away. The delivery stays unacked and the broker redelivers
|
|
540
|
+
* it once the channel closes.
|
|
541
|
+
*/
|
|
542
|
+
settle(channel, message, action) {
|
|
543
|
+
if (channel !== this.channel) {
|
|
544
|
+
this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
if (action === "ack") {
|
|
549
|
+
channel.ack(message);
|
|
550
|
+
} else {
|
|
551
|
+
channel.nack(message, false, false);
|
|
454
552
|
}
|
|
553
|
+
return true;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
this.logger.error("Failed to settle message; leaving it unacked for redelivery", {
|
|
556
|
+
error,
|
|
557
|
+
action,
|
|
558
|
+
exchange: message.fields.exchange,
|
|
559
|
+
routingKey: message.fields.routingKey
|
|
560
|
+
});
|
|
561
|
+
return false;
|
|
455
562
|
}
|
|
456
563
|
}
|
|
457
564
|
/**
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/logger.ts"],"sourcesContent":["export { RabbitMQClient } from './client.js'\nexport { defaultLogger, silentLogger } from './logger.js'\nexport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\n","import amqp from 'amqplib'\nimport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger } from './logger.js'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: ILogger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n private consumerTags = new Map<string, string>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = options.logger ?? defaultLogger\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n let attempts = 0\n while (!this.connected) {\n try {\n this.connection = await amqp.connect(this.options.url)\n this.logger.info('Connected to server')\n this.channel = await this.connection.createConfirmChannel()\n this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n this.connection.on('close', () => {\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n this.channel.on('error', (error: Error) => {\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n this.channel.on('close', () => {\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error('Connection failed after maximum attempts', { error, attempts, maxAttempts })\n throw new Error(\n `Failed to connect to RabbitMQ after ${attempts} attempts. ` +\n 'Check RABBITMQ_URL configuration and RabbitMQ server availability.',\n )\n }\n this.logger.warn('Connection attempt failed, retrying...', {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n })\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms.\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n this.channel.publish(exchange, routingKey, content, {\n persistent: true,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n })\n\n await this.channel.waitForConfirms()\n\n if (attempts > 0) {\n this.logger.info('Published message after retries', { exchange, routingKey, messageSize: content.length, attempts })\n } else {\n this.logger.info('Published message', { exchange, routingKey, messageSize: content.length })\n }\n this.logger.debug('Published message payload', { exchange, routingKey, payload: message })\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error('Publish failed after max attempts', { error, exchange, routingKey, attempts, maxAttempts })\n throw new Error(\n `Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = Math.min(\n baseDelay * Math.pow(2, attempts - 1),\n MAX_PUBLISH_RETRY_DELAY_MS,\n )\n\n this.logger.warn('Publish attempt failed, retrying', { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay })\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n try {\n if (this.inflightCount > 0) {\n this.logger.info('Waiting for in-flight messages to drain', { inflightCount: this.inflightCount })\n await this.waitForDrain(this.options.closeTimeout)\n }\n await this.channel?.close()\n await this.connection?.close()\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error('Error closing connection', { error })\n throw error\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info('Re-establishing subscriptions', { count: this.subscriptions.length })\n const subs = [...this.subscriptions]\n const results = await Promise.allSettled(\n subs.map(async (sub) => {\n await this.setupSubscription(sub)\n return sub.queue\n }),\n )\n const succeeded: string[] = []\n const failed: string[] = []\n results.forEach((result, index) => {\n if (result.status === 'fulfilled') {\n succeeded.push(result.value)\n } else {\n const queue = subs[index].queue\n failed.push(queue)\n this.logger.error('Failed to re-subscribe to queue', { error: result.reason, queue })\n }\n })\n if (succeeded.length > 0) {\n this.logger.info('Successfully re-subscribed to queues', { count: succeeded.length, queues: succeeded })\n }\n if (failed.length > 0) {\n this.logger.warn('Some subscriptions failed to restore', { count: failed.length, queues: failed })\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n }\n await this.setupSubscription(sub)\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info('Unsubscribed from queue', { queue })\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n const dlqRoutingKey = `${routingKey}.dead`\n\n await this.channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await this.channel.assertQueue(dlqQueue, { durable: true })\n await this.channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await this.channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n await this.channel.bindQueue(queue, exchange, routingKey)\n const { consumerTag } = await this.channel.consume(\n queue,\n (message) => {\n if (message) {\n this.handleWithRetry(message, handler).catch((error) => {\n this.logger.error('Unhandled error in message handler', { error })\n })\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { exchange, routingKey, queue })\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n ): Promise<void> {\n // Capture the channel reference so ack/nack always targets the channel\n // that delivered this message, even if a reconnection swaps this.channel.\n const channel = this.channel\n if (!channel) {\n this.logger.warn('Cannot process message, channel unavailable')\n return\n }\n this.inflightCount++\n try {\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error('Failed to parse message JSON, sending to DLQ', {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n })\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n return\n }\n\n this.logger.debug('Message received, processing', { exchange, routingKey, payload: content })\n\n while (attempts < this.options.maxRetries) {\n try {\n await handler(content)\n const duration = Date.now() - startTime\n this.logger.info('Message processed successfully', { exchange, routingKey, duration, attempts: attempts + 1 })\n channel.ack(message)\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n return\n } catch (error) {\n attempts++\n this.logger.error('Handler failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n exchange,\n routingKey,\n attempt: attempts,\n maxRetries: this.options.maxRetries,\n })\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n }\n }\n\n const duration = Date.now() - startTime\n this.logger.error('Message failed after max retries, sending to DLQ', { exchange, routingKey, maxRetries: this.options.maxRetries, duration })\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n } finally {\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n }\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn('Health check failed', { error })\n return false\n }\n }\n\n private waitForDrain(timeout: number): Promise<void> {\n if (this.inflightCount === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.logger.warn('Close timeout reached with messages still in flight', { inflightCount: this.inflightCount })\n this.drainResolve = null\n resolve()\n }, timeout)\n this.drainResolve = () => {\n clearTimeout(timer)\n this.drainResolve = null\n resolve()\n }\n })\n }\n\n /** Invokes a hook callback, swallowing errors so hooks never break message flow. */\n private callHook<T>(hook: ((info: T) => void) | undefined, info: T): void {\n try { hook?.(info) } catch (error) { this.logger.debug('Observability hook error', { error }) }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import type { ILogger } from './types.js'\n\n/**\n * Default logger used when the consumer does not inject one.\n *\n * Forwards `warn` and `error` to the console so connection drops, channel\n * errors, and failed handlers stay visible out of the box, while staying\n * silent for `info`/`debug` so routine operation never pollutes the\n * consumer's output. Pass `silentLogger` for zero output, or any `ILogger`\n * (console, Winston, pino, Bunyan) to take full control.\n */\nexport const defaultLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: (message, ...args) => console.warn('[rabbitmq-client]', message, ...args),\n error: (message, ...args) => console.error('[rabbitmq-client]', message, ...args),\n}\n\n/**\n * No-op logger that discards every message. Use this to silence the client\n * entirely, including the `warn`/`error` output the default logger emits.\n */\nexport const silentLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n"],"mappings":"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","names":["amqp","duration"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/logger.ts","../src/semaphore.ts"],"sourcesContent":["export { RabbitMQClient } from './client.js'\nexport { defaultLogger, silentLogger } from './logger.js'\nexport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\n","import amqp from 'amqplib'\nimport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger } from './logger.js'\nimport { Semaphore } from './semaphore.js'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: ILogger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n private consumerTags = new Map<string, string>()\n // Per-queue concurrency limiter, keyed by queue name. Persisted across\n // reconnects (unlike consumerTags) so handlers still running when a\n // connection drops keep holding their permits, and the new consumer cannot\n // exceed the ceiling while the old pipeline drains. `null` = no limit.\n private subscriptionSemaphores = new Map<string, Semaphore | null>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n // Default concurrency tracks prefetch, preserving the previous behaviour\n // (handlers ran fire-and-forget, so up to `prefetch` ran at once) while\n // making the ceiling explicit and independently tunable. A value <= 0\n // (including an unlimited `prefetch: 0`) means \"no concurrency limit\".\n concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = options.logger ?? defaultLogger\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n let attempts = 0\n while (!this.connected) {\n try {\n this.connection = await amqp.connect(this.options.url)\n this.logger.info('Connected to server')\n this.channel = await this.connection.createConfirmChannel()\n this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n this.connection.on('close', () => {\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n this.channel.on('error', (error: Error) => {\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n this.channel.on('close', () => {\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error('Connection failed after maximum attempts', { error, attempts, maxAttempts })\n throw new Error(\n `Failed to connect to RabbitMQ after ${attempts} attempts. ` +\n 'Check RABBITMQ_URL configuration and RabbitMQ server availability.',\n )\n }\n this.logger.warn('Connection attempt failed, retrying...', {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n })\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms.\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n this.channel.publish(exchange, routingKey, content, {\n persistent: true,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n })\n\n await this.channel.waitForConfirms()\n\n if (attempts > 0) {\n this.logger.info('Published message after retries', { exchange, routingKey, messageSize: content.length, attempts })\n } else {\n this.logger.info('Published message', { exchange, routingKey, messageSize: content.length })\n }\n this.logger.debug('Published message payload', { exchange, routingKey, payload: message })\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error('Publish failed after max attempts', { error, exchange, routingKey, attempts, maxAttempts })\n throw new Error(\n `Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = Math.min(\n baseDelay * Math.pow(2, attempts - 1),\n MAX_PUBLISH_RETRY_DELAY_MS,\n )\n\n this.logger.warn('Publish attempt failed, retrying', { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay })\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n try {\n if (this.inflightCount > 0) {\n this.logger.info('Waiting for in-flight messages to drain', { inflightCount: this.inflightCount })\n await this.waitForDrain(this.options.closeTimeout)\n }\n await this.channel?.close()\n await this.connection?.close()\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n this.subscriptionSemaphores.clear()\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error('Error closing connection', { error })\n throw error\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info('Re-establishing subscriptions', { count: this.subscriptions.length })\n const subs = [...this.subscriptions]\n const results = await Promise.allSettled(\n subs.map(async (sub) => {\n await this.setupSubscription(sub)\n return sub.queue\n }),\n )\n const succeeded: string[] = []\n const failed: string[] = []\n results.forEach((result, index) => {\n if (result.status === 'fulfilled') {\n succeeded.push(result.value)\n } else {\n const queue = subs[index].queue\n failed.push(queue)\n this.logger.error('Failed to re-subscribe to queue', { error: result.reason, queue })\n }\n })\n if (succeeded.length > 0) {\n this.logger.info('Successfully re-subscribed to queues', { count: succeeded.length, queues: succeeded })\n }\n if (failed.length > 0) {\n this.logger.warn('Some subscriptions failed to restore', { count: failed.length, queues: failed })\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n // Re-subscribing may change concurrency; drop the old limiter so\n // setupSubscription rebuilds it from the new options. (A reconnect goes\n // through setupSubscription directly and keeps the existing limiter.)\n this.subscriptionSemaphores.delete(queue)\n }\n await this.setupSubscription(sub)\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptionSemaphores.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info('Unsubscribed from queue', { queue })\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n // Bind this consumer to the exact channel that will deliver its messages,\n // so ack/nack always target that channel even after a reconnect swaps\n // `this.channel`.\n const channel = this.channel\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n const dlqRoutingKey = `${routingKey}.dead`\n\n await channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await channel.assertQueue(dlqQueue, { durable: true })\n await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n await channel.bindQueue(queue, exchange, routingKey)\n // Reuse the queue's existing limiter across reconnects; only build a new\n // one the first time (or after subscribe()/unsubscribe() cleared it).\n // A concurrency <= 0 means \"no limit\" (null), matching an unlimited prefetch.\n if (!this.subscriptionSemaphores.has(queue)) {\n const concurrency = options?.concurrency ?? this.options.concurrency\n this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null)\n }\n const semaphore = this.subscriptionSemaphores.get(queue) ?? null\n const { consumerTag } = await channel.consume(\n queue,\n (message) => {\n if (message) {\n this.dispatch(message, handler, semaphore, channel)\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { exchange, routingKey, queue })\n }\n\n /**\n * Gates a delivered message on the subscription's concurrency semaphore, then\n * processes it. A message counts as in-flight from delivery until its handler\n * settles (including time spent waiting for a permit), so graceful `close()`\n * drains queued messages as well as actively-processing ones.\n */\n private dispatch(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n semaphore: Semaphore | null,\n channel: amqp.ConfirmChannel,\n ): void {\n this.inflightCount++\n const acquire = semaphore ? semaphore.acquire() : Promise.resolve()\n acquire\n .then(() => this.handleWithRetry(message, handler, channel))\n .catch((error) => {\n this.logger.error('Unhandled error in message handler', { error })\n })\n .finally(() => {\n semaphore?.release()\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n })\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n channel: amqp.ConfirmChannel,\n ): Promise<void> {\n // `channel` is the one that delivered this message. If a reconnect has\n // since replaced it, this message was never acked and the broker will\n // redeliver it on the new channel, so drop this stale attempt rather than\n // run the handler again or ack a tag the new channel does not know.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return\n }\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error('Failed to parse message JSON, sending to DLQ', {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n }\n return\n }\n\n this.logger.debug('Message received, processing', { exchange, routingKey, payload: content })\n\n while (attempts < this.options.maxRetries) {\n // Only the handler call belongs in this try. Acking inside it would make\n // a dead channel look like a failed handler and re-run its side effects.\n try {\n await handler(content)\n } catch (error) {\n attempts++\n this.logger.error('Handler failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n exchange,\n routingKey,\n attempt: attempts,\n maxRetries: this.options.maxRetries,\n })\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n continue\n }\n const duration = Date.now() - startTime\n this.logger.info('Message processed successfully', { exchange, routingKey, duration, attempts: attempts + 1 })\n if (this.settle(channel, message, 'ack')) {\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n }\n return\n }\n\n const duration = Date.now() - startTime\n this.logger.error('Message failed after max retries, sending to DLQ', { exchange, routingKey, maxRetries: this.options.maxRetries, duration })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n }\n }\n\n /**\n * Acks or nacks a delivery, reporting whether the broker accepted it.\n *\n * A settle failure is a transport error, never a handler failure: by this\n * point the handler has already run, so it must not be retried just because\n * the channel went away. The delivery stays unacked and the broker redelivers\n * it once the channel closes.\n */\n private settle(\n channel: amqp.ConfirmChannel,\n message: amqp.ConsumeMessage,\n action: 'ack' | 'nack',\n ): boolean {\n // A handler can run for minutes, so the channel that delivered this\n // message may have been replaced by a reconnect since the entry check.\n // Settling on it would throw, and replaying the delivery tag on the\n // current channel would settle an unrelated message, since tags are scoped\n // to the channel that issued them. The broker requeues a closed channel's\n // unacked deliveries, so let it be redelivered instead.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return false\n }\n try {\n if (action === 'ack') {\n channel.ack(message)\n } else {\n channel.nack(message, false, false)\n }\n return true\n } catch (error) {\n this.logger.error('Failed to settle message; leaving it unacked for redelivery', {\n error,\n action,\n exchange: message.fields.exchange,\n routingKey: message.fields.routingKey,\n })\n return false\n }\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn('Health check failed', { error })\n return false\n }\n }\n\n private waitForDrain(timeout: number): Promise<void> {\n if (this.inflightCount === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.logger.warn('Close timeout reached with messages still in flight', { inflightCount: this.inflightCount })\n this.drainResolve = null\n resolve()\n }, timeout)\n this.drainResolve = () => {\n clearTimeout(timer)\n this.drainResolve = null\n resolve()\n }\n })\n }\n\n /** Invokes a hook callback, swallowing errors so hooks never break message flow. */\n private callHook<T>(hook: ((info: T) => void) | undefined, info: T): void {\n try { hook?.(info) } catch (error) { this.logger.debug('Observability hook error', { error }) }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import type { ILogger } from './types.js'\n\n/**\n * Default logger used when the consumer does not inject one.\n *\n * Forwards `warn` and `error` to the console so connection drops, channel\n * errors, and failed handlers stay visible out of the box, while staying\n * silent for `info`/`debug` so routine operation never pollutes the\n * consumer's output. Pass `silentLogger` for zero output, or any `ILogger`\n * (console, Winston, pino, Bunyan) to take full control.\n */\nexport const defaultLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: (message, ...args) => console.warn('[rabbitmq-client]', message, ...args),\n error: (message, ...args) => console.error('[rabbitmq-client]', message, ...args),\n}\n\n/**\n * No-op logger that discards every message. Use this to silence the client\n * entirely, including the `warn`/`error` output the default logger emits.\n */\nexport const silentLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n","/**\n * Minimal FIFO counting semaphore used to cap how many message handlers run\n * concurrently, independently of channel prefetch.\n *\n * Prefetch bounds how many messages the broker delivers into consumer memory;\n * the semaphore bounds how many of those are actively being processed at once.\n * Keeping the two separate lets a consumer buffer a large prefetch window while\n * still protecting a slow downstream (a database, an HTTP API) from a burst.\n */\nexport class Semaphore {\n private permits: number\n private readonly waiters: Array<() => void> = []\n\n constructor(permits: number) {\n if (!Number.isInteger(permits) || permits < 1) {\n throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`)\n }\n this.permits = permits\n }\n\n /**\n * Acquires a permit, resolving immediately when one is free or queueing\n * (FIFO) until another holder releases. Always pair with `release()` in a\n * `finally` so a throwing handler never leaks a permit.\n */\n acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => {\n this.waiters.push(resolve)\n })\n }\n\n /**\n * Releases a permit. Hands it directly to the next waiter if any are queued,\n * otherwise returns it to the pool.\n */\n release(): void {\n const next = this.waiters.shift()\n if (next) {\n next()\n } else {\n this.permits++\n }\n }\n\n /** Permits currently available. Exposed for tests and introspection. */\n get available(): number {\n return this.permits\n }\n\n /** Number of callers currently queued waiting for a permit. */\n get waiting(): number {\n return this.waiters.length\n }\n}\n"],"mappings":"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","names":["amqp","duration"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -52,6 +52,13 @@ interface RabbitMQHooks {
|
|
|
52
52
|
interface SubscribeOptions {
|
|
53
53
|
/** Override default AMQP queue arguments (merged with DLQ wiring defaults) */
|
|
54
54
|
queueArguments?: Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* Max message handlers to run concurrently for this subscription.
|
|
57
|
+
* Overrides the client-level `concurrency` for this queue only. Prefetch
|
|
58
|
+
* bounds messages held in memory; concurrency bounds how many are processed
|
|
59
|
+
* at once, and only throttles when set below `prefetch`. `0` means no limit.
|
|
60
|
+
*/
|
|
61
|
+
concurrency?: number;
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
64
|
* Configuration options for the RabbitMQ client.
|
|
@@ -71,6 +78,15 @@ interface RabbitMQClientOptions {
|
|
|
71
78
|
publishMaxAttempts?: number;
|
|
72
79
|
/** Channel prefetch count (default: 10) */
|
|
73
80
|
prefetch?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Max message handlers to run concurrently per subscription (default: equal
|
|
83
|
+
* to `prefetch`). Bounds how many `handler` callbacks execute at once, on top
|
|
84
|
+
* of how many messages prefetch keeps buffered in memory. Only throttles when
|
|
85
|
+
* set below `prefetch`, since the broker never delivers more than `prefetch`
|
|
86
|
+
* unacked messages at a time; `0` (or a negative value) means no limit.
|
|
87
|
+
* Override per queue via `SubscribeOptions.concurrency`.
|
|
88
|
+
*/
|
|
89
|
+
concurrency?: number;
|
|
74
90
|
/** Logger satisfying the ILogger contract; defaults to console warn/error only if omitted */
|
|
75
91
|
logger?: ILogger;
|
|
76
92
|
/** Timeout in ms to wait for in-flight messages during close (default: 5000) */
|
|
@@ -131,6 +147,7 @@ declare class RabbitMQClient {
|
|
|
131
147
|
private reconnectionPromise;
|
|
132
148
|
private assertedExchanges;
|
|
133
149
|
private consumerTags;
|
|
150
|
+
private subscriptionSemaphores;
|
|
134
151
|
private inflightCount;
|
|
135
152
|
private drainResolve;
|
|
136
153
|
constructor(options: RabbitMQClientOptions);
|
|
@@ -174,7 +191,23 @@ declare class RabbitMQClient {
|
|
|
174
191
|
*/
|
|
175
192
|
unsubscribe(queue: string): Promise<void>;
|
|
176
193
|
private setupSubscription;
|
|
194
|
+
/**
|
|
195
|
+
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
196
|
+
* processes it. A message counts as in-flight from delivery until its handler
|
|
197
|
+
* settles (including time spent waiting for a permit), so graceful `close()`
|
|
198
|
+
* drains queued messages as well as actively-processing ones.
|
|
199
|
+
*/
|
|
200
|
+
private dispatch;
|
|
177
201
|
private handleWithRetry;
|
|
202
|
+
/**
|
|
203
|
+
* Acks or nacks a delivery, reporting whether the broker accepted it.
|
|
204
|
+
*
|
|
205
|
+
* A settle failure is a transport error, never a handler failure: by this
|
|
206
|
+
* point the handler has already run, so it must not be retried just because
|
|
207
|
+
* the channel went away. The delivery stays unacked and the broker redelivers
|
|
208
|
+
* it once the channel closes.
|
|
209
|
+
*/
|
|
210
|
+
private settle;
|
|
178
211
|
/**
|
|
179
212
|
* Lightweight liveness probe: creates and immediately deletes a temporary
|
|
180
213
|
* exclusive queue. Returns `false` when disconnected or on broker error.
|
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,13 @@ interface RabbitMQHooks {
|
|
|
52
52
|
interface SubscribeOptions {
|
|
53
53
|
/** Override default AMQP queue arguments (merged with DLQ wiring defaults) */
|
|
54
54
|
queueArguments?: Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* Max message handlers to run concurrently for this subscription.
|
|
57
|
+
* Overrides the client-level `concurrency` for this queue only. Prefetch
|
|
58
|
+
* bounds messages held in memory; concurrency bounds how many are processed
|
|
59
|
+
* at once, and only throttles when set below `prefetch`. `0` means no limit.
|
|
60
|
+
*/
|
|
61
|
+
concurrency?: number;
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
64
|
* Configuration options for the RabbitMQ client.
|
|
@@ -71,6 +78,15 @@ interface RabbitMQClientOptions {
|
|
|
71
78
|
publishMaxAttempts?: number;
|
|
72
79
|
/** Channel prefetch count (default: 10) */
|
|
73
80
|
prefetch?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Max message handlers to run concurrently per subscription (default: equal
|
|
83
|
+
* to `prefetch`). Bounds how many `handler` callbacks execute at once, on top
|
|
84
|
+
* of how many messages prefetch keeps buffered in memory. Only throttles when
|
|
85
|
+
* set below `prefetch`, since the broker never delivers more than `prefetch`
|
|
86
|
+
* unacked messages at a time; `0` (or a negative value) means no limit.
|
|
87
|
+
* Override per queue via `SubscribeOptions.concurrency`.
|
|
88
|
+
*/
|
|
89
|
+
concurrency?: number;
|
|
74
90
|
/** Logger satisfying the ILogger contract; defaults to console warn/error only if omitted */
|
|
75
91
|
logger?: ILogger;
|
|
76
92
|
/** Timeout in ms to wait for in-flight messages during close (default: 5000) */
|
|
@@ -131,6 +147,7 @@ declare class RabbitMQClient {
|
|
|
131
147
|
private reconnectionPromise;
|
|
132
148
|
private assertedExchanges;
|
|
133
149
|
private consumerTags;
|
|
150
|
+
private subscriptionSemaphores;
|
|
134
151
|
private inflightCount;
|
|
135
152
|
private drainResolve;
|
|
136
153
|
constructor(options: RabbitMQClientOptions);
|
|
@@ -174,7 +191,23 @@ declare class RabbitMQClient {
|
|
|
174
191
|
*/
|
|
175
192
|
unsubscribe(queue: string): Promise<void>;
|
|
176
193
|
private setupSubscription;
|
|
194
|
+
/**
|
|
195
|
+
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
196
|
+
* processes it. A message counts as in-flight from delivery until its handler
|
|
197
|
+
* settles (including time spent waiting for a permit), so graceful `close()`
|
|
198
|
+
* drains queued messages as well as actively-processing ones.
|
|
199
|
+
*/
|
|
200
|
+
private dispatch;
|
|
177
201
|
private handleWithRetry;
|
|
202
|
+
/**
|
|
203
|
+
* Acks or nacks a delivery, reporting whether the broker accepted it.
|
|
204
|
+
*
|
|
205
|
+
* A settle failure is a transport error, never a handler failure: by this
|
|
206
|
+
* point the handler has already run, so it must not be retried just because
|
|
207
|
+
* the channel went away. The delivery stays unacked and the broker redelivers
|
|
208
|
+
* it once the channel closes.
|
|
209
|
+
*/
|
|
210
|
+
private settle;
|
|
178
211
|
/**
|
|
179
212
|
* Lightweight liveness probe: creates and immediately deletes a temporary
|
|
180
213
|
* exclusive queue. Returns `false` when disconnected or on broker error.
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,52 @@ var silentLogger = {
|
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
// src/semaphore.ts
|
|
25
|
+
var Semaphore = class {
|
|
26
|
+
permits;
|
|
27
|
+
waiters = [];
|
|
28
|
+
constructor(permits) {
|
|
29
|
+
if (!Number.isInteger(permits) || permits < 1) {
|
|
30
|
+
throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`);
|
|
31
|
+
}
|
|
32
|
+
this.permits = permits;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Acquires a permit, resolving immediately when one is free or queueing
|
|
36
|
+
* (FIFO) until another holder releases. Always pair with `release()` in a
|
|
37
|
+
* `finally` so a throwing handler never leaks a permit.
|
|
38
|
+
*/
|
|
39
|
+
acquire() {
|
|
40
|
+
if (this.permits > 0) {
|
|
41
|
+
this.permits--;
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
this.waiters.push(resolve);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Releases a permit. Hands it directly to the next waiter if any are queued,
|
|
50
|
+
* otherwise returns it to the pool.
|
|
51
|
+
*/
|
|
52
|
+
release() {
|
|
53
|
+
const next = this.waiters.shift();
|
|
54
|
+
if (next) {
|
|
55
|
+
next();
|
|
56
|
+
} else {
|
|
57
|
+
this.permits++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Permits currently available. Exposed for tests and introspection. */
|
|
61
|
+
get available() {
|
|
62
|
+
return this.permits;
|
|
63
|
+
}
|
|
64
|
+
/** Number of callers currently queued waiting for a permit. */
|
|
65
|
+
get waiting() {
|
|
66
|
+
return this.waiters.length;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
24
70
|
// src/client.ts
|
|
25
71
|
var DEFAULT_PREFETCH = 10;
|
|
26
72
|
var MAX_PUBLISH_RETRY_DELAY_MS = 6e4;
|
|
@@ -45,6 +91,11 @@ var RabbitMQClient = class {
|
|
|
45
91
|
reconnectionPromise = null;
|
|
46
92
|
assertedExchanges = /* @__PURE__ */ new Set();
|
|
47
93
|
consumerTags = /* @__PURE__ */ new Map();
|
|
94
|
+
// Per-queue concurrency limiter, keyed by queue name. Persisted across
|
|
95
|
+
// reconnects (unlike consumerTags) so handlers still running when a
|
|
96
|
+
// connection drops keep holding their permits, and the new consumer cannot
|
|
97
|
+
// exceed the ceiling while the old pipeline drains. `null` = no limit.
|
|
98
|
+
subscriptionSemaphores = /* @__PURE__ */ new Map();
|
|
48
99
|
inflightCount = 0;
|
|
49
100
|
drainResolve = null;
|
|
50
101
|
constructor(options) {
|
|
@@ -56,6 +107,11 @@ var RabbitMQClient = class {
|
|
|
56
107
|
initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,
|
|
57
108
|
publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,
|
|
58
109
|
prefetch: options.prefetch ?? DEFAULTS.prefetch,
|
|
110
|
+
// Default concurrency tracks prefetch, preserving the previous behaviour
|
|
111
|
+
// (handlers ran fire-and-forget, so up to `prefetch` ran at once) while
|
|
112
|
+
// making the ceiling explicit and independently tunable. A value <= 0
|
|
113
|
+
// (including an unlimited `prefetch: 0`) means "no concurrency limit".
|
|
114
|
+
concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,
|
|
59
115
|
closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout
|
|
60
116
|
};
|
|
61
117
|
this.logger = options.logger ?? defaultLogger;
|
|
@@ -222,6 +278,7 @@ var RabbitMQClient = class {
|
|
|
222
278
|
this.consumerTags.clear();
|
|
223
279
|
if (clearSubscriptions) {
|
|
224
280
|
this.subscriptions = [];
|
|
281
|
+
this.subscriptionSemaphores.clear();
|
|
225
282
|
}
|
|
226
283
|
this.logger.info("Connection closed");
|
|
227
284
|
} catch (error) {
|
|
@@ -294,6 +351,7 @@ var RabbitMQClient = class {
|
|
|
294
351
|
this.subscriptions.push(sub);
|
|
295
352
|
} else {
|
|
296
353
|
this.subscriptions[existingIndex] = sub;
|
|
354
|
+
this.subscriptionSemaphores.delete(queue);
|
|
297
355
|
}
|
|
298
356
|
await this.setupSubscription(sub);
|
|
299
357
|
}
|
|
@@ -306,6 +364,7 @@ var RabbitMQClient = class {
|
|
|
306
364
|
await this.channel.cancel(tag);
|
|
307
365
|
}
|
|
308
366
|
this.consumerTags.delete(queue);
|
|
367
|
+
this.subscriptionSemaphores.delete(queue);
|
|
309
368
|
this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue);
|
|
310
369
|
this.logger.info("Unsubscribed from queue", { queue });
|
|
311
370
|
}
|
|
@@ -313,15 +372,16 @@ var RabbitMQClient = class {
|
|
|
313
372
|
if (!this.channel) {
|
|
314
373
|
throw new Error("Channel not available");
|
|
315
374
|
}
|
|
375
|
+
const channel = this.channel;
|
|
316
376
|
const { exchange, routingKey, queue, handler, options } = sub;
|
|
317
377
|
const dlxExchange = `${exchange}.dlx`;
|
|
318
378
|
const dlqQueue = `${queue}.dlq`;
|
|
319
379
|
const dlqRoutingKey = `${routingKey}.dead`;
|
|
320
|
-
await
|
|
321
|
-
await
|
|
322
|
-
await
|
|
380
|
+
await channel.assertExchange(dlxExchange, "topic", { durable: true });
|
|
381
|
+
await channel.assertQueue(dlqQueue, { durable: true });
|
|
382
|
+
await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
|
|
323
383
|
if (!this.assertedExchanges.has(exchange)) {
|
|
324
|
-
await
|
|
384
|
+
await channel.assertExchange(exchange, "topic", { durable: true });
|
|
325
385
|
this.assertedExchanges.add(exchange);
|
|
326
386
|
}
|
|
327
387
|
const queueType = options?.queueArguments?.["x-queue-type"] ?? "quorum";
|
|
@@ -332,20 +392,23 @@ var RabbitMQClient = class {
|
|
|
332
392
|
if (queueType === "quorum") {
|
|
333
393
|
queueArgs["x-dead-letter-strategy"] = "at-least-once";
|
|
334
394
|
}
|
|
335
|
-
await
|
|
395
|
+
await channel.assertQueue(queue, {
|
|
336
396
|
durable: true,
|
|
337
397
|
deadLetterExchange: dlxExchange,
|
|
338
398
|
deadLetterRoutingKey: dlqRoutingKey,
|
|
339
399
|
arguments: { ...queueArgs, ...options?.queueArguments }
|
|
340
400
|
});
|
|
341
|
-
await
|
|
342
|
-
|
|
401
|
+
await channel.bindQueue(queue, exchange, routingKey);
|
|
402
|
+
if (!this.subscriptionSemaphores.has(queue)) {
|
|
403
|
+
const concurrency = options?.concurrency ?? this.options.concurrency;
|
|
404
|
+
this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null);
|
|
405
|
+
}
|
|
406
|
+
const semaphore = this.subscriptionSemaphores.get(queue) ?? null;
|
|
407
|
+
const { consumerTag } = await channel.consume(
|
|
343
408
|
queue,
|
|
344
409
|
(message) => {
|
|
345
410
|
if (message) {
|
|
346
|
-
this.
|
|
347
|
-
this.logger.error("Unhandled error in message handler", { error });
|
|
348
|
-
});
|
|
411
|
+
this.dispatch(message, handler, semaphore, channel);
|
|
349
412
|
}
|
|
350
413
|
},
|
|
351
414
|
{ noAck: false }
|
|
@@ -353,67 +416,111 @@ var RabbitMQClient = class {
|
|
|
353
416
|
this.consumerTags.set(queue, consumerTag);
|
|
354
417
|
this.logger.info("Subscribed to queue", { exchange, routingKey, queue });
|
|
355
418
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
421
|
+
* processes it. A message counts as in-flight from delivery until its handler
|
|
422
|
+
* settles (including time spent waiting for a permit), so graceful `close()`
|
|
423
|
+
* drains queued messages as well as actively-processing ones.
|
|
424
|
+
*/
|
|
425
|
+
dispatch(message, handler, semaphore, channel) {
|
|
426
|
+
this.inflightCount++;
|
|
427
|
+
const acquire = semaphore ? semaphore.acquire() : Promise.resolve();
|
|
428
|
+
acquire.then(() => this.handleWithRetry(message, handler, channel)).catch((error) => {
|
|
429
|
+
this.logger.error("Unhandled error in message handler", { error });
|
|
430
|
+
}).finally(() => {
|
|
431
|
+
semaphore?.release();
|
|
432
|
+
this.inflightCount--;
|
|
433
|
+
if (this.inflightCount === 0 && this.drainResolve) {
|
|
434
|
+
this.drainResolve();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
async handleWithRetry(message, handler, channel) {
|
|
439
|
+
if (channel !== this.channel) {
|
|
440
|
+
this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
|
|
360
441
|
return;
|
|
361
442
|
}
|
|
362
|
-
|
|
443
|
+
const startTime = Date.now();
|
|
444
|
+
let attempts = 0;
|
|
445
|
+
const routingKey = message.fields.routingKey;
|
|
446
|
+
const exchange = message.fields.exchange;
|
|
447
|
+
let content;
|
|
363
448
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
449
|
+
content = JSON.parse(message.content.toString());
|
|
450
|
+
} catch (parseError) {
|
|
451
|
+
const rawContent = message.content.toString();
|
|
452
|
+
const rawPreview = rawContent.substring(0, 100);
|
|
453
|
+
this.logger.error("Failed to parse message JSON, sending to DLQ", {
|
|
454
|
+
error: parseError,
|
|
455
|
+
exchange,
|
|
456
|
+
routingKey,
|
|
457
|
+
rawContentPreview: rawPreview + (rawContent.length > 100 ? "..." : "")
|
|
458
|
+
});
|
|
459
|
+
if (this.settle(channel, message, "nack")) {
|
|
460
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: "invalid_json" });
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
|
|
465
|
+
while (attempts < this.options.maxRetries) {
|
|
369
466
|
try {
|
|
370
|
-
|
|
371
|
-
} catch (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
error:
|
|
467
|
+
await handler(content);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
attempts++;
|
|
470
|
+
this.logger.error("Handler failed", {
|
|
471
|
+
error: error instanceof Error ? error.message : error,
|
|
472
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
376
473
|
exchange,
|
|
377
474
|
routingKey,
|
|
378
|
-
|
|
475
|
+
attempt: attempts,
|
|
476
|
+
maxRetries: this.options.maxRetries
|
|
379
477
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
|
|
385
|
-
while (attempts < this.options.maxRetries) {
|
|
386
|
-
try {
|
|
387
|
-
await handler(content);
|
|
388
|
-
const duration2 = Date.now() - startTime;
|
|
389
|
-
this.logger.info("Message processed successfully", { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
390
|
-
channel.ack(message);
|
|
391
|
-
this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
392
|
-
return;
|
|
393
|
-
} catch (error) {
|
|
394
|
-
attempts++;
|
|
395
|
-
this.logger.error("Handler failed", {
|
|
396
|
-
error: error instanceof Error ? error.message : error,
|
|
397
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
398
|
-
exchange,
|
|
399
|
-
routingKey,
|
|
400
|
-
attempt: attempts,
|
|
401
|
-
maxRetries: this.options.maxRetries
|
|
402
|
-
});
|
|
403
|
-
if (attempts < this.options.maxRetries) {
|
|
404
|
-
await this.sleep(this.options.retryDelay);
|
|
405
|
-
}
|
|
478
|
+
if (attempts < this.options.maxRetries) {
|
|
479
|
+
await this.sleep(this.options.retryDelay);
|
|
406
480
|
}
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const duration2 = Date.now() - startTime;
|
|
484
|
+
this.logger.info("Message processed successfully", { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
485
|
+
if (this.settle(channel, message, "ack")) {
|
|
486
|
+
this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
407
487
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const duration = Date.now() - startTime;
|
|
491
|
+
this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries: this.options.maxRetries, duration });
|
|
492
|
+
if (this.settle(channel, message, "nack")) {
|
|
411
493
|
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Acks or nacks a delivery, reporting whether the broker accepted it.
|
|
498
|
+
*
|
|
499
|
+
* A settle failure is a transport error, never a handler failure: by this
|
|
500
|
+
* point the handler has already run, so it must not be retried just because
|
|
501
|
+
* the channel went away. The delivery stays unacked and the broker redelivers
|
|
502
|
+
* it once the channel closes.
|
|
503
|
+
*/
|
|
504
|
+
settle(channel, message, action) {
|
|
505
|
+
if (channel !== this.channel) {
|
|
506
|
+
this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
if (action === "ack") {
|
|
511
|
+
channel.ack(message);
|
|
512
|
+
} else {
|
|
513
|
+
channel.nack(message, false, false);
|
|
416
514
|
}
|
|
515
|
+
return true;
|
|
516
|
+
} catch (error) {
|
|
517
|
+
this.logger.error("Failed to settle message; leaving it unacked for redelivery", {
|
|
518
|
+
error,
|
|
519
|
+
action,
|
|
520
|
+
exchange: message.fields.exchange,
|
|
521
|
+
routingKey: message.fields.routingKey
|
|
522
|
+
});
|
|
523
|
+
return false;
|
|
417
524
|
}
|
|
418
525
|
}
|
|
419
526
|
/**
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/logger.ts"],"sourcesContent":["import amqp from 'amqplib'\nimport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger } from './logger.js'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: ILogger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n private consumerTags = new Map<string, string>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = options.logger ?? defaultLogger\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n let attempts = 0\n while (!this.connected) {\n try {\n this.connection = await amqp.connect(this.options.url)\n this.logger.info('Connected to server')\n this.channel = await this.connection.createConfirmChannel()\n this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n this.connection.on('close', () => {\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n this.channel.on('error', (error: Error) => {\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n this.channel.on('close', () => {\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error('Connection failed after maximum attempts', { error, attempts, maxAttempts })\n throw new Error(\n `Failed to connect to RabbitMQ after ${attempts} attempts. ` +\n 'Check RABBITMQ_URL configuration and RabbitMQ server availability.',\n )\n }\n this.logger.warn('Connection attempt failed, retrying...', {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n })\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms.\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n this.channel.publish(exchange, routingKey, content, {\n persistent: true,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n })\n\n await this.channel.waitForConfirms()\n\n if (attempts > 0) {\n this.logger.info('Published message after retries', { exchange, routingKey, messageSize: content.length, attempts })\n } else {\n this.logger.info('Published message', { exchange, routingKey, messageSize: content.length })\n }\n this.logger.debug('Published message payload', { exchange, routingKey, payload: message })\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error('Publish failed after max attempts', { error, exchange, routingKey, attempts, maxAttempts })\n throw new Error(\n `Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = Math.min(\n baseDelay * Math.pow(2, attempts - 1),\n MAX_PUBLISH_RETRY_DELAY_MS,\n )\n\n this.logger.warn('Publish attempt failed, retrying', { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay })\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n try {\n if (this.inflightCount > 0) {\n this.logger.info('Waiting for in-flight messages to drain', { inflightCount: this.inflightCount })\n await this.waitForDrain(this.options.closeTimeout)\n }\n await this.channel?.close()\n await this.connection?.close()\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error('Error closing connection', { error })\n throw error\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info('Re-establishing subscriptions', { count: this.subscriptions.length })\n const subs = [...this.subscriptions]\n const results = await Promise.allSettled(\n subs.map(async (sub) => {\n await this.setupSubscription(sub)\n return sub.queue\n }),\n )\n const succeeded: string[] = []\n const failed: string[] = []\n results.forEach((result, index) => {\n if (result.status === 'fulfilled') {\n succeeded.push(result.value)\n } else {\n const queue = subs[index].queue\n failed.push(queue)\n this.logger.error('Failed to re-subscribe to queue', { error: result.reason, queue })\n }\n })\n if (succeeded.length > 0) {\n this.logger.info('Successfully re-subscribed to queues', { count: succeeded.length, queues: succeeded })\n }\n if (failed.length > 0) {\n this.logger.warn('Some subscriptions failed to restore', { count: failed.length, queues: failed })\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n }\n await this.setupSubscription(sub)\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info('Unsubscribed from queue', { queue })\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n const dlqRoutingKey = `${routingKey}.dead`\n\n await this.channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await this.channel.assertQueue(dlqQueue, { durable: true })\n await this.channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await this.channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n await this.channel.bindQueue(queue, exchange, routingKey)\n const { consumerTag } = await this.channel.consume(\n queue,\n (message) => {\n if (message) {\n this.handleWithRetry(message, handler).catch((error) => {\n this.logger.error('Unhandled error in message handler', { error })\n })\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { exchange, routingKey, queue })\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n ): Promise<void> {\n // Capture the channel reference so ack/nack always targets the channel\n // that delivered this message, even if a reconnection swaps this.channel.\n const channel = this.channel\n if (!channel) {\n this.logger.warn('Cannot process message, channel unavailable')\n return\n }\n this.inflightCount++\n try {\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error('Failed to parse message JSON, sending to DLQ', {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n })\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n return\n }\n\n this.logger.debug('Message received, processing', { exchange, routingKey, payload: content })\n\n while (attempts < this.options.maxRetries) {\n try {\n await handler(content)\n const duration = Date.now() - startTime\n this.logger.info('Message processed successfully', { exchange, routingKey, duration, attempts: attempts + 1 })\n channel.ack(message)\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n return\n } catch (error) {\n attempts++\n this.logger.error('Handler failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n exchange,\n routingKey,\n attempt: attempts,\n maxRetries: this.options.maxRetries,\n })\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n }\n }\n\n const duration = Date.now() - startTime\n this.logger.error('Message failed after max retries, sending to DLQ', { exchange, routingKey, maxRetries: this.options.maxRetries, duration })\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n } finally {\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n }\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn('Health check failed', { error })\n return false\n }\n }\n\n private waitForDrain(timeout: number): Promise<void> {\n if (this.inflightCount === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.logger.warn('Close timeout reached with messages still in flight', { inflightCount: this.inflightCount })\n this.drainResolve = null\n resolve()\n }, timeout)\n this.drainResolve = () => {\n clearTimeout(timer)\n this.drainResolve = null\n resolve()\n }\n })\n }\n\n /** Invokes a hook callback, swallowing errors so hooks never break message flow. */\n private callHook<T>(hook: ((info: T) => void) | undefined, info: T): void {\n try { hook?.(info) } catch (error) { this.logger.debug('Observability hook error', { error }) }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import type { ILogger } from './types.js'\n\n/**\n * Default logger used when the consumer does not inject one.\n *\n * Forwards `warn` and `error` to the console so connection drops, channel\n * errors, and failed handlers stay visible out of the box, while staying\n * silent for `info`/`debug` so routine operation never pollutes the\n * consumer's output. Pass `silentLogger` for zero output, or any `ILogger`\n * (console, Winston, pino, Bunyan) to take full control.\n */\nexport const defaultLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: (message, ...args) => console.warn('[rabbitmq-client]', message, ...args),\n error: (message, ...args) => console.error('[rabbitmq-client]', message, ...args),\n}\n\n/**\n * No-op logger that discards every message. Use this to silence the client\n * entirely, including the `warn`/`error` output the default logger emits.\n */\nexport const silentLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n"],"mappings":"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","names":["duration"]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/logger.ts","../src/semaphore.ts"],"sourcesContent":["import amqp from 'amqplib'\nimport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger } from './logger.js'\nimport { Semaphore } from './semaphore.js'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: ILogger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n private consumerTags = new Map<string, string>()\n // Per-queue concurrency limiter, keyed by queue name. Persisted across\n // reconnects (unlike consumerTags) so handlers still running when a\n // connection drops keep holding their permits, and the new consumer cannot\n // exceed the ceiling while the old pipeline drains. `null` = no limit.\n private subscriptionSemaphores = new Map<string, Semaphore | null>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n // Default concurrency tracks prefetch, preserving the previous behaviour\n // (handlers ran fire-and-forget, so up to `prefetch` ran at once) while\n // making the ceiling explicit and independently tunable. A value <= 0\n // (including an unlimited `prefetch: 0`) means \"no concurrency limit\".\n concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = options.logger ?? defaultLogger\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n let attempts = 0\n while (!this.connected) {\n try {\n this.connection = await amqp.connect(this.options.url)\n this.logger.info('Connected to server')\n this.channel = await this.connection.createConfirmChannel()\n this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n this.connection.on('close', () => {\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n this.channel.on('error', (error: Error) => {\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n this.channel.on('close', () => {\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error('Connection failed after maximum attempts', { error, attempts, maxAttempts })\n throw new Error(\n `Failed to connect to RabbitMQ after ${attempts} attempts. ` +\n 'Check RABBITMQ_URL configuration and RabbitMQ server availability.',\n )\n }\n this.logger.warn('Connection attempt failed, retrying...', {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n })\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms.\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n this.channel.publish(exchange, routingKey, content, {\n persistent: true,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n })\n\n await this.channel.waitForConfirms()\n\n if (attempts > 0) {\n this.logger.info('Published message after retries', { exchange, routingKey, messageSize: content.length, attempts })\n } else {\n this.logger.info('Published message', { exchange, routingKey, messageSize: content.length })\n }\n this.logger.debug('Published message payload', { exchange, routingKey, payload: message })\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error('Publish failed after max attempts', { error, exchange, routingKey, attempts, maxAttempts })\n throw new Error(\n `Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = Math.min(\n baseDelay * Math.pow(2, attempts - 1),\n MAX_PUBLISH_RETRY_DELAY_MS,\n )\n\n this.logger.warn('Publish attempt failed, retrying', { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay })\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n try {\n if (this.inflightCount > 0) {\n this.logger.info('Waiting for in-flight messages to drain', { inflightCount: this.inflightCount })\n await this.waitForDrain(this.options.closeTimeout)\n }\n await this.channel?.close()\n await this.connection?.close()\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n this.subscriptionSemaphores.clear()\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error('Error closing connection', { error })\n throw error\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info('Re-establishing subscriptions', { count: this.subscriptions.length })\n const subs = [...this.subscriptions]\n const results = await Promise.allSettled(\n subs.map(async (sub) => {\n await this.setupSubscription(sub)\n return sub.queue\n }),\n )\n const succeeded: string[] = []\n const failed: string[] = []\n results.forEach((result, index) => {\n if (result.status === 'fulfilled') {\n succeeded.push(result.value)\n } else {\n const queue = subs[index].queue\n failed.push(queue)\n this.logger.error('Failed to re-subscribe to queue', { error: result.reason, queue })\n }\n })\n if (succeeded.length > 0) {\n this.logger.info('Successfully re-subscribed to queues', { count: succeeded.length, queues: succeeded })\n }\n if (failed.length > 0) {\n this.logger.warn('Some subscriptions failed to restore', { count: failed.length, queues: failed })\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n // Re-subscribing may change concurrency; drop the old limiter so\n // setupSubscription rebuilds it from the new options. (A reconnect goes\n // through setupSubscription directly and keeps the existing limiter.)\n this.subscriptionSemaphores.delete(queue)\n }\n await this.setupSubscription(sub)\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptionSemaphores.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info('Unsubscribed from queue', { queue })\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n // Bind this consumer to the exact channel that will deliver its messages,\n // so ack/nack always target that channel even after a reconnect swaps\n // `this.channel`.\n const channel = this.channel\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n const dlqRoutingKey = `${routingKey}.dead`\n\n await channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await channel.assertQueue(dlqQueue, { durable: true })\n await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n await channel.bindQueue(queue, exchange, routingKey)\n // Reuse the queue's existing limiter across reconnects; only build a new\n // one the first time (or after subscribe()/unsubscribe() cleared it).\n // A concurrency <= 0 means \"no limit\" (null), matching an unlimited prefetch.\n if (!this.subscriptionSemaphores.has(queue)) {\n const concurrency = options?.concurrency ?? this.options.concurrency\n this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null)\n }\n const semaphore = this.subscriptionSemaphores.get(queue) ?? null\n const { consumerTag } = await channel.consume(\n queue,\n (message) => {\n if (message) {\n this.dispatch(message, handler, semaphore, channel)\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { exchange, routingKey, queue })\n }\n\n /**\n * Gates a delivered message on the subscription's concurrency semaphore, then\n * processes it. A message counts as in-flight from delivery until its handler\n * settles (including time spent waiting for a permit), so graceful `close()`\n * drains queued messages as well as actively-processing ones.\n */\n private dispatch(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n semaphore: Semaphore | null,\n channel: amqp.ConfirmChannel,\n ): void {\n this.inflightCount++\n const acquire = semaphore ? semaphore.acquire() : Promise.resolve()\n acquire\n .then(() => this.handleWithRetry(message, handler, channel))\n .catch((error) => {\n this.logger.error('Unhandled error in message handler', { error })\n })\n .finally(() => {\n semaphore?.release()\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n })\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n channel: amqp.ConfirmChannel,\n ): Promise<void> {\n // `channel` is the one that delivered this message. If a reconnect has\n // since replaced it, this message was never acked and the broker will\n // redeliver it on the new channel, so drop this stale attempt rather than\n // run the handler again or ack a tag the new channel does not know.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return\n }\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error('Failed to parse message JSON, sending to DLQ', {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n }\n return\n }\n\n this.logger.debug('Message received, processing', { exchange, routingKey, payload: content })\n\n while (attempts < this.options.maxRetries) {\n // Only the handler call belongs in this try. Acking inside it would make\n // a dead channel look like a failed handler and re-run its side effects.\n try {\n await handler(content)\n } catch (error) {\n attempts++\n this.logger.error('Handler failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n exchange,\n routingKey,\n attempt: attempts,\n maxRetries: this.options.maxRetries,\n })\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n continue\n }\n const duration = Date.now() - startTime\n this.logger.info('Message processed successfully', { exchange, routingKey, duration, attempts: attempts + 1 })\n if (this.settle(channel, message, 'ack')) {\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n }\n return\n }\n\n const duration = Date.now() - startTime\n this.logger.error('Message failed after max retries, sending to DLQ', { exchange, routingKey, maxRetries: this.options.maxRetries, duration })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n }\n }\n\n /**\n * Acks or nacks a delivery, reporting whether the broker accepted it.\n *\n * A settle failure is a transport error, never a handler failure: by this\n * point the handler has already run, so it must not be retried just because\n * the channel went away. The delivery stays unacked and the broker redelivers\n * it once the channel closes.\n */\n private settle(\n channel: amqp.ConfirmChannel,\n message: amqp.ConsumeMessage,\n action: 'ack' | 'nack',\n ): boolean {\n // A handler can run for minutes, so the channel that delivered this\n // message may have been replaced by a reconnect since the entry check.\n // Settling on it would throw, and replaying the delivery tag on the\n // current channel would settle an unrelated message, since tags are scoped\n // to the channel that issued them. The broker requeues a closed channel's\n // unacked deliveries, so let it be redelivered instead.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return false\n }\n try {\n if (action === 'ack') {\n channel.ack(message)\n } else {\n channel.nack(message, false, false)\n }\n return true\n } catch (error) {\n this.logger.error('Failed to settle message; leaving it unacked for redelivery', {\n error,\n action,\n exchange: message.fields.exchange,\n routingKey: message.fields.routingKey,\n })\n return false\n }\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn('Health check failed', { error })\n return false\n }\n }\n\n private waitForDrain(timeout: number): Promise<void> {\n if (this.inflightCount === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.logger.warn('Close timeout reached with messages still in flight', { inflightCount: this.inflightCount })\n this.drainResolve = null\n resolve()\n }, timeout)\n this.drainResolve = () => {\n clearTimeout(timer)\n this.drainResolve = null\n resolve()\n }\n })\n }\n\n /** Invokes a hook callback, swallowing errors so hooks never break message flow. */\n private callHook<T>(hook: ((info: T) => void) | undefined, info: T): void {\n try { hook?.(info) } catch (error) { this.logger.debug('Observability hook error', { error }) }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import type { ILogger } from './types.js'\n\n/**\n * Default logger used when the consumer does not inject one.\n *\n * Forwards `warn` and `error` to the console so connection drops, channel\n * errors, and failed handlers stay visible out of the box, while staying\n * silent for `info`/`debug` so routine operation never pollutes the\n * consumer's output. Pass `silentLogger` for zero output, or any `ILogger`\n * (console, Winston, pino, Bunyan) to take full control.\n */\nexport const defaultLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: (message, ...args) => console.warn('[rabbitmq-client]', message, ...args),\n error: (message, ...args) => console.error('[rabbitmq-client]', message, ...args),\n}\n\n/**\n * No-op logger that discards every message. Use this to silence the client\n * entirely, including the `warn`/`error` output the default logger emits.\n */\nexport const silentLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n","/**\n * Minimal FIFO counting semaphore used to cap how many message handlers run\n * concurrently, independently of channel prefetch.\n *\n * Prefetch bounds how many messages the broker delivers into consumer memory;\n * the semaphore bounds how many of those are actively being processed at once.\n * Keeping the two separate lets a consumer buffer a large prefetch window while\n * still protecting a slow downstream (a database, an HTTP API) from a burst.\n */\nexport class Semaphore {\n private permits: number\n private readonly waiters: Array<() => void> = []\n\n constructor(permits: number) {\n if (!Number.isInteger(permits) || permits < 1) {\n throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`)\n }\n this.permits = permits\n }\n\n /**\n * Acquires a permit, resolving immediately when one is free or queueing\n * (FIFO) until another holder releases. Always pair with `release()` in a\n * `finally` so a throwing handler never leaks a permit.\n */\n acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => {\n this.waiters.push(resolve)\n })\n }\n\n /**\n * Releases a permit. Hands it directly to the next waiter if any are queued,\n * otherwise returns it to the pool.\n */\n release(): void {\n const next = this.waiters.shift()\n if (next) {\n next()\n } else {\n this.permits++\n }\n }\n\n /** Permits currently available. Exposed for tests and introspection. */\n get available(): number {\n return this.permits\n }\n\n /** Number of callers currently queued waiting for a permit. */\n get waiting(): number {\n return this.waiters.length\n }\n}\n"],"mappings":"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","names":["duration"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linagora/rabbitmq-client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Production-grade RabbitMQ client with confirm channels, DLQ infrastructure, auto-reconnection, exponential backoff, graceful shutdown, and observability hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|