queen-mq 0.1.1 → 0.1.2

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 (139) hide show
  1. package/API.md +1226 -0
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +276 -53
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-dashboard-api.js +200 -0
  17. package/package.json +4 -2
  18. package/server.log +1 -0
  19. package/src/benchmark/consumer.js +207 -0
  20. package/src/benchmark/consumer_multi.js +216 -0
  21. package/src/benchmark/producer.js +75 -0
  22. package/src/benchmark/producer_multi.js +115 -0
  23. package/src/client/client.js +192 -16
  24. package/src/client/queenClient.js +5 -0
  25. package/src/cluster-server.js +242 -0
  26. package/src/config.js +19 -5
  27. package/src/database/connection.js +42 -16
  28. package/src/database/poolManager.js +7 -0
  29. package/src/database/schema-v2.sql +194 -130
  30. package/src/managers/queueManagerOptimized.js +823 -933
  31. package/src/managers/systemEventManager.js +8 -3
  32. package/src/routes/messages.js +127 -57
  33. package/src/routes/pop.js +27 -43
  34. package/src/routes/resources.js +61 -27
  35. package/src/routes/status.js +1037 -0
  36. package/src/server.js +308 -272
  37. package/src/services/evictionService.js +57 -28
  38. package/src/services/retentionService.js +44 -11
  39. package/src/test/advanced-pattern-tests.js +5 -5
  40. package/src/test/bus-mode-tests.js +24 -11
  41. package/src/test/edge-case-tests.js +12 -5
  42. package/src/test/enterprise-tests.js +48 -15
  43. package/src/test/utils.js +1 -1
  44. package/src/utils/streaming.js +231 -0
  45. package/src/utils/uuid.js +2 -2
  46. package/src/websocket/wsServer.js +10 -3
  47. package/test-keepalive-v2.sh +22 -0
  48. package/webapp/COLOR_GUIDE.md +118 -0
  49. package/webapp/README.md +143 -0
  50. package/webapp/index.html +14 -0
  51. package/webapp/package-lock.json +3184 -0
  52. package/webapp/package.json +25 -0
  53. package/webapp/postcss.config.js +7 -0
  54. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  55. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  56. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  57. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  58. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  59. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  60. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  61. package/webapp/public/assets/queen-logo.svg +263 -0
  62. package/webapp/src/App.vue +19 -0
  63. package/webapp/src/api/analytics.js +10 -0
  64. package/webapp/src/api/client.js +29 -0
  65. package/webapp/src/api/consumers.js +52 -0
  66. package/webapp/src/api/health.js +7 -0
  67. package/webapp/src/api/messages.js +26 -0
  68. package/webapp/src/api/queues.js +14 -0
  69. package/webapp/src/api/resources.js +8 -0
  70. package/webapp/src/assets/styles/main.css +357 -0
  71. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  72. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  73. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  74. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  75. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  76. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  77. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  78. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  79. package/webapp/src/components/common/MetricCard.vue +43 -0
  80. package/webapp/src/components/common/StatusBadge.vue +45 -0
  81. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  82. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  83. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  84. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  85. package/webapp/src/components/layout/AppLayout.vue +110 -0
  86. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  87. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  88. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  89. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  90. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  91. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  92. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  93. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  94. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  95. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  96. package/webapp/src/composables/useApi.js +34 -0
  97. package/webapp/src/composables/useTheme.js +36 -0
  98. package/webapp/src/main.js +11 -0
  99. package/webapp/src/router/index.js +42 -0
  100. package/webapp/src/utils/colors.js +96 -0
  101. package/webapp/src/utils/formatters.js +49 -0
  102. package/webapp/src/views/Analytics.vue +377 -0
  103. package/webapp/src/views/ConsumerGroups.vue +433 -0
  104. package/webapp/src/views/Dashboard.vue +418 -0
  105. package/webapp/src/views/Messages.vue +361 -0
  106. package/webapp/src/views/QueueDetail.vue +582 -0
  107. package/webapp/src/views/Queues.vue +496 -0
  108. package/webapp/tailwind.config.js +25 -0
  109. package/webapp/vite.config.js +10 -0
  110. package/dashboard/.vscode/extensions.json +0 -3
  111. package/dashboard/README.md +0 -5
  112. package/dashboard/index.html +0 -14
  113. package/dashboard/package-lock.json +0 -1458
  114. package/dashboard/package.json +0 -25
  115. package/dashboard/public/vite.svg +0 -1
  116. package/dashboard/src/App.vue +0 -29
  117. package/dashboard/src/assets/styles/main.css +0 -908
  118. package/dashboard/src/assets/vue.svg +0 -1
  119. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  120. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  121. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  122. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  123. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  124. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  125. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  126. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  127. package/dashboard/src/main.js +0 -44
  128. package/dashboard/src/router.js +0 -54
  129. package/dashboard/src/services/api.js +0 -187
  130. package/dashboard/src/services/websocket.js +0 -167
  131. package/dashboard/src/utils/constants.js +0 -56
  132. package/dashboard/src/utils/helpers.js +0 -118
  133. package/dashboard/src/views/Analytics.vue +0 -912
  134. package/dashboard/src/views/Dashboard.vue +0 -906
  135. package/dashboard/src/views/Messages.vue +0 -437
  136. package/dashboard/src/views/QueueDetail.vue +0 -501
  137. package/dashboard/src/views/Queues.vue +0 -333
  138. package/dashboard/vite.config.js +0 -30
  139. package/src/routes/analytics.js +0 -812
