comq 0.17.0 → 0.18.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.
@@ -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('./helpers')
7
+ const { flip, random, timeout, immediate } = require('./helpers')
8
8
 
9
9
  const backpressure = require('./backpressure')
10
10
  const { amqplib } = require('./amqplib.mock')
@@ -347,9 +347,12 @@ describe.each(['group', 'exclusive'])('%s subscribe', (option) => {
347
347
  })
348
348
 
349
349
  it('should assert fanout exchange', async () => {
350
- expect(chan.assertExchange).toHaveBeenCalledTimes(1)
350
+ // the retry exchange is asserted alongside it when the channel acknowledges
351
+ const calls = chan.assertExchange.mock.calls.filter(([name]) => name === exchange)
351
352
 
352
- const [name, type, options] = chan.assertExchange.mock.calls[0]
353
+ expect(calls).toHaveLength(1)
354
+
355
+ const [name, type, options] = calls[0]
353
356
 
354
357
  expect(name).toStrictEqual(exchange)
355
358
  expect(type).toStrictEqual('fanout')
@@ -385,7 +388,10 @@ describe.each(['group', 'exclusive'])('%s subscribe', (option) => {
385
388
  it('should bind queue to exchange', async () => {
386
389
  const { queue } = await chan.assertQueue.mock.results[0].value
387
390
 
388
- expect(chan.bindQueue).toHaveBeenCalledTimes(1)
391
+ // comq binds its own retry queue too, when the channel acknowledges
392
+ const bindings = chan.bindQueue.mock.calls.filter(([, name]) => !name.startsWith('comq.'))
393
+
394
+ expect(bindings).toHaveLength(1)
389
395
  expect(chan.bindQueue).toHaveBeenCalledWith(queue, exchange, '')
390
396
  })
391
397
 
@@ -910,7 +916,7 @@ describe('diagnostics', () => {
910
916
 
911
917
  const callback = /** @type {Function} */ chan.consume.mock.calls[0][1]
912
918
  const content = randomBytes(8)
913
- const properties = { headers: { 'x-attempt': 5 } }
919
+ const properties = { headers: { 'x-comq-attempt': 5 } }
914
920
  const message = /** @type {comq.amqp.Message} */ { content, properties }
915
921
 
916
922
  await callback(message)
@@ -1029,3 +1035,585 @@ describe('release', () => {
1029
1035
  expect(release).toHaveBeenCalledWith(channel)
1030
1036
  })
1031
1037
  })
1038
+
1039
+ describe('failed messages', () => {
1040
+ const DELAY = 1000
1041
+ const RETRY = 'comq.retry.' + DELAY
1042
+
1043
+ let queue
1044
+ let exception
1045
+ let consumer
1046
+
1047
+ /** @returns {comq.amqp.Message} */
1048
+ const delivery = (properties = {}, fields = {}) => ({
1049
+ content: randomBytes(8),
1050
+ properties,
1051
+ fields: { exchange: generate(), routingKey: generate(), ...fields }
1052
+ })
1053
+
1054
+ /** The consumer amqplib was given, which is comq's wrapper rather than the callback. */
1055
+ const deliver = async (message) => {
1056
+ const callback = /** @type {Function} */ chan.consume.mock.calls[0][1]
1057
+
1058
+ return await callback(message)
1059
+ }
1060
+
1061
+ const publications = () => chan.publish.mock.calls
1062
+
1063
+ beforeEach(async () => {
1064
+ jest.clearAllMocks()
1065
+
1066
+ topology.acknowledgments = true
1067
+ topology.durable = true
1068
+ topology.confirms = true
1069
+ topology.persistent = false
1070
+ topology.delay = [DELAY, DELAY, DELAY, DELAY] // four rungs, hence five attempts
1071
+
1072
+ channel = await create(connection, topology)
1073
+ chan = await getCreatedChannel()
1074
+
1075
+ queue = generate()
1076
+ exception = new Error(generate())
1077
+ consumer = jest.fn(async () => { throw exception })
1078
+ })
1079
+
1080
+ describe('topology', () => {
1081
+ it('should assert the retry queue and its exchange', async () => {
1082
+ await channel.consume(queue, consumer)
1083
+
1084
+ expect(chan.assertExchange).toHaveBeenCalledWith(RETRY, 'fanout', expect.objectContaining({ durable: true }))
1085
+
1086
+ expect(chan.assertQueue).toHaveBeenCalledWith(RETRY, expect.objectContaining({
1087
+ durable: true,
1088
+ arguments: {
1089
+ 'x-message-ttl': DELAY,
1090
+ 'x-dead-letter-exchange': ''
1091
+ }
1092
+ }))
1093
+
1094
+ expect(chan.bindQueue).toHaveBeenCalledWith(RETRY, RETRY, '')
1095
+ })
1096
+
1097
+ it('should not set a dead letter routing key', async () => {
1098
+ // the message keeps its own, which names the queue it came from;
1099
+ // a fixed one would make the queue serve a single source
1100
+ await channel.consume(queue, consumer)
1101
+
1102
+ const [, options] = chan.assertQueue.mock.calls.find(([name]) => name === RETRY)
1103
+
1104
+ expect(options.arguments['x-dead-letter-routing-key']).toBeUndefined()
1105
+ })
1106
+
1107
+ it('should assert the parked queue', async () => {
1108
+ await channel.consume(queue, consumer)
1109
+
1110
+ expect(chan.assertQueue).toHaveBeenCalledWith('comq.parked.' + queue,
1111
+ expect.objectContaining({ durable: true }))
1112
+ })
1113
+
1114
+ it('should assert the source queue first', async () => {
1115
+ await channel.consume(queue, consumer)
1116
+
1117
+ expect(chan.assertQueue.mock.calls[0][0]).toStrictEqual(queue)
1118
+ })
1119
+
1120
+ it('should assert the retry topology once per channel', async () => {
1121
+ await channel.consume(generate(), consumer)
1122
+ await channel.consume(generate(), consumer)
1123
+ await channel.consume(generate(), consumer)
1124
+
1125
+ const exchanges = chan.assertExchange.mock.calls.filter(([name]) => name === RETRY)
1126
+ const queues = chan.assertQueue.mock.calls.filter(([name]) => name === RETRY)
1127
+
1128
+ expect(exchanges).toHaveLength(1)
1129
+ expect(queues).toHaveLength(1)
1130
+ })
1131
+
1132
+ it('should assert a parked queue per consumed queue', async () => {
1133
+ const one = generate()
1134
+ const another = generate()
1135
+
1136
+ await channel.consume(one, consumer)
1137
+ await channel.consume(another, consumer)
1138
+
1139
+ expect(chan.assertQueue).toHaveBeenCalledWith('comq.parked.' + one, expect.anything())
1140
+ expect(chan.assertQueue).toHaveBeenCalledWith('comq.parked.' + another, expect.anything())
1141
+ })
1142
+
1143
+ it('should not assert either without acknowledgments', async () => {
1144
+ jest.clearAllMocks()
1145
+
1146
+ topology.acknowledgments = false
1147
+ channel = await create(connection, topology)
1148
+ chan = await getCreatedChannel()
1149
+
1150
+ await channel.consume(queue, consumer)
1151
+
1152
+ const internal = chan.assertQueue.mock.calls.filter(([name]) => name.startsWith('comq.'))
1153
+
1154
+ expect(internal).toHaveLength(0)
1155
+ expect(chan.assertExchange).not.toHaveBeenCalled()
1156
+ })
1157
+
1158
+ it('should declare the parked queue exclusive for an exclusive queue', async () => {
1159
+ jest.clearAllMocks()
1160
+
1161
+ topology.durable = false
1162
+ channel = await create(connection, topology)
1163
+ chan = await getCreatedChannel()
1164
+
1165
+ await channel.consume(queue, consumer)
1166
+
1167
+ const [, options] = chan.assertQueue.mock.calls
1168
+ .find(([name]) => name === 'comq.parked.' + queue)
1169
+
1170
+ expect(options).toMatchObject({ exclusive: true })
1171
+ })
1172
+
1173
+ it('should re-assert after recovery', async () => {
1174
+ await channel.consume(queue, consumer)
1175
+
1176
+ const replacement = await amqplib.connect()
1177
+
1178
+ await channel.recover(replacement)
1179
+
1180
+ const repl = await getCreatedChannel(replacement)
1181
+
1182
+ expect(repl.assertExchange).toHaveBeenCalledWith(RETRY, 'fanout', expect.anything())
1183
+ expect(repl.assertQueue).toHaveBeenCalledWith('comq.parked.' + queue, expect.anything())
1184
+ })
1185
+ })
1186
+
1187
+ describe('retry', () => {
1188
+ it('should publish to the retry exchange keyed by the source queue', async () => {
1189
+ await channel.consume(queue, consumer)
1190
+
1191
+ const message = delivery()
1192
+
1193
+ await deliver(message)
1194
+
1195
+ const [exchange, key] = publications()[0]
1196
+
1197
+ expect(exchange).toStrictEqual(RETRY)
1198
+ expect(key).toStrictEqual(queue)
1199
+ })
1200
+
1201
+ it('should not republish to the exchange the message came from', async () => {
1202
+ await channel.consume(queue, consumer)
1203
+
1204
+ const message = delivery()
1205
+
1206
+ await deliver(message)
1207
+
1208
+ const [exchange] = publications()[0]
1209
+
1210
+ // a fanout source exchange would otherwise redeliver the retry to every subscriber
1211
+ expect(exchange).not.toStrictEqual(message.fields.exchange)
1212
+ })
1213
+
1214
+ it('should publish before acknowledging', async () => {
1215
+ await channel.consume(queue, consumer)
1216
+
1217
+ await deliver(delivery())
1218
+
1219
+ expect(chan.publish.mock.invocationCallOrder[0])
1220
+ .toBeLessThan(chan.ack.mock.invocationCallOrder[0])
1221
+ })
1222
+
1223
+ it('should wait for the confirmation before acknowledging', async () => {
1224
+ await channel.consume(queue, consumer)
1225
+
1226
+ let confirm
1227
+
1228
+ chan.publish.mockImplementationOnce((_0, _1, _2, _3, callback) => { confirm = callback })
1229
+
1230
+ const pending = deliver(delivery())
1231
+
1232
+ await immediate()
1233
+
1234
+ expect(chan.ack).not.toHaveBeenCalled()
1235
+
1236
+ confirm(null)
1237
+ await pending
1238
+
1239
+ expect(chan.ack).toHaveBeenCalled()
1240
+ })
1241
+
1242
+ it('should increment the attempt', async () => {
1243
+ await channel.consume(queue, consumer)
1244
+
1245
+ await deliver(delivery({ headers: { 'x-comq-attempt': 2 } }))
1246
+
1247
+ const [, , , options] = publications()[0]
1248
+
1249
+ expect(options.headers['x-comq-attempt']).toStrictEqual(3)
1250
+ })
1251
+
1252
+ it('should tolerate a message without headers', async () => {
1253
+ // a message published by something that is not comq carries no field table,
1254
+ // and the first delivery of any message carries no attempt either
1255
+ await channel.consume(queue, consumer)
1256
+
1257
+ await expect(deliver(delivery({}))).resolves.not.toThrow()
1258
+
1259
+ const [, , , options] = publications()[0]
1260
+
1261
+ expect(options.headers['x-comq-attempt']).toStrictEqual(2)
1262
+ })
1263
+
1264
+ it('should record the origin on the first failure', async () => {
1265
+ await channel.consume(queue, consumer)
1266
+
1267
+ const message = delivery()
1268
+
1269
+ await deliver(message)
1270
+
1271
+ const [, , , options] = publications()[0]
1272
+
1273
+ expect(options.headers['x-comq-exchange']).toStrictEqual(message.fields.exchange)
1274
+ expect(options.headers['x-comq-key']).toStrictEqual(message.fields.routingKey)
1275
+ })
1276
+
1277
+ it('should keep the recorded origin on later failures', async () => {
1278
+ // a returned retry arrives through the default exchange, so its own fields
1279
+ // no longer say where it was published
1280
+ await channel.consume(queue, consumer)
1281
+
1282
+ const origin = generate()
1283
+ const message = delivery(
1284
+ { headers: { 'x-comq-attempt': 1, 'x-comq-exchange': origin, 'x-comq-key': generate() } },
1285
+ { exchange: '' })
1286
+
1287
+ await deliver(message)
1288
+
1289
+ const [, , , options] = publications()[0]
1290
+
1291
+ expect(options.headers['x-comq-exchange']).toStrictEqual(origin)
1292
+ })
1293
+
1294
+ it('should publish persistent from a transient topology', async () => {
1295
+ await channel.consume(queue, consumer)
1296
+
1297
+ await deliver(delivery())
1298
+
1299
+ const [, , , options] = publications()[0]
1300
+
1301
+ expect(options.persistent).toStrictEqual(true)
1302
+ })
1303
+
1304
+ it('should not mutate the message', async () => {
1305
+ await channel.consume(queue, consumer)
1306
+
1307
+ const message = delivery({})
1308
+
1309
+ await deliver(message)
1310
+
1311
+ expect(message.properties.headers).toBeUndefined()
1312
+ })
1313
+
1314
+ it('should emit the `retry` event', async () => {
1315
+ const listener = jest.fn()
1316
+
1317
+ channel.diagnose('retry', listener)
1318
+
1319
+ await channel.consume(queue, consumer)
1320
+
1321
+ const message = delivery()
1322
+
1323
+ await deliver(message)
1324
+
1325
+ expect(listener).toHaveBeenCalledWith(message, exception, 1)
1326
+ })
1327
+
1328
+ it('should not throw', async () => {
1329
+ // amqplib drops the promise it gets back, so a rejection ends the process
1330
+ await channel.consume(queue, consumer)
1331
+
1332
+ await expect(deliver(delivery())).resolves.not.toThrow()
1333
+ })
1334
+
1335
+ it('should not seal the channel', async () => {
1336
+ await channel.consume(queue, consumer)
1337
+
1338
+ await deliver(delivery())
1339
+
1340
+ expect(chan.cancel).not.toHaveBeenCalled()
1341
+ })
1342
+
1343
+ it('should keep consuming', async () => {
1344
+ const another = generate()
1345
+
1346
+ await channel.consume(queue, consumer)
1347
+ await deliver(delivery())
1348
+
1349
+ await expect(channel.consume(another, jest.fn())).resolves.not.toThrow()
1350
+
1351
+ expect(chan.consume).toHaveBeenCalledWith(another, expect.any(Function), expect.anything())
1352
+ })
1353
+
1354
+ it('should ignore an exception thrown after the channel closed', async () => {
1355
+ await channel.consume(queue, consumer)
1356
+
1357
+ consumer.mockImplementationOnce(async () => { throw new Error('Channel closed') })
1358
+
1359
+ await expect(deliver(delivery())).resolves.not.toThrow()
1360
+
1361
+ expect(chan.publish).not.toHaveBeenCalled()
1362
+ expect(chan.ack).not.toHaveBeenCalled()
1363
+ expect(chan.nack).not.toHaveBeenCalled()
1364
+ })
1365
+
1366
+ it('should tolerate a rejection that is not an Error', async () => {
1367
+ await channel.consume(queue, consumer)
1368
+
1369
+ consumer.mockImplementationOnce(async () => { throw undefined }) // eslint-disable-line
1370
+
1371
+ await expect(deliver(delivery())).resolves.not.toThrow()
1372
+
1373
+ expect(chan.publish).toHaveBeenCalled()
1374
+ })
1375
+ })
1376
+
1377
+ describe('the healthy path', () => {
1378
+ it('should not make ordinary publishing persistent', async () => {
1379
+ // only a message that has already failed is worth a disk write; Requests are
1380
+ // transient for the sake of latency and must stay that way
1381
+ jest.clearAllMocks()
1382
+
1383
+ topology.persistent = false
1384
+ channel = await create(connection, topology)
1385
+ chan = await getCreatedChannel()
1386
+
1387
+ await channel.send(generate(), randomBytes(8))
1388
+ await channel.publish(generate(), randomBytes(8))
1389
+
1390
+ for (const [, , , options] of chan.publish.mock.calls) {
1391
+ expect(options.persistent).toStrictEqual(false)
1392
+ }
1393
+ })
1394
+ })
1395
+
1396
+ describe('backoff', () => {
1397
+ const LADDER = [1000, 5000, 25000]
1398
+
1399
+ /** @returns {string[]} the retry queues asserted, in order */
1400
+ const asserted = () => chan.assertQueue.mock.calls
1401
+ .map(([name]) => name)
1402
+ .filter((name) => name.startsWith('comq.retry.'))
1403
+
1404
+ beforeEach(async () => {
1405
+ jest.clearAllMocks()
1406
+
1407
+ topology.delay = LADDER
1408
+
1409
+ channel = await create(connection, topology)
1410
+ chan = await getCreatedChannel()
1411
+ })
1412
+
1413
+ it('should declare a queue and an exchange per rung', async () => {
1414
+ await channel.consume(queue, consumer)
1415
+
1416
+ expect(asserted()).toStrictEqual(LADDER.map((delay) => 'comq.retry.' + delay))
1417
+
1418
+ for (const delay of LADDER) {
1419
+ const name = 'comq.retry.' + delay
1420
+
1421
+ expect(chan.assertExchange).toHaveBeenCalledWith(name, 'fanout', expect.anything())
1422
+ expect(chan.bindQueue).toHaveBeenCalledWith(name, name, '')
1423
+
1424
+ const [, options] = chan.assertQueue.mock.calls.find(([asserted]) => asserted === name)
1425
+
1426
+ expect(options.arguments['x-message-ttl']).toStrictEqual(delay)
1427
+ }
1428
+ })
1429
+
1430
+ it('should declare a repeated rung once', async () => {
1431
+ jest.clearAllMocks()
1432
+
1433
+ topology.delay = [1000, 5000, 1000]
1434
+ channel = await create(connection, topology)
1435
+ chan = await getCreatedChannel()
1436
+
1437
+ await channel.consume(queue, consumer)
1438
+
1439
+ expect(asserted()).toStrictEqual(['comq.retry.1000', 'comq.retry.5000'])
1440
+ })
1441
+
1442
+ it('should climb the ladder', async () => {
1443
+ await channel.consume(queue, consumer)
1444
+
1445
+ await deliver(delivery({}))
1446
+ await deliver(delivery({ headers: { 'x-comq-attempt': 2 } }))
1447
+ await deliver(delivery({ headers: { 'x-comq-attempt': 3 } }))
1448
+ await deliver(delivery({ headers: { 'x-comq-attempt': 4 } }))
1449
+
1450
+ expect(publications().map(([exchange]) => exchange)).toStrictEqual([
1451
+ 'comq.retry.1000',
1452
+ 'comq.retry.5000',
1453
+ 'comq.retry.25000',
1454
+ '' // parked
1455
+ ])
1456
+ })
1457
+
1458
+ it('should take a number as a ladder of one', async () => {
1459
+ jest.clearAllMocks()
1460
+
1461
+ topology.delay = 1000
1462
+
1463
+ channel = await create(connection, topology)
1464
+ chan = await getCreatedChannel()
1465
+
1466
+ await channel.consume(queue, consumer)
1467
+
1468
+ await deliver(delivery({}))
1469
+ await deliver(delivery({ headers: { 'x-comq-attempt': 2 } }))
1470
+
1471
+ expect(asserted()).toStrictEqual(['comq.retry.1000'])
1472
+ expect(publications().map(([exchange, key]) => exchange || key))
1473
+ .toStrictEqual(['comq.retry.1000', 'comq.parked.' + queue])
1474
+ })
1475
+ })
1476
+
1477
+ describe('parking', () => {
1478
+ const exhausted = () => delivery({ headers: { 'x-comq-attempt': 5 } })
1479
+
1480
+ it('should publish to the parked queue once the attempts are spent', async () => {
1481
+ await channel.consume(queue, consumer)
1482
+
1483
+ await deliver(exhausted())
1484
+
1485
+ const [exchange, key] = publications()[0]
1486
+
1487
+ expect(exchange).toStrictEqual('')
1488
+ expect(key).toStrictEqual('comq.parked.' + queue)
1489
+ })
1490
+
1491
+ it('should not retry once the attempts are spent', async () => {
1492
+ await channel.consume(queue, consumer)
1493
+
1494
+ await deliver(exhausted())
1495
+
1496
+ expect(publications()).toHaveLength(1)
1497
+ expect(chan.nack).not.toHaveBeenCalled()
1498
+ })
1499
+
1500
+ it('should publish before acknowledging', async () => {
1501
+ await channel.consume(queue, consumer)
1502
+
1503
+ await deliver(exhausted())
1504
+
1505
+ expect(chan.publish.mock.invocationCallOrder[0])
1506
+ .toBeLessThan(chan.ack.mock.invocationCallOrder[0])
1507
+ })
1508
+
1509
+ it('should record where the message came from', async () => {
1510
+ await channel.consume(queue, consumer)
1511
+
1512
+ const origin = generate()
1513
+ const message = delivery({ headers: { 'x-comq-attempt': 5, 'x-comq-exchange': origin } })
1514
+
1515
+ await deliver(message)
1516
+
1517
+ const [, , , options] = publications()[0]
1518
+
1519
+ expect(options.headers['x-comq-exchange']).toStrictEqual(origin)
1520
+ expect(options.headers['x-comq-queue']).toStrictEqual(queue)
1521
+ expect(options.headers['x-comq-reason']).toStrictEqual(exception.message)
1522
+ expect(options.headers['x-comq-at']).toStrictEqual(expect.any(Number))
1523
+ })
1524
+
1525
+ it('should keep replyTo and correlationId', async () => {
1526
+ // a parked Request remains answerable while its caller is still waiting
1527
+ await channel.consume(queue, consumer)
1528
+
1529
+ const replyTo = generate()
1530
+ const correlationId = generate()
1531
+
1532
+ await deliver(delivery({ headers: { 'x-comq-attempt': 5 }, replyTo, correlationId }))
1533
+
1534
+ const [, , , options] = publications()[0]
1535
+
1536
+ expect(options.replyTo).toStrictEqual(replyTo)
1537
+ expect(options.correlationId).toStrictEqual(correlationId)
1538
+ })
1539
+
1540
+ it('should publish persistent from a transient topology', async () => {
1541
+ await channel.consume(queue, consumer)
1542
+
1543
+ await deliver(exhausted())
1544
+
1545
+ const [, , , options] = publications()[0]
1546
+
1547
+ expect(options.persistent).toStrictEqual(true)
1548
+ })
1549
+
1550
+ it('should count the first delivery as an attempt', async () => {
1551
+ // a ladder of one rung is a single retry, so the second delivery is the last
1552
+ jest.clearAllMocks()
1553
+
1554
+ topology.delay = 1000
1555
+ channel = await create(connection, topology)
1556
+ chan = await getCreatedChannel()
1557
+
1558
+ await channel.consume(queue, consumer)
1559
+
1560
+ await deliver(delivery({}))
1561
+ await deliver(delivery({ headers: { 'x-comq-attempt': 2 } }))
1562
+
1563
+ const targets = publications().map(([, key]) => key)
1564
+
1565
+ expect(targets).toStrictEqual([queue, 'comq.parked.' + queue])
1566
+ })
1567
+
1568
+ it('should give a message one delivery more than the ladder has rungs', async () => {
1569
+ jest.clearAllMocks()
1570
+
1571
+ topology.delay = [1000, 2000]
1572
+ channel = await create(connection, topology)
1573
+ chan = await getCreatedChannel()
1574
+
1575
+ await channel.consume(queue, consumer)
1576
+
1577
+ await deliver(delivery({}))
1578
+ await deliver(delivery({ headers: { 'x-comq-attempt': 2 } }))
1579
+ await deliver(delivery({ headers: { 'x-comq-attempt': 3 } }))
1580
+
1581
+ const targets = publications().map(([, key]) => key)
1582
+
1583
+ expect(targets).toStrictEqual([queue, queue, 'comq.parked.' + queue])
1584
+ })
1585
+ })
1586
+
1587
+ describe('when the message cannot be moved', () => {
1588
+ it('should give the delivery back', async () => {
1589
+ await channel.consume(queue, consumer)
1590
+
1591
+ chan.publish.mockImplementation(() => { throw new Error(generate()) })
1592
+
1593
+ await expect(deliver(delivery())).resolves.not.toThrow()
1594
+
1595
+ expect(chan.nack).toHaveBeenCalledWith(expect.anything(), false, true)
1596
+ expect(chan.ack).not.toHaveBeenCalled()
1597
+ })
1598
+
1599
+ it('should give it back when the confirmation is rejected', async () => {
1600
+ await channel.consume(queue, consumer)
1601
+
1602
+ chan.publish.mockImplementationOnce((_0, _1, _2, _3, callback) => callback(new Error(generate())))
1603
+
1604
+ await expect(deliver(delivery())).resolves.not.toThrow()
1605
+
1606
+ expect(chan.nack).toHaveBeenCalledWith(expect.anything(), false, true)
1607
+ expect(chan.ack).not.toHaveBeenCalled()
1608
+ })
1609
+
1610
+ it('should not throw when giving it back also fails', async () => {
1611
+ await channel.consume(queue, consumer)
1612
+
1613
+ chan.publish.mockImplementation(() => { throw new Error(generate()) })
1614
+ chan.nack.mockImplementation(() => { throw new Error('Channel closed') })
1615
+
1616
+ await expect(deliver(delivery())).resolves.not.toThrow()
1617
+ })
1618
+ })
1619
+ })
@@ -40,7 +40,7 @@ describe('single connection', () => {
40
40
  })
41
41
 
42
42
  it('should pass active connection', async () => {
43
- expect(Connection).toHaveBeenCalledWith(url)
43
+ expect(Connection).toHaveBeenCalledWith(url, {})
44
44
 
45
45
  /** @type {jest.MockedObject<comq.Connection>} */
46
46
  const instance = Connection.mock.instances[0]
@@ -62,10 +62,39 @@ describe('sharded connection', () => {
62
62
  })
63
63
 
64
64
  it('should pass single connection instances', async () => {
65
- urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url))
65
+ urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, {}))
66
66
 
67
67
  const instances = Connection.mock.instances
68
68
 
69
69
  expect(shards.Connection).toHaveBeenCalledWith(instances)
70
70
  })
71
71
  })
