queen-mq 0.1.5 → 0.2.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 (135) hide show
  1. package/README.md +467 -1775
  2. package/package.json +12 -14
  3. package/src/client/client.js +664 -5
  4. package/src/managers/queueManagerOptimized.js +250 -47
  5. package/src/routes/ack.js +3 -3
  6. package/src/server.js +131 -3
  7. package/src/test/advanced-client-tests.js +761 -0
  8. package/src/test/advanced-pattern-tests.js +2 -2
  9. package/src/test/bus-mode-tests.js +6 -6
  10. package/src/test/core-tests.js +1 -1
  11. package/src/test/edge-case-tests.js +2 -2
  12. package/src/test/test-new.js +36 -1
  13. package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +1 -0
  14. package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +1 -0
  15. package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +1 -0
  16. package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +1 -0
  17. package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +1 -0
  18. package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +1 -0
  19. package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +1 -0
  20. package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +1 -0
  21. package/src/webapp-dist/assets/Messages-BOfVHKma.css +1 -0
  22. package/src/webapp-dist/assets/Messages-EKURCbLl.js +1 -0
  23. package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +1 -0
  24. package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +1 -0
  25. package/src/webapp-dist/assets/Queues-CvrJkrZT.css +1 -0
  26. package/src/webapp-dist/assets/Queues-yUx9_3-9.js +1 -0
  27. package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +1 -0
  28. package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +1 -0
  29. package/src/webapp-dist/assets/analytics-BapTekLM.js +1 -0
  30. package/src/webapp-dist/assets/colors-5z6Q_kUr.js +18 -0
  31. package/src/webapp-dist/assets/index-B7w2bKT6.css +1 -0
  32. package/src/webapp-dist/assets/index-CUSnwYTH.js +31 -0
  33. package/src/webapp-dist/assets/messages-CRV6E-xU.js +1 -0
  34. package/src/webapp-dist/assets/queues-BdUbxfBC.js +1 -0
  35. package/src/webapp-dist/assets/resources-Bs9U-ceF.js +1 -0
  36. package/{webapp → src/webapp-dist}/index.html +2 -1
  37. package/API.md +0 -1226
  38. package/AUTH.md +0 -2044
  39. package/CHANGELOG_STATIC_SERVING.md +0 -213
  40. package/DEPLOYMENT.md +0 -425
  41. package/PROGRAMMATIC_SERVER.md +0 -239
  42. package/WEBAPP.md +0 -1889
  43. package/assets/dashboard-01.png +0 -0
  44. package/assets/dashboard.png +0 -0
  45. package/examples/batch-processing.js +0 -58
  46. package/examples/bus-mode.js +0 -239
  47. package/examples/continuous-consumer-optimized.js +0 -215
  48. package/examples/continuous-consumer.js +0 -159
  49. package/examples/continuous-producer.js +0 -343
  50. package/examples/mixed-mode.js +0 -277
  51. package/examples/multi-server-test.js +0 -305
  52. package/examples/programmatic-server.js +0 -58
  53. package/examples/server-with-frontend.js +0 -46
  54. package/examples/single.js +0 -64
  55. package/examples/smartchat-dealyed.js +0 -42
  56. package/examples/smartchat.js +0 -52
  57. package/examples/test-cache-invalidation.js +0 -119
  58. package/examples/test-cache-multi-server.js +0 -245
  59. package/examples/test-complete-client.js +0 -260
  60. package/examples/test-connection-recovery.js +0 -66
  61. package/examples/test-dashboard-api.js +0 -200
  62. package/examples/test-minimal-client.js +0 -112
  63. package/examples/test-queue-creation-policy.js +0 -137
  64. package/examples/test-traceid.js +0 -147
  65. package/server.log +0 -1
  66. package/test-keepalive-v2.sh +0 -22
  67. package/webapp/COLOR_GUIDE.md +0 -118
  68. package/webapp/README.md +0 -143
  69. package/webapp/package-lock.json +0 -3184
  70. package/webapp/package.json +0 -25
  71. package/webapp/postcss.config.js +0 -7
  72. package/webapp/public/assets/queen-logo-blue.svg +0 -210
  73. package/webapp/public/assets/queen-logo-cyan.svg +0 -210
  74. package/webapp/public/assets/queen-logo-indigo.svg +0 -210
  75. package/webapp/public/assets/queen-logo-orange.svg +0 -210
  76. package/webapp/public/assets/queen-logo-pink.svg +0 -210
  77. package/webapp/public/assets/queen-logo-purple.svg +0 -210
  78. package/webapp/public/assets/queen-logo-rose.svg +0 -239
  79. package/webapp/public/assets/queen-logo.svg +0 -263
  80. package/webapp/src/App.vue +0 -19
  81. package/webapp/src/api/analytics.js +0 -10
  82. package/webapp/src/api/client.js +0 -34
  83. package/webapp/src/api/consumers.js +0 -52
  84. package/webapp/src/api/health.js +0 -7
  85. package/webapp/src/api/messages.js +0 -26
  86. package/webapp/src/api/queues.js +0 -14
  87. package/webapp/src/api/resources.js +0 -8
  88. package/webapp/src/assets/styles/main.css +0 -357
  89. package/webapp/src/components/analytics/AnalyticsFilters.vue +0 -87
  90. package/webapp/src/components/analytics/AnalyticsMetrics.vue +0 -57
  91. package/webapp/src/components/analytics/MessageDistributionChart.vue +0 -111
  92. package/webapp/src/components/analytics/MessageFlowChart.vue +0 -173
  93. package/webapp/src/components/analytics/TimeRangeSelector.vue +0 -27
  94. package/webapp/src/components/analytics/TopQueuesChart.vue +0 -132
  95. package/webapp/src/components/common/ConfirmDialog.vue +0 -56
  96. package/webapp/src/components/common/LoadingSpinner.vue +0 -6
  97. package/webapp/src/components/common/MetricCard.vue +0 -43
  98. package/webapp/src/components/common/StatusBadge.vue +0 -45
  99. package/webapp/src/components/dashboard/MessageStatusCard.vue +0 -50
  100. package/webapp/src/components/dashboard/PerformanceCard.vue +0 -38
  101. package/webapp/src/components/dashboard/ThroughputChart.vue +0 -182
  102. package/webapp/src/components/dashboard/TopQueuesTable.vue +0 -53
  103. package/webapp/src/components/layout/AppLayout.vue +0 -110
  104. package/webapp/src/components/layout/AppSidebar.vue +0 -304
  105. package/webapp/src/components/messages/MessageDetailPanel.vue +0 -242
  106. package/webapp/src/components/messages/MessageFilters.vue +0 -101
  107. package/webapp/src/components/queue-detail/PartitionList.vue +0 -79
  108. package/webapp/src/components/queue-detail/PushMessageModal.vue +0 -175
  109. package/webapp/src/components/queue-detail/QueueConfig.vue +0 -63
  110. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +0 -53
  111. package/webapp/src/components/queue-detail/RecentMessages.vue +0 -76
  112. package/webapp/src/components/queues/CreateQueueModal.vue +0 -193
  113. package/webapp/src/components/queues/QueueFilters.vue +0 -90
  114. package/webapp/src/composables/useApi.js +0 -34
  115. package/webapp/src/composables/useTheme.js +0 -36
  116. package/webapp/src/main.js +0 -11
  117. package/webapp/src/router/index.js +0 -42
  118. package/webapp/src/utils/colors.js +0 -96
  119. package/webapp/src/utils/formatters.js +0 -49
  120. package/webapp/src/views/Analytics.vue +0 -377
  121. package/webapp/src/views/ConsumerGroups.vue +0 -433
  122. package/webapp/src/views/Dashboard.vue +0 -418
  123. package/webapp/src/views/Messages.vue +0 -363
  124. package/webapp/src/views/QueueDetail.vue +0 -582
  125. package/webapp/src/views/Queues.vue +0 -496
  126. package/webapp/tailwind.config.js +0 -25
  127. package/webapp/vite.config.js +0 -10
  128. /package/{assets → src/webapp-dist/assets}/queen-logo-blue.svg +0 -0
  129. /package/{assets → src/webapp-dist/assets}/queen-logo-cyan.svg +0 -0
  130. /package/{assets → src/webapp-dist/assets}/queen-logo-indigo.svg +0 -0
  131. /package/{assets → src/webapp-dist/assets}/queen-logo-orange.svg +0 -0
  132. /package/{assets → src/webapp-dist/assets}/queen-logo-pink.svg +0 -0
  133. /package/{assets → src/webapp-dist/assets}/queen-logo-purple.svg +0 -0
  134. /package/{assets → src/webapp-dist/assets}/queen-logo-rose.svg +0 -0
  135. /package/{assets → src/webapp-dist/assets}/queen-logo.svg +0 -0
