queen-mq 0.1.0 → 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 (160) hide show
  1. package/API.md +862 -752
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +1705 -1051
  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-complete-client.js +260 -0
  17. package/examples/test-dashboard-api.js +200 -0
  18. package/examples/test-traceid.js +147 -0
  19. package/package.json +17 -4
  20. package/server.log +1 -0
  21. package/src/benchmark/consumer.js +207 -0
  22. package/src/benchmark/consumer_multi.js +216 -0
  23. package/src/benchmark/producer.js +75 -0
  24. package/src/benchmark/producer_multi.js +115 -0
  25. package/src/client/client.js +300 -31
  26. package/src/client/queenClient.js +5 -0
  27. package/src/cluster-server.js +242 -0
  28. package/src/config.js +19 -5
  29. package/src/database/connection.js +42 -16
  30. package/src/database/poolManager.js +7 -0
  31. package/src/database/schema-v2.sql +194 -130
  32. package/src/managers/queueManagerOptimized.js +823 -933
  33. package/src/managers/systemEventManager.js +8 -3
  34. package/src/routes/messages.js +127 -57
  35. package/src/routes/pop.js +27 -43
  36. package/src/routes/resources.js +61 -27
  37. package/src/routes/status.js +1037 -0
  38. package/src/server.js +308 -272
  39. package/src/services/evictionService.js +57 -28
  40. package/src/services/retentionService.js +44 -11
  41. package/src/test/MIGRATION_ISSUES.md +174 -0
  42. package/src/test/README.md +203 -0
  43. package/src/test/advanced-pattern-tests.js +1137 -0
  44. package/src/test/bus-mode-tests.js +361 -0
  45. package/src/test/core-tests.js +342 -0
  46. package/src/test/edge-case-tests.js +561 -0
  47. package/src/test/enterprise-tests.js +637 -0
  48. package/src/test/partition-locking-tests.js +545 -0
  49. package/src/test/test-new.js +278 -0
  50. package/src/test/test.js +6 -3
  51. package/src/test/utils.js +169 -0
  52. package/src/utils/streaming.js +231 -0
  53. package/src/utils/uuid.js +2 -2
  54. package/src/websocket/wsServer.js +10 -3
  55. package/test-keepalive-v2.sh +22 -0
  56. package/webapp/COLOR_GUIDE.md +118 -0
  57. package/webapp/README.md +143 -0
  58. package/webapp/index.html +14 -0
  59. package/webapp/package-lock.json +3184 -0
  60. package/webapp/package.json +25 -0
  61. package/webapp/postcss.config.js +7 -0
  62. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  63. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  64. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  65. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  66. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  67. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  68. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  69. package/webapp/public/assets/queen-logo.svg +263 -0
  70. package/webapp/src/App.vue +19 -0
  71. package/webapp/src/api/analytics.js +10 -0
  72. package/webapp/src/api/client.js +29 -0
  73. package/webapp/src/api/consumers.js +52 -0
  74. package/webapp/src/api/health.js +7 -0
  75. package/webapp/src/api/messages.js +26 -0
  76. package/webapp/src/api/queues.js +14 -0
  77. package/webapp/src/api/resources.js +8 -0
  78. package/webapp/src/assets/styles/main.css +357 -0
  79. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  80. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  81. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  82. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  83. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  84. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  85. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  86. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  87. package/webapp/src/components/common/MetricCard.vue +43 -0
  88. package/webapp/src/components/common/StatusBadge.vue +45 -0
  89. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  90. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  91. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  92. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  93. package/webapp/src/components/layout/AppLayout.vue +110 -0
  94. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  95. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  96. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  97. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  98. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  99. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  100. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  101. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  102. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  103. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  104. package/webapp/src/composables/useApi.js +34 -0
  105. package/webapp/src/composables/useTheme.js +36 -0
  106. package/webapp/src/main.js +11 -0
  107. package/webapp/src/router/index.js +42 -0
  108. package/webapp/src/utils/colors.js +96 -0
  109. package/webapp/src/utils/formatters.js +49 -0
  110. package/webapp/src/views/Analytics.vue +377 -0
  111. package/webapp/src/views/ConsumerGroups.vue +433 -0
  112. package/webapp/src/views/Dashboard.vue +418 -0
  113. package/webapp/src/views/Messages.vue +361 -0
  114. package/webapp/src/views/QueueDetail.vue +582 -0
  115. package/webapp/src/views/Queues.vue +496 -0
  116. package/webapp/tailwind.config.js +25 -0
  117. package/webapp/vite.config.js +10 -0
  118. package/CACHE.md +0 -519
  119. package/DASHBOARD-V3.md +0 -478
  120. package/DASHBOARD.md +0 -382
  121. package/MOD_QUEUE.md +0 -453
  122. package/PARTITION_LOCKING_DESIGN.md +0 -989
  123. package/PLAN.md +0 -707
  124. package/QUERY_ANALSYS.md +0 -72
  125. package/QUEUE_BUS.md +0 -334
  126. package/V2-PLAN.md +0 -236
  127. package/dashboard/.vscode/extensions.json +0 -3
  128. package/dashboard/README.md +0 -5
  129. package/dashboard/index.html +0 -14
  130. package/dashboard/package-lock.json +0 -1458
  131. package/dashboard/package.json +0 -25
  132. package/dashboard/public/vite.svg +0 -1
  133. package/dashboard/src/App.vue +0 -29
  134. package/dashboard/src/assets/styles/main.css +0 -908
  135. package/dashboard/src/assets/vue.svg +0 -1
  136. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  137. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  138. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  139. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  140. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  141. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  142. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  143. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  144. package/dashboard/src/main.js +0 -44
  145. package/dashboard/src/router.js +0 -54
  146. package/dashboard/src/services/api.js +0 -187
  147. package/dashboard/src/services/websocket.js +0 -167
  148. package/dashboard/src/utils/constants.js +0 -56
  149. package/dashboard/src/utils/helpers.js +0 -118
  150. package/dashboard/src/views/Analytics.vue +0 -912
  151. package/dashboard/src/views/Dashboard.vue +0 -906
  152. package/dashboard/src/views/Messages.vue +0 -437
  153. package/dashboard/src/views/QueueDetail.vue +0 -501
  154. package/dashboard/src/views/Queues.vue +0 -333
  155. package/dashboard/vite.config.js +0 -30
  156. package/debug-namespace.js +0 -110
  157. package/docs/long-polling.md +0 -159
  158. package/docs/multi-server-cache-solutions.md +0 -185
  159. package/docs/performance-tuning.md +0 -222
  160. package/src/routes/analytics.js +0 -812
