comq 0.19.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/features/rpc.call.feature +71 -0
- package/features/rpc.call.recovery.feature +75 -0
- package/features/rpc.call.shards.feature +24 -0
- package/features/rpc.parked.feature +20 -0
- package/features/rpc.timeout.feature +19 -0
- package/features/steps/call.js +462 -0
- package/features/steps/context.js +4 -0
- package/features/steps/network.js +8 -0
- package/features/steps/networks.js +24 -4
- package/features/steps/parked.js +102 -0
- package/package.json +1 -1
- package/readme.md +141 -31
- package/source/attributes/recall.js +2 -1
- package/source/channel.js +128 -1
- package/source/events.js +1 -1
- package/source/index.js +2 -0
- package/source/io.js +212 -13
- package/source/shards/channel.js +20 -3
- package/source/unroutable.js +27 -0
- package/test/amqplib.mock.js +1 -0
- package/test/channel.held.test.js +237 -0
- package/test/connection.mock.js +1 -0
- package/test/io.call.test.js +161 -0
- package/test/io.request.terms.test.js +247 -0
- package/test/shards/channel.test.js +68 -0
- package/types/channel.d.ts +3 -0
- package/types/diagnostic.d.ts +1 -1
- package/types/index.d.ts +9 -0
- package/types/io.d.ts +42 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// region setup
|
|
4
|
+
|
|
5
|
+
const { randomBytes } = require('node:crypto')
|
|
6
|
+
const { generate } = require('randomstring')
|
|
7
|
+
const { immediate } = require('./helpers')
|
|
8
|
+
const { encode } = require('../source/encode')
|
|
9
|
+
const { Unroutable } = require('../source/unroutable')
|
|
10
|
+
|
|
11
|
+
const mock = require('./connection.mock')
|
|
12
|
+
const { IO } = require('../source/io')
|
|
13
|
+
|
|
14
|
+
/** @type {comq.IO} */
|
|
15
|
+
let io
|
|
16
|
+
|
|
17
|
+
/** @type {jest.MockedObject<comq.Connection>} */
|
|
18
|
+
let connection
|
|
19
|
+
|
|
20
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
21
|
+
let requests
|
|
22
|
+
|
|
23
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
24
|
+
let replies
|
|
25
|
+
|
|
26
|
+
const exchange = generate()
|
|
27
|
+
const key = generate()
|
|
28
|
+
const payload = { [generate()]: generate() }
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
jest.clearAllMocks()
|
|
32
|
+
|
|
33
|
+
connection = mock.connection()
|
|
34
|
+
io = new IO(connection)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// endregion
|
|
38
|
+
|
|
39
|
+
describe('call', () => {
|
|
40
|
+
/** @type {Promise<any>} */
|
|
41
|
+
let promise
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
promise = io.call(exchange, key, payload)
|
|
45
|
+
|
|
46
|
+
// allows initializers to run
|
|
47
|
+
await immediate()
|
|
48
|
+
|
|
49
|
+
requests = await findChannel('request')
|
|
50
|
+
replies = await findChannel('reply')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should publish to the exchange under the key', async () => {
|
|
54
|
+
expect(requests.route).toHaveBeenCalledWith(exchange, key, expect.any(Buffer),
|
|
55
|
+
expect.objectContaining({ mandatory: true }))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should declare no queue to publish to', async () => {
|
|
59
|
+
expect(requests.send).not.toHaveBeenCalled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should consume the queue it is answered to', async () => {
|
|
63
|
+
const properties = requests.route.mock.calls[0][3]
|
|
64
|
+
|
|
65
|
+
expect(replies.consume).toHaveBeenCalledWith(properties.replyTo, expect.any(Function))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should resolve with the reply', async () => {
|
|
69
|
+
const content = randomBytes(8)
|
|
70
|
+
const { correlationId } = requests.route.mock.calls[0][3]
|
|
71
|
+
const deliver = replies.consume.mock.calls[0][1]
|
|
72
|
+
|
|
73
|
+
await deliver({ content, properties: { correlationId } })
|
|
74
|
+
|
|
75
|
+
await expect(promise).resolves.toStrictEqual(content)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should reject with Unroutable once the broker returns it', async () => {
|
|
79
|
+
const properties = requests.route.mock.calls[0][3]
|
|
80
|
+
|
|
81
|
+
emit(requests, 'return', { content: randomBytes(8), fields: { exchange, routingKey: key }, properties })
|
|
82
|
+
|
|
83
|
+
await expect(promise).rejects.toBeInstanceOf(Unroutable)
|
|
84
|
+
await expect(promise).rejects.toMatchObject({ exchange, key })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should leave it waiting when another Request is returned', async () => {
|
|
88
|
+
const settled = jest.fn()
|
|
89
|
+
const properties = { ...requests.route.mock.calls[0][3], correlationId: generate() }
|
|
90
|
+
|
|
91
|
+
promise.then(settled, settled)
|
|
92
|
+
|
|
93
|
+
emit(requests, 'return', { content: randomBytes(8), fields: { exchange, routingKey: key }, properties })
|
|
94
|
+
|
|
95
|
+
await immediate()
|
|
96
|
+
|
|
97
|
+
expect(settled).not.toHaveBeenCalled()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
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
|
+
describe('back', () => {
|
|
112
|
+
const producer = jest.fn(() => generate())
|
|
113
|
+
|
|
114
|
+
beforeEach(async () => {
|
|
115
|
+
await io.back(exchange, key, producer)
|
|
116
|
+
|
|
117
|
+
requests = await findChannel('request')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should hold a queue named after the exchange and the key', async () => {
|
|
121
|
+
expect(requests.held).toHaveBeenCalledWith(exchange, `${exchange}.${key}`, key, expect.any(Function))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should answer a Request', async () => {
|
|
125
|
+
const consumer = requests.held.mock.calls[0][3]
|
|
126
|
+
const contentType = 'application/json'
|
|
127
|
+
const properties = { contentType, correlationId: generate(), replyTo: generate() }
|
|
128
|
+
|
|
129
|
+
await consumer({ content: encode(payload, contentType), properties })
|
|
130
|
+
|
|
131
|
+
replies = await findChannel('reply')
|
|
132
|
+
|
|
133
|
+
expect(producer).toHaveBeenCalledWith(payload)
|
|
134
|
+
|
|
135
|
+
expect(replies.fire).toHaveBeenCalledWith(properties.replyTo, expect.any(Buffer),
|
|
136
|
+
expect.objectContaining({ correlationId: properties.correlationId }))
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {comq.topology.type} type
|
|
142
|
+
* @returns {jest.MockedObject<comq.Channel>}
|
|
143
|
+
*/
|
|
144
|
+
const findChannel = (type) => {
|
|
145
|
+
const index = connection.createChannel.mock.calls.findIndex(([t]) => (t === type))
|
|
146
|
+
|
|
147
|
+
if (index === -1) throw new Error(`${type} channel hasn't been created`)
|
|
148
|
+
|
|
149
|
+
return connection.createChannel.mock.results[index].value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {jest.MockedObject<comq.Channel>} channel
|
|
154
|
+
* @param {string} event
|
|
155
|
+
* @param {...any} args
|
|
156
|
+
*/
|
|
157
|
+
function emit (channel, event, ...args) {
|
|
158
|
+
for (const [name, listener] of channel.diagnose.mock.calls) {
|
|
159
|
+
if (name === event) listener(...args)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// region setup
|
|
4
|
+
|
|
5
|
+
const { randomBytes } = require('node:crypto')
|
|
6
|
+
const { generate } = require('randomstring')
|
|
7
|
+
const { immediate, timeout } = require('./helpers')
|
|
8
|
+
|
|
9
|
+
const mock = require('./connection.mock')
|
|
10
|
+
const { IO } = require('../source/io')
|
|
11
|
+
|
|
12
|
+
/** @type {comq.IO} */
|
|
13
|
+
let io
|
|
14
|
+
|
|
15
|
+
/** @type {jest.MockedObject<comq.Connection>} */
|
|
16
|
+
let connection
|
|
17
|
+
|
|
18
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
19
|
+
let requests
|
|
20
|
+
|
|
21
|
+
/** @type {jest.MockedObject<comq.Channel>} */
|
|
22
|
+
let replies
|
|
23
|
+
|
|
24
|
+
const queue = generate()
|
|
25
|
+
const payload = { [generate()]: generate() }
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
jest.clearAllMocks()
|
|
29
|
+
|
|
30
|
+
connection = mock.connection()
|
|
31
|
+
io = new IO(connection)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// endregion
|
|
35
|
+
|
|
36
|
+
describe('timeout', () => {
|
|
37
|
+
it('should expire the Request when the caller stops waiting', async () => {
|
|
38
|
+
io.request(queue, payload, { timeout: 10000 }).catch(noop)
|
|
39
|
+
|
|
40
|
+
await initialized()
|
|
41
|
+
|
|
42
|
+
const expiration = Number(requests.send.mock.calls[0][2].expiration)
|
|
43
|
+
|
|
44
|
+
expect(expiration).toBeGreaterThan(9000)
|
|
45
|
+
expect(expiration).toBeLessThanOrEqual(10000)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should keep the encoding', async () => {
|
|
49
|
+
const encoding = 'application/octet-stream'
|
|
50
|
+
|
|
51
|
+
io.request(queue, randomBytes(8), { encoding, timeout: 10000 }).catch(noop)
|
|
52
|
+
|
|
53
|
+
await initialized()
|
|
54
|
+
|
|
55
|
+
expect(requests.send.mock.calls[0][2].contentType).toStrictEqual(encoding)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should stop waiting while the channels are being created', async () => {
|
|
59
|
+
connection.createChannel.mockImplementation(() => new Promise(noop))
|
|
60
|
+
|
|
61
|
+
const promise = io.request(queue, payload, { timeout: 20 })
|
|
62
|
+
|
|
63
|
+
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should reject once it has passed', async () => {
|
|
67
|
+
const promise = io.request(queue, payload, { timeout: 20 })
|
|
68
|
+
|
|
69
|
+
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should resolve with a reply that arrives in time', async () => {
|
|
73
|
+
const promise = io.request(queue, payload, { timeout: 10000 })
|
|
74
|
+
const content = randomBytes(8)
|
|
75
|
+
|
|
76
|
+
await initialized()
|
|
77
|
+
await answer(0, content)
|
|
78
|
+
|
|
79
|
+
await expect(promise).resolves.toStrictEqual(content)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should re-send with the time that is left', async () => {
|
|
83
|
+
io.request(queue, payload, { timeout: 10000 }).catch(noop)
|
|
84
|
+
|
|
85
|
+
await initialized()
|
|
86
|
+
await timeout(20)
|
|
87
|
+
|
|
88
|
+
emit(requests, 'recover')
|
|
89
|
+
|
|
90
|
+
await immediate()
|
|
91
|
+
|
|
92
|
+
expect(requests.send).toHaveBeenCalledTimes(2)
|
|
93
|
+
|
|
94
|
+
const first = Number(requests.send.mock.calls[0][2].expiration)
|
|
95
|
+
const second = Number(requests.send.mock.calls[1][2].expiration)
|
|
96
|
+
|
|
97
|
+
expect(second).toBeLessThan(first)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('signal', () => {
|
|
102
|
+
it('should reject with the reason it was aborted for', async () => {
|
|
103
|
+
const controller = new AbortController()
|
|
104
|
+
const reason = new Error(generate())
|
|
105
|
+
const promise = io.request(queue, payload, { signal: controller.signal })
|
|
106
|
+
|
|
107
|
+
await initialized()
|
|
108
|
+
|
|
109
|
+
controller.abort(reason)
|
|
110
|
+
|
|
111
|
+
await expect(promise).rejects.toBe(reason)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should leave the Request in its queue', async () => {
|
|
115
|
+
const controller = new AbortController()
|
|
116
|
+
|
|
117
|
+
io.request(queue, payload, { signal: controller.signal }).catch(noop)
|
|
118
|
+
|
|
119
|
+
await initialized()
|
|
120
|
+
|
|
121
|
+
expect(requests.send.mock.calls[0][2].expiration).toBeUndefined()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should reject at once when aborted before the channels exist', async () => {
|
|
125
|
+
connection.createChannel.mockImplementation(() => new Promise(noop))
|
|
126
|
+
|
|
127
|
+
const controller = new AbortController()
|
|
128
|
+
const reason = new Error(generate())
|
|
129
|
+
|
|
130
|
+
controller.abort(reason)
|
|
131
|
+
|
|
132
|
+
await expect(io.request(queue, payload, { signal: controller.signal })).rejects.toBe(reason)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should send nothing once aborted', async () => {
|
|
136
|
+
const controller = new AbortController()
|
|
137
|
+
|
|
138
|
+
// the channels exist before the aborted Request is made
|
|
139
|
+
io.request(queue, payload).catch(noop)
|
|
140
|
+
|
|
141
|
+
await initialized()
|
|
142
|
+
|
|
143
|
+
controller.abort()
|
|
144
|
+
|
|
145
|
+
await expect(io.request(queue, payload, { signal: controller.signal })).rejects.toBeDefined()
|
|
146
|
+
|
|
147
|
+
expect(requests.send).toHaveBeenCalledTimes(1)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should not re-send what it abandoned', async () => {
|
|
151
|
+
const controller = new AbortController()
|
|
152
|
+
const promise = io.request(queue, payload, { signal: controller.signal })
|
|
153
|
+
|
|
154
|
+
await initialized()
|
|
155
|
+
|
|
156
|
+
controller.abort()
|
|
157
|
+
|
|
158
|
+
await promise.catch(noop)
|
|
159
|
+
|
|
160
|
+
emit(requests, 'recover')
|
|
161
|
+
|
|
162
|
+
await immediate()
|
|
163
|
+
|
|
164
|
+
expect(requests.send).toHaveBeenCalledTimes(1)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should stop waiting for a publication', async () => {
|
|
168
|
+
io.request(queue, payload).catch(noop)
|
|
169
|
+
|
|
170
|
+
await initialized()
|
|
171
|
+
|
|
172
|
+
requests.send.mockImplementationOnce(() => new Promise(noop))
|
|
173
|
+
|
|
174
|
+
const controller = new AbortController()
|
|
175
|
+
const reason = new Error(generate())
|
|
176
|
+
const promise = io.request(queue, payload, { signal: controller.signal })
|
|
177
|
+
|
|
178
|
+
await immediate()
|
|
179
|
+
|
|
180
|
+
controller.abort(reason)
|
|
181
|
+
|
|
182
|
+
await expect(promise).rejects.toBe(reason)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should discard a reply that arrives afterwards', async () => {
|
|
186
|
+
const controller = new AbortController()
|
|
187
|
+
const promise = io.request(queue, payload, { signal: controller.signal })
|
|
188
|
+
|
|
189
|
+
await initialized()
|
|
190
|
+
|
|
191
|
+
controller.abort()
|
|
192
|
+
|
|
193
|
+
await promise.catch(noop)
|
|
194
|
+
|
|
195
|
+
await expect(answer(0)).resolves.toBeUndefined()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('should end the wait within the timeout', async () => {
|
|
199
|
+
const controller = new AbortController()
|
|
200
|
+
const promise = io.request(queue, payload, { signal: controller.signal, timeout: 20 })
|
|
201
|
+
|
|
202
|
+
await expect(promise).rejects.toMatchObject({ name: 'TimeoutError' })
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
async function initialized () {
|
|
207
|
+
// allows initializers to run
|
|
208
|
+
await immediate()
|
|
209
|
+
|
|
210
|
+
requests = await findChannel('request')
|
|
211
|
+
replies = await findChannel('reply')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {number} index of the publication answered
|
|
216
|
+
* @param {Buffer} [content]
|
|
217
|
+
*/
|
|
218
|
+
async function answer (index, content = randomBytes(8)) {
|
|
219
|
+
const { correlationId } = requests.send.mock.calls[index][2]
|
|
220
|
+
const deliver = replies.consume.mock.calls[0][1]
|
|
221
|
+
|
|
222
|
+
await deliver({ content, properties: { correlationId } })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {comq.topology.type} type
|
|
227
|
+
* @returns {jest.MockedObject<comq.Channel>}
|
|
228
|
+
*/
|
|
229
|
+
const findChannel = (type) => {
|
|
230
|
+
const index = connection.createChannel.mock.calls.findIndex(([t]) => (t === type))
|
|
231
|
+
|
|
232
|
+
if (index === -1) throw new Error(`${type} channel hasn't been created`)
|
|
233
|
+
|
|
234
|
+
return connection.createChannel.mock.results[index].value
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @param {jest.MockedObject<comq.Channel>} channel
|
|
239
|
+
* @param {string} event
|
|
240
|
+
*/
|
|
241
|
+
function emit (channel, event) {
|
|
242
|
+
for (const [name, listener] of channel.diagnose.mock.calls) {
|
|
243
|
+
if (name === event) listener()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function noop () {}
|
|
@@ -569,6 +569,74 @@ describe('diagnose', () => {
|
|
|
569
569
|
expect(listener).toHaveBeenCalledWith(message, index)
|
|
570
570
|
})
|
|
571
571
|
|
|
572
|
+
it('should re-route a returned Request under a key on another shard', async () => {
|
|
573
|
+
const listener = /** @type {jest.MockedFunction} */ jest.fn()
|
|
574
|
+
const index = random(channels.length)
|
|
575
|
+
const chan = channels[index]
|
|
576
|
+
const message = returned()
|
|
577
|
+
|
|
578
|
+
message.fields.exchange = generate()
|
|
579
|
+
channel.diagnose('return', listener)
|
|
580
|
+
|
|
581
|
+
emitReturn(chan, message)
|
|
582
|
+
|
|
583
|
+
const rest = channels.filter((one) => one !== chan)
|
|
584
|
+
|
|
585
|
+
const properties = expect.objectContaining(
|
|
586
|
+
{ mandatory: true, headers: { 'x-return': 1 } })
|
|
587
|
+
|
|
588
|
+
for (const one of rest) {
|
|
589
|
+
expect(one.route).toHaveBeenCalledWith(message.fields.exchange, message.fields.routingKey,
|
|
590
|
+
message.content, properties)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
expect(listener).not.toHaveBeenCalled()
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('should report a returned retry at once', async () => {
|
|
597
|
+
const listener = /** @type {jest.MockedFunction} */ jest.fn()
|
|
598
|
+
const index = random(channels.length)
|
|
599
|
+
const chan = channels[index]
|
|
600
|
+
const message = returned()
|
|
601
|
+
|
|
602
|
+
message.fields.exchange = 'comq.retry.' + random(10000)
|
|
603
|
+
channel.diagnose('return', listener)
|
|
604
|
+
|
|
605
|
+
emitReturn(chan, message)
|
|
606
|
+
|
|
607
|
+
for (const one of channels) {
|
|
608
|
+
expect(one.fire).not.toHaveBeenCalled()
|
|
609
|
+
expect(one.route).not.toHaveBeenCalled()
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
expect(listener).toHaveBeenCalledWith(message, index)
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('should hold a queue on every shard', async () => {
|
|
616
|
+
const exchange = generate()
|
|
617
|
+
const queue = generate()
|
|
618
|
+
const key = generate()
|
|
619
|
+
const consumer = jest.fn()
|
|
620
|
+
|
|
621
|
+
await channel.held(exchange, queue, key, consumer)
|
|
622
|
+
|
|
623
|
+
for (const one of channels) expect(one.held).toHaveBeenCalledWith(exchange, queue, key, consumer)
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it('should hold a queue once any shard holds it', async () => {
|
|
627
|
+
const [, ...rest] = channels
|
|
628
|
+
|
|
629
|
+
for (const one of rest) one.held.mockImplementationOnce(() => new Promise(() => undefined))
|
|
630
|
+
|
|
631
|
+
await expect(channel.held(generate(), generate(), generate(), jest.fn())).resolves.toBeUndefined()
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
it('should reject when every shard refuses to hold a queue', async () => {
|
|
635
|
+
for (const one of channels) one.held.mockImplementationOnce(async () => { throw new Error('refused') })
|
|
636
|
+
|
|
637
|
+
await expect(channel.held(generate(), generate(), generate(), jest.fn())).rejects.toBeInstanceOf(AggregateError)
|
|
638
|
+
})
|
|
639
|
+
|
|
572
640
|
it('should emit `pause` and `unpause` events', async () => {
|
|
573
641
|
const queue = generate()
|
|
574
642
|
const buffer = randomBytes(8)
|
package/types/channel.d.ts
CHANGED
|
@@ -22,6 +22,9 @@ declare namespace comq {
|
|
|
22
22
|
/** consumes `queue`, bound to a routed `exchange` under `key` */
|
|
23
23
|
bound (exchange: string, queue: string, key: string, consumer: channels.Consumer): Promise<void>
|
|
24
24
|
|
|
25
|
+
/** consumes `queue`, exclusive to this connection and bound to a routed `exchange` under `key` */
|
|
26
|
+
held (exchange: string, queue: string, key: string, consumer: channels.Consumer): Promise<void>
|
|
27
|
+
|
|
25
28
|
send (queue: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
|
26
29
|
|
|
27
30
|
publish (exchange: string, buffer: Buffer, options?: Options.Publish): Promise<void>
|
package/types/diagnostic.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare namespace comq.diagnostics {
|
|
2
2
|
|
|
3
3
|
type Event = 'open' | 'close' | 'error' | 'reconnect' | 'exhausted' | 'flow' | 'drain' |
|
|
4
|
-
'remove' | 'lost' | 'recover' | 'discard' | 'retry' | 'pause' | 'resume' | 'return'
|
|
4
|
+
'remove' | 'lost' | 'recover' | 'discard' | 'retry' | 'pause' | 'resume' | 'return' | 'taken'
|
|
5
5
|
|
|
6
6
|
interface Diagnosable {
|
|
7
7
|
diagnose(event: Event, listener: Function): void
|
package/types/index.d.ts
CHANGED
|
@@ -21,3 +21,12 @@ export class Retry extends Error {}
|
|
|
21
21
|
* the count like any other failure.
|
|
22
22
|
*/
|
|
23
23
|
export class Park extends Error {}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A Request `IO.call` sent under a key nobody holds. The broker returned it, so it reached no
|
|
27
|
+
* one and was not processed.
|
|
28
|
+
*/
|
|
29
|
+
export class Unroutable extends Error {
|
|
30
|
+
readonly exchange: string
|
|
31
|
+
readonly key: string
|
|
32
|
+
}
|
package/types/io.d.ts
CHANGED
|
@@ -34,14 +34,52 @@ declare namespace comq {
|
|
|
34
34
|
interface Request {
|
|
35
35
|
emitter: ReplyEmitter
|
|
36
36
|
properties: _amqp.Properties
|
|
37
|
+
signal?: AbortSignal
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** How long a caller waits for a Reply. */
|
|
41
|
+
interface RequestOptions {
|
|
42
|
+
encoding?: _encoding.Encoding
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Milliseconds to wait for the Reply. A Request nobody has taken by then is dropped by the
|
|
46
|
+
* broker; one already taken may still be processed, and its Reply is discarded.
|
|
47
|
+
*/
|
|
48
|
+
timeout?: number
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ends the wait when aborted, within the timeout. The Request stays in its queue until the
|
|
52
|
+
* timeout passes, or, without one, until a Producer takes it.
|
|
53
|
+
*/
|
|
54
|
+
signal?: AbortSignal
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The options of a Request, settled once for all its attempts. */
|
|
58
|
+
interface Terms {
|
|
59
|
+
encoding?: _encoding.Encoding
|
|
60
|
+
expires?: number
|
|
61
|
+
signal?: AbortSignal
|
|
37
62
|
}
|
|
38
63
|
|
|
39
64
|
interface IO extends _diagnostics.Diagnosable {
|
|
40
65
|
reply(queue: string, produce: Producer): Promise<void>
|
|
41
66
|
|
|
42
|
-
request<Reply = any, Request = any>(queue: string, payload: Request,
|
|
67
|
+
request<Reply = any, Request = any>(queue: string, payload: Request, options?: _encoding.Encoding | RequestOptions): Promise<Reply> | Promise<Readable>
|
|
68
|
+
|
|
69
|
+
request(queue: string, stream: Readable, options?: _encoding.Encoding | RequestOptions): Promise<Readable>
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sends a Request to whoever holds `key` on the routed `exchange`, through `back`. One that
|
|
73
|
+
* nobody holds is refused with `Unroutable`.
|
|
74
|
+
*/
|
|
75
|
+
call<Reply = any, Request = any>(exchange: string, key: string, payload: Request, options?: _encoding.Encoding | RequestOptions): Promise<Reply> | Promise<Readable>
|
|
43
76
|
|
|
44
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Answers the Requests `call` sends under `key`, from a queue this connection alone holds.
|
|
79
|
+
* While another connection holds it on a broker, claims it there again and again, emitting
|
|
80
|
+
* `taken`. Resolves once a broker holds it.
|
|
81
|
+
*/
|
|
82
|
+
back(exchange: string, key: string, produce: Producer): Promise<void>
|
|
45
83
|
|
|
46
84
|
consume<T = any>(exchange: string, group: string, consumer: Consumer<T>): Promise<void>
|
|
47
85
|
|
|
@@ -107,6 +145,8 @@ declare namespace comq {
|
|
|
107
145
|
|
|
108
146
|
diagnose(event: 'retry', listener: (channel: _topology.type, message: any, error: Error, attempt: number, index?: number) => void): void
|
|
109
147
|
|
|
148
|
+
diagnose(event: 'taken', listener: (channel: _topology.type, queue: string, index?: number) => void): void
|
|
149
|
+
|
|
110
150
|
diagnose(event: 'pause', listener: (channel: _topology.type) => void): void
|
|
111
151
|
|
|
112
152
|
diagnose(event: 'resume', listener: (channel: _topology.type) => void): void
|