@onify/fake-amqplib 2.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/README.md CHANGED
@@ -14,7 +14,7 @@ Mocked version of https://www.npmjs.com/package/amqplib.
14
14
 
15
15
  ## RabbitMQ versions
16
16
 
17
- Some behaviour differs between versions. To specify your version of RabbitMQ you can call `setVersion(minorVersionFloatOrString)`. Default version is 3.5.
17
+ RabbitMQ behaviour differs between versions. To specify your version of RabbitMQ you can call `setVersion(minorVersionFloatOrString)`. Default version is 3.5.
18
18
 
19
19
  Example:
20
20
  ```js
@@ -26,7 +26,7 @@ var fakeAmqp = require('@onify/fake-amqplib');
26
26
  const conn2 = await fakeAmqp.connect('amqp://rabbit2-2');
27
27
 
28
28
  fakeAmqp.setVersion('3.2');
29
- const conn3 = await fakeAmqp.connect('amqp://rabbit3-1');
29
+ const conn3 = await fakeAmqp.connect('amqp://rabbit3-2');
30
30
 
31
31
  fakeAmqp.setVersion('3.7');
32
32
  const conn37 = await fakeAmqp.connect('amqp://rabbit3-7');
@@ -35,7 +35,7 @@ var fakeAmqp = require('@onify/fake-amqplib');
35
35
 
36
36
  ## Mocking amqplib
37
37
 
38
- You might want to override `amqplib` with `@onify/fake-amqplib` in tests. This can be done this way:
38
+ You might want to override `amqplib` with `@onify/fake-amqplib` in tests. This can be done in a number of ways.
39
39
 
40
40
  ### CommonJS
41
41
 
@@ -72,7 +72,7 @@ Example on how to mock amqplib import when working with modules.
72
72
 
73
73
  **[Quibble](https://www.npmjs.com/package/quibble) mocha example**
74
74
 
75
- Both amqplib and fake-amqplib have to be mocked if reset mock is used during testing.
75
+ Both amqplib and fake-amqplib have to be quibbled if reset mock is used during testing.
76
76
 
77
77
  _test/setup.js_
78
78
  ```js
package/index.d.ts CHANGED
@@ -1,28 +1,28 @@
1
1
  /// <reference types="amqplib" />
2
2
  /// <reference types="node" />
3
3
 
4
- import { Options, Connection, Channel, credentials } from "amqplib";
4
+ import { Options, Connection, Channel } from "amqplib";
5
5
  import { EventEmitter } from "events";
6
6
  import { Broker } from 'smqp';
7
7
 
8
8
  export interface FakeAmqplibChannel extends Channel {
9
+ /** Channel name and identifier, for faking purposes */
9
10
  _channelName: string;
10
- _version: number;
11
11
  _broker: Broker;
12
+ _version: number;
12
13
  new(broker: Broker, connection: FakeAmqplibConnection): FakeAmqplibChannel;
13
14
  get _closed(): boolean;
14
- get _emitter(): EventEmitter;
15
15
  }
16
16
 
17
17
  export interface FakeAmqplibConnection extends Connection {
18
18
  _channels: FakeAmqplibChannel[];
19
19
  _url: URL;
20
+ /** Connection identifier, for faking purposes */
20
21
  _id: string;
21
22
  _broker: Broker;
22
23
  _version: number;
23
24
  new(broker: Broker, version: number, amqpUrl: string, options?: any): FakeAmqplibConnection;
24
25
  get _closed(): boolean;
25
- get _emitter(): EventEmitter;
26
26
  }
27
27
 
28
28
  interface SocketOptions {
package/index.js CHANGED
@@ -4,8 +4,26 @@ import { format as urlFormat } from 'url';
4
4
 
5
5
  const kSmqp = Symbol.for('smqp');
6
6
  const kClosed = Symbol.for('closed');
7
- const kEmitter = Symbol.for('event emitter');
7
+ const kDeliveryTag = Symbol.for('channel delivery tag');
8
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
+ }
9
27
 
