amqplib 1.0.7 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Change log for amqplib
2
2
 
3
+ ## v1.1.1
4
+ - Forward `handler-error` events from the underlying connection to the recovery wrapper, so listeners attached to `RecoveringPromiseModel`/`RecoveringCallbackModel` receive handler errors
5
+
6
+ ## v1.1.0
7
+ - Add connection recovery via `connectWithRecoveryPromise` and `connectWithRecoveryCallback` — automatically reconnects on connection loss with configurable backoff delay, jitter, and retry limits (thanks [@ShiriNmi1520](https://github.com/ShiriNmi1520))
8
+
3
9
  ## v1.0.7
4
10
  - Fix `update-secret-ok` event not being forwarded by `ChannelModel` and `CallbackModel` (fixes #849)
5
11
  - Add `handler-error` event to connections and channels. If a user-supplied event handler (e.g. `connection.on('close', ...)`, `channel.on('error', ...)`, `channel.on('delivery', ...)` etc.) throws a synchronous error, amqplib will emit a `handler-error` event on the same emitter with the thrown error and the name of the event whose handler threw — provided a `handler-error` listener is registered. If no `handler-error` listener is registered, behaviour is unchanged from previous versions. Note: in previous versions, errors thrown in connection `close` event handlers were silently swallowed; errors thrown in channel event handlers (other than `delivery`/`return`) would kill the channel and possibly the connection (fixes #334).
package/README.md CHANGED
@@ -118,6 +118,66 @@ const amqplib = require('amqplib');
118
118
 
119
119
  ```
120
120
 
121
+ ## Opt-in recovery
122
+
123
+ Automatic recovery is available as an opt-in feature through `connect` options:
124
+
125
+ ```javascript
126
+ const amqplib = require('amqplib');
127
+
128
+ const connection = await amqplib.connect('amqp://localhost', {
129
+ recovery: {
130
+ initialDelay: 200, // ms
131
+ maxDelay: 5000, // ms
132
+ factor: 2,
133
+ jitter: 0.2,
134
+ maxRetries: Infinity,
135
+ async setup(model) {
136
+ // Called after every successful (re)connect.
137
+ // Recreate topology/consumers here.
138
+ const ch = await model.createChannel();
139
+ await ch.assertQueue('tasks', {durable: true});
140
+ },
141
+ },
142
+ });
143
+
144
+ connection.on('connect', () => {
145
+ console.log('connected');
146
+ });
147
+
148
+ connection.on('disconnect', (err) => {
149
+ console.warn('disconnected', err.message);
150
+ });
151
+ ```
152
+
153
+ Callback API supports the same option:
154
+
155
+ ```javascript
156
+ const amqplib = require('amqplib/callback_api');
157
+
158
+ amqplib.connect(
159
+ 'amqp://localhost',
160
+ {
161
+ recovery: {
162
+ initialDelay: 200,
163
+ maxDelay: 5000,
164
+ setup(model, done) {
165
+ model.createChannel((err, ch) => {
166
+ if (err) return done(err);
167
+ ch.assertQueue('tasks', {durable: true}, done);
168
+ });
169
+ },
170
+ },
171
+ },
172
+ (err, conn) => {
173
+ if (err) throw err;
174
+ conn.on('connect', () => console.log('connected'));
175
+ },
176
+ );
177
+ ```
178
+
179
+ Without `recovery` options, behavior is unchanged.
180
+
121
181
  ## Error handling in event handlers
122
182
 
123
183
  If a user-supplied event handler throws a synchronous error, the throw will
package/callback_api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const raw_connect = require('./lib/connect').connect;
2
2
  const CallbackModel = require('./lib/callback_model').CallbackModel;
3
+ const recovery = require('./lib/recovery');
3
4
 
4
5
  // Supports three shapes:
5
6
  // connect(url, options, callback)
@@ -15,7 +16,21 @@ function connect(url, options, cb) {
15
16
  options = false;
16
17
  }
17
18
 
18
- raw_connect(url, options, (err, c) => {
19
+ const {connectionOptions, recovery: recoveryOptions} = recovery.splitConnectionOptions(options);
20
+ if (recovery.recoveryEnabled(recoveryOptions)) {
21
+ const openModel = () => {
22
+ return new Promise((resolve, reject) => {
23
+ raw_connect(url, connectionOptions, (err, c) => {
24
+ if (err === null) resolve(new CallbackModel(c));
25
+ else reject(err);
26
+ });
27
+ });
28
+ };
29
+
30
+ return recovery.connectWithRecoveryCallback(openModel, recoveryOptions, cb);
31
+ }
32
+
33
+ raw_connect(url, connectionOptions, (err, c) => {
19
34
  if (err === null) cb(null, new CallbackModel(c));
20
35
  else cb(err);
21
36
  });
package/channel_api.js CHANGED
@@ -1,9 +1,17 @@
1
1
  const raw_connect = require('./lib/connect').connect;
2
2
  const ChannelModel = require('./lib/channel_model').ChannelModel;
3
3
  const promisify = require('node:util').promisify;
4
+ const recovery = require('./lib/recovery');
4
5
 
5
6
  function connect(url, connOptions) {
6
- return promisify((cb) => raw_connect(url, connOptions, cb))().then((conn) => new ChannelModel(conn));
7
+ const {connectionOptions, recovery: recoveryOptions} = recovery.splitConnectionOptions(connOptions);
8
+ if (recovery.recoveryEnabled(recoveryOptions)) {
9
+ const openModel = () => promisify((cb) => raw_connect(url, connectionOptions, cb))().then((conn) => new ChannelModel(conn));
10
+
11
+ return recovery.connectWithRecoveryPromise(openModel, recoveryOptions);
12
+ }
13
+
14
+ return promisify((cb) => raw_connect(url, connectionOptions, cb))().then((conn) => new ChannelModel(conn));
7
15
  }
8
16
 
9
17
  module.exports.connect = connect;
@@ -0,0 +1,471 @@
1
+ const EventEmitter = require('node:events');
2
+
3
+ const DEFAULT_RECOVERY = {
4
+ initialDelay: 100,
5
+ maxDelay: 30000,
6
+ factor: 2,
7
+ jitter: 0.2,
8
+ maxRetries: Infinity,
9
+ setup: null,
10
+ };
11
+
12
+ function splitConnectionOptions(connOptions) {
13
+ if (!connOptions || typeof connOptions !== 'object') {
14
+ return {
15
+ connectionOptions: connOptions,
16
+ recovery: null,
17
+ };
18
+ }
19
+
20
+ if (!Object.hasOwn(connOptions, 'recovery')) {
21
+ return {
22
+ connectionOptions: connOptions,
23
+ recovery: null,
24
+ };
25
+ }
26
+
27
+ const connectionOptions = Object.assign({}, connOptions);
28
+ const recovery = connectionOptions.recovery;
29
+ delete connectionOptions.recovery;
30
+
31
+ return { connectionOptions, recovery };
32
+ }
33
+
34
+ function recoveryEnabled(recovery) {
35
+ if (!recovery) return false;
36
+ if (recovery === true) return true;
37
+ return recovery.enabled !== false;
38
+ }
39
+
40
+ function toFiniteNumber(value, fallback) {
41
+ const n = Number(value);
42
+ return Number.isFinite(n) ? n : fallback;
43
+ }
44
+
45
+ function normaliseRecoveryOptions(recovery) {
46
+ const source = recovery === true ? {} : recovery || {};
47
+ const initialDelay = Math.max(0, toFiniteNumber(source.initialDelay, DEFAULT_RECOVERY.initialDelay));
48
+ const maxDelay = Math.max(initialDelay, toFiniteNumber(source.maxDelay, DEFAULT_RECOVERY.maxDelay));
49
+ const factor = Math.max(1, toFiniteNumber(source.factor, DEFAULT_RECOVERY.factor));
50
+ const jitter = Math.min(1, Math.max(0, toFiniteNumber(source.jitter, DEFAULT_RECOVERY.jitter)));
51
+ const parsedMaxRetries = source.maxRetries == null ? Infinity : toFiniteNumber(source.maxRetries, Infinity);
52
+
53
+ return {
54
+ initialDelay,
55
+ maxDelay,
56
+ factor,
57
+ jitter,
58
+ maxRetries: parsedMaxRetries < 0 ? 0 : parsedMaxRetries,
59
+ setup: typeof source.setup === 'function' ? source.setup : null,
60
+ };
61
+ }
62
+
63
+ function toError(err, fallbackMessage) {
64
+ if (err instanceof Error) return err;
65
+ return new Error(err ? String(err) : fallbackMessage);
66
+ }
67
+
68
+ function calculateDelay(recovery, attempt) {
69
+ const base = Math.min(recovery.maxDelay, recovery.initialDelay * recovery.factor ** (attempt - 1));
70
+ const jitterPart = base * recovery.jitter;
71
+ const offset = jitterPart > 0 ? Math.random() * jitterPart * 2 - jitterPart : 0;
72
+ return Math.max(0, Math.round(base + offset));
73
+ }
74
+
75
+ function runSetup(setup, model) {
76
+ if (!setup) return Promise.resolve();
77
+
78
+ return new Promise((resolve, reject) => {
79
+ let settled = false;
80
+
81
+ function done(err) {
82
+ if (settled) return;
83
+ settled = true;
84
+ if (err) reject(toError(err, 'Recovery setup failed'));
85
+ else resolve();
86
+ }
87
+
88
+ try {
89
+ if (setup.length >= 2) {
90
+ setup(model, done);
91
+ } else {
92
+ Promise.resolve(setup(model)).then(() => done(), done);
93
+ }
94
+ } catch (err) {
95
+ done(err);
96
+ }
97
+ });
98
+ }
99
+
100
+ function makeCallback(promise, cb) {
101
+ const callback = typeof cb === 'function' ? cb : () => {};
102
+ promise.then(
103
+ (value) => callback(null, value),
104
+ (err) => callback(toError(err, 'Operation failed')),
105
+ );
106
+ }
107
+
108
+ class RecoveringCore extends EventEmitter {
109
+ constructor(openModel, recovery, adapters) {
110
+ super();
111
+ this._openModel = openModel;
112
+ this._recovery = normaliseRecoveryOptions(recovery);
113
+ this._adapters = adapters;
114
+ this._model = null;
115
+ this._modelListeners = null;
116
+ this._waiters = [];
117
+ this._connecting = false;
118
+ this._stopped = false;
119
+ this._timer = null;
120
+ this._attempt = 0;
121
+
122
+ this._initialReady = false;
123
+ this._initialWait = new Promise((resolve, reject) => {
124
+ this._resolveInitial = resolve;
125
+ this._rejectInitial = reject;
126
+ });
127
+
128
+ this._connect();
129
+ }
130
+
131
+ waitForConnect() {
132
+ return this._initialWait;
133
+ }
134
+
135
+ async close() {
136
+ if (this._stopped) return;
137
+ this._stopped = true;
138
+
139
+ if (this._timer) {
140
+ clearTimeout(this._timer);
141
+ this._timer = null;
142
+ }
143
+
144
+ const closedError = new Error('Connection closed');
145
+ this._rejectPendingWaiters(closedError);
146
+ this._rejectInitialConnection(closedError);
147
+
148
+ if (this._model) {
149
+ const model = this._model;
150
+ this._model = null;
151
+ this._unbindModel();
152
+ await this._closeModelNoThrow(model);
153
+ }
154
+ }
155
+
156
+ async createChannel(options) {
157
+ const model = await this._waitForConnection();
158
+ return this._adapters.createChannel(model, options, false);
159
+ }
160
+
161
+ async createConfirmChannel(options) {
162
+ const model = await this._waitForConnection();
163
+ return this._adapters.createChannel(model, options, true);
164
+ }
165
+
166
+ async updateSecret(newSecret, reason) {
167
+ const model = await this._waitForConnection();
168
+ return this._adapters.updateSecret(model, newSecret, reason);
169
+ }
170
+
171
+ _resolveInitialConnection() {
172
+ if (this._initialReady) return;
173
+ this._initialReady = true;
174
+ this._resolveInitial();
175
+ }
176
+
177
+ _rejectInitialConnection(err) {
178
+ if (this._initialReady) return;
179
+ this._initialReady = true;
180
+ this._rejectInitial(err);
181
+ }
182
+
183
+ _resolvePendingWaiters(model) {
184
+ const waiters = this._waiters.splice(0);
185
+ waiters.forEach((waiter) => {
186
+ waiter.resolve(model);
187
+ });
188
+ }
189
+
190
+ _rejectPendingWaiters(err) {
191
+ const waiters = this._waiters.splice(0);
192
+ waiters.forEach((waiter) => {
193
+ waiter.reject(err);
194
+ });
195
+ }
196
+
197
+ _waitForConnection() {
198
+ if (this._model) return Promise.resolve(this._model);
199
+ if (this._stopped) return Promise.reject(new Error('Connection closed'));
200
+
201
+ return new Promise((resolve, reject) => {
202
+ this._waiters.push({ resolve, reject });
203
+ });
204
+ }
205
+
206
+ _bindModel(model) {
207
+ const onClose = (maybeErr) => {
208
+ if (this._model !== model) return;
209
+
210
+ this._model = null;
211
+ this._unbindModel();
212
+ if (this._stopped) return;
213
+
214
+ const err = toError(maybeErr, 'Connection closed');
215
+ this.emit('disconnect', err);
216
+ this._scheduleReconnect(err);
217
+ };
218
+
219
+ const onBlocked = (reason) => this.emit('blocked', reason);
220
+ const onUnblocked = () => this.emit('unblocked');
221
+ const onConnectionError = (err) => this.emit('error', err);
222
+ const onUpdateSecretOk = () => this.emit('update-secret-ok');
223
+ const onHandlerError = (err, event) => this.emit('handler-error', err, event);
224
+
225
+ model.on('close', onClose);
226
+ model.on('blocked', onBlocked);
227
+ model.on('unblocked', onUnblocked);
228
+ model.on('error', onConnectionError);
229
+ model.on('update-secret-ok', onUpdateSecretOk);
230
+ model.on('handler-error', onHandlerError);
231
+
232
+ this._modelListeners = {
233
+ model,
234
+ onClose,
235
+ onBlocked,
236
+ onUnblocked,
237
+ onConnectionError,
238
+ onUpdateSecretOk,
239
+ onHandlerError,
240
+ };
241
+ }
242
+
243
+ _unbindModel() {
244
+ if (!this._modelListeners) return;
245
+ const { model, onClose, onBlocked, onUnblocked, onConnectionError, onUpdateSecretOk, onHandlerError } = this._modelListeners;
246
+ model.removeListener('close', onClose);
247
+ model.removeListener('blocked', onBlocked);
248
+ model.removeListener('unblocked', onUnblocked);
249
+ model.removeListener('error', onConnectionError);
250
+ model.removeListener('update-secret-ok', onUpdateSecretOk);
251
+ model.removeListener('handler-error', onHandlerError);
252
+ model.on('error', () => {});
253
+ this._modelListeners = null;
254
+ }
255
+
256
+ async _connect() {
257
+ if (this._stopped || this._connecting) return;
258
+ this._connecting = true;
259
+
260
+ let model;
261
+ try {
262
+ model = await this._openModel();
263
+ await runSetup(this._recovery.setup, model);
264
+ if (this._stopped) {
265
+ await this._closeModelNoThrow(model);
266
+ return;
267
+ }
268
+
269
+ this._model = model;
270
+ this._attempt = 0;
271
+ this._bindModel(model);
272
+ this._resolveInitialConnection();
273
+ this._resolvePendingWaiters(model);
274
+ this.emit('connect', model);
275
+ } catch (err) {
276
+ const error = toError(err, 'Connection recovery failed');
277
+ if (model) await this._closeModelNoThrow(model);
278
+ this.emit('connect-failed', error);
279
+ this._scheduleReconnect(error);
280
+ } finally {
281
+ this._connecting = false;
282
+ }
283
+ }
284
+
285
+ _scheduleReconnect(err) {
286
+ if (this._stopped) return;
287
+
288
+ if (this._timer) return;
289
+
290
+ if (this._attempt >= this._recovery.maxRetries) {
291
+ this._rejectInitialConnection(err);
292
+ this._rejectPendingWaiters(err);
293
+ this.emit('reconnect-failed', err);
294
+ return;
295
+ }
296
+
297
+ const attempt = this._attempt + 1;
298
+ const delay = calculateDelay(this._recovery, attempt);
299
+ this._attempt = attempt;
300
+
301
+ this.emit('reconnect-scheduled', { attempt, delay, error: err });
302
+ this._timer = setTimeout(() => {
303
+ this._timer = null;
304
+ this._connect();
305
+ }, delay);
306
+ }
307
+
308
+ _closeModelNoThrow(model) {
309
+ return this._adapters.closeModel(model).catch(() => {});
310
+ }
311
+ }
312
+
313
+ const PROMISE_ADAPTERS = {
314
+ closeModel(model) {
315
+ return model.close();
316
+ },
317
+ createChannel(model, options, confirm) {
318
+ return confirm ? model.createConfirmChannel(options) : model.createChannel(options);
319
+ },
320
+ updateSecret(model, newSecret, reason) {
321
+ return model.updateSecret(newSecret, reason);
322
+ },
323
+ };
324
+
325
+ function callbackClose(model) {
326
+ return new Promise((resolve, reject) => {
327
+ model.close((err) => {
328
+ if (err) reject(err);
329
+ else resolve();
330
+ });
331
+ });
332
+ }
333
+
334
+ function callbackCreateChannel(model, options, confirm) {
335
+ return new Promise((resolve, reject) => {
336
+ const method = confirm ? 'createConfirmChannel' : 'createChannel';
337
+ const cb = (err, channel) => {
338
+ if (err) reject(err);
339
+ else resolve(channel);
340
+ };
341
+
342
+ if (options === undefined) model[method](cb);
343
+ else model[method](options, cb);
344
+ });
345
+ }
346
+
347
+ function callbackUpdateSecret(model, newSecret, reason) {
348
+ return new Promise((resolve, reject) => {
349
+ model.updateSecret(newSecret, reason, (err, ok) => {
350
+ if (err) reject(err);
351
+ else resolve(ok);
352
+ });
353
+ });
354
+ }
355
+
356
+ const CALLBACK_ADAPTERS = {
357
+ closeModel: callbackClose,
358
+ createChannel: callbackCreateChannel,
359
+ updateSecret: callbackUpdateSecret,
360
+ };
361
+
362
+ function wireCommonEvents(source, target) {
363
+ [
364
+ 'connect',
365
+ 'disconnect',
366
+ 'connect-failed',
367
+ 'reconnect-scheduled',
368
+ 'reconnect-failed',
369
+ 'blocked',
370
+ 'unblocked',
371
+ 'error',
372
+ 'update-secret-ok',
373
+ 'handler-error',
374
+ ].forEach((eventName) => {
375
+ source.on(eventName, (...args) => target.emit(eventName, ...args));
376
+ });
377
+ }
378
+
379
+ class RecoveringPromiseModel extends EventEmitter {
380
+ constructor(openModel, recovery) {
381
+ super();
382
+ this._core = new RecoveringCore(openModel, recovery, PROMISE_ADAPTERS);
383
+ wireCommonEvents(this._core, this);
384
+ }
385
+
386
+ async waitForConnect() {
387
+ await this._core.waitForConnect();
388
+ return this;
389
+ }
390
+
391
+ close() {
392
+ return this._core.close();
393
+ }
394
+
395
+ createChannel(options) {
396
+ return this._core.createChannel(options);
397
+ }
398
+
399
+ createConfirmChannel(options) {
400
+ return this._core.createConfirmChannel(options);
401
+ }
402
+
403
+ updateSecret(newSecret, reason) {
404
+ return this._core.updateSecret(newSecret, reason);
405
+ }
406
+ }
407
+
408
+ class RecoveringCallbackModel extends EventEmitter {
409
+ constructor(openModel, recovery) {
410
+ super();
411
+ this._core = new RecoveringCore(openModel, recovery, CALLBACK_ADAPTERS);
412
+ wireCommonEvents(this._core, this);
413
+ }
414
+
415
+ waitForConnect(cb) {
416
+ const callback = typeof cb === 'function' ? cb : () => {};
417
+ this._core.waitForConnect().then(
418
+ () => callback(null, this),
419
+ (err) => callback(toError(err, 'Operation failed')),
420
+ );
421
+ return this;
422
+ }
423
+
424
+ close(cb) {
425
+ makeCallback(this._core.close(), cb);
426
+ return this;
427
+ }
428
+
429
+ updateSecret(newSecret, reason, cb) {
430
+ makeCallback(this._core.updateSecret(newSecret, reason), cb);
431
+ return this;
432
+ }
433
+
434
+ createChannel(options, cb) {
435
+ if (typeof options === 'function') {
436
+ cb = options;
437
+ options = undefined;
438
+ }
439
+ makeCallback(this._core.createChannel(options), cb);
440
+ return this;
441
+ }
442
+
443
+ createConfirmChannel(options, cb) {
444
+ if (typeof options === 'function') {
445
+ cb = options;
446
+ options = undefined;
447
+ }
448
+ makeCallback(this._core.createConfirmChannel(options), cb);
449
+ return this;
450
+ }
451
+ }
452
+
453
+ function connectWithRecoveryPromise(openModel, recovery) {
454
+ const recovering = new RecoveringPromiseModel(openModel, recovery);
455
+ return recovering.waitForConnect();
456
+ }
457
+
458
+ function connectWithRecoveryCallback(openModel, recovery, cb) {
459
+ const recovering = new RecoveringCallbackModel(openModel, recovery);
460
+ recovering.waitForConnect(cb);
461
+ return recovering;
462
+ }
463
+
464
+ module.exports = {
465
+ splitConnectionOptions,
466
+ recoveryEnabled,
467
+ connectWithRecoveryPromise,
468
+ connectWithRecoveryCallback,
469
+ RecoveringPromiseModel,
470
+ RecoveringCallbackModel,
471
+ };
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": "1.0.7",
5
+ "version": "1.1.1",
6
6
  "description": "An AMQP 0-9-1 (e.g., RabbitMQ) library and client.",
7
7
  "repository": {
8
8
  "type": "git",
@@ -0,0 +1,226 @@
1
+
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert');
5
+ const EventEmitter = require('node:events');
6
+ const recovery = require('../lib/recovery');
7
+
8
+ class FakePromiseModel extends EventEmitter {
9
+ constructor(id) {
10
+ super();
11
+ this.id = id;
12
+ }
13
+
14
+ close() {
15
+ return Promise.resolve();
16
+ }
17
+
18
+ createChannel(options) {
19
+ return Promise.resolve({kind: 'channel', id: this.id, options});
20
+ }
21
+
22
+ createConfirmChannel(options) {
23
+ return Promise.resolve({kind: 'confirm', id: this.id, options});
24
+ }
25
+
26
+ updateSecret(_newSecret, _reason) {
27
+ return Promise.resolve();
28
+ }
29
+ }
30
+
31
+ class FakeCallbackModel extends EventEmitter {
32
+ constructor(id) {
33
+ super();
34
+ this.id = id;
35
+ }
36
+
37
+ close(cb) {
38
+ cb && cb(null);
39
+ }
40
+
41
+ createChannel(options, cb) {
42
+ if (typeof options === 'function') {
43
+ cb = options;
44
+ options = undefined;
45
+ }
46
+ cb && cb(null, {kind: 'channel', id: this.id, options});
47
+ }
48
+
49
+ createConfirmChannel(options, cb) {
50
+ if (typeof options === 'function') {
51
+ cb = options;
52
+ options = undefined;
53
+ }
54
+ cb && cb(null, {kind: 'confirm', id: this.id, options});
55
+ }
56
+
57
+ updateSecret(_newSecret, _reason, cb) {
58
+ cb && cb(null);
59
+ }
60
+ }
61
+
62
+ describe('recovery', () => {
63
+ it('promise recovery reconnects after close', async () => {
64
+ const models = [];
65
+ let opened = 0;
66
+
67
+ function openModel() {
68
+ const model = new FakePromiseModel(++opened);
69
+ models.push(model);
70
+ return Promise.resolve(model);
71
+ }
72
+
73
+ const client = await recovery.connectWithRecoveryPromise(openModel, {
74
+ initialDelay: 1,
75
+ maxDelay: 1,
76
+ jitter: 0,
77
+ maxRetries: 3,
78
+ });
79
+
80
+ assert.equal(1, opened);
81
+
82
+ await new Promise((resolve) => {
83
+ client.once('connect', () => {
84
+ assert.equal(2, opened);
85
+ resolve();
86
+ });
87
+
88
+ models[0].emit('close', new Error('socket closed'));
89
+ });
90
+
91
+ await client.close();
92
+ });
93
+
94
+ it('promise recovery runs setup on every connect', async () => {
95
+ const models = [];
96
+ let setupCalls = 0;
97
+ let opened = 0;
98
+
99
+ function openModel() {
100
+ const model = new FakePromiseModel(++opened);
101
+ models.push(model);
102
+ return Promise.resolve(model);
103
+ }
104
+
105
+ const client = await recovery.connectWithRecoveryPromise(openModel, {
106
+ initialDelay: 1,
107
+ maxDelay: 1,
108
+ jitter: 0,
109
+ maxRetries: 3,
110
+ setup() {
111
+ setupCalls++;
112
+ },
113
+ });
114
+
115
+ assert.equal(1, setupCalls);
116
+
117
+ await new Promise((resolve) => {
118
+ client.once('connect', () => {
119
+ assert.equal(2, setupCalls);
120
+ resolve();
121
+ });
122
+
123
+ models[0].emit('close', new Error('socket closed'));
124
+ });
125
+
126
+ await client.close();
127
+ });
128
+
129
+ it('callback recovery reconnects and creates channels after reconnect', (done) => {
130
+ const models = [];
131
+ let opened = 0;
132
+
133
+ function openModel() {
134
+ const model = new FakeCallbackModel(++opened);
135
+ models.push(model);
136
+ return Promise.resolve(model);
137
+ }
138
+
139
+ const client = recovery.connectWithRecoveryCallback(
140
+ openModel,
141
+ {
142
+ initialDelay: 1,
143
+ maxDelay: 1,
144
+ jitter: 0,
145
+ maxRetries: 3,
146
+ },
147
+ (err, c) => {
148
+ if (err) return done(err);
149
+
150
+ c.createChannel((createErr, ch) => {
151
+ if (createErr) return done(createErr);
152
+ assert.equal(1, ch.id);
153
+
154
+ c.once('connect', () => {
155
+ c.createChannel((reconnectErr, reconnectedChannel) => {
156
+ if (reconnectErr) return done(reconnectErr);
157
+ assert.equal(2, reconnectedChannel.id);
158
+ c.close(done);
159
+ });
160
+ });
161
+
162
+ models[0].emit('close', new Error('socket closed'));
163
+ });
164
+ },
165
+ );
166
+
167
+ assert(client);
168
+ });
169
+
170
+ it('handler-error from underlying model is forwarded to recovery wrapper', async () => {
171
+ const models = [];
172
+ let opened = 0;
173
+
174
+ function openModel() {
175
+ const model = new FakePromiseModel(++opened);
176
+ models.push(model);
177
+ return Promise.resolve(model);
178
+ }
179
+
180
+ const client = await recovery.connectWithRecoveryPromise(openModel, {
181
+ initialDelay: 1,
182
+ maxDelay: 1,
183
+ jitter: 0,
184
+ });
185
+
186
+ const expectedErr = new Error('user close handler explodes');
187
+
188
+ await new Promise((resolve) => {
189
+ client.on('handler-error', (err, event) => {
190
+ assert.strictEqual(err, expectedErr);
191
+ assert.strictEqual(event, 'close');
192
+ resolve();
193
+ });
194
+
195
+ // Simulate a handler throwing in a 'close' listener on the underlying model
196
+ models[0].emit('handler-error', expectedErr, 'close');
197
+ });
198
+
199
+ await client.close();
200
+ });
201
+
202
+ it('promise recovery fails after max retries', async () => {
203
+ let attempts = 0;
204
+
205
+ function openModel() {
206
+ attempts++;
207
+ return Promise.reject(new Error('connect failed'));
208
+ }
209
+
210
+ let failed = false;
211
+ try {
212
+ await recovery.connectWithRecoveryPromise(openModel, {
213
+ initialDelay: 1,
214
+ maxDelay: 1,
215
+ jitter: 0,
216
+ maxRetries: 1,
217
+ });
218
+ } catch (err) {
219
+ failed = true;
220
+ assert.equal('connect failed', err.message);
221
+ }
222
+
223
+ assert(failed);
224
+ assert.equal(2, attempts);
225
+ });
226
+ });