queen-mq 2.1.0 → 2.3.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.
@@ -0,0 +1,264 @@
1
+ /**
2
+ * The framed pop answer (client-v2/http/popFrames.js), against hand-built
3
+ * answers and a server that plays them. What is at stake: a consumer that
4
+ * sets framedPop must be handed exactly the messages the JSON answer gives it,
5
+ * and one that does not set it must send the request every released broker
6
+ * knows.
7
+ */
8
+
9
+ import { describe, it } from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+ import { zstdCompressSync } from 'node:zlib'
12
+
13
+ import { Queen, CONFLATION_UNSUPPORTED } from '../../client-v2/index.js'
14
+ import { decodePopFrames, isPopFrames } from '../../client-v2/http/popFrames.js'
15
+ import { withWireServer, query } from './_wireServer.js'
16
+
17
+ const u16 = (n) => { const b = Buffer.alloc(2); b.writeUInt16LE(n); return b }
18
+ const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32LE(n); return b }
19
+ const u64 = (n) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n)); return b }
20
+ const text = (s) => Buffer.concat([u16(Buffer.byteLength(s)), Buffer.from(s)])
21
+
22
+ /** One stored frame: u32 body_len | flags | id | [trace] | txn | [producer] | payload. */
23
+ function storedFrame(id, txn, payload, { producer = null, trace = null } = {}) {
24
+ const idBytes = Buffer.alloc(16)
25
+ idBytes[0] = id
26
+ idBytes[6] = 0x70
27
+ idBytes[8] = 0x80
28
+ const body = Buffer.concat([
29
+ Buffer.of((trace ? 1 : 0) | (producer ? 2 : 0)),
30
+ idBytes,
31
+ trace ? Buffer.from(trace.replaceAll('-', ''), 'hex') : Buffer.alloc(0),
32
+ text(txn),
33
+ producer ? text(producer) : Buffer.alloc(0),
34
+ Buffer.from(payload)
35
+ ])
36
+ return Buffer.concat([u32(body.length), body])
37
+ }
38
+
39
+ /**
40
+ * A framed pop answer of one claim on partition 7 ("p7") of "orders", lease
41
+ * "lease-1": one record at base offset 10 holding `frames`, created at
42
+ * 2023-11-14T22:13:20Z, of which the claim covers start..=end.
43
+ */
44
+ function framedAnswer(own, start, end, compress, frames) {
45
+ let raw = Buffer.concat(frames)
46
+ if (compress) raw = zstdCompressSync(raw)
47
+ return Buffer.concat([
48
+ Buffer.from('QPF1'), Buffer.of(1), u32(Buffer.byteLength(own)), Buffer.from(own),
49
+ text('orders'), text('lease-1'), text('__QUEUE_MODE__'),
50
+ u32(1), // claims
51
+ u64(7), text('p7'), u64(start), u64(end),
52
+ u32(3), // delivery attempt
53
+ u32(1), // records
54
+ u64(10), u64(1_700_000_000_000_000n), u32(frames.length), Buffer.of(compress ? 1 : 0), u32(raw.length), raw
55
+ ])
56
+ }
57
+
58
+ const twoFrames = () => [storedFrame(1, 't-10', '{"n":10}'), storedFrame(2, 't-11', '{"n":11,"s":"x"}', { producer: 'svc' })]
59
+
60
+ // The JSON answer of the same two messages, as the broker renders it.
61
+ const TWO_MESSAGES_JSON = '{"success":true,"queue":"orders","partition":"p7","partitionId":"7","leaseId":"lease-1","consumerGroup":"__QUEUE_MODE__","messages":[' +
62
+ '{"id":"01000000-0000-7000-8000-000000000000","transactionId":"t-10","traceId":null,"data":{"n":10},"producerSub":null,"createdAt":"2023-11-14T22:13:20.000Z","partitionId":"7","partition":"p7","leaseId":"lease-1","consumerGroup":"__QUEUE_MODE__","deliveryAttempt":3,"offset":10},' +
63
+ '{"id":"02000000-0000-7000-8000-000000000000","transactionId":"t-11","traceId":null,"data":{"n":11,"s":"x"},"producerSub":"svc","createdAt":"2023-11-14T22:13:20.000Z","partitionId":"7","partition":"p7","leaseId":"lease-1","consumerGroup":"__QUEUE_MODE__","deliveryAttempt":3,"offset":11}' +
64
+ '],"partitionsClaimed":1}'
65
+
66
+ const json = (body) => ({ body })
67
+ const framed = (body) => ({ contentType: 'application/x-queen-pop', body })
68
+ const pops = (hits) => hits.filter(h => h.method === 'GET' && h.url.startsWith('/api/v1/pop'))
69
+
70
+ async function withQueen(config, answer, run) {
71
+ await withWireServer(answer, async (url, hits) => {
72
+ const queen = new Queen({ url, handleSignals: false, retryAttempts: 1, ...config })
73
+ try {
74
+ await run(queen, hits)
75
+ } finally {
76
+ await queen.close()
77
+ }
78
+ })
79
+ }
80
+
81
+ describe('decodePopFrames', () => {
82
+ for (const compress of [false, true]) {
83
+ it(`unpacks ${compress ? 'a zstd' : 'a raw'} record into the JSON answer's object`, async () => {
84
+ const got = await decodePopFrames(framedAnswer('{"frames":true}', 10, 11, compress, twoFrames()))
85
+
86
+ assert.deepEqual(got, JSON.parse(TWO_MESSAGES_JSON))
87
+ assert.deepEqual(Object.keys(got.messages[0]), Object.keys(JSON.parse(TWO_MESSAGES_JSON).messages[0]))
88
+ })
89
+
90
+ it(`keeps only the messages inside the claim (${compress ? 'zstd' : 'raw'})`, async () => {
91
+ const got = await decodePopFrames(framedAnswer('{"frames":true}', 11, 11, compress, twoFrames()))
92
+
93
+ assert.equal(got.messages.length, 1)
94
+ assert.equal(got.messages[0].offset, 11)
95
+ assert.equal(got.messages[0].transactionId, 't-11')
96
+ assert.equal(got.messages[0].producerSub, 'svc')
97
+ assert.equal(got.messages[0].id, '02000000-0000-7000-8000-000000000000')
98
+ })
99
+ }
100
+
101
+ it('reads a trace id, an empty payload as null, and the answer without its own keys', async () => {
102
+ const trace = '0190aaaa-0000-4000-8000-000000000001'
103
+ const answer = framedAnswer('', 10, 11, false, [storedFrame(1, 't-10', '', { trace }), storedFrame(2, 't-11', '[1,"due"]')])
104
+ const got = await decodePopFrames(answer)
105
+
106
+ assert.equal(got.messages[0].traceId, trace)
107
+ assert.equal(got.messages[0].data, null)
108
+ assert.deepEqual(got.messages[1].data, [1, 'due'])
109
+
110
+ // Version 0 has no JSON object in front of the names.
111
+ const v0 = Buffer.concat([Buffer.from('QPF1'), Buffer.of(0), answer.subarray(9)])
112
+ assert.deepEqual(await decodePopFrames(v0), got)
113
+ })
114
+
115
+ it('adds the answer\'s own keys, without `frames`', async () => {
116
+ const own = '{"frames":true,"conflation":true,"autopilot":{"partitions":4,"batch":50}}'
117
+ const got = await decodePopFrames(framedAnswer(own, 10, 11, false, twoFrames()))
118
+
119
+ assert.equal('frames' in got, false)
120
+ assert.equal(got.conflation, true)
121
+ assert.deepEqual(got.autopilot, { partitions: 4, batch: 50 })
122
+ })
123
+
124
+ it('rejects damage', async () => {
125
+ const answer = framedAnswer('{"frames":true}', 10, 11, false, twoFrames())
126
+
127
+ assert.equal(isPopFrames(answer), true)
128
+ assert.equal(isPopFrames(Buffer.from(TWO_MESSAGES_JSON)), false)
129
+ await assert.rejects(decodePopFrames(answer.subarray(0, answer.length - 3)), /framed pop answer/)
130
+ await assert.rejects(decodePopFrames(Buffer.from(TWO_MESSAGES_JSON)), /not a framed pop answer/)
131
+
132
+ const future = Buffer.from(answer)
133
+ future[4] = 9
134
+ await assert.rejects(decodePopFrames(future), /unknown version 9/)
135
+
136
+ // A record that declares more frames than it holds.
137
+ const short = Buffer.from(answer)
138
+ const frames = twoFrames()
139
+ short.writeUInt32LE(3, short.length - frames[0].length - frames[1].length - 9)
140
+ await assert.rejects(decodePopFrames(short), /a record is shorter than its 3 frames/)
141
+
142
+ const badCodec = Buffer.from(answer)
143
+ badCodec[badCodec.length - frames[0].length - frames[1].length - 5] = 7
144
+ await assert.rejects(decodePopFrames(badCodec), /unknown codec 7/)
145
+ })
146
+ })
147
+
148
+ describe('pop — the default', () => {
149
+ it('sends no frames parameter', async () => {
150
+ await withQueen({}, () => json(TWO_MESSAGES_JSON), async (queen, hits) => {
151
+ const messages = await queen.queue('orders').batch(2).wait(false).pop()
152
+
153
+ assert.equal(messages.length, 2)
154
+ assert.equal(query(pops(hits)[0].url).has('frames'), false)
155
+ })
156
+ })
157
+ })
158
+
159
+ describe('pop — framedPop', () => {
160
+ for (const compress of [false, true]) {
161
+ it(`gives the messages of the JSON answer (${compress ? 'zstd' : 'raw'} record)`, async () => {
162
+ let want = null
163
+ await withQueen({}, () => json(TWO_MESSAGES_JSON), async (queen) => {
164
+ want = await queen.queue('orders').batch(2).wait(false).pop()
165
+ })
166
+
167
+ const answer = framedAnswer('{"frames":true}', 10, 11, compress, twoFrames())
168
+ await withQueen({ framedPop: true }, () => framed(answer), async (queen, hits) => {
169
+ const got = await queen.queue('orders').batch(2).wait(false).pop()
170
+
171
+ assert.equal(query(pops(hits)[0].url).get('frames'), 'true')
172
+ assert.equal(got.length, 2)
173
+ assert.deepEqual(got, want)
174
+ })
175
+ })
176
+ }
177
+
178
+ it('asks for frames on the partition and the namespace routes too', async () => {
179
+ await withQueen({ framedPop: true }, () => ({ status: 204 }), async (queen, hits) => {
180
+ await queen.queue('orders').partition('p7').wait(false).pop()
181
+ await queen.queue().namespace('ns').wait(false).pop()
182
+
183
+ const [byPartition, byNamespace] = pops(hits)
184
+ assert.ok(byPartition.url.startsWith('/api/v1/pop/queue/orders/partition/p7?'))
185
+ assert.equal(query(byPartition.url).get('frames'), 'true')
186
+ assert.ok(byNamespace.url.startsWith('/api/v1/pop?'))
187
+ assert.equal(query(byNamespace.url).get('frames'), 'true')
188
+ })
189
+ })
190
+
191
+ it('knows a framed answer by its first bytes, whatever its content type', async () => {
192
+ const answer = framedAnswer('{"frames":true}', 10, 11, false, twoFrames())
193
+ for (const contentType of ['application/octet-stream', 'application/json']) {
194
+ await withQueen({ framedPop: true }, () => ({ contentType, body: answer }), async (queen) => {
195
+ const got = await queen.queue('orders').batch(2).wait(false).pop()
196
+
197
+ assert.deepEqual(got.map(m => m.transactionId), ['t-10', 't-11'], contentType)
198
+ })
199
+ }
200
+ })
201
+
202
+ it('reads a JSON answer as always: a broker that does not know the parameter', async () => {
203
+ await withQueen({ framedPop: true }, () => json(TWO_MESSAGES_JSON), async (queen) => {
204
+ const got = await queen.queue('orders').batch(2).wait(false).pop()
205
+
206
+ assert.equal(got.length, 2)
207
+ assert.equal(got[1].transactionId, 't-11')
208
+ assert.equal(got[1].data.s, 'x')
209
+ })
210
+ })
211
+
212
+ it('reads an empty pop', async () => {
213
+ await withQueen({ framedPop: true }, () => ({ status: 204 }), async (queen) => {
214
+ assert.deepEqual(await queen.queue('orders').wait(false).pop(), [])
215
+ })
216
+ })
217
+
218
+ it('carries the autopilot\'s choice in the answer\'s own keys', async () => {
219
+ const own = '{"frames":true,"autopilot":{"partitions":4,"batch":50}}'
220
+ await withQueen({ framedPop: true }, () => framed(framedAnswer(own, 10, 11, false, twoFrames())), async (queen) => {
221
+ const { messages, autopilot } = await queen.queue('orders').wait(false).popResult()
222
+
223
+ assert.equal(messages.length, 2)
224
+ assert.deepEqual(autopilot, { partitions: 4, batch: 50, waitMillis: 0 })
225
+ })
226
+ })
227
+
228
+ it('carries the conflation echo, and still raises when the broker sent none', async () => {
229
+ const echoed = framedAnswer('{"frames":true,"conflation":true}', 10, 11, false, twoFrames())
230
+ await withQueen({ framedPop: true }, () => framed(echoed), async (queen) => {
231
+ const got = await queen.queue('orders').group('g').conflation(true).wait(false).pop()
232
+ assert.equal(got.length, 2)
233
+ })
234
+
235
+ const silent = framedAnswer('{"frames":true}', 10, 11, false, twoFrames())
236
+ await withQueen({ framedPop: true }, () => framed(silent), async (queen) => {
237
+ await assert.rejects(
238
+ queen.queue('orders').group('g2').conflation(true).wait(false).pop(),
239
+ { code: CONFLATION_UNSUPPORTED }
240
+ )
241
+ })
242
+ })
243
+ })
244
+
245
+ describe('consume — framedPop', () => {
246
+ // The consumer loop on framed answers: the handler gets every message of
247
+ // the claim, in order, and the loop stops at its limit.
248
+ it('hands the handler the messages of the claim, in order', async () => {
249
+ const answer = framedAnswer('{"frames":true}', 10, 11, true, twoFrames())
250
+ await withQueen({ framedPop: true }, () => framed(answer), async (queen, hits) => {
251
+ const seen = []
252
+ await queen.queue('orders').batch(2).wait(false).autoAck(false).each().limit(2)
253
+ .consume(async (message) => {
254
+ seen.push(`${message.transactionId}@${message.partitionId}/${message.leaseId}`)
255
+ })
256
+
257
+ assert.deepEqual(seen, ['t-10@7/lease-1', 't-11@7/lease-1'])
258
+ assert.ok(pops(hits).length >= 1)
259
+ for (const hit of pops(hits)) {
260
+ assert.equal(query(hit.url).get('frames'), 'true')
261
+ }
262
+ })
263
+ })
264
+ })
@@ -0,0 +1,235 @@
1
+ /**
2
+ * The binary push body (client-v2/http/pushBinary.js), asserted against a
3
+ * server that records the bytes. What is at stake is the default: a client
4
+ * that does not set binaryPush must send the JSON request every released
5
+ * broker knows.
6
+ */
7
+
8
+ import { describe, it } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+ import { zstdDecompressSync } from 'node:zlib'
11
+
12
+ import { Queen } from '../../client-v2/index.js'
13
+ import { binaryPushBody, encodePushBinary, encodePushFrames } from '../../client-v2/http/pushBinary.js'
14
+ import { withWireServer } from './_wireServer.js'
15
+
16
+ const TXN = '0190aaaa-0000-7000-8000-000000000001'
17
+ // The client sends a trace id only when it is a version-4 UUID.
18
+ const TRACE = '0190aaaa-0000-4000-8000-000000000001'
19
+
20
+ const queued = (n = 1) => ({ body: JSON.stringify(Array.from({ length: n }, () => ({ status: 'queued' }))) })
21
+ const pushes = (hits) => hits.filter(h => h.method === 'POST' && h.url === '/api/v1/push')
22
+
23
+ async function withQueen(config, answer, run) {
24
+ await withWireServer(answer, async (url, hits) => {
25
+ const queen = new Queen({ url, handleSignals: false, retryAttempts: 1, ...config })
26
+ try {
27
+ await run(queen, hits)
28
+ } finally {
29
+ await queen.close()
30
+ }
31
+ })
32
+ }
33
+
34
+ /** Reads the fields of a binary push body in order. */
35
+ function reader(bytes) {
36
+ let at = 0
37
+ const r = {
38
+ u16() { at += 2; return bytes.readUInt16LE(at - 2) },
39
+ u32() { at += 4; return bytes.readUInt32LE(at - 4) },
40
+ take(n) { at += n; return bytes.subarray(at - n, at) },
41
+ text16() { return r.take(r.u16()).toString() },
42
+ text32() { return r.take(r.u32()).toString() },
43
+ left() { return bytes.length - at }
44
+ }
45
+ return r
46
+ }
47
+
48
+ /** The groups of a plain binary push body (after magic and flags). */
49
+ function plainGroups(bytes, withTxn) {
50
+ const r = reader(bytes)
51
+ const groups = []
52
+ for (let g = r.u32(); g > 0; g--) {
53
+ const group = { queue: r.text16(), partition: r.text16(), items: [] }
54
+ for (let n = r.u32(); n > 0; n--) {
55
+ const txn = withTxn ? r.text16() : null
56
+ group.items.push({ txn, payload: r.text32() })
57
+ }
58
+ groups.push(group)
59
+ }
60
+ assert.equal(r.left(), 0, 'bytes left after the last payload')
61
+ return groups
62
+ }
63
+
64
+ /** The groups of a framed binary push body, each with its frames unpacked. */
65
+ function framedGroups(bytes) {
66
+ const r = reader(bytes)
67
+ const groups = []
68
+ for (let g = r.u32(); g > 0; g--) {
69
+ const group = { queue: r.text16(), partition: r.text16(), count: r.u32(), frames: [] }
70
+ const f = reader(zstdDecompressSync(r.take(r.u32())))
71
+ while (f.left() > 0) {
72
+ const body = reader(f.take(f.u32()))
73
+ const flags = body.take(1)[0]
74
+ const id = Buffer.from(body.take(16))
75
+ group.frames.push({ flags, id, txn: body.text16(), payload: body.take(body.left()).toString() })
76
+ }
77
+ groups.push(group)
78
+ }
79
+ assert.equal(r.left(), 0, 'bytes left after the last group')
80
+ return groups
81
+ }
82
+
83
+ describe('push — the default', () => {
84
+ it('is the JSON request', async () => {
85
+ await withQueen({}, () => queued(), async (queen, hits) => {
86
+ const res = await queen.queue('orders').partition('p1').push({ data: { a: 1 } })
87
+
88
+ assert.equal(res[0].status, 'queued')
89
+ const hit = pushes(hits)[0]
90
+ assert.equal(hit.headers['content-type'], 'application/json')
91
+ const body = JSON.parse(hit.raw.toString())
92
+ assert.deepEqual(Object.keys(body), ['items'])
93
+ assert.deepEqual(body.items[0].payload, { a: 1 })
94
+ })
95
+ })
96
+ })
97
+
98
+ describe('push — binaryPush', () => {
99
+ it('sends the binary body, each item with its transaction id', async () => {
100
+ await withQueen({ binaryPush: true }, () => queued(), async (queen, hits) => {
101
+ const res = await queen.queue('orders').partition('p1').push({ data: { a: 1 }, transactionId: TXN })
102
+
103
+ assert.equal(res[0].status, 'queued')
104
+ const hit = pushes(hits)[0]
105
+ assert.equal(hit.headers['content-type'], 'application/x-queen-push')
106
+ assert.equal(hit.raw.subarray(0, 5).toString('latin1'), 'QPB1\x04')
107
+ assert.deepEqual(plainGroups(hit.raw.subarray(5), true), [
108
+ { queue: 'orders', partition: 'p1', items: [{ txn: TXN, payload: '{"a":1}' }] }
109
+ ])
110
+ })
111
+ })
112
+
113
+ it('keeps the order of the items: consecutive items of one partition share a group', async () => {
114
+ const item = (partition, payload) => ({ queue: 'q', partition, payload, transactionId: `t${payload}` })
115
+ const bytes = Buffer.from(await binaryPushBody([item('a', 1), item('a', 2), item('b', 3), item('a', 4)], false))
116
+
117
+ assert.equal(bytes.subarray(0, 5).toString('latin1'), 'QPB1\x04')
118
+ assert.deepEqual(
119
+ plainGroups(bytes.subarray(5), true).map(g => [g.partition, g.items.map(i => i.txn + '=' + i.payload)]),
120
+ [['a', ['t1=1', 't2=2']], ['b', ['t3=3']], ['a', ['t4=4']]]
121
+ )
122
+ })
123
+
124
+ it('carries payloads that are not objects, and non-ASCII text', async () => {
125
+ await withQueen({ binaryPush: true }, () => queued(5), async (queen, hits) => {
126
+ await queen.queue('ordini').partition('città').push([
127
+ { data: 'testo "con" virgolette\n' }, { data: [1, 2] }, { data: 7 }, { data: null }, { data: { è: '€' } }
128
+ ])
129
+
130
+ const [group] = plainGroups(pushes(hits)[0].raw.subarray(5), true)
131
+ assert.equal(group.partition, 'città')
132
+ assert.deepEqual(group.items.map(i => JSON.parse(i.payload)), ['testo "con" virgolette\n', [1, 2], 7, null, { è: '€' }])
133
+ })
134
+ })
135
+
136
+ it('leaves an item with a trace id to the JSON request', async () => {
137
+ await withQueen({ binaryPush: true }, () => queued(), async (queen, hits) => {
138
+ await queen.queue('orders').push({ data: { a: 1 }, traceId: TRACE })
139
+
140
+ const hit = pushes(hits)[0]
141
+ assert.equal(hit.headers['content-type'], 'application/json')
142
+ assert.equal(JSON.parse(hit.raw.toString()).items[0].traceId, TRACE)
143
+ })
144
+ })
145
+
146
+ it('sends a buffered push as the binary body too', async () => {
147
+ const { flags, body } = await bodyOf(async (queen) => {
148
+ await queen.queue('orders').partition('p1').buffer({ messageCount: 10, timeMillis: 60000 }).push({ data: { a: 1 } })
149
+ await queen.flushAllBuffers()
150
+ })
151
+ assert.equal(flags, 4)
152
+ assert.equal(body[0].items[0].payload, '{"a":1}')
153
+ })
154
+
155
+ /** Runs `use` on a binaryPush client and returns the one push it sent, decoded. */
156
+ async function bodyOf(use) {
157
+ let sent = null
158
+ await withQueen({ binaryPush: true }, () => queued(2), async (queen, hits) => {
159
+ await use(queen)
160
+ assert.equal(pushes(hits).length, 1)
161
+ sent = pushes(hits)[0]
162
+ })
163
+ assert.equal(sent.raw.subarray(0, 4).toString('latin1'), 'QPB1')
164
+ return { flags: sent.raw[4], body: plainGroups(sent.raw.subarray(5), true) }
165
+ }
166
+ })
167
+
168
+ describe('push — binaryPush with pushCompression', () => {
169
+ it('sends each partition as one zstd frame of stored frames, ids minted here', async () => {
170
+ await withQueen({ binaryPush: true, pushCompression: true }, () => queued(3), async (queen, hits) => {
171
+ await queen.queue('orders').partition('p1').push([{ data: { a: 1 }, transactionId: TXN }, { data: { a: 2 } }, { data: 'x' }])
172
+
173
+ const hit = pushes(hits)[0]
174
+ assert.equal(hit.headers['content-type'], 'application/x-queen-push')
175
+ assert.equal(hit.raw.subarray(0, 5).toString('latin1'), 'QPB1\x08')
176
+ const [group, ...rest] = framedGroups(hit.raw.subarray(5))
177
+ assert.equal(rest.length, 0)
178
+ assert.equal(group.queue, 'orders')
179
+ assert.equal(group.partition, 'p1')
180
+ assert.equal(group.count, 3)
181
+ assert.deepEqual(group.frames.map(f => f.payload), ['{"a":1}', '{"a":2}', '"x"'])
182
+ assert.equal(group.frames[0].txn, TXN)
183
+ for (const frame of group.frames) {
184
+ assert.equal(frame.flags, 0)
185
+ assert.equal(frame.id[6] >> 4, 7, 'the message id is a version-7 UUID')
186
+ assert.equal(frame.id[8] & 0xc0, 0x80)
187
+ assert.equal(frame.txn.length, 36)
188
+ }
189
+ assert.equal(new Set(group.frames.map(f => f.id.toString('hex'))).size, 3)
190
+ })
191
+ })
192
+
193
+ it('gives a message without a transaction id the text of its id', async () => {
194
+ const bytes = Buffer.from(await encodePushFrames([{ queue: 'q', partition: 'p', payloads: ['1'] }]))
195
+ const [{ frames: [frame] }] = framedGroups(bytes.subarray(5))
196
+ const hex = frame.id.toString('hex')
197
+ assert.equal(frame.txn, `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`)
198
+ })
199
+
200
+ it('writes the content size in each zstd frame: the broker asks for it', async () => {
201
+ const bytes = Buffer.from(await encodePushFrames([{ queue: 'q', partition: 'p', payloads: ['{"a":1}'] }]))
202
+ const r = reader(bytes.subarray(5))
203
+ r.u32(); r.text16(); r.text16(); r.u32()
204
+ const frame = r.take(r.u32())
205
+ assert.equal(frame.readUInt32LE(0), 0xfd2fb528, 'a zstd frame')
206
+ // Frame header descriptor: the single-segment bit, or a content-size
207
+ // field of 2 bytes or more, says the frame carries its content size.
208
+ assert.ok((frame[4] & 0x20) !== 0 || (frame[4] >> 6) !== 0)
209
+ })
210
+ })
211
+
212
+ describe('encodePushBinary — flags', () => {
213
+ const batches = [{ queue: 'q', partition: 'p', payloads: ['1'] }]
214
+
215
+ it('asks for the counts answer with flag 2', async () => {
216
+ const bytes = Buffer.from(await encodePushBinary(batches, { counts: true }))
217
+ assert.equal(bytes.subarray(0, 5).toString('latin1'), 'QPB1\x02')
218
+ assert.equal(Buffer.from(await encodePushFrames(batches, { counts: true }))[4], 8 | 2)
219
+ })
220
+
221
+ it('compresses the whole body with flag 1, its raw length in front', async () => {
222
+ const plain = Buffer.from(await encodePushBinary(batches))
223
+ const packed = Buffer.from(await encodePushBinary(batches, { compress: true }))
224
+ assert.equal(packed.subarray(0, 5).toString('latin1'), 'QPB1\x01')
225
+ assert.equal(packed.readUInt32LE(5), plain.length - 5)
226
+ assert.deepEqual(zstdDecompressSync(packed.subarray(9)), plain.subarray(5))
227
+ })
228
+
229
+ it('refuses transaction ids that do not match the payloads', async () => {
230
+ await assert.rejects(
231
+ encodePushBinary([{ queue: 'q', partition: 'p', payloads: ['1', '2'], transactionIds: ['t'] }]),
232
+ { message: '1 transaction ids for 2 payloads' }
233
+ )
234
+ })
235
+ })