10
28
  class FakeAmqpError extends Error {
11
29
  constructor(message, code, killChannel, killConnection) {
@@ -22,23 +40,41 @@ class FakeAmqpNotFoundError extends FakeAmqpError {
22
40
  }
23
41
  }
24
42
 
25
- export class FakeAmqplibChannel {
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 {
26
60
  constructor(broker, connection) {
61
+ super();
27
62
  this.connection = connection;
28
63
 
29
64
  this[kPrefetch] = 10000;
65
+ this[kChannelPrefetch] = Infinity;
30
66
  this[kClosed] = false;
31
- this[kEmitter] = new EventEmitter();
32
- this._channelName = `channel-${generateId()}`;
67
+ const channelName = this._channelName = `channel-${generateId()}`;
33
68
  this._version = connection._version;
34
69
  this._broker = broker;
35
70
 
71
+ this._channelQueue = broker.assertQueue(`#${channelName}`);
36
72
  this._emitReturn = this._emitReturn.bind(this);
37
73
 
38
74
  broker.on('return', this._emitReturn);
39
- }
40
- get _emitter() {
41
- return this[kEmitter];
75
+
76
+ this._createChannelMessage = this._createChannelMessage.bind(this);
77
+ this._calculateChannelCapacity = this._calculateChannelCapacity.bind(this);
42
78
  }
43
79
  get _closed() {
44
80
  return this[kClosed];
@@ -104,14 +140,16 @@ export class FakeAmqplibChannel {
104
140
  }
105
141
  get(queue, ...args) {
106
142
  const connPath = this.connection._url.pathname;
143
+ const createMessage = this._createChannelMessage;
107
144
  return this._callBroker(getMessage, ...args);
108
145
 
109
146
  function getMessage(...getargs) {
110
147
  const q = this.getQueue(queue);
111
- if (!q) throw new FakeAmqpNotFoundError('queue', queue, connPath._url.pathname);
148
+ if (!q) throw new FakeAmqpNotFoundError('queue', queue, connPath);
112
149
  const msg = q.get(...getargs) || false;
113
150
  if (!msg) return msg;
114
- return new Message(msg);
151
+
152
+ return createMessage(msg, args[0]?.noAck);
115
153
  }
116
154
  }
117
155
  deleteExchange(exchange, ...args) {
@@ -145,7 +183,7 @@ export class FakeAmqplibChannel {
145
183
  this.checkExchange(exchange).then(() => {
146
184
  return this._callBroker(...args);
147
185
  }).catch((err) => {
148
- this[kEmitter].emit('error', err);
186
+ this.emit('error', err);
149
187
  });
150
188
 
151
189
  return true;
@@ -170,7 +208,7 @@ export class FakeAmqplibChannel {
170
208
  this.checkQueue(queue).then(() => {
171
209
  return this._callBroker(...args);
172
210
  }).catch((err) => {
173
- this[kEmitter].emit('error', err);
211
+ this.emit('error', err);
174
212
  });
175
213
 
176
214
  return true;
@@ -214,12 +252,14 @@ export class FakeAmqplibChannel {
214
252
  }
215
253
  consume(queue, onMessage, options = {}, callback) {
216
254
  const { _id: connId, _url: connUrl } = this.connection;
255
+ const createMessage = this._createChannelMessage;
256
+ const calculateCapacity = this._calculateChannelCapacity;
217
257
  const channelName = this._channelName;
218
258
  const prefetch = this[kPrefetch];
219
259
 
220
- return this._callBroker(check, callback);
260
+ return this._callBroker(consume, callback);
221
261
 
222
- function check() {
262
+ function consume() {
223
263
  const q = queue && this.getQueue(queue);
224
264
  if (!q) {
225
265
  throw new FakeAmqpNotFoundError('queue', queue, connUrl.pathname);
@@ -229,16 +269,25 @@ export class FakeAmqplibChannel {
229
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);
230
270
  }
231
271
 
232
- const { consumerTag } = this.consume(queue, onMessage && handler, {
272
+ const consumer = this.consume(queue, onMessage && handler, {
233
273
  ...options,
234
274
  channelName,
235
- prefetch,
275
+ prefetch: calculateCapacity(prefetch),
276
+ _consumerPrefetch: prefetch,
236
277
  });
237
- return { consumerTag };
278
+
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
+ });
285
+
286
+ return { consumerTag: consumer.consumerTag };
238
287
  }
239
288
 
240
289
  function handler(_, msg) {
241
- onMessage(new Message(msg));
290
+ onMessage(createMessage(msg, options.noAck));
242
291
  }
243
292
  }
244
293
  cancel(consumerTag, ...args) {
@@ -246,48 +295,134 @@ export class FakeAmqplibChannel {
246
295
  }
247
296
  close(callback) {
248
297
  if (this[kClosed]) return;
249
- const channelName = this._channelName;
250
- const broker = this._broker;
251
-
252
- broker.off('return', this._emitReturn);
253
- const channelConsumers = broker.getConsumers().filter((f) => f.options.channelName === channelName);
254
- channelConsumers.forEach((c) => broker.cancel(c.consumerTag));
255
- this[kClosed] = true;
256
- this[kEmitter].emit('close');
298
+ this._teardown();
299
+ this.emit('close');
257
300
  return resolveOrCallback(callback);
258
301
  }
259
- ack(message, ...args) {
260
- this._broker.ack(message[kSmqp], ...args);
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
+ }
315
+
316
+ channelQ.ack(channelMessage, false);
317
+ this.ack(msg, false);
318
+ }
319
+
320
+ function ackAllUpToMessage() {
321
+ const msg = message[kSmqp];
322
+ if (!channelMessage || !msg.pending) {
323
+ throw new FakeAmqpUnknownDeliveryTag(message.fields.deliveryTag);
324
+ }
325
+
326
+ const brokerMessages = allUpToDeliveryTag(channelQ, deliveryTag, 'ack', false);
327
+ for (const brokerMessage of brokerMessages) {
328
+ brokerMessage.ack(false);
329
+ }
330
+
331
+ channelQ.ack(channelMessage, false);
332
+ this.ack(msg, false);
333
+ }
261
334
  }
262
335
  ackAll() {
263
- const broker = this._broker;
264
- const channelName = this._channelName;
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
+ }
265
343
 
266
- const consumers = broker.getConsumers().filter(({ options }) => options.channelName === channelName);
267
- consumers.forEach((c) => broker.getConsumer(c.consumerTag).ackAll());
268
- }
269
- nack(message, ...args) {
270
- if (this.connection._version >= 2.3) throw new Error(`Nack is not implemented in versions before 2.3 (${this.connection._version})`);
271
- return this._broker.nack(message[kSmqp], ...args);
272
- }
273
- reject(message, ...args) {
274
- this._broker.reject(message[kSmqp], ...args);
344
+ for (const brokerMessage of brokerMessages) {
345
+ brokerMessage.ack();
346
+ }
275
347
  }
276
- nackAll(requeue = false) {
277
- const broker = this._broker;
278
- const channelName = this._channelName;
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;
352
+
353
+ this._callBroker(rejectMessage);
279
354
 
280
- const consumers = broker.getConsumers().filter(({ options }) => options.channelName === channelName);
281
- consumers.forEach((c) => broker.getConsumer(c.consumerTag).nackAll(requeue));
355
+ function rejectMessage() {
356
+ const msg = message[kSmqp];
357
+ if (!channelMessage || !msg.pending) {
358
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
359
+ }
360
+
361
+ channelQ.reject(channelMessage, false);
362
+ this.reject(msg, requeue);
363
+ }
282
364
  }
283
- prefetch(val) {
284
- this[kPrefetch] = val;
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})`);
367
+
368
+ const deliveryTag = message.fields.deliveryTag;
369
+ const channelMessage = this._broker._getMessageByDeliveryTag(this._channelQueue.name, deliveryTag);
370
+ const channelQ = this._channelQueue;
371
+
372
+ if (!allUpTo) this._callBroker(nackMessage);
373
+ else this._callBroker(nackAllUpToMessage);
374
+
375
+ function nackMessage() {
376
+ const msg = message[kSmqp];
377
+ if (!channelMessage || !msg.pending) {
378
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
379
+ }
380
+
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);
389
+ }
390
+
391
+ const brokerMessages = allUpToDeliveryTag(channelQ, deliveryTag, 'nack', false, false);
392
+ for (const brokerMessage of brokerMessages) {
393
+ brokerMessage.nack(false, requeue);
394
+ }
395
+
396
+ channelMessage.nack(false, false);
397
+ this.nack(msg, false, requeue);
398
+ }
285
399
  }
286
- on(...args) {
287
- return this[kEmitter].on(...args);
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);
407
+ }
408
+
409
+ for (const brokerMessage of brokerMessages) {
410
+ brokerMessage.reject(requeue);
411
+ }
288
412
  }
289
- once(...args) {
290
- return this[kEmitter].once(...args);
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
+ }
291
426
  }
292
427
  _callBroker(fn, ...args) {
293
428
  let [ poppedCb ] = args.slice(-1);
@@ -304,7 +439,8 @@ export class FakeAmqplibChannel {
304
439
  return resolve(result);
305
440
  } catch (err) {
306
441
  if (err._killConnection) this.connection.close();
307
- else if (err._killChannel) this[kClosed] = true;
442
+ else if (err._killChannel) this._teardown();
443
+ if (err._emit) this.emit('error', err);
308
444
  if (!poppedCb) return reject(err);
309
445
  poppedCb(err);
310
446
  return resolve();
@@ -313,9 +449,44 @@ export class FakeAmqplibChannel {
313
449
  }
314
450
  _emitReturn({ fields, content, properties }) {
315
451
  process.nextTick(() => {
316
- this[kEmitter].emit('return', { fields, content, properties });
452
+ this.emit('return', { fields, content, properties });
317
453
  });
318
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
+ }
319
490
  }
320
491
 
321
492
  export class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
@@ -330,7 +501,7 @@ export class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
330
501
  this.checkExchange(exchange).then(() => {
331
502
  return this._callBroker(...args);
332
503
  }).catch((err) => {
333
- this[kEmitter].emit('error', err);
504
+ this.emit('error', err);
334
505
  });
335
506
 
336
507
  return true;
@@ -345,16 +516,16 @@ export class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
345
516
  this.checkQueue(queue).then(() => {
346
517
  return this._callBroker(...args);
347
518
  }).catch((err) => {
348
- this[kEmitter].emit('error', err);
519
+ this.emit('error', err);
349
520
  });
350
521
 
351
522
  return true;
352
523
  }
353
524
  }
354
525
 
355
- export class FakeAmqplibConnection {
526
+ export class FakeAmqplibConnection extends EventEmitter {
356
527
  constructor(broker, version, amqpUrl) {
357
- this[kEmitter] = new EventEmitter();
528
+ super();
358
529
  this[kClosed] = false;
359
530
  this._channels = [];
360
531
  this._url = normalizeAmqpUrl(amqpUrl);
@@ -365,9 +536,6 @@ export class FakeAmqplibConnection {
365
536
  get _closed() {
366
537
  return this[kClosed];
367
538
  }
368
- get _emitter() {
369
- return this[kEmitter];
370
- }
371
539
  get connection() {
372
540
  return {
373
541
  serverProperties: {
@@ -401,16 +569,10 @@ export class FakeAmqplibConnection {
401
569
 
402
570
  this._channels.splice(0).forEach((channel) => channel.close());
403
571
 
404
- this[kEmitter].emit('close');
572
+ this.emit('close');
405
573
 
406
574
  return resolveOrCallback(args.slice(-1)[0]);
407
575
  }
408
- on(...args) {
409
- return this[kEmitter].on(...args);
410
- }
411
- once(...args) {
412
- return this[kEmitter].once(...args);
413
- }
414
576
  }
415
577
 
416
578
  export function FakeAmqplib(minorVersion = '3.5') {
@@ -434,14 +596,14 @@ FakeAmqplib.prototype.connect = function fakeConnect(amqpUrl, ...args) {
434
596
 
435
597
  FakeAmqplib.prototype.connectSync = function fakeConnectSync(amqpUrl, ...args) {
436
598
  const { _broker } = this.connections.find((conn) => compareConnectionString(conn._url, amqpUrl)) || {};
437
- const broker = _broker || new Broker(this);
599
+ const broker = _broker || new AmqplibBroker(this);
438
600
  const connection = new FakeAmqplibConnection(broker, this.version, amqpUrl, ...args);
439
601
 
440
602
  const connections = this.connections;
441
603
 
442
604
  connections.push(connection);
443
605
 
444
- connection._emitter.once('close', () => {
606
+ connection.once('close', () => {
445
607
  const idx = connections.indexOf(connection);
446
608
  if (idx > -1) connections.splice(idx, 1);
447
609
  });
@@ -477,13 +639,6 @@ function compareConnectionString(url1, url2) {
477
639
  return parsedUrl1.host === parsedUrl2.host && parsedUrl1.pathname === parsedUrl2.pathname;
478
640
  }
479
641
 
480
- function Message(smqpMessage) {
481
- this[kSmqp] = smqpMessage;
482
- this.content = smqpMessage.content;
483
- this.fields = smqpMessage.fields;
484
- this.properties = smqpMessage.properties;
485
- }
486
-
487
642
  function normalizeAmqpUrl(url) {
488
643
  if (!url) return new URL('amqp://localhost:5672/');
489
644
  if (typeof url === 'string') url = new URL(url);
@@ -559,6 +714,23 @@ function addConfirmCallback(broker, options, callback) {
559
714
  return [ options, confirmCallback ];
560
715
  }
561
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
+
562
734
  const defaultFake = new FakeAmqplib('3.5');
563
735
  export const connections = defaultFake.connections;
564
736
 
package/main.cjs CHANGED
@@ -6,8 +6,26 @@ var url = require('url');
6
6
 
7
7
  const kSmqp = Symbol.for('smqp');
8
8
  const kClosed = Symbol.for('closed');
9
- const kEmitter = Symbol.for('event emitter');
9
+ const kDeliveryTag = Symbol.for('channel delivery tag');
10
10
  const kPrefetch = Symbol.for('prefetch');
11
+ const kChannelPrefetch = Symbol.for('channel prefetch');
12
+
13
+ class AmqplibBroker extends smqp.Broker {
14
+ constructor(...args) {
15
+ super(...args);
16
+ this[kDeliveryTag] = 0;
17
+ }
18
+ _getNextDeliveryTag() {
19
+ return ++this[kDeliveryTag];
20
+ }
21
+ _getMessageByDeliveryTag(queue, deliveryTag) {
22
+ const q = this.getQueue(queue);
23
+ return q.messages.find((m) => m.fields.deliveryTag === deliveryTag);
24
+ }
25
+ _getChannelConsumers(channelName) {
26
+ return this.getConsumers().filter((f) => f.options.channelName === channelName);
27
+ }
28
+ }
11
29
 
12
30
  class FakeAmqpError extends Error {
13
31
  constructor(message, code, killChannel, killConnection) {
@@ -24,23 +42,41 @@ class FakeAmqpNotFoundError extends FakeAmqpError {
24
42
  }
25
43
  }
26
44
 
27
- class FakeAmqplibChannel {
45
+ class FakeAmqpUnknownDeliveryTag extends FakeAmqpError {
46
+ constructor(deliveryTag) {
47
+ super(`Channel closed by server: 406 (PRECONDITION-FAILED) with message "PRECONDITION_FAILED - unknown delivery tag ${deliveryTag}`, 406, true, false);
48
+ }
49
+ get _emit() {
50
+ return true;
51
+ }
52
+ }
53
+
54
+ function Message(smqpMessage, deliveryTag) {
55
+ this[kSmqp] = smqpMessage;
56
+ this.fields = { ...smqpMessage.fields, deliveryTag };
57
+ this.content = Buffer.from(smqpMessage.content);
58
+ this.properties = { ...smqpMessage.properties };
59
+ }
60
+
61
+ class FakeAmqplibChannel extends events.EventEmitter {
28
62
  constructor(broker, connection) {
63
+ super();
29
64
  this.connection = connection;
30
65
 
31
66
  this[kPrefetch] = 10000;
67
+ this[kChannelPrefetch] = Infinity;
32
68
  this[kClosed] = false;
33
- this[kEmitter] = new events.EventEmitter();
34
- this._channelName = `channel-${generateId()}`;
69
+ const channelName = this._channelName = `channel-${generateId()}`;
35
70
  this._version = connection._version;
36
71
  this._broker = broker;
37
72
 
73
+ this._channelQueue = broker.assertQueue(`#${channelName}`);
38
74
  this._emitReturn = this._emitReturn.bind(this);
39
75
 
40
76
  broker.on('return', this._emitReturn);
41
- }
42
- get _emitter() {
43
- return this[kEmitter];
77
+
78
+ this._createChannelMessage = this._createChannelMessage.bind(this);
79
+ this._calculateChannelCapacity = this._calculateChannelCapacity.bind(this);
44
80
  }
45
81
  get _closed() {
46
82
  return this[kClosed];
@@ -106,14 +142,16 @@ class FakeAmqplibChannel {
106
142
  }
107
143
  get(queue, ...args) {
108
144
  const connPath = this.connection._url.pathname;
145
+ const createMessage = this._createChannelMessage;
109
146
  return this._callBroker(getMessage, ...args);
110
147
 
111
148
  function getMessage(...getargs) {
112
149
  const q = this.getQueue(queue);
113
- if (!q) throw new FakeAmqpNotFoundError('queue', queue, connPath._url.pathname);
150
+ if (!q) throw new FakeAmqpNotFoundError('queue', queue, connPath);
114
151
  const msg = q.get(...getargs) || false;
115
152
  if (!msg) return msg;
116
- return new Message(msg);
153
+
154
+ return createMessage(msg, args[0]?.noAck);
117
155
  }
118
156
  }
119
157
  deleteExchange(exchange, ...args) {
@@ -147,7 +185,7 @@ class FakeAmqplibChannel {
147
185
  this.checkExchange(exchange).then(() => {
148
186
  return this._callBroker(...args);
149
187
  }).catch((err) => {
150
- this[kEmitter].emit('error', err);
188
+ this.emit('error', err);
151
189
  });
152
190
 
153
191
  return true;
@@ -172,7 +210,7 @@ class FakeAmqplibChannel {
172
210
  this.checkQueue(queue).then(() => {
173
211
  return this._callBroker(...args);
174
212
  }).catch((err) => {
175
- this[kEmitter].emit('error', err);
213
+ this.emit('error', err);
176
214
  });
177
215
 
178
216
  return true;
@@ -216,12 +254,14 @@ class FakeAmqplibChannel {
216
254
  }
217
255
  consume(queue, onMessage, options = {}, callback) {
218
256
  const { _id: connId, _url: connUrl } = this.connection;
257
+ const createMessage = this._createChannelMessage;
258
+ const calculateCapacity = this._calculateChannelCapacity;
219
259
  const channelName = this._channelName;
220
260
  const prefetch = this[kPrefetch];
221
261
 
222
- return this._callBroker(check, callback);
262
+ return this._callBroker(consume, callback);
223
263
 
224
- function check() {
264
+ function consume() {
225
265
  const q = queue && this.getQueue(queue);
226
266
  if (!q) {
227
267
  throw new FakeAmqpNotFoundError('queue', queue, connUrl.pathname);
@@ -231,16 +271,25 @@ class FakeAmqplibChannel {
231
271
  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);
232
272
  }
233
273
 
234
- const { consumerTag } = this.consume(queue, onMessage && handler, {
274
+ const consumer = this.consume(queue, onMessage && handler, {
235
275
  ...options,
236
276
  channelName,
237
- prefetch,
277
+ prefetch: calculateCapacity(prefetch),
278
+ _consumerPrefetch: prefetch,
238
279
  });
239
- return { consumerTag };
280
+
281
+ const capacityProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(consumer), 'capacity');
282
+ Object.defineProperty(consumer, 'capacity', {
283
+ get() {
284
+ return calculateCapacity(capacityProp.get.call(this));
285
+ },
286
+ });
287
+
288
+ return { consumerTag: consumer.consumerTag };
240
289
  }
241
290
 
242
291
  function handler(_, msg) {
243
- onMessage(new Message(msg));
292
+ onMessage(createMessage(msg, options.noAck));
244
293
  }
245
294
  }
246
295
  cancel(consumerTag, ...args) {
@@ -248,48 +297,134 @@ class FakeAmqplibChannel {
248
297
  }
249
298
  close(callback) {
250
299
  if (this[kClosed]) return;
251
- const channelName = this._channelName;
252
- const broker = this._broker;
253
-
254
- broker.off('return', this._emitReturn);
255
- const channelConsumers = broker.getConsumers().filter((f) => f.options.channelName === channelName);
256
- channelConsumers.forEach((c) => broker.cancel(c.consumerTag));
257
- this[kClosed] = true;
258
- this[kEmitter].emit('close');
300
+ this._teardown();
301
+ this.emit('close');
259
302
  return resolveOrCallback(callback);
260
303
  }
261
- ack(message, ...args) {
262
- this._broker.ack(message[kSmqp], ...args);
304
+ ack(message, allUpTo) {
305
+ const deliveryTag = message.fields.deliveryTag;
306
+ const channelMessage = this._broker._getMessageByDeliveryTag(this._channelQueue.name, deliveryTag);
307
+ const channelQ = this._channelQueue;
308
+
309
+ if (!allUpTo) this._callBroker(ackMessage);
310
+ else this._callBroker(ackAllUpToMessage);
311
+
312
+ function ackMessage() {
313
+ const msg = message[kSmqp];
314
+ if (!channelMessage || !msg.pending) {
315
+ throw new FakeAmqpUnknownDeliveryTag(message.fields.deliveryTag);
316
+ }
317
+
318
+ channelQ.ack(channelMessage, false);
319
+ this.ack(msg, false);
320
+ }
321
+
322
+ function ackAllUpToMessage() {
323
+ const msg = message[kSmqp];
324
+ if (!channelMessage || !msg.pending) {
325
+ throw new FakeAmqpUnknownDeliveryTag(message.fields.deliveryTag);
326
+ }
327
+
328
+ const brokerMessages = allUpToDeliveryTag(channelQ, deliveryTag, 'ack', false);
329
+ for (const brokerMessage of brokerMessages) {
330
+ brokerMessage.ack(false);
331
+ }
332
+
333
+ channelQ.ack(channelMessage, false);
334
+ this.ack(msg, false);
335
+ }
263
336
  }
264
337
  ackAll() {
265
- const broker = this._broker;
266
- const channelName = this._channelName;
338
+ const channelQ = this._channelQueue;
339
+ let msg;
340
+ const brokerMessages = [];
341
+ while ((msg = channelQ.get())) {
342
+ brokerMessages.push(msg.content[kSmqp]);
343
+ msg.ack();
344
+ }
267
345
 
268
- const consumers = broker.getConsumers().filter(({ options }) => options.channelName === channelName);
269
- consumers.forEach((c) => broker.getConsumer(c.consumerTag).ackAll());
270
- }
271
- nack(message, ...args) {
272
- if (this.connection._version >= 2.3) throw new Error(`Nack is not implemented in versions before 2.3 (${this.connection._version})`);
273
- return this._broker.nack(message[kSmqp], ...args);
274
- }
275
- reject(message, ...args) {
276
- this._broker.reject(message[kSmqp], ...args);
346
+ for (const brokerMessage of brokerMessages) {
347
+ brokerMessage.ack();
348
+ }
277
349
  }
278
- nackAll(requeue = false) {
279
- const broker = this._broker;
280
- const channelName = this._channelName;
350
+ reject(message, requeue = false) {
351
+ const deliveryTag = message.fields.deliveryTag;
352
+ const channelMessage = this._broker._getMessageByDeliveryTag(this._channelQueue.name, deliveryTag);
353
+ const channelQ = this._channelQueue;
354
+
355
+ this._callBroker(rejectMessage);
281
356
 
282
- const consumers = broker.getConsumers().filter(({ options }) => options.channelName === channelName);
283
- consumers.forEach((c) => broker.getConsumer(c.consumerTag).nackAll(requeue));
357
+ function rejectMessage() {
358
+ const msg = message[kSmqp];
359
+ if (!channelMessage || !msg.pending) {
360
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
361
+ }
362
+
363
+ channelQ.reject(channelMessage, false);
364
+ this.reject(msg, requeue);
365
+ }
284
366
  }
285
- prefetch(val) {
286
- this[kPrefetch] = val;
367
+ nack(message, allUpTo = false, requeue = false) {
368
+ if (this.connection._version < 2.3) throw new Error(`Nack is not implemented in versions before 2.3 (${this.connection._version})`);
369
+
370
+ const deliveryTag = message.fields.deliveryTag;
371
+ const channelMessage = this._broker._getMessageByDeliveryTag(this._channelQueue.name, deliveryTag);
372
+ const channelQ = this._channelQueue;
373
+
374
+ if (!allUpTo) this._callBroker(nackMessage);
375
+ else this._callBroker(nackAllUpToMessage);
376
+
377
+ function nackMessage() {
378
+ const msg = message[kSmqp];
379
+ if (!channelMessage || !msg.pending) {
380
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
381
+ }
382
+
383
+ channelQ.nack(channelMessage, false, false);
384
+ this.nack(msg, false, requeue);
385
+ }
386
+
387
+ function nackAllUpToMessage() {
388
+ const msg = message[kSmqp];
389
+ if (!channelMessage || !msg.pending) {
390
+ throw new FakeAmqpUnknownDeliveryTag(deliveryTag);
391
+ }
392
+
393
+ const brokerMessages = allUpToDeliveryTag(channelQ, deliveryTag, 'nack', false, false);
394
+ for (const brokerMessage of brokerMessages) {
395
+ brokerMessage.nack(false, requeue);
396
+ }
397
+
398
+ channelMessage.nack(false, false);
399
+ this.nack(msg, false, requeue);
400
+ }
287
401
  }
288
- on(...args) {
289
- return this[kEmitter].on(...args);
402
+ nackAll(requeue = true) {
403
+ const channelQ = this._channelQueue;
404
+ let msg;
405
+ const brokerMessages = [];
406
+ while ((msg = channelQ.get())) {
407
+ brokerMessages.push(msg.content[kSmqp]);
408
+ msg.reject(false);
409
+ }
410
+
411
+ for (const brokerMessage of brokerMessages) {
412
+ brokerMessage.reject(requeue);
413
+ }
290
414
  }
291
- once(...args) {
292
- return this[kEmitter].once(...args);
415
+ prefetch(val, isChannelPrefetch) {
416
+ if (this.connection._version < 3.3) {
417
+ if (isChannelPrefetch !== undefined) {
418
+ return this.connection.close();
419
+ }
420
+ this[kChannelPrefetch] = val;
421
+ } else {
422
+ if (isChannelPrefetch) {
423
+ this[kChannelPrefetch] = val;
424
+ } else {
425
+ this[kPrefetch] = val;
426
+ }
427
+ }
293
428
  }
294
429
  _callBroker(fn, ...args) {
295
430
  let [ poppedCb ] = args.slice(-1);
@@ -306,7 +441,8 @@ class FakeAmqplibChannel {
306
441
  return resolve(result);
307
442
  } catch (err) {
308
443
  if (err._killConnection) this.connection.close();
309
- else if (err._killChannel) this[kClosed] = true;
444
+ else if (err._killChannel) this._teardown();
445
+ if (err._emit) this.emit('error', err);
310
446
  if (!poppedCb) return reject(err);
311
447
  poppedCb(err);
312
448
  return resolve();
@@ -315,9 +451,44 @@ class FakeAmqplibChannel {
315
451
  }
316
452
  _emitReturn({ fields, content, properties }) {
317
453
  process.nextTick(() => {
318
- this[kEmitter].emit('return', { fields, content, properties });
454
+ this.emit('return', { fields, content, properties });
319
455
  });
320
456
  }
457
+ _createChannelMessage(smqpMessage, noAck) {
458
+ const deliveryTag = this._broker._getNextDeliveryTag();
459
+ const consumeMessage = new Message(smqpMessage, deliveryTag);
460
+ if (!noAck) {
461
+ const channelQ = this._channelQueue;
462
+ channelQ.queueMessage(consumeMessage.fields, consumeMessage);
463
+ }
464
+ return consumeMessage;
465
+ }
466
+ _teardown() {
467
+ this[kClosed] = true;
468
+ const channelName = this._channelName;
469
+ const broker = this._broker;
470
+ const channelConsumers = broker._getChannelConsumers(channelName);
471
+ channelConsumers.forEach((c) => broker.cancel(c.consumerTag));
472
+
473
+ let msg;
474
+ while ((msg = this._channelQueue.get())) {
475
+ msg.content[kSmqp].reject(true);
476
+ msg.reject(false);
477
+ }
478
+
479
+ broker.off('return', this._emitReturn);
480
+ }
481
+ _calculateChannelCapacity(consumerCapacity) {
482
+ const channelPrefetch = this[kChannelPrefetch];
483
+ if (channelPrefetch === Infinity) return consumerCapacity;
484
+
485
+ const channelCapacity = channelPrefetch - this._channelQueue.messageCount;
486
+
487
+ let capacity = consumerCapacity;
488
+ if (channelCapacity <= 0) capacity = 0;
489
+ else if (channelCapacity < capacity) capacity = channelCapacity;
490
+ return capacity;
491
+ }
321
492
  }
322
493
 
323
494
  class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
@@ -332,7 +503,7 @@ class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
332
503
  this.checkExchange(exchange).then(() => {
333
504
  return this._callBroker(...args);
334
505
  }).catch((err) => {
335
- this[kEmitter].emit('error', err);
506
+ this.emit('error', err);
336
507
  });
337
508
 
338
509
  return true;
@@ -347,16 +518,16 @@ class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
347
518
  this.checkQueue(queue).then(() => {
348
519
  return this._callBroker(...args);
349
520
  }).catch((err) => {
350
- this[kEmitter].emit('error', err);
521
+ this.emit('error', err);
351
522
  });
352
523
 
353
524
  return true;
354
525
  }
355
526
  }
356
527
 
357
- class FakeAmqplibConnection {
528
+ class FakeAmqplibConnection extends events.EventEmitter {
358
529
  constructor(broker, version, amqpUrl) {
359
- this[kEmitter] = new events.EventEmitter();
530
+ super();
360
531
  this[kClosed] = false;
361
532
  this._channels = [];
362
533
  this._url = normalizeAmqpUrl(amqpUrl);
@@ -367,9 +538,6 @@ class FakeAmqplibConnection {
367
538
  get _closed() {
368
539
  return this[kClosed];
369
540
  }
370
- get _emitter() {
371
- return this[kEmitter];
372
- }
373
541
  get connection() {
374
542
  return {
375
543
  serverProperties: {
@@ -403,16 +571,10 @@ class FakeAmqplibConnection {
403
571
 
404
572
  this._channels.splice(0).forEach((channel) => channel.close());
405
573
 
406
- this[kEmitter].emit('close');
574
+ this.emit('close');
407
575
 
408
576
  return resolveOrCallback(args.slice(-1)[0]);
409
577
  }
410
- on(...args) {
411
- return this[kEmitter].on(...args);
412
- }
413
- once(...args) {
414
- return this[kEmitter].once(...args);
415
- }
416
578
  }
417
579
 
418
580
  function FakeAmqplib(minorVersion = '3.5') {
@@ -436,14 +598,14 @@ FakeAmqplib.prototype.connect = function fakeConnect(amqpUrl, ...args) {
436
598
 
437
599
  FakeAmqplib.prototype.connectSync = function fakeConnectSync(amqpUrl, ...args) {
438
600
  const { _broker } = this.connections.find((conn) => compareConnectionString(conn._url, amqpUrl)) || {};
439
- const broker = _broker || new smqp.Broker(this);
601
+ const broker = _broker || new AmqplibBroker(this);
440
602
  const connection = new FakeAmqplibConnection(broker, this.version, amqpUrl, ...args);
441
603
 
442
604
  const connections = this.connections;
443
605
 
444
606
  connections.push(connection);
445
607
 
446
- connection._emitter.once('close', () => {
608
+ connection.once('close', () => {
447
609
  const idx = connections.indexOf(connection);
448
610
  if (idx > -1) connections.splice(idx, 1);
449
611
  });
@@ -479,13 +641,6 @@ function compareConnectionString(url1, url2) {
479
641
  return parsedUrl1.host === parsedUrl2.host && parsedUrl1.pathname === parsedUrl2.pathname;
480
642
  }
481
643
 
482
- function Message(smqpMessage) {
483
- this[kSmqp] = smqpMessage;
484
- this.content = smqpMessage.content;
485
- this.fields = smqpMessage.fields;
486
- this.properties = smqpMessage.properties;
487
- }
488
-
489
644
  function normalizeAmqpUrl(url$1) {
490
645
  if (!url$1) return new URL('amqp://localhost:5672/');
491
646
  if (typeof url$1 === 'string') url$1 = new URL(url$1);
@@ -561,6 +716,23 @@ function addConfirmCallback(broker, options, callback) {
561
716
  return [ options, confirmCallback ];
562
717
  }
563
718
 
719
+ function allUpToDeliveryTag(q, deliveryTag, op, ...args) {
720
+ const brokerMessages = [];
721
+
722
+ const consumer = q.consume((_, cmsg) => {
723
+ const msgDeliveryTag = cmsg.fields.deliveryTag;
724
+ if (msgDeliveryTag >= deliveryTag) {
725
+ return q.cancel(cmsg.fields.consumerTag);
726
+ }
727
+ brokerMessages.push(cmsg.content[kSmqp]);
728
+ cmsg[op](...args);
729
+ }, { prefetch: Infinity });
730
+
731
+ consumer.cancel();
732
+
733
+ return brokerMessages;
734
+ }
735
+
564
736
  const defaultFake = new FakeAmqplib('3.5');
565
737
  const connections = defaultFake.connections;
566
738
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@onify/fake-amqplib",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Fake amqplib",
5
5
  "type": "module",
6
6
  "exports": {
7
+ "types": "./index.d.ts",
7
8
  "require": "./main.cjs",
8
9
  "import": "./index.js"
9
10
  },