comq 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -6,7 +6,8 @@ for distributed, eventually consistent systems running on Node.js.
6
6
  ## Features
7
7
 
8
8
  - [Dynamic topology](#topology)
9
- - [Request](#request)-[reply](#reply) (RPC)
9
+ - [Request](#request)-[reply](#reply) (RPC), with a [timeout](#timeout)
10
+ - [Addressed requests](#addressed-requests) to the one connection holding a Key
10
11
  - Events ([pub](#emission)/[sub](#consumption)), fanned out or [routed](#routing)
11
12
  - [Tasks](#tasks)
12
13
  - [Pipelines](#pipelines)
@@ -106,19 +107,50 @@ await io.reply('add_numbers', ({ a, b }) => (a + b))
106
107
 
107
108
  ## Request
108
109
 
109
- `async IO.request(queue: string, payload: any, encoding?: string): any`
110
+ `async IO.request(queue: string, payload: any, options?: string | RequestOptions): any`
110
111
 
111
112
  Send encoded Request message with `replyTo` and `correlationId` properties set and
112
- return decoded Reply content. The promise stays pending until the Reply arrives.
113
+ return decoded Reply content. The promise stays pending until the Reply arrives, or until the
114
+ [timeout](#timeout) passes.
113
115
 
114
116
  On the initial call, queues for Requests and Replies are asserted.
115
117
 
118
+ `options` is the encoding, or an object:
119
+
120
+ | Option | Type | Default |
121
+ |------------|---------------|--------------------|
122
+ | `encoding` | `string` | `application/json` |
123
+ | `timeout` | `number`, ms | none |
124
+ | `signal` | `AbortSignal` | none |
125
+
116
126
  ### Example
117
127
 
118
128
  ```javascript
119
129
  const sum = await io.request('add_numbers', { a: 1, b: 2 })
120
130
  ```
121
131
 
132
+ ### Timeout
133
+
134
+ A Request with a `timeout` rejects once it passes, with the `TimeoutError` of
135
+ [`AbortSignal.timeout`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static).
136
+ It is published with that much
137
+ [expiration](https://www.rabbitmq.com/docs/ttl#per-message-ttl-in-publishers), so a Request no
138
+ Producer has taken by then is dropped by the broker and never processed. A Request a Producer has
139
+ already taken is processed to the end, and its Reply is discarded. A Request re-sent after a lost
140
+ connection carries the time it has left.
141
+
142
+ A `signal` rejects the Request with its reason once aborted, within the `timeout` where both are
143
+ given. It ends the wait and leaves the Request where it is: in its queue until the `timeout`
144
+ passes, or, without a `timeout`, until a Producer takes it. A Request abandoned by its `signal` may
145
+ therefore still be processed.
146
+
147
+ A Request that failed and waits for its [next attempt](#retries) loses its expiration on the way
148
+ back to its queue, so it may be processed after its caller has stopped waiting.
149
+
150
+ ```javascript
151
+ const sum = await io.request('add_numbers', { a: 1, b: 2 }, { timeout: 5000 })
152
+ ```
153
+
122
154
  ## Consumption
123
155
 
124
156
  `async IO.consume(exchange: string, group?: string, consumer): void`
@@ -194,6 +226,65 @@ await io.route('records', 'store.orders', { id: 1, status: 'paid' })
194
226
  await io.route('records', 'store.customers', { id: 2 }) // not delivered to the above
195
227
  ```
196
228
 
229
+ ## Addressed Requests
230
+
231
+ `async IO.back(exchange: string, key: string, producer): void`
232
+
233
+ `async IO.call(exchange: string, key: string, payload: any, options?: string | RequestOptions): any`
234
+
235
+ A Request to the one connection holding a Key, where a [Request](#request) goes to whichever
236
+ Producer takes it first.
237
+
238
+ `back` asserts a [direct exchange](#routing) and a queue named `<exchange>.<key>`, *exclusive* to
239
+ the connection, binds it under the `key` and starts consuming Requests, as [`reply`](#reply) does.
240
+ **On each broker, one connection holds a Key at a time.** While another connection holds it,
241
+ `back` claims it again and again, with the backoff a lost connection is
242
+ [restored](#connection-tolerance) with, and emits [`taken`](#diagnostics) on every refusal. The Key
243
+ is let go when its connection closes, however it closes — once the broker has noticed, for a
244
+ connection that went without a word. `back` returns once a broker holds the Key. The rest of the
245
+ connection works throughout, and a connection that is restored claims its Keys again the same way.
246
+
247
+ `call` publishes the encoded Request to the exchange under the `key` and returns the decoded Reply,
248
+ taking the same options as [`request`](#request).
249
+
250
+ A call ends in one of three ways:
251
+
252
+ - **A Reply.**
253
+ - **Refused at once**, rejecting with `Unroutable`, when no connection holds the Key: it never did,
254
+ its holder has closed, or its holder is [sealed](#sealing). A refused call reached no one.
255
+ - **At its [timeout](#timeout)**, when its holder has gone without being sealed — a crashed process,
256
+ a lost connection — or takes longer. A call nobody has taken by then is dropped.
257
+
258
+ A call waits for as long as it takes unless it has a timeout, and a holder that is gone never
259
+ answers it. **Give every call a timeout.**
260
+
261
+ Sealing withdraws every Key before it stops consuming: a call published from then on is refused,
262
+ and one published before it is delivered and answered. Calls queued beyond the
263
+ [prefetch](#channels) at that moment go with the connection and end at their timeout.
264
+
265
+ A Key is as alive as its connection: while the holder reconnects, calls to it are refused, and
266
+ calls queued for it are lost.
267
+
268
+ Over a [sharded connection](#sharded-connection), `back` claims the Key on every shard and returns
269
+ once one of them holds it; a shard where it is taken goes on claiming it. Two connections given the
270
+ same Key can therefore hold it on different shards and both answer calls, and `taken` is what says
271
+ so. A call returned by one shard is published on the next, and refused once every shard has
272
+ returned it.
273
+
274
+ ### Example
275
+
276
+ ```javascript
277
+ const { Unroutable } = require('comq')
278
+
279
+ await io.back('sessions', 'a1', (message) => sessions.get(message.id))
280
+
281
+ try {
282
+ const session = await io.call('sessions', 'a1', { id: 7 }, { timeout: 5000 })
283
+ } catch (error) {
284
+ if (error instanceof Unroutable) console.log('Nobody holds', error.key)
285
+ }
286
+ ```
287
+
197
288
  ## Tasks
198
289
 
199
290
  `async IO.enqueue(queue: string, payload: any, encoding?: string): void`
@@ -425,24 +516,6 @@ dynamic, such as those that depend on runtime data like incoming messages, makin
425
516
  impossible or hard to maintain. The tradeoff of potentially encountering runtime topology
426
517
  declaration exceptions, which are more likely to happen during development, is deemed acceptable.
427
518
 
428
- ### Settings
429
-
430
- Each channel type has a preset, and the trailing argument of `connect` overrides it:
431
-
432
- ```javascript
433
- const io = await comq.connect(url, {
434
- event: { delay: [5000, 60000] }, // two retries
435
- request: { delay: 1000 } // one
436
- })
437
- ```
438
-
439
- `delay` governs [retries](#retries); the rest of [the settings](./types/topology.d.ts) are not
440
- meant to be changed.
441
-
442
- > Changing `delay` declares new retry queues rather than redeclaring the existing ones, so a
443
- > rolling deploy that changes it has no window in which either version fails. The queues left
444
- > behind are empty and can be removed once nothing is publishing to them.
445
-
446
519
  ### Channels
447
520
 
448
521
  `IO` lazy creates individual channels for Requests, Replies, and Events.
@@ -467,6 +540,8 @@ requests and are expecting replies.
467
540
  and [Consumption](#consumption), and as _direct_ for [Routing](#routing). One name is one or
468
541
  the other: asserting it as both is what the broker refuses.
469
542
  - Queues for Replies are _exclusive_ and _auto deleted_.
543
+ - A queue [`back`](#addressed-requests) holds is _exclusive_, bound under its Key to a _direct_
544
+ exchange.
470
545
 
471
546
  comq declares two kinds of queue of its own, for [failed messages](#retries):
472
547
 
@@ -508,13 +583,48 @@ other consumers nor that consumer's next message; only the message that failed i
508
583
  | Event | 1s, 10s, 30s, 90s | 5 | 131s |
509
584
  | Request | 1s, 3s, 5s, 10s | 5 | 19s |
510
585
 
511
- Requests are shorter because a caller is blocked on one with no timeout, and a Reply arriving
512
- long after it gave up has nowhere useful to land. Nobody waits on an Event.
586
+ Requests are shorter because a caller is blocked on one, and a Reply arriving long after it
587
+ stopped waiting has nowhere useful to land. Nobody waits on an Event.
513
588
 
514
589
  One retry queue and one exchange are declared per distinct wait and shared by every queue that
515
590
  uses them, so their number grows with the length of the ladder rather than with the number of
516
591
  queues.
517
592
 
593
+ #### Saying how it failed
594
+
595
+ A consumer can classify its failure, which decides whether the message is worth another
596
+ attempt at all:
597
+
598
+ ```javascript
599
+ const { Retry, Park } = require('comq')
600
+
601
+ await io.consume('orders', 'billing', async (order) => {
602
+ if (typeof order.total !== 'number') throw new Park('no total on order')
603
+
604
+ try {
605
+ await billing.charge(order)
606
+ } catch (e) {
607
+ throw new Retry('billing unavailable', { cause: e })
608
+ }
609
+ })
610
+ ```
611
+
612
+ - `Park` says this consumer will never process this message, however many times it is handed
613
+ over. It is [parked](#parked-messages) at once, without climbing the ladder.
614
+ - `Retry` says whatever was missing may be back shortly. This is what an unclassified rejection
615
+ already means, so throwing it changes nothing but the reader's certainty.
616
+
617
+ **A bare rejection means `Retry`** — a failure nobody classified is a failure nobody chose — so
618
+ consumers written before this behave exactly as they did.
619
+
620
+ Both are `Error`s and both take a `cause`, which is recorded on the parked message alongside the
621
+ verdict's own message.
622
+
623
+ > **`Park` is not applicable to a `Producer` given to [`IO.reply`](#reply).** A Request
624
+ > has a caller awaiting a reply, so what happens to a failed one is not a policy choice. A `Park`
625
+ > thrown there is treated as an ordinary failure — retried, then parked on the count — and the
626
+ > parked message says so as its reason.
627
+
518
628
  #### Parked messages
519
629
 
520
630
  A message that has run out of attempts is published to `comq.parked.<queue>` and acknowledged
@@ -531,11 +641,12 @@ Parked queues grow until somebody drains them, which is deliberate — the alter
531
641
  evidence. Alert on [`discard`](#diagnostics), and do not delete `comq.retry.*` or `comq.parked.*`
532
642
  queues on a running system.
533
643
 
534
- > **A parked Request is never answered.** A Consumer awaiting its Reply waits indefinitely, and
535
- > with a limited prefetch that can deadlock it. Parking keeps the Request rather than deleting
644
+ > **A parked Request is never answered.** A Consumer awaiting its Reply waits until its
645
+ > [timeout](#timeout), or indefinitely without one, and with a limited prefetch that can deadlock
646
+ > it. Parking keeps the Request rather than deleting
536
647
  > it — and it keeps `replyTo` and `correlationId`, so a Reply can still be produced from it by
537
648
  > hand while the caller is alive — but comq itself sends no Reply and reports no error to the
538
- > caller. Give a Request a timeout on the calling side if you cannot tolerate that.
649
+ > caller.
539
650
 
540
651
  #### What is guaranteed
541
652
 
@@ -550,11 +661,21 @@ A retried message re-enters its queue behind the messages published while it wai
550
661
  Retries and parked messages are published *persistent* whatever the channel is, so they survive a
551
662
  restart of the broker even on the Request channel. Ordinary publishing is untouched: Requests and
552
663
  Replies stay [delivery mode 1](#messages), and only a message that has already failed is written
553
- to disk. Two weaker points remain on that channel: it
554
- does not use publisher confirms, so comq has no positive acknowledgement that the broker took the
555
- copy; and the return hop of a retry is performed by the broker's dead-lettering, which on classic
556
- queues is at-most-once and can lose the message if the source queue is unavailable when the delay
557
- expires.
664
+ to disk.
665
+
666
+ On the Request channel the copy is published without [publisher confirms](#channels). Those are
667
+ an Events property here, because a confirm is a round trip and Requests are where that is felt —
668
+ the same reason they are not persistent. Confirm mode belongs to the channel rather than to a
669
+ publish, so the failure path cannot ask for it on its own.
670
+
671
+ What that costs is narrow. Commands on a channel are handled in order, so the broker takes the
672
+ copy before it releases the original, and `mandatory` brings back a copy it could not route. What
673
+ is left uncovered is a broker that accepted the frame and then failed to keep it.
674
+
675
+ The return hop of a retry — the broker moving a message out of the retry queue when its wait
676
+ expires — is dead-lettering, and on classic queues that is at-most-once: a retry can be lost if
677
+ its source queue is unavailable at the moment the delay expires. This applies to every channel,
678
+ not only Requests.
558
679
 
559
680
  See:
560
681
 
@@ -570,6 +691,25 @@ See:
570
691
  | Reply | unlimited | no | exclusive | automatic | no | — |
571
692
  | Event | limited | yes | durable | manual | yes | 1s, 10s, 30s, 90s |
572
693
 
694
+ ### Settings
695
+
696
+ Each channel type has a [preset](./source/topology), and the trailing argument of `connect`
697
+ overrides any of its fields:
698
+
699
+ ```javascript
700
+ const io = await comq.connect(url, {
701
+ event: { delay: [5000, 60000] }, // two retries
702
+ request: { delay: 1000 } // one
703
+ })
704
+ ```
705
+
706
+ `delay` is the one meant to be set. Changing the rest will change what a Request, a Reply and an
707
+ Event *are*.
708
+
709
+ > Changing `delay` declares new retry queues rather than redeclaring the existing ones, so a
710
+ > rolling deploy that changes it has no window in which either version fails. The queues left
711
+ > behind are empty and can be removed once nothing is publishing to them.
712
+
573
713
  ## Graceful shutdown
574
714
 
575
715
  ### Sealing
@@ -580,6 +720,9 @@ See:
580
720
  Requests.
581
721
  Sending Requests, receiving Replies, and emitting Events will still be available.
582
722
 
723
+ Keys held by [`back`](#addressed-requests) are withdrawn first, so a call published from then on is
724
+ refused.
725
+
583
726
  ### Disconnection
584
727
 
585
728
  `async IO.close(): void`
@@ -650,6 +793,8 @@ Subscribe to one of the diagnostic events:
650
793
  [amqp message object](https://amqp-node.github.io/amqplib/channel_api.html#channel_publish) are
651
794
  passed as arguments. In the case of a [sharded connection](#sharded-connection), the message is
652
795
  reported only once every shard has rejected it.
796
+ - `taken`: a Key [`back`](#addressed-requests) claims is held by another connection on this broker, and
797
+ is claimed again. Channel type and the queue name are passed.
653
798
  - `pause`: channel is paused. Channel type is passed.
654
799
  In the case of a [sharded connection](#sharded-connection), it means that there is no shard left
655
800
  to publish to, be it because every one of them has rejected a publish or lost its connection.
@@ -15,7 +15,8 @@ const recorder = (context, method) => async function (...args) {
15
15
 
16
16
  const result = await method.apply(context, args)
17
17
 
18
- method[CALLS].push(args)
18
+ // forgotten while it ran, by a seal: a sealed context replays nothing
19
+ method[CALLS]?.push(args)
19
20
 
20
21
  return result
21
22
  }
package/source/channel.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { Promex } = require('promex')
4
+ const { retry } = require('reretry')
4
5
  const { failsafe, lazy, recall } = require('./attributes')
6
+ const { verdictOf, PARK } = require('./verdicts')
5
7
  const emitter = require('./emitter')
6
8
 
7
9
  /**
@@ -33,6 +35,13 @@ class Channel {
33
35
  */
34
36
  #queues = new Map()
35
37
 
38
+ /**
39
+ * The exchange and key each held queue is bound under, so that sealing withdraws them.
40
+ *
41
+ * @type {Map<string, [string, string]>}
42
+ */
43
+ #held = new Map()
44
+
36
45
  /** @type {Promex | null} */
37
46
  #paused = null
38
47
 
@@ -79,8 +88,9 @@ class Channel {
79
88
  // the consumers of the previous channel went down with it, their tags mean nothing here
80
89
  this.#tags = []
81
90
 
82
- // a fresh channel has declared nothing
91
+ // a fresh channel has declared nothing, and bound nothing
83
92
  this.#queues.clear()
93
+ this.#held.clear()
84
94
 
85
95
  await this.#channel.prefetch(this.#topology.prefetch)
86
96
 
@@ -130,6 +140,33 @@ class Channel {
130
140
  if (!this.#sealed) await this.#consume(queue, callback)
131
141
  })))
132
142
 
143
+ /**
144
+ * Consumes a queue exclusive to this connection, bound to a direct exchange under a key:
145
+ * whatever is published under that key reaches this connection and no other. While another
146
+ * connection holds the queue, it is claimed again and again until it is let go, and every
147
+ * refusal is reported as `taken`.
148
+ */
149
+ held = recall(this,
150
+ failsafe(this, this.#recover,
151
+ lazy(this, this.#assertRouted,
152
+ /**
153
+ * @param {string} exchange
154
+ * @param {string} queue
155
+ * @param {string} key
156
+ * @param {comq.channels.Consumer} callback
157
+ * @returns {Promise<void>}
158
+ */
159
+ async (exchange, queue, key, callback) => {
160
+ if (this.#sealed) return
161
+
162
+ const holding = this.#hold(exchange, queue, key, callback)
163
+
164
+ // a recovery restores the rest of the connection while a key another connection holds
165
+ // is claimed, and a claim the connection is lost under is replayed by the next recovery
166
+ if (this.#recovering) holding.catch(noop)
167
+ else await holding
168
+ })))
169
+
133
170
  send = failsafe(this, this.#recover,
134
171
  lazy(this, this.#assertQueue,
135
172
  /**
@@ -200,6 +237,15 @@ class Channel {
200
237
  async seal () {
201
238
  this.#sealed = true
202
239
 
240
+ // a key is withdrawn before its consumer goes: a Request published from here on is
241
+ // returned to its caller, and one published before it is delivered and answered
242
+ const unbindings = Array.from(this.#held,
243
+ ([queue, [exchange, key]]) => this.#channel.unbindQueue(queue, exchange, key))
244
+
245
+ await Promise.allSettled(unbindings)
246
+
247
+ this.#held.clear()
248
+
203
249
  const cancellations = this.#tags.map((tag) => this.#channel.cancel(tag))
204
250
 
205
251
  await Promise.all(cancellations).catch(noop) // won't recover anyway
@@ -248,6 +294,84 @@ class Channel {
248
294
  this.#diagnostics.emit('recover')
249
295
  }
250
296
 
297
+ /**
298
+ * @param {string} exchange
299
+ * @param {string} queue
300
+ * @param {string} key
301
+ * @param {comq.channels.Consumer} callback
302
+ * @returns {Promise<void>}
303
+ */
304
+ async #hold (exchange, queue, key, callback) {
305
+ const claimed = await this.#claim(queue)
306
+
307
+ if (!claimed) return
308
+
309
+ await this.#assertQueue(queue, EXCLUSIVE)
310
+ await this.#channel.bindQueue(queue, exchange, key)
311
+
312
+ this.#held.set(queue, [exchange, key])
313
+
314
+ if (!this.#sealed) await this.#consume(queue, callback)
315
+ }
316
+
317
+ /**
318
+ * Claims a queue exclusive to this connection, again and again while another connection
319
+ * holds it. The broker announces nothing when a queue is let go, so it is asked again, with
320
+ * the backoff a lost connection is restored with.
321
+ *
322
+ * @param {string} queue
323
+ * @returns {Promise<boolean>} whether the queue is claimed, as opposed to sealed while claiming
324
+ */
325
+ async #claim (queue) {
326
+ const connection = this.#connection
327
+
328
+ return await retry(async (again) => {
329
+ if (this.#sealed) return false
330
+
331
+ // a connection that is gone is recovered from, like any other interruption
332
+ if (connection !== this.#connection) throw INTERRUPTION
333
+
334
+ if (await this.#declare(connection, queue)) return true
335
+
336
+ this.#diagnostics.emit('taken', queue)
337
+
338
+ return again
339
+ }, global.COMQ_TESTING_CLAIM_BACKOFF)
340
+ }
341
+
342
+ /**
343
+ * Declares a queue exclusive to this connection, and says whether another connection holds it.
344
+ *
345
+ * The broker refuses such a queue by closing the channel that asked for it, and a channel error
346
+ * nobody listens for closes the whole connection. So the declaration is made on a channel of its
347
+ * own, which is all that closes. The queue belongs to the connection, and outlives that channel.
348
+ *
349
+ * @param {comq.amqp.Connection} connection
350
+ * @param {string} queue
351
+ * @returns {Promise<boolean>}
352
+ */
353
+ async #declare (connection, queue) {
354
+ const probe = await connection.createChannel().catch(() => { throw INTERRUPTION })
355
+
356
+ /** @type {Error & { code?: number } | undefined} */
357
+ let refusal
358
+
359
+ probe.on('error', (error) => { refusal = error })
360
+
361
+ try {
362
+ await probe.assertQueue(queue, EXCLUSIVE)
363
+ } catch (exception) {
364
+ if (refusal?.code === RESOURCE_LOCKED) return false
365
+
366
+ // what the broker refused with says more than the channel it closed
367
+ throw refusal ?? exception
368
+ }
369
+
370
+ await probe.close().catch(noop)
371
+
372
+ return true
373
+ }
374
+
251
375
  // region initializers
252
376
 
253
377
  /**
@@ -490,8 +614,11 @@ class Channel {
490
614
  // the header is which attempt this delivery is, and the first does not carry one
491
615
  const attempt = message.properties.headers?.[ATTEMPT_HEADER] ?? 1
492
616
 
493
- // one rung per retry: a message that has climbed the ladder has nowhere left to wait
494
- if (attempt > this.#delays.length) await this.#park(queue, message, exception)
617
+ // a consumer that says the message will never work is taken at its word; otherwise
618
+ // one rung per retry, and a message that has climbed the ladder has nowhere left to wait
619
+ const parking = verdictOf(exception) === PARK || attempt > this.#delays.length
620
+
621
+ if (parking) await this.#park(queue, message, exception)
495
622
  else await this.#retry(queue, message, attempt, exception)
496
623
  } catch {
497
624
  // the message could not be moved: give the delivery back rather than lose it
@@ -527,6 +654,9 @@ class Channel {
527
654
  const properties = this.#carry(message, {
528
655
  [PARKED_QUEUE_HEADER]: queue,
529
656
  [PARKED_REASON_HEADER]: exception?.message,
657
+ // a verdict is usually thrown with the exception that prompted it, and that one
658
+ // says what actually went wrong
659
+ [PARKED_CAUSE_HEADER]: exception?.cause?.message,
530
660
  [PARKED_AT_HEADER]: Date.now()
531
661
  })
532
662
 
@@ -641,6 +771,9 @@ const EXCLUSIVE = { exclusive: true }
641
771
 
642
772
  const INTERRUPTION = /** @type {Error} */ Symbol('internal interruption')
643
773
 
774
+ // the AMQP reply code a broker closes a channel with when another connection holds the queue
775
+ const RESOURCE_LOCKED = 405
776
+
644
777
  const RETRY_PREFIX = 'comq.retry.'
645
778
  const PARKED_PREFIX = 'comq.parked.'
646
779
 
@@ -659,8 +792,10 @@ const ORIGIN_EXCHANGE_HEADER = 'x-comq-exchange'
659
792
  const ORIGIN_KEY_HEADER = 'x-comq-key'
660
793
  const PARKED_QUEUE_HEADER = 'x-comq-queue'
661
794
  const PARKED_REASON_HEADER = 'x-comq-reason'
795
+ const PARKED_CAUSE_HEADER = 'x-comq-cause'
662
796
  const PARKED_AT_HEADER = 'x-comq-at'
663
797
 
664
798
  function noop () {}
665
799
 
666
800
  exports.create = create
801
+ exports.RETRY_PREFIX = RETRY_PREFIX
package/source/events.js CHANGED
@@ -4,4 +4,4 @@
4
4
  exports.connection = ['open', 'close', 'error', 'reconnect', 'exhausted']
5
5
 
6
6
  /** @type {comq.diagnostics.Event[]} */
7
- exports.channel = ['flow', 'drain', 'recover', 'discard', 'retry', 'pause', 'resume', 'return', 'lost', 'remove']
7
+ exports.channel = ['flow', 'drain', 'recover', 'discard', 'retry', 'pause', 'resume', 'return', 'lost', 'remove', 'taken']
package/source/index.js CHANGED
@@ -1,6 +1,12 @@
1
1
  'use strict'
2
2
 
3
3
  const { connect, assert } = require('./connect')
4
+ const { Retry, Park } = require('./verdicts')
5
+ const { Unroutable } = require('./unroutable')
4
6
 
5
7
  exports.connect = connect
6
8
  exports.assert = assert
9
+
10
+ exports.Retry = Retry
11
+ exports.Park = Park
12
+ exports.Unroutable = Unroutable