comq 0.24.0 → 0.25.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 (48) hide show
  1. package/.husky/commit-msg +5 -0
  2. package/benchmarks/run.js +66 -4
  3. package/benchmarks/server.js +14 -0
  4. package/commitlint.config.js +6 -0
  5. package/features/heartbeat.feature +16 -0
  6. package/features/rpc.call.recovery.feature +2 -2
  7. package/features/rpc.shards.retransmission.feature +46 -0
  8. package/features/rpc.streams.feature +28 -0
  9. package/features/shards.feature +1 -1
  10. package/features/silence.feature +3 -2
  11. package/features/steps/.types/context.d.ts +6 -1
  12. package/features/steps/brokers.js +1 -1
  13. package/features/steps/connection.js +23 -0
  14. package/features/steps/context.js +1 -2
  15. package/features/steps/hooks.js +0 -1
  16. package/features/steps/network.js +11 -2
  17. package/features/steps/networks.js +31 -2
  18. package/features/steps/retransmission.js +165 -0
  19. package/features/steps/rpc.js +31 -7
  20. package/package.json +31 -9
  21. package/readme.md +15 -4
  22. package/source/.io/ReplyPipe.js +130 -11
  23. package/source/.io/ReplyStream.js +41 -2
  24. package/source/.io/const.js +6 -0
  25. package/source/attributes/track.test.js +1 -1
  26. package/source/channel.js +5 -3
  27. package/source/connection.js +11 -88
  28. package/source/io.js +91 -16
  29. package/source/promex.js +47 -0
  30. package/source/promex.test.js +79 -0
  31. package/source/retry.js +42 -0
  32. package/source/retry.test.js +120 -0
  33. package/source/shards/channel.js +74 -13
  34. package/test/.types/jest.d.ts +22 -0
  35. package/test/ReplyPipe.test.js +71 -2
  36. package/test/ReplyStream.test.js +33 -0
  37. package/test/connection.mock.js +3 -2
  38. package/test/connection.test.js +16 -78
  39. package/test/globals.js +143 -0
  40. package/test/io.call.test.js +5 -3
  41. package/test/io.close.test.js +1 -1
  42. package/test/io.reply.test.js +5 -3
  43. package/test/io.request.test.js +264 -2
  44. package/test/memory.test.js +0 -2
  45. package/test/shards/channel.test.js +169 -3
  46. package/test/shards/connection.test.js +2 -3
  47. package/types/channel.d.ts +13 -4
  48. package/types/io.d.ts +9 -0
package/source/io.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const stream = require('node:stream')
4
4
  const { setTimeout } = require('node:timers/promises')
5
- const { Promex } = require('promex')
5
+ const { Promex } = require('./promex')
6
6
  const { memo, failsafe, lazy, track } = require('./attributes')
7
7
  const { verdictOf, PARK } = require('./verdicts')
8
8
  const { Unroutable } = require('./unroutable')
@@ -154,7 +154,7 @@ class IO {
154
154
  }
155
155
 
156
156
  return await this.#ask(queue, payload, terms,
157
- (buffer, properties) => this.#requests.send(queue, buffer, properties))
157
+ (buffer, properties, via) => this.#requests.send(queue, buffer, properties, via))
158
158
  }))
159
159
 
160
160
  // the terms are taken once, so a re-sent call keeps the deadline of the first
@@ -170,7 +170,8 @@ class IO {
170
170
  async (exchange, key, payload, terms) => {
171
171
  // mandatory: a Request nobody holds the key of is returned rather than dropped
172
172
  return await this.#ask(exchange, payload, terms,
173
- (buffer, properties) => this.#requests.route(exchange, key, buffer, { ...properties, mandatory: true }))
173
+ (buffer, properties, via) =>
174
+ this.#requests.route(exchange, key, buffer, { ...properties, mandatory: true }, via))
174
175
  }))
175
176
 
