amqplib 2.0.1 → 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,9 @@
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
+
3
7
  ## v2.0.1
4
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
5
9
 
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
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/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.1",
5
+ "version": "2.1.0",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
8
8
  ".": {
@@ -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
  });