@@ -0,0 +1,75 @@
1
+ import { Queen } from '../client/client.js';
2
+ import fs from 'fs';
3
+
4
+ const TOTAL_MESSAGES = 100000;
5
+ const PARTITIONS = 10;
6
+ const MAX_BATCH_SIZE = 1000; // Reduced from 10000
7
+ const PUSH_PARALLEL = 10; // Reduced from 10 to avoid lock contention
8
+ const QUEUE_NAME = 'benchmark-queue-001';
9
+
10
+ const q = new Queen({
11
+ baseUrls: ['http://localhost:6632'],
12
+ timeout: 120000
13
+ });
14
+
15
+ //await q.queueDelete(QUEUE_NAME);
16
+ await q.queue(QUEUE_NAME, {
17
+ leaseTime: 10,
18
+ retryLimit: 3
19
+ }, { namespace: 'benchmark' });
20
+
21
+
22
+
23
+ let batches = []
24
+ const messagesPerPartition = Math.floor(TOTAL_MESSAGES / PARTITIONS);
25
+
26
+ // Create batches for each partition
27
+ const partitionBatches = [];
28
+ for (let i = 0; i < PARTITIONS; i++) {
29
+ partitionBatches[i] = [];
30
+ let messages = [];
31
+ for (let j = 0; j < messagesPerPartition; j++) {
32
+ messages.push({
33
+ id: j,
34
+ //message: `Message ${globalMessageId}`,
35
+ //timestamp: new Date().toISOString()
36
+ });
37
+ if (messages.length >= MAX_BATCH_SIZE) {
38
+ console.log(`Pushing ${messages.length} messages for partition ${i}`);
39
+ partitionBatches[i].push({
40
+ partition: i,
41
+ messages: messages
42
+ });
43
+ messages = [];
44
+ }
45
+ }
46
+ // Only add the remaining batch if it has messages
47
+ if (messages.length > 0) {
48
+ console.log(`Pushing ${messages.length} messages for partition ${i}`);
49
+ partitionBatches[i].push({
50
+ partition: i,
51
+ messages: messages
52
+ });
53
+ }
54
+ }
55
+
56
+ // Interleave batches from different partitions to avoid lock contention
57
+ // This ensures parallel pushes go to different partitions
58
+ const maxBatchesPerPartition = Math.max(...partitionBatches.map(pb => pb.length));
59
+ for (let batchIndex = 0; batchIndex < maxBatchesPerPartition; batchIndex++) {
60
+ for (let partition = 0; partition < PARTITIONS; partition++) {
61
+ if (partitionBatches[partition][batchIndex]) {
62
+ batches.push(partitionBatches[partition][batchIndex]);
63
+ }
64
+ }
65
+ }
66
+
67
+ const start = Date.now();
68
+ console.log(`Total batches: ${batches.length}, pushing ${PUSH_PARALLEL} at a time`);
69
+ for (let i = 0; i < batches.length; i += PUSH_PARALLEL) {
70
+ const batchGroup = batches.slice(i, i + PUSH_PARALLEL);
71
+ console.log(`Pushing batch group ${Math.floor(i/PUSH_PARALLEL) + 1}/${Math.ceil(batches.length/PUSH_PARALLEL)}: ${batchGroup.map(b => `partition ${b.partition} (${b.messages.length} msgs)`).join(', ')}`);
72
+ await Promise.all(batchGroup.map(batch => q.push(`${QUEUE_NAME}/${batch.partition}`, batch.messages)));
73
+ }
74
+ const end = Date.now();
75
+ console.log(`Pushed ${TOTAL_MESSAGES} messages in ${(end - start) / 1000}s, ${(TOTAL_MESSAGES / ((end - start) / 1000)).toFixed(2)} msg/s`);
@@ -0,0 +1,115 @@
1
+ import { Queen } from '../client/client.js';
2
+ import fs from 'fs';
3
+
4
+ const TOTAL_MESSAGES = 100000;
5
+ const NUM_QUEUES = 50; // X: Number of queues
6
+ const PARTITIONS_PER_QUEUE = 10; // Y: Partitions per queue
7
+ const MAX_BATCH_SIZE = 10;
8
+ const PUSH_PARALLEL = 10;
9
+
10
+ const q = new Queen({
11
+ baseUrls: ['http://localhost:6632'],
12
+ timeout: 120000
13
+ });
14
+
15
+ // Create X queues, each with Y partitions, all in 'benchmark' namespace
16
+ console.log(`Creating ${NUM_QUEUES} queues with ${PARTITIONS_PER_QUEUE} partitions each...`);
17
+ for (let queueIdx = 0; queueIdx < NUM_QUEUES; queueIdx++) {
18
+ const queueName = `benchmark-queue-${String(queueIdx + 1).padStart(2, '0')}`;
19
+ try {
20
+ //await q.queueDelete(queueName);
21
+ } catch (e) {
22
+ // Queue might not exist
23
+ }
24
+ await q.queue(queueName, {
25
+ leaseTime: 10,
26
+ retryLimit: 3
27
+ }, { namespace: 'benchmark' });
28
+ console.log(` Created queue: ${queueName}`);
29
+ }
30
+
31
+
32
+ // Calculate total partitions across all queues
33
+ const TOTAL_PARTITIONS = NUM_QUEUES * PARTITIONS_PER_QUEUE;
34
+ const messagesPerPartition = Math.floor(TOTAL_MESSAGES / TOTAL_PARTITIONS);
35
+
36
+ console.log(`\nDistributing ${TOTAL_MESSAGES} messages across ${NUM_QUEUES} queues × ${PARTITIONS_PER_QUEUE} partitions = ${TOTAL_PARTITIONS} total partitions`);
37
+ console.log(`Messages per partition: ${messagesPerPartition}\n`);
38
+
39
+ // Create batches for each queue/partition combination
40
+ const queuePartitionBatches = [];
41
+ for (let queueIdx = 0; queueIdx < NUM_QUEUES; queueIdx++) {
42
+ for (let partitionIdx = 0; partitionIdx < PARTITIONS_PER_QUEUE; partitionIdx++) {
43
+ const key = `q${queueIdx}_p${partitionIdx}`;
44
+ queuePartitionBatches.push({ queueIdx, partitionIdx, batches: [] });
45
+
46
+ let messages = [];
47
+ for (let msgIdx = 0; msgIdx < messagesPerPartition; msgIdx++) {
48
+ messages.push({
49
+ id: msgIdx,
50
+ queue: queueIdx,
51
+ partition: partitionIdx
52
+ });
53
+
54
+ if (messages.length >= MAX_BATCH_SIZE) {
55
+ queuePartitionBatches[queuePartitionBatches.length - 1].batches.push([...messages]);
56
+ messages = [];
57
+ }
58
+ }
59
+
60
+ // Add remaining messages
61
+ if (messages.length > 0) {
62
+ queuePartitionBatches[queuePartitionBatches.length - 1].batches.push([...messages]);
63
+ }
64
+ }
65
+ }
66
+
67
+ // Flatten and interleave batches to distribute load evenly
68
+ let batches = [];
69
+ const maxBatchesPerQueuePartition = Math.max(...queuePartitionBatches.map(qp => qp.batches.length));
70
+ for (let batchIdx = 0; batchIdx < maxBatchesPerQueuePartition; batchIdx++) {
71
+ for (const qp of queuePartitionBatches) {
72
+ if (qp.batches[batchIdx]) {
73
+ batches.push({
74
+ queueIdx: qp.queueIdx,
75
+ partitionIdx: qp.partitionIdx,
76
+ messages: qp.batches[batchIdx]
77
+ });
78
+ }
79
+ }
80
+ }
81
+
82
+ const start = Date.now();
83
+ console.log(`Total batches: ${batches.length}, pushing ${PUSH_PARALLEL} at a time\n`);
84
+
85
+ for (let i = 0; i < batches.length; i += PUSH_PARALLEL) {
86
+ const batchGroup = batches.slice(i, i + PUSH_PARALLEL);
87
+ const batchGroupNum = Math.floor(i/PUSH_PARALLEL) + 1;
88
+ const totalBatchGroups = Math.ceil(batches.length/PUSH_PARALLEL);
89
+
90
+ console.log(`[${batchGroupNum}/${totalBatchGroups}] Pushing: ${batchGroup.map(b => {
91
+ const queueName = `benchmark-queue-${String(b.queueIdx + 1).padStart(2, '0')}`;
92
+ return `${queueName}/p${b.partitionIdx} (${b.messages.length} msgs)`;
93
+ }).join(', ')}`);
94
+
95
+ await Promise.all(batchGroup.map(batch => {
96
+ const queueName = `benchmark-queue-${String(batch.queueIdx + 1).padStart(2, '0')}`;
97
+ return q.push(`${queueName}/${batch.partitionIdx}`, batch.messages);
98
+ }));
99
+ }
100
+
101
+ const end = Date.now();
102
+ const duration = (end - start) / 1000;
103
+ const throughput = (TOTAL_MESSAGES / duration).toFixed(2);
104
+
105
+ console.log('\n' + '='.repeat(80));
106
+ console.log('📤 PRODUCER RESULTS');
107
+ console.log('='.repeat(80));
108
+ console.log(`Total Messages Pushed: ${TOTAL_MESSAGES.toLocaleString()}`);
109
+ console.log(`Queues: ${NUM_QUEUES}`);
110
+ console.log(`Partitions per Queue: ${PARTITIONS_PER_QUEUE}`);
111
+ console.log(`Total Partitions: ${TOTAL_PARTITIONS}`);
112
+ console.log(`Duration: ${duration.toFixed(2)}s`);
113
+ console.log(`Throughput: ${throughput} msg/s`);
114
+ console.log('='.repeat(80));
115
+
@@ -212,12 +212,13 @@ export class Queen {
212
212
  }
