amqplib 2.1.0 → 2.2.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 +14 -0
- package/README.md +54 -0
- package/callback_api.d.ts +19 -3
- package/index.d.ts +18 -2
- package/lib/callback_model.js +3 -22
- package/lib/channel.js +27 -1
- package/lib/channel_model.js +4 -29
- package/lib/properties.d.ts +3 -0
- package/lib/recovery.js +71 -10
- package/package.json +12 -2
- package/.claude/commands/lint-disable.md +0 -9
- package/.claude/commands/lint-enable.md +0 -12
- package/.claude/settings.local.json +0 -7
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -41
- package/.github/workflows/publish.yml +0 -59
- package/.github/workflows/test.yml +0 -54
- package/CLAUDE.md +0 -12
- package/Makefile +0 -42
- package/bin/amqp-rabbitmq-0.9.1.json +0 -483
- package/bin/generate-defs.js +0 -734
- package/biome.json +0 -51
- package/lefthook.yml +0 -17
- package/test/bitset.test.js +0 -65
- package/test/callback_api.test.js +0 -439
- package/test/channel.test.js +0 -918
- package/test/channel_api.test.js +0 -599
- package/test/codec.test.js +0 -171
- package/test/connect.test.js +0 -155
- package/test/connection.test.js +0 -772
- package/test/data.test.js +0 -33
- package/test/frame.test.js +0 -180
- package/test/lib/data.js +0 -249
- package/test/lib/util.js +0 -223
- package/test/mux.test.js +0 -258
- package/test/recovery.test.js +0 -402
- package/test/uri_spec.test.js +0 -210
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Change log for amqplib
|
|
2
2
|
|
|
3
|
+
## v2.2.1
|
|
4
|
+
- Return the server acknowledgement from promise-based `channel.cancel`, matching the callback API (thanks [@edsonsims](https://github.com/edsonsims)).
|
|
5
|
+
- Type `cancel` replies as `Replies.Cancel` (`{ consumerTag }`) in both the promise and callback API declarations, which previously claimed `Replies.Empty`.
|
|
6
|
+
- Fix callback-based `waitForConfirms` never calling back when invoked after the confirm channel closed with unconfirmed messages; it now yields a `channel closed` error, matching the promise API (thanks [@youdie006](https://github.com/youdie006)).
|
|
7
|
+
- `waitForConfirms` (both APIs) now always fails with `channel closed` after the channel closes with unconfirmed messages. Previously it reported success if every outstanding message had its own callback, or on any call after the first.
|
|
8
|
+
- The callback API's `waitForConfirms` callback is now required in the TypeScript declarations, matching the docs. Calling it without one threw at runtime.
|
|
9
|
+
- Replace `.npmignore` with a `files` allowlist in `package.json` so the published package no longer includes tests, tooling config, dotfiles or the defs generator. Install size drops from 577 kB to 358 kB (fixes #859, thanks [@rubiesonthesky](https://github.com/rubiesonthesky))
|
|
10
|
+
|
|
11
|
+
## v2.2.0
|
|
12
|
+
- Add `calculateDelay` recovery option for supplying a custom reconnect delay strategy, e.g. full jitter, decorrelated jitter or a fixed schedule. When set, `maxDelay` is not applied to its return value; when it throws or returns an invalid value, recovery gives up as if `maxRetries` were exhausted (fixes #855, thanks [@GiHoon1123](https://github.com/GiHoon1123))
|
|
13
|
+
- The built-in reconnect delay no longer exceeds `maxDelay`. Previously the cap was applied before jitter, so delays could overshoot it by up to the jitter fraction. The exponential base is now capped at `maxDelay / (1 + jitter)` so the full jitter range fits under the cap; with default settings the steady-state delay is now spread over 20-30s rather than 24-36s
|
|
14
|
+
- Channel operations attempted after recovery has given up (`reconnect-failed`) now reject with the failure instead of waiting forever
|
|
15
|
+
- Add `initialMaxRetries` recovery option to bound retries before the first successful connection separately from `maxRetries`, so startup can fail fast while steady-state recovery retries indefinitely (fixes #856)
|
|
16
|
+
|
|
3
17
|
## v2.1.0
|
|
4
18
|
- 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
19
|
- Fix `setup` being run when `close()` is called while the initial connection attempt is in flight
|
package/README.md
CHANGED
|
@@ -210,6 +210,60 @@ callback is invoked immediately with the handle instead of after the first
|
|
|
210
210
|
connection, and `waitForConnect(callback)` can be used to be notified once
|
|
211
211
|
connected.
|
|
212
212
|
|
|
213
|
+
### Custom delay strategy
|
|
214
|
+
|
|
215
|
+
By default, reconnect delays follow an exponential backoff with jitter,
|
|
216
|
+
controlled by `initialDelay`, `maxDelay`, `factor` and `jitter`. To use a
|
|
217
|
+
different strategy entirely (full jitter, decorrelated jitter, a fixed step
|
|
218
|
+
schedule, etc.), provide a `calculateDelay` function instead:
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
const connection = await amqplib.connect('amqp://localhost', {
|
|
222
|
+
recovery: {
|
|
223
|
+
maxRetries: Infinity,
|
|
224
|
+
// Called with the reconnect attempt number, starting at 1.
|
|
225
|
+
// Must return the delay in milliseconds.
|
|
226
|
+
calculateDelay(attempt) {
|
|
227
|
+
return Math.min(30000, 100 * 2 ** (attempt - 1));
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`maxDelay` only bounds the built-in strategy. Once you provide `calculateDelay`,
|
|
234
|
+
amqplib does not cap its return value - you're responsible for enforcing your
|
|
235
|
+
own maximum, as the example above does with `Math.min`.
|
|
236
|
+
|
|
237
|
+
When `calculateDelay` is absent, the built-in strategy is used. If it throws,
|
|
238
|
+
or returns something other than a finite, non-negative number, amqplib does
|
|
239
|
+
not fall back to the built-in strategy. Instead recovery gives up as if
|
|
240
|
+
`maxRetries` had been exhausted: the initial `connect` rejects (or the
|
|
241
|
+
callback receives the error), pending channel operations reject, and
|
|
242
|
+
`reconnect-failed` is emitted with the error. A broken `calculateDelay` is a
|
|
243
|
+
bug in caller-supplied code, so it is surfaced rather than papered over.
|
|
244
|
+
|
|
245
|
+
### Separate retry budget for the initial connection
|
|
246
|
+
|
|
247
|
+
By default `maxRetries` bounds every phase of recovery, including the attempts
|
|
248
|
+
made before the first connection succeeds. To give the very first connection
|
|
249
|
+
its own budget, set `initialMaxRetries`; once connected, `maxRetries` applies.
|
|
250
|
+
A common production posture is to fail fast at startup, so a misconfigured or
|
|
251
|
+
unreachable broker fails the deployment, while never giving up on a service
|
|
252
|
+
that has already connected:
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
const connection = await amqplib.connect('amqp://localhost', {
|
|
256
|
+
recovery: {
|
|
257
|
+
initialMaxRetries: 5, // give up (and reject connect) after 5 failed retries at startup
|
|
258
|
+
maxRetries: Infinity, // but keep reconnecting forever once connected
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When the initial budget is exhausted `connect` rejects (or the callback receives
|
|
264
|
+
the error) and `reconnect-failed` is emitted. `initialMaxRetries` defaults to
|
|
265
|
+
`maxRetries`, so behaviour is unchanged unless it is set.
|
|
266
|
+
|
|
213
267
|
## Error handling in event handlers
|
|
214
268
|
|
|
215
269
|
If a user-supplied event handler throws a synchronous error, the throw will
|
package/callback_api.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export interface Channel extends events.EventEmitter {
|
|
|
103
103
|
callback?: (err: Error, ok: Replies.Consume) => void,
|
|
104
104
|
): void;
|
|
105
105
|
|
|
106
|
-
cancel(consumerTag: string, callback?: (err: Error, ok: Replies.
|
|
106
|
+
cancel(consumerTag: string, callback?: (err: Error, ok: Replies.Cancel) => void): void;
|
|
107
107
|
get(queue: string, options?: Options.Get, callback?: (err: Error, ok: GetMessage | false) => void): void;
|
|
108
108
|
|
|
109
109
|
ack(message: Message, allUpTo?: boolean): void;
|
|
@@ -132,7 +132,7 @@ export interface ConfirmChannel extends Channel {
|
|
|
132
132
|
callback?: (err: Error, ok: Replies.Empty) => void,
|
|
133
133
|
): boolean;
|
|
134
134
|
|
|
135
|
-
waitForConfirms(callback
|
|
135
|
+
waitForConfirms(callback: (err?: Error) => void): void;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
export declare class IllegalOperationError extends Error {
|
|
@@ -169,8 +169,15 @@ export interface RecoveryOptions {
|
|
|
169
169
|
factor?: number;
|
|
170
170
|
/** Jitter factor (0–1) applied to delay to avoid thundering herd. Default: 0.2 */
|
|
171
171
|
jitter?: number;
|
|
172
|
-
/** Maximum number of reconnect attempts. Default: Infinity */
|
|
172
|
+
/** Maximum number of reconnect attempts. Also bounds the initial connection unless `initialMaxRetries` is set. Default: Infinity */
|
|
173
173
|
maxRetries?: number;
|
|
174
|
+
/**
|
|
175
|
+
* Maximum number of retries before the first successful connection. Once
|
|
176
|
+
* connected, `maxRetries` applies instead. Lets startup fail fast against an
|
|
177
|
+
* unreachable broker while steady-state recovery keeps retrying.
|
|
178
|
+
* Default: the value of `maxRetries`
|
|
179
|
+
*/
|
|
180
|
+
initialMaxRetries?: number;
|
|
174
181
|
/** Optional setup function called after each successful connection */
|
|
175
182
|
setup?: ((model: Connection) => Promise<void>) | ((model: Connection, done: (err?: Error) => void) => void);
|
|
176
183
|
/**
|
|
@@ -179,6 +186,15 @@ export interface RecoveryOptions {
|
|
|
179
186
|
* Use `waitForConnect()` to be notified of the first connection. Default: true
|
|
180
187
|
*/
|
|
181
188
|
waitForConnect?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Optional custom delay strategy. Called with the reconnect attempt number
|
|
191
|
+
* (starting at 1) and must return the delay in milliseconds. Falls back to
|
|
192
|
+
* the built-in exponential-backoff-with-jitter strategy when absent. If it
|
|
193
|
+
* throws, or returns something other than a finite, non-negative number,
|
|
194
|
+
* recovery gives up as if `maxRetries` were exhausted: the initial connection
|
|
195
|
+
* fails with the error and `reconnect-failed` is emitted.
|
|
196
|
+
*/
|
|
197
|
+
calculateDelay?: (attempt: number) => number;
|
|
182
198
|
}
|
|
183
199
|
|
|
184
200
|
export interface RecoveringConnection extends events.EventEmitter {
|
package/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface Channel extends events.EventEmitter {
|
|
|
77
77
|
options?: Options.Consume,
|
|
78
78
|
): Promise<Replies.Consume>;
|
|
79
79
|
|
|
80
|
-
cancel(consumerTag: string): Promise<Replies.
|
|
80
|
+
cancel(consumerTag: string): Promise<Replies.Cancel>;
|
|
81
81
|
get(queue: string, options?: Options.Get): Promise<GetMessage | false>;
|
|
82
82
|
|
|
83
83
|
ack(message: Message, allUpTo?: boolean): void;
|
|
@@ -143,8 +143,15 @@ export interface RecoveryOptions {
|
|
|
143
143
|
factor?: number;
|
|
144
144
|
/** Jitter factor (0–1) applied to delay to avoid thundering herd. Default: 0.2 */
|
|
145
145
|
jitter?: number;
|
|
146
|
-
/** Maximum number of reconnect attempts. Default: Infinity */
|
|
146
|
+
/** Maximum number of reconnect attempts. Also bounds the initial connection unless `initialMaxRetries` is set. Default: Infinity */
|
|
147
147
|
maxRetries?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Maximum number of retries before the first successful connection. Once
|
|
150
|
+
* connected, `maxRetries` applies instead. Lets startup fail fast against an
|
|
151
|
+
* unreachable broker while steady-state recovery keeps retrying.
|
|
152
|
+
* Default: the value of `maxRetries`
|
|
153
|
+
*/
|
|
154
|
+
initialMaxRetries?: number;
|
|
148
155
|
/** Optional setup function called after each successful connection */
|
|
149
156
|
setup?: ((model: ChannelModel) => Promise<void>) | ((model: ChannelModel, done: (err?: Error) => void) => void);
|
|
150
157
|
/**
|
|
@@ -154,6 +161,15 @@ export interface RecoveryOptions {
|
|
|
154
161
|
* first connection. Default: true
|
|
155
162
|
*/
|
|
156
163
|
waitForConnect?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Optional custom delay strategy. Called with the reconnect attempt number
|
|
166
|
+
* (starting at 1) and must return the delay in milliseconds. Falls back to
|
|
167
|
+
* the built-in exponential-backoff-with-jitter strategy when absent. If it
|
|
168
|
+
* throws, or returns something other than a finite, non-negative number,
|
|
169
|
+
* recovery gives up as if `maxRetries` were exhausted: the initial connection
|
|
170
|
+
* fails with the error and `reconnect-failed` is emitted.
|
|
171
|
+
*/
|
|
172
|
+
calculateDelay?: (attempt: number) => number;
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
export interface RecoveringChannelModel extends events.EventEmitter {
|
package/lib/callback_model.js
CHANGED
|
@@ -255,28 +255,9 @@ class ConfirmChannel extends Channel {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
waitForConfirms(k) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (val === null); // already confirmed
|
|
262
|
-
else {
|
|
263
|
-
const confirmed = new Promise((resolve, reject) => {
|
|
264
|
-
unconfirmed[index] = (err) => {
|
|
265
|
-
if (val) val(err);
|
|
266
|
-
if (err === null) resolve();
|
|
267
|
-
else reject(err);
|
|
268
|
-
};
|
|
269
|
-
});
|
|
270
|
-
awaiting.push(confirmed);
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
return Promise.all(awaiting).then(
|
|
274
|
-
() => {
|
|
275
|
-
k();
|
|
276
|
-
},
|
|
277
|
-
(err) => {
|
|
278
|
-
k(err);
|
|
279
|
-
},
|
|
258
|
+
this._waitForConfirms().then(
|
|
259
|
+
() => k(),
|
|
260
|
+
(err) => k(err),
|
|
280
261
|
);
|
|
281
262
|
}
|
|
282
263
|
}
|
package/lib/channel.js
CHANGED
|
@@ -21,6 +21,7 @@ class Channel extends EventEmitter {
|
|
|
21
21
|
// for unconfirmed messages
|
|
22
22
|
this.lwm = 1; // the least, unconfirmed deliveryTag
|
|
23
23
|
this.unconfirmed = []; // rolling window of delivery callbacks
|
|
24
|
+
this.closedWithUnconfirmed = false;
|
|
24
25
|
this.on(
|
|
25
26
|
'ack',
|
|
26
27
|
this.handleConfirm.bind(this, (cb) => {
|
|
@@ -39,7 +40,10 @@ class Channel extends EventEmitter {
|
|
|
39
40
|
const cb = this.unconfirmed[i];
|
|
40
41
|
if (cb) cb(err);
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
+
// Remember that confirms were lost, so every later waitForConfirms
|
|
44
|
+
// fails, whether or not the messages had their own callbacks
|
|
45
|
+
this.closedWithUnconfirmed = this.unconfirmed.some((cb) => cb !== null);
|
|
46
|
+
this.unconfirmed = [];
|
|
43
47
|
});
|
|
44
48
|
// message frame state machine
|
|
45
49
|
this.handleMessage = acceptDeliveryOrReturn;
|
|
@@ -243,6 +247,28 @@ class Channel extends EventEmitter {
|
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
|
|
250
|
+
// Resolves once every outstanding message is confirmed, and rejects on
|
|
251
|
+
// the first nack, or if the channel closes before all are confirmed.
|
|
252
|
+
// Shared by the promise and callback ConfirmChannel#waitForConfirms.
|
|
253
|
+
_waitForConfirms() {
|
|
254
|
+
if (this.closedWithUnconfirmed) return Promise.reject(new Error('channel closed'));
|
|
255
|
+
const awaiting = [];
|
|
256
|
+
const unconfirmed = this.unconfirmed;
|
|
257
|
+
unconfirmed.forEach((cb, index) => {
|
|
258
|
+
if (cb !== null) {
|
|
259
|
+
const confirmed = new Promise((resolve, reject) => {
|
|
260
|
+
unconfirmed[index] = (err) => {
|
|
261
|
+
if (cb) cb(err);
|
|
262
|
+
if (err === null) resolve();
|
|
263
|
+
else reject(err);
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
awaiting.push(confirmed);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return Promise.all(awaiting);
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
pushConfirmCallback(cb) {
|
|
247
273
|
// `null` is used specifically for marking already confirmed slots,
|
|
248
274
|
// so I coerce `undefined` and `null` to false; functions are never
|
package/lib/channel_model.js
CHANGED
|
@@ -146,12 +146,9 @@ class Channel extends BaseChannel {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
async cancel(consumerTag) {
|
|
149
|
-
await
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.unregisterConsumer(consumerTag);
|
|
153
|
-
return ok.fields;
|
|
154
|
-
});
|
|
149
|
+
const fields = await this.rpc(defs.BasicCancel, Args.cancel(consumerTag), defs.BasicCancelOk);
|
|
150
|
+
this.unregisterConsumer(consumerTag);
|
|
151
|
+
return fields;
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
get(queue, options) {
|
|
@@ -234,29 +231,7 @@ class ConfirmChannel extends Channel {
|
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
waitForConfirms() {
|
|
237
|
-
|
|
238
|
-
const unconfirmed = this.unconfirmed;
|
|
239
|
-
unconfirmed.forEach((cb, index) => {
|
|
240
|
-
if (cb !== null) {
|
|
241
|
-
const confirmed = new Promise((resolve, reject) => {
|
|
242
|
-
unconfirmed[index] = (err) => {
|
|
243
|
-
if (cb) cb(err);
|
|
244
|
-
if (err === null) resolve();
|
|
245
|
-
else reject(err);
|
|
246
|
-
};
|
|
247
|
-
});
|
|
248
|
-
awaiting.push(confirmed);
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
// Channel closed
|
|
252
|
-
if (!this.pending) {
|
|
253
|
-
const err = new Error('channel closed');
|
|
254
|
-
while (unconfirmed.length > 0) {
|
|
255
|
-
const cb = unconfirmed.shift();
|
|
256
|
-
if (cb) cb(err);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return Promise.all(awaiting);
|
|
234
|
+
return this._waitForConfirms();
|
|
260
235
|
}
|
|
261
236
|
}
|
|
262
237
|
|
package/lib/properties.d.ts
CHANGED
package/lib/recovery.js
CHANGED
|
@@ -8,6 +8,7 @@ const DEFAULT_RECOVERY = {
|
|
|
8
8
|
maxRetries: Infinity,
|
|
9
9
|
setup: null,
|
|
10
10
|
waitForConnect: true,
|
|
11
|
+
calculateDelay: null,
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
function splitConnectionOptions(connOptions) {
|
|
@@ -43,22 +44,32 @@ function toFiniteNumber(value, fallback) {
|
|
|
43
44
|
return Number.isFinite(n) ? n : fallback;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function toRetryCount(value, fallback) {
|
|
48
|
+
if (value == null) return fallback;
|
|
49
|
+
if (value === Infinity) return Infinity;
|
|
50
|
+
const n = toFiniteNumber(value, fallback);
|
|
51
|
+
return n < 0 ? 0 : n;
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
function normaliseRecoveryOptions(recovery) {
|
|
47
55
|
const source = recovery === true ? {} : recovery || {};
|
|
48
56
|
const initialDelay = Math.max(0, toFiniteNumber(source.initialDelay, DEFAULT_RECOVERY.initialDelay));
|
|
49
57
|
const maxDelay = Math.max(initialDelay, toFiniteNumber(source.maxDelay, DEFAULT_RECOVERY.maxDelay));
|
|
50
58
|
const factor = Math.max(1, toFiniteNumber(source.factor, DEFAULT_RECOVERY.factor));
|
|
51
59
|
const jitter = Math.min(1, Math.max(0, toFiniteNumber(source.jitter, DEFAULT_RECOVERY.jitter)));
|
|
52
|
-
const
|
|
60
|
+
const maxRetries = toRetryCount(source.maxRetries, Infinity);
|
|
61
|
+
const initialMaxRetries = toRetryCount(source.initialMaxRetries, maxRetries);
|
|
53
62
|
|
|
54
63
|
return {
|
|
55
64
|
initialDelay,
|
|
56
65
|
maxDelay,
|
|
57
66
|
factor,
|
|
58
67
|
jitter,
|
|
59
|
-
maxRetries
|
|
68
|
+
maxRetries,
|
|
69
|
+
initialMaxRetries,
|
|
60
70
|
setup: typeof source.setup === 'function' ? source.setup : null,
|
|
61
71
|
waitForConnect: source.waitForConnect !== false,
|
|
72
|
+
calculateDelay: typeof source.calculateDelay === 'function' ? source.calculateDelay : null,
|
|
62
73
|
};
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -67,13 +78,39 @@ function toError(err, fallbackMessage) {
|
|
|
67
78
|
return new Error(err ? String(err) : fallbackMessage);
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
function
|
|
71
|
-
|
|
81
|
+
function calculateBuiltinDelay(recovery, attempt) {
|
|
82
|
+
// Capping the base at maxDelay and then clamping base+offset to maxDelay
|
|
83
|
+
// would put a point mass on maxDelay itself: once base saturates, every
|
|
84
|
+
// positive jitter draw (half of them) collapses onto the same value,
|
|
85
|
+
// defeating the point of jitter. Capping the base at
|
|
86
|
+
// maxDelay / (1 + jitter) instead means the maximum possible offset
|
|
87
|
+
// lands exactly on maxDelay, so the result never exceeds it without
|
|
88
|
+
// needing a final clamp - the distribution stays smooth right up to the cap.
|
|
89
|
+
const cappedBase = recovery.maxDelay / (1 + recovery.jitter);
|
|
90
|
+
const base = Math.min(cappedBase, recovery.initialDelay * recovery.factor ** (attempt - 1));
|
|
72
91
|
const jitterPart = base * recovery.jitter;
|
|
73
92
|
const offset = jitterPart > 0 ? Math.random() * jitterPart * 2 - jitterPart : 0;
|
|
74
93
|
return Math.max(0, Math.round(base + offset));
|
|
75
94
|
}
|
|
76
95
|
|
|
96
|
+
// A custom `calculateDelay` that throws, or returns something other than a
|
|
97
|
+
// finite, non-negative number, is a bug in caller-supplied code. Rather than
|
|
98
|
+
// silently falling back to the built-in strategy, the error is treated as a
|
|
99
|
+
// terminal recovery failure by the caller (see _scheduleReconnect), so it
|
|
100
|
+
// surfaces immediately instead of manifesting as unexplained delay behavior.
|
|
101
|
+
function calculateDelay(recovery, attempt) {
|
|
102
|
+
if (!recovery.calculateDelay) {
|
|
103
|
+
return calculateBuiltinDelay(recovery, attempt);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const custom = recovery.calculateDelay(attempt);
|
|
107
|
+
if (typeof custom === 'number' && Number.isFinite(custom) && custom >= 0) {
|
|
108
|
+
return Math.round(custom);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error(`calculateDelay must return a finite, non-negative number of milliseconds (got ${String(custom)})`);
|
|
112
|
+
}
|
|
113
|
+
|
|
77
114
|
function runSetup(setup, model) {
|
|
78
115
|
if (!setup) return Promise.resolve();
|
|
79
116
|
|
|
@@ -118,6 +155,8 @@ class RecoveringCore extends EventEmitter {
|
|
|
118
155
|
this._waiters = [];
|
|
119
156
|
this._connecting = false;
|
|
120
157
|
this._stopped = false;
|
|
158
|
+
this._failure = null;
|
|
159
|
+
this._connectedOnce = false;
|
|
121
160
|
this._timer = null;
|
|
122
161
|
this._attempt = 0;
|
|
123
162
|
|
|
@@ -205,7 +244,7 @@ class RecoveringCore extends EventEmitter {
|
|
|
205
244
|
|
|
206
245
|
_waitForConnection() {
|
|
207
246
|
if (this._model) return Promise.resolve(this._model);
|
|
208
|
-
if (this._stopped) return Promise.reject(new Error('Connection closed'));
|
|
247
|
+
if (this._stopped) return Promise.reject(this._failure || new Error('Connection closed'));
|
|
209
248
|
|
|
210
249
|
return new Promise((resolve, reject) => {
|
|
211
250
|
this._waiters.push({ resolve, reject });
|
|
@@ -277,6 +316,7 @@ class RecoveringCore extends EventEmitter {
|
|
|
277
316
|
|
|
278
317
|
this._model = model;
|
|
279
318
|
this._attempt = 0;
|
|
319
|
+
this._connectedOnce = true;
|
|
280
320
|
this._bindModel(model);
|
|
281
321
|
this._resolveInitialConnection();
|
|
282
322
|
this._resolvePendingWaiters(model);
|
|
@@ -296,15 +336,24 @@ class RecoveringCore extends EventEmitter {
|
|
|
296
336
|
|
|
297
337
|
if (this._timer) return;
|
|
298
338
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
this.
|
|
302
|
-
this.emit('reconnect-failed', err);
|
|
339
|
+
const maxRetries = this._connectedOnce ? this._recovery.maxRetries : this._recovery.initialMaxRetries;
|
|
340
|
+
if (this._attempt >= maxRetries) {
|
|
341
|
+
this._abandonRecovery(err);
|
|
303
342
|
return;
|
|
304
343
|
}
|
|
305
344
|
|
|
306
345
|
const attempt = this._attempt + 1;
|
|
307
|
-
|
|
346
|
+
let delay;
|
|
347
|
+
try {
|
|
348
|
+
delay = calculateDelay(this._recovery, attempt);
|
|
349
|
+
} catch (delayErr) {
|
|
350
|
+
// Both callers of _scheduleReconnect run from timers or socket events
|
|
351
|
+
// where a throw would either be an unhandled rejection or an uncaught
|
|
352
|
+
// exception. Give up recovery cleanly instead so the failure reaches the
|
|
353
|
+
// initial connect promise, pending channel waiters and 'reconnect-failed'.
|
|
354
|
+
this._abandonRecovery(toError(delayErr, 'calculateDelay failed'));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
308
357
|
this._attempt = attempt;
|
|
309
358
|
|
|
310
359
|
this.emit('reconnect-scheduled', { attempt, delay, error: err });
|
|
@@ -314,6 +363,18 @@ class RecoveringCore extends EventEmitter {
|
|
|
314
363
|
}, delay);
|
|
315
364
|
}
|
|
316
365
|
|
|
366
|
+
_abandonRecovery(err) {
|
|
367
|
+
// Recovery is over and cannot be restarted, so treat the connection as
|
|
368
|
+
// stopped. Remember why, so channel operations attempted afterwards
|
|
369
|
+
// reject with the underlying failure instead of waiting forever for a
|
|
370
|
+
// connection that will never arrive.
|
|
371
|
+
this._stopped = true;
|
|
372
|
+
this._failure = err;
|
|
373
|
+
this._rejectInitialConnection(err);
|
|
374
|
+
this._rejectPendingWaiters(err);
|
|
375
|
+
this.emit('reconnect-failed', err);
|
|
376
|
+
}
|
|
377
|
+
|
|
317
378
|
_closeModelNoThrow(model) {
|
|
318
379
|
return this._adapters.closeModel(model).catch(() => {});
|
|
319
380
|
}
|
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.1
|
|
5
|
+
"version": "2.2.1",
|
|
6
6
|
"types": "./index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
@@ -46,5 +46,15 @@
|
|
|
46
46
|
"RabbitMQ"
|
|
47
47
|
],
|
|
48
48
|
"author": "Michael Bridgen <mikeb@squaremobius.net>",
|
|
49
|
-
"license": "MIT"
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"files": [
|
|
51
|
+
"lib",
|
|
52
|
+
"examples",
|
|
53
|
+
"channel_api.js",
|
|
54
|
+
"callback_api.js",
|
|
55
|
+
"index.d.ts",
|
|
56
|
+
"callback_api.d.ts",
|
|
57
|
+
"CHANGELOG.md",
|
|
58
|
+
"LICENSE-MIT"
|
|
59
|
+
]
|
|
50
60
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
Please follow the following process when I ask you to disable a lint rule using /lint-disable $rule
|
|
2
|
-
|
|
3
|
-
1. Create and change to a new branch. Ask for the branch name.
|
|
4
|
-
2. If the rule is off by default, delete it (See https://biomejs.dev/linter/javascript/rules/ for rule details)
|
|
5
|
-
3. If the rule is on by default, configure it to off in biome.json
|
|
6
|
-
4. Update the project changelog
|
|
7
|
-
5. Add the changes
|
|
8
|
-
6. Commit the changes
|
|
9
|
-
7. Push the changes
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
Please follow the following process when I ask you to enable a lint rule using /lint-enable $rule
|
|
2
|
-
|
|
3
|
-
1. Create and change to a new branch. Ask for the branch name.
|
|
4
|
-
2. Configure the rule to error in biome.json
|
|
5
|
-
3. Run lint (npm run lint). If there are NO errors, goto step 8
|
|
6
|
-
4. Use biome to SAFELY auto fix (npm run lint -- --fix). If there are errors stop and ask for instruction.
|
|
7
|
-
5. Run the tests (npm test). If there are errors, stop and ask for instruction.
|
|
8
|
-
6. If the rule defaults to error, delete it (See https://biomejs.dev/linter/javascript/rules/ for rule details)
|
|
9
|
-
7. Update the project changelog
|
|
10
|
-
8. Add the changes
|
|
11
|
-
9. Commit the changes
|
|
12
|
-
10. Push the changes
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Bug report / Support request
|
|
3
|
-
about: Report a bug or ask for help
|
|
4
|
-
title: ''
|
|
5
|
-
labels: ''
|
|
6
|
-
assignees: ''
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
Before reporting a bug or requesting support please...
|
|
11
|
-
|
|
12
|
-
1. Read the troubleshooting guide
|
|
13
|
-
|
|
14
|
-
- https://amqp-node.github.io/amqplib/#troubleshooting
|
|
15
|
-
|
|
16
|
-
2. Search for existing open and closed issues
|
|
17
|
-
|
|
18
|
-
- https://github.com/amqp-node/amqplib/issues?q=is%3Aissue+TYPE+YOUR+KEYWORDS+HERE
|
|
19
|
-
|
|
20
|
-
3. Ensure you are familiar with the amqplib Channel API and RabbitMQ basics
|
|
21
|
-
|
|
22
|
-
- https://amqp-node.github.io/amqplib/channel_api.html
|
|
23
|
-
- https://www.cloudamqp.com/blog/part1-rabbitmq-for-beginners-what-is-rabbitmq.html
|
|
24
|
-
- https://www.rabbitmq.com/tutorials/amqp-concepts.html
|
|
25
|
-
|
|
26
|
-
If the above does not help, please provide as much of the following information as is relevant...
|
|
27
|
-
|
|
28
|
-
- A clear and concise description of the problem
|
|
29
|
-
- RabbitMQ version
|
|
30
|
-
- amqplib version
|
|
31
|
-
- Node.js version
|
|
32
|
-
- The simplest possible code snippet that demonstrates the problem
|
|
33
|
-
- A stack trace
|
|
34
|
-
|
|
35
|
-
Please format code snippets and/or stack traces using GitHub Markdown
|
|
36
|
-
|
|
37
|
-
- https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks).
|
|
38
|
-
|
|
39
|
-
Thank you
|
|
40
|
-
|
|
41
|
-
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
name: Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [created]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
build:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
services:
|
|
11
|
-
rabbitmq:
|
|
12
|
-
image: rabbitmq:4.2-alpine
|
|
13
|
-
ports:
|
|
14
|
-
- 5672:5672
|
|
15
|
-
|
|
16
|
-
strategy:
|
|
17
|
-
matrix:
|
|
18
|
-
node-version: [18.x, 20.x, 22.x, 24.x]
|
|
19
|
-
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
20
|
-
|
|
21
|
-
steps:
|
|
22
|
-
- uses: actions/checkout@v3
|
|
23
|
-
|
|
24
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
25
|
-
uses: actions/setup-node@v3
|
|
26
|
-
with:
|
|
27
|
-
node-version: ${{ matrix.node-version }}
|
|
28
|
-
cache: "npm"
|
|
29
|
-
|
|
30
|
-
- run: npm ci
|
|
31
|
-
|
|
32
|
-
- run: |
|
|
33
|
-
n=0
|
|
34
|
-
while :
|
|
35
|
-
do
|
|
36
|
-
sleep 5
|
|
37
|
-
echo 'HELO\n\n\n\n' | nc localhost 5672 | grep AMQP
|
|
38
|
-
[[ $? = 0 ]] && break || ((n++))
|
|
39
|
-
(( n >= 5 )) && break
|
|
40
|
-
done
|
|
41
|
-
|
|
42
|
-
- run: echo 'HELO\n\n\n\n' | nc localhost 5672 | grep AMQP
|
|
43
|
-
|
|
44
|
-
- run: make test
|
|
45
|
-
|
|
46
|
-
publish:
|
|
47
|
-
needs: build
|
|
48
|
-
runs-on: ubuntu-latest
|
|
49
|
-
steps:
|
|
50
|
-
- uses: actions/checkout@v3
|
|
51
|
-
- uses: actions/setup-node@v4
|
|
52
|
-
with:
|
|
53
|
-
node-version: '20.x'
|
|
54
|
-
cache: "npm"
|
|
55
|
-
registry-url: https://registry.npmjs.org/
|
|
56
|
-
- run: npm ci
|
|
57
|
-
- run: npm publish --dry-run
|
|
58
|
-
env:
|
|
59
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
|