176
177
  consume = lazy(this, this.#createEventChannel,
@@ -357,12 +358,20 @@ class IO {
357
358
  return channel
358
359
  }
359
360
 
361
+ /**
362
+ * On a sharded connection, what is re-sent is what the shard in question holds: the index of
363
+ * the shard comes with each of the events, and a Request that went through another shard is
364
+ * answered through that one.
365
+ */
360
366
  #setupRetransmission () {
361
367
  if (this.#requests.sharded === true) {
362
368
  // a shard leaves the pool when it rejects a publish, and is lost when its
363
369
  // connection drops, which leaves an already sent request unanswered
364
370
  this.#requests.diagnose('remove', this.#retransmit)
365
371
  this.#requests.diagnose('lost', this.#retransmit)
372
+
373
+ // a returned call is published on another shard, and is held by that one from then on
374
+ this.#requests.diagnose('reroute', this.#rerouted)
366
375
  } else {
367
376
  this.#requests.diagnose('recover', this.#retransmit)
368
377
  }
@@ -462,11 +471,33 @@ class IO {
462
471
 
463
472
  if (signal !== undefined) this.#abandon(request, reply, signal)
464
473
 
465
- await abortable(publish(buffer, properties), signal)
474
+ request.publishing = true
475
+
476
+ try {
477
+ await abortable(publish(buffer, properties, (index) => this.#board(request, index)), signal)
478
+ } finally {
479
+ request.publishing = false
480
+ }
481
+
482
+ // the shard it went through was lost while it was being published, and it has not failed
483
+ // over to another since
484
+ if (request.lost === true) this.#resend(request, reply)
466
485
 
467
486
  return reply
468
487
  }
469
488
 
489
+ /**
490
+ * Records the shard a Request is published through. It is recorded as the shard is chosen,
491
+ * rather than once the publish is done, so that a shard lost in between is known to hold it.
492
+ *
493
+ * @param {comq.Request} request
494
+ * @param {number} index
495
+ */
496
+ #board (request, index) {
497
+ request.shard = index
498
+ request.lost = false
499
+ }
500
+
470
501
  /**
471
502
  * Stops waiting for a Reply once the signal aborts. The Request is forgotten at once, so a
472
503
  * retransmission leaves it be, and a Reply arriving later finds nobody waiting for it.
@@ -494,17 +525,41 @@ class IO {
494
525
  * @param {comq.amqp.Message} message
495
526
  */
496
527
  #returned = (message) => {
528
+ const pending = this.#pendingOf(message)
529
+
530
+ if (pending === undefined) return
531
+
532
+ const [reply, request] = pending
533
+
534
+ request.emitter.off(request.properties.correlationId)
535
+ this.#pendingReplies.delete(reply)
536
+ reply.reject(new Unroutable(message.fields.exchange, message.fields.routingKey))
537
+ }
538
+
539
+ /**
540
+ * A Request one shard returned is published on another.
541
+ *
542
+ * @param {comq.amqp.Message} message
543
+ * @param {number} index
544
+ */
545
+ #rerouted = (message, index) => {
546
+ const pending = this.#pendingOf(message)
547
+
548
+ if (pending !== undefined) this.#board(pending[1], index)
549
+ }
550
+
551
+ /**
552
+ * @param {comq.amqp.Message} message
553
+ * @return {[Promex, comq.Request] | undefined}
554
+ */
555
+ #pendingOf (message) {
497
556
  const { correlationId, replyTo } = message.properties
498
557
 
499
558
  for (const [reply, request] of this.#pendingReplies) {
500
559
  if (request.properties.correlationId !== correlationId) continue
501
560
  if (request.emitter.queue !== replyTo) continue
502
561
 
503
- request.emitter.off(correlationId)
504
- this.#pendingReplies.delete(reply)
505
- reply.reject(new Unroutable(message.fields.exchange, message.fields.routingKey))
506
-
507
- return
562
+ return [reply, request]
508
563
  }
