comq 0.4.0-dev.7 → 0.4.1-dev.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.
@@ -87,14 +87,14 @@ Feature: Request-Reply Topology Recovery
87
87
  Given the broker is down
88
88
  And `logger` consuming events from the `numbers_added` exchange is expected
89
89
  When the broker is up
90
- And I emit an event to the `numbers_added` exchange
90
+ And an event is emitted to the `numbers_added` exchange
91
91
  Then `logger` receives the event
92
92
 
93
93
  Scenario: Defining emitter while broker crashed
94
94
  Given `someone` consuming events from the `numbers_added` exchange is expected
95
95
  And the broker has crashed
96
96
  When the broker is up
97
- Then I emit an event to the `numbers_added` exchange
97
+ Then an event is emitted to the `numbers_added` exchange
98
98
  And `someone` receives the event
99
99
 
100
100
  Scenario: Start consuming events while broker is down
@@ -105,5 +105,5 @@ Feature: Request-Reply Topology Recovery
105
105
  And `second` consuming events from the `numbers_added` exchange is expected
106
106
  When the broker has crashed
107
107
  And the broker is up
108
- Then I emit an event to the `numbers_added` exchange
108
+ Then an event is emitted to the `numbers_added` exchange
109
109
  And `second` receives the event
@@ -125,23 +125,24 @@ describe('When I attempt to connect to the broker as {string} with password {str
125
125
  })
126
126
  })
127
127
 