@@ -55,6 +55,14 @@ export class Queen {
55
55
  this.#connected = true;
56
56
  }
57
57
 
58
+ /**
59
+ * Validate if a string is a valid UUID v4
60
+ */
61
+ #isValidUUID(str) {
62
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
63
+ return uuidRegex.test(str);
64
+ }
65
+
58
66
  /**
59
67
  * Parse address string into components
60
68
  * Examples:
@@ -62,9 +70,37 @@ export class Queen {
62
70
  * - "myqueue/urgent" -> { queue: "myqueue", partition: "urgent" }
63
71
  * - "myqueue@workers" -> { queue: "myqueue", consumerGroup: "workers" }
64
72
  * - "myqueue/urgent@workers" -> { queue: "myqueue", partition: "urgent", consumerGroup: "workers" }
73
+ * - "namespace:billing" -> { namespace: "billing" }
74
+ * - "task:process" -> { task: "process" }
75
+ * - "namespace:billing/task:process" -> { namespace: "billing", task: "process" }
65
76
  */
66
77
  #parseAddress(address) {
67
- // Match: queue[/partition][@group]
78
+ // Check for namespace/task pattern
79
+ if (address.includes('namespace:') || address.includes('task:')) {
80
+ const parts = {};
81
+
82
+ // Extract consumer group if present (after @)
83
+ let workingAddress = address;
84
+ const atIndex = address.lastIndexOf('@');
85
+ if (atIndex > 0) {
86
+ parts.consumerGroup = address.substring(atIndex + 1);
87
+ workingAddress = address.substring(0, atIndex);
88
+ }
89
+
90
+ const segments = workingAddress.split('/');
91
+
92
+ for (const segment of segments) {
93
+ if (segment.startsWith('namespace:')) {
94
+ parts.namespace = segment.substring(10);
95
+ } else if (segment.startsWith('task:')) {
96
+ parts.task = segment.substring(5);
97
+ }
98
+ }
99
+
100
+ return parts;
101
+ }
102
+
103
+ // Standard queue[/partition][@group] pattern
68
104
  const match = address.match(/^([^/@]+)(?:\/([^@]+))?(?:@(.+))?$/);
