@linagora/rabbitmq-client 0.1.2 → 0.2.1
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 +12 -2
- package/dist/index.cjs +186 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -5
- package/dist/index.d.ts +60 -5
- package/dist/index.js +183 -121
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -37,9 +37,10 @@ Only `url` is required. Everything else has defaults.
|
|
|
37
37
|
| `connectionRetryDelay` | `5000` | Milliseconds between reconnection attempts |
|
|
38
38
|
| `initMaxAttempts` | `5` | Connection attempts on startup before giving up |
|
|
39
39
|
| `publishMaxAttempts` | `5` | Publish retries (exponential backoff, capped at 60s) |
|
|
40
|
-
| `prefetch` | `10` | Channel prefetch
|
|
40
|
+
| `prefetch` | `10` | Channel prefetch: how many messages the broker delivers into consumer memory before waiting for acks |
|
|
41
|
+
| `concurrency` | = `prefetch` | How many handlers run at once per subscription. Prefetch buffers messages in memory; concurrency bounds how many are processed simultaneously. Only throttles when set below `prefetch` (the broker never delivers more than `prefetch` unacked at a time); `0` means no limit. Override per queue via `subscribe(..., { concurrency })` |
|
|
41
42
|
| `closeTimeout` | `5000` | Milliseconds to wait for in-flight messages when closing |
|
|
42
|
-
| `logger` |
|
|
43
|
+
| `logger` | console (warn/error) | Any `ILogger` (`console`, Winston, pino, Bunyan). Omitted: logs warn/error to the console, stays quiet otherwise. Pass `silentLogger` for no output. |
|
|
43
44
|
| `hooks` | -- | Observability callbacks (see [Hooks](#hooks)) |
|
|
44
45
|
|
|
45
46
|
## How it works
|
|
@@ -79,6 +80,15 @@ await client.subscribe('events', 'order.placed', 'order-queue', handler, {
|
|
|
79
80
|
|
|
80
81
|
Custom arguments are merged with the DLQ wiring defaults, so you can swap the queue type or add a max-length without losing dead-letter routing.
|
|
81
82
|
|
|
83
|
+
To limit how many messages this queue processes at once (for example, to protect a downstream database from a burst), set `concurrency` on the subscription. It defaults to the client-level `concurrency`, which in turn defaults to `prefetch`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Buffer up to 50 messages in memory, but process at most 4 at a time.
|
|
87
|
+
await client.subscribe('events', 'order.placed', 'order-queue', handler, {
|
|
88
|
+
concurrency: 4,
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
82
92
|
### Unsubscribing
|
|
83
93
|
|
|
84
94
|
Cancel a consumer and remove it from the auto-restoration list:
|
package/dist/index.cjs
CHANGED
|
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var src_exports = {};
|
|
32
32
|
__export(src_exports, {
|
|
33
|
-
RabbitMQClient: () => RabbitMQClient
|
|
33
|
+
RabbitMQClient: () => RabbitMQClient,
|
|
34
|
+
defaultLogger: () => defaultLogger,
|
|
35
|
+
silentLogger: () => silentLogger
|
|
34
36
|
});
|
|
35
37
|
module.exports = __toCommonJS(src_exports);
|
|
36
38
|
|
|
@@ -38,12 +40,69 @@ module.exports = __toCommonJS(src_exports);
|
|
|
38
40
|
var import_amqplib = __toESM(require("amqplib"), 1);
|
|
39
41
|
|
|
40
42
|
// src/logger.ts
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
var defaultLogger = {
|
|
44
|
+
debug: () => {
|
|
45
|
+
},
|
|
46
|
+
info: () => {
|
|
47
|
+
},
|
|
48
|
+
warn: (message, ...args) => console.warn("[rabbitmq-client]", message, ...args),
|
|
49
|
+
error: (message, ...args) => console.error("[rabbitmq-client]", message, ...args)
|
|
50
|
+
};
|
|
51
|
+
var silentLogger = {
|
|
52
|
+
debug: () => {
|
|
53
|
+
},
|
|
54
|
+
info: () => {
|
|
55
|
+
},
|
|
56
|
+
warn: () => {
|
|
57
|
+
},
|
|
58
|
+
error: () => {
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/semaphore.ts
|
|
63
|
+
var Semaphore = class {
|
|
64
|
+
permits;
|
|
65
|
+
waiters = [];
|
|
66
|
+
constructor(permits) {
|
|
67
|
+
if (!Number.isInteger(permits) || permits < 1) {
|
|
68
|
+
throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`);
|
|
69
|
+
}
|
|
70
|
+
this.permits = permits;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Acquires a permit, resolving immediately when one is free or queueing
|
|
74
|
+
* (FIFO) until another holder releases. Always pair with `release()` in a
|
|
75
|
+
* `finally` so a throwing handler never leaks a permit.
|
|
76
|
+
*/
|
|
77
|
+
acquire() {
|
|
78
|
+
if (this.permits > 0) {
|
|
79
|
+
this.permits--;
|
|
80
|
+
return Promise.resolve();
|
|
81
|
+
}
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
this.waiters.push(resolve);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Releases a permit. Hands it directly to the next waiter if any are queued,
|
|
88
|
+
* otherwise returns it to the pool.
|
|
89
|
+
*/
|
|
90
|
+
release() {
|
|
91
|
+
const next = this.waiters.shift();
|
|
92
|
+
if (next) {
|
|
93
|
+
next();
|
|
94
|
+
} else {
|
|
95
|
+
this.permits++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Permits currently available. Exposed for tests and introspection. */
|
|
99
|
+
get available() {
|
|
100
|
+
return this.permits;
|
|
101
|
+
}
|
|
102
|
+
/** Number of callers currently queued waiting for a permit. */
|
|
103
|
+
get waiting() {
|
|
104
|
+
return this.waiters.length;
|
|
45
105
|
}
|
|
46
|
-
return (0, import_pino.default)({ name: "rabbitmq-client" });
|
|
47
106
|
};
|
|
48
107
|
|
|
49
108
|
// src/client.ts
|
|
@@ -70,6 +129,11 @@ var RabbitMQClient = class {
|
|
|
70
129
|
reconnectionPromise = null;
|
|
71
130
|
assertedExchanges = /* @__PURE__ */ new Set();
|
|
72
131
|
consumerTags = /* @__PURE__ */ new Map();
|
|
132
|
+
// Per-queue concurrency limiter, keyed by queue name. Persisted across
|
|
133
|
+
// reconnects (unlike consumerTags) so handlers still running when a
|
|
134
|
+
// connection drops keep holding their permits, and the new consumer cannot
|
|
135
|
+
// exceed the ceiling while the old pipeline drains. `null` = no limit.
|
|
136
|
+
subscriptionSemaphores = /* @__PURE__ */ new Map();
|
|
73
137
|
inflightCount = 0;
|
|
74
138
|
drainResolve = null;
|
|
75
139
|
constructor(options) {
|
|
@@ -81,9 +145,14 @@ var RabbitMQClient = class {
|
|
|
81
145
|
initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,
|
|
82
146
|
publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,
|
|
83
147
|
prefetch: options.prefetch ?? DEFAULTS.prefetch,
|
|
148
|
+
// Default concurrency tracks prefetch, preserving the previous behaviour
|
|
149
|
+
// (handlers ran fire-and-forget, so up to `prefetch` ran at once) while
|
|
150
|
+
// making the ceiling explicit and independently tunable. A value <= 0
|
|
151
|
+
// (including an unlimited `prefetch: 0`) means "no concurrency limit".
|
|
152
|
+
concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,
|
|
84
153
|
closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout
|
|
85
154
|
};
|
|
86
|
-
this.logger =
|
|
155
|
+
this.logger = options.logger ?? defaultLogger;
|
|
87
156
|
this.hooks = options.hooks ?? {};
|
|
88
157
|
}
|
|
89
158
|
/**
|
|
@@ -117,10 +186,10 @@ var RabbitMQClient = class {
|
|
|
117
186
|
this.channel = await this.connection.createConfirmChannel();
|
|
118
187
|
this.logger.info("Confirm channel created");
|
|
119
188
|
await this.channel.prefetch(this.options.prefetch);
|
|
120
|
-
this.logger.info({ prefetch: this.options.prefetch }
|
|
189
|
+
this.logger.info("Channel prefetch set", { prefetch: this.options.prefetch });
|
|
121
190
|
this.connection.on("error", (error) => {
|
|
122
191
|
this.connected = false;
|
|
123
|
-
this.logger.error(
|
|
192
|
+
this.logger.error("Connection error", { error });
|
|
124
193
|
});
|
|
125
194
|
this.connection.on("close", () => {
|
|
126
195
|
this.connected = false;
|
|
@@ -128,7 +197,7 @@ var RabbitMQClient = class {
|
|
|
128
197
|
this.reconnectWithRetry();
|
|
129
198
|
});
|
|
130
199
|
this.channel.on("error", (error) => {
|
|
131
|
-
this.logger.error(
|
|
200
|
+
this.logger.error("Channel error", { error });
|
|
132
201
|
this.handleChannelFailure();
|
|
133
202
|
});
|
|
134
203
|
this.channel.on("close", () => {
|
|
@@ -141,20 +210,17 @@ var RabbitMQClient = class {
|
|
|
141
210
|
attempts++;
|
|
142
211
|
this.connected = false;
|
|
143
212
|
if (maxAttempts !== void 0 && attempts >= maxAttempts) {
|
|
144
|
-
this.logger.error({ error, attempts, maxAttempts }
|
|
213
|
+
this.logger.error("Connection failed after maximum attempts", { error, attempts, maxAttempts });
|
|
145
214
|
throw new Error(
|
|
146
215
|
`Failed to connect to RabbitMQ after ${attempts} attempts. Check RABBITMQ_URL configuration and RabbitMQ server availability.`
|
|
147
216
|
);
|
|
148
217
|
}
|
|
149
|
-
this.logger.warn(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
},
|
|
156
|
-
"Connection attempt failed, retrying..."
|
|
157
|
-
);
|
|
218
|
+
this.logger.warn("Connection attempt failed, retrying...", {
|
|
219
|
+
error,
|
|
220
|
+
attempt: attempts,
|
|
221
|
+
maxAttempts: maxAttempts ?? "unlimited",
|
|
222
|
+
retryDelayMs: this.options.connectionRetryDelay
|
|
223
|
+
});
|
|
158
224
|
await this.sleep(this.options.connectionRetryDelay);
|
|
159
225
|
}
|
|
160
226
|
}
|
|
@@ -202,21 +268,18 @@ var RabbitMQClient = class {
|
|
|
202
268
|
});
|
|
203
269
|
await this.channel.waitForConfirms();
|
|
204
270
|
if (attempts > 0) {
|
|
205
|
-
this.logger.info({ exchange, routingKey, messageSize: content.length, attempts }
|
|
271
|
+
this.logger.info("Published message after retries", { exchange, routingKey, messageSize: content.length, attempts });
|
|
206
272
|
} else {
|
|
207
|
-
this.logger.info({ exchange, routingKey, messageSize: content.length }
|
|
273
|
+
this.logger.info("Published message", { exchange, routingKey, messageSize: content.length });
|
|
208
274
|
}
|
|
209
|
-
this.logger.debug({ exchange, routingKey, payload: message }
|
|
275
|
+
this.logger.debug("Published message payload", { exchange, routingKey, payload: message });
|
|
210
276
|
this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 });
|
|
211
277
|
return;
|
|
212
278
|
} catch (error) {
|
|
213
279
|
attempts++;
|
|
214
280
|
this.connected = false;
|
|
215
281
|
if (attempts >= maxAttempts) {
|
|
216
|
-
this.logger.error(
|
|
217
|
-
{ error, exchange, routingKey, attempts, maxAttempts },
|
|
218
|
-
"Publish failed after max attempts"
|
|
219
|
-
);
|
|
282
|
+
this.logger.error("Publish failed after max attempts", { error, exchange, routingKey, attempts, maxAttempts });
|
|
220
283
|
throw new Error(
|
|
221
284
|
`Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`
|
|
222
285
|
);
|
|
@@ -225,10 +288,7 @@ var RabbitMQClient = class {
|
|
|
225
288
|
baseDelay * Math.pow(2, attempts - 1),
|
|
226
289
|
MAX_PUBLISH_RETRY_DELAY_MS
|
|
227
290
|
);
|
|
228
|
-
this.logger.warn(
|
|
229
|
-
{ error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay },
|
|
230
|
-
"Publish attempt failed, retrying"
|
|
231
|
-
);
|
|
291
|
+
this.logger.warn("Publish attempt failed, retrying", { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay });
|
|
232
292
|
await this.sleep(retryDelay);
|
|
233
293
|
}
|
|
234
294
|
}
|
|
@@ -242,7 +302,7 @@ var RabbitMQClient = class {
|
|
|
242
302
|
async close(clearSubscriptions = true) {
|
|
243
303
|
try {
|
|
244
304
|
if (this.inflightCount > 0) {
|
|
245
|
-
this.logger.info(
|
|
305
|
+
this.logger.info("Waiting for in-flight messages to drain", { inflightCount: this.inflightCount });
|
|
246
306
|
await this.waitForDrain(this.options.closeTimeout);
|
|
247
307
|
}
|
|
248
308
|
await this.channel?.close();
|
|
@@ -256,10 +316,11 @@ var RabbitMQClient = class {
|
|
|
256
316
|
this.consumerTags.clear();
|
|
257
317
|
if (clearSubscriptions) {
|
|
258
318
|
this.subscriptions = [];
|
|
319
|
+
this.subscriptionSemaphores.clear();
|
|
259
320
|
}
|
|
260
321
|
this.logger.info("Connection closed");
|
|
261
322
|
} catch (error) {
|
|
262
|
-
this.logger.error(
|
|
323
|
+
this.logger.error("Error closing connection", { error });
|
|
263
324
|
throw error;
|
|
264
325
|
}
|
|
265
326
|
}
|
|
@@ -285,7 +346,7 @@ var RabbitMQClient = class {
|
|
|
285
346
|
if (this.subscriptions.length === 0) {
|
|
286
347
|
return;
|
|
287
348
|
}
|
|
288
|
-
this.logger.info({ count: this.subscriptions.length }
|
|
349
|
+
this.logger.info("Re-establishing subscriptions", { count: this.subscriptions.length });
|
|
289
350
|
const subs = [...this.subscriptions];
|
|
290
351
|
const results = await Promise.allSettled(
|
|
291
352
|
subs.map(async (sub) => {
|
|
@@ -301,14 +362,14 @@ var RabbitMQClient = class {
|
|
|
301
362
|
} else {
|
|
302
363
|
const queue = subs[index].queue;
|
|
303
364
|
failed.push(queue);
|
|
304
|
-
this.logger.error({ error: result.reason, queue }
|
|
365
|
+
this.logger.error("Failed to re-subscribe to queue", { error: result.reason, queue });
|
|
305
366
|
}
|
|
306
367
|
});
|
|
307
368
|
if (succeeded.length > 0) {
|
|
308
|
-
this.logger.info({ count: succeeded.length, queues: succeeded }
|
|
369
|
+
this.logger.info("Successfully re-subscribed to queues", { count: succeeded.length, queues: succeeded });
|
|
309
370
|
}
|
|
310
371
|
if (failed.length > 0) {
|
|
311
|
-
this.logger.warn({ count: failed.length, queues: failed }
|
|
372
|
+
this.logger.warn("Some subscriptions failed to restore", { count: failed.length, queues: failed });
|
|
312
373
|
}
|
|
313
374
|
this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length });
|
|
314
375
|
}
|
|
@@ -328,6 +389,7 @@ var RabbitMQClient = class {
|
|
|
328
389
|
this.subscriptions.push(sub);
|
|
329
390
|
} else {
|
|
330
391
|
this.subscriptions[existingIndex] = sub;
|
|
392
|
+
this.subscriptionSemaphores.delete(queue);
|
|
331
393
|
}
|
|
332
394
|
await this.setupSubscription(sub);
|
|
333
395
|
}
|
|
@@ -340,22 +402,24 @@ var RabbitMQClient = class {
|
|
|
340
402
|
await this.channel.cancel(tag);
|
|
341
403
|
}
|
|
342
404
|
this.consumerTags.delete(queue);
|
|
405
|
+
this.subscriptionSemaphores.delete(queue);
|
|
343
406
|
this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue);
|
|
344
|
-
this.logger.info(
|
|
407
|
+
this.logger.info("Unsubscribed from queue", { queue });
|
|
345
408
|
}
|
|
346
409
|
async setupSubscription(sub) {
|
|
347
410
|
if (!this.channel) {
|
|
348
411
|
throw new Error("Channel not available");
|
|
349
412
|
}
|
|
413
|
+
const channel = this.channel;
|
|
350
414
|
const { exchange, routingKey, queue, handler, options } = sub;
|
|
351
415
|
const dlxExchange = `${exchange}.dlx`;
|
|
352
416
|
const dlqQueue = `${queue}.dlq`;
|
|
353
417
|
const dlqRoutingKey = `${routingKey}.dead`;
|
|
354
|
-
await
|
|
355
|
-
await
|
|
356
|
-
await
|
|
418
|
+
await channel.assertExchange(dlxExchange, "topic", { durable: true });
|
|
419
|
+
await channel.assertQueue(dlqQueue, { durable: true });
|
|
420
|
+
await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
|
|
357
421
|
if (!this.assertedExchanges.has(exchange)) {
|
|
358
|
-
await
|
|
422
|
+
await channel.assertExchange(exchange, "topic", { durable: true });
|
|
359
423
|
this.assertedExchanges.add(exchange);
|
|
360
424
|
}
|
|
361
425
|
const queueType = options?.queueArguments?.["x-queue-type"] ?? "quorum";
|
|
@@ -366,101 +430,102 @@ var RabbitMQClient = class {
|
|
|
366
430
|
if (queueType === "quorum") {
|
|
367
431
|
queueArgs["x-dead-letter-strategy"] = "at-least-once";
|
|
368
432
|
}
|
|
369
|
-
await
|
|
433
|
+
await channel.assertQueue(queue, {
|
|
370
434
|
durable: true,
|
|
371
435
|
deadLetterExchange: dlxExchange,
|
|
372
436
|
deadLetterRoutingKey: dlqRoutingKey,
|
|
373
437
|
arguments: { ...queueArgs, ...options?.queueArguments }
|
|
374
438
|
});
|
|
375
|
-
await
|
|
376
|
-
|
|
439
|
+
await channel.bindQueue(queue, exchange, routingKey);
|
|
440
|
+
if (!this.subscriptionSemaphores.has(queue)) {
|
|
441
|
+
const concurrency = options?.concurrency ?? this.options.concurrency;
|
|
442
|
+
this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null);
|
|
443
|
+
}
|
|
444
|
+
const semaphore = this.subscriptionSemaphores.get(queue) ?? null;
|
|
445
|
+
const { consumerTag } = await channel.consume(
|
|
377
446
|
queue,
|
|
378
447
|
(message) => {
|
|
379
448
|
if (message) {
|
|
380
|
-
this.
|
|
381
|
-
this.logger.error({ error }, "Unhandled error in message handler");
|
|
382
|
-
});
|
|
449
|
+
this.dispatch(message, handler, semaphore, channel);
|
|
383
450
|
}
|
|
384
451
|
},
|
|
385
452
|
{ noAck: false }
|
|
386
453
|
);
|
|
387
454
|
this.consumerTags.set(queue, consumerTag);
|
|
388
|
-
this.logger.info({ exchange, routingKey, queue }
|
|
455
|
+
this.logger.info("Subscribed to queue", { exchange, routingKey, queue });
|
|
389
456
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
457
|
+
/**
|
|
458
|
+
* Gates a delivered message on the subscription's concurrency semaphore, then
|
|
459
|
+
* processes it. A message counts as in-flight from delivery until its handler
|
|
460
|
+
* settles (including time spent waiting for a permit), so graceful `close()`
|
|
461
|
+
* drains queued messages as well as actively-processing ones.
|
|
462
|
+
*/
|
|
463
|
+
dispatch(message, handler, semaphore, channel) {
|
|
464
|
+
this.inflightCount++;
|
|
465
|
+
const acquire = semaphore ? semaphore.acquire() : Promise.resolve();
|
|
466
|
+
acquire.then(() => this.handleWithRetry(message, handler, channel)).catch((error) => {
|
|
467
|
+
this.logger.error("Unhandled error in message handler", { error });
|
|
468
|
+
}).finally(() => {
|
|
469
|
+
semaphore?.release();
|
|
470
|
+
this.inflightCount--;
|
|
471
|
+
if (this.inflightCount === 0 && this.drainResolve) {
|
|
472
|
+
this.drainResolve();
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async handleWithRetry(message, handler, channel) {
|
|
477
|
+
if (channel !== this.channel) {
|
|
478
|
+
this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
|
|
394
479
|
return;
|
|
395
480
|
}
|
|
396
|
-
|
|
481
|
+
const startTime = Date.now();
|
|
482
|
+
let attempts = 0;
|
|
483
|
+
const routingKey = message.fields.routingKey;
|
|
484
|
+
const exchange = message.fields.exchange;
|
|
485
|
+
let content;
|
|
397
486
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
487
|
+
content = JSON.parse(message.content.toString());
|
|
488
|
+
} catch (parseError) {
|
|
489
|
+
const rawContent = message.content.toString();
|
|
490
|
+
const rawPreview = rawContent.substring(0, 100);
|
|
491
|
+
this.logger.error("Failed to parse message JSON, sending to DLQ", {
|
|
492
|
+
error: parseError,
|
|
493
|
+
exchange,
|
|
494
|
+
routingKey,
|
|
495
|
+
rawContentPreview: rawPreview + (rawContent.length > 100 ? "..." : "")
|
|
496
|
+
});
|
|
497
|
+
channel.nack(message, false, false);
|
|
498
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: "invalid_json" });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
|
|
502
|
+
while (attempts < this.options.maxRetries) {
|
|
403
503
|
try {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
this.
|
|
409
|
-
{
|
|
410
|
-
error: parseError,
|
|
411
|
-
exchange,
|
|
412
|
-
routingKey,
|
|
413
|
-
rawContentPreview: rawPreview + (rawContent.length > 100 ? "..." : "")
|
|
414
|
-
},
|
|
415
|
-
"Failed to parse message JSON, sending to DLQ"
|
|
416
|
-
);
|
|
417
|
-
channel.nack(message, false, false);
|
|
418
|
-
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: "invalid_json" });
|
|
504
|
+
await handler(content);
|
|
505
|
+
const duration2 = Date.now() - startTime;
|
|
506
|
+
this.logger.info("Message processed successfully", { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
507
|
+
channel.ack(message);
|
|
508
|
+
this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
|
|
419
509
|
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.
|
|
432
|
-
return;
|
|
433
|
-
} catch (error) {
|
|
434
|
-
attempts++;
|
|
435
|
-
this.logger.error(
|
|
436
|
-
{
|
|
437
|
-
error: error instanceof Error ? error.message : error,
|
|
438
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
439
|
-
exchange,
|
|
440
|
-
routingKey,
|
|
441
|
-
attempt: attempts,
|
|
442
|
-
maxRetries: this.options.maxRetries
|
|
443
|
-
},
|
|
444
|
-
"Handler failed"
|
|
445
|
-
);
|
|
446
|
-
if (attempts < this.options.maxRetries) {
|
|
447
|
-
await this.sleep(this.options.retryDelay);
|
|
448
|
-
}
|
|
510
|
+
} catch (error) {
|
|
511
|
+
attempts++;
|
|
512
|
+
this.logger.error("Handler failed", {
|
|
513
|
+
error: error instanceof Error ? error.message : error,
|
|
514
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
515
|
+
exchange,
|
|
516
|
+
routingKey,
|
|
517
|
+
attempt: attempts,
|
|
518
|
+
maxRetries: this.options.maxRetries
|
|
519
|
+
});
|
|
520
|
+
if (attempts < this.options.maxRetries) {
|
|
521
|
+
await this.sleep(this.options.retryDelay);
|
|
449
522
|
}
|
|
450
523
|
}
|
|
451
|
-
const duration = Date.now() - startTime;
|
|
452
|
-
this.logger.error(
|
|
453
|
-
{ exchange, routingKey, maxRetries: this.options.maxRetries, duration },
|
|
454
|
-
"Message failed after max retries, sending to DLQ"
|
|
455
|
-
);
|
|
456
|
-
channel.nack(message, false, false);
|
|
457
|
-
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
|
|
458
|
-
} finally {
|
|
459
|
-
this.inflightCount--;
|
|
460
|
-
if (this.inflightCount === 0 && this.drainResolve) {
|
|
461
|
-
this.drainResolve();
|
|
462
|
-
}
|
|
463
524
|
}
|
|
525
|
+
const duration = Date.now() - startTime;
|
|
526
|
+
this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries: this.options.maxRetries, duration });
|
|
527
|
+
channel.nack(message, false, false);
|
|
528
|
+
this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
|
|
464
529
|
}
|
|
465
530
|
/**
|
|
466
531
|
* Lightweight liveness probe: creates and immediately deletes a temporary
|
|
@@ -478,7 +543,7 @@ var RabbitMQClient = class {
|
|
|
478
543
|
await this.channel.deleteQueue(queue);
|
|
479
544
|
return true;
|
|
480
545
|
} catch (error) {
|
|
481
|
-
this.logger.warn(
|
|
546
|
+
this.logger.warn("Health check failed", { error });
|
|
482
547
|
return false;
|
|
483
548
|
}
|
|
484
549
|
}
|
|
@@ -486,10 +551,7 @@ var RabbitMQClient = class {
|
|
|
486
551
|
if (this.inflightCount === 0) return Promise.resolve();
|
|
487
552
|
return new Promise((resolve) => {
|
|
488
553
|
const timer = setTimeout(() => {
|
|
489
|
-
this.logger.warn(
|
|
490
|
-
{ inflightCount: this.inflightCount },
|
|
491
|
-
"Close timeout reached with messages still in flight"
|
|
492
|
-
);
|
|
554
|
+
this.logger.warn("Close timeout reached with messages still in flight", { inflightCount: this.inflightCount });
|
|
493
555
|
this.drainResolve = null;
|
|
494
556
|
resolve();
|
|
495
557
|
}, timeout);
|
|
@@ -505,7 +567,7 @@ var RabbitMQClient = class {
|
|
|
505
567
|
try {
|
|
506
568
|
hook?.(info);
|
|
507
569
|
} catch (error) {
|
|
508
|
-
this.logger.debug(
|
|
570
|
+
this.logger.debug("Observability hook error", { error });
|
|
509
571
|
}
|
|
510
572
|
}
|
|
511
573
|
sleep(ms) {
|
|
@@ -514,6 +576,8 @@ var RabbitMQClient = class {
|
|
|
514
576
|
};
|
|
515
577
|
// Annotate the CommonJS export names for ESM import in node:
|
|
516
578
|
0 && (module.exports = {
|
|
517
|
-
RabbitMQClient
|
|
579
|
+
RabbitMQClient,
|
|
580
|
+
defaultLogger,
|
|
581
|
+
silentLogger
|
|
518
582
|
});
|
|
519
583
|
//# sourceMappingURL=index.cjs.map
|