@@ -406,6 +406,74 @@ export class Queen {
406
406
  }
407
407
  }
408
408
 
409
+ /**
410
+ * Take a single batch of messages (convenience method)
411
+ * @param {string} address - Queue address
412
+ * @param {Object} options - Options for taking messages
413
+ * @returns {Promise<Array>} Array of messages
414
+ */
415
+ async takeSingleBatch(address, options = {}) {
416
+ const messages = [];
417
+ for await (const batch of this.takeBatch(address, options)) {
418
+ messages.push(...batch);
419
+ break; // Take only one batch
420
+ }
421
+ return messages;
422
+ }
423
+
424
+ /**
425
+ * Renew lease for a message or batch of messages
426
+ * @param {Object|Array|string} messageOrLeaseId - Message object(s) or lease ID(s)
427
+ * @returns {Promise<Object>} Renewal result
428
+ */
429
+ async renewLease(messageOrLeaseId) {
430
+ await this.#ensureConnected();
431
+
432
+ // Handle different input types
433
+ let leaseIds = [];
434
+
435
+ if (typeof messageOrLeaseId === 'string') {
436
+ // Direct lease ID
437
+ leaseIds = [messageOrLeaseId];
438
+ } else if (Array.isArray(messageOrLeaseId)) {
439
+ // Array of messages or lease IDs
440
+ leaseIds = messageOrLeaseId.map(item =>
441
+ typeof item === 'string' ? item : item.leaseId
442
+ ).filter(Boolean);
443
+ } else if (messageOrLeaseId && typeof messageOrLeaseId === 'object') {
444
+ // Single message object
445
+ if (messageOrLeaseId.leaseId) {
446
+ leaseIds = [messageOrLeaseId.leaseId];
447
+ }
448
+ }
449
+
450
+ if (leaseIds.length === 0) {
451
+ throw new Error('No valid lease IDs found for renewal');
452
+ }
453
+
454
+ // Renew all leases
455
+ const results = [];
456
+ for (const leaseId of leaseIds) {
457
+ try {
458
+ const result = await withRetry(
459
+ () => this.#http.post(`/api/v1/lease/${leaseId}/extend`, {}),
460
+ this.#config.retryAttempts,
461
+ this.#config.retryDelay
462
+ );
463
+ results.push({
464
+ leaseId,
465
+ success: true,
466
+ newExpiresAt: result.leaseId ? result.newExpiresAt : result.lease_expires_at
467
+ });
468
+ } catch (error) {
469
+ results.push({ leaseId, success: false, error: error.message });
470
+ }
471
+ }
472
+
473
+ // Return single result for single input, array for array input
474
+ return Array.isArray(messageOrLeaseId) ? results : results[0];
475
+ }
476
+
409
477
  /**
410
478
  * Acknowledge a message or batch of messages
411
479
  * @param {Object|string|Array} message - Message object, transaction ID, or array of messages
@@ -431,12 +499,15 @@ export class Queen {
431
499
  if (hasIndividualStatus) {
432
500
  // Option B: Each message has its own status
433
501
  acknowledgments = message.map(msg => {
434
- // Extract transaction ID
502
+ // Extract transaction ID and lease ID
435
503
  let transactionId;
504
+ let leaseId = null;
505
+
436
506
  if (typeof msg === 'string') {
437
507
  transactionId = msg;
438
508
  } else if (typeof msg === 'object' && msg !== null) {
439
509
  transactionId = msg.transactionId || msg.id;
510
+ leaseId = msg.leaseId || null;
440
511
  if (!transactionId) {
441
512
  throw new Error('Message object must have transactionId or id property');
442
513
  }
@@ -453,11 +524,18 @@ export class Queen {
453
524
  statusStr = msgStatus;
454
525
  }
455
526
 
456
- return {
527
+ const ack = {
457
528
  transactionId,
458
529
  status: statusStr,
459
530
  error: msg._error || context.error || null
460
531
  };
532
+
533
+ // Include lease ID if available
534
+ if (leaseId) {
535
+ ack.leaseId = leaseId;
536
+ }
537
+
538
+ return ack;
461
539
  });
462
540
  } else {
463
541
  // Option A: Same status for all messages
@@ -466,12 +544,15 @@ export class Queen {
466
544
  : status;
467
545
 
468
546
  acknowledgments = message.map(msg => {
469
- // Extract transaction ID
547
+ // Extract transaction ID and lease ID
470
548
  let transactionId;
549
+ let leaseId = null;
550
+
471
551
  if (typeof msg === 'string') {
472
552
  transactionId = msg;
473
553
  } else if (typeof msg === 'object' && msg !== null) {
474
554
  transactionId = msg.transactionId || msg.id;
555
+ leaseId = msg.leaseId || null;
475
556
  if (!transactionId) {
476
557
  throw new Error('Message object must have transactionId or id property');
477
558
  }
@@ -479,11 +560,18 @@ export class Queen {
479
560
  throw new Error('Invalid message in batch');
480
561
  }
481
562
 
482
- return {
563
+ const ack = {
483
564
  transactionId,
484
565
  status: statusStr,
485
566
  error: context.error || null
486
567
  };
568
+
569
+ // Include lease ID if available
570
+ if (leaseId) {
571
+ ack.leaseId = leaseId;
572
+ }
573
+
574
+ return ack;
487
575
  });
488
576
  }
489
577
 
@@ -505,12 +593,15 @@ export class Queen {
505
593
  }
506
594
 
507
595
  // Handle single message acknowledgment
508
- // Extract transaction ID
596
+ // Extract transaction ID and lease ID
509
597
  let transactionId;
598
+ let leaseId = null;
599
+
510
600
  if (typeof message === 'string') {
511
601
  transactionId = message;
512
602
  } else if (typeof message === 'object' && message !== null) {
513
603
  transactionId = message.transactionId || message.id;
604
+ leaseId = message.leaseId || null; // Extract lease ID if present
514
605
  if (!transactionId) {
515
606
  throw new Error('Message object must have transactionId or id property');
516
607
  }
@@ -534,6 +625,11 @@ export class Queen {
534
625
  consumerGroup: context.group || null
535
626
  };
536
627
 
628
+ // Include lease ID if available for lease validation
629
+ if (leaseId) {
630
+ body.leaseId = leaseId;
631
+ }
632
+
537
633
  const result = await withRetry(
538
634
  () => this.#http.post('/api/v1/ack', body),
539
635
  this.#config.retryAttempts,
@@ -580,6 +676,569 @@ export class Queen {
580
676
  this.#http = null;
581
677
  this.#loadBalancer = null;
582
678
  }
679
+
680
+ /**
681
+ * Create a transaction builder for atomic operations
682
+ * @returns {TransactionBuilder} Transaction builder instance
683
+ */
684
+ transaction() {
685
+ return new TransactionBuilder(this);
686
+ }
687
+
688
+ /**
689
+ * Create a pipeline for message processing workflows
690
+ * @param {string} queue - Source queue name
691
+ * @returns {PipelineBuilder} Pipeline builder instance
692
+ */
693
+ pipeline(queue) {
694
+ return new PipelineBuilder(this, queue);
695
+ }
696
+
697
+ // Internal helper methods for Transaction and Pipeline
698
+ async _ensureConnected() {
699
+ return this.#ensureConnected();
700
+ }
701
+
702
+ get _http() {
703
+ return this.#http;
704
+ }
705
+ }
706
+
707
+ /**
708
+ * Transaction Builder for atomic operations
709
+ */
710
+ class TransactionBuilder {
711
+ #client;
712
+ #operations = [];
713
+ #requiredLeases = [];
714
+
715
+ constructor(client) {
716
+ this.#client = client;
717
+ }
718
+
719
+ /**
720
+ * Add ACK operation to transaction
721
+ * @param {Array|Object} messages - Messages to acknowledge
722
+ * @param {string} status - Status ('completed' or 'failed')
723
+ * @returns {TransactionBuilder} this for chaining
724
+ */
725
+ ack(messages, status = 'completed') {
726
+ const msgs = Array.isArray(messages) ? messages : [messages];
727
+
728
+ msgs.forEach(msg => {
729
+ const transactionId = msg.transactionId || msg.id || msg;
730
+ const leaseId = msg.leaseId || null;
731
+
732
+ this.#operations.push({
733
+ type: 'ack',
734
+ transactionId,
735
+ status
736
+ });
737
+
738
+ if (leaseId) {
739
+ this.#requiredLeases.push(leaseId);
740
+ }
741
+ });
742
+
743
+ return this;
744
+ }
745
+
746
+ /**
747
+ * Add PUSH operation to transaction
748
+ * @param {string} queue - Target queue
749
+ * @param {Array|Object} items - Items to push
750
+ * @returns {TransactionBuilder} this for chaining
751
+ */
752
+ push(queue, items) {
753
+ const itemArray = Array.isArray(items) ? items : [items];
754
+
755
+ this.#operations.push({
756
+ type: 'push',
757
+ items: itemArray.map(item => ({
758
+ queue,
759
+ payload: item
760
+ }))
761
+ });
762
+
763
+ return this;
764
+ }
765
+
766
+ /**
767
+ * Add lease extension to transaction
768
+ * @param {string} leaseId - Lease ID to extend
769
+ * @returns {TransactionBuilder} this for chaining
770
+ */
771
+ extend(leaseId) {
772
+ this.#operations.push({
773
+ type: 'extend',
774
+ leaseId
775
+ });
776
+
777
+ this.#requiredLeases.push(leaseId);
778
+ return this;
779
+ }
780
+
781
+ /**
782
+ * Execute the transaction
783
+ * @returns {Promise<Object>} Transaction result
784
+ */
785
+ async commit() {
786
+ await this.#client._ensureConnected();
787
+
788
+ const result = await this.#client._http.post('/api/v1/transaction', {
789
+ operations: this.#operations,
790
+ requiredLeases: [...new Set(this.#requiredLeases)] // Unique leases
791
+ });
792
+
793
+ if (!result.success) {
794
+ throw new Error(result.error || 'Transaction failed');
795
+ }
796
+
797
+ return result;
798
+ }
799
+ }
800
+
801
+ /**
802
+ * Pipeline Builder for message processing workflows
803
+ */
804
+ class PipelineBuilder {
805
+ #client;
806
+ #queue;
807
+ #options = {};
808
+ #processor = null;
809
+ #errorHandler = null;
810
+ #atomicOps = null;
811
+ #leaseRenewal = null;
812
+ #repeatConfig = null;
813
+ #concurrency = 1;
814
+
815
+ constructor(client, queue) {
816
+ this.#client = client;
817
+ this.#queue = queue;
818
+ }
819
+
820
+ /**
821
+ * Take messages from the queue
822
+ * @param {number} count - Number of messages to take
823
+ * @param {Object} options - Take options
824
+ * @returns {PipelineBuilder} this for chaining
825
+ */
826
+ take(count, options = {}) {
827
+ this.#options = { ...options, batch: count, limit: count };
828
+ return this;
829
+ }
830
+
831
+ /**
832
+ * Process messages individually (one at a time)
833
+ * @param {Function} handler - Async function to process a single message
834
+ * @returns {PipelineBuilder} this for chaining
835
+ */
836
+ process(handler) {
837
+ // Wrap the single-message handler to process messages one by one
838
+ this.#processor = async (messages) => {
839
+ const results = [];
840
+ for (const message of messages) {
841
+ const result = await handler(message);
842
+ results.push(result);
843
+ }
844
+ return results;
845
+ };
846
+ return this;
847
+ }
848
+
849
+ /**
850
+ * Process messages as a batch
851
+ * @param {Function} handler - Async function to process a batch of messages
852
+ * @returns {PipelineBuilder} this for chaining
853
+ */
854
+ processBatch(handler) {
855
+ this.#processor = handler;
856
+ return this;
857
+ }
858
+
859
+ /**
860
+ * Define atomic operations to execute after processing
861
+ * @param {Function} txBuilder - Function that receives a TransactionBuilder
862
+ * @returns {PipelineBuilder} this for chaining
863
+ */
864
+ atomically(txBuilder) {
865
+ this.#atomicOps = txBuilder;
866
+ return this;
867
+ }
868
+
869
+ /**
870
+ * Handle errors
871
+ * @param {Function} handler - Error handler function
872
+ * @returns {PipelineBuilder} this for chaining
873
+ */
874
+ onError(handler) {
875
+ this.#errorHandler = handler;
876
+ return this;
877
+ }
878
+
879
+ /**
880
+ * Enable automatic lease renewal
881
+ * @param {Object} options - Renewal options
882
+ * @returns {PipelineBuilder} this for chaining
883
+ */
884
+ withAutoRenewal(options = {}) {
885
+ this.#leaseRenewal = {
886
+ interval: options.interval || 30000, // Renew every 30s by default
887
+ enabled: true
888
+ };
889
+ return this;
890
+ }
891
+
892
+ /**
893
+ * Set concurrency level for parallel batch processing
894
+ * @param {number} level - Number of concurrent batches to process
895
+ * @returns {PipelineBuilder} this for chaining
896
+ */
897
+ withConcurrency(level) {
898
+ this.#concurrency = Math.max(1, level);
899
+ return this;
900
+ }
901
+
902
+ /**
903
+ * Configure repeated execution
904
+ * @param {Object} options - Repeat options
905
+ * @returns {PipelineBuilder} this for chaining
906
+ */
907
+ repeat(options = {}) {
908
+ this.#repeatConfig = {
909
+ maxIterations: options.maxIterations || Infinity,
910
+ delay: options.delay || 0,
911
+ continuous: options.continuous !== undefined ? options.continuous : true, // Default to continuous mode
912
+ waitOnEmpty: options.waitOnEmpty || 1000 // Wait time when no messages
913
+ };
914
+ return this;
915
+ }
916
+
917
+ /**
918
+ * Execute the pipeline (once or repeatedly based on configuration)
919
+ * @param {Object} options - Execution options
920
+ * @param {boolean} options.returnGenerator - For repeat mode, return generator instead of running loop
921
+ * @returns {Promise<Object>} Result or summary of all iterations
922
+ */
923
+ async execute(options = {}) {
924
+ // If repeat is configured
925
+ if (this.#repeatConfig) {
926
+ // Option to return generator for advanced users
927
+ if (options.returnGenerator) {
928
+ return this.#executeRepeatedly();
929
+ }
930
+
931
+ // Default: run the loop internally and return summary
932
+ return this.#executeRepeatedlyWithSummary();
933
+ }
934
+
935
+ // Otherwise execute once
936
+ return this.#executeSingle();
937
+ }
938
+
939
+ /**
940
+ * Execute the pipeline once (internal)
941
+ * @private
942
+ * @returns {Promise<Object>} Pipeline execution result
943
+ */
944
+ async #executeSingle() {
945
+ // Use local variables to avoid race conditions in parallel execution
946
+ let messages = null;
947
+ let processedMessages = null;
948
+
949
+ try {
950
+ // 1. Take messages
951
+ messages = await this.#client.takeSingleBatch(this.#queue, this.#options);
952
+
953
+ if (!messages || messages.length === 0) {
954
+ return { processed: 0, messages: [] };
955
+ }
956
+
957
+ // 2. Set up auto-renewal if enabled
958
+ let renewalTimer = null;
959
+ if (this.#leaseRenewal?.enabled) {
960
+ renewalTimer = this.#setupAutoRenewal(messages);
961
+ }
962
+
963
+ try {
964
+ // 3. Process messages
965
+ if (this.#processor) {
966
+ processedMessages = await this.#processor(messages);
967
+ } else {
968
+ processedMessages = messages;
969
+ }
970
+
971
+ // 4. Execute atomic operations
972
+ if (this.#atomicOps) {
973
+ const tx = new TransactionBuilder(this.#client);
974
+
975
+ // Apply the atomic operations
976
+ this.#atomicOps(tx, messages, processedMessages);
977
+
978
+ await tx.commit();
979
+ } else {
980
+ // Default: just ACK the messages
981
+ await this.#client.ack(messages, true);
982
+ }
983
+
984
+ return {
985
+ processed: messages.length,
986
+ messages: processedMessages
987
+ };
988
+
989
+ } finally {
990
+ // Clean up renewal timer
991
+ if (renewalTimer) {
992
+ clearInterval(renewalTimer);
993
+ }
994
+ }
995
+
996
+ } catch (error) {
997
+ if (this.#errorHandler) {
998
+ await this.#errorHandler(error, messages || []);
999
+ return { processed: 0, messages: [], error: error.message };
1000
+ } else {
1001
+ throw error;
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Execute the pipeline repeatedly (internal)
1008
+ * @private
1009
+ * @returns {AsyncGenerator} Async generator of results
1010
+ */
1011
+ async *#executeRepeatedly() {
1012
+ const { maxIterations, delay } = this.#repeatConfig;
1013
+ let iteration = 0;
1014
+
1015
+ while (iteration < maxIterations) {
1016
+ const result = await this.#executeSingle();
1017
+ yield result;
1018
+
1019
+ iteration++;
1020
+
1021
+ if (delay > 0) {
1022
+ await new Promise(resolve => setTimeout(resolve, delay));
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Execute the pipeline repeatedly and return summary
1029
+ * @private
1030
+ * @returns {Promise<Object>} Summary of all iterations
1031
+ */
1032
+ async #executeRepeatedlyWithSummary() {
1033
+ const { maxIterations, delay, continuous, waitOnEmpty } = this.#repeatConfig;
1034
+
1035
+ // If concurrency > 1, use parallel workers
1036
+ if (this.#concurrency > 1) {
1037
+ return this.#executeRepeatedlyParallel();
1038
+ }
1039
+
1040
+ // Sequential processing
1041
+ let iteration = 0;
1042
+ let totalProcessed = 0;
1043
+ const results = [];
1044
+ let consecutiveEmpty = 0;
1045
+ const maxConsecutiveEmpty = continuous ? Infinity : 3; // In continuous mode, never stop on empty
1046
+
1047
+ while (iteration < maxIterations) {
1048
+ const result = await this.#executeSingle();
1049
+
1050
+ if (result.processed === 0) {
1051
+ consecutiveEmpty++;
1052
+
1053
+ // In continuous mode, wait and retry
1054
+ if (continuous || consecutiveEmpty < maxConsecutiveEmpty) {
1055
+ await new Promise(resolve => setTimeout(resolve, waitOnEmpty));
1056
+ continue; // Don't increment iteration for empty results
1057
+ } else {
1058
+ // Not continuous and too many empty results
1059
+ break;
1060
+ }
1061
+ } else {
1062
+ // Reset empty counter on successful batch
1063
+ consecutiveEmpty = 0;
1064
+ results.push(result);
1065
+ totalProcessed += result.processed;
1066
+ iteration++;
1067
+ }
1068
+
1069
+ if (delay > 0 && iteration < maxIterations) {
1070
+ await new Promise(resolve => setTimeout(resolve, delay));
1071
+ }
1072
+ }
1073
+
1074
+ return {
1075
+ iterations: iteration,
1076
+ totalProcessed,
1077
+ results,
1078
+ summary: {
1079
+ averagePerBatch: iteration > 0 ? totalProcessed / iteration : 0,
1080
+ completed: iteration >= maxIterations ? 'maxIterations' : 'noMessages',
1081
+ concurrency: this.#concurrency,
1082
+ continuous
1083
+ }
1084
+ };
1085
+ }
1086
+
1087
+ /**
1088
+ * Execute pipeline with parallel processing
1089
+ * Each worker independently calls take(), allowing them to work on different partitions
1090
+ * @private
1091
+ * @returns {Promise<Object>} Summary of all parallel executions
1092
+ */
1093
+ async #executeRepeatedlyParallel() {
1094
+ const { maxIterations = Infinity, delay, continuous, waitOnEmpty } = this.#repeatConfig || {};
1095
+ const workers = [];
1096
+ const results = [];
1097
+
1098
+ // Shared state for coordination
1099
+ const sharedState = {
1100
+ shouldStop: false,
1101
+ emptyWorkers: new Set(),
1102
+ lock: Promise.resolve()
1103
+ };
1104
+
1105
+ // Worker function - each worker independently takes and processes messages
1106
+ const worker = async (workerId) => {
1107
+ let workerIterations = 0;
1108
+ let workerProcessed = 0;
1109
+ let consecutiveEmpty = 0;
1110
+ const maxConsecutiveEmpty = continuous ? Infinity : 10;
1111
+
1112
+ while (!sharedState.shouldStop && workerIterations < maxIterations) {
1113
+ try {
1114
+ // Each worker independently calls executeSingle (which calls take)
1115
+ const result = await this.#executeSingle();
1116
+
1117
+ if (result.processed === 0) {
1118
+ consecutiveEmpty++;
1119
+
1120
+ // Add this worker to empty set
1121
+ sharedState.emptyWorkers.add(workerId);
1122
+
1123
+ // If all workers are empty, consider stopping
1124
+ if (!continuous && sharedState.emptyWorkers.size === this.#concurrency) {
1125
+ // Double-check with a small delay to avoid race conditions
1126
+ await new Promise(resolve => setTimeout(resolve, 200));
1127
+
1128
+ // Re-check after delay
1129
+ if (sharedState.emptyWorkers.size === this.#concurrency) {
1130
+ sharedState.shouldStop = true;
1131
+ break;
1132
+ }
1133
+ }
1134
+
1135
+ // Individual worker timeout
1136
+ if (!continuous && consecutiveEmpty >= maxConsecutiveEmpty) {
1137
+ break;
1138
+ }
1139
+
1140
+ // Wait before retry
1141
+ await new Promise(resolve => setTimeout(resolve, waitOnEmpty || 1000));
1142
+ continue; // Don't count empty iterations
1143
+ }
1144
+
1145
+ // Reset empty counter on successful batch
1146
+ consecutiveEmpty = 0;
1147
+ sharedState.emptyWorkers.delete(workerId);
1148
+
1149
+ // Store result with worker info
1150
+ results.push({
1151
+ ...result,
1152
+ workerId,
1153
+ timestamp: new Date().toISOString()
1154
+ });
1155
+
1156
+ workerProcessed += result.processed;
1157
+ workerIterations++;
1158
+
1159
+ if (delay > 0) {
1160
+ await new Promise(resolve => setTimeout(resolve, delay));
1161
+ }
1162
+ } catch (error) {
1163
+ console.error(`Worker ${workerId} error:`, error);
1164
+ if (this.#errorHandler) {
1165
+ await this.#errorHandler(error, []);
1166
+ } else {
1167
+ sharedState.shouldStop = true;
1168
+ throw error;
1169
+ }
1170
+ }
1171
+ }
1172
+
1173
+ return {
1174
+ workerId,
1175
+ iterations: workerIterations,
1176
+ processed: workerProcessed,
1177
+ consecutiveEmpty
1178
+ };
1179
+ };
1180
+
1181
+ // Start parallel workers
1182
+ console.log(`🚀 Starting ${this.#concurrency} parallel workers...`);
1183
+ for (let i = 0; i < this.#concurrency; i++) {
1184
+ workers.push(worker(i));
1185
+ }
1186
+
1187
+ // Wait for all workers
1188
+ const workerResults = await Promise.all(workers);
1189
+
1190
+ // Aggregate results
1191
+ let totalProcessed = 0;
1192
+ let totalIterations = 0;
1193
+
1194
+ workerResults.forEach(wr => {
1195
+ totalProcessed += wr.processed;
1196
+ totalIterations += wr.iterations;
1197
+ if (wr.iterations > 0) {
1198
+ console.log(`Worker ${wr.workerId}: Processed ${wr.processed} messages in ${wr.iterations} batches`);
1199
+ }
1200
+ });
1201
+
1202
+ return {
1203
+ iterations: totalIterations,
1204
+ totalProcessed,
1205
+ results,
1206
+ workers: workerResults,
1207
+ summary: {
1208
+ averagePerBatch: totalIterations > 0 ? totalProcessed / totalIterations : 0,
1209
+ completed: totalIterations >= (maxIterations * this.#concurrency) ? 'maxIterations' : 'noMessages',
1210
+ concurrency: this.#concurrency,
1211
+ workersUtilized: workerResults.filter(w => w.iterations > 0).length,
1212
+ continuous
1213
+ }
1214
+ };
1215
+ }
1216
+
1217
+ /**
1218
+ * Set up automatic lease renewal
1219
+ * @private
1220
+ */
1221
+ #setupAutoRenewal(messages) {
1222
+ const leaseIds = messages
1223
+ .map(m => m.leaseId)
1224
+ .filter(id => id != null);
1225
+
1226
+ if (leaseIds.length === 0) return null;
1227
+
1228
+ return setInterval(async () => {
1229
+ try {
1230
+ // Extend all leases using the client's renewLease method
1231
+ for (const leaseId of leaseIds) {
1232
+ const result = await this.#client.renewLease(leaseId);
1233
+ if (!result.success) {
1234
+ console.error('Lease renewal failed:', result.error);
1235
+ }
1236
+ }
1237
+ } catch (error) {
1238
+ console.error('Failed to renew lease:', error);
1239
+ }
1240
+ }, this.#leaseRenewal.interval);
1241
+ }
583
1242
  }
584
1243
 
585
1244
  // Export the class as default as well for convenience