amqplib 1.0.6 → 1.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,466 @@
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
+
224
+ model.on('close', onClose);
225
+ model.on('blocked', onBlocked);
226
+ model.on('unblocked', onUnblocked);
227
+ model.on('error', onConnectionError);
228
+ model.on('update-secret-ok', onUpdateSecretOk);
229
+
230
+ this._modelListeners = {
231
+ model,
232
+ onClose,
233
+ onBlocked,
234
+ onUnblocked,
235
+ onConnectionError,
236
+ onUpdateSecretOk,
237
+ };
238
+ }
239
+
240
+ _unbindModel() {
241
+ if (!this._modelListeners) return;
242
+ const { model, onClose, onBlocked, onUnblocked, onConnectionError, onUpdateSecretOk } = this._modelListeners;
243
+ model.removeListener('close', onClose);
244
+ model.removeListener('blocked', onBlocked);
245
+ model.removeListener('unblocked', onUnblocked);
246
+ model.removeListener('error', onConnectionError);
247
+ model.removeListener('update-secret-ok', onUpdateSecretOk);
248
+ model.on('error', () => {});
249
+ this._modelListeners = null;
250
+ }
251
+
252
+ async _connect() {
253
+ if (this._stopped || this._connecting) return;
254
+ this._connecting = true;
255
+
256
+ let model;
257
+ try {
258
+ model = await this._openModel();
259
+ await runSetup(this._recovery.setup, model);
260
+ if (this._stopped) {
261
+ await this._closeModelNoThrow(model);
262
+ return;
263
+ }
264
+
265
+ this._model = model;
266
+ this._attempt = 0;
267
+ this._bindModel(model);
268
+ this._resolveInitialConnection();
269
+ this._resolvePendingWaiters(model);
270
+ this.emit('connect', model);
271
+ } catch (err) {
272
+ const error = toError(err, 'Connection recovery failed');
273
+ if (model) await this._closeModelNoThrow(model);
274
+ this.emit('connect-failed', error);
275
+ this._scheduleReconnect(error);
276
+ } finally {
277
+ this._connecting = false;
278
+ }
279
+ }
280
+
281
+ _scheduleReconnect(err) {
282
+ if (this._stopped) return;
283
+
284
+ if (this._timer) return;
285
+
286
+ if (this._attempt >= this._recovery.maxRetries) {
287
+ this._rejectInitialConnection(err);
288
+ this._rejectPendingWaiters(err);
289
+ this.emit('reconnect-failed', err);
290
+ return;
291
+ }
292
+
293
+ const attempt = this._attempt + 1;
294
+ const delay = calculateDelay(this._recovery, attempt);
295
+ this._attempt = attempt;
296
+
297
+ this.emit('reconnect-scheduled', { attempt, delay, error: err });
298
+ this._timer = setTimeout(() => {
299
+ this._timer = null;
300
+ this._connect();
301
+ }, delay);
302
+ }
303
+
304
+ _closeModelNoThrow(model) {
305
+ return this._adapters.closeModel(model).catch(() => {});
306
+ }
307
+ }
308
+
309
+ const PROMISE_ADAPTERS = {
310
+ closeModel(model) {
311
+ return model.close();
312
+ },
313
+ createChannel(model, options, confirm) {
314
+ return confirm ? model.createConfirmChannel(options) : model.createChannel(options);
315
+ },
316
+ updateSecret(model, newSecret, reason) {
317
+ return model.updateSecret(newSecret, reason);
318
+ },
319
+ };
320
+
321
+ function callbackClose(model) {
322
+ return new Promise((resolve, reject) => {
323
+ model.close((err) => {
324
+ if (err) reject(err);
325
+ else resolve();
326
+ });
327
+ });
328
+ }
329
+
330
+ function callbackCreateChannel(model, options, confirm) {
331
+ return new Promise((resolve, reject) => {
332
+ const method = confirm ? 'createConfirmChannel' : 'createChannel';
333
+ const cb = (err, channel) => {
334
+ if (err) reject(err);
335
+ else resolve(channel);
336
+ };
337
+
338
+ if (options === undefined) model[method](cb);
339
+ else model[method](options, cb);
340
+ });
341
+ }
342
+
343
+ function callbackUpdateSecret(model, newSecret, reason) {
344
+ return new Promise((resolve, reject) => {
345
+ model.updateSecret(newSecret, reason, (err, ok) => {
346
+ if (err) reject(err);
347
+ else resolve(ok);
348
+ });
349
+ });
350
+ }
351
+
352
+ const CALLBACK_ADAPTERS = {
353
+ closeModel: callbackClose,
354
+ createChannel: callbackCreateChannel,
355
+ updateSecret: callbackUpdateSecret,
356
+ };
357
+
358
+ function wireCommonEvents(source, target) {
359
+ [
360
+ 'connect',
361
+ 'disconnect',
362
+ 'connect-failed',
363
+ 'reconnect-scheduled',
364
+ 'reconnect-failed',
365
+ 'blocked',
366
+ 'unblocked',
367
+ 'error',
368
+ 'update-secret-ok',
369
+ ].forEach((eventName) => {
370
+ source.on(eventName, (...args) => target.emit(eventName, ...args));
371
+ });
372
+ }
373
+
374
+ class RecoveringPromiseModel extends EventEmitter {
375
+ constructor(openModel, recovery) {
376
+ super();
377
+ this._core = new RecoveringCore(openModel, recovery, PROMISE_ADAPTERS);
378
+ wireCommonEvents(this._core, this);
379
+ }
380
+
381
+ async waitForConnect() {
382
+ await this._core.waitForConnect();
383
+ return this;
384
+ }
385
+
386
+ close() {
387
+ return this._core.close();
388
+ }
389
+
390
+ createChannel(options) {
391
+ return this._core.createChannel(options);
392
+ }
393
+
394
+ createConfirmChannel(options) {
395
+ return this._core.createConfirmChannel(options);
396
+ }
397
+
398
+ updateSecret(newSecret, reason) {
399
+ return this._core.updateSecret(newSecret, reason);
400
+ }
401
+ }
402
+
403
+ class RecoveringCallbackModel extends EventEmitter {
404
+ constructor(openModel, recovery) {
405
+ super();
406
+ this._core = new RecoveringCore(openModel, recovery, CALLBACK_ADAPTERS);
407
+ wireCommonEvents(this._core, this);
408
+ }
409
+
410
+ waitForConnect(cb) {
411
+ const callback = typeof cb === 'function' ? cb : () => {};
412
+ this._core.waitForConnect().then(
413
+ () => callback(null, this),
414
+ (err) => callback(toError(err, 'Operation failed')),
415
+ );
416
+ return this;
417
+ }
418
+
419
+ close(cb) {
420
+ makeCallback(this._core.close(), cb);
421
+ return this;
422
+ }
423
+
424
+ updateSecret(newSecret, reason, cb) {
425
+ makeCallback(this._core.updateSecret(newSecret, reason), cb);
426
+ return this;
427
+ }
428
+
429
+ createChannel(options, cb) {
430
+ if (typeof options === 'function') {
431
+ cb = options;
432
+ options = undefined;
433
+ }
434
+ makeCallback(this._core.createChannel(options), cb);
435
+ return this;
436
+ }
437
+
438
+ createConfirmChannel(options, cb) {
439
+ if (typeof options === 'function') {
440
+ cb = options;
441
+ options = undefined;
442
+ }
443
+ makeCallback(this._core.createConfirmChannel(options), cb);
444
+ return this;
445
+ }
446
+ }
447
+
448
+ function connectWithRecoveryPromise(openModel, recovery) {
449
+ const recovering = new RecoveringPromiseModel(openModel, recovery);
450
+ return recovering.waitForConnect();
451
+ }
452
+
453
+ function connectWithRecoveryCallback(openModel, recovery, cb) {
454
+ const recovering = new RecoveringCallbackModel(openModel, recovery);
455
+ recovering.waitForConnect(cb);
456
+ return recovering;
457
+ }
458
+
459
+ module.exports = {
460
+ splitConnectionOptions,
461
+ recoveryEnabled,
462
+ connectWithRecoveryPromise,
463
+ connectWithRecoveryCallback,
464
+ RecoveringPromiseModel,
465
+ RecoveringCallbackModel,
466
+ };
@@ -0,0 +1,13 @@
1
+ function safeEmit(emitter, event, ...args) {
2
+ try {
3
+ emitter.emit(event, ...args);
4
+ } catch (e) {
5
+ if (emitter.listenerCount('handler-error') > 0) {
6
+ setImmediate(() => emitter.emit('handler-error', e, event));
7
+ } else {
8
+ throw e;
9
+ }
10
+ }
11
+ }
12
+
13
+ module.exports = safeEmit;
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.6",
5
+ "version": "1.1.0",
6
6
  "description": "An AMQP 0-9-1 (e.g., RabbitMQ) library and client.",
7
7
  "repository": {
8
8
  "type": "git",
@@ -42,6 +42,17 @@ describe('Callback API', () => {
42
42
  });
43
43
  });
44
44
  });
45
+
46
+ it('emits update-secret-ok event', (_t, done) => {
47
+ connect((err, _c) => {
48
+ assert.ifError(err);
49
+ c = _c;
50
+ c.on('update-secret-ok', () => done());
51
+ c.updateSecret(Buffer.from('new secret'), 'no reason', (err) => {
52
+ assert.ifError(err);
53
+ });
54
+ });
55
+ });
45
56
  });
46
57
 
47
58
  const channel_test_fn = (method) => {