@onify/fake-amqplib 1.0.0 → 3.0.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/index.js CHANGED
@@ -1,17 +1,36 @@
1
- 'use strict';
2
-
3
- const {Broker} = require('smqp');
4
- const {EventEmitter} = require('events');
5
- const {URL, format: urlFormat} = require('url');
6
-
7
- const smqpSymbol = Symbol.for('smqp');
1
+ import { Broker } from 'smqp';
2
+ import { EventEmitter } from 'events';
3
+ import { format as urlFormat } from 'url';
4
+
5
+ const kSmqp = Symbol.for('smqp');
6
+ const kClosed = Symbol.for('closed');
7
+ const kDeliveryTag = Symbol.for('channel delivery tag');
8
+ const kPrefetch = Symbol.for('prefetch');
9
+ const kChannelPrefetch = Symbol.for('channel prefetch');
10
+
11
+ class AmqplibBroker extends Broker {
12
+ constructor(...args) {
13
+ super(...args);
14
+ this[kDeliveryTag] = 0;
15
+ }
16
+ _getNextDeliveryTag() {
17
+ return ++this[kDeliveryTag];
18
+ }
19
+ _getMessageByDeliveryTag(queue, deliveryTag) {
20
+ const q = this.getQueue(queue);
21
+ return q.messages.find((m) => m.fields.deliveryTag === deliveryTag);
22
+ }
23
+ _getChannelConsumers(channelName) {
24
+ return this.getConsumers().filter((f) => f.options.channelName === channelName);
25
+ }
26
+ }
8
27
 
9
28
  class FakeAmqpError extends Error {
10
29
  constructor(message, code, killChannel, killConnection) {
11
30
  super(message);
12
31
  this.code = code;
13
32
  this._killChannel = killChannel;
14
- this._killConnection = killConnection;
33
+ this._killConnection = killConnection;
15
34
  }
16
35
  }
17
36
 
@@ -21,405 +40,588 @@ class FakeAmqpNotFoundError extends FakeAmqpError {
21
40
  }
22
41
  }
23
42
 
