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.
- package/.claude/settings.local.json +7 -0
- package/CHANGELOG.md +4 -0
- package/README.md +33 -1
- package/callback_api.d.ts +9 -1
- package/index.d.ts +9 -0
- package/lib/recovery.js +18 -4
- package/package.json +1 -1
- package/test/recovery.test.js +176 -0
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
|
[](https://github.com/amqp-node/amqplib/actions?query=workflow%3A%22Node.js+CI%22)
|
|
6
6
|
[](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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
package/test/recovery.test.js
CHANGED
|
@@ -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
|
});
|