128
- describe.each([['', true], [' not', false]])('Then the connection is%s established', (not, defined) => {
129
- const step = tomato.steps.Th(`the connection is${not} established`)
128
+ describe.each([['', true], [' not', false]])('Then the connection is%s established',
129
+ (not, connected) => {
130
+ const step = tomato.steps.Th(`the connection is${not} established`)
130
131
 
131
- it('should be', async () => undefined)
132
+ it('should be', async () => undefined)
132
133
 
133
- it(`should fail if io is${defined ? 'n\'t' : ''} defined`, async () => {
134
- context.io = defined ? undefined : generate()
134
+ it(`should fail if is${connected ? 'n\'t' : ''} connected`, async () => {
135
+ context.connected = !connected
135
136
 
136
- await expect(step.call(context)).rejects.toThrow(AssertionError)
137
- })
137
+ await expect(step.call(context)).rejects.toThrow(AssertionError)
138
+ })
138
139
 
139
- it(`should pass if io is${defined ? '' : 'n\'t'} defined`, async () => {
140
- context.io = defined ? generate() : undefined
140
+ it(`should pass if is${connected ? '' : 'n\'t'} connected`, async () => {
141
+ context.connected = connected
141
142
 
142
- await expect(step.call(context)).resolves.not.toThrow()
143
+ await expect(step.call(context)).resolves.not.toThrow()
144
+ })
143
145
  })
144
- })
145
146
 
146
147
  describe('Then no exceptions are thrown', () => {
147
148
  const step = tomato.steps.Th('no exceptions are thrown')
@@ -3,5 +3,12 @@
3
3
  const { generate } = require('randomstring')
4
4
 
5
5
  const connect = jest.fn(async () => generate())
6
+ const disconnect = jest.fn(async () => undefined)
6
7
 
7
- exports.context = () => ({ connect })
8
+ /**
9
+ * @return {jest.MockedObject<comq.features.Context>}
10
+ */
11
+ exports.context = () => (/** @type {jest.MockedObject<comq.features.Context>} */{
12
+ connect,
13
+ disconnect
14
+ })
@@ -40,21 +40,12 @@ describe('connect', () => {
40
40
  expect(comq.connect).toHaveBeenCalledWith('amqp://developer:secret@localhost:5673')
41
41
  })
42
42
 
43
- it('should not connect twice', async () => {
44
- await context.connect()
45
-
46
- expect(comq.connect).toHaveBeenCalledTimes(1)
47
- })
48
-
49
- it('should reconnect with credentials', async () => {
43
+ it('should connect with credentials', async () => {
50
44
  const user = generate()
51
45
  const password = generate()
52
46
 
53
47
  await context.connect(user, password)
54
48
 
55
- expect(io.close).toHaveBeenCalled()
56
-
57
- expect(comq.connect).toHaveBeenCalledTimes(2)
58
49
  expect(comq.connect).toHaveBeenCalledWith(`amqp://${user}:${password}@localhost:5673`)
59
50
  })
60
51
 
@@ -4,16 +4,17 @@ import * as _io from '../../../types/io'
4
4
  declare namespace comq.features {
5
5
 
6
6
  interface Context {
7
- url?: string
8
7
  io?: _io.IO
8
+ connected?: boolean
9
9
  reply?: Promise<any>
10
10
  consumed?: Record<string, any>
11
11
  published?: any
12
12
  events?: { [K in _diagnostics.event]?: boolean }
13
13
  exception?: Error
14
- expected: Promise<any>
14
+ expected?: Promise<any>
15
15
 
16
16
  connect(user?: string, password?: string): Promise<void>
17
+ disconnect(): Promise<void>
17
18
  }
18
19
 
19
20
  }
@@ -51,7 +51,7 @@ Then('the connection is not established',
51
51
  * @this {comq.features.Context}
52
52
  */
53
53
  async function () {
54
- assert.equal(this.io, undefined, 'connection is established contrary to expectations')
54
+ assert.equal(this.connected, false, 'connection is established contrary to expectations')
55
55
  })
56
56
 
57
57
  Then('the connection is established',
@@ -59,9 +59,9 @@ Then('the connection is established',
59
59
  * @this {comq.features.Context}
60
60
  */
61
61
  async function () {
62
- if (this.connecting) await this.connecting
62
+ if (this.connecting !== undefined) await this.connecting
63
63
 
64
- assert.notEqual(this.io, undefined, 'connection is not established')
64
+ assert.equal(this.connected, true, 'connection is not established')
65
65
  })
66
66
 
67
67
  Then('no exceptions are thrown',
@@ -7,11 +7,8 @@ const { connect } = require('../../')
7
7
  * @implements {comq.features.Context}
8
8
  */
9
9
  class Context extends World {
10
- /** @type {comq.IO} */
11
- static io
12
- static url
13
-
14
10
  io
11
+ connected = false
15
12
  connecting
16
13
  reply
17
14
  consumed
@@ -23,26 +20,33 @@ class Context extends World {
23
20
  async connect (user, password) {
24
21
  const url = locator(user, password)
25
22
 
26
- if (url !== Context.url) await this.#connect(url)
23
+ await this.#connect(url)
24
+ }
25
+
26
+ async disconnect () {
27
+ if (this.io === undefined) return
27
28
 
28
- this.io = Context.io
29
+ await this.io.close()
29
30
 
30
- for (const event of EVENTS) this.io.diagnose(event, () => (this.events[event] = true))
31
+ this.io = undefined
32
+ this.connected = false
33
+ this.events = {}
31
34
  }
32
35
 
36
+ /**
37
+ * @param {string} url
38
+ * @return {Promise<void>}
39
+ */
33
40
  async #connect (url) {
34
- if (Context.url !== undefined) await Context.disconnect()
41
+ if (this.io !== undefined) await this.disconnect()
35
42
 
36
- Context.url = url
37
- Context.io = await connect(url)
38
- }
39
-
40
- static async disconnect () {
41
- if (Context.io === undefined) return
43
+ this.io = await connect(url)
44
+ this.connected = true
42
45
 
43
- await Context.io.close()
46
+ for (const event of EVENTS) this.io.diagnose(event, () => (this.events[event] = true))
44
47
 
45
- Context.io = undefined
48
+ this.io.diagnose('close', () => (this.connected = false))
49
+ this.io.diagnose('open', () => (this.connected = true))
46
50
  }
47
51
  }
48
52
 
@@ -1,12 +1,11 @@
1
1
  'use strict'
2
2
 
3
- const { AfterAll } = require('@cucumber/cucumber')
4
- const { Context } = require('./context')
3
+ const { After } = require('@cucumber/cucumber')
5
4
 
6
- AfterAll(
5
+ After(
7
6
  /**
8
7
  * @this {comq.features.Context}
9
8
  */
10
9
  async function () {
11
- await Context.disconnect()
10
+ await this.disconnect()
12
11
  })
package/lerna.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "useWorkspaces": true,
3
- "version": "0.4.0-dev.7",
3
+ "version": "0.4.1-dev.0",
4
4
  "conventionalCommits": true
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comq",
3
- "version": "0.4.0-dev.7",
3
+ "version": "0.4.1-dev.0",
4
4
  "description": "ComQ",
5
5
  "author": {
6
6
  "name": "Artem Gurtovoi",
package/readme.md CHANGED
@@ -10,7 +10,7 @@ Node.js.
10
10
  3. Events (pub/sub)
11
11
  4. Content encoding
12
12
  5. Flow control and back pressure handling
13
- 6. Consumer acknowledgements and publisher confirms
13
+ 6. Consumer acknowledgments and publisher confirms
14
14
  7. Poison message handling
15
15
  8. Connection tolerance
16
16
  9. Broker restart resilience
@@ -225,7 +225,7 @@ requests and are expecting replies.
225
225
  *persistent* ([delivery mode 2](https://www.rabbitmq.com/publishers.html#message-properties)),
226
226
  while Requests and Replies are not (mode 1).
227
227
  - Events and Requests are consumed using
228
- manual [acknowledgment mode](https://www.rabbitmq.com/confirms.html#acknowledgement-modes),
228
+ manual [acknowledgment mode](https://www.rabbitmq.com/confirms.html#acknowledgment-modes),
229
229
  and Replies are consumed using automatic mode.
230
230
 
231
231
  If incoming message causes an exception, then it is "negative acknowledged" and requeued. If it
@@ -237,13 +237,13 @@ causes an exception again, it will be discarded.
237
237
 
238
238
  See:
239
239
 
240
- - [Consumer Acknowledgements and Publisher Confirms](https://www.rabbitmq.com/confirms.html).
241
- - [Negative Acknowledgement and Requeuing of Deliveries](https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue)
240
+ - [Consumer Acknowledgments and Publisher Confirms](https://www.rabbitmq.com/confirms.html).
241
+ - [Negative Acknowledgment and Requeuing of Deliveries](https://www.rabbitmq.com/confirms.html#consumer-nacks-requeue)
242
242
  - [Dead Letter Exchanges](https://www.rabbitmq.com/dlx.html)
243
243
 
244
244
  ### Cheatsheet
245
245
 
246
- | Message | Prefetch | Confirms | Queue | Acknowledgement | Persistent |
246
+ | Message | Prefetch | Confirms | Queue | Acknowledgment | Persistent |
247
247
  |---------|-----------|----------|-----------|-----------------|------------|
248
248
  | Request | limited | no | durable | manual | no |
249
249
  | Reply | unlimited | no | exclusive | automatic | no |
package/source/channel.js CHANGED
@@ -211,7 +211,7 @@ class Channel {
211
211
  /** @type {import('amqplib').Options.Consume} */
212
212
  const options = {}
213
213
 
214
- if (this.#topology.acknowledgements) consumer = this.#getAcknowledgingConsumer(consumer)
214
+ if (this.#topology.acknowledgments) consumer = this.#getAcknowledgingConsumer(consumer)
215
215
  else options.noAck = true
216
216
 
217
217
  const response = await this.#channel.consume(queue, consumer, options)
@@ -230,6 +230,8 @@ class Channel {
230
230
 
231
231
  this.#channel.ack(message)
232
232
  } catch (exception) {
233
+ if (exception.message === 'Channel closed') return // message will be requeued by the broker
234
+
233
235
  if (message.fields.redelivered) this.#discard(message, exception)
234
236
  else this.#requeue(message)
235
237
  }
@@ -37,7 +37,11 @@ class Connection {
37
37
  }
38
38
 
39
39
  async close () {
40
- await this.#connection.close()
40
+ try {
41
+ await this.#connection.close()
42
+ } catch (e) {
43
+ // connection already closed
44
+ }
41
45
  }
42
46
 
43
47
  createChannel = failsafe(this, this.#recover,
@@ -2,6 +2,6 @@
2
2
  "prefetch": 300,
3
3
  "confirms": true,
4
4
  "durable": true,
5
- "acknowledgements": true,
5
+ "acknowledgments": true,
6
6
  "persistent": true
7
7
  }
@@ -2,6 +2,6 @@
2
2
  "prefetch": 0,
3
3
  "confirms": false,
4
4
  "durable": false,
5
- "acknowledgements": false,
5
+ "acknowledgments": false,
6
6
  "persistent": false
7
7
  }
@@ -2,6 +2,6 @@
2
2
  "prefetch": 300,
3
3
  "confirms": false,
4
4
  "durable": true,
5
- "acknowledgements": true,
5
+ "acknowledgments": true,
6
6
  "persistent": false
7
7
  }
@@ -6,7 +6,7 @@ const preset = () => ({
6
6
  prefetch: random(10),
7
7
  confirms: flip(),
8
8
  durable: flip(),
9
- acknowledgements: flip(),
9
+ acknowledgments: flip(),
10
10
  persistent: flip()
11
11
  })
12
12
 
@@ -92,7 +92,7 @@ describe('consume', () => {
92
92
  })
93
93
  })
94
94
 
95
- describe('acknowledgements', () => {
95
+ describe('acknowledgments', () => {
96
96
  const consumer = /** @type {comq.channel.consumer} */ jest.fn(async () => undefined)
97
97
  const queue = generate()
98
98
 
@@ -100,7 +100,7 @@ describe('acknowledgements', () => {
100
100
  ['', true],
101
101
  ['not ', false]
102
102
  ])('should %sack incoming messages', async (_, ack) => {
103
- topology.acknowledgements = ack
103
+ topology.acknowledgments = ack
104
104
  channel = await create(connection, topology)
105
105
  chan = await getCreatedChannel()
106
106
 
@@ -119,8 +119,8 @@ describe('acknowledgements', () => {
119
119
  it.each([
120
120
  ['manual', true],
121
121
  ['automatic', false]
122
- ])('should create consumer with %s acknowledgements', async (_, ack) => {
123
- topology.acknowledgements = ack
122
+ ])('should create consumer with %s acknowledgments', async (_, ack) => {
123
+ topology.acknowledgments = ack
124
124
  channel = await create(connection, topology)
125
125
  chan = await getCreatedChannel()
126
126
 
@@ -136,7 +136,7 @@ describe('acknowledgements', () => {
136
136
  ['nack', true],
137
137
  ['discard', false]
138
138
  ])('should %s the message caused an exception', async (_, requeue) => {
139
- topology.acknowledgements = true
139
+ topology.acknowledgments = true
140
140
 
141
141
  channel = await create(connection, topology)
142
142
  chan = await getCreatedChannel()
@@ -157,6 +157,28 @@ describe('acknowledgements', () => {
157
157
 
158
158
  expect(chan.nack).toHaveBeenCalledWith(message, false, requeue)
159
159
  })
160
+
161
+ it('should ignore Channel ended exception', async () => {
162
+ topology.acknowledgments = true
163
+
164
+ channel = await create(connection, topology)
165
+ chan = await getCreatedChannel()
166
+
167
+ const consumer = /** @type {Function} */ jest.fn(async () => generate())
168
+
169
+ await channel.consume(queue, consumer)
170
+
171
+ const callback = chan.consume.mock.calls[0][1]
172
+ const content = randomBytes(8)
173
+ const properties = {}
174
+ const fields = {}
175
+ const message = /** @type {import('amqplib').ConsumeMessage} */ { content, properties, fields }
176
+
177
+ chan.ack.mockImplementation(() => { throw new Error('Channel closed') })
178
+ chan.nack.mockImplementation(() => { throw new Error('Channel closed') })
179
+
180
+ await expect(callback(message)).resolves.not.toThrow()
181
+ })
160
182
  })
161
183
 
162
184
  describe('send', () => {
@@ -352,10 +374,10 @@ describe.each(['group', 'exclusive'])('%s subscribe', (option) => {
352
374
  })
353
375
 
354
376
  it.each([
355
- ['with acknowledgements', true],
356
- ['without acknowledgements', false]
377
+ ['with acknowledgments', true],
378
+ ['without acknowledgments', false]
357
379
  ])('should start consuming %s', async (_, ack) => {
358
- topology.acknowledgements = ack
380
+ topology.acknowledgments = ack
359
381
 
360
382
  jest.clearAllMocks()
361
383
 
@@ -764,7 +786,7 @@ describe('diagnostics', () => {
764
786
  ])('should%s emit `discard` event', async (_, redelivered) => {
765
787
  jest.clearAllMocks()
766
788
 
767
- topology.acknowledgements = true
789
+ topology.acknowledgments = true
768
790
  channel = await create(connection, topology)
769
791
  chan = await getCreatedChannel()
770
792
 
@@ -154,13 +154,26 @@ describe('create channel', () => {
154
154
  })
155
155
  })
156
156
 
157
- it('should close connection', async () => {
158
- await connection.open()
159
- await connection.close()
157
+ describe('close', () => {
158
+ it('should close connection', async () => {
159
+ await connection.open()
160
+ await connection.close()
161
+
162
+ const amqp = await amqplib.connect.mock.results[0].value
163
+
164
+ expect(amqp.close).toHaveBeenCalled()
165
+ })
160
166
 
161
- const amqp = await amqplib.connect.mock.results[0].value
167
+ it('should ignore exceptions', async () => {
168
+ await connection.open()
169
+
170
+ /** @type {jest.MockedObject<import('amqplib').Connection>} */
171
+ const amqp = await amqplib.connect.mock.results[0].value
162
172
 
163
- expect(amqp.close).toHaveBeenCalled()
173
+ amqp.close.mockImplementation(async () => { throw new Error() })
174
+
175
+ await expect(connection.close()).resolves.not.toThrow()
176
+ })
164
177
  })
165
178
 
166
179
  describe('diagnostics', () => {
@@ -7,7 +7,7 @@ it('should define request preset', async () => {
7
7
  prefetch: 300,
8
8
  confirms: false,
9
9
  durable: true,
10
- acknowledgements: true,
10
+ acknowledgments: true,
11
11
  persistent: false
12
12
  })
13
13
  })
@@ -17,7 +17,7 @@ it('should define reply preset', async () => {
17
17
  prefetch: 0,
18
18
  confirms: false,
19
19
  durable: false,
20
- acknowledgements: false,
20
+ acknowledgments: false,
21
21
  persistent: false
22
22
  })
23
23
  })
@@ -27,7 +27,7 @@ it('should define event preset', async () => {
27
27
  prefetch: 300,
28
28
  confirms: true,
29
29
  durable: true,
30
- acknowledgements: true,
30
+ acknowledgments: true,
31
31
  persistent: true
32
32
  })
33
33
  })
@@ -10,7 +10,7 @@ declare namespace comq {
10
10
  prefetch: number
11
11
  confirms: boolean
12
12
  durable: boolean
13
- acknowledgements: boolean
13
+ acknowledgments: boolean
14
14
  persistent: boolean
15
15
  }
16
16