509
564
  }
510
565
 
@@ -665,24 +720,44 @@ class IO {
665
720
  // an unroutable reply must be returned by the broker rather than dropped
666
721
  properties.mandatory = true
667
722
 
668
- return await this.#replies.fire(replyTo, buffer, properties)
723
+ // back the way the request came: see `fire` of a sharded channel
724
+ return await this.#replies.fire(replyTo, buffer, properties, request)
669
725
  }
670
726
 
671
727
  #recover (exception) {
672
728
  if (exception !== RETRANSMISSION) return false
673
729
  }
674
730
 
675
- #retransmit = () => {
731
+ /**
732
+ * Re-sends the Requests awaiting their replies on a shard, or on the connection when no shard
733
+ * is given. A Request that went through another shard is not re-sent: it is answered through
734
+ * that shard, and it is not for comq to assume that it is idempotent.
735
+ *
736
+ * @param {number} [index]
737
+ */
738
+ #retransmit = (index) => {
676
739
  for (const [reply, request] of this.#pendingReplies) {
677
- // detaching this attempt alone leaves the listeners of the reply streams
678
- // that are still flowing over the other shards in place
679
- request.emitter.off(request.properties.correlationId)
740
+ if (index !== undefined && request.shard !== index) continue
680
741
 
681
- // trigger failsafe attribute
682
- reply.reject(RETRANSMISSION)
742
+ // one still being published is re-sent once it is, unless it fails over to another shard
743
+ if (request.publishing === true) request.lost = true
744
+ else this.#resend(request, reply)
683
745
  }
684
746
  }
685
747
 
