@linagora/rabbitmq-client 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/dist/index.cjs +99 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +98 -20
- package/dist/index.js.map +1 -1
- package/dist/testing/index.cjs +8 -2
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +4 -1
- package/dist/testing/index.d.ts +4 -1
- package/dist/testing/index.js +8 -2
- package/dist/testing/index.js.map +1 -1
- package/dist/{types-D29GyoZX.d.cts → types-A2viogDe.d.cts} +28 -2
- package/dist/{types-D29GyoZX.d.ts → types-A2viogDe.d.ts} +28 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RabbitMQClientOptions, a as RabbitMQMessage, P as PublishOptions, b as RabbitMQMessageHandler, S as SubscribeOptions, I as ILogger } from './types-
|
|
2
|
-
export { c as
|
|
1
|
+
import { R as RabbitMQClientOptions, a as RabbitMQMessage, P as PublishOptions, b as RabbitMQMessageHandler, S as SubscribeOptions, I as ILogger } from './types-A2viogDe.js';
|
|
2
|
+
export { c as RabbitMQBinding, d as RabbitMQHooks, e as RabbitMQMessageProperties, f as RabbitMQSubscription } from './types-A2viogDe.js';
|
|
3
3
|
|
|
4
4
|
/** A `mandatory` publish the broker could route to no queue. */
|
|
5
5
|
declare class UnroutableMessageError extends Error {
|
|
@@ -7,6 +7,12 @@ declare class UnroutableMessageError extends Error {
|
|
|
7
7
|
readonly routingKey: string;
|
|
8
8
|
constructor(exchange: string, routingKey: string);
|
|
9
9
|
}
|
|
10
|
+
/** Thrown by a handler to send its message to the DLQ at once, without retrying. */
|
|
11
|
+
declare class DeadLetterError extends Error {
|
|
12
|
+
constructor(message: string, options?: {
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
10
16
|
/**
|
|
11
17
|
* Production-grade RabbitMQ client with confirm channels, automatic DLQ
|
|
12
18
|
* infrastructure, reconnection with subscription restoration, and
|
|
@@ -35,6 +41,7 @@ declare class RabbitMQClient {
|
|
|
35
41
|
private returnWaiters;
|
|
36
42
|
private consumerTags;
|
|
37
43
|
private subscriptionSemaphores;
|
|
44
|
+
private retryWakers;
|
|
38
45
|
private inflightCount;
|
|
39
46
|
private drainResolve;
|
|
40
47
|
constructor(options: RabbitMQClientOptions);
|
|
@@ -79,6 +86,7 @@ declare class RabbitMQClient {
|
|
|
79
86
|
*/
|
|
80
87
|
unsubscribe(queue: string): Promise<void>;
|
|
81
88
|
private setupSubscription;
|
|
89
|
+
private checkExchanges;
|
|
82
90
|
/**
|
|
83
91
|
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
84
92
|
* processes it. A message counts as in-flight from delivery until its handler
|
|
@@ -105,6 +113,10 @@ declare class RabbitMQClient {
|
|
|
105
113
|
/** Invokes a hook callback, swallowing errors so hooks never break message flow. */
|
|
106
114
|
private callHook;
|
|
107
115
|
private sleep;
|
|
116
|
+
private retryDelayFor;
|
|
117
|
+
private wakeRetries;
|
|
118
|
+
/** Also cut short by a reconnect, via wakeRetries(). */
|
|
119
|
+
private sleepUntilClose;
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
/**
|
|
@@ -123,4 +135,4 @@ declare const defaultLogger: ILogger;
|
|
|
123
135
|
*/
|
|
124
136
|
declare const silentLogger: ILogger;
|
|
125
137
|
|
|
126
|
-
export { ILogger, PublishOptions, RabbitMQClient, RabbitMQClientOptions, RabbitMQMessage, RabbitMQMessageHandler, SubscribeOptions, UnroutableMessageError, defaultLogger, silentLogger };
|
|
138
|
+
export { DeadLetterError, ILogger, PublishOptions, RabbitMQClient, RabbitMQClientOptions, RabbitMQMessage, RabbitMQMessageHandler, SubscribeOptions, UnroutableMessageError, defaultLogger, silentLogger };
|
package/dist/index.js
CHANGED
|
@@ -89,6 +89,9 @@ var DEFAULTS = {
|
|
|
89
89
|
prefetch: DEFAULT_PREFETCH,
|
|
90
90
|
closeTimeout: 5e3
|
|
91
91
|
};
|
|
92
|
+
function backoff(baseDelay, attempt, maxDelay) {
|
|
93
|
+
return Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
94
|
+
}
|
|
92
95
|
var UnroutableMessageError = class extends Error {
|
|
93
96
|
constructor(exchange, routingKey) {
|
|
94
97
|
super(`No queue is bound to receive ${exchange || "(default exchange)"}/${routingKey}`);
|
|
@@ -99,6 +102,12 @@ var UnroutableMessageError = class extends Error {
|
|
|
99
102
|
exchange;
|
|
100
103
|
routingKey;
|
|
101
104
|
};
|
|
105
|
+
var DeadLetterError = class extends Error {
|
|
106
|
+
constructor(message, options) {
|
|
107
|
+
super(message, options);
|
|
108
|
+
this.name = "DeadLetterError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
102
111
|
var RabbitMQClient = class {
|
|
103
112
|
connection = null;
|
|
104
113
|
channel = null;
|
|
@@ -119,6 +128,7 @@ var RabbitMQClient = class {
|
|
|
119
128
|
// connection drops keep holding their permits, and the new consumer cannot
|
|
120
129
|
// exceed the ceiling while the old pipeline drains. `null` = no limit.
|
|
121
130
|
subscriptionSemaphores = /* @__PURE__ */ new Map();
|
|
131
|
+
retryWakers = /* @__PURE__ */ new Set();
|
|
122
132
|
inflightCount = 0;
|
|
123
133
|
drainResolve = null;
|
|
124
134
|
constructor(options) {
|
|
@@ -173,6 +183,7 @@ var RabbitMQClient = class {
|
|
|
173
183
|
this.logger.info("Connected to server");
|
|
174
184
|
const channel = await connection.createConfirmChannel();
|
|
175
185
|
this.channel = channel;
|
|
186
|
+
this.wakeRetries();
|
|
176
187
|
const returnWaiters = /* @__PURE__ */ new Set();
|
|
177
188
|
this.returnWaiters = returnWaiters;
|
|
178
189
|
channel.on("return", (msg) => {
|
|
@@ -311,10 +322,7 @@ var RabbitMQClient = class {
|
|
|
311
322
|
`Failed to publish to ${exchange || "(default exchange)"}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`
|
|
312
323
|
);
|
|
313
324
|
}
|
|
314
|
-
const retryDelay =
|
|
315
|
-
baseDelay * Math.pow(2, attempts - 1),
|
|
316
|
-
MAX_PUBLISH_RETRY_DELAY_MS
|
|
317
|
-
);
|
|
325
|
+
const retryDelay = backoff(baseDelay, attempts, MAX_PUBLISH_RETRY_DELAY_MS);
|
|
318
326
|
this.logger.warn("Publish attempt failed, retrying", { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay });
|
|
319
327
|
await this.sleep(retryDelay);
|
|
320
328
|
}
|
|
@@ -330,6 +338,7 @@ var RabbitMQClient = class {
|
|
|
330
338
|
this.closing = true;
|
|
331
339
|
try {
|
|
332
340
|
await Promise.allSettled([this.initializationPromise, this.reconnectionPromise]);
|
|
341
|
+
this.wakeRetries();
|
|
333
342
|
if (this.inflightCount > 0) {
|
|
334
343
|
this.logger.info("Waiting for in-flight messages to drain", { inflightCount: this.inflightCount });
|
|
335
344
|
await this.waitForDrain(this.options.closeTimeout);
|
|
@@ -422,13 +431,19 @@ var RabbitMQClient = class {
|
|
|
422
431
|
}
|
|
423
432
|
const sub = { exchange, routingKey, queue, handler, options };
|
|
424
433
|
const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue);
|
|
434
|
+
const previous = existingIndex === -1 ? void 0 : this.subscriptions[existingIndex];
|
|
425
435
|
if (existingIndex === -1) {
|
|
426
436
|
this.subscriptions.push(sub);
|
|
427
437
|
} else {
|
|
428
438
|
this.subscriptions[existingIndex] = sub;
|
|
429
439
|
this.subscriptionSemaphores.delete(queue);
|
|
430
440
|
}
|
|
431
|
-
|
|
441
|
+
try {
|
|
442
|
+
await this.setupSubscription(sub);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
this.subscriptions = previous ? this.subscriptions.map((s) => s === sub ? previous : s) : this.subscriptions.filter((s) => s !== sub);
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
432
447
|
}
|
|
433
448
|
/**
|
|
434
449
|
* Cancels a queue subscription and removes it from the restoration list.
|
|
@@ -449,16 +464,24 @@ var RabbitMQClient = class {
|
|
|
449
464
|
}
|
|
450
465
|
const channel = this.channel;
|
|
451
466
|
const { exchange, routingKey, queue, handler, options } = sub;
|
|
452
|
-
const dlxExchange = `${exchange}.dlx`;
|
|
467
|
+
const dlxExchange = options?.deadLetterExchange ?? `${exchange}.dlx`;
|
|
453
468
|
const dlqQueue = `${queue}.dlq`;
|
|
454
469
|
const dlqRoutingKey = `${routingKey}.dead`;
|
|
470
|
+
const bindings = [{ exchange, routingKey }, ...options?.bindings ?? []];
|
|
455
471
|
await channel.assertExchange(dlxExchange, "topic", { durable: true });
|
|
456
472
|
await channel.assertQueue(dlqQueue, { durable: true });
|
|
457
473
|
await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
474
|
+
const passive = options?.passiveExchanges;
|
|
475
|
+
const isPassive = (name) => passive === true || Array.isArray(passive) && passive.includes(name);
|
|
476
|
+
const missing = [...new Set(bindings.map((b) => b.exchange))].filter((name) => !this.assertedExchanges.has(name));
|
|
477
|
+
const toCheck = missing.filter(isPassive);
|
|
478
|
+
if (toCheck.length > 0) {
|
|
479
|
+
await this.checkExchanges(toCheck);
|
|
461
480
|
}
|
|
481
|
+
for (const name of missing.filter((name2) => !isPassive(name2))) {
|
|
482
|
+
await channel.assertExchange(name, "topic", { durable: true });
|
|
483
|
+
}
|
|
484
|
+
for (const name of missing) this.assertedExchanges.add(name);
|
|
462
485
|
const queueType = options?.queueArguments?.["x-queue-type"] ?? "quorum";
|
|
463
486
|
const queueArgs = {
|
|
464
487
|
"x-queue-type": queueType,
|
|
@@ -473,7 +496,9 @@ var RabbitMQClient = class {
|
|
|
473
496
|
deadLetterRoutingKey: dlqRoutingKey,
|
|
474
497
|
arguments: { ...queueArgs, ...options?.queueArguments }
|
|
475
498
|
});
|
|
476
|
-
|
|
499
|
+
for (const binding of bindings) {
|
|
500
|
+
await channel.bindQueue(queue, binding.exchange, binding.routingKey);
|
|
501
|
+
}
|
|
477
502
|
if (!this.subscriptionSemaphores.has(queue)) {
|
|
478
503
|
const concurrency = options?.concurrency ?? this.options.concurrency;
|
|
479
504
|
this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null);
|
|
@@ -483,13 +508,29 @@ var RabbitMQClient = class {
|
|
|
483
508
|
queue,
|
|
484
509
|
(message) => {
|
|
485
510
|
if (message) {
|
|
486
|
-
this.dispatch(message, handler, semaphore, channel);
|
|
511
|
+
this.dispatch(message, handler, semaphore, channel, options);
|
|
487
512
|
}
|
|
488
513
|
},
|
|
489
514
|
{ noAck: false }
|
|
490
515
|
);
|
|
491
516
|
this.consumerTags.set(queue, consumerTag);
|
|
492
|
-
this.logger.info("Subscribed to queue", {
|
|
517
|
+
this.logger.info("Subscribed to queue", { queue, bindings });
|
|
518
|
+
}
|
|
519
|
+
// A failed check closes the channel it ran on, so it runs on a throwaway one
|
|
520
|
+
// rather than take down the shared channel and every consumer on it.
|
|
521
|
+
async checkExchanges(exchanges) {
|
|
522
|
+
if (!this.connection) {
|
|
523
|
+
throw new Error("Connection not available");
|
|
524
|
+
}
|
|
525
|
+
const probe = await this.connection.createChannel();
|
|
526
|
+
probe.on("error", () => void 0);
|
|
527
|
+
try {
|
|
528
|
+
for (const name of exchanges) {
|
|
529
|
+
await probe.checkExchange(name);
|
|
530
|
+
}
|
|
531
|
+
} finally {
|
|
532
|
+
await probe.close().catch(() => void 0);
|
|
533
|
+
}
|
|
493
534
|
}
|
|
494
535
|
/**
|
|
495
536
|
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
@@ -497,10 +538,10 @@ var RabbitMQClient = class {
|
|
|
497
538
|
* settles (including time spent waiting for a permit), so graceful `close()`
|
|
498
539
|
* drains queued messages as well as actively-processing ones.
|
|
499
540
|
*/
|
|
500
|
-
dispatch(message, handler, semaphore, channel) {
|
|
541
|
+
dispatch(message, handler, semaphore, channel, options) {
|
|
501
542
|
this.inflightCount++;
|
|
502
543
|
const acquire = semaphore ? semaphore.acquire() : Promise.resolve();
|
|
503
|
-
acquire.then(() => this.handleWithRetry(message, handler, channel)).catch((error) => {
|
|
544
|
+
acquire.then(() => this.handleWithRetry(message, handler, channel, options)).catch((error) => {
|
|
504
545
|
this.logger.error("Unhandled error in message handler", { error });
|
|
505
546
|
}).finally(() => {
|
|
506
547
|
semaphore?.release();
|
|
@@ -510,7 +551,7 @@ var RabbitMQClient = class {
|
|
|
510
551
|
}
|
|
511
552
|
});
|
|
512
553
|
}
|
|
513
|
-
async handleWithRetry(message, handler, channel) {
|
|
554
|
+
async handleWithRetry(message, handler, channel, options) {
|
|
514
555
|
if (channel !== this.channel) {
|
|
515
556
|
this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
|
|
516
557
|
return;
|
|
@@ -538,26 +579,41 @@ var RabbitMQClient = class {
|
|
|
538
579
|
}
|
|
539
580
|
this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
|
|
540
581
|
const properties = {
|
|
582
|
+
exchange,
|
|
583
|
+
routingKey,
|
|
541
584
|
headers: message.properties.headers ?? {},
|
|
542
585
|
timestamp: message.properties.timestamp,
|
|
543
586
|
messageId: message.properties.messageId,
|
|
544
587
|
correlationId: message.properties.correlationId
|
|
545
588
|
};
|
|
546
|
-
|
|
589
|
+
const maxRetries = options?.maxRetries ?? this.options.maxRetries;
|
|
590
|
+
while (attempts < maxRetries) {
|
|
547
591
|
try {
|
|
548
592
|
await handler(content, properties);
|
|
549
593
|
} catch (error) {
|
|
550
594
|
attempts++;
|
|
595
|
+
if (error instanceof Error && error.name === "DeadLetterError") {
|
|
596
|
+
const duration3 = Date.now() - startTime;
|
|
597
|
+
this.logger.warn("Handler dead-lettered the message", { error: error.message, exchange, routingKey });
|
|
598
|
+
if (this.settle(channel, message, "nack")) {
|
|
599
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: duration3, reason: "dead_letter_error" });
|
|
600
|
+
}
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
551
603
|
this.logger.error("Handler failed", {
|
|
552
604
|
error: error instanceof Error ? error.message : error,
|
|
553
605
|
stack: error instanceof Error ? error.stack : void 0,
|
|
554
606
|
exchange,
|
|
555
607
|
routingKey,
|
|
556
608
|
attempt: attempts,
|
|
557
|
-
maxRetries
|
|
609
|
+
maxRetries
|
|
558
610
|
});
|
|
559
|
-
if (attempts <
|
|
560
|
-
await this.
|
|
611
|
+
if (attempts < maxRetries) {
|
|
612
|
+
await this.sleepUntilClose(this.retryDelayFor(attempts, options));
|
|
613
|
+
if (this.closing || channel !== this.channel) {
|
|
614
|
+
this.logger.warn("Stopped retrying a message on close or reconnect; it will be redelivered", { exchange, routingKey });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
561
617
|
}
|
|
562
618
|
continue;
|
|
563
619
|
}
|
|
@@ -569,7 +625,7 @@ var RabbitMQClient = class {
|
|
|
569
625
|
return;
|
|
570
626
|
}
|
|
571
627
|
const duration = Date.now() - startTime;
|
|
572
|
-
this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries
|
|
628
|
+
this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries, duration });
|
|
573
629
|
if (this.settle(channel, message, "nack")) {
|
|
574
630
|
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
|
|
575
631
|
}
|
|
@@ -650,8 +706,30 @@ var RabbitMQClient = class {
|
|
|
650
706
|
sleep(ms) {
|
|
651
707
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
652
708
|
}
|
|
709
|
+
retryDelayFor(attempts, options) {
|
|
710
|
+
const { retryDelay } = this.options;
|
|
711
|
+
if (options?.maxRetryDelay === void 0) return retryDelay;
|
|
712
|
+
return backoff(retryDelay, attempts, options.maxRetryDelay);
|
|
713
|
+
}
|
|
714
|
+
wakeRetries() {
|
|
715
|
+
for (const wake of [...this.retryWakers]) wake();
|
|
716
|
+
}
|
|
717
|
+
/** Also cut short by a reconnect, via wakeRetries(). */
|
|
718
|
+
sleepUntilClose(ms) {
|
|
719
|
+
if (this.closing) return Promise.resolve();
|
|
720
|
+
return new Promise((resolve) => {
|
|
721
|
+
const wake = () => {
|
|
722
|
+
clearTimeout(timer);
|
|
723
|
+
this.retryWakers.delete(wake);
|
|
724
|
+
resolve();
|
|
725
|
+
};
|
|
726
|
+
const timer = setTimeout(wake, ms);
|
|
727
|
+
this.retryWakers.add(wake);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
653
730
|
};
|
|
654
731
|
export {
|
|
732
|
+
DeadLetterError,
|
|
655
733
|
RabbitMQClient,
|
|
656
734
|
UnroutableMessageError,
|
|
657
735
|
defaultLogger,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 RabbitMQMessageProperties,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger, forLibraryCalls } 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/** A `mandatory` publish the broker could route to no queue. */\nexport class UnroutableMessageError extends Error {\n constructor(\n readonly exchange: string,\n readonly routingKey: string,\n ) {\n super(`No queue is bound to receive ${exchange || '(default exchange)'}/${routingKey}`)\n this.name = 'UnroutableMessageError'\n }\n}\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 closing = 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 // One per channel, so a late return on a replaced channel reaches no waiter.\n private returnWaiters = new Set<(msg: amqp.Message) => void>()\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 = forLibraryCalls(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 // A channel failure leaves its connection open; drop it so it does not\n // outlive the client and keep the process alive.\n this.connection?.close().catch(() => undefined)\n this.connection = null\n let attempts = 0\n while (!this.connected && !this.closing) {\n try {\n const connection = await amqp.connect(this.options.url)\n this.connection = connection\n this.logger.info('Connected to server')\n const channel = await connection.createConfirmChannel()\n this.channel = channel\n const returnWaiters = new Set<(msg: amqp.Message) => void>()\n this.returnWaiters = returnWaiters\n channel.on('return', (msg: amqp.Message) => {\n for (const waiter of returnWaiters) waiter(msg)\n })\n this.logger.info('Confirm channel created')\n await channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n // Events from a connection or channel a reconnect has replaced must not\n // tear down the current one.\n connection.on('error', (error: Error) => {\n if (connection !== this.connection) return\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n connection.on('close', () => {\n if (connection !== this.connection) return\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n channel.on('error', (error: Error) => {\n if (channel !== this.channel) return\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n channel.on('close', () => {\n if (channel !== this.channel) return\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 if (!this.connected) {\n throw new Error('RabbitMQ client closed while connecting')\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, or\n * straight to a queue through the default exchange (`''`, routing key = queue name).\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 // Taken once, not per attempt: a retried publish must not look newer than\n // a message published after it succeeded.\n const timestamp = Math.floor(Date.now() / 1000)\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 // The default exchange ('') always exists and the broker refuses to\n // declare it, so publishing straight to a queue skips the assertion.\n if (exchange !== '' && !this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const channel = this.channel\n const returnWaiters = this.returnWaiters\n // The broker sends basic.return before the confirm of the same message,\n // so the flag is settled by the time waitForConfirms resolves. A return\n // carries no delivery tag, so it is matched on where it was sent plus the\n // message id, or the content when there is none.\n let returned = false\n const messageId = options?.messageId\n const onReturn = (msg: amqp.Message) => {\n if (msg.fields.exchange !== exchange || msg.fields.routingKey !== routingKey) return\n if (messageId !== undefined ? msg.properties.messageId === messageId : msg.content.equals(content)) {\n returned = true\n }\n }\n if (options?.mandatory) returnWaiters.add(onReturn)\n\n try {\n channel.publish(exchange, routingKey, content, {\n persistent: true,\n timestamp,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n mandatory: options?.mandatory,\n })\n\n await channel.waitForConfirms()\n } finally {\n returnWaiters.delete(onReturn)\n }\n\n if (returned) {\n throw new UnroutableMessageError(exchange, routingKey)\n }\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 // The broker answered, and the same routing would fail the same way.\n if (error instanceof UnroutableMessageError) {\n this.logger.warn('Published message was unroutable', { exchange, routingKey })\n throw error\n }\n\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 || '(default 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 // Closing the channel fires its 'close' handler, which would otherwise\n // reconnect and leave a connection behind that keeps the process alive.\n this.closing = true\n try {\n // A connect already running gives up after its current attempt; whatever\n // it opened is closed below.\n await Promise.allSettled([this.initializationPromise, this.reconnectionPromise])\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 try {\n await this.channel?.close()\n } finally {\n await this.connection?.close()\n }\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 } finally {\n this.closing = false\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.closing) return\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 } catch (error) {\n // The event handlers call this without awaiting it, so a reconnect that\n // close() interrupted must not surface as an unhandled rejection.\n if (!this.closing) throw error\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 const properties: RabbitMQMessageProperties = {\n headers: message.properties.headers ?? {},\n timestamp: message.properties.timestamp,\n messageId: message.properties.messageId,\n correlationId: message.properties.correlationId,\n }\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, properties)\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\ntype Level = keyof ILogger\ntype ObjectFirst = (context: object, message: string) => void\n\n/**\n * pino takes `(context, message)` and drops a context passed after the\n * message, and only serializes an Error under `err`. It is the one logger\n * with a `bindings()` method, so it is recognised by that and called its way.\n */\nexport const forLibraryCalls = (logger: ILogger): ILogger => {\n if (typeof (logger as { bindings?: unknown }).bindings !== 'function') return logger\n const call =\n (level: Level) =>\n (message: string, context?: unknown): void => {\n const log = (logger[level] as unknown as ObjectFirst).bind(logger)\n if (typeof context !== 'object' || context === null) return log({}, message)\n const { error, ...rest } = context as { error?: unknown }\n log(error === undefined ? rest : { ...rest, err: error }, message)\n }\n return { debug: call('debug'), info: call('info'), warn: call('warn'), error: call('error') }\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"]}
|
|
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 RabbitMQMessageProperties,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger, forLibraryCalls } 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\nfunction backoff(baseDelay: number, attempt: number, maxDelay: number): number {\n return Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay)\n}\n\n/** A `mandatory` publish the broker could route to no queue. */\nexport class UnroutableMessageError extends Error {\n constructor(\n readonly exchange: string,\n readonly routingKey: string,\n ) {\n super(`No queue is bound to receive ${exchange || '(default exchange)'}/${routingKey}`)\n this.name = 'UnroutableMessageError'\n }\n}\n\n/** Thrown by a handler to send its message to the DLQ at once, without retrying. */\nexport class DeadLetterError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options)\n this.name = 'DeadLetterError'\n }\n}\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 closing = 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 // One per channel, so a late return on a replaced channel reaches no waiter.\n private returnWaiters = new Set<(msg: amqp.Message) => void>()\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 retryWakers = new Set<() => void>()\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 = forLibraryCalls(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 // A channel failure leaves its connection open; drop it so it does not\n // outlive the client and keep the process alive.\n this.connection?.close().catch(() => undefined)\n this.connection = null\n let attempts = 0\n while (!this.connected && !this.closing) {\n try {\n const connection = await amqp.connect(this.options.url)\n this.connection = connection\n this.logger.info('Connected to server')\n const channel = await connection.createConfirmChannel()\n this.channel = channel\n // Retries sleeping on the replaced channel give up; the broker redelivers their messages.\n this.wakeRetries()\n const returnWaiters = new Set<(msg: amqp.Message) => void>()\n this.returnWaiters = returnWaiters\n channel.on('return', (msg: amqp.Message) => {\n for (const waiter of returnWaiters) waiter(msg)\n })\n this.logger.info('Confirm channel created')\n await channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n // Events from a connection or channel a reconnect has replaced must not\n // tear down the current one.\n connection.on('error', (error: Error) => {\n if (connection !== this.connection) return\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n connection.on('close', () => {\n if (connection !== this.connection) return\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n channel.on('error', (error: Error) => {\n if (channel !== this.channel) return\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n channel.on('close', () => {\n if (channel !== this.channel) return\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 if (!this.connected) {\n throw new Error('RabbitMQ client closed while connecting')\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, or\n * straight to a queue through the default exchange (`''`, routing key = queue name).\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 // Taken once, not per attempt: a retried publish must not look newer than\n // a message published after it succeeded.\n const timestamp = Math.floor(Date.now() / 1000)\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 // The default exchange ('') always exists and the broker refuses to\n // declare it, so publishing straight to a queue skips the assertion.\n if (exchange !== '' && !this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const channel = this.channel\n const returnWaiters = this.returnWaiters\n // The broker sends basic.return before the confirm of the same message,\n // so the flag is settled by the time waitForConfirms resolves. A return\n // carries no delivery tag, so it is matched on where it was sent plus the\n // message id, or the content when there is none.\n let returned = false\n const messageId = options?.messageId\n const onReturn = (msg: amqp.Message) => {\n if (msg.fields.exchange !== exchange || msg.fields.routingKey !== routingKey) return\n if (messageId !== undefined ? msg.properties.messageId === messageId : msg.content.equals(content)) {\n returned = true\n }\n }\n if (options?.mandatory) returnWaiters.add(onReturn)\n\n try {\n channel.publish(exchange, routingKey, content, {\n persistent: true,\n timestamp,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n mandatory: options?.mandatory,\n })\n\n await channel.waitForConfirms()\n } finally {\n returnWaiters.delete(onReturn)\n }\n\n if (returned) {\n throw new UnroutableMessageError(exchange, routingKey)\n }\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 // The broker answered, and the same routing would fail the same way.\n if (error instanceof UnroutableMessageError) {\n this.logger.warn('Published message was unroutable', { exchange, routingKey })\n throw error\n }\n\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 || '(default exchange)'}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = backoff(baseDelay, attempts, MAX_PUBLISH_RETRY_DELAY_MS)\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 // Closing the channel fires its 'close' handler, which would otherwise\n // reconnect and leave a connection behind that keeps the process alive.\n this.closing = true\n try {\n // A connect already running gives up after its current attempt; whatever\n // it opened is closed below.\n await Promise.allSettled([this.initializationPromise, this.reconnectionPromise])\n this.wakeRetries()\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 try {\n await this.channel?.close()\n } finally {\n await this.connection?.close()\n }\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 } finally {\n this.closing = false\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.closing) return\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 } catch (error) {\n // The event handlers call this without awaiting it, so a reconnect that\n // close() interrupted must not surface as an unhandled rejection.\n if (!this.closing) throw error\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 const previous = existingIndex === -1 ? undefined : this.subscriptions[existingIndex]\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 try {\n await this.setupSubscription(sub)\n } catch (error) {\n // A subscription that cannot be set up must not be retried on every reconnect.\n this.subscriptions = previous\n ? this.subscriptions.map((s) => (s === sub ? previous : s))\n : this.subscriptions.filter((s) => s !== sub)\n throw error\n }\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 = options?.deadLetterExchange ?? `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n // Set once on the queue, so every binding's dead letters reach the DLQ.\n const dlqRoutingKey = `${routingKey}.dead`\n const bindings = [{ exchange, routingKey }, ...(options?.bindings ?? [])]\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 const passive = options?.passiveExchanges\n const isPassive = (name: string) => passive === true || (Array.isArray(passive) && passive.includes(name))\n const missing = [...new Set(bindings.map((b) => b.exchange))].filter((name) => !this.assertedExchanges.has(name))\n const toCheck = missing.filter(isPassive)\n if (toCheck.length > 0) {\n await this.checkExchanges(toCheck)\n }\n for (const name of missing.filter((name) => !isPassive(name))) {\n await channel.assertExchange(name, 'topic', { durable: true })\n }\n for (const name of missing) this.assertedExchanges.add(name)\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 for (const binding of bindings) {\n await channel.bindQueue(queue, binding.exchange, binding.routingKey)\n }\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, options)\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { queue, bindings })\n }\n\n // A failed check closes the channel it ran on, so it runs on a throwaway one\n // rather than take down the shared channel and every consumer on it.\n private async checkExchanges(exchanges: string[]): Promise<void> {\n if (!this.connection) {\n throw new Error('Connection not available')\n }\n const probe = await this.connection.createChannel()\n probe.on('error', () => undefined)\n try {\n for (const name of exchanges) {\n await probe.checkExchange(name)\n }\n } finally {\n await probe.close().catch(() => undefined)\n }\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 options: SubscribeOptions | undefined,\n ): void {\n this.inflightCount++\n const acquire = semaphore ? semaphore.acquire() : Promise.resolve()\n acquire\n .then(() => this.handleWithRetry(message, handler, channel, options))\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 options: SubscribeOptions | undefined,\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 const properties: RabbitMQMessageProperties = {\n exchange,\n routingKey,\n headers: message.properties.headers ?? {},\n timestamp: message.properties.timestamp,\n messageId: message.properties.messageId,\n correlationId: message.properties.correlationId,\n }\n\n const maxRetries = options?.maxRetries ?? this.options.maxRetries\n while (attempts < 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, properties)\n } catch (error) {\n attempts++\n // By name, not instanceof: the ESM and CJS builds each define their own class.\n if (error instanceof Error && error.name === 'DeadLetterError') {\n const duration = Date.now() - startTime\n this.logger.warn('Handler dead-lettered the message', { error: error.message, exchange, routingKey })\n if (this.settle(channel, message, 'nack')) {\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'dead_letter_error' })\n }\n return\n }\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,\n })\n if (attempts < maxRetries) {\n await this.sleepUntilClose(this.retryDelayFor(attempts, options))\n if (this.closing || channel !== this.channel) {\n this.logger.warn('Stopped retrying a message on close or reconnect; it will be redelivered', { exchange, routingKey })\n return\n }\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, 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 private retryDelayFor(attempts: number, options: SubscribeOptions | undefined): number {\n const { retryDelay } = this.options\n if (options?.maxRetryDelay === undefined) return retryDelay\n return backoff(retryDelay, attempts, options.maxRetryDelay)\n }\n\n private wakeRetries(): void {\n for (const wake of [...this.retryWakers]) wake()\n }\n\n /** Also cut short by a reconnect, via wakeRetries(). */\n private sleepUntilClose(ms: number): Promise<void> {\n if (this.closing) return Promise.resolve()\n return new Promise((resolve) => {\n const wake = () => {\n clearTimeout(timer)\n this.retryWakers.delete(wake)\n resolve()\n }\n const timer = setTimeout(wake, ms)\n this.retryWakers.add(wake)\n })\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\ntype Level = keyof ILogger\ntype ObjectFirst = (context: object, message: string) => void\n\n/**\n * pino takes `(context, message)` and drops a context passed after the\n * message, and only serializes an Error under `err`. It is the one logger\n * with a `bindings()` method, so it is recognised by that and called its way.\n */\nexport const forLibraryCalls = (logger: ILogger): ILogger => {\n if (typeof (logger as { bindings?: unknown }).bindings !== 'function') return logger\n const call =\n (level: Level) =>\n (message: string, context?: unknown): void => {\n const log = (logger[level] as unknown as ObjectFirst).bind(logger)\n if (typeof context !== 'object' || context === null) return log({}, message)\n const { error, ...rest } = context as { error?: unknown }\n log(error === undefined ? rest : { ...rest, err: error }, message)\n }\n return { debug: call('debug'), info: call('info'), warn: call('warn'), error: call('error') }\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":["name","duration"]}
|
package/dist/testing/index.cjs
CHANGED
|
@@ -50,6 +50,7 @@ var MockRabbitMQChannel = class extends import_events.EventEmitter {
|
|
|
50
50
|
isClosed = false;
|
|
51
51
|
// Stubs
|
|
52
52
|
assertExchange = mockFn().mockResolvedValue({});
|
|
53
|
+
checkExchange = mockFn().mockResolvedValue({});
|
|
53
54
|
assertQueue = mockFn().mockResolvedValue({ queue: "" });
|
|
54
55
|
bindQueue = mockFn().mockResolvedValue({});
|
|
55
56
|
ack = mockFn();
|
|
@@ -100,10 +101,11 @@ var MockRabbitMQChannel = class extends import_events.EventEmitter {
|
|
|
100
101
|
}
|
|
101
102
|
_deliver(content, properties = {}) {
|
|
102
103
|
if (!this._consumeCallback) return;
|
|
104
|
+
const { exchange = "", routingKey = "", ...amqpProperties } = properties;
|
|
103
105
|
this._consumeCallback({
|
|
104
106
|
content,
|
|
105
|
-
fields: { deliveryTag: 1, redelivered: false, exchange
|
|
106
|
-
properties: { headers: {}, ...
|
|
107
|
+
fields: { deliveryTag: 1, redelivered: false, exchange, routingKey, consumerTag: "mock-consumer" },
|
|
108
|
+
properties: { headers: {}, ...amqpProperties }
|
|
107
109
|
});
|
|
108
110
|
}
|
|
109
111
|
/** Emits a close event and marks the channel as closed. */
|
|
@@ -163,6 +165,10 @@ var MockRabbitMQConnection = class extends import_events.EventEmitter {
|
|
|
163
165
|
this._channels.push(ch);
|
|
164
166
|
return Promise.resolve(ch);
|
|
165
167
|
}
|
|
168
|
+
/** Creates an untracked plain channel, as used for passive exchange checks. */
|
|
169
|
+
createChannel() {
|
|
170
|
+
return Promise.resolve(new MockRabbitMQChannel());
|
|
171
|
+
}
|
|
166
172
|
close() {
|
|
167
173
|
this.isConnected = false;
|
|
168
174
|
return Promise.resolve();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["import { EventEmitter } from 'events'\nimport type { RabbitMQMessageProperties } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Framework-agnostic mock function factory\n// ---------------------------------------------------------------------------\n\ninterface MockFn {\n (...args: unknown[]): unknown\n mock: { calls: unknown[][] }\n mockResolvedValue: (val: unknown) => MockFn\n mockImplementation: (fn: (...args: unknown[]) => unknown) => MockFn\n}\n\nfunction mockFn(): MockFn {\n let impl: ((...args: unknown[]) => unknown) | null = null\n const calls: unknown[][] = []\n\n const fn = ((...args: unknown[]) => {\n calls.push(args)\n return impl ? impl(...args) : undefined\n }) as MockFn\n\n fn.mock = { calls }\n\n fn.mockResolvedValue = (val: unknown) => {\n impl = () => Promise.resolve(val)\n return fn\n }\n\n fn.mockImplementation = (newImpl: (...args: unknown[]) => unknown) => {\n impl = newImpl\n return fn\n }\n\n return fn\n}\n\n// ---------------------------------------------------------------------------\n// Published message record\n// ---------------------------------------------------------------------------\n\nexport interface PublishedMessage {\n exchange: string\n routingKey: string\n message: unknown\n options: unknown\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQChannel\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQChannel extends EventEmitter {\n private _publishedMessages: PublishedMessage[] = []\n private _consumeCallback: ((msg: unknown) => void) | null = null\n\n isClosed = false\n\n // Stubs\n readonly assertExchange = mockFn().mockResolvedValue({})\n readonly assertQueue = mockFn().mockResolvedValue({ queue: '' })\n readonly bindQueue = mockFn().mockResolvedValue({})\n readonly ack = mockFn()\n readonly nack = mockFn()\n readonly close = mockFn().mockResolvedValue(undefined)\n readonly waitForConfirms = mockFn().mockResolvedValue(undefined)\n readonly prefetch = mockFn().mockResolvedValue(undefined)\n readonly deleteQueue = mockFn().mockResolvedValue({})\n readonly cancel = mockFn().mockResolvedValue(undefined)\n\n /**\n * Tracks all published messages and stores them for later inspection.\n */\n publish(exchange: string, routingKey: string, content: Buffer, options?: unknown): boolean {\n let message: unknown\n try {\n message = JSON.parse(content.toString())\n } catch {\n message = content.toString()\n }\n this._publishedMessages.push({ exchange, routingKey, message, options })\n return true\n }\n\n private _consumerCounter = 0\n\n /**\n * Stores the consumer callback for use with simulateMessage.\n * Returns a unique consumer tag per call.\n */\n consume(_queue: string, callback: (msg: unknown) => void): Promise<{ consumerTag: string }> {\n this._consumeCallback = callback\n const consumerTag = `mock-consumer-${++this._consumerCounter}`\n return Promise.resolve({ consumerTag })\n }\n\n // ---------------------------------------------------------------------------\n // Simulation helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Delivers a valid JSON message to the registered consume callback, with\n * optional AMQP properties such as `headers` or `timestamp`.\n */\n simulateMessage(content: unknown, properties: Partial<RabbitMQMessageProperties> = {}): void {\n this._deliver(Buffer.from(JSON.stringify(content)), properties)\n }\n\n /**\n * Delivers a message with content that is not valid JSON.\n */\n simulateInvalidJsonMessage(): void {\n this._deliver(Buffer.from('not valid json {'))\n }\n\n private _deliver(content: Buffer, properties: Partial<RabbitMQMessageProperties> = {}): void {\n if (!this._consumeCallback) return\n this._consumeCallback({\n content,\n fields: { deliveryTag: 1, redelivered: false, exchange: '', routingKey: '', consumerTag: 'mock-consumer' },\n properties: { headers: {}, ...properties },\n })\n }\n\n /** Emits a close event and marks the channel as closed. */\n simulateClose(): void {\n this.isClosed = true\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n // ---------------------------------------------------------------------------\n // Inspection helpers\n // ---------------------------------------------------------------------------\n\n getPublishedMessages(): PublishedMessage[] {\n return [...this._publishedMessages]\n }\n\n clearMessages(): void {\n this._publishedMessages = []\n }\n\n /** Resets all state back to initial values. */\n reset(): void {\n this._publishedMessages = []\n this._consumeCallback = null\n this._consumerCounter = 0\n this.isClosed = false\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQConnection\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQConnection extends EventEmitter {\n private _channels: MockRabbitMQChannel[] = []\n\n isConnected = true\n\n constructor() {\n super()\n // Create the initial channel eagerly so `conn.channel` is defined\n // before the first createConfirmChannel call.\n this._channels.push(new MockRabbitMQChannel())\n }\n\n /**\n * Returns the most recently created channel.\n */\n get channel(): MockRabbitMQChannel {\n return this._channels[this._channels.length - 1]\n }\n\n /**\n * Returns all channels created so far (including the initial one).\n */\n get channels(): MockRabbitMQChannel[] {\n return [...this._channels]\n }\n\n get channelCount(): number {\n return this._channels.length\n }\n\n /**\n * Creates a new channel (simulates a reconnection / new confirm-channel).\n */\n createConfirmChannel(): Promise<MockRabbitMQChannel> {\n const ch = new MockRabbitMQChannel()\n this._channels.push(ch)\n return Promise.resolve(ch)\n }\n\n close(): Promise<void> {\n this.isConnected = false\n return Promise.resolve()\n }\n\n /** Emits a close event and marks the connection as disconnected. */\n simulateClose(): void {\n this.isConnected = false\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n /** Resets to the initial connected state with a single fresh channel. */\n reset(): void {\n this._channels = [new MockRabbitMQChannel()]\n this.isConnected = true\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// createMockAmqplib\n// ---------------------------------------------------------------------------\n\nexport interface AmqpMock {\n connect: MockFn\n}\n\nexport interface MockAmqplibResult {\n mockConnection: MockRabbitMQConnection\n amqpMock: AmqpMock\n}\n\n/**\n * Returns a pre-wired { mockConnection, amqpMock } pair that can be used as\n * a drop-in replacement for amqplib in tests (e.g. via vi.mock or jest.mock).\n */\nexport function createMockAmqplib(): MockAmqplibResult {\n const mockConnection = new MockRabbitMQConnection()\n const amqpMock: AmqpMock = {\n connect: mockFn().mockResolvedValue(mockConnection),\n }\n return { mockConnection, amqpMock }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts"],"sourcesContent":["import { EventEmitter } from 'events'\nimport type { RabbitMQMessageProperties } from '../types.js'\n\n// ---------------------------------------------------------------------------\n// Framework-agnostic mock function factory\n// ---------------------------------------------------------------------------\n\ninterface MockFn {\n (...args: unknown[]): unknown\n mock: { calls: unknown[][] }\n mockResolvedValue: (val: unknown) => MockFn\n mockImplementation: (fn: (...args: unknown[]) => unknown) => MockFn\n}\n\nfunction mockFn(): MockFn {\n let impl: ((...args: unknown[]) => unknown) | null = null\n const calls: unknown[][] = []\n\n const fn = ((...args: unknown[]) => {\n calls.push(args)\n return impl ? impl(...args) : undefined\n }) as MockFn\n\n fn.mock = { calls }\n\n fn.mockResolvedValue = (val: unknown) => {\n impl = () => Promise.resolve(val)\n return fn\n }\n\n fn.mockImplementation = (newImpl: (...args: unknown[]) => unknown) => {\n impl = newImpl\n return fn\n }\n\n return fn\n}\n\n// ---------------------------------------------------------------------------\n// Published message record\n// ---------------------------------------------------------------------------\n\nexport interface PublishedMessage {\n exchange: string\n routingKey: string\n message: unknown\n options: unknown\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQChannel\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQChannel extends EventEmitter {\n private _publishedMessages: PublishedMessage[] = []\n private _consumeCallback: ((msg: unknown) => void) | null = null\n\n isClosed = false\n\n // Stubs\n readonly assertExchange = mockFn().mockResolvedValue({})\n readonly checkExchange = mockFn().mockResolvedValue({})\n readonly assertQueue = mockFn().mockResolvedValue({ queue: '' })\n readonly bindQueue = mockFn().mockResolvedValue({})\n readonly ack = mockFn()\n readonly nack = mockFn()\n readonly close = mockFn().mockResolvedValue(undefined)\n readonly waitForConfirms = mockFn().mockResolvedValue(undefined)\n readonly prefetch = mockFn().mockResolvedValue(undefined)\n readonly deleteQueue = mockFn().mockResolvedValue({})\n readonly cancel = mockFn().mockResolvedValue(undefined)\n\n /**\n * Tracks all published messages and stores them for later inspection.\n */\n publish(exchange: string, routingKey: string, content: Buffer, options?: unknown): boolean {\n let message: unknown\n try {\n message = JSON.parse(content.toString())\n } catch {\n message = content.toString()\n }\n this._publishedMessages.push({ exchange, routingKey, message, options })\n return true\n }\n\n private _consumerCounter = 0\n\n /**\n * Stores the consumer callback for use with simulateMessage.\n * Returns a unique consumer tag per call.\n */\n consume(_queue: string, callback: (msg: unknown) => void): Promise<{ consumerTag: string }> {\n this._consumeCallback = callback\n const consumerTag = `mock-consumer-${++this._consumerCounter}`\n return Promise.resolve({ consumerTag })\n }\n\n // ---------------------------------------------------------------------------\n // Simulation helpers\n // ---------------------------------------------------------------------------\n\n /**\n * Delivers a valid JSON message to the registered consume callback, with\n * optional AMQP properties such as `headers` or `timestamp`.\n */\n simulateMessage(content: unknown, properties: Partial<RabbitMQMessageProperties> = {}): void {\n this._deliver(Buffer.from(JSON.stringify(content)), properties)\n }\n\n /**\n * Delivers a message with content that is not valid JSON.\n */\n simulateInvalidJsonMessage(): void {\n this._deliver(Buffer.from('not valid json {'))\n }\n\n private _deliver(content: Buffer, properties: Partial<RabbitMQMessageProperties> = {}): void {\n if (!this._consumeCallback) return\n const { exchange = '', routingKey = '', ...amqpProperties } = properties\n this._consumeCallback({\n content,\n fields: { deliveryTag: 1, redelivered: false, exchange, routingKey, consumerTag: 'mock-consumer' },\n properties: { headers: {}, ...amqpProperties },\n })\n }\n\n /** Emits a close event and marks the channel as closed. */\n simulateClose(): void {\n this.isClosed = true\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n // ---------------------------------------------------------------------------\n // Inspection helpers\n // ---------------------------------------------------------------------------\n\n getPublishedMessages(): PublishedMessage[] {\n return [...this._publishedMessages]\n }\n\n clearMessages(): void {\n this._publishedMessages = []\n }\n\n /** Resets all state back to initial values. */\n reset(): void {\n this._publishedMessages = []\n this._consumeCallback = null\n this._consumerCounter = 0\n this.isClosed = false\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// MockRabbitMQConnection\n// ---------------------------------------------------------------------------\n\nexport class MockRabbitMQConnection extends EventEmitter {\n private _channels: MockRabbitMQChannel[] = []\n\n isConnected = true\n\n constructor() {\n super()\n // Create the initial channel eagerly so `conn.channel` is defined\n // before the first createConfirmChannel call.\n this._channels.push(new MockRabbitMQChannel())\n }\n\n /**\n * Returns the most recently created channel.\n */\n get channel(): MockRabbitMQChannel {\n return this._channels[this._channels.length - 1]\n }\n\n /**\n * Returns all channels created so far (including the initial one).\n */\n get channels(): MockRabbitMQChannel[] {\n return [...this._channels]\n }\n\n get channelCount(): number {\n return this._channels.length\n }\n\n /**\n * Creates a new channel (simulates a reconnection / new confirm-channel).\n */\n createConfirmChannel(): Promise<MockRabbitMQChannel> {\n const ch = new MockRabbitMQChannel()\n this._channels.push(ch)\n return Promise.resolve(ch)\n }\n\n /** Creates an untracked plain channel, as used for passive exchange checks. */\n createChannel(): Promise<MockRabbitMQChannel> {\n return Promise.resolve(new MockRabbitMQChannel())\n }\n\n close(): Promise<void> {\n this.isConnected = false\n return Promise.resolve()\n }\n\n /** Emits a close event and marks the connection as disconnected. */\n simulateClose(): void {\n this.isConnected = false\n this.emit('close')\n }\n\n /** Emits an error event. */\n simulateError(error: Error): void {\n this.emit('error', error)\n }\n\n /** Resets to the initial connected state with a single fresh channel. */\n reset(): void {\n this._channels = [new MockRabbitMQChannel()]\n this.isConnected = true\n this.removeAllListeners()\n }\n}\n\n// ---------------------------------------------------------------------------\n// createMockAmqplib\n// ---------------------------------------------------------------------------\n\nexport interface AmqpMock {\n connect: MockFn\n}\n\nexport interface MockAmqplibResult {\n mockConnection: MockRabbitMQConnection\n amqpMock: AmqpMock\n}\n\n/**\n * Returns a pre-wired { mockConnection, amqpMock } pair that can be used as\n * a drop-in replacement for amqplib in tests (e.g. via vi.mock or jest.mock).\n */\nexport function createMockAmqplib(): MockAmqplibResult {\n const mockConnection = new MockRabbitMQConnection()\n const amqpMock: AmqpMock = {\n connect: mockFn().mockResolvedValue(mockConnection),\n }\n return { mockConnection, amqpMock }\n}\n"],"mappings":"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","names":[]}
|