comq 0.4.1-dev.3 → 0.5.0-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.
Files changed (61) hide show
  1. package/.github/labels.yaml +15 -0
  2. package/.github/workflows/labels.yaml +18 -0
  3. package/.github/workflows/release.yaml +20 -8
  4. package/docker-compose.yaml +20 -2
  5. package/docs/notes.md +14 -8
  6. package/examples/const.js +1 -3
  7. package/examples/events/consumer.js +5 -6
  8. package/examples/events/producer.js +3 -3
  9. package/examples/package.json +3 -0
  10. package/examples/rpc/consumer.js +3 -3
  11. package/examples/rpc/producer.js +3 -3
  12. package/features/connection.feature +3 -0
  13. package/features/events.feature +1 -1
  14. package/features/shards.feature +61 -0
  15. package/features/steps/.test/backpressure.test.js +2 -2
  16. package/features/steps/.test/broker.test.js +32 -6
  17. package/features/steps/.test/connection.test.js +105 -1
  18. package/features/steps/.test/context.mock.js +2 -2
  19. package/features/steps/.test/context.test.js +13 -5
  20. package/features/steps/.test/events.test.js +66 -3
  21. package/features/steps/.test/rpc.test.js +99 -1
  22. package/features/steps/.types/context.d.ts +11 -1
  23. package/features/steps/backpressure.js +1 -1
  24. package/features/steps/broker.js +26 -9
  25. package/features/steps/connection.js +69 -2
  26. package/features/steps/context.js +28 -12
  27. package/features/steps/events.js +41 -2
  28. package/features/steps/rpc.js +58 -1
  29. package/lerna.json +1 -1
  30. package/package.json +5 -5
  31. package/readme.md +75 -25
  32. package/source/channel.js +56 -32
  33. package/source/connect.js +28 -2
  34. package/source/connection.js +24 -17
  35. package/source/decode.js +1 -1
  36. package/source/events.js +7 -0
  37. package/source/io.js +21 -14
  38. package/source/shards/channel.js +219 -0
  39. package/source/shards/connection.js +56 -0
  40. package/source/shards/index.js +5 -0
  41. package/test/channel.test.js +240 -145
  42. package/test/connect.test.js +39 -11
  43. package/test/connection.mock.js +17 -13
  44. package/test/connection.test.js +65 -12
  45. package/test/decode.test.js +5 -5
  46. package/test/io.close.test.js +2 -2
  47. package/test/io.consume.test.js +1 -1
  48. package/test/io.reply.test.js +7 -7
  49. package/test/io.request.test.js +27 -1
  50. package/test/presets.test.js +1 -1
  51. package/test/shards/channel.test.js +367 -0
  52. package/test/shards/connection.test.js +106 -0
  53. package/types/amqp.d.ts +3 -1
  54. package/types/channel.d.ts +8 -5
  55. package/types/connection.d.ts +3 -1
  56. package/types/diagnostic.d.ts +1 -1
  57. package/types/topology.d.ts +4 -0
  58. /package/source/{presets → topology}/event.json +0 -0
  59. /package/source/{presets → topology}/index.js +0 -0
  60. /package/source/{presets → topology}/reply.json +0 -0
  61. /package/source/{presets → topology}/request.json +0 -0
package/readme.md CHANGED
@@ -5,16 +5,18 @@ Node.js.
5
5
 
6
6
  ## Features
7
7
 