69
105
  if (!match) {
70
106
  throw new Error(`Invalid address format: ${address}`);
@@ -81,13 +117,18 @@ export class Queen {
81
117
  * Configure a queue with options
82
118
  * @param {string} name - Queue name
83
119
  * @param {Object} options - Queue configuration options
120
+ * @param {Object} metadata - Optional metadata { namespace, task }
84
121
  */
85
- async queue(name, options = {}) {
122
+ async queue(name, options = {}, metadata = {}) {
86
123
  await this.#ensureConnected();
87
124
 
125
+ const { namespace, task } = metadata;
126
+
88
127
  const result = await withRetry(
89
128
  () => this.#http.post('/api/v1/configure', {
90
129
  queue: name,
130
+ namespace: namespace || null,
131
+ task: task || null,
91
132
  options
92
133
  }),
93
134
  this.#config.retryAttempts,
@@ -105,8 +146,9 @@ export class Queen {
105
146
  * Push messages to a queue
106
147
  * @param {string} address - Queue address (e.g., "myqueue" or "myqueue/partition")
107
148
  * @param {Object|Array} payload - Single message or array of messages
149
+ * @param {Object} options - Optional message properties { transactionId, traceId }
108
150
  */
109
- async push(address, payload) {
151
+ async push(address, payload, options = {}) {
110
152
  await this.#ensureConnected();
111
153
 
112
154
  const { queue, partition } = this.#parseAddress(address);
@@ -115,12 +157,44 @@ export class Queen {
115
157
  const items = Array.isArray(payload) ? payload : [payload];
116
158
 
117
159
  // Format items for the API
118
- const formattedItems = items.map(item => ({
119
- queue,
120
- partition,
121
- payload: item,
122
- transactionId: item.transactionId // Optional, for idempotency
123
- }));
160
+ const formattedItems = items.map(item => {
161
+ // If item is an object with special properties, extract them
162
+ const isMessageObject = typeof item === 'object' && item !== null &&
163
+ (item._payload || item._transactionId || item._traceId);
164
+
165
+ if (isMessageObject) {
166
+ const result = {
167
+ queue,
168
+ partition,
169
+ payload: item._payload || item,
170
+ transactionId: item._transactionId || item.transactionId
171
+ };
172
+
173
+ // Include traceId if provided and valid UUID
174
+ const traceId = item._traceId || item.traceId;
175
+ if (traceId && this.#isValidUUID(traceId)) {
176
+ result.traceId = traceId;
177
+ }
178
+
179
+ return result;
180
+ }
181
+
182
+ // Otherwise use the item as payload and apply options
183
+ const result = {
184
+ queue,
185
+ partition,
186
+ payload: item,
187
+ transactionId: options.transactionId || (item && typeof item === 'object' ? item.transactionId : undefined)
188
+ };
189
+
190
+ // Include traceId if provided and valid UUID
191
+ const traceId = options.traceId || (item && typeof item === 'object' ? item.traceId : undefined);
192
+ if (traceId && this.#isValidUUID(traceId)) {
193
+ result.traceId = traceId;
194
+ }
195
+
196
+ return result;
197
+ });
124
198
 
125
199
  const result = await withRetry(
126
200
  () => this.#http.post('/api/v1/push', {
@@ -138,38 +212,52 @@ export class Queen {
138
212
  }
139
213
 
140
214
  /**
141
- * Take messages from a queue (async iterator)
142
- * @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
143
218
  * @param {Object} options - Options for taking messages
144
- * @yields {Object} Message objects
219
+ * @yields {Array} Arrays of message objects
145
220
  */
146
- async *take(address, options = {}) {
221
+ async *#takeInternal(address, options = {}) {
147
222
  await this.#ensureConnected();
148
223
 
149
- const { queue, partition, consumerGroup } = this.#parseAddress(address);
224
+ const { queue, partition, consumerGroup, namespace, task } = this.#parseAddress(address);
150
225
  const {
151
226
  limit = null,
152
227
  batch = 1,
153
228
  wait = false,
154
229
  timeout = 30000,
155
230
  subscriptionMode = null,
156
- subscriptionFrom = null
231
+ subscriptionFrom = null,
232
+ idleTimeout = null
157
233
  } = options;
158
234
 
159
- let count = 0;
235
+ let totalCount = 0;
160
236
  let consecutiveEmptyResponses = 0;
161
237
  const maxConsecutiveEmpty = 3;
238
+ let lastMessageTime = idleTimeout ? Date.now() : null;
162
239
 
163
240
  while (true) {
164
241
  // Check if we've reached the limit
165
- 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;
166
254
 
167
255
  // Build the request path and parameters
168
256
  let path;
169
257
  const params = new URLSearchParams({
170
258
  wait: wait.toString(),
171
259
  timeout: timeout.toString(),
172
- batch: Math.min(batch, limit ? limit - count : batch).toString()
260
+ batch: effectiveBatch.toString()
173
261
  });
174
262
 
175
263
  // Add consumer group parameters if provided
@@ -178,10 +266,19 @@ export class Queen {
178
266
  if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
179
267
 
180
268
  // Determine the endpoint based on parameters
181
- if (partition && partition !== 'Default') {
182
- path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
269
+ if (queue) {
270
+ if (partition && partition !== 'Default') {
271
+ path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
272
+ } else {
273
+ path = `/api/v1/pop/queue/${queue}`;
274
+ }
275
+ } else if (namespace || task) {
276
+ // Pop by namespace/task
277
+ path = '/api/v1/pop';
278
+ if (namespace) params.append('namespace', namespace);
279
+ if (task) params.append('task', task);
183
280
  } else {
184
- path = `/api/v1/pop/queue/${queue}`;
281
+ throw new Error('Must specify either queue, namespace, or task');
185
282
  }
186
283
 
187
284
  try {
@@ -210,11 +307,22 @@ export class Queen {
210
307
  // Reset empty counter on successful fetch
211
308
  consecutiveEmptyResponses = 0;
212
309
 
213
- // Yield each message
214
- for (const message of result.messages) {
215
- yield message;
216
- count++;
217
- 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;
325
+ }
218
326
  }
219
327
 
220
328
  } catch (error) {
@@ -235,18 +343,154 @@ export class Queen {
235
343
  }
236
344
 
237
345
  /**
238
- * Acknowledge a message
239
- * @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
240
386
  * @param {boolean|string} status - true for success, false for failure, or 'retry'
241
387
  * @param {Object} context - Optional context (e.g., { group: 'workers', error: 'reason' })
242
388
  */
243
389
  async ack(message, status = true, context = {}) {
244
390
  await this.#ensureConnected();
245
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
246
482
  // Extract transaction ID
247
- const transactionId = typeof message === 'object' ?
248
- (message.transactionId || message.id) :
249
- message;
483
+ let transactionId;
484
+ if (typeof message === 'string') {
485
+ transactionId = message;
486
+ } else if (typeof message === 'object' && message !== null) {
487
+ transactionId = message.transactionId || message.id;
488
+ if (!transactionId) {
489
+ throw new Error('Message object must have transactionId or id property');
490
+ }
491
+ } else {
492
+ throw new Error('Message must be a string (transaction ID) or object with transactionId');
493
+ }
250
494
 
251
495
  // Determine status string
252
496
  let statusStr;
@@ -277,6 +521,31 @@ export class Queen {
277
521
  return result;
278
522
  }
279
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
+
280
549
  /**
281
550
  * Close the client connection
282
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
 
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Queen Message Queue - Clustered Server
5
+ *
6
+ * This module enables multi-core utilization using Node.js cluster module.
7
+ * Each worker runs an independent instance of the Queen server, sharing the same port.
8
+ *
9
+ * Benefits:
10
+ * - 10x throughput improvement (utilizes all CPU cores)
11
+ * - Automatic load balancing across workers
12
+ * - Fault tolerance (automatic worker restart on crash)
13
+ * - Zero-downtime deploys (rolling worker restarts)
14
+ *
15
+ * Usage:
16
+ * node src/cluster-server.js
17
+ *
18
+ * Environment Variables:
19
+ * QUEEN_WORKERS=N - Number of workers (default: CPU count)
20
+ * WORKER_ID=name - Base worker ID prefix (default: queen-worker)
21
+ */
22
+
23
+ import cluster from 'cluster';
24
+ import os from 'os';
25
+ import { log } from './utils/logger.js';
26
+
27
+ // Configuration
28
+ const numWorkers = parseInt(process.env.QUEEN_WORKERS) || os.cpus().length;
29
+ const workerIdPrefix = process.env.WORKER_ID || 'queen-worker';
30
+
31
+ if (cluster.isPrimary) {
32
+ console.log('');
33
+ console.log('═══════════════════════════════════════════════════════════');
34
+ console.log('🚀 Queen Message Queue - Clustered Mode');
35
+ console.log('═══════════════════════════════════════════════════════════');
36
+ console.log(`📊 System: ${os.cpus()[0].model}`);
37
+ console.log(`🔢 CPU Cores: ${os.cpus().length}`);
38
+ console.log(`👷 Starting Workers: ${numWorkers}`);
39
+ console.log(`🆔 Worker ID Prefix: ${workerIdPrefix}`);
40
+ console.log('═══════════════════════════════════════════════════════════');
41
+ console.log('');
42
+
43
+ // Track worker statistics
44
+ const workerStats = {
45
+ startTime: Date.now(),
46
+ totalRestarts: 0,
47
+ workers: new Map()
48
+ };
49
+
50
+ // Graceful shutdown state (declared early for use in event handlers)
51
+ let isShuttingDown = false;
52
+ let forceExitTimeout = null;
53
+ let statusInterval = null;
54
+
55
+ // Fork workers
56
+ for (let i = 0; i < numWorkers; i++) {
57
+ forkWorker(i, workerStats);
58
+ }
59
+
60
+ // Handle worker exit
61
+ cluster.on('exit', (worker, code, signal) => {
62
+ const workerInfo = workerStats.workers.get(worker.id);
63
+ const workerId = workerInfo ? workerInfo.workerId : `worker-${worker.id}`;
64
+ const workerIndex = workerInfo ? workerInfo.index : 0;
65
+
66
+ if (code !== 0 && !worker.exitedAfterDisconnect) {
67
+ // Unexpected crash
68
+ console.log('');
69
+ console.log(`⚠️ Worker ${workerId} (PID: ${worker.process.pid}) crashed!`);
70
+ console.log(` Exit code: ${code}, Signal: ${signal}`);
71
+ console.log(` Restarting worker...`);
72
+
73
+ workerStats.totalRestarts++;
74
+
75
+ // Remove from stats
76
+ workerStats.workers.delete(worker.id);
77
+
78
+ // Restart with same index
79
+ setTimeout(() => {
80
+ forkWorker(workerIndex, workerStats);
81
+ }, 1000); // Wait 1 second before restart
82
+ } else {
83
+ // Graceful shutdown
84
+ console.log(`✅ Worker ${workerId} (PID: ${worker.process.pid}) exited gracefully`);
85
+ workerStats.workers.delete(worker.id);
86
+
87
+ // Check if all workers have exited during shutdown
88
+ if (isShuttingDown && Object.keys(cluster.workers).length === 0) {
89
+ console.log('✅ All workers exited, shutting down master process');
90
+ clearInterval(statusInterval);
91
+ clearTimeout(forceExitTimeout);
92
+ process.exit(0);
93
+ }
94
+ }
95
+ });
96
+
97
+ // Handle worker online
98
+ cluster.on('online', (worker) => {
99
+ const workerInfo = workerStats.workers.get(worker.id);
100
+ if (workerInfo) {
101
+ // Update PID now that it's available
102
+ workerInfo.pid = worker.process.pid;
103
+ console.log(`✅ Worker ${workerInfo.workerId} (PID: ${worker.process.pid}) online and ready`);
104
+ } else {
105
+ console.log(`✅ Worker ${worker.id} (PID: ${worker.process.pid}) online`);
106
+ }
107
+ });
108
+
109
+ // Handle worker listening
110
+ cluster.on('listening', (worker, address) => {
111
+ const workerInfo = workerStats.workers.get(worker.id);
112
+ const workerId = workerInfo ? workerInfo.workerId : `worker-${worker.id}`;
113
+ console.log(`🎧 Worker ${workerId} listening on ${address.address}:${address.port}`);
114
+ });
115
+
116
+ // Handle cluster errors
117
+ cluster.on('error', (error) => {
118
+ console.error('⚠️ Cluster error:', error.message);
119
+ });
120
+
121
+ // Handle uncaught exceptions in master
122
+ process.on('uncaughtException', (error) => {
123
+ console.error('❌ Uncaught exception in master process:', error);
124
+ console.error(error.stack);
125
+ // Don't exit - try to keep the cluster running
126
+ });
127
+
128
+ process.on('unhandledRejection', (reason, promise) => {
129
+ console.error('❌ Unhandled rejection in master process:', reason);
130
+ // Don't exit - try to keep the cluster running
131
+ });
132
+
133
+ // Graceful shutdown handling
134
+ const gracefulShutdown = (signal) => {
135
+ if (isShuttingDown) return;
136
+ isShuttingDown = true;
137
+
138
+ console.log('');
139
+ console.log(`🛑 Received ${signal}, shutting down gracefully...`);
140
+ console.log(`📊 Total worker restarts during uptime: ${workerStats.totalRestarts}`);
141
+ console.log(`⏱️ Uptime: ${((Date.now() - workerStats.startTime) / 1000 / 60).toFixed(2)} minutes`);
142
+
143
+ // Stop status reporting during shutdown
144
+ clearInterval(statusInterval);
145
+
146
+ // Check if we already have no workers
147
+ if (Object.keys(cluster.workers).length === 0) {
148
+ console.log('✅ No active workers, exiting immediately');
149
+ process.exit(0);
150
+ }
151
+
152
+ // Disconnect all workers
153
+ for (const id in cluster.workers) {
154
+ cluster.workers[id].disconnect();
155
+ }
156
+
157
+ // Force exit if workers don't shut down in 10 seconds (reduced from 30)
158
+ forceExitTimeout = setTimeout(() => {
159
+ console.log('⚠️ Forcing shutdown after 10 second timeout');
160
+ console.log(` Remaining workers: ${Object.keys(cluster.workers).length}`);
161
+ process.exit(0);
162
+ }, 10000);
163
+ };
164
+
165
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
166
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
167
+
168
+ // Status reporting every 60 seconds
169
+ statusInterval = setInterval(() => {
170
+ // Don't show status during shutdown
171
+ if (isShuttingDown) return;
172
+
173
+ const aliveWorkers = Object.keys(cluster.workers).length;
174
+ const uptime = ((Date.now() - workerStats.startTime) / 1000 / 60).toFixed(2);
175
+
176
+ console.log('');
177
+ console.log(`📊 Cluster Status (${new Date().toISOString()})`);
178
+ console.log(` Active Workers: ${aliveWorkers}/${numWorkers}`);
179
+ console.log(` Total Restarts: ${workerStats.totalRestarts}`);
180
+ console.log(` Uptime: ${uptime} minutes`);
181
+ }, 60000);
182
+
183
+ } else {
184
+ // Worker process - run the normal server
185
+ const workerId = process.env.WORKER_ID || `worker-${process.pid}`;
186
+ const workerIndex = process.env.WORKER_INDEX || '0';
187
+
188
+ console.log(`👷 Worker ${workerId} (index: ${workerIndex}, PID: ${process.pid}) starting...`);
189
+
190
+ try {
191
+ // Import and run the main server
192
+ await import('./server.js');
193
+
194
+ // Worker-specific metrics could be added here
195
+ // e.g., track messages processed, errors, etc.
196
+ } catch (error) {
197
+ console.error(`❌ Worker ${workerId} failed to start:`, error.message);
198
+ console.error(error.stack);
199
+ process.exit(1);
200
+ }
201
+
202
+ // Handle uncaught exceptions in worker
203
+ process.on('uncaughtException', (error) => {
204
+ console.error(`❌ Uncaught exception in worker ${workerId}:`, error);
205
+ process.exit(1);
206
+ });
207
+
208
+ process.on('unhandledRejection', (reason, promise) => {
209
+ console.error(`❌ Unhandled rejection in worker ${workerId}:`, reason);
210
+ process.exit(1);
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Fork a new worker with proper environment setup
216
+ */
217
+ function forkWorker(index, stats) {
218
+ const workerId = `${workerIdPrefix}-${index}`;
219
+
220
+ try {
221
+ const worker = cluster.fork({
222
+ WORKER_ID: workerId,
223
+ WORKER_INDEX: index.toString(),
224
+ // Each worker gets a unique ID for system events
225
+ SYSTEM_WORKER_ID: `${workerIdPrefix}-${index}-${Date.now()}`
226
+ });
227
+
228
+ // Store worker info immediately (PID will be updated in 'online' event)
229
+ stats.workers.set(worker.id, {
230
+ workerId,
231
+ index,
232
+ startTime: Date.now(),
233
+ pid: null // Will be set when worker comes online
234
+ });
235
+
236
+ return worker;
237
+ } catch (error) {
238
+ console.error(`❌ Failed to fork worker ${workerId}:`, error.message);
239
+ return null;
240
+ }
241
+ }
242
+