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.
- package/README.md +467 -1775
- package/package.json +12 -14
- package/src/client/client.js +664 -5
- package/src/managers/queueManagerOptimized.js +250 -47
- package/src/routes/ack.js +3 -3
- package/src/server.js +131 -3
- package/src/test/advanced-client-tests.js +761 -0
- package/src/test/advanced-pattern-tests.js +2 -2
- package/src/test/bus-mode-tests.js +6 -6
- package/src/test/core-tests.js +1 -1
- package/src/test/edge-case-tests.js +2 -2
- package/src/test/test-new.js +36 -1
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +1 -0
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +1 -0
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +1 -0
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +1 -0
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +1 -0
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +1 -0
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +1 -0
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +1 -0
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +1 -0
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +1 -0
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +1 -0
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +1 -0
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +1 -0
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +1 -0
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +1 -0
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +1 -0
- package/src/webapp-dist/assets/analytics-BapTekLM.js +1 -0
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +18 -0
- package/src/webapp-dist/assets/index-B7w2bKT6.css +1 -0
- package/src/webapp-dist/assets/index-CUSnwYTH.js +31 -0
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +1 -0
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +1 -0
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +1 -0
- package/{webapp → src/webapp-dist}/index.html +2 -1
- package/API.md +0 -1226
- package/AUTH.md +0 -2044
- package/CHANGELOG_STATIC_SERVING.md +0 -213
- package/DEPLOYMENT.md +0 -425
- package/PROGRAMMATIC_SERVER.md +0 -239
- package/WEBAPP.md +0 -1889
- package/assets/dashboard-01.png +0 -0
- package/assets/dashboard.png +0 -0
- package/examples/batch-processing.js +0 -58
- package/examples/bus-mode.js +0 -239
- package/examples/continuous-consumer-optimized.js +0 -215
- package/examples/continuous-consumer.js +0 -159
- package/examples/continuous-producer.js +0 -343
- package/examples/mixed-mode.js +0 -277
- package/examples/multi-server-test.js +0 -305
- package/examples/programmatic-server.js +0 -58
- package/examples/server-with-frontend.js +0 -46
- package/examples/single.js +0 -64
- package/examples/smartchat-dealyed.js +0 -42
- package/examples/smartchat.js +0 -52
- package/examples/test-cache-invalidation.js +0 -119
- package/examples/test-cache-multi-server.js +0 -245
- package/examples/test-complete-client.js +0 -260
- package/examples/test-connection-recovery.js +0 -66
- package/examples/test-dashboard-api.js +0 -200
- package/examples/test-minimal-client.js +0 -112
- package/examples/test-queue-creation-policy.js +0 -137
- package/examples/test-traceid.js +0 -147
- package/server.log +0 -1
- package/test-keepalive-v2.sh +0 -22
- package/webapp/COLOR_GUIDE.md +0 -118
- package/webapp/README.md +0 -143
- package/webapp/package-lock.json +0 -3184
- package/webapp/package.json +0 -25
- package/webapp/postcss.config.js +0 -7
- package/webapp/public/assets/queen-logo-blue.svg +0 -210
- package/webapp/public/assets/queen-logo-cyan.svg +0 -210
- package/webapp/public/assets/queen-logo-indigo.svg +0 -210
- package/webapp/public/assets/queen-logo-orange.svg +0 -210
- package/webapp/public/assets/queen-logo-pink.svg +0 -210
- package/webapp/public/assets/queen-logo-purple.svg +0 -210
- package/webapp/public/assets/queen-logo-rose.svg +0 -239
- package/webapp/public/assets/queen-logo.svg +0 -263
- package/webapp/src/App.vue +0 -19
- package/webapp/src/api/analytics.js +0 -10
- package/webapp/src/api/client.js +0 -34
- package/webapp/src/api/consumers.js +0 -52
- package/webapp/src/api/health.js +0 -7
- package/webapp/src/api/messages.js +0 -26
- package/webapp/src/api/queues.js +0 -14
- package/webapp/src/api/resources.js +0 -8
- package/webapp/src/assets/styles/main.css +0 -357
- package/webapp/src/components/analytics/AnalyticsFilters.vue +0 -87
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +0 -57
- package/webapp/src/components/analytics/MessageDistributionChart.vue +0 -111
- package/webapp/src/components/analytics/MessageFlowChart.vue +0 -173
- package/webapp/src/components/analytics/TimeRangeSelector.vue +0 -27
- package/webapp/src/components/analytics/TopQueuesChart.vue +0 -132
- package/webapp/src/components/common/ConfirmDialog.vue +0 -56
- package/webapp/src/components/common/LoadingSpinner.vue +0 -6
- package/webapp/src/components/common/MetricCard.vue +0 -43
- package/webapp/src/components/common/StatusBadge.vue +0 -45
- package/webapp/src/components/dashboard/MessageStatusCard.vue +0 -50
- package/webapp/src/components/dashboard/PerformanceCard.vue +0 -38
- package/webapp/src/components/dashboard/ThroughputChart.vue +0 -182
- package/webapp/src/components/dashboard/TopQueuesTable.vue +0 -53
- package/webapp/src/components/layout/AppLayout.vue +0 -110
- package/webapp/src/components/layout/AppSidebar.vue +0 -304
- package/webapp/src/components/messages/MessageDetailPanel.vue +0 -242
- package/webapp/src/components/messages/MessageFilters.vue +0 -101
- package/webapp/src/components/queue-detail/PartitionList.vue +0 -79
- package/webapp/src/components/queue-detail/PushMessageModal.vue +0 -175
- package/webapp/src/components/queue-detail/QueueConfig.vue +0 -63
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +0 -53
- package/webapp/src/components/queue-detail/RecentMessages.vue +0 -76
- package/webapp/src/components/queues/CreateQueueModal.vue +0 -193
- package/webapp/src/components/queues/QueueFilters.vue +0 -90
- package/webapp/src/composables/useApi.js +0 -34
- package/webapp/src/composables/useTheme.js +0 -36
- package/webapp/src/main.js +0 -11
- package/webapp/src/router/index.js +0 -42
- package/webapp/src/utils/colors.js +0 -96
- package/webapp/src/utils/formatters.js +0 -49
- package/webapp/src/views/Analytics.vue +0 -377
- package/webapp/src/views/ConsumerGroups.vue +0 -433
- package/webapp/src/views/Dashboard.vue +0 -418
- package/webapp/src/views/Messages.vue +0 -363
- package/webapp/src/views/QueueDetail.vue +0 -582
- package/webapp/src/views/Queues.vue +0 -496
- package/webapp/tailwind.config.js +0 -25
- package/webapp/vite.config.js +0 -10
- /package/{assets → src/webapp-dist/assets}/queen-logo-blue.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-cyan.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-indigo.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-orange.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-pink.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-purple.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-rose.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo.svg +0 -0
package/src/client/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|