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.
- package/API.md +1226 -0
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +276 -53
- package/WEBAPP.md +1889 -0
- package/assets/dashboard-01.png +0 -0
- package/assets/queen-logo-blue.svg +210 -0
- package/assets/queen-logo-cyan.svg +210 -0
- package/assets/queen-logo-indigo.svg +210 -0
- package/assets/queen-logo-orange.svg +210 -0
- package/assets/queen-logo-pink.svg +210 -0
- package/assets/queen-logo-purple.svg +210 -0
- package/assets/queen-logo-rose.svg +239 -0
- package/assets/queen-logo.svg +263 -0
- package/examples/batch-processing.js +58 -0
- package/examples/test-dashboard-api.js +200 -0
- package/package.json +4 -2
- package/server.log +1 -0
- package/src/benchmark/consumer.js +207 -0
- package/src/benchmark/consumer_multi.js +216 -0
- package/src/benchmark/producer.js +75 -0
- package/src/benchmark/producer_multi.js +115 -0
- package/src/client/client.js +192 -16
- package/src/client/queenClient.js +5 -0
- package/src/cluster-server.js +242 -0
- package/src/config.js +19 -5
- package/src/database/connection.js +42 -16
- package/src/database/poolManager.js +7 -0
- package/src/database/schema-v2.sql +194 -130
- package/src/managers/queueManagerOptimized.js +823 -933
- package/src/managers/systemEventManager.js +8 -3
- package/src/routes/messages.js +127 -57
- package/src/routes/pop.js +27 -43
- package/src/routes/resources.js +61 -27
- package/src/routes/status.js +1037 -0
- package/src/server.js +308 -272
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/test/advanced-pattern-tests.js +5 -5
- package/src/test/bus-mode-tests.js +24 -11
- package/src/test/edge-case-tests.js +12 -5
- package/src/test/enterprise-tests.js +48 -15
- package/src/test/utils.js +1 -1
- package/src/utils/streaming.js +231 -0
- package/src/utils/uuid.js +2 -2
- package/src/websocket/wsServer.js +10 -3
- package/test-keepalive-v2.sh +22 -0
- package/webapp/COLOR_GUIDE.md +118 -0
- package/webapp/README.md +143 -0
- package/webapp/index.html +14 -0
- package/webapp/package-lock.json +3184 -0
- package/webapp/package.json +25 -0
- package/webapp/postcss.config.js +7 -0
- package/webapp/public/assets/queen-logo-blue.svg +210 -0
- package/webapp/public/assets/queen-logo-cyan.svg +210 -0
- package/webapp/public/assets/queen-logo-indigo.svg +210 -0
- package/webapp/public/assets/queen-logo-orange.svg +210 -0
- package/webapp/public/assets/queen-logo-pink.svg +210 -0
- package/webapp/public/assets/queen-logo-purple.svg +210 -0
- package/webapp/public/assets/queen-logo-rose.svg +239 -0
- package/webapp/public/assets/queen-logo.svg +263 -0
- package/webapp/src/App.vue +19 -0
- package/webapp/src/api/analytics.js +10 -0
- package/webapp/src/api/client.js +29 -0
- package/webapp/src/api/consumers.js +52 -0
- package/webapp/src/api/health.js +7 -0
- package/webapp/src/api/messages.js +26 -0
- package/webapp/src/api/queues.js +14 -0
- package/webapp/src/api/resources.js +8 -0
- package/webapp/src/assets/styles/main.css +357 -0
- package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
- package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
- package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
- package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
- package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
- package/webapp/src/components/common/ConfirmDialog.vue +56 -0
- package/webapp/src/components/common/LoadingSpinner.vue +6 -0
- package/webapp/src/components/common/MetricCard.vue +43 -0
- package/webapp/src/components/common/StatusBadge.vue +45 -0
- package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
- package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
- package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
- package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
- package/webapp/src/components/layout/AppLayout.vue +110 -0
- package/webapp/src/components/layout/AppSidebar.vue +304 -0
- package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
- package/webapp/src/components/messages/MessageFilters.vue +114 -0
- package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
- package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
- package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
- package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
- package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
- package/webapp/src/components/queues/QueueFilters.vue +90 -0
- package/webapp/src/composables/useApi.js +34 -0
- package/webapp/src/composables/useTheme.js +36 -0
- package/webapp/src/main.js +11 -0
- package/webapp/src/router/index.js +42 -0
- package/webapp/src/utils/colors.js +96 -0
- package/webapp/src/utils/formatters.js +49 -0
- package/webapp/src/views/Analytics.vue +377 -0
- package/webapp/src/views/ConsumerGroups.vue +433 -0
- package/webapp/src/views/Dashboard.vue +418 -0
- package/webapp/src/views/Messages.vue +361 -0
- package/webapp/src/views/QueueDetail.vue +582 -0
- package/webapp/src/views/Queues.vue +496 -0
- package/webapp/tailwind.config.js +25 -0
- package/webapp/vite.config.js +10 -0
- package/dashboard/.vscode/extensions.json +0 -3
- package/dashboard/README.md +0 -5
- package/dashboard/index.html +0 -14
- package/dashboard/package-lock.json +0 -1458
- package/dashboard/package.json +0 -25
- package/dashboard/public/vite.svg +0 -1
- package/dashboard/src/App.vue +0 -29
- package/dashboard/src/assets/styles/main.css +0 -908
- package/dashboard/src/assets/vue.svg +0 -1
- package/dashboard/src/components/cards/MetricCard.vue +0 -298
- package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
- package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
- package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
- package/dashboard/src/components/common/ActivityFeed.vue +0 -251
- package/dashboard/src/components/layout/AppHeader.vue +0 -208
- package/dashboard/src/components/layout/AppLayout.vue +0 -88
- package/dashboard/src/components/layout/AppSidebar.vue +0 -261
- package/dashboard/src/main.js +0 -44
- package/dashboard/src/router.js +0 -54
- package/dashboard/src/services/api.js +0 -187
- package/dashboard/src/services/websocket.js +0 -167
- package/dashboard/src/utils/constants.js +0 -56
- package/dashboard/src/utils/helpers.js +0 -118
- package/dashboard/src/views/Analytics.vue +0 -912
- package/dashboard/src/views/Dashboard.vue +0 -906
- package/dashboard/src/views/Messages.vue +0 -437
- package/dashboard/src/views/QueueDetail.vue +0 -501
- package/dashboard/src/views/Queues.vue +0 -333
- package/dashboard/vite.config.js +0 -30
- 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
|
+
|
package/src/client/client.js
CHANGED
|
@@ -212,12 +212,13 @@ export class Queen {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
|
-
*
|
|
216
|
-
* @
|
|
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 {
|
|
219
|
+
* @yields {Array} Arrays of message objects
|
|
219
220
|
*/
|
|
220
|
-
async
|
|
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
|
|
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 &&
|
|
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:
|
|
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
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
*
|
|
324
|
-
* @param {
|
|
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
|
|