queen-mq 2.0.0 → 2.0.3

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,236 @@
1
+ /**
2
+ * pop() and consume() share one builder but not their defaults.
3
+ *
4
+ * autoAck commitOnDelivery wait
5
+ * pop() never sent off; on sends autoAck true (POP_DEFAULTS)
6
+ * consume true, client-side refused true (CONSUME_DEFAULTS)
7
+ *
8
+ * autoAck() is consume()'s ack after the handler and never reaches the wire.
9
+ * The broker's at-most-once commit at delivery is the pop's own option,
10
+ * commitOnDelivery(), which sends autoAck=true; consume() always leases, so it
11
+ * refuses that option before any request.
12
+ * POP_DEFAULTS.wait said false while every pop long-polled; the long poll is
13
+ * what callers rely on, so it stays, and POP_DEFAULTS and the guide say so.
14
+ *
15
+ * The broker contract (server/src/handlers/data.rs, PopParams): autoAck=true
16
+ * commits the messages at delivery, with an empty leaseId; an absent autoAck
17
+ * is false. An absent wait is false too, but this SDK always sends wait.
18
+ *
19
+ * Same style as conflation-unit/conflationWire.test.js: a real node:http
20
+ * server playing a canned plan, real fetch, and every assertion is about the
21
+ * bytes that crossed the socket.
22
+ */
23
+
24
+ import { describe, it } from 'node:test'
25
+ import assert from 'node:assert/strict'
26
+
27
+ import { Queen } from '../../client-v2/index.js'
28
+ import { withPlanServer, ok } from '../kv-unit/_planServer.js'
29
+
30
+ const QUEUE = 'q'
31
+ const GROUP = 'g'
32
+
33
+ const frame = (n = 1) => ({
34
+ transactionId: `txn-${n}`,
35
+ partitionId: `part-${n}`,
36
+ partition: 'Default',
37
+ payload: { n },
38
+ leaseId: 'lease-1',
39
+ consumerGroup: GROUP
40
+ })
41
+
42
+ const popBody = (frames = [frame()]) => ok({ messages: frames, partitionsClaimed: frames.length })
43
+
44
+ // Acks answer with one result per acknowledgment; the plan only needs enough
45
+ // of them for the consume() cases below.
46
+ const ackBody = ok([{ index: 0, transactionId: 'txn-1', success: true, error: null }])
47
+
48
+ function query(url) {
49
+ const i = url.indexOf('?')
50
+ return new URLSearchParams(i < 0 ? '' : url.slice(i + 1))
51
+ }
52
+
53
+ const pops = (hits) => hits.filter(h => h.method === 'GET' && h.url.startsWith('/api/v1/pop'))
54
+ const acks = (hits) => hits.filter(h => h.method === 'POST' && h.url.startsWith('/api/v1/ack'))
55
+ const statusesOf = (hit) => (hit.body.acknowledgments || [hit.body]).map(a => a.status)
56
+
57
+ async function withQueen(plan, defaultResponse, run) {
58
+ await withPlanServer(plan, defaultResponse, async (url, hits) => {
59
+ const queen = new Queen({ url, handleSignals: false })
60
+ try {
61
+ await run(queen, hits)
62
+ } finally {
63
+ await queen.close()
64
+ }
65
+ })
66
+ }
67
+
68
+ describe('pop() — autoAck', () => {
69
+ // autoAck() is consume()'s ack after the handler and never reaches the
70
+ // wire: a pop comes back leased whatever autoAck() said. The broker's
71
+ // at-most-once autoAck is commitOnDelivery(), below.
72
+ for (const [label, build] of [
73
+ ['autoAck() never called', (q) => q.group(GROUP)],
74
+ ['autoAck(true)', (q) => q.group(GROUP).autoAck(true)],
75
+ ['autoAck(false)', (q) => q.group(GROUP).autoAck(false)],
76
+ ]) {
77
+ it(`sends no autoAck after ${label}`, async () => {
78
+ await withQueen([popBody(), popBody()], popBody(), async (queen, hits) => {
79
+ await build(queen.queue(QUEUE)).pop()
80
+ await build(queen.queue(QUEUE)).popResult()
81
+
82
+ for (const hit of pops(hits)) {
83
+ assert.equal(query(hit.url).has('autoAck'), false)
84
+ }
85
+ })
86
+ })
87
+ }
88
+ })
89
+
90
+ describe('pop() — commitOnDelivery', () => {
91
+ // The broker's commit at delivery, on the wire name every 2.x broker reads:
92
+ // the pop carries autoAck=true and nothing else changes.
93
+ it('pop() and popResult() send autoAck=true and nothing else', async () => {
94
+ await withQueen([popBody(), popBody(), popBody()], popBody(), async (queen, hits) => {
95
+ await queen.queue(QUEUE).group(GROUP).pop()
96
+ await queen.queue(QUEUE).group(GROUP).commitOnDelivery().pop()
97
+ await queen.queue(QUEUE).group(GROUP).commitOnDelivery(true).popResult()
98
+
99
+ const [plain, ...committed] = pops(hits)
100
+ assert.equal(committed.length, 2)
101
+ for (const hit of committed) {
102
+ const q = query(hit.url)
103
+ assert.equal(q.get('autoAck'), 'true')
104
+ q.delete('autoAck')
105
+ assert.deepEqual(Object.fromEntries(q), Object.fromEntries(query(plain.url)))
106
+ assert.equal(hit.url.split('?')[0], plain.url.split('?')[0])
107
+ }
108
+ })
109
+ })
110
+
111
+ it('sends no autoAck after commitOnDelivery(false)', async () => {
112
+ await withQueen([popBody()], popBody(), async (queen, hits) => {
113
+ await queen.queue(QUEUE).group(GROUP).commitOnDelivery(true).commitOnDelivery(false).pop()
114
+
115
+ assert.equal(query(pops(hits)[0].url).has('autoAck'), false)
116
+ })
117
+ })
118
+
119
+ it('does not depend on autoAck(), which stays consume()\'s', async () => {
120
+ await withQueen([popBody()], popBody(), async (queen, hits) => {
121
+ await queen.queue(QUEUE).group(GROUP).autoAck(false).commitOnDelivery().pop()
122
+
123
+ assert.equal(query(pops(hits)[0].url).get('autoAck'), 'true')
124
+ })
125
+ })
126
+
127
+ it('with conflation(), sends both and raises the broker\'s 400 instead of returning []', async () => {
128
+ // The broker refuses conflation together with a commit at delivery
129
+ // (server/src/handlers/data.rs, conflation_refusal).
130
+ const refusal = {
131
+ status: 400,
132
+ body: { success: false, error: 'conflation cannot be combined with autoAck', messages: [] }
133
+ }
134
+ await withQueen([refusal], refusal, async (queen, hits) => {
135
+ await assert.rejects(
136
+ () => queen.queue(QUEUE).group(GROUP).conflation().commitOnDelivery().pop(),
137
+ (err) => err.status === 400
138
+ )
139
+
140
+ const q = query(pops(hits)[0].url)
141
+ assert.equal(q.get('autoAck'), 'true')
142
+ assert.equal(q.get('conflation'), 'true')
143
+ })
144
+ })
145
+ })
146
+
147
+ describe('consume() refuses commitOnDelivery()', () => {
148
+ it('throws before any request', async () => {
149
+ await withQueen([], popBody(), async (queen, hits) => {
150
+ assert.throws(
151
+ () => queen.queue(QUEUE).group(GROUP).commitOnDelivery().consume(async () => {}),
152
+ {
153
+ message: 'commitOnDelivery() is a pop() option; consume() always leases its messages'
154
+ }
155
+ )
156
+ assert.equal(hits.length, 0)
157
+ })
158
+ })
159
+
160
+ it('consumes as before after commitOnDelivery(false)', async () => {
161
+ await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
162
+ await queen.queue(QUEUE).group(GROUP).commitOnDelivery(false).limit(1).consume(async () => {})
163
+
164
+ assert.equal(query(pops(hits)[0].url).has('autoAck'), false)
165
+ assert.equal(acks(hits).length, 1)
166
+ })
167
+ })
168
+ })
169
+
170
+ describe('pop() — wait', () => {
171
+ it('long-polls when wait() was never called', async () => {
172
+ await withQueen([popBody()], popBody(), async (queen, hits) => {
173
+ await queen.queue(QUEUE).group(GROUP).pop()
174
+
175
+ assert.equal(query(pops(hits)[0].url).get('wait'), 'true')
176
+ })
177
+ })
178
+
179
+ it('long-polls for timeoutMillis() when only that was called', async () => {
180
+ await withQueen([popBody()], popBody(), async (queen, hits) => {
181
+ await queen.queue(QUEUE).timeoutMillis(2000).pop()
182
+
183
+ const q = query(pops(hits)[0].url)
184
+ assert.equal(q.get('wait'), 'true')
185
+ assert.equal(q.get('timeout'), '2000')
186
+ })
187
+ })
188
+
189
+ it('sends wait=false after wait(false)', async () => {
190
+ await withQueen([popBody()], popBody(), async (queen, hits) => {
191
+ await queen.queue(QUEUE).wait(false).pop()
192
+
193
+ assert.equal(query(pops(hits)[0].url).get('wait'), 'false')
194
+ })
195
+ })
196
+ })
197
+
198
+ describe('consume() keeps its own defaults', () => {
199
+ it('long-polls and never sends autoAck, also after autoAck(true): it acks after the handler', async () => {
200
+ await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
201
+ await queen.queue(QUEUE).group(GROUP).autoAck(true).limit(1)
202
+ .consume(async () => {})
203
+
204
+ const q = query(pops(hits)[0].url)
205
+ assert.equal(q.get('wait'), 'true')
206
+ assert.equal(q.has('autoAck'), false)
207
+ assert.equal(acks(hits).length, 1, 'the consumer acked after the handler returned')
208
+ assert.deepEqual(statusesOf(acks(hits)[0]), ['completed'])
209
+ })
210
+ })
211
+
212
+ it('acks after the handler when autoAck() was never called', async () => {
213
+ await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
214
+ await queen.queue(QUEUE).group(GROUP).limit(1).consume(async () => {})
215
+
216
+ assert.equal(query(pops(hits)[0].url).get('wait'), 'true')
217
+ assert.equal(acks(hits).length, 1)
218
+ })
219
+ })
220
+
221
+ it('sends no ack after autoAck(false)', async () => {
222
+ await withQueen([popBody()], popBody(), async (queen, hits) => {
223
+ await queen.queue(QUEUE).group(GROUP).autoAck(false).limit(1).consume(async () => {})
224
+
225
+ assert.equal(acks(hits).length, 0)
226
+ })
227
+ })
228
+
229
+ it('sends wait=false after wait(false)', async () => {
230
+ await withQueen([popBody(), ackBody], ackBody, async (queen, hits) => {
231
+ await queen.queue(QUEUE).group(GROUP).wait(false).limit(1).consume(async () => {})
232
+
233
+ assert.equal(query(pops(hits)[0].url).get('wait'), 'false')
234
+ })
235
+ })
236
+ })
package/test-v2/pop.js CHANGED
@@ -361,3 +361,71 @@ export async function renewReportsAReleasedLease(client) {
361
361
  released.success === false && released.newExpiresAt === null && typeof released.error === 'string'
362
362
  return { success, message: `live lease: ${JSON.stringify(live)}; after the ack: ${JSON.stringify(released)}` }
363
363
  }
