comq 0.15.6 → 0.15.8
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/.releaserc +1 -8
- package/features/recovery.feature +30 -0
- package/features/silence.feature +46 -0
- package/features/steps/.types/context.d.ts +17 -0
- package/features/steps/backpressure.js +1 -1
- package/features/steps/broker.js +1 -1
- package/features/steps/brokers.js +1 -1
- package/features/steps/common.js +1 -1
- package/features/steps/connection.js +41 -1
- package/features/steps/context.js +12 -2
- package/features/steps/events.js +1 -1
- package/features/steps/hooks.js +1 -0
- package/features/steps/network.js +50 -0
- package/features/steps/networks.js +90 -0
- package/features/steps/parameters.js +6 -0
- package/features/steps/poison.js +1 -1
- package/features/steps/rpc.js +1 -1
- package/features/steps/tasks.js +1 -1
- package/package.json +3 -6
- package/readme.md +21 -6
- package/source/attributes/lazy.test.js +1 -1
- package/source/attributes/track.test.js +3 -3
- package/source/connection.js +137 -12
- package/source/emitter.js +23 -1
- package/source/encoders/index.js +0 -2
- package/source/events.js +2 -2
- package/source/io.js +6 -5
- package/source/shards/channel.js +34 -17
- package/source/shards/connection.js +0 -5
- package/test/amqplib.mock.js +16 -4
- package/test/channel.fixtures.js +1 -1
- package/test/channel.test.js +1 -1
- package/test/connection.test.js +176 -6
- package/test/decode.test.js +0 -13
- package/test/emitter.test.js +71 -0
- package/test/encode.test.js +0 -9
- package/test/encodings.js +1 -1
- package/test/helpers.js +77 -0
- package/test/io.close.test.js +4 -3
- package/test/io.diagnostics.test.js +2 -2
- package/test/io.reply.test.js +2 -2
- package/test/io.request.test.js +16 -2
- package/test/pipeline.test.js +1 -1
- package/test/shards/channel.test.js +97 -9
- package/test/shards/connection.test.js +4 -3
- package/types/diagnostic.d.ts +2 -2
- package/types/encoding.d.ts +1 -2
- package/types/io.d.ts +2 -0
- package/AGENTS.md +0 -35
- package/CHANGELOG.md +0 -76
- package/source/encoders/msgpack.js +0 -6
package/source/connection.js
CHANGED
|
@@ -50,10 +50,7 @@ class Connection {
|
|
|
50
50
|
* @param {string} url
|
|
51
51
|
*/
|
|
52
52
|
constructor (url) {
|
|
53
|
-
this.#url = url
|
|
54
|
-
|
|
55
|
-
// EventEmitter throws on 'error' with no listeners
|
|
56
|
-
this.#diagnostics.on('error', noop)
|
|
53
|
+
this.#url = heartbeaten(url)
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
get connected () {
|
|
@@ -73,6 +70,11 @@ class Connection {
|
|
|
73
70
|
|
|
74
71
|
await this.#opening
|
|
75
72
|
|
|
73
|
+
if (this.#closed) return
|
|
74
|
+
|
|
75
|
+
// close may have landed after this attempt succeeded but before `#opening` was cleared
|
|
76
|
+
if (this.#connection === undefined) return this.open()
|
|
77
|
+
|
|
76
78
|
this.#running = true
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -109,16 +111,18 @@ class Connection {
|
|
|
109
111
|
#open = async (retry) => {
|
|
110
112
|
if (this.#closed) return
|
|
111
113
|
|
|
114
|
+
// the initial connect finishes before the caller can subscribe
|
|
115
|
+
if (this.#running) this.#diagnostics.emit('reconnect')
|
|
116
|
+
|
|
112
117
|
/** @type {comq.amqp.Connection} */
|
|
113
118
|
let connection
|
|
114
119
|
|
|
115
120
|
try {
|
|
116
|
-
connection = await
|
|
121
|
+
connection = await this.#connect()
|
|
117
122
|
} catch (exception) {
|
|
118
123
|
if (this.#closed) return
|
|
119
124
|
if (!this.#transient(exception)) throw exception
|
|
120
125
|
|
|
121
|
-
// attempts are made until one succeeds, so an outage is observable only here
|
|
122
126
|
this.#diagnostics.emit('error', exception)
|
|
123
127
|
|
|
124
128
|
return retry
|
|
@@ -193,30 +197,78 @@ class Connection {
|
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
connection.removeAllListeners()
|
|
196
|
-
|
|
200
|
+
|
|
201
|
+
// amqplib keeps its heartbeater running on a connection it does not know is
|
|
202
|
+
// gone and emits 'error' on it, which throws once no listener is left
|
|
203
|
+
connection.on('error', noop)
|
|
204
|
+
|
|
205
|
+
// a socket destroyed without an error tells amqplib nothing, leaving its
|
|
206
|
+
// timers running and everything pending on it hanging forever
|
|
207
|
+
connection.connection?.stream?.destroy(silence())
|
|
197
208
|
}
|
|
198
209
|
|
|
199
210
|
#recover () {
|
|
200
211
|
return this.#recovery
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
/**
|
|
215
|
+
* amqplib's `timeout` is a socket idle timer and starts only after DNS.
|
|
216
|
+
* After a machine wakes, `getaddrinfo` itself can hang, so the attempt is
|
|
217
|
+
* also bounded here.
|
|
218
|
+
*
|
|
219
|
+
* @return {Promise<comq.amqp.Connection>}
|
|
220
|
+
*/
|
|
221
|
+
async #connect () {
|
|
222
|
+
const connecting = amqp.connect(this.#url, SOCKET_OPTIONS)
|
|
223
|
+
|
|
224
|
+
let timer
|
|
225
|
+
|
|
226
|
+
const expired = new Promise((_resolve, reject) => {
|
|
227
|
+
timer = setTimeout(() => {
|
|
228
|
+
const exception = new Error('connect ETIMEDOUT')
|
|
229
|
+
|
|
230
|
+
exception.code = 'ETIMEDOUT'
|
|
231
|
+
|
|
232
|
+
reject(exception)
|
|
233
|
+
}, CONNECT_MS)
|
|
234
|
+
|
|
235
|
+
timer.unref()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
expired.catch(noop)
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
return await Promise.race([connecting, expired])
|
|
242
|
+
} catch (exception) {
|
|
243
|
+
connecting.then((connection) => this.#shutdown(connection), noop)
|
|
244
|
+
|
|
245
|
+
throw exception
|
|
246
|
+
} finally {
|
|
247
|
+
clearTimeout(timer)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
203
251
|
/**
|
|
204
252
|
* @param {comq.amqp.Connection} connection
|
|
205
253
|
* @param {number} [timeoutMs]
|
|
206
254
|
*/
|
|
207
|
-
#armWatchdog (connection, timeoutMs =
|
|
255
|
+
#armWatchdog (connection, timeoutMs = tolerance(connection)) {
|
|
256
|
+
this.#disarmWatchdog()
|
|
257
|
+
|
|
208
258
|
const socket = connection.connection?.stream
|
|
209
259
|
|
|
210
|
-
if (socket === undefined) return
|
|
260
|
+
if (socket === undefined || timeoutMs === undefined) return
|
|
211
261
|
|
|
212
262
|
const reset = () => {
|
|
213
263
|
clearTimeout(this.#heartbeatTimer)
|
|
214
|
-
|
|
264
|
+
// destroying a socket without an error tells amqplib nothing: it only
|
|
265
|
+
// listens for 'error' and 'end', so a bare destroy() leaves the connection
|
|
266
|
+
// silently dead — no 'close' event, no recovery, every pending operation
|
|
267
|
+
// hanging forever
|
|
268
|
+
this.#heartbeatTimer = setTimeout(() => socket.destroy(silence()), timeoutMs)
|
|
215
269
|
this.#heartbeatTimer.unref()
|
|
216
270
|
}
|
|
217
271
|
|
|
218
|
-
this.#disarmWatchdog()
|
|
219
|
-
|
|
220
272
|
this.#heartbeatSocket = socket
|
|
221
273
|
this.#heartbeatReset = reset
|
|
222
274
|
|
|
@@ -245,7 +297,25 @@ class Connection {
|
|
|
245
297
|
}
|
|
246
298
|
}
|
|
247
299
|
|
|
300
|
+
/**
|
|
301
|
+
* The heartbeat to ask for when the caller has not, in seconds. A broker is free
|
|
302
|
+
* to suggest one that leaves a connection lost for minutes before anything
|
|
303
|
+
* notices, and RabbitMQ suggests 60 by default.
|
|
304
|
+
*
|
|
305
|
+
* @type {number}
|
|
306
|
+
*/
|
|
307
|
+
const HEARTBEAT_S = 15
|
|
308
|
+
|
|
309
|
+
/** @type {number} */
|
|
310
|
+
const KEEPALIVE_MS = 10_000
|
|
311
|
+
|
|
312
|
+
/** How many heartbeats a connection may miss before it is destroyed. */
|
|
313
|
+
const MISSED_HEARTBEATS = 3
|
|
314
|
+
|
|
248
315
|
/** @type {number} */
|
|
316
|
+
const WATCHDOG_MIN_MS = 15_000
|
|
317
|
+
|
|
318
|
+
/** The watchdog interval used when the negotiated heartbeat is unknown. */
|
|
249
319
|
const WATCHDOG_MS = 60_000
|
|
250
320
|
|
|
251
321
|
/** @type {number} */
|
|
@@ -254,6 +324,15 @@ const CONNECT_MS = 30_000
|
|
|
254
324
|
/** @type {number} */
|
|
255
325
|
const SHUTDOWN_MS = 5_000
|
|
256
326
|
|
|
327
|
+
const SOCKET_OPTIONS = {
|
|
328
|
+
timeout: CONNECT_MS,
|
|
329
|
+
// a peer that went away without a word is noticed by the kernel as well
|
|
330
|
+
keepAlive: true,
|
|
331
|
+
keepAliveDelay: KEEPALIVE_MS
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const HEARTBEAT_SET = /[?&]heartbeat=/
|
|
335
|
+
|
|
257
336
|
const TRANSIENT_CODES = new Set([
|
|
258
337
|
'ECONNREFUSED',
|
|
259
338
|
'EAI_AGAIN',
|
|
@@ -279,6 +358,52 @@ function expiration () {
|
|
|
279
358
|
return delay(timeoutMs, undefined, { ref: false })
|
|
280
359
|
}
|
|
281
360
|
|
|
361
|
+
/**
|
|
362
|
+
* amqplib reads the heartbeat from the URL only, hence it cannot be passed along
|
|
363
|
+
* with the socket options.
|
|
364
|
+
*
|
|
365
|
+
* @param {string} url
|
|
366
|
+
* @return {string}
|
|
367
|
+
*/
|
|
368
|
+
function heartbeaten (url) {
|
|
369
|
+
if (HEARTBEAT_SET.test(url)) return url
|
|
370
|
+
|
|
371
|
+
return url + (url.includes('?') ? '&' : '?') + 'heartbeat=' + HEARTBEAT_S
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* The watchdog measures silence, so it cannot be shorter than the interval at
|
|
376
|
+
* which a healthy broker is expected to say something, which is a heartbeat
|
|
377
|
+
* frame every half a heartbeat. A connection that has agreed to no heartbeats
|
|
378
|
+
* says nothing at all while it is idle, leaving nothing to measure.
|
|
379
|
+
*
|
|
380
|
+
* @param {comq.amqp.Connection} connection
|
|
381
|
+
* @return {number | undefined}
|
|
382
|
+
*/
|
|
383
|
+
function tolerance (connection) {
|
|
384
|
+
const override = global.COMQ_TESTING_WATCHDOG_INTERVAL
|
|
385
|
+
|
|
386
|
+
if (override !== undefined) return override
|
|
387
|
+
|
|
388
|
+
const heartbeat = connection.connection?.heartbeat
|
|
389
|
+
|
|
390
|
+
if (heartbeat === undefined) return WATCHDOG_MS
|
|
391
|
+
if (heartbeat === 0) return undefined
|
|
392
|
+
|
|
393
|
+
return Math.max(heartbeat * MISSED_HEARTBEATS * 1000, WATCHDOG_MIN_MS)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @return {Error}
|
|
398
|
+
*/
|
|
399
|
+
function silence () {
|
|
400
|
+
const exception = new Error('Connection is silent')
|
|
401
|
+
|
|
402
|
+
exception.code = 'ETIMEDOUT'
|
|
403
|
+
|
|
404
|
+
return exception
|
|
405
|
+
}
|
|
406
|
+
|
|
282
407
|
function noop () {}
|
|
283
408
|
|
|
284
409
|
exports.Connection = Connection
|
package/source/emitter.js
CHANGED
|
@@ -2,8 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
const { EventEmitter } = require('node:events')
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* A listener must not be able to break what it observes: an exception thrown by
|
|
7
|
+
* one used to leave the reconnection it was reporting on unfinished, or take the
|
|
8
|
+
* process down through the emitter amqplib was calling.
|
|
9
|
+
*/
|
|
10
|
+
class Emitter extends EventEmitter {
|
|
11
|
+
emit (event, ...args) {
|
|
12
|
+
// raw listeners are used so that `once` still removes itself
|
|
13
|
+
const listeners = this.rawListeners(event)
|
|
14
|
+
|
|
15
|
+
for (const listener of listeners) {
|
|
16
|
+
try {
|
|
17
|
+
listener.apply(this, args)
|
|
18
|
+
} catch {
|
|
19
|
+
// a diagnostic listener has no one to report to
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return listeners.length > 0
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
5
27
|
function create () {
|
|
6
|
-
const emitter = new
|
|
28
|
+
const emitter = new Emitter()
|
|
7
29
|
|
|
8
30
|
emitter.setMaxListeners(0)
|
|
9
31
|
|
package/source/encoders/index.js
CHANGED
package/source/events.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
/** @type {comq.diagnostics.Event[]} */
|
|
4
|
-
exports.connection = ['open', 'close', 'error']
|
|
4
|
+
exports.connection = ['open', 'close', 'error', 'reconnect']
|
|
5
5
|
|
|
6
6
|
/** @type {comq.diagnostics.Event[]} */
|
|
7
|
-
exports.channel = ['flow', 'drain', 'recover', 'discard', 'pause', 'resume', 'return', 'lost']
|
|
7
|
+
exports.channel = ['flow', 'drain', 'recover', 'discard', 'pause', 'resume', 'return', 'lost', 'remove']
|
package/source/io.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const stream = require('node:stream')
|
|
4
|
-
const { EventEmitter } = require('node:events')
|
|
5
4
|
const { randomBytes } = require('node:crypto')
|
|
6
5
|
const { setTimeout } = require('node:timers/promises')
|
|
7
6
|
const { Promex } = require('promex')
|
|
@@ -11,6 +10,7 @@ const { decode } = require('./decode')
|
|
|
11
10
|
const { encode } = require('./encode')
|
|
12
11
|
const { pipeline, transform } = require('./pipeline')
|
|
13
12
|
const events = require('./events')
|
|
13
|
+
const emitter = require('./emitter')
|
|
14
14
|
const io = require('./.io')
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -44,7 +44,7 @@ class IO {
|
|
|
44
44
|
/** @type {Set<comq.Destroyable>} */
|
|
45
45
|
#replyPipes = new Set()
|
|
46
46
|
|
|
47
|
-
#diagnostics =
|
|
47
|
+
#diagnostics = emitter.create()
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* @param {comq.Connection} connection
|
|
@@ -52,9 +52,6 @@ class IO {
|
|
|
52
52
|
constructor (connection) {
|
|
53
53
|
this.#connection = connection
|
|
54
54
|
|
|
55
|
-
// EventEmitter throws on 'error' with no listeners
|
|
56
|
-
this.#diagnostics.on('error', noop)
|
|
57
|
-
|
|
58
55
|
for (const event of events.connection) {
|
|
59
56
|
this.#connection.diagnose(event, (...args) => this.#diagnostics.emit(event, ...args))
|
|
60
57
|
}
|
|
@@ -220,6 +217,10 @@ class IO {
|
|
|
220
217
|
} else {
|
|
221
218
|
this.#requests.diagnose('recover', this.#retransmit)
|
|
222
219
|
}
|
|
220
|
+
|
|
221
|
+
// a reply that could not be routed is dropped by the broker, and the queue
|
|
222
|
+
// it was addressed to only exists again once this channel has recovered
|
|
223
|
+
this.#replies.diagnose('recover', this.#retransmit)
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
/**
|
package/source/shards/channel.js
CHANGED
|
@@ -31,6 +31,9 @@ class Channel {
|
|
|
31
31
|
/** @type {Map<comq.Channel, Promex>} */
|
|
32
32
|
#bench = new Map()
|
|
33
33
|
|
|
34
|
+
/** @type {boolean} */
|
|
35
|
+
#paused = false
|
|
36
|
+
|
|
34
37
|
/** @type {comq.topology.type} */
|
|
35
38
|
#type
|
|
36
39
|
|
|
@@ -126,6 +129,9 @@ class Channel {
|
|
|
126
129
|
this.#alive[index] = false
|
|
127
130
|
this.#down[index].resolve()
|
|
128
131
|
|
|
132
|
+
// the channel stays in the pool, yet there is one less shard to publish to
|
|
133
|
+
this.#update()
|
|
134
|
+
|
|
129
135
|
if (error !== undefined && connection.closed !== true) {
|
|
130
136
|
this.#diagnostics.emit(LOST, index)
|
|
131
137
|
}
|
|
@@ -134,6 +140,8 @@ class Channel {
|
|
|
134
140
|
connection.diagnose('open', () => {
|
|
135
141
|
this.#alive[index] = true
|
|
136
142
|
this.#down[index] = new Promex()
|
|
143
|
+
|
|
144
|
+
this.#update()
|
|
137
145
|
})
|
|
138
146
|
}
|
|
139
147
|
|
|
@@ -159,6 +167,7 @@ class Channel {
|
|
|
159
167
|
for (const event of events.channel) {
|
|
160
168
|
if (event === RETURN) continue // returns are retried before being reported
|
|
161
169
|
if (event === LOST) continue // a shard is lost with its connection, not its channel
|
|
170
|
+
if (event === REMOVE) continue // it is the pool that removes a channel, not the channel
|
|
162
171
|
|
|
163
172
|
channel.diagnose(event, (...args) => this.#diagnostics.emit(event, ...args, channel.index))
|
|
164
173
|
}
|
|
@@ -214,20 +223,23 @@ class Channel {
|
|
|
214
223
|
this.#bench.set(channel, new Promex())
|
|
215
224
|
this.#channels.delete(channel)
|
|
216
225
|
this.#update()
|
|
217
|
-
this.#diagnostics.emit(
|
|
226
|
+
this.#diagnostics.emit(REMOVE, channel.index)
|
|
218
227
|
}
|
|
219
228
|
|
|
229
|
+
/**
|
|
230
|
+
* A shard leaves the pool when it rejects a publish, and stops being reachable
|
|
231
|
+
* when its connection is lost, which the pool is not told about. Both leave
|
|
232
|
+
* `#one` waiting, so both are reported the same way.
|
|
233
|
+
*/
|
|
220
234
|
#update () {
|
|
221
|
-
const from = this.#pool?.length
|
|
222
|
-
const to = this.#channels.size
|
|
223
|
-
|
|
224
235
|
this.#pool = Array.from(this.#channels)
|
|
225
236
|
|
|
226
|
-
|
|
237
|
+
const paused = this.#reachable().length === 0
|
|
227
238
|
|
|
228
|
-
if (
|
|
239
|
+
if (paused === this.#paused) return
|
|
229
240
|
|
|
230
|
-
|
|
241
|
+
this.#paused = paused
|
|
242
|
+
this.#diagnostics.emit(paused ? 'pause' : 'resume')
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
/**
|
|
@@ -318,26 +330,30 @@ class Channel {
|
|
|
318
330
|
}
|
|
319
331
|
|
|
320
332
|
/**
|
|
321
|
-
* The channels of the shards that are known to be connected
|
|
322
|
-
*
|
|
333
|
+
* The channels of the shards that are known to be connected.
|
|
334
|
+
* An empty result means every shard is down or benched — wait rather than
|
|
335
|
+
* publish: a destroyed socket accepts a write without complaining.
|
|
323
336
|
*
|
|
324
337
|
* @return {comq.Channel[]}
|
|
325
338
|
*/
|
|
326
339
|
#reachable () {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return reachable.length === 0 ? this.#pool : reachable
|
|
340
|
+
return this.#pool.filter((channel) => this.#alive[channel.index] !== false)
|
|
330
341
|
}
|
|
331
342
|
|
|
332
343
|
/**
|
|
333
344
|
* @param {(channel: comq.Channel) => void} fn
|
|
334
345
|
*/
|
|
335
346
|
async #one (fn) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// publishing to a lost shard is silently accepted by the destroyed socket, so
|
|
339
|
-
// it must be avoided rather than retried: nothing would report the loss twice
|
|
347
|
+
// capture before the check: recover may replace `#recovery` in between
|
|
348
|
+
const waiting = this.#recovery
|
|
340
349
|
const pool = this.#reachable()
|
|
350
|
+
|
|
351
|
+
if (pool.length === 0) {
|
|
352
|
+
await waiting
|
|
353
|
+
|
|
354
|
+
return this.#one(fn)
|
|
355
|
+
}
|
|
356
|
+
|
|
341
357
|
const channel = pool[Math.floor(Math.random() * pool.length)]
|
|
342
358
|
|
|
343
359
|
try {
|
|
@@ -345,7 +361,7 @@ class Channel {
|
|
|
345
361
|
} catch {
|
|
346
362
|
this.#remove(channel)
|
|
347
363
|
|
|
348
|
-
return
|
|
364
|
+
return this.#one(fn)
|
|
349
365
|
}
|
|
350
366
|
}
|
|
351
367
|
}
|
|
@@ -365,6 +381,7 @@ async function create (connections, type) {
|
|
|
365
381
|
|
|
366
382
|
const DEFAULT = ''
|
|
367
383
|
const RETURN = 'return'
|
|
384
|
+
const REMOVE = 'remove'
|
|
368
385
|
const RETURN_HEADER = 'x-return'
|
|
369
386
|
const LOST = 'lost'
|
|
370
387
|
|
|
@@ -19,9 +19,6 @@ class Connection {
|
|
|
19
19
|
constructor (connections) {
|
|
20
20
|
this.#connections = connections
|
|
21
21
|
|
|
22
|
-
// EventEmitter throws on 'error' with no listeners
|
|
23
|
-
this.#diagnostics.on('error', noop)
|
|
24
|
-
|
|
25
22
|
connections.map(this.#pipe)
|
|
26
23
|
}
|
|
27
24
|
|
|
@@ -75,6 +72,4 @@ class Connection {
|
|
|
75
72
|
}
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
function noop () {}
|
|
79
|
-
|
|
80
75
|
exports.Connection = Connection
|
package/test/amqplib.mock.js
CHANGED
|
@@ -18,14 +18,26 @@ class Channel extends EventEmitter {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
class Connection extends EventEmitter {
|
|
21
|
-
constructor () {
|
|
21
|
+
constructor (url = '') {
|
|
22
22
|
super()
|
|
23
23
|
|
|
24
24
|
const stream = new EventEmitter()
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// amqplib listens for 'error' and 'end' on the socket only, so destroying it
|
|
27
|
+
// without an error leaves the connection unaware and silent forever
|
|
28
|
+
stream.destroy = jest.fn((error) => {
|
|
29
|
+
if (error === undefined || stream.destroyed === true) return
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
stream.destroyed = true
|
|
32
|
+
|
|
33
|
+
this.emit('error', error)
|
|
34
|
+
this.emit('close', error)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// a broker accepts the heartbeat the client asks for
|
|
38
|
+
const heartbeat = Number(/[?&]heartbeat=(\d+)/.exec(url)?.[1] ?? 60)
|
|
39
|
+
|
|
40
|
+
this.connection = { stream, heartbeat }
|
|
29
41
|
|
|
30
42
|
// noinspection JSValidateTypes
|
|
31
43
|
this.removeAllListeners = jest.spyOn(this, 'removeAllListeners')
|
|
@@ -39,7 +51,7 @@ class Connection extends EventEmitter {
|
|
|
39
51
|
close = jest.fn(async () => undefined)
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
const connect = async () => new Connection()
|
|
54
|
+
const connect = async (url) => new Connection(url)
|
|
43
55
|
|
|
44
56
|
/** @type {jest.MockedObject<import('amqplib')>} */
|
|
45
57
|
const amqplib = {
|
package/test/channel.fixtures.js
CHANGED
package/test/channel.test.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const { randomBytes } = require('node:crypto')
|
|
6
6
|
const { generate } = require('randomstring')
|
|
7
|
-
const { flip, random, timeout } = require('
|
|
7
|
+
const { flip, random, timeout } = require('./helpers')
|
|
8
8
|
|
|
9
9
|
const backpressure = require('./backpressure')
|
|
10
10
|
const { amqplib } = require('./amqplib.mock')
|