8
- 1. Dynamic topology
9
- 2. Request-reply (RPC)
10
- 3. Events (pub/sub)
11
- 4. Content encoding
12
- 5. Flow control and back pressure handling
13
- 6. Consumer acknowledgments and publisher confirms
14
- 7. Poison message handling
15
- 8. Connection tolerance
16
- 9. Broker restart resilience
17
- 10. Graceful shutdown
8
+ 1. [Dynamic topology](#topology)
9
+ 2. [Request](#request)-[reply](#reply) (RPC)
10
+ 3. Events ([pub](#emission)/[sub](#consumption))
11
+ 4. [Content encoding](#encoding)
12
+ 5. [Flow control](#flow-control) and back pressure handling
13
+ 6. [Consumer acknowledgments](#messages) and [publisher confirms](#channels)
14
+ 7. [Poison message handling](#messages)
15
+ 8. [Connection tolerance](#connection-tolerance) and broker restart resilience
16
+ 9. [Sharded connection](#sharded-connection)
17
+ 10. [Graceful shutdown](#graceful-shutdown)
18
+
19
+ > CommonJS, ECMAScript and TypeScript compatible (types included).
18
20
 
19
21
  ## TL;DR
20
22
 
@@ -37,7 +39,7 @@ to [`amqplib.connect`](https://amqp-node.github.io/amqplib/channel_api.html#conn
37
39
  ### Example
38
40
 
39
41
  ```javascript
40
- const { connect } = require('comq')
42
+ import { connect } from 'comq'
41
43
 
42
44
  const url = 'amqp://developer:secret@localhost'
43
45
  const io = await connect(url)
@@ -66,7 +68,7 @@ value as in the Request.
66
68
 
67
69
  ## Reply
68
70
 
69
- `async IO.reply(queue: string, producer)`
71
+ `async IO.reply(queue: string, producer): void`
70
72
 
71
73
  `producer` function's signature is `async? (message: any): any`
72
74
 
@@ -94,7 +96,7 @@ await io.reply('add_numbers', ({ a, b }) => (a + b))
94
96
 
95
97
  ## Request
96
98
 
97
- `async IO.request(queue: string, payload: any, [encoding: string])`
99
+ `async IO.request(queue: string, payload: any, [encoding: string]): any`
98
100
 
99
101
  Send encoded Request message with `replyTo` and `correlationId` properties set and
100
102
  return decoded Reply content.
@@ -109,7 +111,7 @@ const sum = await io.request('add_numbers', { a: 1, b: 2 })
109
111
 
110
112
  ## Consumption
111
113
 
112
- `async IO.consume(exchange: string, [group: string], consumer)`
114
+ `async IO.consume(exchange: string, [group: string], consumer): void`
113
115
 
114
116
  `consumer` function's signature is `async? (payload: any): void`
115
117
 
@@ -135,7 +137,7 @@ await io.consume('numbers_added', 'logger', ({ a, b }) => {
135
137
 
136
138
  ## Emission
137
139
 
138
- `async IO.emit(exchange: string, payload: any, [encoding: string])`
140
+ `async IO.emit(exchange: string, payload: any, [encoding: string]): void`
139
141
 
140
142
  Publish encoded Event to the `exchange`.
141
143
 
@@ -175,18 +177,61 @@ The following encoding formats are supported:
175
177
 
176
178
  When initially connecting to the broker or if the established connection is lost, connection
177
179
  attempts will be repeated indefinitely with intervals increasing up to 30 seconds. Once reconnected,
178
- the topology will be recovered, and unanswered Requests and unconfirmed Events will be resent.
180
+ the topology will be recovered, and unanswered Requests and unconfirmed Events will be
181
+ retransmitted.
179
182
 
180
183
  If the broker rejects the connection (for example, due to access being denied), an exception will be
181
184
  thrown.
182
185
 
186
+ ## Sharded Connection
187
+
188
+ *Send to one, receive from all.*
189
+
190
+ A sharded connection is a mechanism that uses multiple connections simultaneously to achieve load
191
+ balancing and mitigate failover scenarios, utilizing a set of broker instances that are **not**
192
+ combined into a cluster.
193
+
194
+ `async connect(...shards: string[]): IO`
195
+
196
+ Returns an instance of `IO` once a successful connection to one of the shards is established.
197
+
198
+ Outgoing messages are sent to a single connection chosen at random from the shard pool. Shards that
199
+ lose their underlying connection or experience channel [back pressure](#flow-control) on
200
+ corresponding channel are removed from the pool until the issue is resolved. Pending messages
201
+ meeting these conditions are immediately routed among the remaining shards in the pool. If no shards
202
+ are available, messages will wait until any shard's connection is re-established.
203
+
204
+ Incoming messages are consumed from all shards.
205
+
206
+ ### Example
207
+
208
+ ```javascript
209
+
210
+ const shard0 = 'amqp://developer:secret@localhost:5673'
211
+ const shard1 = 'amqp://developer:secret@localhost:5674'
212
+
213
+ const io = await connect(shard0, shard1)
214
+
215
+ // ...
216
+
217
+ await io.close()
218
+
219
+ ```
220
+
221
+ ## Flow Control
222
+
223
+ When [back pressure](https://www.rabbitmq.com/flow-control.html) is applied to a channel or the
224
+ underlying broker connection is lost, any current and future outgoing messages will be paused.
225
+ Corresponding returned promises will remain in a `pending` state until the pressure is removed or
226
+ the connection is restored.
227
+
183
228
  ## Topology
184
229
 
185
230
  Topology is designed to deliver maximum performance while ensuring that the **at least once**
186
231
  guarantee provided by RabbitMQ is maintained.
187
232
 
188
- > Topology design assumes that unanswered Requests and unconfirmed Events will be resent upon
189
- > reconnection.
233
+ > Topology design assumes that unanswered Requests and unconfirmed Events will be retransmitted
234
+ > upon reconnection.
190
235
 
191
236
  ### Dynamic
192
237
 
@@ -244,23 +289,23 @@ See:
244
289
  ### Cheatsheet
245
290
 
246
291
  | Message | Prefetch | Confirms | Queue | Acknowledgment | Persistent |
247
- |---------|-----------|----------|-----------|-----------------|------------|
248
- | Request | limited | no | durable | manual | no |
249
- | Reply | unlimited | no | exclusive | automatic | no |
250
- | Event | limited | yes | durable | manual | yes |
292
+ |---------|-----------|----------|-----------|----------------|------------|
293
+ | Request | limited | no | durable | manual | no |
294
+ | Reply | unlimited | no | exclusive | automatic | no |
295
+ | Event | limited | yes | durable | manual | yes |
251
296
 
252
297
  ## Graceful Shutdown
253
298
 
254
299
  ### Sealing
255
300
 
256
- `async IO.seal()`
301
+ `async IO.seal(): void`
257
302
 
258
303
  [Stop receiving](https://amqp-node.github.io/amqplib/channel_api.html#channel_cancel) new Events and
259
304
  Requests.
260
305
 
261
306
  ### Disconnection
262
307
 
263
- `async IO.close()`
308
+ `async IO.close(): void`
264
309
 
265
310
  1. Call `IO.seal()`.
266
311
  2. Wait for any outstanding messages to be processed[^2] and acknowledged. Sending Requests,
@@ -295,19 +340,24 @@ calling `IO.close()`.
295
340
 
296
341
  Subscribe to one of the diagnostic events:
297
342
 
298
- - `open`: connection is restored[^3].
343
+ - `open`: connection is opened[^3].
299
344
  - `close`: connection is closed.
300
345
  Optional [`error`](https://amqp-node.github.io/amqplib/channel_api.html#model_events) is passed
301
346
  as an argument.
302
347
  - `flow`: back pressure is applied to a channel. [Channel type](./types/topology.d.ts) is passed as
303
348
  an argument.
304
349
  - `drain`: back pressure is removed from a channel. Channel type is passed.
350
+ - `remove`: channel is removed from the [pool](#sharded-connection).
305
351
  - `recover`: channel's topology is recovered. Channel type is passed.
306
352
  - `discard`: message is [discarded](#messages) as it repeatedly caused
307
353
  exceptions. Channel type,
308
354
  raw [amqp message object](https://amqp-node.github.io/amqplib/channel_api.html#channel_consume)
309
355
  and the exception are passed as arguments.
310
356
 
357
+ In the case of a [sharded connection](#sharded-connection), an additional argument specifying the
358
+ shard number will be passed to listeners. The shard number corresponds to the number of the argument
359
+ used in the `connect` function call.
360
+
311
361
  [^3]: As [`connect`](#connect) function returns an instance of `IO` *after* the connection has been
312
362
  established, there is no way to capture the initial `open` event.
313
363
 
package/source/channel.js CHANGED
@@ -7,18 +7,29 @@ const { lazy, recall, promex, failsafe, immediate } = require('@toa.io/generic')
7
7
  * @implements {comq.Channel}
8
8
  */
9
9
  class Channel {
10
+ index
11
+
12
+ /** @type {comq.amqp.Connection} */
13
+ #connection
14
+
10
15
  /** @type {comq.Topology} */
11
16
  #topology
12
17
 
13
18
  /** @type {comq.amqp.Channel} */
14
19
  #channel
15
20
 
21
+ /** @type {boolean} */
22
+ #failfast
23
+
16
24
  /** @type {string[]} */
17
25
  #tags = []
18
26
 
19
27
  /** @type {toa.generic.Promex} */
20
28
  #paused
21
29
 
30
+ /** @type {boolean} */
31
+ #sealed = false
32
+
22
33
  /** @type {toa.generic.Promex} */
23
34
  #recovery = promex()
24
35
 
@@ -28,16 +39,23 @@ class Channel {
28
39
  #diagnostics = new EventEmitter()
29
40
 
30
41
  /**
42
+ * @param {comq.amqp.Connection} connection
31
43
  * @param {comq.Topology} topology
44
+ * @param {number} index
32
45
  */
33
- constructor (topology) {
46
+ constructor (connection, topology, index) {
47
+ this.index = index
48
+
49
+ this.#connection = connection
34
50
  this.#topology = topology
35
- }
51
+ this.#failfast = index !== undefined
36
52
 
37
- async create (connection) {
38
- const method = `create${this.#topology.confirms ? 'Confirm' : ''}Channel`
53
+ if (this.#failfast) failsafe.disable(this.send, this.publish)
54
+ }
39
55
 
40
- this.#channel = await connection[method]()
56
+ async create () {
57
+ if (this.#topology.confirms) this.#channel = await this.#connection.createConfirmChannel()
58
+ else this.#channel = await this.#connection.createChannel()
41
59
  }
42
60
 
43
61
  consume = recall(this,
@@ -48,7 +66,7 @@ class Channel {
48
66
  * @param {comq.channels.consumer} callback
49
67
  */
50
68
  async (queue, callback) => {
51
- await this.#consume(queue, callback)
69
+ if (!this.#sealed) await this.#consume(queue, callback)
52
70
  })))
53
71
 
54
72
  subscribe = recall(this,
@@ -61,7 +79,7 @@ class Channel {
61
79
  * @returns {Promise<void>}
62
80
  */
63
81
  async (exchange, queue, callback) => {
64
- await this.#consume(queue, callback)
82
+ if (!this.#sealed) await this.#consume(queue, callback)
65
83
  })))
66
84
 
67
85
  send = failsafe(this, this.#recover,
@@ -80,7 +98,7 @@ class Channel {
80
98
  /**
81
99
  * @param {string} exchange
82
100
  * @param {Buffer} buffer
83
- * @param {import('amqplib').Options.Publish} [options]
101
+ * @param {comq.amqp.options.Publish} [options]
84
102
  */
85
103
  async (exchange, buffer, options) => {
86
104
  await this.#publish(exchange, DEFAULT, buffer, options)
@@ -89,15 +107,18 @@ class Channel {
89
107
  async throw (queue, buffer, options) {
90
108
  try {
91
109
  await this.#publish(DEFAULT, queue, buffer, options)
92
- } catch {
93
- // whatever
110
+ } catch (exception) {
111
+ if (this.#failfast) throw exception
112
+ // ignore otherwise
94
113
  }
95
114
  }
96
115
 
97
116
  async seal () {
117
+ this.#sealed = true
118
+
98
119
  const cancellations = this.#tags.map((tag) => this.#channel.cancel(tag))
99
120
 
100
- await Promise.all(cancellations)
121
+ await Promise.all(cancellations).catch(noop) // won't recover anyway
101
122
  }
102
123
 
103
124
  diagnose (event, listener) {
@@ -105,16 +126,18 @@ class Channel {
105
126
  }
106
127
 
107
128
  async recover (connection) {
108
- await this.create(connection)
129
+ this.#connection = connection
130
+
131
+ await this.create()
109
132
 
110
133
  lazy.reset(this)
111
134
  await recall(this)
112
135
 
113
- this.#unpause(REJECTION)
136
+ this.#unpause(INTERRUPTION)
114
137
 
115
- for (const confirmation of this.#confirmations) confirmation.reject(REJECTION)
138
+ for (const confirmation of this.#confirmations) confirmation.reject(INTERRUPTION)
116
139
 
117
- // let unpause and confirmation rejections be handled
140
+ // let unpause and confirmation interruptions be handled
118
141
  await immediate()
119
142
 
120
143
  this.#recovery.resolve()
@@ -142,7 +165,7 @@ class Channel {
142
165
  * @returns {Promise<void>}
143
166
  */
144
167
  async #assertExchange (exchange) {
145
- /** @type {import('amqplib').Options.AssertExchange} */
168
+ /** @type {comq.amqp.options.Exchange} */
146
169
  const options = { durable: this.#topology.durable }
147
170
 
148
171
  await this.#channel.assertExchange(exchange, 'fanout', options)
@@ -208,7 +231,7 @@ class Channel {
208
231
  * @returns {Promise<void>}
209
232
  */
210
233
  async #consume (queue, consumer) {
211
- /** @type {import('amqplib').Options.Consume} */
234
+ /** @type {comq.amqp.options.Consume} */
212
235
  const options = {}
213
236
 
214
237
  if (this.#topology.acknowledgments) consumer = this.#getAcknowledgingConsumer(consumer)
@@ -230,7 +253,7 @@ class Channel {
230
253
 
231
254
  this.#channel.ack(message)
232
255
  } catch (exception) {
233
- if (exception.message === 'Channel closed') return // message will be requeued by the broker
256
+ if (exception.message === 'Channel closed') return // the message will be requeued by the broker
234
257
 
235
258
  if (message.fields.redelivered) this.#discard(message, exception)
236
259
  else this.#requeue(message)
@@ -238,14 +261,14 @@ class Channel {
238
261
  }
239
262
 
240
263
  /**
241
- * @param {import('amqplib').ConsumeMessage} message
264
+ * @param {comq.amqp.Message} message
242
265
  */
243
266
  #requeue (message) {
244
267
  this.#channel.nack(message, false, true)
245
268
  }
246
269
 
247
270
  /**
248
- * @param {import('amqplib').ConsumeMessage} message
271
+ * @param {comq.amqp.Message} message
249
272
  * @param {Error} [exception]
250
273
  */
251
274
  #discard (message, exception) {
@@ -259,6 +282,8 @@ class Channel {
259
282
  this.#paused = promex()
260
283
  this.#channel.once('drain', this.#unpause)
261
284
  this.#diagnostics.emit('flow')
285
+
286
+ if (this.#failfast) throw INTERRUPTION
262
287
  }
263
288
 
264
289
  /**
@@ -276,20 +301,20 @@ class Channel {
276
301
 
277
302
  async #recover (exception) {
278
303
  if (permanent(exception)) return false
279
-
280
- await this.#recovery
304
+ else await this.#recovery
281
305
  }
282
306
  }
283
307
 
284
308
  /**
285
309
  * @param {comq.amqp.Connection} connection
286
310
  * @param {comq.Topology} topology
311
+ * @param {number} [index]
287
312
  * @return {Promise<comq.Channel>}
288
313
  */
289
- const create = async (connection, topology) => {
290
- const channel = new Channel(topology)
314
+ async function create (connection, topology, index) {
315
+ const channel = new Channel(connection, topology, index)
291
316
 
292
- await channel.create(connection)
317
+ await channel.create()
293
318
 
294
319
  return channel
295
320
  }
@@ -297,24 +322,23 @@ const create = async (connection, topology) => {
297
322
  /**
298
323
  * @return {boolean}
299
324
  */
300
- const permanent = (exception) => {
325
+ function permanent (exception) {
301
326
  const closed = exception.message === 'Channel closed'
302
327
  const ended = exception.message === 'Channel ended, no reply will be forthcoming'
303
- const internal = exception === REJECTION
304
- const unpause = exception.pause === 1
328
+ const internal = exception === INTERRUPTION
305
329
 
306
- return !closed && !ended && !internal && !unpause
330
+ return !closed && !ended && !internal
307
331
  }
308
332
 
309
333
  const DEFAULT = ''
310
334
 
311
- /** @type {import('amqplib').Options.AssertQueue} */
335
+ /** @type {comq.amqp.options.Queue} */
312
336
  const DURABLE = { durable: true }
313
337
 
314
- /** @type {import('amqplib').Options.AssertQueue} */
338
+ /** @type {comq.amqp.options.Queue} */
315
339
  const EXCLUSIVE = { exclusive: true }
316
340
 
317
- const REJECTION = /** @type {Error} */ Symbol('rejection')
341
+ const INTERRUPTION = /** @type {Error} */ Symbol('internal interruption')
318
342
 
319
343
  function noop () {}
320
344
 
package/source/connect.js CHANGED
@@ -2,14 +2,40 @@
2
2
 
3
3
  const { IO } = require('./io')
4
4
  const { Connection } = require('./connection')
5
+ const shards = require('./shards')
5
6
 
6
7
  /** @type {comq.connect} */
7
- const connect = async (url) => {
8
- const connection = new Connection(url)
8
+ const connect = async (...urls) => {
9
+ const connection = create(urls)
9
10
 
10
11
  await connection.open()
11
12
 
12
13
  return new IO(connection)
13
14
  }
14
15
 
16
+ /**
17
+ * @param {string[]} urls
18
+ * @return {comq.Connection}
19
+ */
20
+ const create = (urls) => {
21
+ if (urls.length === 1) return single(urls[0])
22
+ else return sharded(urls)
23
+ }
24
+
25
+ /**
26
+ * @param {string} url
27
+ * @return {comq.Connection}
28
+ */
29
+ const single = (url) => new Connection(url)
30
+
31
+ /**
32
+ * @param {string[]} urls
33
+ * @return {comq.Connection}
34
+ */
35
+ const sharded = (urls) => {
36
+ const connections = urls.map(single)
37
+
38
+ return new shards.Connection(connections)
39
+ }
40
+
15
41
  exports.connect = connect
@@ -4,7 +4,7 @@ const { EventEmitter } = require('node:events')
4
4
  const amqp = require('amqplib')
5
5
  const { retry, promex, failsafe } = require('@toa.io/generic')
6
6
 
7
- const presets = require('./presets')
7
+ const presets = require('./topology')
8
8
  const channels = require('./channel')
9
9
 
10
10
  /**
@@ -14,7 +14,7 @@ class Connection {
14
14
  /** @type {string} */
15
15
  #url
16
16
 
17
- /** @type {import('amqplib').Connection} */
17
+ /** @type {comq.amqp.Connection} */
18
18
  #connection
19
19
 
20
20
  /** @type {comq.Channel[]} */
@@ -33,21 +33,26 @@ class Connection {
33
33
  }
34
34
 
35
35
  async open () {
36
- await retry((retry) => this.#open(retry), RETRY)
36
+ await retry(this.#open, RETRY)
37
37
  }
38
38
 
39
39
  async close () {
40
- try {
41
- await this.#connection.close()
42
- } catch (e) {
43
- // connection already closed
44
- }
40
+ if (this.#connection === undefined) await this.#recovery
41
+
42
+ await this.#connection.close()
45
43
  }
46
44
 
47
45
  createChannel = failsafe(this, this.#recover,
48
- async (type) => {
49
- const preset = presets[type]
50
- const channel = await channels.create(this.#connection, preset)
46
+ /**
47
+ * @param {comq.topology.type} type
48
+ * @param {number} [index]
49
+ * @return {Promise<comq.Channel>}
50
+ */
51
+ async (type, index) => {
52
+ if (this.#connection === undefined) await this.#recovery
53
+
54
+ const topology = presets[type]
55
+ const channel = await channels.create(this.#connection, topology, index)
51
56
 
52
57
  this.#channels.push(channel)
53
58
 
@@ -59,9 +64,9 @@ class Connection {
59
64
  }
60
65
 
61
66
  /**
62
- * @param {Function} retry
67
+ * @type {toa.generic.retry.Task}
63
68
  */
64
- async #open (retry) {
69
+ #open = async (retry) => {
65
70
  try {
66
71
  this.#connection = await amqp.connect(this.#url)
67
72
  } catch (exception) {
@@ -69,11 +74,11 @@ class Connection {
69
74
  else throw exception
70
75
  }
71
76
 
72
- this.#connection.on('close', this.#close)
73
-
74
77
  // prevents process crash, 'close' will be emitted next
75
78
  // https://amqp-node.github.io/amqplib/channel_api.html#model_events
76
- this.#connection.on('error', () => undefined)
79
+ this.#connection.on('error', noop)
80
+
81
+ this.#connection.on('close', this.#close)
77
82
  this.#diagnostics.emit('open')
78
83
 
79
84
  for (const channel of this.#channels) await channel.recover(this.#connection)
@@ -87,8 +92,8 @@ class Connection {
87
92
  */
88
93
  #close = async (error) => {
89
94
  this.#diagnostics.emit('close', error)
90
-
91
95
  this.#connection.removeAllListeners()
96
+ this.#connection = undefined
92
97
 
93
98
  if (error !== undefined) await this.open()
94
99
  }
@@ -114,4 +119,6 @@ const transient = (exception) => {
114
119
  return refused || handshake
115
120
  }
116
121
 
122
+ function noop () {}
123
+
117
124
  exports.Connection = Connection
package/source/decode.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const decoders = require('./encoders')
4
4
 
5
5
  /**
6
- * @param {import('amqplib').ConsumeMessage} message
6
+ * @param {comq.amqp.Message} message
7
7
  * @returns {any}
8
8
  */
9
9
  const decode = (message) => {
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ /** @type {comq.diagnostics.event[]} */
4
+ exports.connection = ['open', 'close']
5
+
6
+ /** @type {comq.diagnostics.event[]} */
7
+ exports.channel = ['flow', 'drain', 'recover', 'discard']
package/source/io.js CHANGED
@@ -6,6 +6,7 @@ const { lazy, track, failsafe, promex } = require('@toa.io/generic')
6
6
 
7
7
  const { decode } = require('./decode')
8
8
  const { encode } = require('./encode')
9
+ const events = require('./events')
9
10
  const io = require('./.io')
10
11
 
11
12
  /**
@@ -38,7 +39,7 @@ class IO {
38
39
  constructor (connection) {
39
40
  this.#connection = connection
40
41
 
41
- for (const event of CONNECTION_EVENTS) {
42
+ for (const event of events.connection) {
42
43
  this.#connection.diagnose(event, (...args) => this.#diagnostics.emit(event, ...args))
43
44
  }
44
45
  }
@@ -55,13 +56,14 @@ class IO {
55
56
  await this.#requests.consume(queue, consumer)
56
57
  })
57
58
 
59
+ // failsafe is aimed to retransmit unanswered messages
58
60
  request = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
59
61
  failsafe(this, this.#recover,
60
62
  /**
61
63
  * @param {string} queue
62
64
  * @param {any} payload
63
65
  * @param {comq.encoding} [encoding]
64
- * @param {comq.ReplyToPropertyFormatter} replyToFormatter
66
+ * @param {comq.ReplyToPropertyFormatter} [replyToFormatter]
65
67
  * @returns {Promise<void>}
66
68
  */
67
69
  async (queue, payload, encoding, replyToFormatter) => {
@@ -123,7 +125,7 @@ class IO {
123
125
  this.#requests = await this.#createChannel('request')
124
126
  this.#replies = await this.#createChannel('reply')
125
127
 
126
- this.#requests.diagnose('recover', this.#resend)
128
+ this.#setupRetransmission()
127
129
  }
128
130
 
129
131
  async #createEventChannel () {
@@ -148,13 +150,19 @@ class IO {
148
150
  async #createChannel (type) {
149
151
  const channel = await this.#connection.createChannel(type)
150
152
 
151
- for (const event of CHANNEL_EVENTS) {
153
+ for (const event of events.channel) {
152
154
  channel.diagnose(event, (...args) => this.#diagnostics.emit(event, type, ...args))
153
155
  }
154
156
 
155
157
  return channel
156
158
  }
157
159
 
160
+ #setupRetransmission () {
161
+ const event = (this.#requests.sharded === true) ? 'remove' : 'recover'
162
+
163
+ this.#requests.diagnose(event, this.#retransmit)
164
+ }
165
+
158
166
  /**
159
167
  * @param {comq.producer} producer
160
168
  * @returns {comq.channels.consumer}
@@ -185,6 +193,9 @@ class IO {
185
193
  * @returns {comq.channels.consumer}
186
194
  */
187
195
  #getReplyConsumer = (queue, emitter) =>
196
+ /**
197
+ * @param {comq.amqp.Message} message
198
+ */
188
199
  (message) => {
189
200
  const payload = decode(message)
190
201
 
@@ -216,12 +227,14 @@ class IO {
216
227
  }
217
228
 
218
229
  #recover (exception) {
219
- if (exception !== REJECTION) return false
230
+ if (exception !== RETRANSMISSION) return false
220
231
  }
221
232
 
222
- #resend = () => {
233
+ #retransmit = () => {
223
234
  for (const emitter of Object.values(this.#emitters)) emitter.clear()
224
- for (const reply of this.#pendingReplies) reply.reject(REJECTION)
235
+
236
+ // trigger failsafe attribute
237
+ for (const reply of this.#pendingReplies) reply.reject(RETRANSMISSION)
225
238
  }
226
239
 
227
240
  /**
@@ -246,13 +259,7 @@ const OCTETS = 'application/octet-stream'
246
259
  /** @type {comq.encoding} */
247
260
  const DEFAULT = 'application/msgpack'
248
261
 
249
- /** @type {comq.diagnostics.event[]} */
250
- const CONNECTION_EVENTS = ['open', 'close']
251
-
252
- /** @type {comq.diagnostics.event[]} */
253
- const CHANNEL_EVENTS = ['flow', 'drain', 'recover', 'discard']
254
-
255
- const REJECTION = /** @type {Error} */ Symbol('resend')
262
+ const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
256
263
 
257
264
  function noop () {}
258
265