comq 0.25.1 → 0.26.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.
Files changed (44) hide show
  1. package/docs/headers.md +16 -1
  2. package/features/properties.feature +39 -0
  3. package/features/shards.virtual.feature +89 -0
  4. package/features/steps/context.js +29 -2
  5. package/features/steps/hooks.js +1 -0
  6. package/features/steps/names.js +245 -0
  7. package/features/steps/networks.js +23 -5
  8. package/features/steps/rpc.js +14 -2
  9. package/package.json +1 -1
  10. package/readme.md +102 -4
  11. package/source/channel.js +147 -2
  12. package/source/connect.js +18 -15
  13. package/source/connection.js +8 -5
  14. package/source/events.js +7 -0
  15. package/source/io.js +31 -9
  16. package/source/shards/channel.js +289 -19
  17. package/source/shards/connection.js +404 -18
  18. package/source/shards/expand.js +57 -0
  19. package/source/shards/index.js +2 -0
  20. package/source/shards/resolve.js +134 -0
  21. package/source/shards/watch.js +257 -0
  22. package/source/singleton.js +3 -2
  23. package/test/amqplib.mock.js +1 -0
  24. package/test/assert.test.js +7 -2
  25. package/test/channel.quiet.test.js +301 -0
  26. package/test/connect.test.js +49 -8
  27. package/test/connection.mock.js +3 -0
  28. package/test/connection.test.js +15 -0
  29. package/test/io.call.test.js +1 -1
  30. package/test/io.diagnostics.test.js +1 -1
  31. package/test/io.reply.test.js +12 -1
  32. package/test/io.request.test.js +18 -0
  33. package/test/io.shards.test.js +85 -0
  34. package/test/shards/channel.test.js +411 -0
  35. package/test/shards/connection.test.js +372 -17
  36. package/test/shards/expand.test.js +58 -0
  37. package/test/shards/resolve.test.js +73 -0
  38. package/test/shards/watch.test.js +264 -0
  39. package/test/singleton.test.js +1 -1
  40. package/types/channel.d.ts +23 -0
  41. package/types/connection.d.ts +13 -2
  42. package/types/diagnostic.d.ts +2 -1
  43. package/types/io.d.ts +17 -1
  44. package/types/shards.d.ts +58 -0
@@ -42,6 +42,21 @@ describe('initial connection', () => {
42
42
  expect.objectContaining({ timeout: expect.any(Number) }))
43
43
  })
44
44
 