748
+ /**
749
+ * @param {comq.Request} request
750
+ * @param {Promex} reply
751
+ */
752
+ #resend (request, reply) {
753
+ // detaching this attempt alone leaves the listeners of the reply streams
754
+ // that are still flowing over the other shards in place
755
+ request.emitter.off(request.properties.correlationId)
756
+
757
+ // trigger failsafe attribute
758
+ reply.reject(RETRANSMISSION)
759
+ }
760
+
686
761
  /**
687
762
  * @param {any} payload
688
763
  * @param {comq.Encoding} [contentType]
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * A promise settled by whoever holds it, rather than only from within its executor.
5
+ * `callback` settles it the way node-style callbacks report: an error, or a value.
6
+ *
7
+ * @template T
8
+ * @extends {Promise<T>}
9
+ */
10
+ class Promex extends Promise {
11
+ /** @type {(value?: T | PromiseLike<T>) => void} */
12
+ resolve
13
+
14
+ /** @type {(reason?: any) => void} */
15
+ reject
16
+
17
+ /**
18
+ * @param {(resolve: (value?: any) => void, reject: (reason?: any) => void) => void} [executor]
19
+ */
20
+ constructor (executor = noop) {
21
+ let ok
22
+ let oh
23
+
24
+ super((resolve, reject) => {
25
+ ok = resolve
26
+ oh = reject
27
+
28
+ return executor(resolve, reject)
29
+ })
30
+
31
+ this.resolve = ok
32
+ this.reject = oh
33
+ }
34
+
35
+ /**
36
+ * @param {any} error
37
+ * @param {T} [value]
38
+ */
39
+ callback = (error, value) => {
40
+ if (error !== null && error !== undefined) this.reject(error)
41
+ else this.resolve(value)
42
+ }
43
+ }
44
+
45
+ function noop () {}
46
+
47
+ exports.Promex = Promex
@@ -0,0 +1,79 @@
1
+ 'use strict'
2
+
3
+ const { Promex } = require('./promex')
4
+
5
+ it('should resolve', async () => {
6
+ const promise = new Promex()
7
+
8
+ setImmediate(() => promise.resolve('ok'))
9
+
10
+ await expect(promise).resolves.toStrictEqual('ok')
11
+ })
12
+
13
+ it('should reject', async () => {
14
+ const promise = new Promex()
15
+
16
+ setImmediate(() => promise.reject(new Error('oh')))
17
+
18
+ await expect(promise).rejects.toThrow('oh')
19
+ })
20
+
21
+ it('should resolve using the callback', async () => {
22
+ const promise = new Promex()
23
+
24
+ setImmediate(() => promise.callback(null, 'ok'))
25
+
26
+ await expect(promise).resolves.toStrictEqual('ok')
27
+ })
28
+
29
+ it('should reject using the callback', async () => {
30
+ const promise = new Promex()
31
+
32
+ setImmediate(() => promise.callback('oh'))
33
+
34
+ await expect(promise).rejects.toStrictEqual('oh')
35
+ })
36
+
37
+ it('should resolve using the callback given no error', async () => {
38
+ const promise = new Promex()
39
+
40
+ setImmediate(() => promise.callback(undefined, 'ok'))
41
+
42
+ await expect(promise).resolves.toStrictEqual('ok')
43
+ })
44
+
45
+ it('should run the executor it is given', async () => {
46
+ const promise = new Promex((resolve) => resolve('ok'))
47
+
48
+ await expect(promise).resolves.toStrictEqual('ok')
49
+ })
50
+
51
+ it('should stay settled by whoever holds it', async () => {
52
+ const promise = new Promex()
53
+
54
+ promise.resolve('first')
55
+ promise.resolve('second')
56
+
57
+ await expect(promise).resolves.toStrictEqual('first')
58
+ })
59
+
60
+ it('should be a promise', async () => {
61
+ const promise = new Promex()
62
+
63
+ expect(promise).toBeInstanceOf(Promise)
64
+
65
+ promise.resolve('ok')
66
+
67
+ await expect(Promise.all([promise])).resolves.toStrictEqual(['ok'])
68
+ })
69
+
70
+ it('should derive promises of its own kind', async () => {
71
+ const promise = new Promex()
72
+ const derived = promise.then((value) => value + '!')
73
+
74
+ expect(derived).toBeInstanceOf(Promex)
75
+
76
+ promise.resolve('ok')
77
+
78
+ await expect(derived).resolves.toStrictEqual('ok!')
79
+ })
@@ -0,0 +1,42 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Runs an operation until it stops asking for another attempt, waiting longer before
5
+ * each one. The operation is handed the token it returns to ask for a retry; anything
6
+ * else it returns is the result, and whatever it throws is thrown on.
7
+ *
8
+ * @template T
9
+ * @param {(again: symbol) => Promise<T | symbol>} operation
10
+ * @param {Partial<typeof DEFAULTS>} [options]
11
+ * @returns {Promise<T>}
12
+ */
13
+ async function retry (operation, options) {
14
+ const { attempts, base, max, factor, dispersion } = { ...DEFAULTS, ...options }
15
+
16
+ for (let attempt = 0; attempt < attempts; attempt++) {
17
+ const result = await operation(again)
18
+
19
+ if (result !== again) return /** @type {T} */ (result)
20
+
21
+ const interval = Math.min(base * factor ** attempt, max)
22
+
23
+ // attempts that started together are spread out rather than kept in step
24
+ const spread = interval * dispersion * (Math.random() - 0.5)
25
+
26
+ await new Promise((resolve) => setTimeout(resolve, interval + spread))
27
+ }
28
+
29
+ throw new Error('Maximum attempts exceeded')
30
+ }
31
+
32
+ const again = Symbol('again')
33
+
34
+ const DEFAULTS = {
35
+ attempts: Infinity,
36
+ base: 1000,
37
+ max: 30000,
38
+ factor: 1.5,
39
+ dispersion: 0.1
40
+ }
41
+
42
+ exports.retry = retry
@@ -0,0 +1,120 @@
1
+ 'use strict'
2
+
3
+ const { retry } = require('./retry')
4
+
5
+ const immediate = { base: 0, max: 0, dispersion: 0 }
6
+
7
+ it('should return the result', async () => {
8
+ const result = await retry(async () => 'ok', immediate)
9
+
10
+ expect(result).toStrictEqual('ok')
11
+ })
12
+
13
+ it('should retry while the operation asks for it', async () => {
14
+ let attempts = 0
15
+
16
+ const result = await retry(async (again) => {
17
+ attempts++
18
+
19
+ return attempts < 3 ? again : 'ok'
20
+ }, immediate)
21
+
22
+ expect(attempts).toStrictEqual(3)
23
+ expect(result).toStrictEqual('ok')
24
+ })
25
+
26
+ it('should pass the same token on every attempt', async () => {
27
+ const tokens = []
28
+
29
+ await retry(async (again) => {
30
+ tokens.push(again)
31
+
32
+ return tokens.length < 3 ? again : 'ok'
33
+ }, immediate)
34
+
35
+ expect(tokens).toHaveLength(3)
36
+ expect(new Set(tokens).size).toStrictEqual(1)
37
+ })
38
+
39
+ it('should throw what the operation throws', async () => {
40
+ const exception = new Error('nope')
41
+
42
+ await expect(retry(async () => { throw exception }, immediate)).rejects.toThrow(exception)
43
+ })
44
+
45
+ it('should give up after the last attempt', async () => {
46
+ let attempts = 0
47
+
48
+ const attempt = retry(async (again) => {
49
+ attempts++
50
+
51
+ return again
52
+ }, { ...immediate, attempts: 3 })
53
+
54
+ await expect(attempt).rejects.toThrow('Maximum attempts exceeded')
55
+ expect(attempts).toStrictEqual(3)
56
+ })
57
+
58
+ it('should not run the operation without an attempt to spare', async () => {
59
+ const operation = jest.fn(async (again) => again)
60
+
61
+ await expect(retry(operation, { ...immediate, attempts: 0 }))
62
+ .rejects.toThrow('Maximum attempts exceeded')
63
+
64
+ expect(operation).not.toHaveBeenCalled()
65
+ })
66
+
67
+ it('should back off, up to the maximum, before each attempt', async () => {
68
+ jest.useFakeTimers()
69
+
70
+ try {
71
+ const waits = []
72
+
73
+ let last = Date.now()
74
+ let attempts = 0
75
+
76
+ const attempt = retry(async (again) => {
77
+ waits.push(Date.now() - last)
78
+ last = Date.now()
79
+ attempts++
80
+
81
+ return attempts < 5 ? again : 'ok'
82
+ }, { base: 100, factor: 2, max: 300, dispersion: 0 })
83
+
84
+ await jest.advanceTimersByTimeAsync(10_000)
85
+
86
+ expect(await attempt).toStrictEqual('ok')
87
+
88
+ // the first attempt waits for nothing; then 100, 200, and 400 capped at 300
89
+ expect(waits).toStrictEqual([0, 100, 200, 300, 300])
90
+ } finally {
91
+ jest.useRealTimers()
92
+ }
93
+ })
94
+
95
+ it('should spread the wait around the interval', async () => {
96
+ const waits = []
97
+
98
+ const timer = global.setTimeout
99
+
100
+ global.setTimeout = (callback, ms) => {
101
+ waits.push(ms)
102
+
103
+ return timer(callback, 0)
104
+ }
105
+
106
+ try {
107
+ let attempts = 0
108
+
109
+ await retry(async (again) => {
110
+ attempts++
111
+
112
+ return attempts < 20 ? again : 'ok'
113
+ }, { base: 1000, factor: 1, max: 1000, dispersion: 0.1 })
114
+ } finally {
115
+ global.setTimeout = timer
116
+ }
117
+
118
+ expect(waits.every((ms) => ms >= 950 && ms <= 1050)).toStrictEqual(true)
119
+ expect(new Set(waits).size).toBeGreaterThan(1)
120
+ })
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { Promex } = require('promex')
3
+ const { Promex } = require('../promex')
4
4
  const { RETRY_PREFIX } = require('../channel')
