queen-mq 0.2.22 → 0.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.
Files changed (37) hide show
  1. package/README.md +162 -136
  2. package/client-js/benchmark/consumer.js +1 -1
  3. package/client-js/client/client.js +24 -3
  4. package/client-js/client-v2/LOGGING.md +240 -0
  5. package/client-js/client-v2/Queen.js +389 -0
  6. package/client-js/client-v2/README.md +1883 -0
  7. package/client-js/client-v2/buffer/BufferManager.js +215 -0
  8. package/client-js/client-v2/buffer/MessageBuffer.js +132 -0
  9. package/client-js/client-v2/builders/QueueBuilder.js +724 -0
  10. package/client-js/client-v2/builders/TransactionBuilder.js +110 -0
  11. package/client-js/client-v2/consumer/ConsumerManager.js +390 -0
  12. package/client-js/client-v2/http/HttpClient.js +215 -0
  13. package/client-js/client-v2/http/LoadBalancer.js +50 -0
  14. package/client-js/client-v2/index.js +7 -0
  15. package/client-js/client-v2/utils/defaults.js +54 -0
  16. package/client-js/client-v2/utils/logger.js +54 -0
  17. package/client-js/client-v2/utils/validation.js +31 -0
  18. package/client-js/test-v2/AI_TEST_SUMMARY.md +226 -0
  19. package/client-js/test-v2/GETTING_STARTED.md +154 -0
  20. package/client-js/test-v2/ai_buffering.js +194 -0
  21. package/client-js/test-v2/ai_error_handling.js +223 -0
  22. package/client-js/test-v2/ai_lease_renewal.js +206 -0
  23. package/client-js/test-v2/ai_mixed_scenarios.js +278 -0
  24. package/client-js/test-v2/ai_priority.js +169 -0
  25. package/client-js/test-v2/ai_resources.js +217 -0
  26. package/client-js/test-v2/ai_ttl_retention.js +170 -0
  27. package/client-js/test-v2/complete.js +59 -0
  28. package/client-js/test-v2/consume.js +655 -0
  29. package/client-js/test-v2/dlq.js +82 -0
  30. package/client-js/test-v2/load.js +177 -0
  31. package/client-js/test-v2/pop.js +114 -0
  32. package/client-js/test-v2/push.js +333 -0
  33. package/client-js/test-v2/queue.js +39 -0
  34. package/client-js/test-v2/run.js +187 -0
  35. package/client-js/test-v2/subscription.js +354 -0
  36. package/client-js/test-v2/transaction.js +443 -0
  37. package/package.json +1 -1