364
+
365
+
366
+ // autoAck() is consume()'s ack after the handler and never reaches the wire: a
367
+ // pop after autoAck(true) is leased like any pop, and with a 1 s lease the
368
+ // message comes back. The broker's at-most-once autoAck is commitOnDelivery().
369
+ export async function popAutoAckStaysLeased(client) {
370
+ const queueName = 'test-queue-v2-pop-auto-ack'
371
+ const queue = await client.queue(queueName).config({ leaseTime: 1 }).create()
372
+ if (!queue.configured) {
373
+ return { success: false, message: 'Queue not created' }
374
+ }
375
+ await client.queue(queueName).push([{ data: { n: 1 } }])
376
+
377
+ const [message] = await client.queue(queueName).batch(1).wait(true).timeoutMillis(5000).autoAck(true).pop()
378
+ if (!message) {
379
+ return { success: false, message: 'Nothing popped' }
380
+ }
381
+ // Past the 1 s lease: a leased message is delivered again now.
382
+ await new Promise(resolve => setTimeout(resolve, 2500))
383
+ const again = await client.queue(queueName).batch(1).wait(true).timeoutMillis(3000).pop()
384
+
385
+ const success = typeof message.leaseId === 'string' && message.leaseId !== '' && again.length === 1
386
+ return { success, message: `leaseId ${JSON.stringify(message.leaseId)}, delivered again after the lease: ${again.length}` }
387
+ }
388
+
389
+ // commitOnDelivery() sends autoAck=true: the broker moves the group's cursor
390
+ // past the message as it hands it out, so the pop has no lease and the message
391
+ // does not come back after the 1 s lease of the queue.
392
+ export async function popCommitOnDeliveryNotRedelivered(client) {
393
+ const queueName = 'test-queue-v2-pop-commit-on-delivery'
394
+ const queue = await client.queue(queueName).config({ leaseTime: 1 }).create()
395
+ if (!queue.configured) {
396
+ return { success: false, message: 'Queue not created' }
397
+ }
398
+ await client.queue(queueName).push([{ data: { n: 1 } }])
399
+
400
+ const [message] = await client.queue(queueName).batch(1).wait(true).timeoutMillis(5000).commitOnDelivery().pop()
401
+ if (!message) {
402
+ return { success: false, message: 'Nothing popped' }
403
+ }
404
+ // Past the 1 s lease: a leased message would be delivered again now.
405
+ await new Promise(resolve => setTimeout(resolve, 2500))
406
+ const again = await client.queue(queueName).batch(1).wait(true).timeoutMillis(1500).pop()
407
+
408
+ const success = !message.leaseId && again.length === 0
409
+ return { success, message: `leaseId ${JSON.stringify(message.leaseId)}, delivered again after the lease: ${again.length}` }
410
+ }
411
+
412
+ // pop() long-polls by default (POP_DEFAULTS.wait), and wait(false) returns at
413
+ // once.
414
+ export async function popLongPollsByDefault(client) {
415
+ const queueName = 'test-queue-v2-pop-wait-default'
416
+ const queue = await client.queue(queueName).create()
417
+ if (!queue.configured) {
418
+ return { success: false, message: 'Queue not created' }
419
+ }
420
+ let started = Date.now()
421
+ const waited = await client.queue(queueName).batch(1).timeoutMillis(1500).pop()
422
+ const waitedMillis = Date.now() - started
423
+ started = Date.now()
424
+ const atOnce = await client.queue(queueName).batch(1).wait(false).pop()
425
+ const atOnceMillis = Date.now() - started
426
+
427
+ return {
428
+ success: waited.length === 0 && waitedMillis >= 1000 && atOnce.length === 0 && atOnceMillis < 1000,
429
+ message: `empty pop: ${waitedMillis} ms by default (long poll of 1500 ms), ${atOnceMillis} ms with wait(false)`,
430
+ }
431
+ }