comq 0.22.0 → 0.24.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/discussions/amqp-1.0.md +127 -0
- package/discussions/one-parking-queue.md +63 -0
- package/discussions/one-reply-queue.md +44 -0
- package/discussions/poison-message-handling.md +2 -1
- package/docs/headers.md +1 -1
- package/features/parking.feature +32 -0
- package/features/rpc.call.feature +23 -1
- package/features/rpc.call.recovery.feature +0 -21
- package/features/rpc.parked.feature +6 -6
- package/features/rpc.replies.feature +26 -0
- package/features/steps/brokers.js +25 -1
- package/features/steps/call.js +90 -30
- package/features/steps/events.js +1 -1
- package/features/steps/parked.js +7 -18
- package/features/steps/parking.js +113 -0
- package/features/steps/poison.js +17 -19
- package/features/steps/replies.js +35 -0
- package/package.json +6 -4
- package/readme.md +54 -75
- package/source/.io/ReplyPipe.js +16 -1
- package/source/channel.js +19 -27
- package/source/connection.js +1 -4
- package/source/index.js +0 -2
- package/source/io.js +48 -59
- package/test/ReplyPipe.test.js +40 -0
- package/test/amqplib.mock.js +1 -1
- package/test/channel.test.js +29 -18
- package/test/connection.test.js +1 -1
- package/test/{io.request.terms.test.js → io.call.terms.test.js} +31 -30
- package/test/io.call.test.js +0 -10
- package/test/io.request.test.js +26 -3
- package/test/memory.test.js +1 -1
- package/types/amqp.d.ts +1 -1
- package/types/channel.d.ts +1 -1
- package/types/index.d.ts +0 -6
- package/types/io.d.ts +9 -14
- package/features/closing.feature +0 -32
- package/features/rpc.timeout.feature +0 -19
- package/features/steps/closing.js +0 -109
- package/source/abandoned.js +0 -15
- package/source/batch.js +0 -62
- package/test/batch.test.js +0 -94
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
|
-
/**
|
|
35
|
-
|
|
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
|
|
|
@@ -81,16 +84,21 @@ class IO {
|
|
|
81
84
|
})
|
|
82
85
|
|
|
83
86
|
/**
|
|
87
|
+
* Waits for the Reply for as long as it takes: a Request is answered eventually, and one no
|
|
88
|
+
* Producer can answer is parked rather than abandoned.
|
|
89
|
+
*
|
|
84
90
|
* @param {string} queue
|
|
85
91
|
* @param {any | Readable} payload
|
|
86
|
-
* @param {comq.Encoding
|
|
92
|
+
* @param {comq.Encoding} [encoding]
|
|
87
93
|
* @returns {Promise<any | Readable>}
|
|
88
94
|
*/
|
|
89
|
-
request (queue, payload,
|
|
90
|
-
|
|
95
|
+
async request (queue, payload, encoding) {
|
|
96
|
+
// a caller of the version that took a timeout here would otherwise wait on in silence
|
|
97
|
+
if (typeof encoding === 'object' && encoding !== null) {
|
|
98
|
+
throw new TypeError('A Request takes an encoding and waits for its Reply')
|
|
99
|
+
}
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
return abortable(this.#request(queue, payload, settled), settled.signal)
|
|
101
|
+
return await this.#request(queue, payload, { encoding })
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
/**
|
|
@@ -100,12 +108,13 @@ class IO {
|
|
|
100
108
|
* @param {string} exchange
|
|
101
109
|
* @param {string} key
|
|
102
110
|
* @param {any} payload
|
|
103
|
-
* @param {comq.Encoding | comq.
|
|
111
|
+
* @param {comq.Encoding | comq.CallOptions} [options]
|
|
104
112
|
* @returns {Promise<any | Readable>}
|
|
105
113
|
*/
|
|
106
114
|
call (exchange, key, payload, options) {
|
|
107
115
|
const settled = terms(options)
|
|
108
116
|
|
|
117
|
+
// the channels a first call declares are waited for within the terms too
|
|
109
118
|
return abortable(this.#call(exchange, key, payload, settled), settled.signal)
|
|
110
119
|
}
|
|
111
120
|
|
|
@@ -126,8 +135,7 @@ class IO {
|
|
|
126
135
|
await this.#requests.held(exchange, exchange + '.' + key, key, consumer)
|
|
127
136
|
})
|
|
128
137
|
|
|
129
|
-
// failsafe is aimed to retransmit unanswered messages
|
|
130
|
-
// re-sent Request keeps the deadline of the first
|
|
138
|
+
// failsafe is aimed to retransmit unanswered messages
|
|
131
139
|
#request = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
|
|
132
140
|
failsafe(this, this.#recover,
|
|
133
141
|
/**
|
|
@@ -149,6 +157,7 @@ class IO {
|
|
|
149
157
|
(buffer, properties) => this.#requests.send(queue, buffer, properties))
|
|
150
158
|
}))
|
|
151
159
|
|
|
160
|
+
// the terms are taken once, so a re-sent call keeps the deadline of the first
|
|
152
161
|
#call = lazy(this, [this.#createRequestReplyChannels, this.#consumeReplies],
|
|
153
162
|
failsafe(this, this.#recover,
|
|
154
163
|
/**
|
|
@@ -254,30 +263,6 @@ class IO {
|
|
|
254
263
|
await this.#destroyStreams(this.#replyStreams)
|
|
255
264
|
})
|
|
256
265
|
|
|
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
266
|
close = memo(async () => {
|
|
282
267
|
await this.seal()
|
|
283
268
|
await this.#destroyStreams(this.#replyPipes)
|
|
@@ -340,13 +325,20 @@ class IO {
|
|
|
340
325
|
return [buffer, properties]
|
|
341
326
|
}
|
|
342
327
|
|
|
343
|
-
async #consumeReplies (
|
|
344
|
-
|
|
345
|
-
|
|
328
|
+
async #consumeReplies () {
|
|
329
|
+
this.#emitter = await this.#consumeRepliesOf(REPLY)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* @param {string} label
|
|
334
|
+
* @returns {Promise<comq.ReplyEmitter>}
|
|
335
|
+
*/
|
|
336
|
+
async #consumeRepliesOf (label) {
|
|
337
|
+
const emitter = io.createReplyEmitter(label)
|
|
346
338
|
|
|
347
|
-
this.#
|
|
339
|
+
await this.#replies.consume(emitter.queue, this.#getReplyConsumer(emitter))
|
|
348
340
|
|
|
349
|
-
|
|
341
|
+
return emitter
|
|
350
342
|
}
|
|
351
343
|
|
|
352
344
|
// endregion
|
|
@@ -417,11 +409,10 @@ class IO {
|
|
|
417
409
|
})
|
|
418
410
|
|
|
419
411
|
/**
|
|
420
|
-
* @param {string} queue
|
|
421
412
|
* @param {comq.ReplyEmitter} emitter
|
|
422
413
|
* @returns {comq.channels.Consumer}
|
|
423
414
|
*/
|
|
424
|
-
#getReplyConsumer = (
|
|
415
|
+
#getReplyConsumer = (emitter) =>
|
|
425
416
|
(message) => {
|
|
426
417
|
const payload = decode(message)
|
|
427
418
|
|
|
@@ -453,11 +444,8 @@ class IO {
|
|
|
453
444
|
|
|
454
445
|
signal?.throwIfAborted()
|
|
455
446
|
|
|
456
|
-
// one made now would be waited for by nobody: the Replies are abandoned already
|
|
457
|
-
if (this.#abandoning) throw new Abandoned()
|
|
458
|
-
|
|
459
447
|
const [buffer, contentType] = this.#encode(payload, terms.encoding)
|
|
460
|
-
const request = this.#createRequest(
|
|
448
|
+
const request = this.#createRequest(contentType, signal)
|
|
461
449
|
const properties = { ...request.properties }
|
|
462
450
|
|
|
463
451
|
if (expires !== undefined) {
|
|
@@ -524,13 +512,12 @@ class IO {
|
|
|
524
512
|
* The request holds no copy of what was sent: a retransmission encodes the
|
|
525
513
|
* payload anew, and an unanswered request would otherwise keep two of it.
|
|
526
514
|
*
|
|
527
|
-
* @param {string} queue
|
|
528
515
|
* @param {comq.Encoding} contentType
|
|
529
516
|
* @param {AbortSignal} [signal]
|
|
530
517
|
* @return {comq.Request}
|
|
531
518
|
*/
|
|
532
|
-
#createRequest (
|
|
533
|
-
const emitter = this.#
|
|
519
|
+
#createRequest (contentType, signal) {
|
|
520
|
+
const emitter = this.#emitter
|
|
534
521
|
const correlationId = emitter.next()
|
|
535
522
|
|
|
536
523
|
/** @type {comq.amqp.Properties} */
|
|
@@ -611,11 +598,7 @@ class IO {
|
|
|
611
598
|
* @return {Promise<comq.ReplyEmitter>}
|
|
612
599
|
*/
|
|
613
600
|
async #createControl () {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
await this.#consumeReplies(queue)
|
|
617
|
-
|
|
618
|
-
return this.#emitters.get(queue)
|
|
601
|
+
return await this.#consumeRepliesOf(CONTROL)
|
|
619
602
|
}
|
|
620
603
|
|
|
621
604
|
/**
|
|
@@ -724,11 +707,17 @@ const DEFAULT = 'application/json'
|
|
|
724
707
|
|
|
725
708
|
const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
|
|
726
709
|
|
|
710
|
+
/** What this IO's replies arrive on, before the random suffix that makes the queue its own. */
|
|
711
|
+
const REPLY = 'comq.reply'
|
|
712
|
+
|
|
713
|
+
/** What a producer steers its reply streams from, likewise. */
|
|
714
|
+
const CONTROL = 'control'
|
|
715
|
+
|
|
727
716
|
/**
|
|
728
|
-
* What a caller passed, settled once: a timeout becomes the moment the
|
|
717
|
+
* What a caller passed, settled once: a timeout becomes the moment the call expires, so that
|
|
729
718
|
* every attempt of it shares one deadline.
|
|
730
719
|
*
|
|
731
|
-
* @param {comq.Encoding | comq.
|
|
720
|
+
* @param {comq.Encoding | comq.CallOptions} [options]
|
|
732
721
|
* @returns {comq.Terms}
|
|
733
722
|
*/
|
|
734
723
|
function terms (options) {
|
package/test/ReplyPipe.test.js
CHANGED
|
@@ -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
|
+
})
|
package/test/amqplib.mock.js
CHANGED
|
@@ -58,7 +58,7 @@ class Connection extends EventEmitter {
|
|
|
58
58
|
|
|
59
59
|
const connect = async (url) => new Connection(url)
|
|
60
60
|
|
|
61
|
-
/** @type {jest.MockedObject<import('amqplib')>} */
|
|
61
|
+
/** @type {jest.MockedObject<import('@toa.io/amqplib')>} */
|
|
62
62
|
const amqplib = {
|
|
63
63
|
connect: jest.fn(connect)
|
|
64
64
|
}
|
package/test/channel.test.js
CHANGED
|
@@ -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(
|
|
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
|
|
1134
|
-
|
|
1135
|
-
|
|
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
|
-
|
|
1138
|
-
await channel.consume(another, consumer)
|
|
1147
|
+
const parked = chan.assertQueue.mock.calls.filter(([name]) => name === PARKED)
|
|
1139
1148
|
|
|
1140
|
-
expect(
|
|
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
|
|
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 ===
|
|
1179
|
+
.find(([name]) => name === PARKED)
|
|
1170
1180
|
|
|
1171
|
-
expect(options).toMatchObject({
|
|
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(
|
|
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',
|
|
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(
|
|
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,
|
|
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,
|
|
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(
|
|
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(
|
|
1698
|
+
expect(publications()[0][1]).toStrictEqual(PARKED)
|
|
1688
1699
|
})
|
|
1689
1700
|
})
|
|
1690
1701
|
|
package/test/connection.test.js
CHANGED
|
@@ -10,7 +10,7 @@ const { amqplib, connect } = require('./amqplib.mock')
|
|
|
10
10
|
const { channel: create } = require('./connection.mock')
|
|
11
11
|
const mock = { amqplib, channel: { create } }
|
|
12
12
|
|
|
13
|
-
jest.mock('amqplib', () => mock.amqplib)
|
|
13
|
+
jest.mock('@toa.io/amqplib', () => mock.amqplib)
|
|
14
14
|
jest.mock('../source/channel', () => mock.channel)
|
|
15
15
|
|
|
16
16
|
const presets = require('../source/topology')
|
|
@@ -21,7 +21,8 @@ let requests
|
|
|
21
21
|
/** @type {jest.MockedObject<comq.Channel>} */
|
|
22
22
|
let replies
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const exchange = generate()
|
|
25
|
+
const key = generate()
|
|
25
26
|
const payload = { [generate()]: generate() }
|
|
26
27
|
|
|
27
28
|
beforeEach(async () => {
|
|
@@ -34,12 +35,12 @@ beforeEach(async () => {
|
|
|
34
35
|
// endregion
|
|
35
36
|
|
|
36
37
|
describe('timeout', () => {
|
|
37
|
-
it('should expire the
|
|
38
|
-
io.
|
|
38
|
+
it('should expire the call when the caller stops waiting', async () => {
|
|
39
|
+
io.call(exchange, key, payload, { timeout: 10000 }).catch(noop)
|
|
39
40
|
|
|
40
41
|
await initialized()
|
|
41
42
|
|
|
42
|
-
const expiration = Number(requests.
|
|
43
|
+
const expiration = Number(requests.route.mock.calls[0][3].expiration)
|
|
43
44
|
|
|
44
45
|
expect(expiration).toBeGreaterThan(9000)
|
|
45
46
|
expect(expiration).toBeLessThanOrEqual(10000)
|
|
@@ -48,29 +49,29 @@ describe('timeout', () => {
|
|
|
48
49
|
it('should keep the encoding', async () => {
|
|
49
50
|
const encoding = 'application/octet-stream'
|
|
50
51
|
|
|
51
|
-
io.
|
|
52
|
+
io.call(exchange, key, randomBytes(8), { encoding, timeout: 10000 }).catch(noop)
|
|
52
53
|
|
|
53
54
|
await initialized()
|
|
54
55
|
|
|
55
|
-
expect(requests.
|
|
56
|
+
expect(requests.route.mock.calls[0][3].contentType).toStrictEqual(encoding)
|
|
56
57
|
})
|
|
57
58
|
|
|
58
59
|
it('should stop waiting while the channels are being created', async () => {
|
|
59
60
|
connection.createChannel.mockImplementation(() => new Promise(noop))
|
|
60
61
|
|
|
61
|
-
const promise = io.
|
|
62
|
+
const promise = io.call(exchange, key, payload, { timeout: 20 })
|
|
62
63
|
|
|
63
64
|
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
64
65
|
})
|
|
65
66
|
|
|
66
67
|
it('should reject once it has passed', async () => {
|
|
67
|
-
const promise = io.
|
|
68
|
+
const promise = io.call(exchange, key, payload, { timeout: 20 })
|
|
68
69
|
|
|
69
70
|
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
70
71
|
})
|
|
71
72
|
|
|
72
73
|
it('should resolve with a reply that arrives in time', async () => {
|
|
73
|
-
const promise = io.
|
|
74
|
+
const promise = io.call(exchange, key, payload, { timeout: 10000 })
|
|
74
75
|
const content = randomBytes(8)
|
|
75
76
|
|
|
76
77
|
await initialized()
|
|
@@ -80,7 +81,7 @@ describe('timeout', () => {
|
|
|
80
81
|
})
|
|
81
82
|
|
|
82
83
|
it('should re-send with the time that is left', async () => {
|
|
83
|
-
io.
|
|
84
|
+
io.call(exchange, key, payload, { timeout: 10000 }).catch(noop)
|
|
84
85
|
|
|
85
86
|
await initialized()
|
|
86
87
|
await timeout(20)
|
|
@@ -89,10 +90,10 @@ describe('timeout', () => {
|
|
|
89
90
|
|
|
90
91
|
await immediate()
|
|
91
92
|
|
|
92
|
-
expect(requests.
|
|
93
|
+
expect(requests.route).toHaveBeenCalledTimes(2)
|
|
93
94
|
|
|
94
|
-
const first = Number(requests.
|
|
95
|
-
const second = Number(requests.
|
|
95
|
+
const first = Number(requests.route.mock.calls[0][3].expiration)
|
|
96
|
+
const second = Number(requests.route.mock.calls[1][3].expiration)
|
|
96
97
|
|
|
97
98
|
expect(second).toBeLessThan(first)
|
|
98
99
|
})
|
|
@@ -102,7 +103,7 @@ describe('signal', () => {
|
|
|
102
103
|
it('should reject with the reason it was aborted for', async () => {
|
|
103
104
|
const controller = new AbortController()
|
|
104
105
|
const reason = new Error(generate())
|
|
105
|
-
const promise = io.
|
|
106
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
106
107
|
|
|
107
108
|
await initialized()
|
|
108
109
|
|
|
@@ -111,14 +112,14 @@ describe('signal', () => {
|
|
|
111
112
|
await expect(promise).rejects.toBe(reason)
|
|
112
113
|
})
|
|
113
114
|
|
|
114
|
-
it('should leave the
|
|
115
|
+
it('should leave the call in its queue', async () => {
|
|
115
116
|
const controller = new AbortController()
|
|
116
117
|
|
|
117
|
-
io.
|
|
118
|
+
io.call(exchange, key, payload, { signal: controller.signal }).catch(noop)
|
|
118
119
|
|
|
119
120
|
await initialized()
|
|
120
121
|
|
|
121
|
-
expect(requests.
|
|
122
|
+
expect(requests.route.mock.calls[0][3].expiration).toBeUndefined()
|
|
122
123
|
})
|
|
123
124
|
|
|
124
125
|
it('should reject at once when aborted before the channels exist', async () => {
|
|
@@ -129,27 +130,27 @@ describe('signal', () => {
|
|
|
129
130
|
|
|
130
131
|
controller.abort(reason)
|
|
131
132
|
|
|
132
|
-
await expect(io.
|
|
133
|
+
await expect(io.call(exchange, key, payload, { signal: controller.signal })).rejects.toBe(reason)
|
|
133
134
|
})
|
|
134
135
|
|
|
135
136
|
it('should send nothing once aborted', async () => {
|
|
136
137
|
const controller = new AbortController()
|
|
137
138
|
|
|
138
|
-
// the channels exist before the aborted
|
|
139
|
-
io.
|
|
139
|
+
// the channels exist before the aborted call is made
|
|
140
|
+
io.call(exchange, key, payload).catch(noop)
|
|
140
141
|
|
|
141
142
|
await initialized()
|
|
142
143
|
|
|
143
144
|
controller.abort()
|
|
144
145
|
|
|
145
|
-
await expect(io.
|
|
146
|
+
await expect(io.call(exchange, key, payload, { signal: controller.signal })).rejects.toBeDefined()
|
|
146
147
|
|
|
147
|
-
expect(requests.
|
|
148
|
+
expect(requests.route).toHaveBeenCalledTimes(1)
|
|
148
149
|
})
|
|
149
150
|
|
|
150
151
|
it('should not re-send what it abandoned', async () => {
|
|
151
152
|
const controller = new AbortController()
|
|
152
|
-
const promise = io.
|
|
153
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
153
154
|
|
|
154
155
|
await initialized()
|
|
155
156
|
|
|
@@ -161,19 +162,19 @@ describe('signal', () => {
|
|
|
161
162
|
|
|
162
163
|
await immediate()
|
|
163
164
|
|
|
164
|
-
expect(requests.
|
|
165
|
+
expect(requests.route).toHaveBeenCalledTimes(1)
|
|
165
166
|
})
|
|
166
167
|
|
|
167
168
|
it('should stop waiting for a publication', async () => {
|
|
168
|
-
io.
|
|
169
|
+
io.call(exchange, key, payload).catch(noop)
|
|
169
170
|
|
|
170
171
|
await initialized()
|
|
171
172
|
|
|
172
|
-
requests.
|
|
173
|
+
requests.route.mockImplementationOnce(() => new Promise(noop))
|
|
173
174
|
|
|
174
175
|
const controller = new AbortController()
|
|
175
176
|
const reason = new Error(generate())
|
|
176
|
-
const promise = io.
|
|
177
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
177
178
|
|
|
178
179
|
await immediate()
|
|
179
180
|
|
|
@@ -184,7 +185,7 @@ describe('signal', () => {
|
|
|
184
185
|
|
|
185
186
|
it('should discard a reply that arrives afterwards', async () => {
|
|
186
187
|
const controller = new AbortController()
|
|
187
|
-
const promise = io.
|
|
188
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal })
|
|
188
189
|
|
|
189
190
|
await initialized()
|
|
190
191
|
|
|
@@ -197,7 +198,7 @@ describe('signal', () => {
|
|
|
197
198
|
|
|
198
199
|
it('should end the wait within the timeout', async () => {
|
|
199
200
|
const controller = new AbortController()
|
|
200
|
-
const promise = io.
|
|
201
|
+
const promise = io.call(exchange, key, payload, { signal: controller.signal, timeout: 20 })
|
|
201
202
|
|
|
202
203
|
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
203
204
|
})
|
|
@@ -216,7 +217,7 @@ async function initialized () {
|
|
|
216
217
|
* @param {Buffer} [content]
|
|
217
218
|
*/
|
|
218
219
|
async function answer (index, content = randomBytes(8)) {
|
|
219
|
-
const { correlationId } = requests.
|
|
220
|
+
const { correlationId } = requests.route.mock.calls[index][3]
|
|
220
221
|
const deliver = replies.consume.mock.calls[0][1]
|
|
221
222
|
|
|
222
223
|
await deliver({ content, properties: { correlationId } })
|
package/test/io.call.test.js
CHANGED
|
@@ -98,16 +98,6 @@ describe('call', () => {
|
|
|
98
98
|
})
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
-
describe('timeout', () => {
|
|
102
|
-
it('should stop waiting while the channels are being created', async () => {
|
|
103
|
-
connection.createChannel.mockImplementation(() => new Promise(() => undefined))
|
|
104
|
-
|
|
105
|
-
const promise = io.call(exchange, key, payload, { timeout: 20 })
|
|
106
|
-
|
|
107
|
-
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
101
|
describe('back', () => {
|
|
112
102
|
const producer = jest.fn(() => generate())
|
|
113
103
|
|