amqp-connection-manager 3.5.0 → 3.7.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 CHANGED
@@ -2,13 +2,8 @@
2
2
 
3
3
  [![NPM version](https://badge.fury.io/js/amqp-connection-manager.svg)](https://npmjs.org/package/amqp-connection-manager)
4
4
  ![Build Status](https://github.com/jwalton/node-amqp-connection-manager/workflows/GitHub%20CI/badge.svg)
5
- [![Coverage Status](https://coveralls.io/repos/jwalton/node-amqp-connection-manager/badge.svg?branch=master&service=github)](https://coveralls.io/github/jwalton/node-amqp-connection-manager?branch=master)
6
5
  [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
7
6
 
8
- [![Dependency Status](https://david-dm.org/jwalton/node-amqp-connection-manager.svg)](https://david-dm.org/jwalton/node-amqp-connection-manager)
9
- [![devDependency Status](https://david-dm.org/jwalton/node-amqp-connection-manager/dev-status.svg)](https://david-dm.org/jwalton/node-amqp-connection-manager#info=devDependencies)
10
- [![peerDependency Status](https://david-dm.org/jwalton/node-amqp-connection-manager/peer-status.svg)](https://david-dm.org/jwalton/node-amqp-connection-manager#info=peerDependencies)
11
-
12
7
  Connection management for amqplib. This is a wrapper around [amqplib](http://www.squaremobius.net/amqp.node/) which provides automatic reconnects.
13
8
 
14
9
  ## Features
@@ -139,6 +134,7 @@ Options:
139
134
  arbitrary data in.
140
135
  - `options.json` if true, then ChannelWrapper assumes all messages passed to `publish()` and `sendToQueue()`
141
136
  are plain JSON objects. These will be encoded automatically before being sent.
137
+ - `options.publishTimeout` - a default timeout for messages published to this channel.
142
138
 
143
139
  ### AmqpConnectionManager#isConnected()
144
140
 
@@ -183,6 +179,12 @@ callback) which resolves when the message is confirmed to have been delivered to
183
179
  either the broker refuses the message, or if `close()` is called on the ChannelWrapper before the message can be
184
180
  delivered.
185
181
 
182
+ Both of these functions take an additional option when passing options:
183
+
184
+ - `timeout` - If specified, if a messages is not acked by the amqp broker within the specified number of milliseconds,
185
+ the message will be rejected. Note that the message _may_ still end up getting delivered after the timeout, as we
186
+ have no way to cancel the in-flight request.
187
+
186
188
  ### ChannelWrapper#ack and ChannelWrapper#nack
187
189
 
188
190
  These are just aliases for calling `ack()` and `nack()` on the underlying channel. They do nothing if the underlying
@@ -72,7 +72,41 @@ class AmqpConnectionManager extends events_1.EventEmitter {
72
72
  // There will be one listener per channel, and there could be a lot of channels, so disable warnings from node.
73
73
  this.setMaxListeners(0);
74
74
  this._findServers = options.findServers || (() => Promise.resolve(urls));
75
+ }
76
+ /**
77
+ * Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
78
+ * reconnect attempts will continue in the background.
79
+ * @param [options={}] -
80
+ * @param [options.timeout] - Time to wait for initial connect
81
+ */
82
+ async connect({ timeout } = {}) {
75
83
  this._connect();
84
+ let reject;
85
+ const onDisconnect = ({ err }) => {
86
+ // Ignore disconnects caused by dead servers etc., but throw on operational errors like bad credentials.
87
+ if (err.isOperational) {
88
+ reject(err);
89
+ }
90
+ };
91
+ try {
92
+ await Promise.race([
93
+ (0, events_1.once)(this, 'connect'),
94
+ new Promise((_resolve, innerReject) => {
95
+ reject = innerReject;
96
+ this.on('disconnect', onDisconnect);
97
+ }),
98
+ ...(timeout
99
+ ? [
100
+ (0, helpers_js_1.wait)(timeout).promise.then(() => {
101
+ throw new Error('amqp-connection-manager: connect timeout');
102
+ }),
103
+ ]
104
+ : []),
105
+ ]);
106
+ }
107
+ finally {
108
+ this.removeListener('disconnect', onDisconnect);
109
+ }
76
110
  }
77
111
  // `options` here are any options that can be passed to ChannelWrapper.
78
112
  createChannel(options = {}) {
@@ -221,7 +255,7 @@ class AmqpConnectionManager extends events_1.EventEmitter {
221
255
  this._currentConnection = undefined;
222
256
  this._disconnectSent = true;
223
257
  this.emit('disconnect', { err });
224
- const handle = helpers_js_1.wait(this.reconnectTimeInSeconds * 1000);
258
+ const handle = (0, helpers_js_1.wait)(this.reconnectTimeInSeconds * 1000);
225
259
  this._cancelRetriesHandler = handle.cancel;
226
260
  handle.promise
227
261
  .then(() => this._connect())
@@ -245,10 +279,10 @@ class AmqpConnectionManager extends events_1.EventEmitter {
245
279
  this._connectPromise = undefined;
246
280
  let handle;
247
281
  if (err.name === 'OperationalError' && err.message === 'connect ETIMEDOUT') {
248
- handle = helpers_js_1.wait(0);
282
+ handle = (0, helpers_js_1.wait)(0);
249
283
  }
250
284
  else {
251
- handle = helpers_js_1.wait(this.reconnectTimeInSeconds * 1000);
285
+ handle = (0, helpers_js_1.wait)(this.reconnectTimeInSeconds * 1000);
252
286
  }
253
287
  this._cancelRetriesHandler = handle.cancel;
254
288
  return handle.promise.then(() => this._connect());
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const events_1 = require("events");
7
7
  const promise_breaker_1 = __importDefault(require("promise-breaker"));
8
+ const MAX_MESSAGES_PER_BATCH = 1000;
8
9
  const IRRECOVERABLE_ERRORS = [
9
10
  403,
10
11
  404,
@@ -56,6 +57,8 @@ class ChannelWrapper extends events_1.EventEmitter {
56
57
  this._messages = [];
57
58
  /** Oublished, but not yet confirmed messages. */
58
59
  this._unconfirmedMessages = [];
60
+ /** Consumers which will be reconnected on channel errors etc. */
61
+ this._consumers = [];
59
62
  /**
60
63
  * True if the "worker" is busy sending messages. False if we need to
61
64
  * start the worker to get stuff done.
@@ -67,13 +70,19 @@ class ChannelWrapper extends events_1.EventEmitter {
67
70
  * workers ever do wake up, they'll know to stop working.
68
71
  */
69
72
  this._workerNumber = 0;
73
+ /**
74
+ * True if the underlying channel has room for more messages.
75
+ */
76
+ this._channelHasRoom = true;
70
77
  this._onConnect = this._onConnect.bind(this);
71
78
  this._onDisconnect = this._onDisconnect.bind(this);
72
79
  this._connectionManager = connectionManager;
73
80
  this.name = options.name;
81
+ this._publishTimeout = options.publishTimeout;
74
82
  this._json = (_a = options.json) !== null && _a !== void 0 ? _a : false;
75
83
  // Array of setup functions to call.
76
84
  this._setups = [];
85
+ this._consumers = [];
77
86
  if (options.setup) {
78
87
  this._setups.push(options.setup);
79
88
  }
@@ -172,15 +181,17 @@ class ChannelWrapper extends events_1.EventEmitter {
172
181
  */
173
182
  publish(exchange, routingKey, content, options, done) {
174
183
  return promise_breaker_1.default.addCallback(done, new Promise((resolve, reject) => {
175
- this._messages.push({
184
+ const { timeout, ...opts } = options || {};
185
+ this._enqueueMessage({
176
186
  type: 'publish',
177
187
  exchange,
178
188
  routingKey,
179
189
  content,
180
- options,
181
190
  resolve,
182
191
  reject,
183
- });
192
+ options: opts,
193
+ isTimedout: false,
194
+ }, timeout || this._publishTimeout);
184
195
  this._startWorker();
185
196
  }));
186
197
  }
@@ -195,24 +206,47 @@ class ChannelWrapper extends events_1.EventEmitter {
195
206
  */
196
207
  sendToQueue(queue, content, options, done) {
197
208
  return promise_breaker_1.default.addCallback(done, new Promise((resolve, reject) => {
198
- this._messages.push({
209
+ const { timeout, ...opts } = options || {};
210
+ this._enqueueMessage({
199
211
  type: 'sendToQueue',
200
212
  queue,
201
213
  content,
202
- options,
203
214
  resolve,
204
215
  reject,
205
- });
206
- return this._startWorker();
216
+ options: opts,
217
+ isTimedout: false,
218
+ }, timeout || this._publishTimeout);
219
+ this._startWorker();
207
220
  }));
208
221
  }
222
+ _enqueueMessage(message, timeout) {
223
+ if (timeout) {
224
+ message.timeout = setTimeout(() => {
225
+ let idx = this._messages.indexOf(message);
226
+ if (idx !== -1) {
227
+ this._messages.splice(idx, 1);
228
+ }
229
+ else {
230
+ idx = this._unconfirmedMessages.indexOf(message);
231
+ if (idx !== -1) {
232
+ this._unconfirmedMessages.splice(idx, 1);
233
+ }
234
+ }
235
+ message.isTimedout = true;
236
+ message.reject(new Error('timeout'));
237
+ }, timeout);
238
+ }
239
+ this._messages.push(message);
240
+ }
209
241
  // Called whenever we connect to the broker.
210
242
  async _onConnect({ connection }) {
211
243
  this._irrecoverableCode = undefined;
212
244
  try {
213
245
  const channel = await connection.createConfirmChannel();
214
246
  this._channel = channel;
247
+ this._channelHasRoom = true;
215
248
  channel.on('close', () => this._onChannelClose(channel));
249
+ channel.on('drain', () => this._onChannelDrain());
216
250
  this._settingUp = Promise.all(this._setups.map((setupFn) =>
217
251
  // TODO: Use a timeout here to guard against setupFns that never resolve?
218
252
  promise_breaker_1.default.call(setupFn, this, channel).catch((err) => {
@@ -221,7 +255,11 @@ class ChannelWrapper extends events_1.EventEmitter {
221
255
  return;
222
256
  }
223
257
  this.emit('error', err, { name: this.name });
224
- }))).then(() => {
258
+ })))
259
+ .then(() => {
260
+ return Promise.all(this._consumers.map((c) => this._reconnectConsumer(c)));
261
+ })
262
+ .then(() => {
225
263
  this._settingUp = undefined;
226
264
  });
227
265
  await this._settingUp;
@@ -229,13 +267,6 @@ class ChannelWrapper extends events_1.EventEmitter {
229
267
  // Can happen if channel closes while we're setting up.
230
268
  return;
231
269
  }
232
- if (this._unconfirmedMessages.length > 0) {
233
- // requeue any messages that were left unconfirmed when connection was lost
234
- let message;
235
- while ((message = this._unconfirmedMessages.shift())) {
236
- this._messages.push(message);
237
- }
238
- }
239
270
  // Since we just connected, publish any queued messages
240
271
  this._startWorker();
241
272
  this.emit('connect');
@@ -253,6 +284,11 @@ class ChannelWrapper extends events_1.EventEmitter {
253
284
  }
254
285
  // Wait for another reconnect to create a new channel.
255
286
  }
287
+ /** Called whenever the channel drains. */
288
+ _onChannelDrain() {
289
+ this._channelHasRoom = true;
290
+ this._startWorker();
291
+ }
256
292
  // Called whenever we disconnect from the AMQP server.
257
293
  _onDisconnect(ex) {
258
294
  this._irrecoverableCode = ex.err instanceof Error ? ex.err.code : undefined;
@@ -275,11 +311,21 @@ class ChannelWrapper extends events_1.EventEmitter {
275
311
  this._working = false;
276
312
  if (this._messages.length !== 0) {
277
313
  // Reject any unsent messages.
278
- this._messages.forEach((message) => message.reject(new Error('Channel closed')));
314
+ this._messages.forEach((message) => {
315
+ if (message.timeout) {
316
+ clearTimeout(message.timeout);
317
+ }
318
+ message.reject(new Error('Channel closed'));
319
+ });
279
320
  }
280
321
  if (this._unconfirmedMessages.length !== 0) {
281
322
  // Reject any unconfirmed messages.
282
- this._unconfirmedMessages.forEach((message) => message.reject(new Error('Channel closed')));
323
+ this._unconfirmedMessages.forEach((message) => {
324
+ if (message.timeout) {
325
+ clearTimeout(message.timeout);
326
+ }
327
+ message.reject(new Error('Channel closed'));
328
+ });
283
329
  }
284
330
  this._connectionManager.removeListener('connect', this._onConnect);
285
331
  this._connectionManager.removeListener('disconnect', this._onDisconnect);
@@ -290,7 +336,7 @@ class ChannelWrapper extends events_1.EventEmitter {
290
336
  });
291
337
  }
292
338
  _shouldPublish() {
293
- return this._messages.length > 0 && !this._settingUp && !!this._channel;
339
+ return (this._messages.length > 0 && !this._settingUp && !!this._channel && this._channelHasRoom);
294
340
  }
295
341
  // Start publishing queued messages, if there isn't already a worker doing this.
296
342
  _startWorker() {
@@ -304,6 +350,44 @@ class ChannelWrapper extends events_1.EventEmitter {
304
350
  _canWaitReconnection() {
305
351
  return !this._irrecoverableCode || !IRRECOVERABLE_ERRORS.includes(this._irrecoverableCode);
306
352
  }
353
+ _messageResolved(message, result) {
354
+ removeUnconfirmedMessage(this._unconfirmedMessages, message);
355
+ message.resolve(result);
356
+ }
357
+ _messageRejected(message, err) {
358
+ if (!this._channel && this._canWaitReconnection()) {
359
+ // Tried to write to a closed channel. Leave the message in the queue and we'll try again when
360
+ // we reconnect.
361
+ removeUnconfirmedMessage(this._unconfirmedMessages, message);
362
+ this._messages.push(message);
363
+ }
364
+ else {
365
+ // Something went wrong trying to send this message - could be JSON.stringify failed, could be
366
+ // the broker rejected the message. Either way, reject it back
367
+ removeUnconfirmedMessage(this._unconfirmedMessages, message);
368
+ message.reject(err);
369
+ }
370
+ }
371
+ _getEncodedMessage(content) {
372
+ let encodedMessage;
373
+ if (this._json) {
374
+ encodedMessage = Buffer.from(JSON.stringify(content));
375
+ }
376
+ else if (typeof content === 'string') {
377
+ encodedMessage = Buffer.from(content);
378
+ }
379
+ else if (content instanceof Buffer) {
380
+ encodedMessage = content;
381
+ }
382
+ else if (typeof content === 'object' && typeof content.toString === 'function') {
383
+ encodedMessage = Buffer.from(content.toString());
384
+ }
385
+ else {
386
+ console.warn('amqp-connection-manager: Sending JSON message, but json option not speicifed');
387
+ encodedMessage = Buffer.from(JSON.stringify(content));
388
+ }
389
+ return encodedMessage;
390
+ }
307
391
  _publishQueuedMessages(workerNumber) {
308
392
  const channel = this._channel;
309
393
  if (!channel ||
@@ -314,89 +398,136 @@ class ChannelWrapper extends events_1.EventEmitter {
314
398
  this._working = false;
315
399
  return;
316
400
  }
317
- const message = this._messages.shift();
318
- if (message) {
319
- this._unconfirmedMessages.push(message);
320
- Promise.resolve()
321
- .then(() => {
322
- let encodedMessage;
323
- if (this._json) {
324
- encodedMessage = Buffer.from(JSON.stringify(message.content));
325
- }
326
- else if (typeof message.content === 'string') {
327
- encodedMessage = Buffer.from(message.content);
328
- }
329
- else if (message.content instanceof Buffer) {
330
- encodedMessage = message.content;
331
- }
332
- else if (typeof message.content === 'object' &&
333
- typeof message.content.toString === 'function') {
334
- encodedMessage = Buffer.from(message.content.toString());
335
- }
336
- else {
337
- throw new Error('Invalid message content');
401
+ try {
402
+ // Send messages in batches of 1000 - don't want to starve the event loop.
403
+ let sendsLeft = MAX_MESSAGES_PER_BATCH;
404
+ while (this._channelHasRoom && this._messages.length > 0 && sendsLeft > 0) {
405
+ sendsLeft--;
406
+ const message = this._messages.shift();
407
+ if (!message) {
408
+ break;
338
409
  }
339
- let result = true;
340
- const sendPromise = (() => {
341
- switch (message.type) {
342
- case 'publish':
343
- return new Promise(function (resolve, reject) {
344
- result = channel.publish(message.exchange, message.routingKey, encodedMessage, message.options, (err) => {
345
- if (err) {
346
- reject(err);
347
- }
348
- else {
349
- resolve(result);
350
- }
351
- });
352
- });
353
- case 'sendToQueue':
354
- return new Promise(function (resolve, reject) {
355
- result = channel.sendToQueue(message.queue, encodedMessage, message.options, (err) => {
356
- if (err) {
357
- reject(err);
358
- }
359
- else {
360
- resolve(result);
361
- }
362
- });
363
- });
364
- /* istanbul ignore next */
365
- default:
366
- throw new Error(`Unhandled message type ${message.type}`);
410
+ this._unconfirmedMessages.push(message);
411
+ const encodedMessage = this._getEncodedMessage(message.content);
412
+ switch (message.type) {
413
+ case 'publish': {
414
+ let thisCanSend = true;
415
+ thisCanSend = this._channelHasRoom = channel.publish(message.exchange, message.routingKey, encodedMessage, message.options, (err) => {
416
+ if (message.isTimedout) {
417
+ return;
418
+ }
419
+ if (message.timeout) {
420
+ clearTimeout(message.timeout);
421
+ }
422
+ if (err) {
423
+ this._messageRejected(message, err);
424
+ }
425
+ else {
426
+ this._messageResolved(message, thisCanSend);
427
+ }
428
+ });
429
+ break;
367
430
  }
368
- })();
369
- if (result) {
370
- this._publishQueuedMessages(workerNumber);
371
- }
372
- else {
373
- channel.once('drain', () => this._publishQueuedMessages(workerNumber));
374
- }
375
- return sendPromise;
376
- })
377
- .then((result) => {
378
- removeUnconfirmedMessage(this._unconfirmedMessages, message);
379
- message.resolve(result);
380
- }, (err) => {
381
- if (!this._channel && this._canWaitReconnection()) {
382
- // Tried to write to a closed channel. Leave the message in the queue and we'll try again when
383
- // we reconnect.
384
- removeUnconfirmedMessage(this._unconfirmedMessages, message);
385
- this._messages.push(message);
386
- }
387
- else {
388
- // Something went wrong trying to send this message - could be JSON.stringify failed, could be
389
- // the broker rejected the message. Either way, reject it back
390
- removeUnconfirmedMessage(this._unconfirmedMessages, message);
391
- message.reject(err);
431
+ case 'sendToQueue': {
432
+ let thisCanSend = true;
433
+ thisCanSend = this._channelHasRoom = channel.sendToQueue(message.queue, encodedMessage, message.options, (err) => {
434
+ if (message.isTimedout) {
435
+ return;
436
+ }
437
+ if (message.timeout) {
438
+ clearTimeout(message.timeout);
439
+ }
440
+ if (err) {
441
+ this._messageRejected(message, err);
442
+ }
443
+ else {
444
+ this._messageResolved(message, thisCanSend);
445
+ }
446
+ });
447
+ break;
448
+ }
449
+ /* istanbul ignore next */
450
+ default:
451
+ throw new Error(`Unhandled message type ${message.type}`);
392
452
  }
393
- })
394
- .catch(
395
- /* istanbul ignore next */ (err) => {
396
- this.emit('error', err);
397
- this._working = false;
398
- });
453
+ }
454
+ // If we didn't send all the messages, send some more...
455
+ if (this._channelHasRoom && this._messages.length > 0) {
456
+ setImmediate(() => this._publishQueuedMessages(workerNumber));
457
+ }
458
+ this._working = false;
459
+ /* istanbul ignore next */
460
+ }
461
+ catch (err) {
462
+ this._working = false;
463
+ this.emit('error', err);
464
+ }
465
+ }
466
+ /**
467
+ * Setup a consumer
468
+ * This consumer will be reconnected on cancellation and channel errors.
469
+ */
470
+ async consume(queue, onMessage, options = {}) {
471
+ const consumer = {
472
+ consumerTag: null,
473
+ queue,
474
+ onMessage,
475
+ options,
476
+ };
477
+ this._consumers.push(consumer);
478
+ await this._consume(consumer);
479
+ }
480
+ async _consume(consumer) {
481
+ if (!this._channel) {
482
+ return;
483
+ }
484
+ const { prefetch, ...options } = consumer.options;
485
+ if (typeof prefetch === 'number') {
486
+ this._channel.prefetch(prefetch, false);
487
+ }
488
+ const { consumerTag } = await this._channel.consume(consumer.queue, (msg) => {
489
+ if (!msg) {
490
+ consumer.consumerTag = null;
491
+ this._reconnectConsumer(consumer).catch((err) => {
492
+ if (err.isOperational && err.message.includes('BasicConsume; 404')) {
493
+ // Ignore errors caused by queue not declared. In
494
+ // those cases the connection will reconnect and
495
+ // then consumers reestablished. The full reconnect
496
+ // might be avoided if we assert the queue again
497
+ // before starting to consume.
498
+ return;
499
+ }
500
+ throw err;
501
+ });
502
+ return;
503
+ }
504
+ consumer.onMessage(msg);
505
+ }, options);
506
+ consumer.consumerTag = consumerTag;
507
+ }
508
+ async _reconnectConsumer(consumer) {
509
+ if (!this._consumers.includes(consumer)) {
510
+ // Intentionally canceled
511
+ return;
399
512
  }
513
+ await this._consume(consumer);
514
+ }
515
+ /**
516
+ * Cancel all consumers
517
+ */
518
+ async cancelAll() {
519
+ const consumers = this._consumers;
520
+ this._consumers = [];
521
+ if (!this._channel) {
522
+ return;
523
+ }
524
+ const channel = this._channel;
525
+ await Promise.all(consumers.reduce((acc, consumer) => {
526
+ if (consumer.consumerTag) {
527
+ acc.push(channel.cancel(consumer.consumerTag));
528
+ }
529
+ return acc;
530
+ }, []));
400
531
  }
401
532
  /** Send an `ack` to the underlying channel. */
402
533
  ack(message, allUpTo) {
package/dist/cjs/index.js CHANGED
@@ -3,10 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.connect = void 0;
6
+ exports.AmqpConnectionManagerClass = exports.connect = void 0;
7
7
  const AmqpConnectionManager_js_1 = __importDefault(require("./AmqpConnectionManager.js"));
8
+ exports.AmqpConnectionManagerClass = AmqpConnectionManager_js_1.default;
8
9
  function connect(urls, options) {
9
- return new AmqpConnectionManager_js_1.default(urls, options);
10
+ const conn = new AmqpConnectionManager_js_1.default(urls, options);
11
+ conn.connect().catch(() => {
12
+ /* noop */
13
+ });
14
+ return conn;
10
15
  }
11
16
  exports.connect = connect;
12
17
  const amqp = { connect };
@@ -59,6 +59,7 @@ export interface IAmqpConnectionManager {
59
59
  addListener(event: 'disconnect', listener: (arg: {
60
60
  err: Error;
61
61
  }) => void): this;
62
+ listeners(eventName: string | symbol): Function[];
62
63
  on(event: string, listener: (...args: any[]) => void): this;
63
64
  on(event: 'connect', listener: ConnectListener): this;
64
65
  on(event: 'blocked', listener: (arg: {
@@ -96,13 +97,17 @@ export interface IAmqpConnectionManager {
96
97
  err: Error;
97
98
  }) => void): this;
98
99
  removeListener(event: string, listener: (...args: any[]) => void): this;
100
+ connect(options?: {
101
+ timeout?: number;
102
+ }): Promise<void>;
103
+ reconnect(): void;
99
104
  createChannel(options?: CreateChannelOpts): ChannelWrapper;
100
105
  close(): Promise<void>;
101
106
  isConnected(): boolean;
102
107
  /** The current connection. */
103
- get connection(): Connection | undefined;
108
+ readonly connection: Connection | undefined;
104
109
  /** Returns the number of registered channels. */
105
- get channelCount(): number;
110
+ readonly channelCount: number;
106
111
  }
107
112
  export default class AmqpConnectionManager extends EventEmitter implements IAmqpConnectionManager {
108
113
  private _channels;
@@ -148,6 +153,15 @@ export default class AmqpConnectionManager extends EventEmitter implements IAmqp
148
153
  * Note that if this is supplied, then `urls` is ignored.
149
154
  */
150
155
  constructor(urls: ConnectionUrl | ConnectionUrl[] | undefined | null, options?: AmqpConnectionManagerOptions);
156
+ /**
157
+ * Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
158
+ * reconnect attempts will continue in the background.
159
+ * @param [options={}] -
160
+ * @param [options.timeout] - Time to wait for initial connect
161
+ */
162
+ connect({ timeout }?: {
163
+ timeout?: number;
164
+ }): Promise<void>;
151
165
  createChannel(options?: CreateChannelOpts): ChannelWrapper;
152
166
  close(): Promise<void>;
153
167
  isConnected(): boolean;
@@ -1,5 +1,5 @@
1
1
  import amqp from 'amqplib';
2
- import { EventEmitter } from 'events';
2
+ import { EventEmitter, once } from 'events';
3
3
  import pb from 'promise-breaker';
4
4
  import { URL } from 'url';
5
5
  import ChannelWrapper from './ChannelWrapper.js';
@@ -67,7 +67,41 @@ export default class AmqpConnectionManager extends EventEmitter {
67
67
  // There will be one listener per channel, and there could be a lot of channels, so disable warnings from node.
68
68
  this.setMaxListeners(0);
69
69
  this._findServers = options.findServers || (() => Promise.resolve(urls));
70
+ }
71
+ /**
72
+ * Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
73
+ * reconnect attempts will continue in the background.
74
+ * @param [options={}] -
75
+ * @param [options.timeout] - Time to wait for initial connect
76
+ */
77
+ async connect({ timeout } = {}) {
70
78
  this._connect();
79
+ let reject;
80
+ const onDisconnect = ({ err }) => {
81
+ // Ignore disconnects caused by dead servers etc., but throw on operational errors like bad credentials.
82
+ if (err.isOperational) {
83
+ reject(err);
84
+ }
85
+ };
86
+ try {
87
+ await Promise.race([
88
+ once(this, 'connect'),
89
+ new Promise((_resolve, innerReject) => {
90
+ reject = innerReject;
91
+ this.on('disconnect', onDisconnect);
92
+ }),
93
+ ...(timeout
94
+ ? [
95
+ wait(timeout).promise.then(() => {
96
+ throw new Error('amqp-connection-manager: connect timeout');
97
+ }),
98
+ ]
99
+ : []),
100
+ ]);
101
+ }
102
+ finally {
103
+ this.removeListener('disconnect', onDisconnect);
104
+ }
71
105
  }
72
106
  // `options` here are any options that can be passed to ChannelWrapper.
73
107
  createChannel(options = {}) {