213
213
 
214
214
  /**
215
- * Take messages from a queue (async iterator)
216
- * @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
215
+ * Internal method that yields batches of messages (arrays)
216
+ * @private
217
+ * @param {string} address - Queue address
217
218
  * @param {Object} options - Options for taking messages
218
- * @yields {Object} Message objects
219
+ * @yields {Array} Arrays of message objects
219
220
  */
220
- async *take(address, options = {}) {
221
+ async *#takeInternal(address, options = {}) {
221
222
  await this.#ensureConnected();
222
223
 
223
224
  const { queue, partition, consumerGroup, namespace, task } = this.#parseAddress(address);
@@ -227,23 +228,36 @@ export class Queen {
227
228
  wait = false,
228
229
  timeout = 30000,
229
230
  subscriptionMode = null,
230
- subscriptionFrom = null
231
+ subscriptionFrom = null,
232
+ idleTimeout = null
231
233
  } = options;
232
234
 
233
- let count = 0;
235
+ let totalCount = 0;
234
236
  let consecutiveEmptyResponses = 0;
235
237
  const maxConsecutiveEmpty = 3;
238
+ let lastMessageTime = idleTimeout ? Date.now() : null;
236
239
 
237
240
  while (true) {
238
241
  // Check if we've reached the limit
239
- if (limit && count >= limit) break;
242
+ if (limit && totalCount >= limit) break;
243
+
244
+ // Check idle timeout
245
+ if (idleTimeout && lastMessageTime) {
246
+ const idleTime = Date.now() - lastMessageTime;
247
+ if (idleTime >= idleTimeout) {
248
+ break; // Exit if idle time exceeded
249
+ }
250
+ }
251
+
252
+ // Calculate batch size respecting the limit
253
+ const effectiveBatch = limit ? Math.min(batch, limit - totalCount) : batch;
240
254
 
241
255
  // Build the request path and parameters
242
256
  let path;
243
257
  const params = new URLSearchParams({
244
258
  wait: wait.toString(),
245
259
  timeout: timeout.toString(),
246
- batch: Math.min(batch, limit ? limit - count : batch).toString()
260
+ batch: effectiveBatch.toString()
247
261
  });
248
262
 
249
263
  // Add consumer group parameters if provided
@@ -293,12 +307,21 @@ export class Queen {
293
307
  // Reset empty counter on successful fetch
294
308
  consecutiveEmptyResponses = 0;
295
309
 
296
- // Yield each message (filter out any null/undefined values)
297
- for (const message of result.messages) {
298
- if (message) { // Skip null/undefined messages
299
- yield message;
300
- count++;
301
- if (limit && count >= limit) return;
310
+ // Update last message time if tracking idle timeout
311
+ if (idleTimeout) {
312
+ lastMessageTime = Date.now();
313
+ }
314
+
315
+ // Filter out null/undefined messages
316
+ const messages = result.messages.filter(msg => msg != null);
317
+
318
+ if (messages.length > 0) {
319
+ totalCount += messages.length;
320
+ yield messages;
321
+
322
+ // If we've hit the limit, stop
323
+ if (limit && totalCount >= limit) {
324
+ break;
302
325
  }
303
326
  }
304
327
 
@@ -320,14 +343,142 @@ export class Queen {
320
343
  }
321
344
 
322
345
  /**
323
- * Acknowledge a message
324
- * @param {Object|string} message - Message object or transaction ID
346
+ * Take messages from a queue one at a time (async iterator)
347
+ * @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
348
+ * @param {Object} options - Options for taking messages
349
+ * @yields {Object} Individual message objects
350
+ */
351
+ async *take(address, options = {}) {
352
+ let count = 0;
353
+ const { limit = null } = options;
354
+
355
+ for await (const messages of this.#takeInternal(address, options)) {
356
+ for (const message of messages) {
357
+ yield message;
358
+ count++;
359
+
360
+ // Double-check limit (internal method also checks, but this ensures exact limit)
361
+ if (limit && count >= limit) {
362
+ return;
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Take messages from a queue in batches (async iterator)
370
+ * @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
371
+ * @param {Object} options - Options for taking messages
372
+ * @yields {Array} Arrays of message objects
373
+ */
374
+ async *takeBatch(address, options = {}) {
375
+ for await (const messages of this.#takeInternal(address, options)) {
376
+ // Only yield non-empty batches
377
+ if (messages && messages.length > 0) {
378
+ yield messages;
379
+ }
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Acknowledge a message or batch of messages
385
+ * @param {Object|string|Array} message - Message object, transaction ID, or array of messages
325
386
  * @param {boolean|string} status - true for success, false for failure, or 'retry'
326
387
  * @param {Object} context - Optional context (e.g., { group: 'workers', error: 'reason' })
327
388
  */
328
389
  async ack(message, status = true, context = {}) {
329
390
  await this.#ensureConnected();
330
391
 
392
+ // Handle batch acknowledgment
393
+ if (Array.isArray(message)) {
394
+ if (message.length === 0) {
395
+ return { processed: 0, results: [] };
396
+ }
397
+
398
+ // Check if messages have individual status (Option B pattern)
399
+ const hasIndividualStatus = message.some(msg =>
400
+ typeof msg === 'object' && msg !== null && ('_status' in msg || '_error' in msg)
401
+ );
402
+
403
+ let acknowledgments;
404
+
405
+ if (hasIndividualStatus) {
406
+ // Option B: Each message has its own status
407
+ acknowledgments = message.map(msg => {
408
+ // Extract transaction ID
409
+ let transactionId;
410
+ if (typeof msg === 'string') {
411
+ transactionId = msg;
412
+ } else if (typeof msg === 'object' && msg !== null) {
413
+ transactionId = msg.transactionId || msg.id;
414
+ if (!transactionId) {
415
+ throw new Error('Message object must have transactionId or id property');
416
+ }
417
+ } else {
418
+ throw new Error('Invalid message in batch');
419
+ }
420
+
421
+ // Get individual status or fall back to parameter
422
+ let msgStatus = msg._status !== undefined ? msg._status : status;
423
+ let statusStr;
424
+ if (typeof msgStatus === 'boolean') {
425
+ statusStr = msgStatus ? 'completed' : 'failed';
426
+ } else {
427
+ statusStr = msgStatus;
428
+ }
429
+
430
+ return {
431
+ transactionId,
432
+ status: statusStr,
433
+ error: msg._error || context.error || null
434
+ };
435
+ });
436
+ } else {
437
+ // Option A: Same status for all messages
438
+ const statusStr = typeof status === 'boolean'
439
+ ? (status ? 'completed' : 'failed')
440
+ : status;
441
+
442
+ acknowledgments = message.map(msg => {
443
+ // Extract transaction ID
444
+ let transactionId;
445
+ if (typeof msg === 'string') {
446
+ transactionId = msg;
447
+ } else if (typeof msg === 'object' && msg !== null) {
448
+ transactionId = msg.transactionId || msg.id;
449
+ if (!transactionId) {
450
+ throw new Error('Message object must have transactionId or id property');
451
+ }
452
+ } else {
453
+ throw new Error('Invalid message in batch');
454
+ }
455
+
456
+ return {
457
+ transactionId,
458
+ status: statusStr,
459
+ error: context.error || null
460
+ };
461
+ });
462
+ }
463
+
464
+ // Call batch ack endpoint
465
+ const result = await withRetry(
466
+ () => this.#http.post('/api/v1/ack/batch', {
467
+ acknowledgments,
468
+ consumerGroup: context.group || null
469
+ }),
470
+ this.#config.retryAttempts,
471
+ this.#config.retryDelay
472
+ );
473
+
474
+ if (result && result.error) {
475
+ throw new Error(result.error);
476
+ }
477
+
478
+ return result;
479
+ }
480
+
481
+ // Handle single message acknowledgment
331
482
  // Extract transaction ID
332
483
  let transactionId;
333
484
  if (typeof message === 'string') {
@@ -370,6 +521,31 @@ export class Queen {
370
521
  return result;
371
522
  }
372
523
 
524
+ /**
525
+ * Delete a queue (removes queue and all its messages/partitions)
526
+ * @param {string} name - Queue name to delete
527
+ * @returns {Promise<Object>} Deletion result
528
+ */
529
+ async queueDelete(name) {
530
+ await this.#ensureConnected();
531
+
532
+ if (typeof name !== 'string' || !name) {
533
+ throw new Error('Queue name must be a non-empty string');
534
+ }
535
+
536
+ const result = await withRetry(
537
+ () => this.#http.delete(`/api/v1/resources/queues/${encodeURIComponent(name)}`),
538
+ this.#config.retryAttempts,
539
+ this.#config.retryDelay
540
+ );
541
+
542
+ if (result && result.error) {
543
+ throw new Error(result.error);
544
+ }
545
+
546
+ return result;
547
+ }
548
+
373
549
  /**
374
550
  * Close the client connection
375
551
  */
@@ -212,6 +212,11 @@ export const createQueenClient = (options = {}) => {
212
212
  clear: async (queue, partition = null) => {
213
213
  const params = partition ? `?partition=${partition}` : '';
214
214
  return http.delete(`/api/v1/queues/${queue}/clear${params}`);
215
+ },
216
+
217
+ // Delete a queue (removes queue and all its messages/partitions)
218
+ delete: async (queue) => {
219
+ return http.delete(`/api/v1/resources/queues/${encodeURIComponent(queue)}`);
215
220
  }
216
221
  };
217
222