@linagora/rabbitmq-client 0.4.0 → 0.5.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 +130 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +129 -31
- 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-Cjg-65Ef.d.cts} +27 -2
- package/dist/{types-D29GyoZX.d.ts → types-Cjg-65Ef.d.ts} +27 -2
- package/package.json +1 -1
package/dist/index.d.cts
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-Cjg-65Ef.cjs';
|
|
2
|
+
export { c as RabbitMQBinding, d as RabbitMQHooks, e as RabbitMQMessageProperties, f as RabbitMQSubscription } from './types-Cjg-65Ef.cjs';
|
|
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
|
|
@@ -24,6 +30,7 @@ declare class RabbitMQClient {
|
|
|
24
30
|
private connection;
|
|
25
31
|
private channel;
|
|
26
32
|
private connected;
|
|
33
|
+
private closing;
|
|
27
34
|
private subscriptions;
|
|
28
35
|
private readonly options;
|
|
29
36
|
private readonly logger;
|
|
@@ -34,6 +41,7 @@ declare class RabbitMQClient {
|
|
|
34
41
|
private returnWaiters;
|
|
35
42
|
private consumerTags;
|
|
36
43
|
private subscriptionSemaphores;
|
|
44
|
+
private retryWakers;
|
|
37
45
|
private inflightCount;
|
|
38
46
|
private drainResolve;
|
|
39
47
|
constructor(options: RabbitMQClientOptions);
|
|
@@ -78,6 +86,7 @@ declare class RabbitMQClient {
|
|
|
78
86
|
*/
|
|
79
87
|
unsubscribe(queue: string): Promise<void>;
|
|
80
88
|
private setupSubscription;
|
|
89
|
+
private checkExchanges;
|
|
81
90
|
/**
|
|
82
91
|
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
83
92
|
* processes it. A message counts as in-flight from delivery until its handler
|
|
@@ -104,6 +113,10 @@ declare class RabbitMQClient {
|
|
|
104
113
|
/** Invokes a hook callback, swallowing errors so hooks never break message flow. */
|
|
105
114
|
private callHook;
|
|
106
115
|
private sleep;
|
|
116
|
+
private retryDelayFor;
|
|
117
|
+
private wakeRetries;
|
|
118
|
+
/** Also cut short by a reconnect, via wakeRetries(). */
|
|
119
|
+
private sleepUntilClose;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/**
|
|
@@ -122,4 +135,4 @@ declare const defaultLogger: ILogger;
|
|
|
122
135
|
*/
|
|
123
136
|
declare const silentLogger: ILogger;
|
|
124
137
|
|
|
125
|
-
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.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-Cjg-65Ef.js';
|
|
2
|
+
export { c as RabbitMQBinding, d as RabbitMQHooks, e as RabbitMQMessageProperties, f as RabbitMQSubscription } from './types-Cjg-65Ef.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
|
|
@@ -24,6 +30,7 @@ declare class RabbitMQClient {
|
|
|
24
30
|
private connection;
|
|
25
31
|
private channel;
|
|
26
32
|
private connected;
|
|
33
|
+
private closing;
|
|
27
34
|
private subscriptions;
|
|
28
35
|
private readonly options;
|
|
29
36
|
private readonly logger;
|
|
@@ -34,6 +41,7 @@ declare class RabbitMQClient {
|
|
|
34
41
|
private returnWaiters;
|
|
35
42
|
private consumerTags;
|
|
36
43
|
private subscriptionSemaphores;
|
|
44
|
+
private retryWakers;
|
|
37
45
|
private inflightCount;
|
|
38
46
|
private drainResolve;
|
|
39
47
|
constructor(options: RabbitMQClientOptions);
|
|
@@ -78,6 +86,7 @@ declare class RabbitMQClient {
|
|
|
78
86
|
*/
|
|
79
87
|
unsubscribe(queue: string): Promise<void>;
|
|
80
88
|
private setupSubscription;
|
|
89
|
+
private checkExchanges;
|
|
81
90
|
/**
|
|
82
91
|
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
83
92
|
* processes it. A message counts as in-flight from delivery until its handler
|
|
@@ -104,6 +113,10 @@ declare class RabbitMQClient {
|
|
|
104
113
|
/** Invokes a hook callback, swallowing errors so hooks never break message flow. */
|
|
105
114
|
private callHook;
|
|
106
115
|
private sleep;
|
|
116
|
+
private retryDelayFor;
|
|
117
|
+
private wakeRetries;
|
|
118
|
+
/** Also cut short by a reconnect, via wakeRetries(). */
|
|
119
|
+
private sleepUntilClose;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/**
|
|
@@ -122,4 +135,4 @@ declare const defaultLogger: ILogger;
|
|
|
122
135
|
*/
|
|
123
136
|
declare const silentLogger: ILogger;
|
|
124
137
|
|
|
125
|
-
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,10 +102,17 @@ 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;
|
|
105
114
|
connected = false;
|
|
115
|
+
closing = false;
|
|
106
116
|
subscriptions = [];
|
|
107
117
|
options;
|
|
108
118
|
logger;
|
|
@@ -118,6 +128,7 @@ var RabbitMQClient = class {
|
|
|
118
128
|
// connection drops keep holding their permits, and the new consumer cannot
|
|
119
129
|
// exceed the ceiling while the old pipeline drains. `null` = no limit.
|
|
120
130
|
subscriptionSemaphores = /* @__PURE__ */ new Map();
|
|
131
|
+
retryWakers = /* @__PURE__ */ new Set();
|
|
121
132
|
inflightCount = 0;
|
|
122
133
|
drainResolve = null;
|
|
123
134
|
constructor(options) {
|
|
@@ -162,34 +173,43 @@ var RabbitMQClient = class {
|
|
|
162
173
|
async doConnect(maxAttempts) {
|
|
163
174
|
this.assertedExchanges.clear();
|
|
164
175
|
this.consumerTags.clear();
|
|
176
|
+
this.connection?.close().catch(() => void 0);
|
|
177
|
+
this.connection = null;
|
|
165
178
|
let attempts = 0;
|
|
166
|
-
while (!this.connected) {
|
|
179
|
+
while (!this.connected && !this.closing) {
|
|
167
180
|
try {
|
|
168
|
-
|
|
181
|
+
const connection = await amqp.connect(this.options.url);
|
|
182
|
+
this.connection = connection;
|
|
169
183
|
this.logger.info("Connected to server");
|
|
170
|
-
|
|
184
|
+
const channel = await connection.createConfirmChannel();
|
|
185
|
+
this.channel = channel;
|
|
186
|
+
this.wakeRetries();
|
|
171
187
|
const returnWaiters = /* @__PURE__ */ new Set();
|
|
172
188
|
this.returnWaiters = returnWaiters;
|
|
173
|
-
|
|
189
|
+
channel.on("return", (msg) => {
|
|
174
190
|
for (const waiter of returnWaiters) waiter(msg);
|
|
175
191
|
});
|
|
176
192
|
this.logger.info("Confirm channel created");
|
|
177
|
-
await
|
|
193
|
+
await channel.prefetch(this.options.prefetch);
|
|
178
194
|
this.logger.info("Channel prefetch set", { prefetch: this.options.prefetch });
|
|
179
|
-
|
|
195
|
+
connection.on("error", (error) => {
|
|
196
|
+
if (connection !== this.connection) return;
|
|
180
197
|
this.connected = false;
|
|
181
198
|
this.logger.error("Connection error", { error });
|
|
182
199
|
});
|
|
183
|
-
|
|
200
|
+
connection.on("close", () => {
|
|
201
|
+
if (connection !== this.connection) return;
|
|
184
202
|
this.connected = false;
|
|
185
203
|
this.logger.warn("Connection closed");
|
|
186
204
|
this.reconnectWithRetry();
|
|
187
205
|
});
|
|
188
|
-
|
|
206
|
+
channel.on("error", (error) => {
|
|
207
|
+
if (channel !== this.channel) return;
|
|
189
208
|
this.logger.error("Channel error", { error });
|
|
190
209
|
this.handleChannelFailure();
|
|
191
210
|
});
|
|
192
|
-
|
|
211
|
+
channel.on("close", () => {
|
|
212
|
+
if (channel !== this.channel) return;
|
|
193
213
|
this.logger.warn("Channel closed");
|
|
194
214
|
this.handleChannelFailure();
|
|
195
215
|
});
|
|
@@ -213,6 +233,9 @@ var RabbitMQClient = class {
|
|
|
213
233
|
await this.sleep(this.options.connectionRetryDelay);
|
|
214
234
|
}
|
|
215
235
|
}
|
|
236
|
+
if (!this.connected) {
|
|
237
|
+
throw new Error("RabbitMQ client closed while connecting");
|
|
238
|
+
}
|
|
216
239
|
}
|
|
217
240
|
handleChannelFailure() {
|
|
218
241
|
if (this.connected) {
|
|
@@ -299,10 +322,7 @@ var RabbitMQClient = class {
|
|
|
299
322
|
`Failed to publish to ${exchange || "(default exchange)"}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`
|
|
300
323
|
);
|
|
301
324
|
}
|
|
302
|
-
const retryDelay =
|
|
303
|
-
baseDelay * Math.pow(2, attempts - 1),
|
|
304
|
-
MAX_PUBLISH_RETRY_DELAY_MS
|
|
305
|
-
);
|
|
325
|
+
const retryDelay = backoff(baseDelay, attempts, MAX_PUBLISH_RETRY_DELAY_MS);
|
|
306
326
|
this.logger.warn("Publish attempt failed, retrying", { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay });
|
|
307
327
|
await this.sleep(retryDelay);
|
|
308
328
|
}
|
|
@@ -315,13 +335,19 @@ var RabbitMQClient = class {
|
|
|
315
335
|
* `init()` / reconnect cycle.
|
|
316
336
|
*/
|
|
317
337
|
async close(clearSubscriptions = true) {
|
|
338
|
+
this.closing = true;
|
|
318
339
|
try {
|
|
340
|
+
await Promise.allSettled([this.initializationPromise, this.reconnectionPromise]);
|
|
341
|
+
this.wakeRetries();
|
|
319
342
|
if (this.inflightCount > 0) {
|
|
320
343
|
this.logger.info("Waiting for in-flight messages to drain", { inflightCount: this.inflightCount });
|
|
321
344
|
await this.waitForDrain(this.options.closeTimeout);
|
|
322
345
|
}
|
|
323
|
-
|
|
324
|
-
|
|
346
|
+
try {
|
|
347
|
+
await this.channel?.close();
|
|
348
|
+
} finally {
|
|
349
|
+
await this.connection?.close();
|
|
350
|
+
}
|
|
325
351
|
this.connection = null;
|
|
326
352
|
this.channel = null;
|
|
327
353
|
this.connected = false;
|
|
@@ -337,9 +363,12 @@ var RabbitMQClient = class {
|
|
|
337
363
|
} catch (error) {
|
|
338
364
|
this.logger.error("Error closing connection", { error });
|
|
339
365
|
throw error;
|
|
366
|
+
} finally {
|
|
367
|
+
this.closing = false;
|
|
340
368
|
}
|
|
341
369
|
}
|
|
342
370
|
async reconnectWithRetry() {
|
|
371
|
+
if (this.closing) return;
|
|
343
372
|
if (this.reconnectionPromise) {
|
|
344
373
|
return this.reconnectionPromise;
|
|
345
374
|
}
|
|
@@ -349,6 +378,8 @@ var RabbitMQClient = class {
|
|
|
349
378
|
this.reconnectionPromise.catch(() => void 0);
|
|
350
379
|
try {
|
|
351
380
|
await this.reconnectionPromise;
|
|
381
|
+
} catch (error) {
|
|
382
|
+
if (!this.closing) throw error;
|
|
352
383
|
} finally {
|
|
353
384
|
this.reconnectionPromise = null;
|
|
354
385
|
}
|
|
@@ -400,13 +431,19 @@ var RabbitMQClient = class {
|
|
|
400
431
|
}
|
|
401
432
|
const sub = { exchange, routingKey, queue, handler, options };
|
|
402
433
|
const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue);
|
|
434
|
+
const previous = existingIndex === -1 ? void 0 : this.subscriptions[existingIndex];
|
|
403
435
|
if (existingIndex === -1) {
|
|
404
436
|
this.subscriptions.push(sub);
|
|
405
437
|
} else {
|
|
406
438
|
this.subscriptions[existingIndex] = sub;
|
|
407
439
|
this.subscriptionSemaphores.delete(queue);
|
|
408
440
|
}
|
|
409
|
-
|
|
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
|
+
}
|
|
410
447
|
}
|
|
411
448
|
/**
|
|
412
449
|
* Cancels a queue subscription and removes it from the restoration list.
|
|
@@ -427,16 +464,22 @@ var RabbitMQClient = class {
|
|
|
427
464
|
}
|
|
428
465
|
const channel = this.channel;
|
|
429
466
|
const { exchange, routingKey, queue, handler, options } = sub;
|
|
430
|
-
const dlxExchange = `${exchange}.dlx`;
|
|
467
|
+
const dlxExchange = options?.deadLetterExchange ?? `${exchange}.dlx`;
|
|
431
468
|
const dlqQueue = `${queue}.dlq`;
|
|
432
469
|
const dlqRoutingKey = `${routingKey}.dead`;
|
|
470
|
+
const bindings = [{ exchange, routingKey }, ...options?.bindings ?? []];
|
|
433
471
|
await channel.assertExchange(dlxExchange, "topic", { durable: true });
|
|
434
472
|
await channel.assertQueue(dlqQueue, { durable: true });
|
|
435
473
|
await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.
|
|
474
|
+
const missing = bindings.map((b) => b.exchange).filter((name) => !this.assertedExchanges.has(name));
|
|
475
|
+
if (options?.passiveExchanges && missing.length > 0) {
|
|
476
|
+
await this.checkExchanges(missing);
|
|
477
|
+
} else {
|
|
478
|
+
for (const name of missing) {
|
|
479
|
+
await channel.assertExchange(name, "topic", { durable: true });
|
|
480
|
+
}
|
|
439
481
|
}
|
|
482
|
+
for (const name of missing) this.assertedExchanges.add(name);
|
|
440
483
|
const queueType = options?.queueArguments?.["x-queue-type"] ?? "quorum";
|
|
441
484
|
const queueArgs = {
|
|
442
485
|
"x-queue-type": queueType,
|
|
@@ -451,7 +494,9 @@ var RabbitMQClient = class {
|
|
|
451
494
|
deadLetterRoutingKey: dlqRoutingKey,
|
|
452
495
|
arguments: { ...queueArgs, ...options?.queueArguments }
|
|
453
496
|
});
|
|
454
|
-
|
|
497
|
+
for (const binding of bindings) {
|
|
498
|
+
await channel.bindQueue(queue, binding.exchange, binding.routingKey);
|
|
499
|
+
}
|
|
455
500
|
if (!this.subscriptionSemaphores.has(queue)) {
|
|
456
501
|
const concurrency = options?.concurrency ?? this.options.concurrency;
|
|
457
502
|
this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null);
|
|
@@ -461,13 +506,29 @@ var RabbitMQClient = class {
|
|
|
461
506
|
queue,
|
|
462
507
|
(message) => {
|
|
463
508
|
if (message) {
|
|
464
|
-
this.dispatch(message, handler, semaphore, channel);
|
|
509
|
+
this.dispatch(message, handler, semaphore, channel, options);
|
|
465
510
|
}
|
|
466
511
|
},
|
|
467
512
|
{ noAck: false }
|
|
468
513
|
);
|
|
469
514
|
this.consumerTags.set(queue, consumerTag);
|
|
470
|
-
this.logger.info("Subscribed to queue", {
|
|
515
|
+
this.logger.info("Subscribed to queue", { queue, bindings });
|
|
516
|
+
}
|
|
517
|
+
// A failed check closes the channel it ran on, so it runs on a throwaway one
|
|
518
|
+
// rather than take down the shared channel and every consumer on it.
|
|
519
|
+
async checkExchanges(exchanges) {
|
|
520
|
+
if (!this.connection) {
|
|
521
|
+
throw new Error("Connection not available");
|
|
522
|
+
}
|
|
523
|
+
const probe = await this.connection.createChannel();
|
|
524
|
+
probe.on("error", () => void 0);
|
|
525
|
+
try {
|
|
526
|
+
for (const name of exchanges) {
|
|
527
|
+
await probe.checkExchange(name);
|
|
528
|
+
}
|
|
529
|
+
} finally {
|
|
530
|
+
await probe.close().catch(() => void 0);
|
|
531
|
+
}
|
|
471
532
|
}
|
|
472
533
|
/**
|
|
473
534
|
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
@@ -475,10 +536,10 @@ var RabbitMQClient = class {
|
|
|
475
536
|
* settles (including time spent waiting for a permit), so graceful `close()`
|
|
476
537
|
* drains queued messages as well as actively-processing ones.
|
|
477
538
|
*/
|
|
478
|
-
dispatch(message, handler, semaphore, channel) {
|
|
539
|
+
dispatch(message, handler, semaphore, channel, options) {
|
|
479
540
|
this.inflightCount++;
|
|
480
541
|
const acquire = semaphore ? semaphore.acquire() : Promise.resolve();
|
|
481
|
-
acquire.then(() => this.handleWithRetry(message, handler, channel)).catch((error) => {
|
|
542
|
+
acquire.then(() => this.handleWithRetry(message, handler, channel, options)).catch((error) => {
|
|
482
543
|
this.logger.error("Unhandled error in message handler", { error });
|
|
483
544
|
}).finally(() => {
|
|
484
545
|
semaphore?.release();
|
|
@@ -488,7 +549,7 @@ var RabbitMQClient = class {
|
|
|
488
549
|
}
|
|
489
550
|
});
|
|
490
551
|
}
|
|
491
|
-
async handleWithRetry(message, handler, channel) {
|
|
552
|
+
async handleWithRetry(message, handler, channel, options) {
|
|
492
553
|
if (channel !== this.channel) {
|
|
493
554
|
this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
|
|
494
555
|
return;
|
|
@@ -516,26 +577,41 @@ var RabbitMQClient = class {
|
|
|
516
577
|
}
|
|
517
578
|
this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
|
|
518
579
|
const properties = {
|
|
580
|
+
exchange,
|
|
581
|
+
routingKey,
|
|
519
582
|
headers: message.properties.headers ?? {},
|
|
520
583
|
timestamp: message.properties.timestamp,
|
|
521
584
|
messageId: message.properties.messageId,
|
|
522
585
|
correlationId: message.properties.correlationId
|
|
523
586
|
};
|
|
524
|
-
|
|
587
|
+
const maxRetries = options?.maxRetries ?? this.options.maxRetries;
|
|
588
|
+
while (attempts < maxRetries) {
|
|
525
589
|
try {
|
|
526
590
|
await handler(content, properties);
|
|
527
591
|
} catch (error) {
|
|
528
592
|
attempts++;
|
|
593
|
+
if (error instanceof Error && error.name === "DeadLetterError") {
|
|
594
|
+
const duration3 = Date.now() - startTime;
|
|
595
|
+
this.logger.warn("Handler dead-lettered the message", { error: error.message, exchange, routingKey });
|
|
596
|
+
if (this.settle(channel, message, "nack")) {
|
|
597
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: duration3, reason: "dead_letter_error" });
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
529
601
|
this.logger.error("Handler failed", {
|
|
530
602
|
error: error instanceof Error ? error.message : error,
|
|
531
603
|
stack: error instanceof Error ? error.stack : void 0,
|
|
532
604
|
exchange,
|
|
533
605
|
routingKey,
|
|
534
606
|
attempt: attempts,
|
|
535
|
-
maxRetries
|
|
607
|
+
maxRetries
|
|
536
608
|
});
|
|
537
|
-
if (attempts <
|
|
538
|
-
await this.
|
|
609
|
+
if (attempts < maxRetries) {
|
|
610
|
+
await this.sleepUntilClose(this.retryDelayFor(attempts, options));
|
|
611
|
+
if (this.closing || channel !== this.channel) {
|
|
612
|
+
this.logger.warn("Stopped retrying a message on close or reconnect; it will be redelivered", { exchange, routingKey });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
539
615
|
}
|
|
540
616
|
continue;
|
|
541
617
|
}
|
|
@@ -547,7 +623,7 @@ var RabbitMQClient = class {
|
|
|
547
623
|
return;
|
|
548
624
|
}
|
|
549
625
|
const duration = Date.now() - startTime;
|
|
550
|
-
this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries
|
|
626
|
+
this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries, duration });
|
|
551
627
|
if (this.settle(channel, message, "nack")) {
|
|
552
628
|
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
|
|
553
629
|
}
|
|
@@ -628,8 +704,30 @@ var RabbitMQClient = class {
|
|
|
628
704
|
sleep(ms) {
|
|
629
705
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
630
706
|
}
|
|
707
|
+
retryDelayFor(attempts, options) {
|
|
708
|
+
const { retryDelay } = this.options;
|
|
709
|
+
if (options?.maxRetryDelay === void 0) return retryDelay;
|
|
710
|
+
return backoff(retryDelay, attempts, options.maxRetryDelay);
|
|
711
|
+
}
|
|
712
|
+
wakeRetries() {
|
|
713
|
+
for (const wake of [...this.retryWakers]) wake();
|
|
714
|
+
}
|
|
715
|
+
/** Also cut short by a reconnect, via wakeRetries(). */
|
|
716
|
+
sleepUntilClose(ms) {
|
|
717
|
+
if (this.closing) return Promise.resolve();
|
|
718
|
+
return new Promise((resolve) => {
|
|
719
|
+
const wake = () => {
|
|
720
|
+
clearTimeout(timer);
|
|
721
|
+
this.retryWakers.delete(wake);
|
|
722
|
+
resolve();
|
|
723
|
+
};
|
|
724
|
+
const timer = setTimeout(wake, ms);
|
|
725
|
+
this.retryWakers.add(wake);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
631
728
|
};
|
|
632
729
|
export {
|
|
730
|
+
DeadLetterError,
|
|
633
731
|
RabbitMQClient,
|
|
634
732
|
UnroutableMessageError,
|
|
635
733
|
defaultLogger,
|