24
- const connections = [];
43
+ class FakeAmqpUnknownDeliveryTag extends FakeAmqpError {
44
+ constructor(deliveryTag) {
45
+ super(`Channel closed by server: 406 (PRECONDITION-FAILED) with message "PRECONDITION_FAILED - unknown delivery tag ${deliveryTag}`, 406, true, false);
46
+ }
47
+ get _emit() {
48
+ return true;
49
+ }
50
+ }
51
+
52
+ function Message(smqpMessage, deliveryTag) {
53
+ this[kSmqp] = smqpMessage;
54
+ this.fields = { ...smqpMessage.fields, deliveryTag };
55
+ this.content = Buffer.from(smqpMessage.content);
56
+ this.properties = { ...smqpMessage.properties };
57
+ }
58
+
59
+ export class FakeAmqplibChannel extends EventEmitter {
60
+ constructor(broker, connection) {
61
+ super();
62
+ this.connection = connection;
63
+
64
+ this[kPrefetch] = 10000;
65
+ this[kChannelPrefetch] = Infinity;
66
+ this[kClosed] = false;
67
+ const channelName = this._channelName = `channel-${generateId()}`;
68
+ this._version = connection._version;
69
+ this._broker = broker;
70
+
71
+ this._channelQueue = broker.assertQueue(`#${channelName}`);
72
+ this._emitReturn = this._emitReturn.bind(this);
25
73
 
26
- module.exports = Fake('3.5');
74
+ broker.on('return', this._emitReturn);
75
+
76
+ this._createChannelMessage = this._createChannelMessage.bind(this);
77
+ this._calculateChannelCapacity = this._calculateChannelCapacity.bind(this);
78
+ }
79
+ get _closed() {
80
+ return this[kClosed];
81
+ }
82
+ assertExchange(...args) {
83
+ return this._callBroker(assertExchange, ...args);
27
84
 
28
- function Fake(minorVersion) {
29
- let defaultVersion = Number(minorVersion);
30
- return {
31
- connections,
32
- resetMock,
33
- connectSync,
34
- setVersion(minor) {
35
- const n = Number(minor);
36
- if (!isNaN(n)) defaultVersion = n;
37
- },
38
- connect,
39
- };
85
+ function assertExchange(exchange, ...assertArgs) {
86
+ this.assertExchange(exchange, ...assertArgs);
87
+ return { exchange };
88
+ }
89
+ }
90
+ assertQueue(...args) {
91
+ const connection = this.connection;
92
+ return this._callBroker(assertQueue, ...args);
93
+
94
+ function assertQueue(queueName, ...assertArgs) {
95
+ const name = queueName ? queueName : `amqp.gen-${generateId()}`;
96
+ const options = typeof assertArgs[0] === 'object' ? assertArgs.shift() : {};
97
+ const queue = this.assertQueue(name, { ...options, _connectionId: connection._id }, ...assertArgs);
98
+ return {
99
+ queue: name,
100
+ messageCount: queue.messageCount,
101
+ consumerCount: queue.consumerCount,
102
+ };
103
+ }
104
+ }
105
+ bindExchange(destination, source, ...args) {
106
+ const broker = this._broker;
107
+ return Promise.all([ this.checkExchange(source), this.checkExchange(destination) ]).then(() => {
108
+ return this._callBroker(broker.bindExchange, source, destination, ...args);
109
+ });
110
+ }
111
+ bindQueue(queue, source, ...args) {
112
+ return Promise.all([ this.checkQueue(queue), this.checkExchange(source) ]).then(() => {
113
+ return this._callBroker(this._broker.bindQueue, queue, source, ...args);
114
+ });
115
+ }
116
+ checkExchange(name, ...args) {
117
+ const connPath = this.connection._url.pathname;
118
+ return this._callBroker(check, ...args);
40
119
 
41
- function connect(amqpUrl, ...args) {
42
- const connection = connectSync(amqpUrl, ...args);
43
- return resolveOrCallback(args.slice(-1)[0], null, connection);
120
+ function check() {
121
+ if (!this.getExchange(name)) throw new FakeAmqpNotFoundError('exchange', name, connPath);
122
+ return true;
123
+ }
44
124
  }
125
+ checkQueue(name, ...args) {
126
+ const connPath = this.connection._url.pathname;
127
+ return this._callBroker(check, ...args);
128
+
129
+ function check() {
130
+ let queue;
131
+ if (!(queue = this.getQueue(name))) {
132
+ throw new FakeAmqpNotFoundError('queue', name, connPath);
133
+ }
45
134
 
46
- function connectSync(amqpUrl, ...args) {
47
- const {_broker} = connections.find((conn) => compareConnectionString(conn._url, amqpUrl)) || {};
48
- const broker = _broker || Broker();
49
- const connection = Connection(broker, defaultVersion, amqpUrl, ...args);
50
- connections.push(connection);
51
- return connection;
135
+ return {
136
+ messageCount: queue.messageCount,
137
+ consumerCount: queue.consumerCount,
138
+ };
139
+ }
140
+ }
141
+ get(queue, ...args) {
142
+ const connPath = this.connection._url.pathname;
143
+ const createMessage = this._createChannelMessage;
144
+ return this._callBroker(getMessage, ...args);
145
+
146
+ function getMessage(...getargs) {
147
+ const q = this.getQueue(queue);
148
+ if (!q) throw new FakeAmqpNotFoundError('queue', queue, connPath);
149
+ const msg = q.get(...getargs) || false;
150
+ if (!msg) return msg;
151
+
152
+ return createMessage(msg, args[0]?.noAck);
153
+ }
154
+ }
155
+ deleteExchange(exchange, ...args) {
156
+ const connPath = this.connection._url.pathname;
157
+ return this._callBroker(check, ...args);
158
+
159
+ function check() {
160
+ const result = this.deleteExchange(exchange, ...args);
161
+ if (!result && this.owner.version < 3.2) throw new FakeAmqpNotFoundError('exchange', exchange, connPath);
162
+ return result;
163
+ }
164
+ }
165
+ deleteQueue(queue, ...args) {
166
+ const connPath = this.connection._url.pathname;
167
+ return this._callBroker(check, ...args);
168
+
169
+ function check() {
170
+ const result = this.deleteQueue(queue, ...args);
171
+ if (!result && this.owner.version < 3.2) throw new FakeAmqpNotFoundError('queue', queue, connPath);
172
+ return result;
173
+ }
52
174
  }
175
+ publish(exchange, routingKey, content, options, callback) {
176
+ if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
177
+ if (exchange === '') return this.sendToQueue(routingKey, content, options, callback);
178
+
179
+ const args = [ this._broker.publish, exchange, routingKey, content ];
180
+
181
+ args.push(options, callback);
182
+
183
+ this.checkExchange(exchange).then(() => {
184
+ return this._callBroker(...args);
185
+ }).catch((err) => {
186
+ this.emit('error', err);
187
+ });
53
188
 
54
- function resetMock() {
55
- for (const connection of connections.splice(0)) {
56
- connection._broker.reset();
189
+ return true;
190
+ }
191
+ purgeQueue(queue, ...args) {
192
+ const connPath = this.connection._url.pathname;
193
+ return this._callBroker(check, ...args);
194
+
195
+ function check() {
196
+ const result = this.purgeQueue(queue);
197
+ if (!result && this.owner.version < 3.2) throw new FakeAmqpNotFoundError('queue', queue, connPath);
198
+ return result === undefined ? undefined : { messageCount: result };
57
199
  }
58
200
  }
201
+ sendToQueue(queue, content, options, callback) {
202
+ if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
59
203
 
60
- function Connection(broker, version, amqpUrl, ...connArgs) {
61
- const emitter = new EventEmitter();
62
- const options = connArgs.filter((a) => typeof a !== 'function');
63
- let closed = false;
64
- const channels = [];
65
- const url = normalizeAmqpUrl(amqpUrl);
204
+ const args = [ this._broker.sendToQueue, queue, content ];
66
205
 
67
- return {
68
- _id: generateId(),
69
- _broker: broker,
70
- _version: version,
71
- _url: url,
72
- get _closed() {
73
- return closed;
74
- },
75
- get _emitter() {
76
- return emitter;
77
- },
78
- options,
79
- createChannel(...args) {
80
- if (closed) return resolveOrCallback(args.slice(-1)[0], unavailable());
206
+ args.push(options, callback);
81
207
 
82
- const channel = Channel(broker, this);
83
- channels.push(channel);
84
- return resolveOrCallback(args.slice(-1)[0], null, channel);
85
- },
86
- createConfirmChannel(...args) {
87
- if (closed) return resolveOrCallback(args.slice(-1)[0], unavailable());
208
+ this.checkQueue(queue).then(() => {
209
+ return this._callBroker(...args);
210
+ }).catch((err) => {
211
+ this.emit('error', err);
212
+ });
88
213
 
89
- const channel = Channel(broker, this, true);
90
- channels.push(channel);
91
- return resolveOrCallback(args.slice(-1)[0], null, channel);
92
- },
93
- close(...args) {
94
- if (closed) return resolveOrCallback(args.slice(-1)[0]);
95
- closed = true;
214
+ return true;
215
+ }
216
+ unbindExchange(destination, source, pattern, ...args) {
217
+ const connPath = this.connection._url.pathname;
218
+ return this._callBroker(check, ...args);
96
219
 
97
- const idx = connections.indexOf(this);
98
- if (idx > -1) connections.splice(idx, 1);
99
- channels.splice(0).forEach((channel) => channel.close());
220
+ function check() {
221
+ const q = this.getExchange(destination);
222
+ if (!q) throw new FakeAmqpNotFoundError('exchange', destination);
100
223
 
101
- emitter.emit('close');
224
+ const exchange = this.getExchange(source);
225
+ if (!exchange) throw new FakeAmqpNotFoundError('exchange', source);
102
226
 
103
- return resolveOrCallback(args.slice(-1)[0]);
104
- },
105
- on(...args) {
106
- return emitter.on(...args);
107
- },
108
- once(...args) {
109
- return emitter.once(...args);
110
- },
111
- };
227
+ const result = this.unbindExchange(source, destination, pattern);
228
+ if (!result && this.owner.version <= 3.2) throw new FakeAmqpNotFoundError('binding', pattern, connPath);
112
229
 
113
- function unavailable() {
114
- return new FakeAmqpError('Connection closed: 504', 504);
230
+ return true;
115
231
  }
116
232
  }
233
+ unbindQueue(queue, source, pattern, ...args) {
234
+ const connPath = this.connection._url.pathname;
235
+ return this._callBroker(check, ...args);
117
236
 
118
- function Channel(broker, connection, confirmChannel) {
119
- let closed = false, prefetch = 10000;
120
- const emitter = new EventEmitter();
121
- const channelName = 'channel-' + generateId();
122
- const version = connection._version;
237
+ function check() {
238
+ const q = this.getQueue(queue);
239
+ if (!q) throw new FakeAmqpNotFoundError('queue', queue);
123
240
 
124
- broker.on('return', emitReturn);
241
+ const exchange = this.getExchange(source);
242
+ if (!exchange) throw new FakeAmqpNotFoundError('exchange', source);
125
243
 
126
- return {
127
- _broker: broker,
128
- _version: version,
129
- get _emitter() {
130
- return emitter;
131
- },
132
- get _closed() {
133
- return closed;
134
- },
135
- assertExchange(...args) {
136
- return callBroker(assertExchange, ...args);
137
-
138
- function assertExchange(exchange, ...args) {
139
- broker.assertExchange(exchange, ...args);
140
- return {
141
- exchange,
142
- };
143
- }
144
- },
145
- assertQueue(...args) {
146
- return callBroker(assertQueue, ...args);
147
-
148
- function assertQueue(queueName, ...args) {
149
- const name = queueName ? queueName : 'amqp.gen-' + generateId();
150
- const options = typeof args[0] === 'object' ? args.shift() : {};
151
- const queue = broker.assertQueue(name, {...options, _connectionId: connection._id}, ...args);
152
- return {
153
- queue: name,
154
- messageCount: queue.messageCount,
155
- consumerCount: queue.consumerCount,
156
- };
157
- }
158
- },
159
- bindExchange(destination, source, ...args) {
160
- return Promise.all([this.checkExchange(source), this.checkExchange(destination)]).then(() => {
161
- return callBroker(broker.bindExchange, source, destination, ...args);
162
- });
163
- },
164
- bindQueue(queue, source, ...args) {
165
- return Promise.all([this.checkQueue(queue), this.checkExchange(source)]).then(() => {
166
- return callBroker(broker.bindQueue, queue, source, ...args);
167
- });
168
- },
169
- checkExchange(name, ...args) {
170
- return callBroker(check, ...args);
244
+ const binding = exchange.getBinding(queue, pattern);
245
+ if (!binding && this.owner.version <= 3.2) {
246
+ throw new FakeAmqpNotFoundError('binding', pattern, connPath, this.owner.version < 3.2);
247
+ }
171
248
 
172
- function check() {
173
- if (!broker.getExchange(name)) throw new FakeAmqpNotFoundError('exchange', name, connection._url.pathname);
174
- return true;
175
- }
176
- },
177
- checkQueue(name, ...args) {
178
- return callBroker(check, ...args);
179
-
180
- function check() {
181
- let queue;
182
- if (!(queue = broker.getQueue(name))) {
183
- throw new FakeAmqpNotFoundError('queue', name, connection._url.pathname);
184
- }
185
-
186
- return {
187
- messageCount: queue.messageCount,
188
- consumerCount: queue.consumerCount,
189
- };
190
- }
191
- },
192
- get(queue, ...args) {
193
- return callBroker(getMessage, ...args);
194
-
195
- function getMessage(...getargs) {
196
- const q = broker.getQueue(queue);
197
- if (!q) throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
198
- const msg = q.get(...getargs) || false;
199
- if (!msg) return msg;
200
- return new Message(msg);
201
- }
202
- },
203
- deleteExchange(exchange, ...args) {
204
- return callBroker(check, ...args);
205
-
206
- function check() {
207
- const result = broker.deleteExchange(exchange, ...args);
208
- if (!result && version < 3.2) throw new FakeAmqpNotFoundError('exchange', exchange, connection._url.pathname);
209
- return result;
210
- }
211
- },
212
- deleteQueue(queue, ...args) {
213
- return callBroker(check, ...args);
214
-
215
- function check() {
216
- const result = broker.deleteQueue(queue, ...args);
217
- if (!result && version < 3.2) throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
218
- return result;
219
- }
220
- },
221
- publish(exchange, routingKey, content, options, callback) {
222
- if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
223
- if (exchange === '') return this.sendToQueue(routingKey, content, options, callback);
249
+ this.unbindQueue(queue, source, pattern);
250
+ return true;
251
+ }
252
+ }
253
+ consume(queue, onMessage, options = {}, callback) {
254
+ const { _id: connId, _url: connUrl } = this.connection;
255
+ const createMessage = this._createChannelMessage;
256
+ const calculateCapacity = this._calculateChannelCapacity;
257
+ const channelName = this._channelName;
258
+ const prefetch = this[kPrefetch];
259
+
260
+ return this._callBroker(consume, callback);
261
+
262
+ function consume() {
263
+ const q = queue && this.getQueue(queue);
264
+ if (!q) {
265
+ throw new FakeAmqpNotFoundError('queue', queue, connUrl.pathname);
266
+ }
224
267
 
225
- const args = [broker.publish, exchange, routingKey, content];
268
+ if (q.exclusive || (q.options.exclusive && q.options._connectionId !== connId)) {
269
+ throw new FakeAmqpError(`Channel closed by server: 403 (ACCESS-REFUSED) with message "ACCESS_REFUSED - queue '${queue}' in vhost '${connUrl.pathname}' in exclusive use"`, 403, true, true);
270
+ }
226
271
 
227
- if (confirmChannel) args.push(...addConfirmCallback(options, callback));
228
- else args.push(options);
272
+ const consumer = this.consume(queue, onMessage && handler, {
273
+ ...options,
274
+ channelName,
275
+ prefetch: calculateCapacity(prefetch),
276
+ _consumerPrefetch: prefetch,
277
+ });
229
278
 
230
- this.checkExchange(exchange).then(() => {
231
- return callBroker(...args);
232
- }).catch((err) => {
233
- emitter.emit('error', err);
234
- });
279
+ const capacityProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(consumer), 'capacity');
280
+ Object.defineProperty(consumer, 'capacity', {
281
+ get() {
282
+ return calculateCapacity(capacityProp.get.call(this));
283
+ },
284
+ });
235
285
 
236
- return true;
237
- },
238
- purgeQueue(queue, ...args) {
239
- return callBroker(check, ...args);
240
-
241
- function check() {
242
- const result = broker.purgeQueue(queue);
243
- if (!result && version < 3.2) throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
244
- return result === undefined ? undefined : {messageCount: result};
245
- }
246
- },
247
- sendToQueue(queue, content, options, callback) {
248
- if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
286
+ return { consumerTag: consumer.consumerTag };
287
+ }
249
288
 
250
- const args = [broker.sendToQueue, queue, content];
289
+ function handler(_, msg) {
290
+ onMessage(createMessage(msg, options.noAck));
291
+ }
292
+ }
293
+ cancel(consumerTag, ...args) {
294
+ return this._callBroker(this._broker.cancel, consumerTag, ...args);
295
+ }
296
+ close(callback) {
297
+ if (this[kClosed]) return;
298
+ this._teardown();
299
+ this.emit('close');
300
+ return resolveOrCallback(callback);
301
+ }
302
+ ack(message, allUpTo) {
303
+ const deliveryTag = message.fields.deliveryTag;
304
+ const channelMessage = this._broker._getMessageByDeliveryTag(this._channelQueue.name, deliveryTag);
305
+ const channelQ = this._channelQueue;
306
+
307
+ if (!allUpTo) this._callBroker(ackMessage);
308
+ else this._callBroker(ackAllUpToMessage);
309
+
310
+ function ackMessage() {
311
+ const msg = message[kSmqp];
312
+ if (!channelMessage || !msg.pending) {
313
+ throw new FakeAmqpUnknownDeliveryTag(message.fields.deliveryTag);
314
+ }
251
315
 
252
- if (confirmChannel) args.push(...addConfirmCallback(options, callback));
253
- else args.push(options);
316
+ channelQ.ack(channelMessage, false);
317
+ this.ack(msg, false);
318
+ }
254
319
 
255
- this.checkQueue(queue).then(() => {
256
- return callBroker(...args);
257
- }).catch((err) => {
258
- emitter.emit('error', err);
259
- });
320
+ function ackAllUpToMessage() {
321
+ const msg = message[kSmqp];
322
+ if (!channelMessage || !msg.pending) {
323
+ throw new FakeAmqpUnknownDeliveryTag(message.fields.deliveryTag);
324
+ }
260
325
 
261
- return true;
262
- },
263
- unbindExchange(destination, source, pattern, ...args) {
264
- return callBroker(check, ...args);
326
+ const brokerMessages = allUpToDeliveryTag(channelQ, deliveryTag, 'ack', false);
327
+ for (const brokerMessage of brokerMessages) {
328
+ brokerMessage.ack(false);
329
+ }
265
330
 
266
- function check() {
267
- const q = broker.getExchange(destination);
268
- if (!q) throw new FakeAmqpNotFoundError('exchange', destination);
331
+ channelQ.ack(channelMessage, false);
332
+ this.ack(msg, false);
333
+ }
334
+ }
335
+ ackAll() {
336
+ const channelQ = this._channelQueue;
337
+ let msg;
338
+ const brokerMessages = [];
339
+ while ((msg = channelQ.get())) {
340
+ brokerMessages.push(msg.content[kSmqp]);
341
+ msg.ack();
342
+ }
269
343
 
270
- const exchange = broker.getExchange(source);
271
- if (!exchange) throw new FakeAmqpNotFoundError('exchange', source);
344
+ for (const brokerMessage of brokerMessages) {
345
+ brokerMessage.ack();
346
+ }
347
+ }
348
+ reject(message, requeue = false) {
349
+ const deliveryTag = message.fields.deliveryTag;
350
+ const channelMessage = this._broker._getMessageByDeliveryTag(this._channelQueue.name, deliveryTag);
351
+ const channelQ = this._channelQueue;
272
352
 
273
- const result = broker.unbindExchange(source, destination, pattern);
274
- if (!result && version <= 3.2) throw new FakeAmqpNotFoundError('binding', pattern, connection._url.pathname);
353
+ this._callBroker(rejectMessage);
275
354
 
276
- return true;
277
- }
278
- },
279
- unbindQueue(queue, source, pattern, ...args) {
280
- return callBroker(check, ...args);
355
+ function rejectMessage() {
356
+ const msg = message[kSmqp];
357
+ if (!channelMessage || !msg.pending) {
358
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
359
+ }
281
360
 
282
- function check() {
283
- const q = broker.getQueue(queue);
284
- if (!q) throw new FakeAmqpNotFoundError('queue', queue);
361
+ channelQ.reject(channelMessage, false);
362
+ this.reject(msg, requeue);
363
+ }
364
+ }
365
+ nack(message, allUpTo = false, requeue = false) {
366
+ if (this.connection._version < 2.3) throw new Error(`Nack is not implemented in versions before 2.3 (${this.connection._version})`);
285
367
 
286
- const exchange = broker.getExchange(source);
287
- if (!exchange) throw new FakeAmqpNotFoundError('exchange', source);
368
+ const deliveryTag = message.fields.deliveryTag;
369
+ const channelMessage = this._broker._getMessageByDeliveryTag(this._channelQueue.name, deliveryTag);
370
+ const channelQ = this._channelQueue;
288
371
 
289
- const binding = exchange.getBinding(queue, pattern);
290
- if (!binding && version <= 3.2) throw new FakeAmqpNotFoundError('binding', pattern, connection._url.pathname, version < 3.2);
372
+ if (!allUpTo) this._callBroker(nackMessage);
373
+ else this._callBroker(nackAllUpToMessage);
291
374
 
292
- broker.unbindQueue(queue, source, pattern);
293
- return true;
294
- }
295
- },
296
- consume(queue, onMessage, options = {}, callback) {
297
- return callBroker(check, callback);
298
-
299
- function check() {
300
- const q = queue && broker.getQueue(queue);
301
- if (!q) {
302
- throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
303
- }
304
-
305
- if (q.exclusive || (q.options.exclusive && q.options._connectionId !== connection._id)) {
306
- throw new FakeAmqpError(`Channel closed by server: 403 (ACCESS-REFUSED) with message "ACCESS_REFUSED - queue '${queue}' in vhost '${connection._url.pathname}' in exclusive use"`, 403, true, true);
307
- }
308
-
309
- const {consumerTag} = broker.consume(queue, onMessage && handler, {...options, channelName, prefetch});
310
- return {consumerTag};
311
- }
312
-
313
- function handler(_, msg) {
314
- onMessage(new Message(msg));
315
- }
316
- },
317
- cancel(consumerTag, ...args) {
318
- return callBroker(broker.cancel, consumerTag, ...args);
319
- },
320
- close(callback) {
321
- if (closed) return;
322
- broker.off('return', emitReturn);
323
- const channelConsumers = broker.getConsumers().filter((f) => f.options.channelName === channelName);
324
- channelConsumers.forEach((c) => broker.cancel(c.consumerTag));
325
- closed = true;
326
- emitter.emit('close');
327
- return resolveOrCallback(callback);
328
- },
329
- ack(message, ...args) {
330
- broker.ack(message[smqpSymbol], ...args);
331
- },
332
- ackAll() {
333
- const consumers = broker.getConsumers().filter(({options}) => options.channelName === channelName);
334
- consumers.forEach((c) => broker.getConsumer(c.consumerTag).ackAll());
335
- },
336
- ...(version >= 2.3 ? {
337
- nack(message, ...args) {
338
- return broker.nack(message[smqpSymbol], ...args);
339
- },
340
- } : undefined),
341
- reject(message, ...args) {
342
- broker.reject(message[smqpSymbol], ...args);
343
- },
344
- nackAll(requeue = false) {
345
- const consumers = broker.getConsumers().filter(({options}) => options.channelName === channelName);
346
- consumers.forEach((c) => broker.getConsumer(c.consumerTag).nackAll(requeue));
347
- },
348
- prefetch(val) {
349
- prefetch = val;
350
- },
351
- on(...args) {
352
- return emitter.on(...args);
353
- },
354
- once(...args) {
355
- return emitter.once(...args);
356
- },
357
- };
375
+ function nackMessage() {
376
+ const msg = message[kSmqp];
377
+ if (!channelMessage || !msg.pending) {
378
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
379
+ }
358
380
 
359
- function addConfirmCallback(options, callback) {
360
- const confirm = 'msg.' + generateId();
361
- const consumerTag = 'ct-' + confirm;
362
- options = {...options, confirm};
363
-
364
- broker.on('message.*', onConsumeMessage, {consumerTag});
365
-
366
- let undelivered;
367
- function onConsumeMessage(event) {
368
- if (event.properties && event.properties.confirm !== confirm) return;
369
- switch(event.name) {
370
- case 'message.nack':
371
- case 'message.undelivered':
372
- undelivered = event.name;
373
- break;
374
- }
381
+ channelQ.nack(channelMessage, false, false);
382
+ this.nack(msg, false, requeue);
383
+ }
384
+
385
+ function nackAllUpToMessage() {
386
+ const msg = message[kSmqp];
387
+ if (!channelMessage || !msg.pending) {
388
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
375
389
  }
376
390
 
377
- function confirmCallback() {
378
- broker.off('message.*', consumerTag);
379
- switch (undelivered) {
380
- case 'message.nack':
381
- return callback(new Error('message nacked'));
382
- case 'message.undelivered':
383
- throw callback(new Error('message undelivered'));
384
- default:
385
- return callback(null, true);
386
- }
391
+ const brokerMessages = allUpToDeliveryTag(channelQ, deliveryTag, 'nack', false, false);
392
+ for (const brokerMessage of brokerMessages) {
393
+ brokerMessage.nack(false, requeue);
387
394
  }
388
395
 
389
- return [options, confirmCallback];
396
+ channelMessage.nack(false, false);
397
+ this.nack(msg, false, requeue);
390
398
  }
391
-
392
- function callBroker(fn, ...args) {
393
- let [poppedCb] = args.slice(-1);
394
- if (typeof poppedCb === 'function') args.splice(-1);
395
- else poppedCb = null;
396
-
397
- if (connection._closed) throw new FakeAmqpError('Connection is closed', 504);
398
- if (closed) throw new Error('Channel is closed');
399
-
400
- return new Promise((resolve, reject) => {
401
- try {
402
- const result = fn.call(broker, ...args);
403
- if (poppedCb) poppedCb(null, result);
404
- return resolve(result);
405
- } catch (err) {
406
- if (err._killConnection) connection.close();
407
- else if (err._killChannel) closed = true;
408
- if (!poppedCb) return reject(err);
409
- poppedCb(err);
410
- return resolve();
411
- }
412
- });
399
+ }
400
+ nackAll(requeue = true) {
401
+ const channelQ = this._channelQueue;
402
+ let msg;
403
+ const brokerMessages = [];
404
+ while ((msg = channelQ.get())) {
405
+ brokerMessages.push(msg.content[kSmqp]);
406
+ msg.reject(false);
413
407
  }
414
408
 
415
- function emitReturn({fields, content, properties}) {
416
- process.nextTick(() => {
417
- emitter.emit('return', {fields, content, properties});
418
- });
409
+ for (const brokerMessage of brokerMessages) {
410
+ brokerMessage.reject(requeue);
419
411
  }
420
412
  }
413
+ prefetch(val, isChannelPrefetch) {
414
+ if (this.connection._version < 3.3) {
415
+ if (isChannelPrefetch !== undefined) {
416
+ return this.connection.close();
417
+ }
418
+ this[kChannelPrefetch] = val;
419
+ } else {
420
+ if (isChannelPrefetch) {
421
+ this[kChannelPrefetch] = val;
422
+ } else {
423
+ this[kPrefetch] = val;
424
+ }
425
+ }
426
+ }
427
+ _callBroker(fn, ...args) {
428
+ let [ poppedCb ] = args.slice(-1);
429
+ if (typeof poppedCb === 'function') args.splice(-1);
430
+ else poppedCb = null;
431
+
432
+ if (this.connection._closed) throw new FakeAmqpError('Connection is closed', 504);
433
+ if (this[kClosed]) throw new Error('Channel is closed');
434
+
435
+ return new Promise((resolve, reject) => {
436
+ try {
437
+ const result = fn.call(this._broker, ...args);
438
+ if (poppedCb) poppedCb(null, result);
439
+ return resolve(result);
440
+ } catch (err) {
441
+ if (err._killConnection) this.connection.close();
442
+ else if (err._killChannel) this._teardown();
443
+ if (err._emit) this.emit('error', err);
444
+ if (!poppedCb) return reject(err);
445
+ poppedCb(err);
446
+ return resolve();
447
+ }
448
+ });
449
+ }
450
+ _emitReturn({ fields, content, properties }) {
451
+ process.nextTick(() => {
452
+ this.emit('return', { fields, content, properties });
453
+ });
454
+ }
455
+ _createChannelMessage(smqpMessage, noAck) {
456
+ const deliveryTag = this._broker._getNextDeliveryTag();
457
+ const consumeMessage = new Message(smqpMessage, deliveryTag);
458
+ if (!noAck) {
459
+ const channelQ = this._channelQueue;
460
+ channelQ.queueMessage(consumeMessage.fields, consumeMessage);
461
+ }
462
+ return consumeMessage;
463
+ }
464
+ _teardown() {
465
+ this[kClosed] = true;
466
+ const channelName = this._channelName;
467
+ const broker = this._broker;
468
+ const channelConsumers = broker._getChannelConsumers(channelName);
469
+ channelConsumers.forEach((c) => broker.cancel(c.consumerTag));
470
+
471
+ let msg;
472
+ while ((msg = this._channelQueue.get())) {
473
+ msg.content[kSmqp].reject(true);
474
+ msg.reject(false);
475
+ }
476
+
477
+ broker.off('return', this._emitReturn);
478
+ }
479
+ _calculateChannelCapacity(consumerCapacity) {
480
+ const channelPrefetch = this[kChannelPrefetch];
481
+ if (channelPrefetch === Infinity) return consumerCapacity;
482
+
483
+ const channelCapacity = channelPrefetch - this._channelQueue.messageCount;
484
+
485
+ let capacity = consumerCapacity;
486
+ if (channelCapacity <= 0) capacity = 0;
487
+ else if (channelCapacity < capacity) capacity = channelCapacity;
488
+ return capacity;
489
+ }
490
+ }
491
+
492
+ export class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
493
+ publish(exchange, routingKey, content, options, callback) {
494
+ if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
495
+ if (exchange === '') return this.sendToQueue(routingKey, content, options, callback);
496
+
497
+ const args = [ this._broker.publish, exchange, routingKey, content ];
498
+
499
+ args.push(...addConfirmCallback(this._broker, options, callback));
500
+
501
+ this.checkExchange(exchange).then(() => {
502
+ return this._callBroker(...args);
503
+ }).catch((err) => {
504
+ this.emit('error', err);
505
+ });
506
+
507
+ return true;
508
+ }
509
+ sendToQueue(queue, content, options, callback) {
510
+ if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
511
+
512
+ const args = [ this._broker.sendToQueue, queue, content ];
513
+
514
+ args.push(...addConfirmCallback(this._broker, options, callback));
515
+
516
+ this.checkQueue(queue).then(() => {
517
+ return this._callBroker(...args);
518
+ }).catch((err) => {
519
+ this.emit('error', err);
520
+ });
521
+
522
+ return true;
523
+ }
524
+ }
525
+
526
+ export class FakeAmqplibConnection extends EventEmitter {
527
+ constructor(broker, version, amqpUrl) {
528
+ super();
529
+ this[kClosed] = false;
530
+ this._channels = [];
531
+ this._url = normalizeAmqpUrl(amqpUrl);
532
+ this._id = generateId();
533
+ this._broker = broker;
534
+ this._version = version;
535
+ }
536
+ get _closed() {
537
+ return this[kClosed];
538
+ }
539
+ get connection() {
540
+ return {
541
+ serverProperties: {
542
+ host: this._url.host,
543
+ product: 'RabbitMQ',
544
+ version: `${this._version.toString()}.0`,
545
+ platform: 'OS',
546
+ copyright: 'MIT',
547
+ information: 'fake',
548
+ },
549
+ };
550
+ }
551
+ createChannel(...args) {
552
+ const callback = args.slice(-1)[0];
553
+ if (this[kClosed]) return resolveOrCallback(callback, new FakeAmqpError('Connection closed: 504', 504));
554
+
555
+ const channel = new FakeAmqplibChannel(this._broker, this);
556
+ this._channels.push(channel);
557
+ return resolveOrCallback(callback, null, channel);
558
+ }
559
+ createConfirmChannel(...args) {
560
+ if (this[kClosed]) return resolveOrCallback(args.slice(-1)[0], new FakeAmqpError('Connection closed: 504', 504));
561
+
562
+ const channel = new FakeAmqplibConfirmChannel(this._broker, this);
563
+ this._channels.push(channel);
564
+ return resolveOrCallback(args.slice(-1)[0], null, channel);
565
+ }
566
+ close(...args) {
567
+ if (this[kClosed]) return resolveOrCallback(args.slice(-1)[0]);
568
+ this[kClosed] = true;
569
+
570
+ this._channels.splice(0).forEach((channel) => channel.close());
571
+
572
+ this.emit('close');
573
+
574
+ return resolveOrCallback(args.slice(-1)[0]);
575
+ }
576
+ }
577
+
578
+ export function FakeAmqplib(minorVersion = '3.5') {
579
+ if (!(this instanceof FakeAmqplib)) {
580
+ return new FakeAmqplib(minorVersion);
581
+ }
582
+
583
+ this.version = Number(minorVersion);
584
+ this.connections = [];
585
+
586
+ this.connect = this.connect.bind(this);
587
+ this.connectSync = this.connectSync.bind(this);
588
+ this.resetMock = this.resetMock.bind(this);
589
+ this.setVersion = this.setVersion.bind(this);
421
590
  }
422
591
 
592
+ FakeAmqplib.prototype.connect = function fakeConnect(amqpUrl, ...args) {
593
+ const connection = this.connectSync(amqpUrl, ...args);
594
+ return resolveOrCallback(args.slice(-1)[0], null, connection);
595
+ };
596
+
597
+ FakeAmqplib.prototype.connectSync = function fakeConnectSync(amqpUrl, ...args) {
598
+ const { _broker } = this.connections.find((conn) => compareConnectionString(conn._url, amqpUrl)) || {};
599
+ const broker = _broker || new AmqplibBroker(this);
600
+ const connection = new FakeAmqplibConnection(broker, this.version, amqpUrl, ...args);
601
+
602
+ const connections = this.connections;
603
+
604
+ connections.push(connection);
605
+
606
+ connection.once('close', () => {
607
+ const idx = connections.indexOf(connection);
608
+ if (idx > -1) connections.splice(idx, 1);
609
+ });
610
+
611
+ return connection;
612
+ };
613
+
614
+ FakeAmqplib.prototype.resetMock = function fakeResetMock() {
615
+ for (const connection of this.connections.splice(0)) {
616
+ connection._broker.reset();
617
+ }
618
+ };
619
+
620
+ FakeAmqplib.prototype.setVersion = function fakeSetVersion(minorVersion) {
621
+ const n = Number(minorVersion);
622
+ if (!isNaN(n)) this.version = n;
623
+ };
624
+
423
625
  function resolveOrCallback(optionalCb, err, ...args) {
424
626
  if (typeof optionalCb === 'function') optionalCb(err, ...args);
425
627
  if (err) return Promise.reject(err);
@@ -437,15 +639,8 @@ function compareConnectionString(url1, url2) {
437
639
  return parsedUrl1.host === parsedUrl2.host && parsedUrl1.pathname === parsedUrl2.pathname;
438
640
  }
439
641
 
440
- function Message(smqpMessage) {
441
- this[smqpSymbol] = smqpMessage;
442
- this.content = smqpMessage.content;
443
- this.fields = smqpMessage.fields;
444
- this.properties = smqpMessage.properties;
445
- }
446
-
447
642
  function normalizeAmqpUrl(url) {
448
- if (!url) return url = new URL('amqp://localhost:5672/');
643
+ if (!url) return new URL('amqp://localhost:5672/');
449
644
  if (typeof url === 'string') url = new URL(url);
450
645
 
451
646
  if (!(url instanceof URL)) {
@@ -460,7 +655,7 @@ function normalizeAmqpUrl(url) {
460
655
  } = url;
461
656
  let auth = username;
462
657
  if (auth && password) {
463
- auth += ':' + password;
658
+ auth += `:${password}`;
464
659
  }
465
660
  url = new URL(urlFormat({
466
661
  protocol,
@@ -486,3 +681,71 @@ function normalizeAmqpUrl(url) {
486
681
  if (!url.pathname) url.pathname = '/';
487
682
  return url;
488
683
  }
684
+
685
+ function addConfirmCallback(broker, options, callback) {
686
+ const confirm = `msg.${generateId()}`;
687
+ const consumerTag = `ct-${confirm}`;
688
+ options = { ...options, confirm };
689
+
690
+ broker.on('message.*', onConsumeMessage, { consumerTag });
691
+
692
+ let undelivered;
693
+ function onConsumeMessage(event) {
694
+ switch (event.name) {
695
+ case 'message.nack':
696
+ case 'message.undelivered':
697
+ undelivered = event.name;
698
+ break;
699
+ }
700
+ }
701
+
702
+ function confirmCallback() {
703
+ broker.off('message.*', consumerTag);
704
+ switch (undelivered) {
705
+ case 'message.nack':
706
+ return callback(new Error('message nacked'));
707
+ case 'message.undelivered':
708
+ throw callback(new Error('message undelivered'));
709
+ default:
710
+ return callback(null, true);
711
+ }
712
+ }
713
+
714
+ return [ options, confirmCallback ];
715
+ }
716
+
717
+ function allUpToDeliveryTag(q, deliveryTag, op, ...args) {
718
+ const brokerMessages = [];
719
+
720
+ const consumer = q.consume((_, cmsg) => {
721
+ const msgDeliveryTag = cmsg.fields.deliveryTag;
722
+ if (msgDeliveryTag >= deliveryTag) {
723
+ return q.cancel(cmsg.fields.consumerTag);
724
+ }
725
+ brokerMessages.push(cmsg.content[kSmqp]);
726
+ cmsg[op](...args);
727
+ }, { prefetch: Infinity });
728
+
729
+ consumer.cancel();
730
+
731
+ return brokerMessages;
732
+ }
733
+
734
+ const defaultFake = new FakeAmqplib('3.5');
735
+ export const connections = defaultFake.connections;
736
+
737
+ export function connect(amqpUrl, ...args) {
738
+ return defaultFake.connect(amqpUrl, ...args);
739
+ }
740
+
741
+ export function connectSync(amqpUrl, ...args) {
742
+ return defaultFake.connectSync(amqpUrl, ...args);
743
+ }
744
+
745
+ export function resetMock() {
746
+ return defaultFake.resetMock();
747
+ }
748
+
749
+ export function setVersion(minorVersion) {
750
+ return defaultFake.setVersion(minorVersion);
751
+ }