5
5
  const events = require('../events')
6
6
  const emitter = require('../emitter')
@@ -57,8 +57,12 @@ class Channel {
57
57
  await Promise.any(promises)
58
58
  }
59
59
 
60
+ /**
61
+ * A message is told which shard it arrived on, so that whatever answers it can go back the same
62
+ * way. See `fire`.
63
+ */
60
64
  async consume (queue, consumer) {
61
- return await this.#every((channel) => channel.consume(queue, consumer))
65
+ return await this.#every((channel) => channel.consume(queue, arrival(consumer, channel)))
62
66
  }
63
67
 
64
68
  async subscribe (queue, group, consumer) {
@@ -74,24 +78,51 @@ class Channel {
74
78
  * claiming it, and holds it as well once it is let go.
75
79
  */
76
80
  async held (exchange, queue, key, consumer) {
77
- await Promise.any(this.#apply((channel) => channel.held(exchange, queue, key, consumer)))
81
+ await Promise.any(this.#apply((channel) => channel.held(exchange, queue, key, arrival(consumer, channel))))
78
82
  }
79
83
 
80
- async send (queue, buffer, options) {
81
- await this.#one((channel) => channel.send(queue, buffer, options))
84
+ /**
85
+ * @param {string} queue
86
+ * @param {Buffer} buffer
87
+ * @param {comq.amqp.options.Publish} [options]
88
+ * @param {(index: number) => void} [via] told which shard the message is published through,
89
+ * as soon as one is chosen and again whenever the publish fails over to another
90
+ */
91
+ async send (queue, buffer, options, via) {
92
+ await this.#one((channel) => channel.send(queue, buffer, options), { via })
82
93
  }
83
94
 
84
95
  async publish (exchange, buffer, options) {
85
96
  await this.#one((channel) => channel.publish(exchange, buffer, options))
86
97
  }
87
98
 
88
- async route (exchange, key, buffer, options) {
89
- await this.#one((channel) => channel.route(exchange, key, buffer, options))
99
+ /**
100
+ * @param {string} exchange
101
+ * @param {string} key
102
+ * @param {Buffer} buffer
103
+ * @param {comq.amqp.options.Publish} [options]
104
+ * @param {(index: number) => void} [via] as `send` has it
105
+ */
106
+ async route (exchange, key, buffer, options, via) {
107
+ await this.#one((channel) => channel.route(exchange, key, buffer, options), { via })
90
108
  }
91
109
 
92
- async fire (queue, buffer, options) {
110
+ /**
111
+ * A reply goes back through the shard the message it answers arrived on. Whoever sent that
112
+ * message re-sends what it sent through a shard once the shard is lost, and nothing else, so a
113
+ * reply taking another way could be lost with a shard that nothing is re-sent for. It takes
114
+ * another way when that shard is not reachable, which leaves its message unacknowledged there,
115
+ * to be delivered and answered again.
116
+ *
117
+ * @param {string} queue
118
+ * @param {Buffer} buffer
119
+ * @param {comq.amqp.options.Publish} [options]
120
+ * @param {comq.amqp.Message} [origin] the message this one answers
121
+ * @return {Promise<boolean>}
122
+ */
123
+ async fire (queue, buffer, options, origin) {
93
124
  // noinspection JSValidateTypes
94
- return await this.#one((channel) => channel.fire(queue, buffer, options))
125
+ return await this.#one((channel) => channel.fire(queue, buffer, options), { prefer: origin?.[ARRIVAL] })
95
126
  }
96
127
 
97
128
  async close () {
@@ -230,6 +261,9 @@ class Channel {
230
261
 
231
262
  const next = rest[Math.floor(Math.random() * rest.length)]
232
263
 
264
+ // the message is on that shard now, and is lost with it rather than with this one
265
+ this.#diagnostics.emit(REROUTE, message, next.index)
266
+
233
267
  // a Request under a key goes through its routed exchange, which `route` declares on that
234
268
  // shard before publishing: publishing to an exchange a broker lacks closes the connection
235
269
  const retried = exchange === DEFAULT
@@ -374,9 +408,13 @@ class Channel {
374
408
  }
375
409
 
376
410
  /**
411
+ * Publishes through a shard chosen at random, or through the one preferred while it is
412
+ * reachable, and fails over to another when that one fails.
413
+ *
377
414
  * @param {(channel: comq.Channel) => void} fn
415
+ * @param {{ via?: (index: number) => void, prefer?: number }} [route]
378
416
  */
379
- async #one (fn) {
417
+ async #one (fn, route = {}) {
380
418
  // capture before the check: recover may replace `#recovery` in between
381
419
  const waiting = this.#recovery
382
420
  const pool = this.#reachable()
@@ -384,21 +422,38 @@ class Channel {
384
422
  if (pool.length === 0) {
385
423
  await waiting
386
424
 
387
- return this.#one(fn)
425
+ return this.#one(fn, route)
388
426
  }
389
427
 
390
- const channel = pool[Math.floor(Math.random() * pool.length)]
428
+ const channel = pool.find((channel) => channel.index === route.prefer) ??
429
+ pool[Math.floor(Math.random() * pool.length)]
430
+
431
+ // told before the publish is awaited: a shard lost while it is under way has it on board
432
+ route.via?.(channel.index)
391
433
 
392
434
  try {
393
435
  return await fn(channel)
394
436
  } catch {
395
437
  this.#remove(channel)
396
438
 
397
- return this.#one(fn)
439
+ return this.#one(fn, route)
398
440
  }
399
441
  }
400
442
  }
401
443
 
444
+ /**
445
+ * @param {comq.channels.Consumer} consumer
446
+ * @param {comq.Channel} channel
447
+ * @return {comq.channels.Consumer}
448
+ */
449
+ function arrival (consumer, channel) {
450
+ return (message) => {
451
+ message[ARRIVAL] = channel.index
452
+
453
+ return consumer(message)
454
+ }
455
+ }
456
+
402
457
  /**
403
458
  * @param {comq.Connection[]} connections
404
459
  * @param {comq.topology.type} type
@@ -418,6 +473,12 @@ const REMOVE = 'remove'
418
473
  const RETURN_HEADER = 'x-return'
419
474
  const LOST = 'lost'
420
475
 
476
+ /** A returned message published on another shard, which is where it can be lost from now on. */
477
+ const REROUTE = 'reroute'
478
+
479
+ /** Which shard a consumed message arrived on. */
480
+ const ARRIVAL = Symbol('arrival')
481
+
421
482
  function noop () {}
422
483
 
423
484
  exports.create = create
@@ -0,0 +1,22 @@
1
+ import * as mock from 'jest-mock'
2
+
3
+ // The test files are written against Jest's mock types, and keep being: the mocks are still
4
+ // Jest's, taken as the standalone `jest-mock` package. This is what @types/jest declared of it.
5
+
6
+ declare global {
7
+
8
+ namespace jest {
9
+
10
+ type Mock<T extends mock.FunctionLike = mock.UnknownFunction> = mock.Mock<T>
11
+
12
+ type Mocked<T extends object> = mock.Mocked<T>
13
+
14
+ type MockedClass<T extends mock.ClassLike> = mock.MockedClass<T>
15
+
16
+ type MockedFunction<T extends mock.FunctionLike = mock.UnknownFunction> = mock.MockedFunction<T>
17
+
18
+ type MockedFn<T extends mock.FunctionLike = mock.UnknownFunction> = mock.MockedFunction<T>
19
+
20
+ type MockedObject<T extends object> = mock.MockedObject<T>
21
+ }
22
+ }