comq 0.22.0 → 0.23.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/source/channel.js CHANGED
@@ -27,14 +27,6 @@ class Channel {
27
27
  /** @type {string[]} */
28
28
  #tags = []
29
29
 
30
- /**
31
- * How each queue was declared, so that the queue holding what it could not
32
- * process is declared to live exactly as long as it does.
33
- *
34
- * @type {Map<string, comq.amqp.options.Queue>}
35
- */
36
- #queues = new Map()
37
-
38
30
  /**
39
31
  * The exchange and key each held queue is bound under, so that sealing withdraws them.
40
32
  *
@@ -88,8 +80,7 @@ class Channel {
88
80
  // the consumers of the previous channel went down with it, their tags mean nothing here
89
81
  this.#tags = []
90
82
 
91
- // a fresh channel has declared nothing, and bound nothing
92
- this.#queues.clear()
83
+ // a fresh channel has bound nothing
93
84
  this.#held.clear()
94
85
 
95
86
  await this.#channel.prefetch(this.#topology.prefetch)
@@ -384,8 +375,6 @@ class Channel {
384
375
 
385
376
  const { queue } = await this.#channel.assertQueue(name, options)
386
377
 
387
- this.#queues.set(queue, options)
388
-
389
378
  return [queue]
390
379
  }
391
380
 
@@ -485,21 +474,23 @@ class Channel {
485
474
  * The queue a message is kept in once it has run out of attempts. It has no consumer
486
475
  * either: what is in it is waiting for a person.
487
476
  *
488
- * It is declared to live exactly as long as the queue it serves, which for a groupless
489
- * subscriber means an exclusive queue whose contents go when the connection does. That
490
- * is deliberate: such a subscriber is ephemeral by construction, and a durable queue per
491
- * generated name would leak one per restart, forever. An ephemeral subscriber's failures
492
- * are ephemeral too.
477
+ * One queue serves every queue this channel consumes, as one retry queue serves every
478
+ * source that shares its delay. A parked message carries the queue it came from, so a
479
+ * queue per source would name what the message already says, and stay on the broker
480
+ * long after the queue it was named for has gone.
481
+ *
482
+ * It is durable whatever the channel is. A groupless subscriber's own queue goes with
483
+ * its connection, but what it could not process is waiting for a person, and there is
484
+ * nobody left to tell.
493
485
  *
494
- * @param {string} queue the queue the message was consumed from
495
486
  * @returns {Promise<void>}
496
487
  */
497
- async #assertParkedQueue (queue) {
488
+ async #assertParkedQueue () {
498
489
  if (!this.#topology.acknowledgments) return
499
490
 
500
- const options = this.#queues.get(queue) ?? (this.#topology.durable ? DURABLE : EXCLUSIVE)
501
-
502
- await this.#assertQueue(parkedQueueOf(queue), options)
491
+ // declared here rather than when a message is first parked, for the reason the retry
492
+ // queues are: the failure handler is the least likely moment for a declare to succeed
493
+ await this.#assertQueue(PARKED, DURABLE)
503
494
  }
504
495
 
505
496
  /** The waits between attempts, as a ladder even when it is one rung. */
@@ -556,8 +547,9 @@ class Channel {
556
547
  }
557
548
 
558
549
  /**
559
- * The retry topology is asserted once per channel and a parked queue once per queue:
560
- * `lazy` keys an initializer by the arguments it takes, and both again after a recovery.
550
+ * The retry and parking topology is asserted once per channel: `lazy` keys an
551
+ * initializer by the arguments it takes, and neither takes any. Both again after
552
+ * a recovery.
561
553
  */
562
554
  #consume = lazy(this, [this.#assertRetryQueue, this.#assertParkedQueue],
563
555
  /**
@@ -660,7 +652,7 @@ class Channel {
660
652
  [PARKED_AT_HEADER]: Date.now()
661
653
  })
662
654
 
663
- await this.#publish(DEFAULT, parkedQueueOf(queue), message.content, properties)
655
+ await this.#publish(DEFAULT, PARKED, message.content, properties)
664
656
 
665
657
  this.#channel.ack(message)
666
658
 
@@ -775,9 +767,9 @@ const INTERRUPTION = /** @type {Error} */ Symbol('internal interruption')
775
767
  const RESOURCE_LOCKED = 405
776
768
 
777
769
  const RETRY_PREFIX = 'comq.retry.'
778
- const PARKED_PREFIX = 'comq.parked.'
779
770
 
780
- const parkedQueueOf = (queue) => PARKED_PREFIX + queue
771
+ /** Where every message that has run out of attempts is kept, whatever it was consumed from. */
772
+ const PARKED = 'comq.parked'
781
773
 
782
774
  // Everything comq writes onto a message, under its own prefix: AMQP defines no retry
783
775
  // counter, so this one is comq's invention rather than a convention, and an unprefixed
package/source/index.js CHANGED
@@ -4,7 +4,6 @@ const { connect, assert } = require('./connect')
4
4
  const { Retry, Park } = require('./verdicts')
5
5
  const { Unroutable } = require('./unroutable')
6
6
  const { Interrupted } = require('./interrupted')
7
- const { Abandoned } = require('./abandoned')
8
7
 
9
8
  exports.connect = connect
10
9
  exports.assert = assert
@@ -13,4 +12,3 @@ exports.Retry = Retry
13
12
  exports.Park = Park
14
13
  exports.Unroutable = Unroutable
15
14
  exports.Interrupted = Interrupted
16
- exports.Abandoned = Abandoned
package/source/io.js CHANGED
@@ -6,7 +6,6 @@ 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')
9
- const { Abandoned } = require('./abandoned')
10
9
 
11
10
  const { decode } = require('./decode')
12
11
  const { encode } = require('./encode')
@@ -31,8 +30,15 @@ class IO {
31
30
  /** @type {comq.Channel} */
32
31
  #events
33
32
 
34
- /** @type {Map<string, comq.ReplyEmitter>} */
35
- #emitters = new Map()
33
+ /**
34
+ * Where this IO's replies arrive. One queue answers every request it makes: a reply is
35
+ * found by its correlation identifier, which is unique across processes, so a queue per
36
+ * target would name what the identifier already tells apart — and the broker would hold
37
+ * one for every distinct queue the caller has ever requested.
38
+ *
39
+ * @type {comq.ReplyEmitter}
40
+ */
41
+ #emitter
36
42
 
37
43
  /** @type {comq.ReplyEmitter | null} */
38
44
  #control = null
@@ -40,9 +46,6 @@ class IO {
40
46
  /** @type {Map<Promex, comq.Request>} */
41
47
  #pendingReplies = new Map()
42
48
 
43
- /** Whether this has stopped waiting for Replies, see `abandon`. */
44
- #abandoning = false
45
-
46
49
  /** @type {[comq.diagnostics.Event, Function][]} */
47
50
  #forwarders = []
48
51
 
@@ -254,30 +257,6 @@ class IO {
254
257
  await this.#destroyStreams(this.#replyStreams)
255
258
  })
256
259
 
257
- /**
258
- * Stops waiting for Replies: every one still outstanding is rejected with `Abandoned`, and a
259
- * Request made from now on is refused with it.
260
- *
261
- * A close waits for every consumer callback to return, and such a callback may be waiting on
262
- * a Request of its own. Where the answer to that Request is the very thing going away — the
263
- * process it would come from is shutting down alongside this one — the close waits for as
264
- * long as the answer takes. This is how that wait is ended. What the Requests themselves do
265
- * is not affected: they were sent and may well be processed; what is given up is this side's
266
- * interest in the answers.
267
- *
268
- * Callable at any time, including while a close is already under way.
269
- */
270
- abandon = memo(async () => {
271
- this.#abandoning = true
272
-
273
- // the same three steps `#abandon` takes for one Reply whose caller stopped waiting
274
- for (const [reply, request] of this.#pendingReplies) {
275
- request.emitter.off(request.properties.correlationId)
276
- this.#pendingReplies.delete(reply)
277
- reply.reject(new Abandoned())
278
- }
279
- })
280
-
281
260
  close = memo(async () => {
282
261
  await this.seal()
283
262
  await this.#destroyStreams(this.#replyPipes)
@@ -340,13 +319,20 @@ class IO {
340
319
  return [buffer, properties]
341
320
  }
342
321
 
343
- async #consumeReplies (queue) {
344
- const emitter = io.createReplyEmitter(queue)
345
- const consumer = this.#getReplyConsumer(queue, emitter)
322
+ async #consumeReplies () {
323
+ this.#emitter = await this.#consumeRepliesOf(REPLY)
324
+ }
346
325
 
347
- this.#emitters.set(queue, emitter)
326
+ /**
327
+ * @param {string} label
328
+ * @returns {Promise<comq.ReplyEmitter>}
329
+ */
330
+ async #consumeRepliesOf (label) {
331
+ const emitter = io.createReplyEmitter(label)
332
+
333
+ await this.#replies.consume(emitter.queue, this.#getReplyConsumer(emitter))
348
334
 
349
- await this.#replies.consume(emitter.queue, consumer)
335
+ return emitter
350
336
  }
351
337
 
352
338
  // endregion
@@ -417,11 +403,10 @@ class IO {
417
403
  })
418
404
 
419
405
  /**
420
- * @param {string} queue
421
406
  * @param {comq.ReplyEmitter} emitter
422
407
  * @returns {comq.channels.Consumer}
423
408
  */
424
- #getReplyConsumer = (queue, emitter) =>
409
+ #getReplyConsumer = (emitter) =>
425
410
  (message) => {
426
411
  const payload = decode(message)
427
412
 
@@ -453,11 +438,8 @@ class IO {
453
438
 
454
439
  signal?.throwIfAborted()
455
440
 
456
- // one made now would be waited for by nobody: the Replies are abandoned already
457
- if (this.#abandoning) throw new Abandoned()
458
-
459
441
  const [buffer, contentType] = this.#encode(payload, terms.encoding)
460
- const request = this.#createRequest(target, contentType, signal)
442
+ const request = this.#createRequest(contentType, signal)
461
443
  const properties = { ...request.properties }
462
444
 
463
445
  if (expires !== undefined) {
@@ -524,13 +506,12 @@ class IO {
524
506
  * The request holds no copy of what was sent: a retransmission encodes the
525
507
  * payload anew, and an unanswered request would otherwise keep two of it.
526
508
  *
527
- * @param {string} queue
528
509
  * @param {comq.Encoding} contentType
529
510
  * @param {AbortSignal} [signal]
530
511
  * @return {comq.Request}
531
512
  */
532
- #createRequest (queue, contentType, signal) {
533
- const emitter = this.#emitters.get(queue)
513
+ #createRequest (contentType, signal) {
514
+ const emitter = this.#emitter
534
515
  const correlationId = emitter.next()
535
516
 
536
517
  /** @type {comq.amqp.Properties} */
@@ -611,11 +592,7 @@ class IO {
611
592
  * @return {Promise<comq.ReplyEmitter>}
612
593
  */
613
594
  async #createControl () {
614
- const queue = 'control'
615
-
616
- await this.#consumeReplies(queue)
617
-
618
- return this.#emitters.get(queue)
595
+ return await this.#consumeRepliesOf(CONTROL)
619
596
  }
620
597
 
621
598
  /**
@@ -724,6 +701,12 @@ const DEFAULT = 'application/json'
724
701
 
725
702
  const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
726
703
 
704
+ /** What this IO's replies arrive on, before the random suffix that makes the queue its own. */
705
+ const REPLY = 'comq.reply'
706
+
707
+ /** What a producer steers its reply streams from, likewise. */
708
+ const CONTROL = 'control'
709
+
727
710
  /**
728
711
  * What a caller passed, settled once: a timeout becomes the moment the Request expires, so that
729
712
  * every attempt of it shares one deadline.
@@ -168,3 +168,43 @@ it('should heartbeat while idle', async () => {
168
168
 
169
169
  pipe.destroy()
170
170
  })
171
+
172
+ // the confirmation is published before the source is read, and a blocked broker holds the publish
173
+ // for as long as it is blocked: the source may fail in the meantime, and nobody listens to it yet
174
+ it('should survive a source that fails before the confirmation is sent', async () => {
175
+ const gate = new Promex()
176
+
177
+ reply.mockImplementation(async (message, properties) => {
178
+ sent.push([message, properties])
179
+ await gate
180
+
181
+ return true
182
+ })
183
+
184
+ const source = new Readable({ objectMode: true, read () {} })
185
+ const creating = ReplyPipe.create(request, source, channel, feedback, reply)
186
+
187
+ source.destroy(new Error('nothing arrived'))
188
+
189
+ await timeout(10)
190
+
191
+ gate.resolve()
192
+
193
+ const pipe = await creating
194
+
195
+ await closing(pipe)
196
+
197
+ expect(source.destroyed).toStrictEqual(true)
198
+ })
199
+
200
+ it('should destroy the source if the confirmation cannot be sent', async () => {
201
+ reply.mockImplementation(async () => {
202
+ throw new Error('channel closed')
203
+ })
204
+
205
+ const source = new Readable({ objectMode: true, read () {} })
206
+
207
+ await expect(ReplyPipe.create(request, source, channel, feedback, reply)).rejects.toThrow('channel closed')
208
+
209
+ expect(source.destroyed).toStrictEqual(true)
210
+ })
@@ -1040,6 +1040,7 @@ describe('release', () => {
1040
1040
  describe('failed messages', () => {
1041
1041
  const DELAY = 1000
1042
1042
  const RETRY = 'comq.retry.' + DELAY
1043
+ const PARKED = 'comq.parked'
1043
1044
 
1044
1045
  let queue
1045
1046
  let exception
@@ -1108,10 +1109,18 @@ describe('failed messages', () => {
1108
1109
  it('should assert the parked queue', async () => {
1109
1110
  await channel.consume(queue, consumer)
1110
1111
 
1111
- expect(chan.assertQueue).toHaveBeenCalledWith('comq.parked.' + queue,
1112
+ expect(chan.assertQueue).toHaveBeenCalledWith(PARKED,
1112
1113
  expect.objectContaining({ durable: true }))
1113
1114
  })
1114
1115
 
1116
+ it('should not name the parked queue after the queue it serves', async () => {
1117
+ await channel.consume(queue, consumer)
1118
+
1119
+ const named = chan.assertQueue.mock.calls.filter(([name]) => name.includes(queue))
1120
+
1121
+ expect(named).toHaveLength(1) // the source queue itself
1122
+ })
1123
+
1115
1124
  it('should assert the source queue first', async () => {
1116
1125
  await channel.consume(queue, consumer)
1117
1126
 
@@ -1130,15 +1139,14 @@ describe('failed messages', () => {
1130
1139
  expect(queues).toHaveLength(1)
1131
1140
  })
1132
1141
 
1133
- it('should assert a parked queue per consumed queue', async () => {
1134
- const one = generate()
1135
- const another = generate()
1142
+ it('should assert one parked queue however many queues are consumed', async () => {
1143
+ await channel.consume(generate(), consumer)
1144
+ await channel.consume(generate(), consumer)
1145
+ await channel.consume(generate(), consumer)
1136
1146
 
1137
- await channel.consume(one, consumer)
1138
- await channel.consume(another, consumer)
1147
+ const parked = chan.assertQueue.mock.calls.filter(([name]) => name === PARKED)
1139
1148
 
1140
- expect(chan.assertQueue).toHaveBeenCalledWith('comq.parked.' + one, expect.anything())
1141
- expect(chan.assertQueue).toHaveBeenCalledWith('comq.parked.' + another, expect.anything())
1149
+ expect(parked).toHaveLength(1)
1142
1150
  })
1143
1151
 
1144
1152
  it('should not assert either without acknowledgments', async () => {
@@ -1156,7 +1164,9 @@ describe('failed messages', () => {
1156
1164
  expect(chan.assertExchange).not.toHaveBeenCalled()
1157
1165
  })
1158
1166
 
1159
- it('should declare the parked queue exclusive for an exclusive queue', async () => {
1167
+ it('should declare the parked queue durable whatever the channel is', async () => {
1168
+ // what a groupless subscriber could not process is waiting for a person, and
1169
+ // there is nobody left to tell once its own queue has gone with its connection
1160
1170
  jest.clearAllMocks()
1161
1171
 
1162
1172
  topology.durable = false
@@ -1166,9 +1176,10 @@ describe('failed messages', () => {
1166
1176
  await channel.consume(queue, consumer)
1167
1177
 
1168
1178
  const [, options] = chan.assertQueue.mock.calls
1169
- .find(([name]) => name === 'comq.parked.' + queue)
1179
+ .find(([name]) => name === PARKED)
1170
1180
 
1171
- expect(options).toMatchObject({ exclusive: true })
1181
+ expect(options).toMatchObject({ durable: true })
1182
+ expect(options.exclusive).toBeUndefined()
1172
1183
  })
1173
1184
 
1174
1185
  it('should re-assert after recovery', async () => {
@@ -1181,7 +1192,7 @@ describe('failed messages', () => {
1181
1192
  const repl = await getCreatedChannel(replacement)
1182
1193
 
1183
1194
  expect(repl.assertExchange).toHaveBeenCalledWith(RETRY, 'fanout', expect.anything())
1184
- expect(repl.assertQueue).toHaveBeenCalledWith('comq.parked.' + queue, expect.anything())
1195
+ expect(repl.assertQueue).toHaveBeenCalledWith(PARKED, expect.anything())
1185
1196
  })
1186
1197
  })
1187
1198
 
@@ -1471,7 +1482,7 @@ describe('failed messages', () => {
1471
1482
 
1472
1483
  expect(asserted()).toStrictEqual(['comq.retry.1000'])
1473
1484
  expect(publications().map(([exchange, key]) => exchange || key))
1474
- .toStrictEqual(['comq.retry.1000', 'comq.parked.' + queue])
1485
+ .toStrictEqual(['comq.retry.1000', PARKED])
1475
1486
  })
1476
1487
  })
1477
1488
 
@@ -1486,7 +1497,7 @@ describe('failed messages', () => {
1486
1497
  const [exchange, key] = publications()[0]
1487
1498
 
1488
1499
  expect(exchange).toStrictEqual('')
1489
- expect(key).toStrictEqual('comq.parked.' + queue)
1500
+ expect(key).toStrictEqual(PARKED)
1490
1501
  })
1491
1502
 
1492
1503
  it('should not retry once the attempts are spent', async () => {
@@ -1563,7 +1574,7 @@ describe('failed messages', () => {
1563
1574
 
1564
1575
  const targets = publications().map(([, key]) => key)
1565
1576
 
1566
- expect(targets).toStrictEqual([queue, 'comq.parked.' + queue])
1577
+ expect(targets).toStrictEqual([queue, PARKED])
1567
1578
  })
1568
1579
 
1569
1580
  it('should give a message one delivery more than the ladder has rungs', async () => {
@@ -1581,7 +1592,7 @@ describe('failed messages', () => {
1581
1592
 
1582
1593
  const targets = publications().map(([, key]) => key)
1583
1594
 
1584
- expect(targets).toStrictEqual([queue, queue, 'comq.parked.' + queue])
1595
+ expect(targets).toStrictEqual([queue, queue, PARKED])
1585
1596
  })
1586
1597
  })
1587
1598
 
@@ -1596,7 +1607,7 @@ describe('failed messages', () => {
1596
1607
  const [exchange, key] = publications()[0]
1597
1608
 
1598
1609
  expect(exchange).toStrictEqual('')
1599
- expect(key).toStrictEqual('comq.parked.' + queue)
1610
+ expect(key).toStrictEqual(PARKED)
1600
1611
  expect(publications()).toHaveLength(1)
1601
1612
  })
1602
1613
 
@@ -1684,7 +1695,7 @@ describe('failed messages', () => {
1684
1695
 
1685
1696
  await deliver(delivery({}))
1686
1697
 
1687
- expect(publications()[0][1]).toStrictEqual('comq.parked.' + queue)
1698
+ expect(publications()[0][1]).toStrictEqual(PARKED)
1688
1699
  })
1689
1700
  })
1690
1701
 
@@ -41,7 +41,7 @@ let replies
41
41
 
42
42
  let promise
43
43
 
44
- const REPLY = new RegExp(`^${queue}..[0-9a-f]+$`)
44
+ const REPLY = /^comq\.reply\.\.[0-9a-f]+$/
45
45
 
46
46
  beforeEach(async () => {
47
47
  promise = io.request(queue, payload)
@@ -62,6 +62,17 @@ it('should consume replies queue', async () => {
62
62
  expect(replies.consume).toHaveBeenCalledWith(expect.stringMatching(REPLY), expect.any(Function))
63
63
  })
64
64
 
65
+ it('should consume one replies queue however many queues are requested', async () => {
66
+ // a reply is found by its correlation identifier, so a queue per target would
67
+ // name what the identifier already tells apart
68
+ io.request(generate(), payload)
69
+ io.request(generate(), payload)
70
+
71
+ await immediate()
72
+
73
+ expect(replies.consume).toHaveBeenCalledTimes(1)
74
+ })
75
+
65
76
  describe('send', () => {
66
77
  let call
67
78
 
@@ -84,9 +95,8 @@ describe('send', () => {
84
95
 
85
96
  it('should set replyTo', async () => {
86
97
  const properties = call[2]
87
- const rx = new RegExp(`^${queue}..[a-z0-9]+`)
88
98
 
89
- expect(properties.replyTo).toMatch(rx)
99
+ expect(properties.replyTo).toMatch(REPLY)
90
100
  })
91
101
 
92
102
  it('should consume replyTo', async () => {
package/types/index.d.ts CHANGED
@@ -38,9 +38,3 @@ export class Unroutable extends Error {
38
38
  export class Interrupted extends Error {
39
39
  readonly reason: string
40
40
  }
41
-
42
- /**
43
- * A Reply nobody is waiting for any more, or a Request made once the waiting has stopped.
44
- * See `IO.abandon`.
45
- */
46
- export class Abandoned extends Error {}
package/types/io.d.ts CHANGED
@@ -119,12 +119,6 @@ declare namespace comq {
119
119
 
120
120
  seal(): Promise<void>
121
121
 
122
- /**
123
- * Stops waiting for Replies: every one still outstanding is rejected with `Abandoned`, and a
124
- * Request made from now on is refused with it. Callable while a close is already under way.
125
- */
126
- abandon(): Promise<void>
127
-
128
122
  close(): Promise<void>
129
123
 
130
124
  diagnose(event: 'open', listener: (index?: number) => void): void
@@ -1,32 +0,0 @@
1
- Feature: Abandoning the Replies a close is waiting for
2
-
3
- A consumer callback may call out while it runs, and a close waits for every callback to return.
4
- Where the answer that callback waits for is the one thing going away with the close, the two
5
- wait for each other. Abandoning ends that wait.
6
-
7
- Background:
8
- Given an active connection to the broker
9
-
10
- Scenario: Abandoning lets a close finish
11
- Given a producer replying `upstream` queue by requesting the `downstream` queue
12
- When the consumer sends a request to the `upstream` queue
13
- And the producer is handling the request
14
- And the connection abandons its replies
15
- Then the connection closes within 1000ms
16
- And the producer was told its reply is abandoned
17
- And the consumer was told its own reply is abandoned
18
-
19
- Scenario: A close already under way is told to stop waiting
20
- Given a producer replying `upstream` queue by requesting the `downstream` queue
21
- When the consumer sends a request to the `upstream` queue
22
- And the producer is handling the request
23
- And the connection has started closing
24
- And the connection abandons its replies
25
- Then the connection closes within 1000ms
26
-
27
- Scenario: A Request made once the Replies are abandoned is refused
28
- Given a producer replying `upstream` queue by requesting the `downstream` queue
29
- When the consumer sends a request to the `upstream` queue
30
- And the producer is handling the request
31
- And the connection abandons its replies
32
- Then a request to the `downstream` queue is refused as abandoned
@@ -1,109 +0,0 @@
1
- 'use strict'
2
-
3
- const assert = require('node:assert')
4
- const { Given, When, Then } = require('@cucumber/cucumber')
5
- const { Abandoned } = require('../../')
6
- const { timeout } = require('../../test/helpers')
7
-
8
- Given('a producer replying {token} queue by requesting the {token} queue',
9
- /**
10
- * @param {string} queue
11
- * @param {string} downstream
12
- * @this {comq.features.Context}
13
- */
14
- async function (queue, downstream) {
15
- this.handling = new Promise((resolve) => { this.handled = resolve })
16
-
17
- const producer = async (request) => {
18
- const reply = this.io.request(downstream, request)
19
-
20
- this.handled()
21
-
22
- try {
23
- return await reply
24
- } catch (error) {
25
- // recorded rather than rethrown: what this asserts is what the caller was told,
26
- // and a rejected producer would have the delivery retried on top of it
27
- this.abandoned = error
28
-
29
- return null
30
- }
31
- }
32
-
33
- await this.io.reply(queue, producer)
34
- })
35
-
36
- When('the producer is handling the request',
37
- /**
38
- * @this {comq.features.Context}
39
- */
40
- async function () {
41
- await this.handling
42
-
43
- // the consumer's own Request is outstanding as well, and abandoning abandons that one too;
44
- // its outcome is taken here so that it is asserted rather than left to surface as an
45
- // unhandled rejection
46
- this.reply.catch((error) => { this.refused = error })
47
- })
48
-
49
- When('the connection has started closing',
50
- /**
51
- * @this {comq.features.Context}
52
- */
53
- async function () {
54
- this.closing = this.io.close()
55
- })
56
-
57
- When('the connection abandons its replies',
58
- /**
59
- * @this {comq.features.Context}
60
- */
61
- async function () {
62
- await this.io.abandon()
63
- })
64
-
65
- Then('the connection closes within {number}ms',
66
- /**
67
- * @param {number} ms
68
- * @this {comq.features.Context}
69
- */
70
- async function (ms) {
71
- const io = this.io
72
-
73
- // taken off the world so that the hook on the way out does not await a close
74
- // this may have just proven does not end
75
- this.io = undefined
76
- this.connected = false
77
-
78
- const closing = io.close().then(() => true, () => true)
79
- const closed = await Promise.race([closing, timeout(ms).then(() => false)])
80
-
81
- assert.ok(closed, `The connection did not close within ${ms}ms`)
82
- })
83
-
84
- Then('the producer was told its reply is abandoned',
85
- /**
86
- * @this {comq.features.Context}
87
- */
88
- async function () {
89
- assert.ok(this.abandoned instanceof Abandoned,
90
- `The producer was told ${this.abandoned ?? 'nothing'}`)
91
- })
92
-
93
- Then('the consumer was told its own reply is abandoned',
94
- /**
95
- * @this {comq.features.Context}
96
- */
97
- async function () {
98
- assert.ok(this.refused instanceof Abandoned,
99
- `The consumer was told ${this.refused ?? 'nothing'}`)
100
- })
101
-
102
- Then('a request to the {token} queue is refused as abandoned',
103
- /**
104
- * @param {string} queue
105
- * @this {comq.features.Context}
106
- */
107
- async function (queue) {
108
- await assert.rejects(() => this.io.request(queue, null), Abandoned)
109
- })
@@ -1,15 +0,0 @@
1
- 'use strict'
2
-
3
- /**
4
- * A Reply nobody is waiting for any more, or a Request made once the waiting has stopped. The
5
- * Request itself was sent and may well be processed; what was given up is the answer.
6
- */
7
- class Abandoned extends Error {
8
- constructor () {
9
- super('The connection no longer waits for a Reply')
10
-
11
- this.name = 'Abandoned'
12
- }
13
- }
14
-
15
- exports.Abandoned = Abandoned