45
+ it('should connect without a name to verify the broker by', async () => {
46
+ await connection.open()
47
+
48
+ expect(amqplib.connect.mock.calls[0][1]).not.toHaveProperty('servername')
49
+ })
50
+
51
+ it('should verify the broker by the name its address was resolved from', async () => {
52
+ connection = new Connection(url, {}, 'rmq.example.com')
53
+
54
+ await connection.open()
55
+
56
+ expect(amqplib.connect).toHaveBeenCalledWith(expect.stringContaining(url),
57
+ expect.objectContaining({ servername: 'rmq.example.com', timeout: expect.any(Number) }))
58
+ })
59
+
45
60
  it.each(/** @type {[string, Partial<Error>][]} */[
46
61
  ['Socket closed', { message: 'Socket closed abruptly during opening handshake' }],
47
62
  ['TLS disconnect', { message: 'Client network socket disconnected before secure TLS connection was established' }],
@@ -122,7 +122,7 @@ describe('back', () => {
122
122
 
123
123
  replies = await findChannel('reply')
124
124
 
125
- expect(producer).toHaveBeenCalledWith(payload)
125
+ expect(producer).toHaveBeenCalledWith(payload, properties)
126
126
 
127
127
  expect(replies.fire).toHaveBeenCalledWith(properties.replyTo, expect.any(Buffer),
128
128
  expect.objectContaining({ correlationId: properties.correlationId }), request)
@@ -61,7 +61,7 @@ it('should not throw on error without listeners', async () => {
61
61
  expect(() => emit(new Error(generate()))).not.toThrow()
62
62
  })
63
63
 
64
- it.each(['open', 'close', 'error', 'reconnect', 'exhausted'])('should re-emit %s from connection',
64
+ it.each(['open', 'close', 'error', 'reconnect', 'exhausted', 'join', 'retire', 'leave'])('should re-emit %s from connection',
65
65
  /**
66
66
  * @param {comq.diagnostics.Event} event
67
67
  */
@@ -102,6 +102,17 @@ describe('reply', () => {
102
102
  expect(replies.fire).toHaveBeenCalledWith(properties.replyTo, buffer, expect.anything(), message)
103
103
  })
104
104
 
105
+ it('should pass the properties of the request to the producer', async () => {
106
+ const content = randomBytes(10)
107
+ const properties = { contentType: 'text/plain', headers: { [generate()]: generate() } }
108
+ const message = /** @type {comq.amqp.Message} */ { content, properties }
109
+ const producer = requests.consume.mock.calls[0][1]
110
+
111
+ await producer(message)
112
+
113
+ expect(produce).toHaveBeenCalledWith(expect.anything(), properties)
114
+ })
115
+
105
116
  it('should throw if producer returned undefined', async () => {
106
117
  jest.clearAllMocks()
107
118
 
@@ -161,7 +172,7 @@ describe('encoding', () => {
161
172
 
162
173
  await producer(message)
163
174
 
164
- expect(produce).toHaveBeenCalledWith(value)
175
+ expect(produce).toHaveBeenCalledWith(value, properties)
165
176
  })
166
177
 
167
178
  it.each(encodings)('should encode reply with same encoding (%s)', async (encoding) => {
@@ -120,6 +120,24 @@ describe('send', () => {
120
120
  expect(call[2].expiration).toBeUndefined()
121
121
  })
122
122
 
123
+ it('should send the properties it is given, as a Request still', async () => {
124
+ requests.send.mockClear()
125
+
126
+ const headers = { [generate()]: generate() }
127
+
128
+ setImmediate(reply)
129
+
130
+ await io.request(queue, payload, { headers, contentType: 'text/plain', replyTo: generate() })
131
+
132
+ const [, buffer, properties] = requests.send.mock.calls[0]
133
+
134
+ expect(buffer).toStrictEqual(encode(payload, 'text/plain'))
135
+ expect(properties.headers).toStrictEqual(headers)
136
+ expect(properties.contentType).toStrictEqual('text/plain')
137
+ expect(properties.replyTo).toStrictEqual(call[2].replyTo)
138
+ expect(properties.correlationId).toBeDefined()
139
+ })
140
+
123
141
  it('should refuse options other than the encoding', async () => {
124
142
  requests.send.mockClear()
125
143
 
@@ -0,0 +1,85 @@
1
+ 'use strict'
2
+
3
+ const { generate } = require('randomstring')
4
+ const { immediate } = require('./helpers')
5
+ const { encode } = require('../source/encode')
6
+
7
+ const mock = require('./connection.mock')
8
+ const { IO } = require('../source/io')
9
+
10
+ /** @type {comq.IO} */
11
+ let io
12
+
13
+ /** @type {jest.MockedObject<comq.Connection>} */
14
+ let connection
15
+
16
+ /** @type {jest.MockedObject<comq.Channel>} */
17
+ let requests
18
+
19
+ /** @type {jest.MockedObject<comq.Channel>} */
20
+ let replies
21
+
22
+ /** @type {(index: number) => boolean} */
23
+ let occupied
24
+
25
+ let promise
26
+
27
+ const findChannel = (type) => {
28
+ const index = connection.createChannel.mock.calls.findIndex(([t]) => (t === type))
29
+
30
+ return connection.createChannel.mock.results[index].value
31
+ }
32
+
33
+ beforeEach(async () => {
34
+ jest.clearAllMocks()
35
+
36
+ connection = mock.connection(true)
37
+ io = new IO(connection)
38
+ promise = io.request(generate(), { [generate()]: generate() })
39
+
40
+ await immediate()
41
+
42
+ requests = await findChannel('request')
43
+ replies = await findChannel('reply')
44
+ occupied = requests.occupy.mock.calls[0][0]
45
+ })
46
+
47
+ it('should tell the request channel which shards owe a Reply', async () => {
48
+ expect(requests.occupy).toHaveBeenCalledWith(expect.any(Function))
49
+ })
50
+
51
+ it('should say so of the shard an unanswered Request went through', async () => {
52
+ expect(occupied(0)).toStrictEqual(true)
53
+ })
54
+
55
+ it('should not say so of another shard', async () => {
56
+ expect(occupied(1)).toStrictEqual(false)
57
+ })
58
+
59
+ it('should not say so once the Request is answered', async () => {
60
+ const { correlationId, contentType } = requests.send.mock.calls[0][2]
61
+ const consumer = replies.consume.mock.calls[0][1]
62
+
63
+ consumer({ content: encode(generate(), contentType), properties: { correlationId, contentType } })
64
+
65
+ await promise
66
+
67
+ expect(occupied(0)).toStrictEqual(false)
68
+ })
69
+
70
+ it('should not tell a channel that is not sharded', async () => {
71
+ connection = mock.connection()
72
+ connection.createChannel.mockImplementation(async (type, index) => {
73
+ const channel = mock.channel(false, index)
74
+
75
+ delete channel.occupy
76
+
77
+ return channel
78
+ })
79
+
80
+ io = new IO(connection)
81
+
82
+ expect(() => io.request(generate(), generate())).not.toThrow()
83
+
84
+ await immediate()
85
+ })
@@ -985,3 +985,414 @@ function emitRecover (chan) {
985
985
 
986
986
  for (const call of calls) call[1]()
987
987
  }
988
+
989
+ describe('a shard that joins', () => {
990
+ /** @type {jest.MockedObject<comq.Connection>} */
991
+ let joiner
992
+
993
+ /** @type {jest.MockedObject<comq.Channel>} */
994
+ let joined
995
+
996
+ const consumer = jest.fn()
997
+
998
+ const channelOf = async (connection) => await connection.createChannel.mock.results.at(-1).value
999
+
1000
+ const sendMany = async () => {
1001
+ for (let i = 0; i < 100; i++) await channel.send(generate(), randomBytes(8))
1002
+ }
1003
+
1004
+ beforeEach(async () => {
1005
+ channel = await create(connections, type)
1006
+ joiner = mock.connection()
1007
+ })
1008
+
1009
+ it('should tell whether a shard is its own', async () => {
1010
+ expect(channel.has(1)).toStrictEqual(true)
1011
+ expect(channel.has(2)).toStrictEqual(false)
1012
+
1013
+ await channel.join(joiner, 2)
1014
+
1015
+ expect(channel.has(2)).toStrictEqual(true)
1016
+ })
1017
+
1018
+ it('should create a channel of the type, numbered as the shard is', async () => {
1019
+ await channel.join(joiner, 2)
1020
+
1021
+ expect(joiner.createChannel).toHaveBeenCalledWith(type, 2)
1022
+ })
1023
+
1024
+ it('should not join twice', async () => {
1025
+ await channel.join(joiner, 2)
1026
+ await channel.join(joiner, 2)
1027
+
1028
+ expect(joiner.createChannel).toHaveBeenCalledTimes(1)
1029
+ })
1030
+
1031
+ it('should consume what has been consumed', async () => {
1032
+ await channel.consume('q', consumer)
1033
+ await channel.subscribe('x', 'g', consumer)
1034
+ await channel.bound('x', 'q', 'k', consumer)
1035
+ await channel.held('x', 'q', 'k', consumer)
1036
+
1037
+ await channel.join(joiner, 2)
1038
+
1039
+ joined = await channelOf(joiner)
1040
+
1041
+ expect(joined.consume).toHaveBeenCalledWith('q', expect.any(Function))
1042
+ expect(joined.subscribe).toHaveBeenCalledWith('x', 'g', consumer)
1043
+ expect(joined.bound).toHaveBeenCalledWith('x', 'q', 'k', consumer)
1044
+ expect(joined.held).toHaveBeenCalledWith('x', 'q', 'k', expect.any(Function))
1045
+ })
1046
+
1047
+ it('should consume what is consumed afterwards', async () => {
1048
+ await channel.join(joiner, 2)
1049
+ await channel.consume('q', consumer)
1050
+
1051
+ joined = await channelOf(joiner)
1052
+
1053
+ expect(joined.consume).toHaveBeenCalledTimes(1)
1054
+ })
1055
+
1056
+ it('should not be published through before it consumes', async () => {
1057
+ const consuming = new Promex()
1058
+
1059
+ await channel.consume('q', consumer)
1060
+
1061
+ joiner.createChannel.mockImplementationOnce(async (type, index) => {
1062
+ const chan = mock.channel(false, index)
1063
+
1064
+ chan.consume.mockImplementation(() => consuming)
1065
+
1066
+ return chan
1067
+ })
1068
+
1069
+ const joining = channel.join(joiner, 2)
1070
+
1071
+ await immediate()
1072
+ await sendMany()
1073
+
1074
+ joined = await channelOf(joiner)
1075
+
1076
+ expect(joined.consume).toHaveBeenCalled()
1077
+ expect(joined.send).not.toHaveBeenCalled()
1078
+
1079
+ consuming.resolve()
1080
+
1081
+ await joining
1082
+ await sendMany()
1083
+
1084
+ expect(joined.send).toHaveBeenCalled()
1085
+ })
1086
+
1087
+ it('should not wait for a key that is held by another', async () => {
1088
+ await channel.held('x', 'q', 'k', consumer)
1089
+
1090
+ joiner.createChannel.mockImplementationOnce(async (type, index) => {
1091
+ const chan = mock.channel(false, index)
1092
+
1093
+ chan.held.mockImplementation(() => new Promex())
1094
+
1095
+ return chan
1096
+ })
1097
+
1098
+ await channel.join(joiner, 2)
1099
+ await sendMany()
1100
+
1101
+ joined = await channelOf(joiner)
1102
+
1103
+ expect(joined.send).toHaveBeenCalled()
1104
+ })
1105
+
1106
+ it('should not consume what a sealed channel consumed', async () => {
1107
+ await channel.consume('q', consumer)
1108
+ await channel.seal()
1109
+ await channel.join(joiner, 2)
1110
+
1111
+ joined = await channelOf(joiner)
1112
+
1113
+ expect(joined.consume).not.toHaveBeenCalled()
1114
+ })
1115
+
1116
+ it('should not be its shard when no channel can be made for it', async () => {
1117
+ const exception = new Error('No channels left to allocate')
1118
+
1119
+ joiner.createChannel.mockImplementationOnce(async () => { throw exception })
1120
+
1121
+ await expect(channel.join(joiner, 2)).rejects.toThrow(exception)
1122
+
1123
+ expect(channel.has(2)).toStrictEqual(false)
1124
+
1125
+ // and may be joined again
1126
+ await channel.join(joiner, 2)
1127
+
1128
+ expect(channel.has(2)).toStrictEqual(true)
1129
+ })
1130
+
1131
+ it('should not join when it cannot consume what the rest do', async () => {
1132
+ const exception = new Error('PRECONDITION_FAILED')
1133
+
1134
+ await channel.consume('q', consumer)
1135
+
1136
+ joiner.createChannel.mockImplementationOnce(async (type, index) => {
1137
+ const chan = mock.channel(false, index)
1138
+
1139
+ chan.consume.mockImplementation(async () => { throw exception })
1140
+
1141
+ return chan
1142
+ })
1143
+
1144
+ await expect(channel.join(joiner, 2)).rejects.toThrow(exception)
1145
+
1146
+ joined = await channelOf(joiner)
1147
+
1148
+ await sendMany()
1149
+
1150
+ expect(channel.has(2)).toStrictEqual(false)
1151
+ expect(joined.close).toHaveBeenCalled()
1152
+ expect(joined.send).not.toHaveBeenCalled()
1153
+ })
1154
+
1155
+ it('should not consume what no shard could', async () => {
1156
+ const chans = await Promise.all(connections.map((conn) => channelOf(conn)))
1157
+
1158
+ for (const chan of chans) chan.consume.mockImplementationOnce(async () => { throw new Error('oops') })
1159
+
1160
+ await expect(channel.consume('q', consumer)).rejects.toThrow()
1161
+ await channel.join(joiner, 2)
1162
+
1163
+ joined = await channelOf(joiner)
1164
+
1165
+ expect(joined.consume).not.toHaveBeenCalled()
1166
+ })
1167
+
1168
+ it.each(['seal', 'close'])('should %s a shard that is joining with the rest', async (method) => {
1169
+ const consuming = new Promex()
1170
+
1171
+ await channel.consume('q', consumer)
1172
+
1173
+ joiner.createChannel.mockImplementationOnce(async (type, index) => {
1174
+ const chan = mock.channel(false, index)
1175
+
1176
+ chan.consume.mockImplementation(() => consuming)
1177
+
1178
+ return chan
1179
+ })
1180
+
1181
+ const joining = channel.join(joiner, 2)
1182
+
1183
+ await immediate()
1184
+ await channel[method]()
1185
+
1186
+ joined = await channelOf(joiner)
1187
+
1188
+ expect(joined[method]).toHaveBeenCalled()
1189
+
1190
+ consuming.resolve()
1191
+
1192
+ await joining
1193
+ })
1194
+
1195
+ it('should not be published through when closed while joining', async () => {
1196
+ const consuming = new Promex()
1197
+
1198
+ await channel.consume('q', consumer)
1199
+
1200
+ joiner.createChannel.mockImplementationOnce(async (type, index) => {
1201
+ const chan = mock.channel(false, index)
1202
+
1203
+ chan.consume.mockImplementation(() => consuming)
1204
+
1205
+ return chan
1206
+ })
1207
+
1208
+ const joining = channel.join(joiner, 2)
1209
+
1210
+ await immediate()
1211
+ await channel.close()
1212
+
1213
+ consuming.resolve()
1214
+
1215
+ await joining
1216
+
1217
+ joined = await channelOf(joiner)
1218
+
1219
+ expect(joined.close).toHaveBeenCalled()
1220
+ expect(joined.send).not.toHaveBeenCalled()
1221
+ })
1222
+
1223
+ it('should tell which shard a message arrived on', async () => {
1224
+ await channel.consume('q', consumer)
1225
+ await channel.join(joiner, 2)
1226
+
1227
+ joined = await channelOf(joiner)
1228
+
1229
+ const message = { content: randomBytes(8) }
1230
+
1231
+ await joined.consume.mock.calls[0][1](message)
1232
+ await channel.fire('reply', randomBytes(8), {}, message)
1233
+
1234
+ expect(joined.fire).toHaveBeenCalled()
1235
+ })
1236
+ })
1237
+
1238
+ describe('a shard that retires', () => {
1239
+ /** @type {jest.MockedObject<comq.Channel>[]} */
1240
+ let chans
1241
+
1242
+ const sendMany = async () => {
1243
+ for (let i = 0; i < 100; i++) await channel.send(generate(), randomBytes(8))
1244
+ }
1245
+
1246
+ beforeEach(async () => {
1247
+ channel = await create(connections, type)
1248
+ chans = await Promise.all(connections.map((conn) => conn.createChannel.mock.results.at(-1).value))
1249
+ })
1250
+
1251
+ it('should not be published through', async () => {
1252
+ channel.retire(1)
1253
+
1254
+ await sendMany()
1255
+
1256
+ expect(chans[0].send).toHaveBeenCalledTimes(100)
1257
+ expect(chans[1].send).not.toHaveBeenCalled()
1258
+ })
1259
+
1260
+ it('should still take back what answers a message that arrived on it', async () => {
1261
+ await channel.consume('q', jest.fn())
1262
+
1263
+ const message = { content: randomBytes(8) }
1264
+
1265
+ await chans[1].consume.mock.calls[0][1](message)
1266
+
1267
+ channel.retire(1)
1268
+
1269
+ for (let i = 0; i < 20; i++) await channel.fire('reply', randomBytes(8), {}, message)
1270
+
1271
+ expect(chans[1].fire).toHaveBeenCalledTimes(20)
1272
+ })
1273
+
1274
+ it('should go on being consumed from', async () => {
1275
+ channel.retire(1)
1276
+
1277
+ await channel.consume('q', jest.fn())
1278
+
1279
+ expect(chans[1].consume).toHaveBeenCalled()
1280
+ })
1281
+
1282
+ it('should pause once no other shard is left', async () => {
1283
+ const pause = jest.fn()
1284
+ const resume = jest.fn()
1285
+
1286
+ channel.diagnose('pause', pause)
1287
+ channel.diagnose('resume', resume)
1288
+
1289
+ channel.retire(0)
1290
+ channel.retire(1)
1291
+
1292
+ expect(pause).toHaveBeenCalled()
1293
+
1294
+ channel.restore(1)
1295
+
1296
+ expect(resume).toHaveBeenCalled()
1297
+ })
1298
+
1299
+ it('should be published through again once restored', async () => {
1300
+ channel.retire(1)
1301
+ channel.restore(1)
1302
+
1303
+ await sendMany()
1304
+
1305
+ expect(chans[1].send).toHaveBeenCalled()
1306
+ })
1307
+
1308
+ it('should tell since when it has had nothing to do', async () => {
1309
+ chans[1].quiet.mockImplementation(async () => 42)
1310
+
1311
+ await expect(channel.quiet(1)).resolves.toStrictEqual(42)
1312
+ })
1313
+
1314
+ it('should have something to do while whoever publishes says so', async () => {
1315
+ channel.occupy((index) => index === 1)
1316
+
1317
+ const now = Date.now()
1318
+
1319
+ expect(await channel.quiet(1)).toBeGreaterThanOrEqual(now)
1320
+ expect(await channel.quiet(0)).toStrictEqual(0)
1321
+ })
1322
+
1323
+ describe('and leaves', () => {
1324
+ it('should seal its channel, wait for what it delivered, and close it', async () => {
1325
+ const order = []
1326
+
1327
+ chans[1].seal.mockImplementation(async () => order.push('seal'))
1328
+ chans[1].settled.mockImplementation(async () => order.push('settled'))
1329
+ chans[1].close.mockImplementation(async () => order.push('close'))
1330
+
1331
+ await channel.leave(1)
1332
+
1333
+ expect(order).toStrictEqual(['seal', 'settled', 'close'])
1334
+ expect(chans[0].close).not.toHaveBeenCalled()
1335
+ })
1336
+
1337
+ it('should be the way back until what it delivered is done with', async () => {
1338
+ const settled = new Promex()
1339
+
1340
+ await channel.consume('q', jest.fn())
1341
+
1342
+ const message = { content: randomBytes(8) }
1343
+
1344
+ await chans[1].consume.mock.calls[0][1](message)
1345
+
1346
+ chans[1].settled.mockImplementation(() => settled)
1347
+ channel.retire(1)
1348
+
1349
+ const leaving = channel.leave(1)
1350
+
1351
+ await immediate()
1352
+ await channel.fire('reply', randomBytes(8), {}, message)
1353
+
1354
+ expect(chans[1].fire).toHaveBeenCalled()
1355
+
1356
+ settled.resolve()
1357
+
1358
+ await leaving
1359
+ await channel.fire('reply', randomBytes(8), {}, message)
1360
+
1361
+ expect(chans[0].fire).toHaveBeenCalled()
1362
+ })
1363
+
1364
+ it('should not be its shard any longer', async () => {
1365
+ await channel.leave(1)
1366
+
1367
+ expect(channel.has(1)).toStrictEqual(false)
1368
+
1369
+ await sendMany()
1370
+
1371
+ expect(chans[1].send).not.toHaveBeenCalled()
1372
+ })
1373
+
1374
+ it('should not be consumed from', async () => {
1375
+ await channel.leave(1)
1376
+ await channel.consume('q', jest.fn())
1377
+
1378
+ expect(chans[1].consume).not.toHaveBeenCalled()
1379
+ })
1380
+
1381
+ it('should not come back when its channel recovers', async () => {
1382
+ await channel.leave(1)
1383
+
1384
+ for (const [event, listener] of chans[1].diagnose.mock.calls) if (event === 'recover') listener()
1385
+
1386
+ await sendMany()
1387
+
1388
+ expect(chans[1].send).not.toHaveBeenCalled()
1389
+ })
1390
+
1391
+ it('should stop listening to its connection', async () => {
1392
+ await channel.leave(1)
1393
+
1394
+ expect(connections[1].forget).toHaveBeenCalledWith('close', expect.any(Function))
1395
+ expect(connections[1].forget).toHaveBeenCalledWith('open', expect.any(Function))
1396
+ })
1397
+ })
1398
+ })