@@ -0,0 +1,724 @@
1
+ /**
2
+ * Queue builder for fluent API
3
+ */
4
+
5
+ import { generateUUID } from '../../utils/uuid.js'
6
+ import { isValidUUID } from '../utils/validation.js'
7
+ import { QUEUE_DEFAULTS, CONSUME_DEFAULTS, POP_DEFAULTS } from '../utils/defaults.js'
8
+ import * as logger from '../utils/logger.js'
9
+
10
+ export class QueueBuilder {
11
+ #queen
12
+ #httpClient
13
+ #bufferManager
14
+ #queueName = null
15
+ #partition = 'Default'
16
+ #namespace = null
17
+ #task = null
18
+ #group = null
19
+ #config = {}
20
+
21
+ // Consume options
22
+ #concurrency = CONSUME_DEFAULTS.concurrency
23
+ #batch = CONSUME_DEFAULTS.batch
24
+ #limit = CONSUME_DEFAULTS.limit
25
+ #idleMillis = CONSUME_DEFAULTS.idleMillis
26
+ #autoAck = CONSUME_DEFAULTS.autoAck
27
+ #wait = CONSUME_DEFAULTS.wait
28
+ #timeoutMillis = CONSUME_DEFAULTS.timeoutMillis
29
+ #renewLease = CONSUME_DEFAULTS.renewLease
30
+ #renewLeaseIntervalMillis = CONSUME_DEFAULTS.renewLeaseIntervalMillis
31
+ #subscriptionMode = CONSUME_DEFAULTS.subscriptionMode
32
+ #subscriptionFrom = CONSUME_DEFAULTS.subscriptionFrom
33
+ #each = false
34
+
35
+ // Buffer options
36
+ #bufferOptions = null
37
+
38
+ constructor(queen, httpClient, bufferManager, queueName = null) {
39
+ this.#queen = queen
40
+ this.#httpClient = httpClient
41
+ this.#bufferManager = bufferManager
42
+ this.#queueName = queueName
43
+ }
44
+
45
+ // ===========================
46
+ // Queue Configuration Methods
47
+ // ===========================
48
+
49
+ namespace(name) {
50
+ this.#namespace = name
51
+ return this
52
+ }
53
+
54
+ task(name) {
55
+ this.#task = name
56
+ return this
57
+ }
58
+
59
+ config(options) {
60
+ this.#config = { ...QUEUE_DEFAULTS, ...options }
61
+ return this
62
+ }
63
+
64
+ create() {
65
+ // Always merge with QUEUE_DEFAULTS to ensure all options are sent
66
+ const fullConfig = Object.keys(this.#config).length > 0
67
+ ? this.#config
68
+ : QUEUE_DEFAULTS
69
+
70
+ const payload = {
71
+ queue: this.#queueName,
72
+ namespace: this.#namespace,
73
+ task: this.#task,
74
+ options: fullConfig
75
+ }
76
+
77
+ logger.log('QueueBuilder.create', { queue: this.#queueName, namespace: this.#namespace, task: this.#task })
78
+ return new OperationBuilder(this.#httpClient, 'POST', '/api/v1/configure', payload)
79
+ }
80
+
81
+ delete() {
82
+ if (!this.#queueName) {
83
+ throw new Error('Queue name is required for delete operation')
84
+ }
85
+
86
+ logger.log('QueueBuilder.delete', { queue: this.#queueName })
87
+ return new OperationBuilder(
88
+ this.#httpClient,
89
+ 'DELETE',
90
+ `/api/v1/resources/queues/${encodeURIComponent(this.#queueName)}`,
91
+ null
92
+ )
93
+ }
94
+
95
+ // ===========================
96
+ // Push Methods
97
+ // ===========================
98
+
99
+ partition(name) {
100
+ this.#partition = name
101
+ return this
102
+ }
103
+
104
+ buffer(options) {
105
+ this.#bufferOptions = options
106
+ return this
107
+ }
108
+
109
+ push(payload) {
110
+ if (!this.#queueName) {
111
+ throw new Error('Queue name is required for push operation')
112
+ }
113
+
114
+ logger.log('QueueBuilder.push', { queue: this.#queueName, partition: this.#partition, count: Array.isArray(payload) ? payload.length : 1, buffered: !!this.#bufferOptions })
115
+
116
+ // Format items
117
+ const items = Array.isArray(payload) ? payload : [payload]
118
+ const formattedItems = items.map(item => {
119
+ // Determine the payload - check if property exists, not just truthy
120
+ let payloadValue
121
+ if ('data' in item) {
122
+ payloadValue = item.data
123
+ } else if ('payload' in item) {
124
+ payloadValue = item.payload
125
+ } else {
126
+ payloadValue = item
127
+ }
128
+
129
+ const result = {
130
+ queue: this.#queueName,
131
+ partition: this.#partition,
132
+ payload: payloadValue,
133
+ transactionId: item.transactionId || generateUUID()
134
+ }
135
+
136
+ // Include traceId if provided and valid UUID
137
+ if (item.traceId && isValidUUID(item.traceId)) {
138
+ result.traceId = item.traceId
139
+ }
140
+
141
+ return result
142
+ })
143
+
144
+ // Return a PushBuilder for chaining callbacks
145
+ return new PushBuilder(this.#httpClient, this.#bufferManager, this.#queueName, this.#partition, formattedItems, this.#bufferOptions)
146
+ }
147
+
148
+ // ===========================
149
+ // Consume Configuration Methods
150
+ // ===========================
151
+
152
+ group(name) {
153
+ this.#group = name
154
+ return this
155
+ }
156
+
157
+ concurrency(count) {
158
+ this.#concurrency = Math.max(1, count)
159
+ return this
160
+ }
161
+
162
+ batch(size) {
163
+ this.#batch = Math.max(1, size)
164
+ return this
165
+ }
166
+
167
+ limit(count) {
168
+ this.#limit = count
169
+ return this
170
+ }
171
+
172
+ idleMillis(millis) {
173
+ this.#idleMillis = millis
174
+ return this
175
+ }
176
+
177
+ autoAck(enabled) {
178
+ this.#autoAck = enabled
179
+ return this
180
+ }
181
+
182
+ renewLease(enabled, intervalMillis) {
183
+ this.#renewLease = enabled
184
+ if (intervalMillis) {
185
+ this.#renewLeaseIntervalMillis = intervalMillis
186
+ }
187
+ return this
188
+ }
189
+
190
+ subscriptionMode(mode) {
191
+ this.#subscriptionMode = mode
192
+ return this
193
+ }
194
+
195
+ subscriptionFrom(from) {
196
+ this.#subscriptionFrom = from
197
+ return this
198
+ }
199
+
200
+ each() {
201
+ this.#each = true
202
+ return this
203
+ }
204
+
205
+ // ===========================
206
+ // Consume Method
207
+ // ===========================
208
+
209
+ consume(handler, options = {}) {
210
+ const consumeOptions = {
211
+ queue: this.#queueName,
212
+ partition: this.#partition !== 'Default' ? this.#partition : null,
213
+ namespace: this.#namespace,
214
+ task: this.#task,
215
+ group: this.#group,
216
+ concurrency: this.#concurrency,
217
+ batch: this.#batch,
218
+ limit: this.#limit,
219
+ idleMillis: this.#idleMillis,
220
+ autoAck: this.#autoAck,
221
+ wait: this.#wait,
222
+ timeoutMillis: this.#timeoutMillis,
223
+ renewLease: this.#renewLease,
224
+ renewLeaseIntervalMillis: this.#renewLeaseIntervalMillis,
225
+ subscriptionMode: this.#subscriptionMode,
226
+ subscriptionFrom: this.#subscriptionFrom,
227
+ each: this.#each,
228
+ signal: options.signal
229
+ }
230
+
231
+ return new ConsumeBuilder(this.#httpClient, this.#queen, handler, consumeOptions)
232
+ }
233
+
234
+ // ===========================
235
+ // Pop Methods
236
+ // ===========================
237
+
238
+ wait(enabled) {
239
+ this.#wait = enabled
240
+ return this
241
+ }
242
+
243
+ async pop() {
244
+ logger.log('QueueBuilder.pop', { queue: this.#queueName, partition: this.#partition, namespace: this.#namespace, task: this.#task, batch: this.#batch, wait: this.#wait, group: this.#group })
245
+
246
+ try {
247
+ const path = this.#buildPopPath()
248
+
249
+ // For pop(), use POP defaults (not CONSUME defaults)
250
+ // Override autoAck to false unless explicitly set
251
+ const effectiveAutoAck = this.#autoAck !== CONSUME_DEFAULTS.autoAck ? this.#autoAck : POP_DEFAULTS.autoAck
252
+
253
+ // Build params with correct autoAck for pop
254
+ const params = new URLSearchParams({
255
+ batch: this.#batch.toString(),
256
+ wait: this.#wait.toString(),
257
+ timeout: this.#timeoutMillis.toString()
258
+ })
259
+
260
+ if (this.#group) params.append('consumerGroup', this.#group)
261
+ if (this.#namespace) params.append('namespace', this.#namespace)
262
+ if (this.#task) params.append('task', this.#task)
263
+ if (effectiveAutoAck) params.append('autoAck', 'true')
264
+ if (this.#subscriptionMode) params.append('subscriptionMode', this.#subscriptionMode)
265
+ if (this.#subscriptionFrom) params.append('subscriptionFrom', this.#subscriptionFrom)
266
+
267
+ const result = await this.#httpClient.get(`${path}?${params}`, this.#timeoutMillis + 5000)
268
+
269
+ if (!result || !result.messages) {
270
+ logger.log('QueueBuilder.pop', { status: 'no-messages' })
271
+ return []
272
+ }
273
+
274
+ const messages = result.messages.filter(msg => msg != null)
275
+ logger.log('QueueBuilder.pop', { status: 'success', count: messages.length })
276
+ return messages
277
+ } catch (error) {
278
+ // Return empty array on error instead of throwing
279
+ logger.error('QueueBuilder.pop', { error: error.message })
280
+ console.warn('Pop failed:', error.message)
281
+ return []
282
+ }
283
+ }
284
+
285
+ #buildPopPath() {
286
+ if (this.#queueName) {
287
+ if (this.#partition && this.#partition !== 'Default') {
288
+ return `/api/v1/pop/queue/${this.#queueName}/partition/${this.#partition}`
289
+ }
290
+ return `/api/v1/pop/queue/${this.#queueName}`
291
+ }
292
+
293
+ if (this.#namespace || this.#task) {
294
+ return '/api/v1/pop'
295
+ }
296
+
297
+ throw new Error('Must specify queue, namespace, or task for pop operation')
298
+ }
299
+
300
+ #buildPopParams() {
301
+ const params = new URLSearchParams({
302
+ batch: this.#batch.toString(),
303
+ wait: this.#wait.toString(),
304
+ timeout: this.#timeoutMillis.toString() // Server expects 'timeout', not 'timeoutMillis'
305
+ })
306
+
307
+ if (this.#group) params.append('consumerGroup', this.#group)
308
+ if (this.#namespace) params.append('namespace', this.#namespace)
309
+ if (this.#task) params.append('task', this.#task)
310
+ if (this.#autoAck) params.append('autoAck', 'true')
311
+ if (this.#subscriptionMode) params.append('subscriptionMode', this.#subscriptionMode)
312
+ if (this.#subscriptionFrom) params.append('subscriptionFrom', this.#subscriptionFrom)
313
+
314
+ return params
315
+ }
316
+
317
+ // ===========================
318
+ // Buffer Management Methods
319
+ // ===========================
320
+
321
+ async flushBuffer() {
322
+ if (!this.#queueName) {
323
+ throw new Error('Queue name is required for buffer flush')
324
+ }
325
+ const queueAddress = `${this.#queueName}/${this.#partition}`
326
+ logger.log('QueueBuilder.flushBuffer', { queueAddress })
327
+ await this.#bufferManager.flushBuffer(queueAddress)
328
+ }
329
+
330
+ // ===========================
331
+ // Dead Letter Queue Methods
332
+ // ===========================
333
+
334
+ dlq(consumerGroup = null) {
335
+ if (!this.#queueName) {
336
+ throw new Error('Queue name is required for DLQ operations')
337
+ }
338
+ logger.log('QueueBuilder.dlq', { queue: this.#queueName, consumerGroup, partition: this.#partition })
339
+ return new DLQBuilder(this.#httpClient, this.#queueName, consumerGroup, this.#partition)
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Operation builder for create/delete operations with callbacks
345
+ */
346
+ class OperationBuilder {
347
+ #httpClient
348
+ #method
349
+ #path
350
+ #body
351
+ #onSuccessCallback = null
352
+ #onErrorCallback = null
353
+ #executed = false
354
+
355
+ constructor(httpClient, method, path, body) {
356
+ this.#httpClient = httpClient
357
+ this.#method = method
358
+ this.#path = path
359
+ this.#body = body
360
+ }
361
+
362
+ onSuccess(callback) {
363
+ this.#onSuccessCallback = callback
364
+ return this
365
+ }
366
+
367
+ onError(callback) {
368
+ this.#onErrorCallback = callback
369
+ return this
370
+ }
371
+
372
+ // Auto-execute when awaited
373
+ then(resolve, reject) {
374
+ if (this.#executed) {
375
+ return Promise.resolve().then(resolve, reject)
376
+ }
377
+ this.#executed = true
378
+ return this.#execute().then(resolve, reject)
379
+ }
380
+
381
+ async #execute() {
382
+ logger.log('OperationBuilder.execute', { method: this.#method, path: this.#path })
383
+
384
+ try {
385
+ let result
386
+
387
+ if (this.#method === 'GET') {
388
+ result = await this.#httpClient.get(this.#path)
389
+ } else if (this.#method === 'POST') {
390
+ result = await this.#httpClient.post(this.#path, this.#body)
391
+ } else if (this.#method === 'PUT') {
392
+ result = await this.#httpClient.put(this.#path, this.#body)
393
+ } else if (this.#method === 'DELETE') {
394
+ result = await this.#httpClient.delete(this.#path)
395
+ }
396
+
397
+ if (result && result.error) {
398
+ const error = new Error(result.error)
399
+ logger.error('OperationBuilder.execute', { method: this.#method, path: this.#path, error: result.error })
400
+ if (this.#onErrorCallback) {
401
+ await this.#onErrorCallback(error)
402
+ return { success: false, error: result.error }
403
+ }
404
+ throw error
405
+ }
406
+
407
+ logger.log('OperationBuilder.execute', { method: this.#method, path: this.#path, status: 'success' })
408
+ if (this.#onSuccessCallback) {
409
+ await this.#onSuccessCallback(result)
410
+ }
411
+
412
+ return result
413
+
414
+ } catch (error) {
415
+ logger.error('OperationBuilder.execute', { method: this.#method, path: this.#path, error: error.message })
416
+ if (this.#onErrorCallback) {
417
+ await this.#onErrorCallback(error)
418
+ return { success: false, error: error.message }
419
+ }
420
+ throw error
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Consume builder for chaining callbacks
427
+ */
428
+ class ConsumeBuilder {
429
+ #httpClient
430
+ #queen
431
+ #handler
432
+ #options
433
+ #onSuccessCallback = null
434
+ #onErrorCallback = null
435
+ #executed = false
436
+
437
+ constructor(httpClient, queen, handler, options) {
438
+ this.#httpClient = httpClient
439
+ this.#queen = queen
440
+ this.#handler = handler
441
+ this.#options = options
442
+ }
443
+
444
+ onSuccess(callback) {
445
+ this.#onSuccessCallback = callback
446
+ return this
447
+ }
448
+
449
+ onError(callback) {
450
+ this.#onErrorCallback = callback
451
+ return this
452
+ }
453
+
454
+ // Auto-execute when awaited
455
+ then(resolve, reject) {
456
+ if (this.#executed) {
457
+ return Promise.resolve().then(resolve, reject)
458
+ }
459
+ this.#executed = true
460
+ return this.#execute().then(resolve, reject)
461
+ }
462
+
463
+ async #execute() {
464
+ // Import ConsumerManager lazily to avoid circular dependency
465
+ const { ConsumerManager } = await import('../consumer/ConsumerManager.js')
466
+ const consumerManager = new ConsumerManager(this.#httpClient, this.#queen)
467
+
468
+ // Wrap the handler to include callback logic
469
+ const wrappedHandler = async (msgOrMsgs) => {
470
+ try {
471
+ const result = await this.#handler(msgOrMsgs)
472
+
473
+ // Call onSuccess if defined
474
+ if (this.#onSuccessCallback) {
475
+ await this.#onSuccessCallback(msgOrMsgs, result)
476
+ }
477
+
478
+ return result
479
+ } catch (error) {
480
+ // Call onError if defined
481
+ if (this.#onErrorCallback) {
482
+ await this.#onErrorCallback(msgOrMsgs, error)
483
+ // Don't re-throw if callback is defined
484
+ return
485
+ }
486
+ // Re-throw if no error callback
487
+ throw error
488
+ }
489
+ }
490
+
491
+ // IMPORTANT: If callbacks are defined, auto-ack must be disabled
492
+ // to prevent double-acking (auto-ack + manual ack in callback)
493
+ const hasCallbacks = this.#onSuccessCallback || this.#onErrorCallback
494
+ const effectiveAutoAck = hasCallbacks ? false : this.#options.autoAck
495
+
496
+ const updatedOptions = {
497
+ ...this.#options,
498
+ autoAck: effectiveAutoAck
499
+ }
500
+
501
+ return consumerManager.start(wrappedHandler, updatedOptions)
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Push builder for chaining callbacks
507
+ */
508
+ class PushBuilder {
509
+ #httpClient
510
+ #bufferManager
511
+ #queueName
512
+ #partition
513
+ #formattedItems
514
+ #bufferOptions
515
+ #onSuccessCallback = null
516
+ #onErrorCallback = null
517
+ #onDuplicateCallback = null
518
+ #executed = false
519
+
520
+ constructor(httpClient, bufferManager, queueName, partition, formattedItems, bufferOptions) {
521
+ this.#httpClient = httpClient
522
+ this.#bufferManager = bufferManager
523
+ this.#queueName = queueName
524
+ this.#partition = partition
525
+ this.#formattedItems = formattedItems
526
+ this.#bufferOptions = bufferOptions
527
+ }
528
+
529
+ onSuccess(callback) {
530
+ this.#onSuccessCallback = callback
531
+ return this
532
+ }
533
+
534
+ onError(callback) {
535
+ this.#onErrorCallback = callback
536
+ return this
537
+ }
538
+
539
+ onDuplicate(callback) {
540
+ this.#onDuplicateCallback = callback
541
+ return this
542
+ }
543
+
544
+ // Auto-execute when awaited
545
+ then(resolve, reject) {
546
+ if (this.#executed) {
547
+ return Promise.resolve().then(resolve, reject)
548
+ }
549
+ this.#executed = true
550
+ return this.#execute().then(resolve, reject)
551
+ }
552
+
553
+ async #execute() {
554
+ logger.log('PushBuilder.execute', { queue: this.#queueName, partition: this.#partition, count: this.#formattedItems.length, buffered: !!this.#bufferOptions })
555
+
556
+ // Client-side buffering
557
+ if (this.#bufferOptions) {
558
+ for (const item of this.#formattedItems) {
559
+ const queueAddress = `${this.#queueName}/${this.#partition}`
560
+ this.#bufferManager.addMessage(queueAddress, item, this.#bufferOptions)
561
+ }
562
+ const result = { buffered: true, count: this.#formattedItems.length }
563
+
564
+ logger.log('PushBuilder.execute', { status: 'buffered', count: this.#formattedItems.length })
565
+
566
+ if (this.#onSuccessCallback) {
567
+ await this.#onSuccessCallback(this.#formattedItems)
568
+ }
569
+
570
+ return result
571
+ }
572
+
573
+ // Immediate push
574
+ try {
575
+ const results = await this.#httpClient.post('/api/v1/push', { items: this.#formattedItems })
576
+
577
+ // Server returns an array of results with status for each item
578
+ if (Array.isArray(results)) {
579
+ // Separate results by status
580
+ const successful = []
581
+ const duplicates = []
582
+ const failed = []
583
+
584
+ for (let i = 0; i < results.length; i++) {
585
+ const result = results[i]
586
+ const originalItem = this.#formattedItems[i]
587
+
588
+ if (result.status === 'duplicate') {
589
+ duplicates.push({ ...originalItem, result })
590
+ } else if (result.status === 'failed') {
591
+ failed.push({ ...originalItem, result, error: result.error })
592
+ } else if (result.status === 'queued') {
593
+ successful.push({ ...originalItem, result })
594
+ }
595
+ }
596
+
597
+ // Call appropriate callbacks
598
+ if (duplicates.length > 0 && this.#onDuplicateCallback) {
599
+ await this.#onDuplicateCallback(duplicates, new Error('Duplicate transaction IDs detected'))
600
+ }
601
+
602
+ if (failed.length > 0 && this.#onErrorCallback) {
603
+ const error = new Error(failed[0].error || 'Push failed')
604
+ await this.#onErrorCallback(failed, error)
605
+ }
606
+
607
+ if (successful.length > 0 && this.#onSuccessCallback) {
608
+ await this.#onSuccessCallback(successful)
609
+ }
610
+
611
+ // Only throw if no error callback is defined
612
+ if (failed.length > 0 && !this.#onErrorCallback) {
613
+ logger.error('PushBuilder.execute', { status: 'failed', count: failed.length })
614
+ throw new Error(failed[0].error || 'Push failed')
615
+ }
616
+
617
+ logger.log('PushBuilder.execute', { status: 'success', successful: successful.length, duplicates: duplicates.length, failed: failed.length })
618
+ return results
619
+ }
620
+
621
+ // Fallback for non-array responses
622
+ if (results && results.error) {
623
+ const error = new Error(results.error)
624
+ if (this.#onErrorCallback) {
625
+ await this.#onErrorCallback(this.#formattedItems, error)
626
+ return results // Don't throw if callback is defined
627
+ }
628
+ throw error
629
+ }
630
+
631
+ if (this.#onSuccessCallback) {
632
+ await this.#onSuccessCallback(this.#formattedItems)
633
+ }
634
+
635
+ return results
636
+
637
+ } catch (error) {
638
+ // Network or HTTP errors
639
+ if (this.#onErrorCallback) {
640
+ await this.#onErrorCallback(this.#formattedItems, error)
641
+ return null // Don't throw if callback is defined
642
+ }
643
+ throw error
644
+ }
645
+ }
646
+ }
647
+
648
+ /**
649
+ * DLQ (Dead Letter Queue) builder for querying failed messages
650
+ */
651
+ class DLQBuilder {
652
+ #httpClient
653
+ #queueName
654
+ #consumerGroup
655
+ #partition
656
+ #limit = 100
657
+ #offset = 0
658
+ #from = null
659
+ #to = null
660
+
661
+ constructor(httpClient, queueName, consumerGroup, partition) {
662
+ this.#httpClient = httpClient
663
+ this.#queueName = queueName
664
+ this.#consumerGroup = consumerGroup
665
+ this.#partition = partition !== 'Default' ? partition : null
666
+ }
667
+
668
+ limit(count) {
669
+ this.#limit = Math.max(1, count)
670
+ return this
671
+ }
672
+
673
+ offset(count) {
674
+ this.#offset = Math.max(0, count)
675
+ return this
676
+ }
677
+
678
+ from(timestamp) {
679
+ this.#from = timestamp
680
+ return this
681
+ }
682
+
683
+ to(timestamp) {
684
+ this.#to = timestamp
685
+ return this
686
+ }
687
+
688
+ async get() {
689
+ const params = new URLSearchParams()
690
+
691
+ params.append('queue', this.#queueName)
692
+ params.append('limit', this.#limit.toString())
693
+ params.append('offset', this.#offset.toString())
694
+
695
+ if (this.#consumerGroup) {
696
+ params.append('consumerGroup', this.#consumerGroup)
697
+ }
698
+
699
+ if (this.#partition) {
700
+ params.append('partition', this.#partition)
701
+ }
702
+
703
+ if (this.#from) {
704
+ params.append('from', this.#from)
705
+ }
706
+
707
+ if (this.#to) {
708
+ params.append('to', this.#to)
709
+ }
710
+
711
+ logger.log('DLQBuilder.get', { queue: this.#queueName, consumerGroup: this.#consumerGroup, partition: this.#partition, limit: this.#limit, offset: this.#offset })
712
+
713
+ try {
714
+ const result = await this.#httpClient.get(`/api/v1/dlq?${params}`)
715
+ logger.log('DLQBuilder.get', { status: 'success', total: result?.total || 0, messages: result?.messages?.length || 0 })
716
+ return result || { messages: [], total: 0 }
717
+ } catch (error) {
718
+ logger.error('DLQBuilder.get', { error: error.message })
719
+ console.warn('DLQ query failed:', error.message)
720
+ return { messages: [], total: 0 }
721
+ }
722
+ }
723
+ }
724
+