amqp-connection-manager 3.5.0 → 3.7.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/README.md +7 -5
- package/dist/cjs/AmqpConnectionManager.js +37 -3
- package/dist/cjs/ChannelWrapper.js +228 -97
- package/dist/cjs/index.js +7 -2
- package/dist/esm/AmqpConnectionManager.d.ts +16 -2
- package/dist/esm/AmqpConnectionManager.js +35 -1
- package/dist/esm/AmqpConnectionManager.js.map +1 -1
- package/dist/esm/ChannelWrapper.d.ts +53 -12
- package/dist/esm/ChannelWrapper.js +228 -97
- package/dist/esm/ChannelWrapper.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +6 -1
- package/dist/esm/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://npmjs.org/package/amqp-connection-manager)
|
|
4
4
|

|
|
5
|
-
[](https://coveralls.io/github/jwalton/node-amqp-connection-manager?branch=master)
|
|
6
5
|
[](https://github.com/semantic-release/semantic-release)
|
|
7
6
|
|
|
8
|
-
[](https://david-dm.org/jwalton/node-amqp-connection-manager)
|
|
9
|
-
[](https://david-dm.org/jwalton/node-amqp-connection-manager#info=devDependencies)
|
|
10
|
-
[](https://david-dm.org/jwalton/node-amqp-connection-manager#info=peerDependencies)
|
|
11
|
-
|
|
12
7
|
Connection management for amqplib. This is a wrapper around [amqplib](http://www.squaremobius.net/amqp.node/) which provides automatic reconnects.
|
|
13
8
|
|
|
14
9
|
## Features
|
|
@@ -139,6 +134,7 @@ Options:
|
|
|
139
134
|
arbitrary data in.
|
|
140
135
|
- `options.json` if true, then ChannelWrapper assumes all messages passed to `publish()` and `sendToQueue()`
|
|
141
136
|
are plain JSON objects. These will be encoded automatically before being sent.
|
|
137
|
+
- `options.publishTimeout` - a default timeout for messages published to this channel.
|
|
142
138
|
|
|
143
139
|
### AmqpConnectionManager#isConnected()
|
|
144
140
|
|
|
@@ -183,6 +179,12 @@ callback) which resolves when the message is confirmed to have been delivered to
|
|
|
183
179
|
either the broker refuses the message, or if `close()` is called on the ChannelWrapper before the message can be
|
|
184
180
|
delivered.
|
|
185
181
|
|
|
182
|
+
Both of these functions take an additional option when passing options:
|
|
183
|
+
|
|
184
|
+
- `timeout` - If specified, if a messages is not acked by the amqp broker within the specified number of milliseconds,
|
|
185
|
+
the message will be rejected. Note that the message _may_ still end up getting delivered after the timeout, as we
|
|
186
|
+
have no way to cancel the in-flight request.
|
|
187
|
+
|
|
186
188
|
### ChannelWrapper#ack and ChannelWrapper#nack
|
|
187
189
|
|
|
188
190
|
These are just aliases for calling `ack()` and `nack()` on the underlying channel. They do nothing if the underlying
|
|
@@ -72,7 +72,41 @@ class AmqpConnectionManager extends events_1.EventEmitter {
|
|
|
72
72
|
// There will be one listener per channel, and there could be a lot of channels, so disable warnings from node.
|
|
73
73
|
this.setMaxListeners(0);
|
|
74
74
|
this._findServers = options.findServers || (() => Promise.resolve(urls));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
|
|
78
|
+
* reconnect attempts will continue in the background.
|
|
79
|
+
* @param [options={}] -
|
|
80
|
+
* @param [options.timeout] - Time to wait for initial connect
|
|
81
|
+
*/
|
|
82
|
+
async connect({ timeout } = {}) {
|
|
75
83
|
this._connect();
|
|
84
|
+
let reject;
|
|
85
|
+
const onDisconnect = ({ err }) => {
|
|
86
|
+
// Ignore disconnects caused by dead servers etc., but throw on operational errors like bad credentials.
|
|
87
|
+
if (err.isOperational) {
|
|
88
|
+
reject(err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
await Promise.race([
|
|
93
|
+
(0, events_1.once)(this, 'connect'),
|
|
94
|
+
new Promise((_resolve, innerReject) => {
|
|
95
|
+
reject = innerReject;
|
|
96
|
+
this.on('disconnect', onDisconnect);
|
|
97
|
+
}),
|
|
98
|
+
...(timeout
|
|
99
|
+
? [
|
|
100
|
+
(0, helpers_js_1.wait)(timeout).promise.then(() => {
|
|
101
|
+
throw new Error('amqp-connection-manager: connect timeout');
|
|
102
|
+
}),
|
|
103
|
+
]
|
|
104
|
+
: []),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
this.removeListener('disconnect', onDisconnect);
|
|
109
|
+
}
|
|
76
110
|
}
|
|
77
111
|
// `options` here are any options that can be passed to ChannelWrapper.
|
|
78
112
|
createChannel(options = {}) {
|
|
@@ -221,7 +255,7 @@ class AmqpConnectionManager extends events_1.EventEmitter {
|
|
|
221
255
|
this._currentConnection = undefined;
|
|
222
256
|
this._disconnectSent = true;
|
|
223
257
|
this.emit('disconnect', { err });
|
|
224
|
-
const handle = helpers_js_1.wait(this.reconnectTimeInSeconds * 1000);
|
|
258
|
+
const handle = (0, helpers_js_1.wait)(this.reconnectTimeInSeconds * 1000);
|
|
225
259
|
this._cancelRetriesHandler = handle.cancel;
|
|
226
260
|
handle.promise
|
|
227
261
|
.then(() => this._connect())
|
|
@@ -245,10 +279,10 @@ class AmqpConnectionManager extends events_1.EventEmitter {
|
|
|
245
279
|
this._connectPromise = undefined;
|
|
246
280
|
let handle;
|
|
247
281
|
if (err.name === 'OperationalError' && err.message === 'connect ETIMEDOUT') {
|
|
248
|
-
handle = helpers_js_1.wait(0);
|
|
282
|
+
handle = (0, helpers_js_1.wait)(0);
|
|
249
283
|
}
|
|
250
284
|
else {
|
|
251
|
-
handle = helpers_js_1.wait(this.reconnectTimeInSeconds * 1000);
|
|
285
|
+
handle = (0, helpers_js_1.wait)(this.reconnectTimeInSeconds * 1000);
|
|
252
286
|
}
|
|
253
287
|
this._cancelRetriesHandler = handle.cancel;
|
|
254
288
|
return handle.promise.then(() => this._connect());
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const events_1 = require("events");
|
|
7
7
|
const promise_breaker_1 = __importDefault(require("promise-breaker"));
|
|
8
|
+
const MAX_MESSAGES_PER_BATCH = 1000;
|
|
8
9
|
const IRRECOVERABLE_ERRORS = [
|
|
9
10
|
403,
|
|
10
11
|
404,
|
|
@@ -56,6 +57,8 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
56
57
|
this._messages = [];
|
|
57
58
|
/** Oublished, but not yet confirmed messages. */
|
|
58
59
|
this._unconfirmedMessages = [];
|
|
60
|
+
/** Consumers which will be reconnected on channel errors etc. */
|
|
61
|
+
this._consumers = [];
|
|
59
62
|
/**
|
|
60
63
|
* True if the "worker" is busy sending messages. False if we need to
|
|
61
64
|
* start the worker to get stuff done.
|
|
@@ -67,13 +70,19 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
67
70
|
* workers ever do wake up, they'll know to stop working.
|
|
68
71
|
*/
|
|
69
72
|
this._workerNumber = 0;
|
|
73
|
+
/**
|
|
74
|
+
* True if the underlying channel has room for more messages.
|
|
75
|
+
*/
|
|
76
|
+
this._channelHasRoom = true;
|
|
70
77
|
this._onConnect = this._onConnect.bind(this);
|
|
71
78
|
this._onDisconnect = this._onDisconnect.bind(this);
|
|
72
79
|
this._connectionManager = connectionManager;
|
|
73
80
|
this.name = options.name;
|
|
81
|
+
this._publishTimeout = options.publishTimeout;
|
|
74
82
|
this._json = (_a = options.json) !== null && _a !== void 0 ? _a : false;
|
|
75
83
|
// Array of setup functions to call.
|
|
76
84
|
this._setups = [];
|
|
85
|
+
this._consumers = [];
|
|
77
86
|
if (options.setup) {
|
|
78
87
|
this._setups.push(options.setup);
|
|
79
88
|
}
|
|
@@ -172,15 +181,17 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
172
181
|
*/
|
|
173
182
|
publish(exchange, routingKey, content, options, done) {
|
|
174
183
|
return promise_breaker_1.default.addCallback(done, new Promise((resolve, reject) => {
|
|
175
|
-
|
|
184
|
+
const { timeout, ...opts } = options || {};
|
|
185
|
+
this._enqueueMessage({
|
|
176
186
|
type: 'publish',
|
|
177
187
|
exchange,
|
|
178
188
|
routingKey,
|
|
179
189
|
content,
|
|
180
|
-
options,
|
|
181
190
|
resolve,
|
|
182
191
|
reject,
|
|
183
|
-
|
|
192
|
+
options: opts,
|
|
193
|
+
isTimedout: false,
|
|
194
|
+
}, timeout || this._publishTimeout);
|
|
184
195
|
this._startWorker();
|
|
185
196
|
}));
|
|
186
197
|
}
|
|
@@ -195,24 +206,47 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
195
206
|
*/
|
|
196
207
|
sendToQueue(queue, content, options, done) {
|
|
197
208
|
return promise_breaker_1.default.addCallback(done, new Promise((resolve, reject) => {
|
|
198
|
-
|
|
209
|
+
const { timeout, ...opts } = options || {};
|
|
210
|
+
this._enqueueMessage({
|
|
199
211
|
type: 'sendToQueue',
|
|
200
212
|
queue,
|
|
201
213
|
content,
|
|
202
|
-
options,
|
|
203
214
|
resolve,
|
|
204
215
|
reject,
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
options: opts,
|
|
217
|
+
isTimedout: false,
|
|
218
|
+
}, timeout || this._publishTimeout);
|
|
219
|
+
this._startWorker();
|
|
207
220
|
}));
|
|
208
221
|
}
|
|
222
|
+
_enqueueMessage(message, timeout) {
|
|
223
|
+
if (timeout) {
|
|
224
|
+
message.timeout = setTimeout(() => {
|
|
225
|
+
let idx = this._messages.indexOf(message);
|
|
226
|
+
if (idx !== -1) {
|
|
227
|
+
this._messages.splice(idx, 1);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
idx = this._unconfirmedMessages.indexOf(message);
|
|
231
|
+
if (idx !== -1) {
|
|
232
|
+
this._unconfirmedMessages.splice(idx, 1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
message.isTimedout = true;
|
|
236
|
+
message.reject(new Error('timeout'));
|
|
237
|
+
}, timeout);
|
|
238
|
+
}
|
|
239
|
+
this._messages.push(message);
|
|
240
|
+
}
|
|
209
241
|
// Called whenever we connect to the broker.
|
|
210
242
|
async _onConnect({ connection }) {
|
|
211
243
|
this._irrecoverableCode = undefined;
|
|
212
244
|
try {
|
|
213
245
|
const channel = await connection.createConfirmChannel();
|
|
214
246
|
this._channel = channel;
|
|
247
|
+
this._channelHasRoom = true;
|
|
215
248
|
channel.on('close', () => this._onChannelClose(channel));
|
|
249
|
+
channel.on('drain', () => this._onChannelDrain());
|
|
216
250
|
this._settingUp = Promise.all(this._setups.map((setupFn) =>
|
|
217
251
|
// TODO: Use a timeout here to guard against setupFns that never resolve?
|
|
218
252
|
promise_breaker_1.default.call(setupFn, this, channel).catch((err) => {
|
|
@@ -221,7 +255,11 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
221
255
|
return;
|
|
222
256
|
}
|
|
223
257
|
this.emit('error', err, { name: this.name });
|
|
224
|
-
})))
|
|
258
|
+
})))
|
|
259
|
+
.then(() => {
|
|
260
|
+
return Promise.all(this._consumers.map((c) => this._reconnectConsumer(c)));
|
|
261
|
+
})
|
|
262
|
+
.then(() => {
|
|
225
263
|
this._settingUp = undefined;
|
|
226
264
|
});
|
|
227
265
|
await this._settingUp;
|
|
@@ -229,13 +267,6 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
229
267
|
// Can happen if channel closes while we're setting up.
|
|
230
268
|
return;
|
|
231
269
|
}
|
|
232
|
-
if (this._unconfirmedMessages.length > 0) {
|
|
233
|
-
// requeue any messages that were left unconfirmed when connection was lost
|
|
234
|
-
let message;
|
|
235
|
-
while ((message = this._unconfirmedMessages.shift())) {
|
|
236
|
-
this._messages.push(message);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
270
|
// Since we just connected, publish any queued messages
|
|
240
271
|
this._startWorker();
|
|
241
272
|
this.emit('connect');
|
|
@@ -253,6 +284,11 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
253
284
|
}
|
|
254
285
|
// Wait for another reconnect to create a new channel.
|
|
255
286
|
}
|
|
287
|
+
/** Called whenever the channel drains. */
|
|
288
|
+
_onChannelDrain() {
|
|
289
|
+
this._channelHasRoom = true;
|
|
290
|
+
this._startWorker();
|
|
291
|
+
}
|
|
256
292
|
// Called whenever we disconnect from the AMQP server.
|
|
257
293
|
_onDisconnect(ex) {
|
|
258
294
|
this._irrecoverableCode = ex.err instanceof Error ? ex.err.code : undefined;
|
|
@@ -275,11 +311,21 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
275
311
|
this._working = false;
|
|
276
312
|
if (this._messages.length !== 0) {
|
|
277
313
|
// Reject any unsent messages.
|
|
278
|
-
this._messages.forEach((message) =>
|
|
314
|
+
this._messages.forEach((message) => {
|
|
315
|
+
if (message.timeout) {
|
|
316
|
+
clearTimeout(message.timeout);
|
|
317
|
+
}
|
|
318
|
+
message.reject(new Error('Channel closed'));
|
|
319
|
+
});
|
|
279
320
|
}
|
|
280
321
|
if (this._unconfirmedMessages.length !== 0) {
|
|
281
322
|
// Reject any unconfirmed messages.
|
|
282
|
-
this._unconfirmedMessages.forEach((message) =>
|
|
323
|
+
this._unconfirmedMessages.forEach((message) => {
|
|
324
|
+
if (message.timeout) {
|
|
325
|
+
clearTimeout(message.timeout);
|
|
326
|
+
}
|
|
327
|
+
message.reject(new Error('Channel closed'));
|
|
328
|
+
});
|
|
283
329
|
}
|
|
284
330
|
this._connectionManager.removeListener('connect', this._onConnect);
|
|
285
331
|
this._connectionManager.removeListener('disconnect', this._onDisconnect);
|
|
@@ -290,7 +336,7 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
290
336
|
});
|
|
291
337
|
}
|
|
292
338
|
_shouldPublish() {
|
|
293
|
-
return this._messages.length > 0 && !this._settingUp && !!this._channel;
|
|
339
|
+
return (this._messages.length > 0 && !this._settingUp && !!this._channel && this._channelHasRoom);
|
|
294
340
|
}
|
|
295
341
|
// Start publishing queued messages, if there isn't already a worker doing this.
|
|
296
342
|
_startWorker() {
|
|
@@ -304,6 +350,44 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
304
350
|
_canWaitReconnection() {
|
|
305
351
|
return !this._irrecoverableCode || !IRRECOVERABLE_ERRORS.includes(this._irrecoverableCode);
|
|
306
352
|
}
|
|
353
|
+
_messageResolved(message, result) {
|
|
354
|
+
removeUnconfirmedMessage(this._unconfirmedMessages, message);
|
|
355
|
+
message.resolve(result);
|
|
356
|
+
}
|
|
357
|
+
_messageRejected(message, err) {
|
|
358
|
+
if (!this._channel && this._canWaitReconnection()) {
|
|
359
|
+
// Tried to write to a closed channel. Leave the message in the queue and we'll try again when
|
|
360
|
+
// we reconnect.
|
|
361
|
+
removeUnconfirmedMessage(this._unconfirmedMessages, message);
|
|
362
|
+
this._messages.push(message);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// Something went wrong trying to send this message - could be JSON.stringify failed, could be
|
|
366
|
+
// the broker rejected the message. Either way, reject it back
|
|
367
|
+
removeUnconfirmedMessage(this._unconfirmedMessages, message);
|
|
368
|
+
message.reject(err);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
_getEncodedMessage(content) {
|
|
372
|
+
let encodedMessage;
|
|
373
|
+
if (this._json) {
|
|
374
|
+
encodedMessage = Buffer.from(JSON.stringify(content));
|
|
375
|
+
}
|
|
376
|
+
else if (typeof content === 'string') {
|
|
377
|
+
encodedMessage = Buffer.from(content);
|
|
378
|
+
}
|
|
379
|
+
else if (content instanceof Buffer) {
|
|
380
|
+
encodedMessage = content;
|
|
381
|
+
}
|
|
382
|
+
else if (typeof content === 'object' && typeof content.toString === 'function') {
|
|
383
|
+
encodedMessage = Buffer.from(content.toString());
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
console.warn('amqp-connection-manager: Sending JSON message, but json option not speicifed');
|
|
387
|
+
encodedMessage = Buffer.from(JSON.stringify(content));
|
|
388
|
+
}
|
|
389
|
+
return encodedMessage;
|
|
390
|
+
}
|
|
307
391
|
_publishQueuedMessages(workerNumber) {
|
|
308
392
|
const channel = this._channel;
|
|
309
393
|
if (!channel ||
|
|
@@ -314,89 +398,136 @@ class ChannelWrapper extends events_1.EventEmitter {
|
|
|
314
398
|
this._working = false;
|
|
315
399
|
return;
|
|
316
400
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
else if (typeof message.content === 'string') {
|
|
327
|
-
encodedMessage = Buffer.from(message.content);
|
|
328
|
-
}
|
|
329
|
-
else if (message.content instanceof Buffer) {
|
|
330
|
-
encodedMessage = message.content;
|
|
331
|
-
}
|
|
332
|
-
else if (typeof message.content === 'object' &&
|
|
333
|
-
typeof message.content.toString === 'function') {
|
|
334
|
-
encodedMessage = Buffer.from(message.content.toString());
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
throw new Error('Invalid message content');
|
|
401
|
+
try {
|
|
402
|
+
// Send messages in batches of 1000 - don't want to starve the event loop.
|
|
403
|
+
let sendsLeft = MAX_MESSAGES_PER_BATCH;
|
|
404
|
+
while (this._channelHasRoom && this._messages.length > 0 && sendsLeft > 0) {
|
|
405
|
+
sendsLeft--;
|
|
406
|
+
const message = this._messages.shift();
|
|
407
|
+
if (!message) {
|
|
408
|
+
break;
|
|
338
409
|
}
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
else {
|
|
360
|
-
resolve(result);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
/* istanbul ignore next */
|
|
365
|
-
default:
|
|
366
|
-
throw new Error(`Unhandled message type ${message.type}`);
|
|
410
|
+
this._unconfirmedMessages.push(message);
|
|
411
|
+
const encodedMessage = this._getEncodedMessage(message.content);
|
|
412
|
+
switch (message.type) {
|
|
413
|
+
case 'publish': {
|
|
414
|
+
let thisCanSend = true;
|
|
415
|
+
thisCanSend = this._channelHasRoom = channel.publish(message.exchange, message.routingKey, encodedMessage, message.options, (err) => {
|
|
416
|
+
if (message.isTimedout) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (message.timeout) {
|
|
420
|
+
clearTimeout(message.timeout);
|
|
421
|
+
}
|
|
422
|
+
if (err) {
|
|
423
|
+
this._messageRejected(message, err);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
this._messageResolved(message, thisCanSend);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
break;
|
|
367
430
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// the broker rejected the message. Either way, reject it back
|
|
390
|
-
removeUnconfirmedMessage(this._unconfirmedMessages, message);
|
|
391
|
-
message.reject(err);
|
|
431
|
+
case 'sendToQueue': {
|
|
432
|
+
let thisCanSend = true;
|
|
433
|
+
thisCanSend = this._channelHasRoom = channel.sendToQueue(message.queue, encodedMessage, message.options, (err) => {
|
|
434
|
+
if (message.isTimedout) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (message.timeout) {
|
|
438
|
+
clearTimeout(message.timeout);
|
|
439
|
+
}
|
|
440
|
+
if (err) {
|
|
441
|
+
this._messageRejected(message, err);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
this._messageResolved(message, thisCanSend);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
/* istanbul ignore next */
|
|
450
|
+
default:
|
|
451
|
+
throw new Error(`Unhandled message type ${message.type}`);
|
|
392
452
|
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
this.
|
|
397
|
-
|
|
398
|
-
|
|
453
|
+
}
|
|
454
|
+
// If we didn't send all the messages, send some more...
|
|
455
|
+
if (this._channelHasRoom && this._messages.length > 0) {
|
|
456
|
+
setImmediate(() => this._publishQueuedMessages(workerNumber));
|
|
457
|
+
}
|
|
458
|
+
this._working = false;
|
|
459
|
+
/* istanbul ignore next */
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
this._working = false;
|
|
463
|
+
this.emit('error', err);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Setup a consumer
|
|
468
|
+
* This consumer will be reconnected on cancellation and channel errors.
|
|
469
|
+
*/
|
|
470
|
+
async consume(queue, onMessage, options = {}) {
|
|
471
|
+
const consumer = {
|
|
472
|
+
consumerTag: null,
|
|
473
|
+
queue,
|
|
474
|
+
onMessage,
|
|
475
|
+
options,
|
|
476
|
+
};
|
|
477
|
+
this._consumers.push(consumer);
|
|
478
|
+
await this._consume(consumer);
|
|
479
|
+
}
|
|
480
|
+
async _consume(consumer) {
|
|
481
|
+
if (!this._channel) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const { prefetch, ...options } = consumer.options;
|
|
485
|
+
if (typeof prefetch === 'number') {
|
|
486
|
+
this._channel.prefetch(prefetch, false);
|
|
487
|
+
}
|
|
488
|
+
const { consumerTag } = await this._channel.consume(consumer.queue, (msg) => {
|
|
489
|
+
if (!msg) {
|
|
490
|
+
consumer.consumerTag = null;
|
|
491
|
+
this._reconnectConsumer(consumer).catch((err) => {
|
|
492
|
+
if (err.isOperational && err.message.includes('BasicConsume; 404')) {
|
|
493
|
+
// Ignore errors caused by queue not declared. In
|
|
494
|
+
// those cases the connection will reconnect and
|
|
495
|
+
// then consumers reestablished. The full reconnect
|
|
496
|
+
// might be avoided if we assert the queue again
|
|
497
|
+
// before starting to consume.
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
throw err;
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
consumer.onMessage(msg);
|
|
505
|
+
}, options);
|
|
506
|
+
consumer.consumerTag = consumerTag;
|
|
507
|
+
}
|
|
508
|
+
async _reconnectConsumer(consumer) {
|
|
509
|
+
if (!this._consumers.includes(consumer)) {
|
|
510
|
+
// Intentionally canceled
|
|
511
|
+
return;
|
|
399
512
|
}
|
|
513
|
+
await this._consume(consumer);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Cancel all consumers
|
|
517
|
+
*/
|
|
518
|
+
async cancelAll() {
|
|
519
|
+
const consumers = this._consumers;
|
|
520
|
+
this._consumers = [];
|
|
521
|
+
if (!this._channel) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const channel = this._channel;
|
|
525
|
+
await Promise.all(consumers.reduce((acc, consumer) => {
|
|
526
|
+
if (consumer.consumerTag) {
|
|
527
|
+
acc.push(channel.cancel(consumer.consumerTag));
|
|
528
|
+
}
|
|
529
|
+
return acc;
|
|
530
|
+
}, []));
|
|
400
531
|
}
|
|
401
532
|
/** Send an `ack` to the underlying channel. */
|
|
402
533
|
ack(message, allUpTo) {
|
package/dist/cjs/index.js
CHANGED
|
@@ -3,10 +3,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.connect = void 0;
|
|
6
|
+
exports.AmqpConnectionManagerClass = exports.connect = void 0;
|
|
7
7
|
const AmqpConnectionManager_js_1 = __importDefault(require("./AmqpConnectionManager.js"));
|
|
8
|
+
exports.AmqpConnectionManagerClass = AmqpConnectionManager_js_1.default;
|
|
8
9
|
function connect(urls, options) {
|
|
9
|
-
|
|
10
|
+
const conn = new AmqpConnectionManager_js_1.default(urls, options);
|
|
11
|
+
conn.connect().catch(() => {
|
|
12
|
+
/* noop */
|
|
13
|
+
});
|
|
14
|
+
return conn;
|
|
10
15
|
}
|
|
11
16
|
exports.connect = connect;
|
|
12
17
|
const amqp = { connect };
|
|
@@ -59,6 +59,7 @@ export interface IAmqpConnectionManager {
|
|
|
59
59
|
addListener(event: 'disconnect', listener: (arg: {
|
|
60
60
|
err: Error;
|
|
61
61
|
}) => void): this;
|
|
62
|
+
listeners(eventName: string | symbol): Function[];
|
|
62
63
|
on(event: string, listener: (...args: any[]) => void): this;
|
|
63
64
|
on(event: 'connect', listener: ConnectListener): this;
|
|
64
65
|
on(event: 'blocked', listener: (arg: {
|
|
@@ -96,13 +97,17 @@ export interface IAmqpConnectionManager {
|
|
|
96
97
|
err: Error;
|
|
97
98
|
}) => void): this;
|
|
98
99
|
removeListener(event: string, listener: (...args: any[]) => void): this;
|
|
100
|
+
connect(options?: {
|
|
101
|
+
timeout?: number;
|
|
102
|
+
}): Promise<void>;
|
|
103
|
+
reconnect(): void;
|
|
99
104
|
createChannel(options?: CreateChannelOpts): ChannelWrapper;
|
|
100
105
|
close(): Promise<void>;
|
|
101
106
|
isConnected(): boolean;
|
|
102
107
|
/** The current connection. */
|
|
103
|
-
|
|
108
|
+
readonly connection: Connection | undefined;
|
|
104
109
|
/** Returns the number of registered channels. */
|
|
105
|
-
|
|
110
|
+
readonly channelCount: number;
|
|
106
111
|
}
|
|
107
112
|
export default class AmqpConnectionManager extends EventEmitter implements IAmqpConnectionManager {
|
|
108
113
|
private _channels;
|
|
@@ -148,6 +153,15 @@ export default class AmqpConnectionManager extends EventEmitter implements IAmqp
|
|
|
148
153
|
* Note that if this is supplied, then `urls` is ignored.
|
|
149
154
|
*/
|
|
150
155
|
constructor(urls: ConnectionUrl | ConnectionUrl[] | undefined | null, options?: AmqpConnectionManagerOptions);
|
|
156
|
+
/**
|
|
157
|
+
* Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
|
|
158
|
+
* reconnect attempts will continue in the background.
|
|
159
|
+
* @param [options={}] -
|
|
160
|
+
* @param [options.timeout] - Time to wait for initial connect
|
|
161
|
+
*/
|
|
162
|
+
connect({ timeout }?: {
|
|
163
|
+
timeout?: number;
|
|
164
|
+
}): Promise<void>;
|
|
151
165
|
createChannel(options?: CreateChannelOpts): ChannelWrapper;
|
|
152
166
|
close(): Promise<void>;
|
|
153
167
|
isConnected(): boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import amqp from 'amqplib';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
2
|
+
import { EventEmitter, once } from 'events';
|
|
3
3
|
import pb from 'promise-breaker';
|
|
4
4
|
import { URL } from 'url';
|
|
5
5
|
import ChannelWrapper from './ChannelWrapper.js';
|
|
@@ -67,7 +67,41 @@ export default class AmqpConnectionManager extends EventEmitter {
|
|
|
67
67
|
// There will be one listener per channel, and there could be a lot of channels, so disable warnings from node.
|
|
68
68
|
this.setMaxListeners(0);
|
|
69
69
|
this._findServers = options.findServers || (() => Promise.resolve(urls));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Start the connect retries and await the first connect result. Even if the initial connect fails or timeouts, the
|
|
73
|
+
* reconnect attempts will continue in the background.
|
|
74
|
+
* @param [options={}] -
|
|
75
|
+
* @param [options.timeout] - Time to wait for initial connect
|
|
76
|
+
*/
|
|
77
|
+
async connect({ timeout } = {}) {
|
|
70
78
|
this._connect();
|
|
79
|
+
let reject;
|
|
80
|
+
const onDisconnect = ({ err }) => {
|
|
81
|
+
// Ignore disconnects caused by dead servers etc., but throw on operational errors like bad credentials.
|
|
82
|
+
if (err.isOperational) {
|
|
83
|
+
reject(err);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
try {
|
|
87
|
+
await Promise.race([
|
|
88
|
+
once(this, 'connect'),
|
|
89
|
+
new Promise((_resolve, innerReject) => {
|
|
90
|
+
reject = innerReject;
|
|
91
|
+
this.on('disconnect', onDisconnect);
|
|
92
|
+
}),
|
|
93
|
+
...(timeout
|
|
94
|
+
? [
|
|
95
|
+
wait(timeout).promise.then(() => {
|
|
96
|
+
throw new Error('amqp-connection-manager: connect timeout');
|
|
97
|
+
}),
|
|
98
|
+
]
|
|
99
|
+
: []),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
this.removeListener('disconnect', onDisconnect);
|
|
104
|
+
}
|
|
71
105
|
}
|
|
72
106
|
// `options` here are any options that can be passed to ChannelWrapper.
|
|
73
107
|
createChannel(options = {}) {
|