72
+
73
+ describe('topology overrides', () => {
74
+ it('should pass the trailing argument to the connection', async () => {
75
+ const overrides = { event: { delay: 100 } }
76
+
77
+ await connect(url, overrides)
78
+
79
+ expect(Connection).toHaveBeenCalledWith(url, overrides)
80
+ })
81
+
82
+ it('should pass them to every shard', async () => {
83
+ const urls = [generate(), generate()]
84
+ const overrides = { request: { attempts: 1 } }
85
+
86
+ await connect(...urls, overrides)
87
+
88
+ urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, overrides))
89
+ expect(shards.Connection).toHaveBeenCalled()
90
+ })
91
+
92
+ it('should not take a url as overrides', async () => {
93
+ const urls = [generate(), generate()]
94
+
95
+ await connect(...urls)
96
+
97
+ expect(Connection).toHaveBeenCalledTimes(2)
98
+ urls.forEach((url) => expect(Connection).toHaveBeenCalledWith(url, {}))
99
+ })
100
+ })
package/test/helpers.js CHANGED
@@ -4,6 +4,20 @@ const timeout = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
4
4
 
5
5
  const immediate = () => new Promise((resolve) => setImmediate(resolve))
6
6
 
7
+ /**
8
+ * Waits for a condition rather than for a duration.
9
+ *
10
+ * @param {() => boolean} condition
11
+ * @param {number} [ms] how long to keep asking
12
+ */
13
+ const until = async (condition, ms = 5000) => {
14
+ const deadline = Date.now() + ms
15
+
16
+ while (!condition() && Date.now() < deadline) await timeout(20)
17
+
18
+ return condition()
19
+ }
20
+
7
21
  const random = (max = 100) => Math.floor(Math.random() * max)
8
22
 
9
23
  const flip = () => Math.random() < 0.5
@@ -70,6 +84,7 @@ const MULTIPLIERS = Object.entries({
70
84
 
71
85
  exports.timeout = timeout
72
86
  exports.immediate = immediate
87
+ exports.until = until
73
88
  exports.random = random
74
89
  exports.flip = flip
75
90
  exports.sample = sample