@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/dist/index.js CHANGED
@@ -2,12 +2,69 @@
2
2
  import amqp from "amqplib";
3
3
 
4
4
  // src/logger.ts
5
- import pino from "pino";
6
- var createLogger = (parent) => {
7
- if (parent) {
8
- return parent.child({ service: "rabbitmq-client" });
5
+ var defaultLogger = {
6
+ debug: () => {
7
+ },
8
+ info: () => {
9
+ },
10
+ warn: (message, ...args) => console.warn("[rabbitmq-client]", message, ...args),
11
+ error: (message, ...args) => console.error("[rabbitmq-client]", message, ...args)
12
+ };
13
+ var silentLogger = {
14
+ debug: () => {
15
+ },
16
+ info: () => {
17
+ },
18
+ warn: () => {
19
+ },
20
+ error: () => {
21
+ }
22
+ };
23
+
24
+ // src/semaphore.ts
25
+ var Semaphore = class {
26
+ permits;
27
+ waiters = [];
28
+ constructor(permits) {
29
+ if (!Number.isInteger(permits) || permits < 1) {
30
+ throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`);
31
+ }
32
+ this.permits = permits;
33
+ }
34
+ /**
35
+ * Acquires a permit, resolving immediately when one is free or queueing
36
+ * (FIFO) until another holder releases. Always pair with `release()` in a
37
+ * `finally` so a throwing handler never leaks a permit.
38
+ */
39
+ acquire() {
40
+ if (this.permits > 0) {
41
+ this.permits--;
42
+ return Promise.resolve();
43
+ }
44
+ return new Promise((resolve) => {
45
+ this.waiters.push(resolve);
46
+ });
47
+ }
48
+ /**
49
+ * Releases a permit. Hands it directly to the next waiter if any are queued,
50
+ * otherwise returns it to the pool.
51
+ */
52
+ release() {
53
+ const next = this.waiters.shift();
54
+ if (next) {
55
+ next();
56
+ } else {
57
+ this.permits++;
58
+ }
59
+ }
60
+ /** Permits currently available. Exposed for tests and introspection. */
61
+ get available() {
62
+ return this.permits;
63
+ }
64
+ /** Number of callers currently queued waiting for a permit. */
65
+ get waiting() {
66
+ return this.waiters.length;
9
67
  }
10
- return pino({ name: "rabbitmq-client" });
11
68
  };
12
69
 
13
70
  // src/client.ts
@@ -34,6 +91,11 @@ var RabbitMQClient = class {
34
91
  reconnectionPromise = null;
35
92
  assertedExchanges = /* @__PURE__ */ new Set();
36
93
  consumerTags = /* @__PURE__ */ new Map();
94
+ // Per-queue concurrency limiter, keyed by queue name. Persisted across
95
+ // reconnects (unlike consumerTags) so handlers still running when a
96
+ // connection drops keep holding their permits, and the new consumer cannot
97
+ // exceed the ceiling while the old pipeline drains. `null` = no limit.
98
+ subscriptionSemaphores = /* @__PURE__ */ new Map();
37
99
  inflightCount = 0;
38
100
  drainResolve = null;
39
101
  constructor(options) {
@@ -45,9 +107,14 @@ var RabbitMQClient = class {
45
107
  initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,
46
108
  publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,
47
109
  prefetch: options.prefetch ?? DEFAULTS.prefetch,
110
+ // Default concurrency tracks prefetch, preserving the previous behaviour
111
+ // (handlers ran fire-and-forget, so up to `prefetch` ran at once) while
112
+ // making the ceiling explicit and independently tunable. A value <= 0
113
+ // (including an unlimited `prefetch: 0`) means "no concurrency limit".
114
+ concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,
48
115
  closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout
49
116
  };
50
- this.logger = createLogger(options.logger);
117
+ this.logger = options.logger ?? defaultLogger;
51
118
  this.hooks = options.hooks ?? {};
52
119
  }
53
120
  /**
@@ -81,10 +148,10 @@ var RabbitMQClient = class {
81
148
  this.channel = await this.connection.createConfirmChannel();
82
149
  this.logger.info("Confirm channel created");
83
150
  await this.channel.prefetch(this.options.prefetch);
84
- this.logger.info({ prefetch: this.options.prefetch }, "Channel prefetch set");
151
+ this.logger.info("Channel prefetch set", { prefetch: this.options.prefetch });
85
152
  this.connection.on("error", (error) => {
86
153
  this.connected = false;
87
- this.logger.error({ error }, "Connection error");
154
+ this.logger.error("Connection error", { error });
88
155
  });
89
156
  this.connection.on("close", () => {
90
157
  this.connected = false;
@@ -92,7 +159,7 @@ var RabbitMQClient = class {
92
159
  this.reconnectWithRetry();
93
160
  });
94
161
  this.channel.on("error", (error) => {
95
- this.logger.error({ error }, "Channel error");
162
+ this.logger.error("Channel error", { error });
96
163
  this.handleChannelFailure();
97
164
  });
98
165
  this.channel.on("close", () => {
@@ -105,20 +172,17 @@ var RabbitMQClient = class {
105
172
  attempts++;
106
173
  this.connected = false;
107
174
  if (maxAttempts !== void 0 && attempts >= maxAttempts) {
108
- this.logger.error({ error, attempts, maxAttempts }, "Connection failed after maximum attempts");
175
+ this.logger.error("Connection failed after maximum attempts", { error, attempts, maxAttempts });
109
176
  throw new Error(
110
177
  `Failed to connect to RabbitMQ after ${attempts} attempts. Check RABBITMQ_URL configuration and RabbitMQ server availability.`
111
178
  );
112
179
  }
113
- this.logger.warn(
114
- {
115
- error,
116
- attempt: attempts,
117
- maxAttempts: maxAttempts ?? "unlimited",
118
- retryDelayMs: this.options.connectionRetryDelay
119
- },
120
- "Connection attempt failed, retrying..."
121
- );
180
+ this.logger.warn("Connection attempt failed, retrying...", {
181
+ error,
182
+ attempt: attempts,
183
+ maxAttempts: maxAttempts ?? "unlimited",
184
+ retryDelayMs: this.options.connectionRetryDelay
185
+ });
122
186
  await this.sleep(this.options.connectionRetryDelay);
123
187
  }
124
188
  }
@@ -166,21 +230,18 @@ var RabbitMQClient = class {
166
230
  });
167
231
  await this.channel.waitForConfirms();
168
232
  if (attempts > 0) {
169
- this.logger.info({ exchange, routingKey, messageSize: content.length, attempts }, "Published message after retries");
233
+ this.logger.info("Published message after retries", { exchange, routingKey, messageSize: content.length, attempts });
170
234
  } else {
171
- this.logger.info({ exchange, routingKey, messageSize: content.length }, "Published message");
235
+ this.logger.info("Published message", { exchange, routingKey, messageSize: content.length });
172
236
  }
173
- this.logger.debug({ exchange, routingKey, payload: message }, "Published message payload");
237
+ this.logger.debug("Published message payload", { exchange, routingKey, payload: message });
174
238
  this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 });
175
239
  return;
176
240
  } catch (error) {
177
241
  attempts++;
178
242
  this.connected = false;
179
243
  if (attempts >= maxAttempts) {
180
- this.logger.error(
181
- { error, exchange, routingKey, attempts, maxAttempts },
182
- "Publish failed after max attempts"
183
- );
244
+ this.logger.error("Publish failed after max attempts", { error, exchange, routingKey, attempts, maxAttempts });
184
245
  throw new Error(
185
246
  `Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`
186
247
  );
@@ -189,10 +250,7 @@ var RabbitMQClient = class {
189
250
  baseDelay * Math.pow(2, attempts - 1),
190
251
  MAX_PUBLISH_RETRY_DELAY_MS
191
252
  );
192
- this.logger.warn(
193
- { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay },
194
- "Publish attempt failed, retrying"
195
- );
253
+ this.logger.warn("Publish attempt failed, retrying", { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay });
196
254
  await this.sleep(retryDelay);
197
255
  }
198
256
  }
@@ -206,7 +264,7 @@ var RabbitMQClient = class {
206
264
  async close(clearSubscriptions = true) {
207
265
  try {
208
266
  if (this.inflightCount > 0) {
209
- this.logger.info({ inflightCount: this.inflightCount }, "Waiting for in-flight messages to drain");
267
+ this.logger.info("Waiting for in-flight messages to drain", { inflightCount: this.inflightCount });
210
268
  await this.waitForDrain(this.options.closeTimeout);
211
269
  }
212
270
  await this.channel?.close();
@@ -220,10 +278,11 @@ var RabbitMQClient = class {
220
278
  this.consumerTags.clear();
221
279
  if (clearSubscriptions) {
222
280
  this.subscriptions = [];
281
+ this.subscriptionSemaphores.clear();
223
282
  }
224
283
  this.logger.info("Connection closed");
225
284
  } catch (error) {
226
- this.logger.error({ error }, "Error closing connection");
285
+ this.logger.error("Error closing connection", { error });
227
286
  throw error;
228
287
  }
229
288
  }
@@ -249,7 +308,7 @@ var RabbitMQClient = class {
249
308
  if (this.subscriptions.length === 0) {
250
309
  return;
251
310
  }
252
- this.logger.info({ count: this.subscriptions.length }, "Re-establishing subscriptions");
311
+ this.logger.info("Re-establishing subscriptions", { count: this.subscriptions.length });
253
312
  const subs = [...this.subscriptions];
254
313
  const results = await Promise.allSettled(
255
314
  subs.map(async (sub) => {
@@ -265,14 +324,14 @@ var RabbitMQClient = class {
265
324
  } else {
266
325
  const queue = subs[index].queue;
267
326
  failed.push(queue);
268
- this.logger.error({ error: result.reason, queue }, "Failed to re-subscribe to queue");
327
+ this.logger.error("Failed to re-subscribe to queue", { error: result.reason, queue });
269
328
  }
270
329
  });
271
330
  if (succeeded.length > 0) {
272
- this.logger.info({ count: succeeded.length, queues: succeeded }, "Successfully re-subscribed to queues");
331
+ this.logger.info("Successfully re-subscribed to queues", { count: succeeded.length, queues: succeeded });
273
332
  }
274
333
  if (failed.length > 0) {
275
- this.logger.warn({ count: failed.length, queues: failed }, "Some subscriptions failed to restore");
334
+ this.logger.warn("Some subscriptions failed to restore", { count: failed.length, queues: failed });
276
335
  }
277
336
  this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length });
278
337
  }
@@ -292,6 +351,7 @@ var RabbitMQClient = class {
292
351
  this.subscriptions.push(sub);
293
352
  } else {
294
353
  this.subscriptions[existingIndex] = sub;
354
+ this.subscriptionSemaphores.delete(queue);
295
355
  }
296
356
  await this.setupSubscription(sub);
297
357
  }
@@ -304,22 +364,24 @@ var RabbitMQClient = class {
304
364
  await this.channel.cancel(tag);
305
365
  }
306
366
  this.consumerTags.delete(queue);
367
+ this.subscriptionSemaphores.delete(queue);
307
368
  this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue);
308
- this.logger.info({ queue }, "Unsubscribed from queue");
369
+ this.logger.info("Unsubscribed from queue", { queue });
309
370
  }
310
371
  async setupSubscription(sub) {
311
372
  if (!this.channel) {
312
373
  throw new Error("Channel not available");
313
374
  }
375
+ const channel = this.channel;
314
376
  const { exchange, routingKey, queue, handler, options } = sub;
315
377
  const dlxExchange = `${exchange}.dlx`;
316
378
  const dlqQueue = `${queue}.dlq`;
317
379
  const dlqRoutingKey = `${routingKey}.dead`;
318
- await this.channel.assertExchange(dlxExchange, "topic", { durable: true });
319
- await this.channel.assertQueue(dlqQueue, { durable: true });
320
- await this.channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
380
+ await channel.assertExchange(dlxExchange, "topic", { durable: true });
381
+ await channel.assertQueue(dlqQueue, { durable: true });
382
+ await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey);
321
383
  if (!this.assertedExchanges.has(exchange)) {
322
- await this.channel.assertExchange(exchange, "topic", { durable: true });
384
+ await channel.assertExchange(exchange, "topic", { durable: true });
323
385
  this.assertedExchanges.add(exchange);
324
386
  }
325
387
  const queueType = options?.queueArguments?.["x-queue-type"] ?? "quorum";
@@ -330,101 +392,102 @@ var RabbitMQClient = class {
330
392
  if (queueType === "quorum") {
331
393
  queueArgs["x-dead-letter-strategy"] = "at-least-once";
332
394
  }
333
- await this.channel.assertQueue(queue, {
395
+ await channel.assertQueue(queue, {
334
396
  durable: true,
335
397
  deadLetterExchange: dlxExchange,
336
398
  deadLetterRoutingKey: dlqRoutingKey,
337
399
  arguments: { ...queueArgs, ...options?.queueArguments }
338
400
  });
339
- await this.channel.bindQueue(queue, exchange, routingKey);
340
- const { consumerTag } = await this.channel.consume(
401
+ await channel.bindQueue(queue, exchange, routingKey);
402
+ if (!this.subscriptionSemaphores.has(queue)) {
403
+ const concurrency = options?.concurrency ?? this.options.concurrency;
404
+ this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null);
405
+ }
406
+ const semaphore = this.subscriptionSemaphores.get(queue) ?? null;
407
+ const { consumerTag } = await channel.consume(
341
408
  queue,
342
409
  (message) => {
343
410
  if (message) {
344
- this.handleWithRetry(message, handler).catch((error) => {
345
- this.logger.error({ error }, "Unhandled error in message handler");
346
- });
411
+ this.dispatch(message, handler, semaphore, channel);
347
412
  }
348
413
  },
349
414
  { noAck: false }
350
415
  );
351
416
  this.consumerTags.set(queue, consumerTag);
352
- this.logger.info({ exchange, routingKey, queue }, "Subscribed to queue");
417
+ this.logger.info("Subscribed to queue", { exchange, routingKey, queue });
353
418
  }
354
- async handleWithRetry(message, handler) {
355
- const channel = this.channel;
356
- if (!channel) {
357
- this.logger.warn("Cannot process message -- channel unavailable");
419
+ /**
420
+ * Gates a delivered message on the subscription's concurrency semaphore, then
421
+ * processes it. A message counts as in-flight from delivery until its handler
422
+ * settles (including time spent waiting for a permit), so graceful `close()`
423
+ * drains queued messages as well as actively-processing ones.
424
+ */
425
+ dispatch(message, handler, semaphore, channel) {
426
+ this.inflightCount++;
427
+ const acquire = semaphore ? semaphore.acquire() : Promise.resolve();
428
+ acquire.then(() => this.handleWithRetry(message, handler, channel)).catch((error) => {
429
+ this.logger.error("Unhandled error in message handler", { error });
430
+ }).finally(() => {
431
+ semaphore?.release();
432
+ this.inflightCount--;
433
+ if (this.inflightCount === 0 && this.drainResolve) {
434
+ this.drainResolve();
435
+ }
436
+ });
437
+ }
438
+ async handleWithRetry(message, handler, channel) {
439
+ if (channel !== this.channel) {
440
+ this.logger.warn("Skipping message from a superseded channel; it will be redelivered");
358
441
  return;
359
442
  }
360
- this.inflightCount++;
443
+ const startTime = Date.now();
444
+ let attempts = 0;
445
+ const routingKey = message.fields.routingKey;
446
+ const exchange = message.fields.exchange;
447
+ let content;
361
448
  try {
362
- const startTime = Date.now();
363
- let attempts = 0;
364
- const routingKey = message.fields.routingKey;
365
- const exchange = message.fields.exchange;
366
- let content;
449
+ content = JSON.parse(message.content.toString());
450
+ } catch (parseError) {
451
+ const rawContent = message.content.toString();
452
+ const rawPreview = rawContent.substring(0, 100);
453
+ this.logger.error("Failed to parse message JSON, sending to DLQ", {
454
+ error: parseError,
455
+ exchange,
456
+ routingKey,
457
+ rawContentPreview: rawPreview + (rawContent.length > 100 ? "..." : "")
458
+ });
459
+ channel.nack(message, false, false);
460
+ this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: "invalid_json" });
461
+ return;
462
+ }
463
+ this.logger.debug("Message received, processing", { exchange, routingKey, payload: content });
464
+ while (attempts < this.options.maxRetries) {
367
465
  try {
368
- content = JSON.parse(message.content.toString());
369
- } catch (parseError) {
370
- const rawContent = message.content.toString();
371
- const rawPreview = rawContent.substring(0, 100);
372
- this.logger.error(
373
- {
374
- error: parseError,
375
- exchange,
376
- routingKey,
377
- rawContentPreview: rawPreview + (rawContent.length > 100 ? "..." : "")
378
- },
379
- "Failed to parse message JSON, sending to DLQ"
380
- );
381
- channel.nack(message, false, false);
382
- this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: "invalid_json" });
466
+ await handler(content);
467
+ const duration2 = Date.now() - startTime;
468
+ this.logger.info("Message processed successfully", { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
469
+ channel.ack(message);
470
+ this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
383
471
  return;
384
- }
385
- this.logger.debug({ exchange, routingKey, payload: content }, "Message received, processing");
386
- while (attempts < this.options.maxRetries) {
387
- try {
388
- await handler(content);
389
- const duration2 = Date.now() - startTime;
390
- this.logger.info(
391
- { exchange, routingKey, duration: duration2, attempts: attempts + 1 },
392
- "Message processed successfully"
393
- );
394
- channel.ack(message);
395
- this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration: duration2, attempts: attempts + 1 });
396
- return;
397
- } catch (error) {
398
- attempts++;
399
- this.logger.error(
400
- {
401
- error: error instanceof Error ? error.message : error,
402
- stack: error instanceof Error ? error.stack : void 0,
403
- exchange,
404
- routingKey,
405
- attempt: attempts,
406
- maxRetries: this.options.maxRetries
407
- },
408
- "Handler failed"
409
- );
410
- if (attempts < this.options.maxRetries) {
411
- await this.sleep(this.options.retryDelay);
412
- }
472
+ } catch (error) {
473
+ attempts++;
474
+ this.logger.error("Handler failed", {
475
+ error: error instanceof Error ? error.message : error,
476
+ stack: error instanceof Error ? error.stack : void 0,
477
+ exchange,
478
+ routingKey,
479
+ attempt: attempts,
480
+ maxRetries: this.options.maxRetries
481
+ });
482
+ if (attempts < this.options.maxRetries) {
483
+ await this.sleep(this.options.retryDelay);
413
484
  }
414
485
  }
415
- const duration = Date.now() - startTime;
416
- this.logger.error(
417
- { exchange, routingKey, maxRetries: this.options.maxRetries, duration },
418
- "Message failed after max retries, sending to DLQ"
419
- );
420
- channel.nack(message, false, false);
421
- this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
422
- } finally {
423
- this.inflightCount--;
424
- if (this.inflightCount === 0 && this.drainResolve) {
425
- this.drainResolve();
426
- }
427
486
  }
487
+ const duration = Date.now() - startTime;
488
+ this.logger.error("Message failed after max retries, sending to DLQ", { exchange, routingKey, maxRetries: this.options.maxRetries, duration });
489
+ channel.nack(message, false, false);
490
+ this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: "max_retries_exhausted" });
428
491
  }
429
492
  /**
430
493
  * Lightweight liveness probe: creates and immediately deletes a temporary
@@ -442,7 +505,7 @@ var RabbitMQClient = class {
442
505
  await this.channel.deleteQueue(queue);
443
506
  return true;
444
507
  } catch (error) {
445
- this.logger.warn({ error }, "Health check failed");
508
+ this.logger.warn("Health check failed", { error });
446
509
  return false;
447
510
  }
448
511
  }
@@ -450,10 +513,7 @@ var RabbitMQClient = class {
450
513
  if (this.inflightCount === 0) return Promise.resolve();
451
514
  return new Promise((resolve) => {
452
515
  const timer = setTimeout(() => {
453
- this.logger.warn(
454
- { inflightCount: this.inflightCount },
455
- "Close timeout reached with messages still in flight"
456
- );
516
+ this.logger.warn("Close timeout reached with messages still in flight", { inflightCount: this.inflightCount });
457
517
  this.drainResolve = null;
458
518
  resolve();
459
519
  }, timeout);
@@ -469,7 +529,7 @@ var RabbitMQClient = class {
469
529
  try {
470
530
  hook?.(info);
471
531
  } catch (error) {
472
- this.logger.debug({ error }, "Observability hook error");
532
+ this.logger.debug("Observability hook error", { error });
473
533
  }
474
534
  }
475
535
  sleep(ms) {
@@ -477,6 +537,8 @@ var RabbitMQClient = class {
477
537
  }
478
538
  };
479
539
  export {
480
- RabbitMQClient
540
+ RabbitMQClient,
541
+ defaultLogger,
542
+ silentLogger
481
543
  };
482
544
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/logger.ts"],"sourcesContent":["import amqp from 'amqplib'\nimport type {\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { createLogger } from './logger.js'\nimport type { Logger } from 'pino'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: Logger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n private consumerTags = new Map<string, string>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = createLogger(options.logger)\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n let attempts = 0\n while (!this.connected) {\n try {\n this.connection = await amqp.connect(this.options.url)\n this.logger.info('Connected to server')\n this.channel = await this.connection.createConfirmChannel()\n this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info({ prefetch: this.options.prefetch }, 'Channel prefetch set')\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error({ error }, 'Connection error')\n })\n this.connection.on('close', () => {\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n this.channel.on('error', (error: Error) => {\n this.logger.error({ error }, 'Channel error')\n this.handleChannelFailure()\n })\n this.channel.on('close', () => {\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error({ error, attempts, maxAttempts }, 'Connection failed after maximum attempts')\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(\n {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n },\n 'Connection attempt failed, retrying...',\n )\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms.\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n this.channel.publish(exchange, routingKey, content, {\n persistent: true,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n })\n\n await this.channel.waitForConfirms()\n\n if (attempts > 0) {\n this.logger.info({ exchange, routingKey, messageSize: content.length, attempts }, 'Published message after retries')\n } else {\n this.logger.info({ exchange, routingKey, messageSize: content.length }, 'Published message')\n }\n this.logger.debug({ exchange, routingKey, payload: message }, 'Published message payload')\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error(\n { error, exchange, routingKey, attempts, maxAttempts },\n 'Publish failed after max attempts',\n )\n throw new Error(\n `Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = Math.min(\n baseDelay * Math.pow(2, attempts - 1),\n MAX_PUBLISH_RETRY_DELAY_MS,\n )\n\n this.logger.warn(\n { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay },\n 'Publish attempt failed, retrying',\n )\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n try {\n if (this.inflightCount > 0) {\n this.logger.info({ inflightCount: this.inflightCount }, 'Waiting for in-flight messages to drain')\n await this.waitForDrain(this.options.closeTimeout)\n }\n await this.channel?.close()\n await this.connection?.close()\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error({ error }, 'Error closing connection')\n throw error\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info({ count: this.subscriptions.length }, 'Re-establishing subscriptions')\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({ error: result.reason, queue }, 'Failed to re-subscribe to queue')\n }\n })\n if (succeeded.length > 0) {\n this.logger.info({ count: succeeded.length, queues: succeeded }, 'Successfully re-subscribed to queues')\n }\n if (failed.length > 0) {\n this.logger.warn({ count: failed.length, queues: failed }, 'Some subscriptions failed to restore')\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n }\n await this.setupSubscription(sub)\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info({ queue }, 'Unsubscribed from queue')\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n const dlqRoutingKey = `${routingKey}.dead`\n\n await this.channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await this.channel.assertQueue(dlqQueue, { durable: true })\n await this.channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await this.channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n await this.channel.bindQueue(queue, exchange, routingKey)\n const { consumerTag } = await this.channel.consume(\n queue,\n (message) => {\n if (message) {\n this.handleWithRetry(message, handler).catch((error) => {\n this.logger.error({ error }, 'Unhandled error in message handler')\n })\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info({ exchange, routingKey, queue }, 'Subscribed to queue')\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n ): Promise<void> {\n // Capture the channel reference so ack/nack always targets the channel\n // that delivered this message, even if a reconnection swaps this.channel.\n const channel = this.channel\n if (!channel) {\n this.logger.warn('Cannot process message -- channel unavailable')\n return\n }\n this.inflightCount++\n try {\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error(\n {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n },\n 'Failed to parse message JSON, sending to DLQ',\n )\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n return\n }\n\n this.logger.debug({ exchange, routingKey, payload: content }, 'Message received, processing')\n\n while (attempts < this.options.maxRetries) {\n try {\n await handler(content)\n const duration = Date.now() - startTime\n this.logger.info(\n { exchange, routingKey, duration, attempts: attempts + 1 },\n 'Message processed successfully',\n )\n channel.ack(message)\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n return\n } catch (error) {\n attempts++\n this.logger.error(\n {\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 'Handler failed',\n )\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n }\n }\n\n const duration = Date.now() - startTime\n this.logger.error(\n { exchange, routingKey, maxRetries: this.options.maxRetries, duration },\n 'Message failed after max retries, sending to DLQ',\n )\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n } finally {\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n }\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn({ error }, 'Health check failed')\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(\n { inflightCount: this.inflightCount },\n 'Close timeout reached with messages still in flight',\n )\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({ error }, 'Observability hook error') }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import pino, { type Logger } from 'pino'\n\n/**\n * Creates a child pino logger for the RabbitMQ client.\n *\n * If a parent logger is provided, the child inherits its configuration\n * (level, transports, etc.) for unified log output. Otherwise, a new\n * root logger is created.\n */\nexport const createLogger = (parent?: Logger): Logger => {\n if (parent) {\n return parent.child({ service: 'rabbitmq-client' })\n }\n return pino({ name: 'rabbitmq-client' })\n}\n"],"mappings":"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","names":["duration"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/logger.ts","../src/semaphore.ts"],"sourcesContent":["import amqp from 'amqplib'\nimport type {\n ILogger,\n PublishOptions,\n RabbitMQClientOptions,\n RabbitMQHooks,\n RabbitMQMessage,\n RabbitMQMessageHandler,\n RabbitMQSubscription,\n SubscribeOptions,\n} from './types.js'\nimport { defaultLogger } from './logger.js'\nimport { Semaphore } from './semaphore.js'\n\nconst DEFAULT_PREFETCH = 10\nconst MAX_PUBLISH_RETRY_DELAY_MS = 60_000\n\nconst DEFAULTS = {\n maxRetries: 3,\n retryDelay: 1000,\n connectionRetryDelay: 5000,\n initMaxAttempts: 5,\n publishMaxAttempts: 5,\n prefetch: DEFAULT_PREFETCH,\n closeTimeout: 5000,\n} as const\n\n/**\n * Production-grade RabbitMQ client with confirm channels, automatic DLQ\n * infrastructure, reconnection with subscription restoration, and\n * exponential-backoff publishing.\n *\n * @example\n * ```ts\n * const client = new RabbitMQClient({ url: 'amqp://localhost' })\n * await client.init()\n * await client.publish('events', 'user.created', { userId: '123' })\n * await client.subscribe('events', 'user.created', 'user-service', handler)\n * ```\n */\nexport class RabbitMQClient {\n private connection: amqp.ChannelModel | null = null\n private channel: amqp.ConfirmChannel | null = null\n private connected = false\n private subscriptions: RabbitMQSubscription[] = []\n private readonly options: Required<Omit<RabbitMQClientOptions, 'logger' | 'hooks'>>\n private readonly logger: ILogger\n private readonly hooks: RabbitMQHooks\n private initializationPromise: Promise<void> | null = null\n private reconnectionPromise: Promise<void> | null = null\n private assertedExchanges = new Set<string>()\n private consumerTags = new Map<string, string>()\n // Per-queue concurrency limiter, keyed by queue name. Persisted across\n // reconnects (unlike consumerTags) so handlers still running when a\n // connection drops keep holding their permits, and the new consumer cannot\n // exceed the ceiling while the old pipeline drains. `null` = no limit.\n private subscriptionSemaphores = new Map<string, Semaphore | null>()\n private inflightCount = 0\n private drainResolve: (() => void) | null = null\n\n constructor(options: RabbitMQClientOptions) {\n this.options = {\n url: options.url,\n maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,\n retryDelay: options.retryDelay ?? DEFAULTS.retryDelay,\n connectionRetryDelay: options.connectionRetryDelay ?? DEFAULTS.connectionRetryDelay,\n initMaxAttempts: options.initMaxAttempts ?? DEFAULTS.initMaxAttempts,\n publishMaxAttempts: options.publishMaxAttempts ?? DEFAULTS.publishMaxAttempts,\n prefetch: options.prefetch ?? DEFAULTS.prefetch,\n // Default concurrency tracks prefetch, preserving the previous behaviour\n // (handlers ran fire-and-forget, so up to `prefetch` ran at once) while\n // making the ceiling explicit and independently tunable. A value <= 0\n // (including an unlimited `prefetch: 0`) means \"no concurrency limit\".\n concurrency: options.concurrency ?? options.prefetch ?? DEFAULTS.prefetch,\n closeTimeout: options.closeTimeout ?? DEFAULTS.closeTimeout,\n }\n this.logger = options.logger ?? defaultLogger\n this.hooks = options.hooks ?? {}\n }\n\n /**\n * Opens a connection and creates a confirm channel. Retries up to\n * `initMaxAttempts` times, then throws. Idempotent and safe to call\n * concurrently — duplicate calls share the same in-flight promise.\n */\n async init(): Promise<void> {\n if (this.connection && this.connected) {\n return\n }\n if (this.initializationPromise) {\n return this.initializationPromise\n }\n // Attach a no-op catch to prevent Node from briefly treating this as an\n // unhandled rejection before the await below registers its own handler.\n this.initializationPromise = this.doConnect(this.options.initMaxAttempts)\n this.initializationPromise.catch(() => undefined)\n try {\n await this.initializationPromise\n } finally {\n this.initializationPromise = null\n }\n }\n\n private async doConnect(maxAttempts?: number): Promise<void> {\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n let attempts = 0\n while (!this.connected) {\n try {\n this.connection = await amqp.connect(this.options.url)\n this.logger.info('Connected to server')\n this.channel = await this.connection.createConfirmChannel()\n this.logger.info('Confirm channel created')\n await this.channel.prefetch(this.options.prefetch)\n this.logger.info('Channel prefetch set', { prefetch: this.options.prefetch })\n\n this.connection.on('error', (error: Error) => {\n this.connected = false\n this.logger.error('Connection error', { error })\n })\n this.connection.on('close', () => {\n this.connected = false\n this.logger.warn('Connection closed')\n this.reconnectWithRetry()\n })\n this.channel.on('error', (error: Error) => {\n this.logger.error('Channel error', { error })\n this.handleChannelFailure()\n })\n this.channel.on('close', () => {\n this.logger.warn('Channel closed')\n this.handleChannelFailure()\n })\n\n this.connected = true\n this.logger.info('Client initialized successfully')\n } catch (error) {\n attempts++\n this.connected = false\n if (maxAttempts !== undefined && attempts >= maxAttempts) {\n this.logger.error('Connection failed after maximum attempts', { error, attempts, maxAttempts })\n throw new Error(\n `Failed to connect to RabbitMQ after ${attempts} attempts. ` +\n 'Check RABBITMQ_URL configuration and RabbitMQ server availability.',\n )\n }\n this.logger.warn('Connection attempt failed, retrying...', {\n error,\n attempt: attempts,\n maxAttempts: maxAttempts ?? 'unlimited',\n retryDelayMs: this.options.connectionRetryDelay,\n })\n await this.sleep(this.options.connectionRetryDelay)\n }\n }\n }\n\n private handleChannelFailure(): void {\n if (this.connected) {\n this.connected = false\n this.reconnectWithRetry()\n }\n }\n\n /**\n * Returns whether the client currently has an active connection and channel.\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * Publishes a JSON message to a topic exchange with publisher confirms.\n * Retries with exponential backoff (capped at 60 s), forcing a reconnect\n * on each failure. Throws after `publishMaxAttempts` exhausted.\n */\n async publish(\n exchange: string,\n routingKey: string,\n message: RabbitMQMessage,\n options?: PublishOptions,\n ): Promise<void> {\n let attempts = 0\n const maxAttempts = options?.maxAttempts ?? this.options.publishMaxAttempts\n const baseDelay = this.options.connectionRetryDelay\n const content = Buffer.from(JSON.stringify(message))\n\n while (attempts < maxAttempts) {\n try {\n if (!this.connected) {\n await this.reconnectWithRetry()\n }\n\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n\n if (!this.assertedExchanges.has(exchange)) {\n await this.channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n this.channel.publish(exchange, routingKey, content, {\n persistent: true,\n headers: options?.headers,\n correlationId: options?.correlationId,\n messageId: options?.messageId,\n expiration: options?.expiration,\n })\n\n await this.channel.waitForConfirms()\n\n if (attempts > 0) {\n this.logger.info('Published message after retries', { exchange, routingKey, messageSize: content.length, attempts })\n } else {\n this.logger.info('Published message', { exchange, routingKey, messageSize: content.length })\n }\n this.logger.debug('Published message payload', { exchange, routingKey, payload: message })\n\n this.callHook(this.hooks.onPublish, { exchange, routingKey, attempts: attempts + 1 })\n\n return\n } catch (error) {\n attempts++\n this.connected = false\n\n if (attempts >= maxAttempts) {\n this.logger.error('Publish failed after max attempts', { error, exchange, routingKey, attempts, maxAttempts })\n throw new Error(\n `Failed to publish to ${exchange}/${routingKey} after ${attempts} attempts: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n\n const retryDelay = Math.min(\n baseDelay * Math.pow(2, attempts - 1),\n MAX_PUBLISH_RETRY_DELAY_MS,\n )\n\n this.logger.warn('Publish attempt failed, retrying', { error, exchange, routingKey, attempt: attempts, maxAttempts, retryDelayMs: retryDelay })\n\n await this.sleep(retryDelay)\n }\n }\n }\n\n /**\n * Gracefully shuts down the client. Waits up to `closeTimeout` ms for\n * in-flight message handlers to finish before closing the channel and\n * connection. Pass `false` to preserve subscriptions for a later\n * `init()` / reconnect cycle.\n */\n async close(clearSubscriptions = true): Promise<void> {\n try {\n if (this.inflightCount > 0) {\n this.logger.info('Waiting for in-flight messages to drain', { inflightCount: this.inflightCount })\n await this.waitForDrain(this.options.closeTimeout)\n }\n await this.channel?.close()\n await this.connection?.close()\n this.connection = null\n this.channel = null\n this.connected = false\n this.initializationPromise = null\n this.reconnectionPromise = null\n this.assertedExchanges.clear()\n this.consumerTags.clear()\n if (clearSubscriptions) {\n this.subscriptions = []\n this.subscriptionSemaphores.clear()\n }\n this.logger.info('Connection closed')\n } catch (error) {\n this.logger.error('Error closing connection', { error })\n throw error\n }\n }\n\n private async reconnectWithRetry(): Promise<void> {\n if (this.reconnectionPromise) {\n return this.reconnectionPromise\n }\n this.connected = false\n this.logger.info('Starting reconnection...')\n this.reconnectionPromise = this.doReconnect()\n this.reconnectionPromise.catch(() => undefined)\n try {\n await this.reconnectionPromise\n } finally {\n this.reconnectionPromise = null\n }\n }\n\n private async doReconnect(): Promise<void> {\n await this.doConnect()\n await this.resubscribeAll()\n }\n\n private async resubscribeAll(): Promise<void> {\n if (this.subscriptions.length === 0) {\n return\n }\n this.logger.info('Re-establishing subscriptions', { count: this.subscriptions.length })\n const subs = [...this.subscriptions]\n const results = await Promise.allSettled(\n subs.map(async (sub) => {\n await this.setupSubscription(sub)\n return sub.queue\n }),\n )\n const succeeded: string[] = []\n const failed: string[] = []\n results.forEach((result, index) => {\n if (result.status === 'fulfilled') {\n succeeded.push(result.value)\n } else {\n const queue = subs[index].queue\n failed.push(queue)\n this.logger.error('Failed to re-subscribe to queue', { error: result.reason, queue })\n }\n })\n if (succeeded.length > 0) {\n this.logger.info('Successfully re-subscribed to queues', { count: succeeded.length, queues: succeeded })\n }\n if (failed.length > 0) {\n this.logger.warn('Some subscriptions failed to restore', { count: failed.length, queues: failed })\n }\n this.callHook(this.hooks.onReconnect, { subscriptionsRestored: succeeded.length, subscriptionsFailed: failed.length })\n }\n\n /**\n * Subscribes to a queue with automatic DLQ infrastructure setup.\n *\n * Pass `options.queueArguments` to override the default quorum-queue\n * arguments (merged with the DLQ wiring defaults).\n */\n async subscribe(\n exchange: string,\n routingKey: string,\n queue: string,\n handler: RabbitMQMessageHandler,\n options?: SubscribeOptions,\n ): Promise<void> {\n if (!this.connected || !this.channel) {\n throw new Error('RabbitMQ client not connected. Call init() first.')\n }\n const sub: RabbitMQSubscription = { exchange, routingKey, queue, handler, options }\n const existingIndex = this.subscriptions.findIndex((s) => s.queue === queue)\n if (existingIndex === -1) {\n this.subscriptions.push(sub)\n } else {\n this.subscriptions[existingIndex] = sub\n // Re-subscribing may change concurrency; drop the old limiter so\n // setupSubscription rebuilds it from the new options. (A reconnect goes\n // through setupSubscription directly and keeps the existing limiter.)\n this.subscriptionSemaphores.delete(queue)\n }\n await this.setupSubscription(sub)\n }\n\n /**\n * Cancels a queue subscription and removes it from the restoration list.\n */\n async unsubscribe(queue: string): Promise<void> {\n const tag = this.consumerTags.get(queue)\n if (tag && this.channel) {\n await this.channel.cancel(tag)\n }\n this.consumerTags.delete(queue)\n this.subscriptionSemaphores.delete(queue)\n this.subscriptions = this.subscriptions.filter((s) => s.queue !== queue)\n this.logger.info('Unsubscribed from queue', { queue })\n }\n\n private async setupSubscription(sub: RabbitMQSubscription): Promise<void> {\n if (!this.channel) {\n throw new Error('Channel not available')\n }\n // Bind this consumer to the exact channel that will deliver its messages,\n // so ack/nack always target that channel even after a reconnect swaps\n // `this.channel`.\n const channel = this.channel\n const { exchange, routingKey, queue, handler, options } = sub\n const dlxExchange = `${exchange}.dlx`\n const dlqQueue = `${queue}.dlq`\n const dlqRoutingKey = `${routingKey}.dead`\n\n await channel.assertExchange(dlxExchange, 'topic', { durable: true })\n await channel.assertQueue(dlqQueue, { durable: true })\n await channel.bindQueue(dlqQueue, dlxExchange, dlqRoutingKey)\n\n if (!this.assertedExchanges.has(exchange)) {\n await channel.assertExchange(exchange, 'topic', { durable: true })\n this.assertedExchanges.add(exchange)\n }\n\n const queueType = options?.queueArguments?.['x-queue-type'] ?? 'quorum'\n const queueArgs: Record<string, unknown> = {\n 'x-queue-type': queueType,\n 'x-overflow': 'reject-publish',\n }\n // at-least-once DLQ strategy is only supported by quorum queues\n if (queueType === 'quorum') {\n queueArgs['x-dead-letter-strategy'] = 'at-least-once'\n }\n\n await channel.assertQueue(queue, {\n durable: true,\n deadLetterExchange: dlxExchange,\n deadLetterRoutingKey: dlqRoutingKey,\n arguments: { ...queueArgs, ...options?.queueArguments },\n })\n\n await channel.bindQueue(queue, exchange, routingKey)\n // Reuse the queue's existing limiter across reconnects; only build a new\n // one the first time (or after subscribe()/unsubscribe() cleared it).\n // A concurrency <= 0 means \"no limit\" (null), matching an unlimited prefetch.\n if (!this.subscriptionSemaphores.has(queue)) {\n const concurrency = options?.concurrency ?? this.options.concurrency\n this.subscriptionSemaphores.set(queue, concurrency > 0 ? new Semaphore(concurrency) : null)\n }\n const semaphore = this.subscriptionSemaphores.get(queue) ?? null\n const { consumerTag } = await channel.consume(\n queue,\n (message) => {\n if (message) {\n this.dispatch(message, handler, semaphore, channel)\n }\n },\n { noAck: false },\n )\n this.consumerTags.set(queue, consumerTag)\n this.logger.info('Subscribed to queue', { exchange, routingKey, queue })\n }\n\n /**\n * Gates a delivered message on the subscription's concurrency semaphore, then\n * processes it. A message counts as in-flight from delivery until its handler\n * settles (including time spent waiting for a permit), so graceful `close()`\n * drains queued messages as well as actively-processing ones.\n */\n private dispatch(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n semaphore: Semaphore | null,\n channel: amqp.ConfirmChannel,\n ): void {\n this.inflightCount++\n const acquire = semaphore ? semaphore.acquire() : Promise.resolve()\n acquire\n .then(() => this.handleWithRetry(message, handler, channel))\n .catch((error) => {\n this.logger.error('Unhandled error in message handler', { error })\n })\n .finally(() => {\n semaphore?.release()\n this.inflightCount--\n if (this.inflightCount === 0 && this.drainResolve) {\n this.drainResolve()\n }\n })\n }\n\n private async handleWithRetry(\n message: amqp.ConsumeMessage,\n handler: RabbitMQMessageHandler,\n channel: amqp.ConfirmChannel,\n ): Promise<void> {\n // `channel` is the one that delivered this message. If a reconnect has\n // since replaced it, this message was never acked and the broker will\n // redeliver it on the new channel, so drop this stale attempt rather than\n // run the handler again or ack a tag the new channel does not know.\n if (channel !== this.channel) {\n this.logger.warn('Skipping message from a superseded channel; it will be redelivered')\n return\n }\n const startTime = Date.now()\n let attempts = 0\n const routingKey = message.fields.routingKey\n const exchange = message.fields.exchange\n\n let content: RabbitMQMessage\n try {\n content = JSON.parse(message.content.toString())\n } catch (parseError) {\n const rawContent = message.content.toString()\n const rawPreview = rawContent.substring(0, 100)\n this.logger.error('Failed to parse message JSON, sending to DLQ', {\n error: parseError,\n exchange,\n routingKey,\n rawContentPreview: rawPreview + (rawContent.length > 100 ? '...' : ''),\n })\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration: 0, reason: 'invalid_json' })\n return\n }\n\n this.logger.debug('Message received, processing', { exchange, routingKey, payload: content })\n\n while (attempts < this.options.maxRetries) {\n try {\n await handler(content)\n const duration = Date.now() - startTime\n this.logger.info('Message processed successfully', { exchange, routingKey, duration, attempts: attempts + 1 })\n channel.ack(message)\n this.callHook(this.hooks.onMessageProcessed, { exchange, routingKey, duration, attempts: attempts + 1 })\n return\n } catch (error) {\n attempts++\n this.logger.error('Handler failed', {\n error: error instanceof Error ? error.message : error,\n stack: error instanceof Error ? error.stack : undefined,\n exchange,\n routingKey,\n attempt: attempts,\n maxRetries: this.options.maxRetries,\n })\n if (attempts < this.options.maxRetries) {\n await this.sleep(this.options.retryDelay)\n }\n }\n }\n\n const duration = Date.now() - startTime\n this.logger.error('Message failed after max retries, sending to DLQ', { exchange, routingKey, maxRetries: this.options.maxRetries, duration })\n channel.nack(message, false, false)\n this.callHook(this.hooks.onMessageDlq, { exchange, routingKey, duration, reason: 'max_retries_exhausted' })\n }\n\n /**\n * Lightweight liveness probe: creates and immediately deletes a temporary\n * exclusive queue. Returns `false` when disconnected or on broker error.\n */\n async checkHealth(): Promise<boolean> {\n if (!this.connected || !this.channel) {\n return false\n }\n try {\n const { queue } = await this.channel.assertQueue('', {\n exclusive: true,\n autoDelete: true,\n })\n await this.channel.deleteQueue(queue)\n return true\n } catch (error) {\n this.logger.warn('Health check failed', { error })\n return false\n }\n }\n\n private waitForDrain(timeout: number): Promise<void> {\n if (this.inflightCount === 0) return Promise.resolve()\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n this.logger.warn('Close timeout reached with messages still in flight', { inflightCount: this.inflightCount })\n this.drainResolve = null\n resolve()\n }, timeout)\n this.drainResolve = () => {\n clearTimeout(timer)\n this.drainResolve = null\n resolve()\n }\n })\n }\n\n /** Invokes a hook callback, swallowing errors so hooks never break message flow. */\n private callHook<T>(hook: ((info: T) => void) | undefined, info: T): void {\n try { hook?.(info) } catch (error) { this.logger.debug('Observability hook error', { error }) }\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n }\n}\n","import type { ILogger } from './types.js'\n\n/**\n * Default logger used when the consumer does not inject one.\n *\n * Forwards `warn` and `error` to the console so connection drops, channel\n * errors, and failed handlers stay visible out of the box, while staying\n * silent for `info`/`debug` so routine operation never pollutes the\n * consumer's output. Pass `silentLogger` for zero output, or any `ILogger`\n * (console, Winston, pino, Bunyan) to take full control.\n */\nexport const defaultLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: (message, ...args) => console.warn('[rabbitmq-client]', message, ...args),\n error: (message, ...args) => console.error('[rabbitmq-client]', message, ...args),\n}\n\n/**\n * No-op logger that discards every message. Use this to silence the client\n * entirely, including the `warn`/`error` output the default logger emits.\n */\nexport const silentLogger: ILogger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n","/**\n * Minimal FIFO counting semaphore used to cap how many message handlers run\n * concurrently, independently of channel prefetch.\n *\n * Prefetch bounds how many messages the broker delivers into consumer memory;\n * the semaphore bounds how many of those are actively being processed at once.\n * Keeping the two separate lets a consumer buffer a large prefetch window while\n * still protecting a slow downstream (a database, an HTTP API) from a burst.\n */\nexport class Semaphore {\n private permits: number\n private readonly waiters: Array<() => void> = []\n\n constructor(permits: number) {\n if (!Number.isInteger(permits) || permits < 1) {\n throw new Error(`Semaphore requires a positive integer of permits, got ${permits}`)\n }\n this.permits = permits\n }\n\n /**\n * Acquires a permit, resolving immediately when one is free or queueing\n * (FIFO) until another holder releases. Always pair with `release()` in a\n * `finally` so a throwing handler never leaks a permit.\n */\n acquire(): Promise<void> {\n if (this.permits > 0) {\n this.permits--\n return Promise.resolve()\n }\n return new Promise<void>((resolve) => {\n this.waiters.push(resolve)\n })\n }\n\n /**\n * Releases a permit. Hands it directly to the next waiter if any are queued,\n * otherwise returns it to the pool.\n */\n release(): void {\n const next = this.waiters.shift()\n if (next) {\n next()\n } else {\n this.permits++\n }\n }\n\n /** Permits currently available. Exposed for tests and introspection. */\n get available(): number {\n return this.permits\n }\n\n /** Number of callers currently queued waiting for a permit. */\n get waiting(): number {\n return this.waiters.length\n }\n}\n"],"mappings":"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","names":["duration"]}