amqplib 2.0.0 → 2.1.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gh pr *)"
5
+ ]
6
+ }
7
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change log for amqplib
2
2
 
3
+ ## v2.1.0
4
+ - Add `waitForConnect` recovery option. When `false`, `connect` returns the recovering connection immediately instead of waiting for the first successful connection, so listeners for `connect`, `connect-failed` and `reconnect-scheduled` can be attached before the initial attempt, and `close()` can cancel it. Expose `waitForConnect()` on recovering connections to await the first connection (fixes #858)
5
+ - Fix `setup` being run when `close()` is called while the initial connection attempt is in flight
6
+
7
+ ## v2.0.1
8
+ - Remove `buffer-more-ints` dependency; use Node.js built-in BigInt Buffer methods (`readBigInt64BE`, `readBigUInt64BE`, `writeBigInt64BE`, `writeBigUInt64BE`) which have been available since Node.js v10.4
9
+
3
10
  ## v2.0.0
4
11
  - **BREAKING**: `heartbeat: 0` now disables heartbeats, overriding any value suggested by the server. Previously, `0` was treated as "no preference" and the server's suggested value was used. If you are passing `heartbeat: 0` and want to preserve the old behaviour, omit the option or pass `null` instead (fixes #467)
5
12
 
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Node.js CI](https://github.com/amqp-node/amqplib/workflows/Node.js%20CI/badge.svg)](https://github.com/amqp-node/amqplib/actions?query=workflow%3A%22Node.js+CI%22)
6
6
  [![amqplib](https://snyk.io/advisor/npm-package/amqplib/badge.svg)](https://snyk.io/advisor/npm-package/amqplib)
7
7
 
8
- A library for making AMQP 0-9-1 clients for Node.JS, and an AMQP 0-9-1 client for Node.JS v10+. This library does not implement [AMQP1.0](https://github.com/amqp-node/amqplib/issues/63) or [AMQP0-10](https://github.com/amqp-node/amqplib/issues/94).
8
+ A library for making AMQP 0-9-1 clients for Node.JS, and an AMQP 0-9-1 client for Node.JS v18+. This library does not implement [AMQP1.0](https://github.com/amqp-node/amqplib/issues/63) or [AMQP0-10](https://github.com/amqp-node/amqplib/issues/94).
9
9
 
10
10
  npm install amqplib
11
11
 
@@ -178,6 +178,38 @@ amqplib.connect(
178
178
 
179
179
  Without `recovery` options, behavior is unchanged.
180
180
 
181
+ ### Attaching listeners before the first connection
182
+
183
+ By default `connect` waits for the first successful connection before
184
+ resolving (or invoking the callback), so events emitted during the initial
185
+ attempt such as `connect-failed` and `reconnect-scheduled` cannot be observed.
186
+ Set `waitForConnect: false` to get the connection handle immediately. You can
187
+ then attach listeners, call `waitForConnect()` to await the first connection,
188
+ or call `close()` to cancel the initial attempt. Channel operations wait for a
189
+ connection internally, so `createChannel()` can be called straight away.
190
+
191
+ ```javascript
192
+ const connection = await amqplib.connect('amqp://localhost', {
193
+ recovery: { waitForConnect: false },
194
+ });
195
+
196
+ connection.on('connect-failed', (err) => {
197
+ console.warn('connection attempt failed', err.message);
198
+ });
199
+
200
+ connection.on('reconnect-scheduled', ({ attempt, delay }) => {
201
+ console.log(`retrying (attempt ${attempt}) in ${delay}ms`);
202
+ });
203
+
204
+ await connection.waitForConnect();
205
+ ```
206
+
207
+ The callback API returns the connection handle synchronously, so listeners can
208
+ always be attached before the first attempt. With `waitForConnect: false` the
209
+ callback is invoked immediately with the handle instead of after the first
210
+ connection, and `waitForConnect(callback)` can be used to be notified once
211
+ connected.
212
+
181
213
  ## Error handling in event handlers
182
214
 
183
215
  If a user-supplied event handler throws a synchronous error, the throw will
@@ -113,7 +113,6 @@ println("'use strict';");
113
113
  nl();
114
114
  nl();
115
115
  println('var codec = require("./codec");');
116
- println('var ints = require("buffer-more-ints");');
117
116
  println('var encodeTable = codec.encodeTable;');
118
117
  println('var decodeFields = codec.decodeFields;');
119
118
  nl();
@@ -412,7 +411,7 @@ function encoderFn(method) {
412
411
  case 'longlong':
413
412
  case 'timestamp':
414
413
  checkAssignArg(a);
415
- println('ints.writeUInt64BE(buffer, val, offset); offset += 8;');
414
+ println('buffer.writeBigUInt64BE(BigInt(val), offset); offset += 8;');
416
415
  break;
417
416
  case 'bit':
418
417
  checkAssignArg(a);
@@ -493,7 +492,7 @@ function decoderFn(method) {
493
492
  break;
494
493
  case 'longlong':
495
494
  case 'timestamp':
496
- println('val = ints.readUInt64BE(buffer, offset); offset += 8;');
495
+ println('val = Number(buffer.readBigUInt64BE(offset)); offset += 8;');
497
496
  break;
498
497
  case 'bit': {
499
498
  const bit = 1 << bitsInARow;
@@ -612,7 +611,7 @@ function encodePropsFn(props) {
612
611
  // skip frame size for now, we'll write it in when we know.
613
612
 
614
613
  // body size
615
- println('ints.writeUInt64BE(buffer, size, 11);');
614
+ println('buffer.writeBigUInt64BE(BigInt(size), 11);');
616
615
 
617
616
  println('flags = 0;');
618
617
  // we'll write the flags later too
@@ -642,7 +641,7 @@ function encodePropsFn(props) {
642
641
  break;
643
642
  case 'longlong':
644
643
  case 'timestamp':
645
- println('ints.writeUInt64BE(buffer, val, offset);');
644
+ println('buffer.writeBigUInt64BE(BigInt(val), offset);');
646
645
  println('offset += 8;');
647
646
  break;
648
647
  case 'shortstr': {
@@ -706,7 +705,7 @@ function decodePropsFn(props) {
706
705
  break;
707
706
  case 'longlong':
708
707
  case 'timestamp':
709
- println('val = ints.readUInt64BE(buffer, offset); offset += 8;');
708
+ println('val = Number(buffer.readBigUInt64BE(offset)); offset += 8;');
710
709
  break;
711
710
  case 'longstr':
712
711
  println('len = buffer.readUInt32BE(offset); offset += 4;');
package/callback_api.d.ts CHANGED
@@ -173,9 +173,17 @@ export interface RecoveryOptions {
173
173
  maxRetries?: number;
174
174
  /** Optional setup function called after each successful connection */
175
175
  setup?: ((model: Connection) => Promise<void>) | ((model: Connection, done: (err?: Error) => void) => void);
176
+ /**
177
+ * When false, the `connect()` callback is invoked immediately with the
178
+ * recovering connection without waiting for the first successful connection.
179
+ * Use `waitForConnect()` to be notified of the first connection. Default: true
180
+ */
181
+ waitForConnect?: boolean;
176
182
  }
177
183
 
178
184
  export interface RecoveringConnection extends events.EventEmitter {
185
+ /** Invokes the callback once the first connection has been established */
186
+ waitForConnect(callback: (err: Error, connection: RecoveringConnection) => void): void;
179
187
  close(callback?: (err: Error) => void): void;
180
188
  createChannel(callback: (err: Error, channel: Channel) => void): Channel;
181
189
  createChannel(options: ChannelOptions, callback: (err: Error, channel: Channel) => void): Channel;
@@ -205,5 +213,5 @@ export declare function connect(
205
213
  export declare function connect(
206
214
  url: string | Options.Connect,
207
215
  socketOptions: SocketOptions & { recovery: RecoveryOptions | true },
208
- callback: (err: Error, connection: RecoveringConnection) => void,
216
+ callback?: (err: Error, connection: RecoveringConnection) => void,
209
217
  ): RecoveringConnection;
package/index.d.ts CHANGED
@@ -147,9 +147,18 @@ export interface RecoveryOptions {
147
147
  maxRetries?: number;
148
148
  /** Optional setup function called after each successful connection */
149
149
  setup?: ((model: ChannelModel) => Promise<void>) | ((model: ChannelModel, done: (err?: Error) => void) => void);
150
+ /**
151
+ * When false, `connect()` resolves immediately with the recovering connection
152
+ * without waiting for the first successful connection, so listeners can be
153
+ * attached before the initial attempt. Use `waitForConnect()` to await the
154
+ * first connection. Default: true
155
+ */
156
+ waitForConnect?: boolean;
150
157
  }
151
158
 
152
159
  export interface RecoveringChannelModel extends events.EventEmitter {
160
+ /** Resolves once the first connection has been established */
161
+ waitForConnect(): Promise<this>;
153
162
  close(): Promise<void>;
154
163
  createChannel(options?: ChannelOptions): Promise<Channel>;
155
164
  createConfirmChannel(options?: ChannelOptions): Promise<ConfirmChannel>;
package/lib/codec.js CHANGED
@@ -45,8 +45,6 @@ properties are present. This scheme can save ones of bytes per message
45
45
  (messages which take a minimum of three frames each to send).
46
46
  */
47
47
 
48
- const ints = require('buffer-more-ints');
49
-
50
48
  // JavaScript uses only doubles so what I'm testing for is whether
51
49
  // it's *better* to encode a number as a float or double. This really
52
50
  // just amounts to testing whether there's a fractional part to the
@@ -219,7 +217,7 @@ function encodeFieldValue(buffer, value, offset) {
219
217
  case 'long':
220
218
  case 'int64':
221
219
  tag('l');
222
- ints.writeInt64BE(buffer, val, offset);
220
+ buffer.writeBigInt64BE(BigInt(val), offset);
223
221
  offset += 8;
224
222
  break;
225
223
 
@@ -227,7 +225,7 @@ function encodeFieldValue(buffer, value, offset) {
227
225
  // `{'!': type, value: val}
228
226
  case 'timestamp':
229
227
  tag('T');
230
- ints.writeUInt64BE(buffer, val, offset);
228
+ buffer.writeBigUInt64BE(BigInt(val), offset);
231
229
  offset += 8;
232
230
  break;
233
231
  case 'float':
@@ -296,7 +294,7 @@ function decodeFields(slice) {
296
294
  break;
297
295
  }
298
296
  case 'T':
299
- val = ints.readUInt64BE(slice, offset);
297
+ val = Number(slice.readBigUInt64BE(offset));
300
298
  offset += 8;
301
299
  val = { '!': 'timestamp', value: val };
302
300
  break;
@@ -321,7 +319,7 @@ function decodeFields(slice) {
321
319
  offset += 4;
322
320
  break;
323
321
  case 'l':
324
- val = ints.readInt64BE(slice, offset);
322
+ val = Number(slice.readBigInt64BE(offset));
325
323
  offset += 8;
326
324
  break;
327
325
  case 's':
package/lib/defs.js CHANGED
@@ -501,7 +501,7 @@ function decodeBasicDeliver(buffer) {
501
501
  val = buffer.toString('utf8', offset, offset + len);
502
502
  offset += len;
503
503
  fields.consumerTag = val;
504
- val = ints.readUInt64BE(buffer, offset);
504
+ val = Number(buffer.readBigUInt64BE(offset));
505
505
  offset += 8;
506
506
  fields.deliveryTag = val;
507
507
  val = !!(1 & buffer[offset]);
@@ -557,7 +557,7 @@ function encodeBasicDeliver(channel, fields) {
557
557
  val = fields.deliveryTag;
558
558
  if (void 0 === val) throw new Error("Missing value for mandatory field 'deliveryTag'");
559
559
  if ('number' != typeof val || isNaN(val)) throw new TypeError("Field 'deliveryTag' is the wrong type; must be a number (but not NaN)");
560
- ints.writeUInt64BE(buffer, val, offset);
560
+ buffer.writeBigUInt64BE(BigInt(val), offset);
561
561
  offset += 8;
562
562
  val = fields.redelivered;
563
563
  void 0 === val && (val = !1);
@@ -652,7 +652,7 @@ function decodeBasicGetOk(buffer) {
652
652
  routingKey: void 0,
653
653
  messageCount: void 0,
654
654
  };
655
- val = ints.readUInt64BE(buffer, offset);
655
+ val = Number(buffer.readBigUInt64BE(offset));
656
656
  offset += 8;
657
657
  fields.deliveryTag = val;
658
658
  val = !!(1 & buffer[offset]);
@@ -699,7 +699,7 @@ function encodeBasicGetOk(channel, fields) {
699
699
  val = fields.deliveryTag;
700
700
  if (void 0 === val) throw new Error("Missing value for mandatory field 'deliveryTag'");
701
701
  if ('number' != typeof val || isNaN(val)) throw new TypeError("Field 'deliveryTag' is the wrong type; must be a number (but not NaN)");
702
- ints.writeUInt64BE(buffer, val, offset);
702
+ buffer.writeBigUInt64BE(BigInt(val), offset);
703
703
  offset += 8;
704
704
  val = fields.redelivered;
705
705
  void 0 === val && (val = !1);
@@ -777,7 +777,7 @@ function decodeBasicAck(buffer) {
777
777
  deliveryTag: void 0,
778
778
  multiple: void 0,
779
779
  };
780
- val = ints.readUInt64BE(buffer, offset);
780
+ val = Number(buffer.readBigUInt64BE(offset));
781
781
  offset += 8;
782
782
  fields.deliveryTag = val;
783
783
  val = !!(1 & buffer[offset]);
@@ -798,7 +798,7 @@ function encodeBasicAck(channel, fields) {
798
798
  if (void 0 === val) val = 0;
799
799
  else if ('number' != typeof val || isNaN(val))
800
800
  throw new TypeError("Field 'deliveryTag' is the wrong type; must be a number (but not NaN)");
801
- ints.writeUInt64BE(buffer, val, offset);
801
+ buffer.writeBigUInt64BE(BigInt(val), offset);
802
802
  offset += 8;
803
803
  val = fields.multiple;
804
804
  void 0 === val && (val = !1);
@@ -817,7 +817,7 @@ function decodeBasicReject(buffer) {
817
817
  deliveryTag: void 0,
818
818
  requeue: void 0,
819
819
  };
820
- val = ints.readUInt64BE(buffer, offset);
820
+ val = Number(buffer.readBigUInt64BE(offset));
821
821
  offset += 8;
822
822
  fields.deliveryTag = val;
823
823
  val = !!(1 & buffer[offset]);
@@ -837,7 +837,7 @@ function encodeBasicReject(channel, fields) {
837
837
  val = fields.deliveryTag;
838
838
  if (void 0 === val) throw new Error("Missing value for mandatory field 'deliveryTag'");
839
839
  if ('number' != typeof val || isNaN(val)) throw new TypeError("Field 'deliveryTag' is the wrong type; must be a number (but not NaN)");
840
- ints.writeUInt64BE(buffer, val, offset);
840
+ buffer.writeBigUInt64BE(BigInt(val), offset);
841
841
  offset += 8;
842
842
  val = fields.requeue;
843
843
  void 0 === val && (val = !0);
@@ -931,7 +931,7 @@ function decodeBasicNack(buffer) {
931
931
  multiple: void 0,
932
932
  requeue: void 0,
933
933
  };
934
- val = ints.readUInt64BE(buffer, offset);
934
+ val = Number(buffer.readBigUInt64BE(offset));
935
935
  offset += 8;
936
936
  fields.deliveryTag = val;
937
937
  val = !!(1 & buffer[offset]);
@@ -954,7 +954,7 @@ function encodeBasicNack(channel, fields) {
954
954
  if (void 0 === val) val = 0;
955
955
  else if ('number' != typeof val || isNaN(val))
956
956
  throw new TypeError("Field 'deliveryTag' is the wrong type; must be a number (but not NaN)");
957
- ints.writeUInt64BE(buffer, val, offset);
957
+ buffer.writeBigUInt64BE(BigInt(val), offset);
958
958
  offset += 8;
959
959
  val = fields.multiple;
960
960
  void 0 === val && (val = !1);
@@ -3313,7 +3313,7 @@ function encodeBasicProperties(channel, size, fields) {
3313
3313
  buffer[0] = 2;
3314
3314
  buffer.writeUInt16BE(channel, 1);
3315
3315
  buffer.writeUInt32BE(3932160, 7);
3316
- ints.writeUInt64BE(buffer, size, 11);
3316
+ buffer.writeBigUInt64BE(BigInt(size), 11);
3317
3317
  flags = 0;
3318
3318
  offset = 21;
3319
3319
  val = fields.contentType;
@@ -3384,7 +3384,7 @@ function encodeBasicProperties(channel, size, fields) {
3384
3384
  val = fields.timestamp;
3385
3385
  if (void 0 != val) {
3386
3386
  flags += 64;
3387
- ints.writeUInt64BE(buffer, val, offset);
3387
+ buffer.writeBigUInt64BE(BigInt(val), offset);
3388
3388
  offset += 8;
3389
3389
  }
3390
3390
  val = fields.type;
@@ -3508,7 +3508,7 @@ function decodeBasicProperties(buffer) {
3508
3508
  fields.messageId = val;
3509
3509
  }
3510
3510
  if (64 & flags) {
3511
- val = ints.readUInt64BE(buffer, offset);
3511
+ val = Number(buffer.readBigUInt64BE(offset));
3512
3512
  offset += 8;
3513
3513
  fields.timestamp = val;
3514
3514
  }
@@ -3544,7 +3544,6 @@ function decodeBasicProperties(buffer) {
3544
3544
  }
3545
3545
 
3546
3546
  var codec = require('./codec'),
3547
- ints = require('buffer-more-ints'),
3548
3547
  encodeTable = codec.encodeTable,
3549
3548
  decodeFields = codec.decodeFields,
3550
3549
  SCRATCH = Buffer.alloc(65536),
package/lib/frame.js CHANGED
@@ -1,4 +1,3 @@
1
- const ints = require('buffer-more-ints');
2
1
  const defs = require('./defs');
3
2
  const constants = defs.constants;
4
3
  const decode = defs.decode;
@@ -43,22 +42,8 @@ const FRAME_END_BYTES = 1;
43
42
  * }} FrameStructure
44
43
  */
45
44
 
46
- /**
47
- * This is a polyfill which will read a big int 64 bit as a number.
48
- * @arg { Buffer } buffer
49
- * @arg { number } offset
50
- * @returns { number }
51
- */
52
45
  function readInt64BE(buffer, offset) {
53
- /**
54
- * We try to use native implementation if available here because
55
- * buffer-more-ints does not
56
- */
57
- if (typeof Buffer.prototype.readBigInt64BE === 'function') {
58
- return Number(buffer.readBigInt64BE(offset));
59
- }
60
-
61
- return ints.readInt64BE(buffer, offset);
46
+ return Number(buffer.readBigInt64BE(offset));
62
47
  }
63
48
 
64
49
  // %%% TESTME possibly better to cons the first bit and write the
package/lib/recovery.js CHANGED
@@ -7,6 +7,7 @@ const DEFAULT_RECOVERY = {
7
7
  jitter: 0.2,
8
8
  maxRetries: Infinity,
9
9
  setup: null,
10
+ waitForConnect: true,
10
11
  };
11
12
 
12
13
  function splitConnectionOptions(connOptions) {
@@ -57,6 +58,7 @@ function normaliseRecoveryOptions(recovery) {
57
58
  jitter,
58
59
  maxRetries: parsedMaxRetries < 0 ? 0 : parsedMaxRetries,
59
60
  setup: typeof source.setup === 'function' ? source.setup : null,
61
+ waitForConnect: source.waitForConnect !== false,
60
62
  };
61
63
  }
62
64
 
@@ -124,8 +126,15 @@ class RecoveringCore extends EventEmitter {
124
126
  this._resolveInitial = resolve;
125
127
  this._rejectInitial = reject;
126
128
  });
127
-
128
- this._connect();
129
+ // The initial connection promise is only consumed if the caller opts to
130
+ // wait for it, so swallow rejections here to avoid unhandled rejections
131
+ // when close() is called or retries are exhausted before anyone awaits it.
132
+ this._initialWait.catch(() => {});
133
+
134
+ // Defer the first attempt so callers that receive the model before the
135
+ // initial connection have a chance to attach listeners before any
136
+ // 'connect', 'connect-failed' or 'reconnect-scheduled' events are emitted.
137
+ setImmediate(() => this._connect());
129
138
  }
130
139
 
131
140
  waitForConnect() {
@@ -260,7 +269,7 @@ class RecoveringCore extends EventEmitter {
260
269
  let model;
261
270
  try {
262
271
  model = await this._openModel();
263
- await runSetup(this._recovery.setup, model);
272
+ if (!this._stopped) await runSetup(this._recovery.setup, model);
264
273
  if (this._stopped) {
265
274
  await this._closeModelNoThrow(model);
266
275
  return;
@@ -452,12 +461,17 @@ class RecoveringCallbackModel extends EventEmitter {
452
461
 
453
462
  function connectWithRecoveryPromise(openModel, recovery) {
454
463
  const recovering = new RecoveringPromiseModel(openModel, recovery);
464
+ if (!recovering._core._recovery.waitForConnect) return Promise.resolve(recovering);
455
465
  return recovering.waitForConnect();
456
466
  }
457
467
 
458
468
  function connectWithRecoveryCallback(openModel, recovery, cb) {
459
469
  const recovering = new RecoveringCallbackModel(openModel, recovery);
460
- recovering.waitForConnect(cb);
470
+ if (recovering._core._recovery.waitForConnect) {
471
+ recovering.waitForConnect(cb);
472
+ } else if (typeof cb === 'function') {
473
+ process.nextTick(cb, null, recovering);
474
+ }
461
475
  return recovering;
462
476
  }
463
477
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "amqplib",
3
3
  "homepage": "http://amqp-node.github.io/amqplib/",
4
4
  "main": "./channel_api.js",
5
- "version": "2.0.0",
5
+ "version": "2.1.0",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
8
8
  ".": {
@@ -22,9 +22,7 @@
22
22
  "engines": {
23
23
  "node": ">=18"
24
24
  },
25
- "dependencies": {
26
- "buffer-more-ints": "~1.0.0"
27
- },
25
+ "dependencies": {},
28
26
  "devDependencies": {
29
27
  "@biomejs/biome": "^2.2.2",
30
28
  "@types/node": "^25.6.0",
@@ -1,6 +1,5 @@
1
1
  const { describe, it } = require('node:test');
2
2
  const assert = require('node:assert');
3
- const ints = require('buffer-more-ints');
4
3
  const { forAll } = require('claire');
5
4
  const codec = require('../lib/codec');
6
5
  const defs = require('../lib/defs');
@@ -152,7 +151,7 @@ describe('Codec', () => {
152
151
  const buf = defs.encodeProperties(properties.id, 0, properties.size, properties.fields);
153
152
  // FIXME depends on framing, ugh
154
153
  const fs1 = defs.decode(properties.id, buf.subarray(19, buf.length));
155
- assert.equal(properties.size, ints.readUInt64BE(buf, 11));
154
+ assert.equal(properties.size, Number(buf.readBigUInt64BE(11)));
156
155
  assertEqualModuloDefaults(properties, fs1);
157
156
  return true;
158
157
  });
@@ -223,4 +223,180 @@ describe('recovery', () => {
223
223
  assert(failed);
224
224
  assert.equal(2, attempts);
225
225
  });
226
+
227
+ describe('waitForConnect: false', () => {
228
+ function watchUnhandledRejections(t) {
229
+ const rejections = [];
230
+ const onRejection = (err) => rejections.push(err);
231
+ process.on('unhandledRejection', onRejection);
232
+ t.after(() => process.removeListener('unhandledRejection', onRejection));
233
+ return rejections;
234
+ }
235
+
236
+ it('promise connect resolves before the first connection and later emits connect', async () => {
237
+ let opened = 0;
238
+
239
+ function openModel() {
240
+ return Promise.resolve(new FakePromiseModel(++opened));
241
+ }
242
+
243
+ const client = await recovery.connectWithRecoveryPromise(openModel, {waitForConnect: false});
244
+
245
+ assert.equal(0, opened);
246
+
247
+ const model = await new Promise((resolve) => client.once('connect', resolve));
248
+ assert.equal(1, model.id);
249
+ assert.equal(1, opened);
250
+
251
+ await client.close();
252
+ });
253
+
254
+ it('listeners attached after connect observe a failed first attempt', async () => {
255
+ let attempts = 0;
256
+
257
+ function openModel() {
258
+ attempts++;
259
+ return attempts === 1 ? Promise.reject(new Error('broker unavailable')) : Promise.resolve(new FakePromiseModel(attempts));
260
+ }
261
+
262
+ const client = await recovery.connectWithRecoveryPromise(openModel, {
263
+ initialDelay: 1,
264
+ maxDelay: 1,
265
+ jitter: 0,
266
+ waitForConnect: false,
267
+ });
268
+
269
+ const failures = [];
270
+ const scheduled = [];
271
+ client.on('connect-failed', (err) => failures.push(err));
272
+ client.on('reconnect-scheduled', (info) => scheduled.push(info));
273
+
274
+ await client.waitForConnect();
275
+
276
+ assert.equal(1, failures.length);
277
+ assert.equal('broker unavailable', failures[0].message);
278
+ assert.equal(1, scheduled.length);
279
+ assert.equal(1, scheduled[0].attempt);
280
+ assert.strictEqual(failures[0], scheduled[0].error);
281
+ assert.equal(2, attempts);
282
+
283
+ await client.close();
284
+ });
285
+
286
+ it('waitForConnect resolves with the recovering model once connected', async () => {
287
+ function openModel() {
288
+ return Promise.resolve(new FakePromiseModel(1));
289
+ }
290
+
291
+ const client = await recovery.connectWithRecoveryPromise(openModel, {waitForConnect: false});
292
+ const connected = await client.waitForConnect();
293
+
294
+ assert.strictEqual(client, connected);
295
+ const ch = await client.createChannel();
296
+ assert.equal(1, ch.id);
297
+
298
+ await client.close();
299
+ });
300
+
301
+ it('close before the first connection cancels it without an unhandled rejection', async (t) => {
302
+ const rejections = watchUnhandledRejections(t);
303
+ let setupCalls = 0;
304
+ let closeCalls = 0;
305
+ let releaseOpen;
306
+
307
+ function openModel() {
308
+ return new Promise((resolve) => {
309
+ const model = new FakePromiseModel(1);
310
+ model.close = () => {
311
+ closeCalls++;
312
+ return Promise.resolve();
313
+ };
314
+ releaseOpen = () => resolve(model);
315
+ });
316
+ }
317
+
318
+ const client = await recovery.connectWithRecoveryPromise(openModel, {
319
+ waitForConnect: false,
320
+ setup() {
321
+ setupCalls++;
322
+ },
323
+ });
324
+
325
+ let connected = false;
326
+ client.on('connect', () => {
327
+ connected = true;
328
+ });
329
+
330
+ const pendingChannel = client.createChannel();
331
+ await new Promise((resolve) => setImmediate(resolve));
332
+ assert(releaseOpen, 'open should be in flight');
333
+
334
+ await client.close();
335
+
336
+ await assert.rejects(pendingChannel, {message: 'Connection closed'});
337
+ await assert.rejects(client.waitForConnect(), {message: 'Connection closed'});
338
+
339
+ releaseOpen();
340
+ await new Promise((resolve) => setImmediate(resolve));
341
+
342
+ assert.equal(0, setupCalls);
343
+ assert.equal(1, closeCalls);
344
+ assert.equal(false, connected);
345
+ assert.deepEqual([], rejections);
346
+ });
347
+
348
+ it('exhausting retries emits reconnect-failed without an unhandled rejection', async (t) => {
349
+ const rejections = watchUnhandledRejections(t);
350
+ let attempts = 0;
351
+
352
+ function openModel() {
353
+ attempts++;
354
+ return Promise.reject(new Error('connect failed'));
355
+ }
356
+
357
+ const client = await recovery.connectWithRecoveryPromise(openModel, {
358
+ initialDelay: 1,
359
+ maxDelay: 1,
360
+ jitter: 0,
361
+ maxRetries: 1,
362
+ waitForConnect: false,
363
+ });
364
+
365
+ const err = await new Promise((resolve) => client.once('reconnect-failed', resolve));
366
+ assert.equal('connect failed', err.message);
367
+ assert.equal(2, attempts);
368
+
369
+ await assert.rejects(client.waitForConnect(), {message: 'connect failed'});
370
+ await new Promise((resolve) => setImmediate(resolve));
371
+ assert.deepEqual([], rejections);
372
+
373
+ await client.close();
374
+ });
375
+
376
+ it('callback connect invokes the callback before the first connection', (_t, done) => {
377
+ let opened = 0;
378
+
379
+ function openModel() {
380
+ return Promise.resolve(new FakeCallbackModel(++opened));
381
+ }
382
+
383
+ const failures = [];
384
+ const client = recovery.connectWithRecoveryCallback(openModel, {waitForConnect: false}, (err, c) => {
385
+ if (err) return done(err);
386
+ assert.strictEqual(client, c);
387
+ assert.equal(0, opened);
388
+
389
+ c.on('connect-failed', (failure) => failures.push(failure));
390
+ c.waitForConnect((waitErr, connected) => {
391
+ if (waitErr) return done(waitErr);
392
+ assert.strictEqual(client, connected);
393
+ assert.equal(1, opened);
394
+ assert.deepEqual([], failures);
395
+ c.close(done);
396
+ });
397
+ });
398
+
399
+ assert(client);
400
+ });
401
+ });
226
402
  });