queen-mq 0.3.1 → 0.4.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 (39) hide show
  1. package/README.md +212 -144
  2. package/client-js/client-v2/Queen.js +28 -0
  3. package/client-js/client-v2/README.md +6 -2
  4. package/client-js/client-v2/builders/QueueBuilder.js +7 -1
  5. package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
  6. package/client-js/client-v2/stream/StreamBuilder.js +72 -0
  7. package/client-js/client-v2/stream/StreamConsumer.js +156 -0
  8. package/client-js/client-v2/stream/Window.js +140 -0
  9. package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
  10. package/client-js/test-v2/consume.js +1 -0
  11. package/client-js/test-v2/maintenance.js +261 -0
  12. package/client-js/test-v2/retention.js +69 -0
  13. package/client-js/test-v2/run.js +7 -1
  14. package/package.json +1 -1
  15. package/client-js/client/client.js +0 -1536
  16. package/client-js/client/index.js +0 -4
  17. package/client-js/client/utils/http.js +0 -173
  18. package/client-js/client/utils/loadBalancer.js +0 -152
  19. package/client-js/client/utils/retry.js +0 -41
  20. package/client-js/services/encryptionService.js +0 -82
  21. package/client-js/services/evictionService.js +0 -160
  22. package/client-js/services/retentionService.js +0 -162
  23. package/client-js/services/startupSync.js +0 -35
  24. package/client-js/test/README.md +0 -224
  25. package/client-js/test/advanced-client-tests.js +0 -761
  26. package/client-js/test/advanced-pattern-tests.js +0 -1137
  27. package/client-js/test/bus-mode-tests.js +0 -361
  28. package/client-js/test/core-tests.js +0 -457
  29. package/client-js/test/edge-case-tests.js +0 -562
  30. package/client-js/test/enterprise-tests.js +0 -637
  31. package/client-js/test/human.js +0 -162
  32. package/client-js/test/partition-locking-tests.js +0 -545
  33. package/client-js/test/partition-transaction-tests.js +0 -482
  34. package/client-js/test/qos0-tests.js +0 -334
  35. package/client-js/test/test-new.js +0 -370
  36. package/client-js/test/utils.js +0 -169
  37. package/client-js/test/window-buffer-test.js +0 -114
  38. package/client-js/utils/logger.js +0 -44
  39. package/client-js/utils/uuid.js +0 -5
@@ -1,1536 +0,0 @@
1
- import { createHttpClient, createLoadBalancedHttpClient } from './utils/http.js';
2
- import { withRetry } from './utils/retry.js';
3
- import { createLoadBalancer, LoadBalancingStrategy } from './utils/loadBalancer.js';
4
- import { generateUUID } from '../utils/uuid.js';
5
-
6
- /**
7
- * Minimalist Queen Message Queue Client
8
- *
9
- * Simple, powerful API with just 4 methods:
10
- * - queue: Configure a queue
11
- * - push: Send messages
12
- * - take: Receive messages (async iterator)
13
- * - ack: Acknowledge messages
14
- */
15
- export class Queen {
16
- #config;
17
- #http;
18
- #loadBalancer;
19
- #connected = false;
20
- #bufferManager;
21
-
22
- constructor(config = {}) {
23
- this.#config = {
24
- baseUrls: null,
25
- loadBalancingStrategy: LoadBalancingStrategy.ROUND_ROBIN,
26
- enableFailover: true,
27
- timeout: 30000,
28
- retryAttempts: 3,
29
- retryDelay: 1000,
30
- ...config
31
- };
32
- this.#bufferManager = new BufferManager(this);
33
- }
34
-
35
- /**
36
- * Ensure HTTP client is connected
37
- */
38
- async #ensureConnected() {
39
- if (this.#connected) return;
40
-
41
- const { baseUrl, baseUrls, loadBalancingStrategy, enableFailover, timeout } = this.#config;
42
-
43
- if (baseUrls && Array.isArray(baseUrls) && baseUrls.length > 0) {
44
- // Multiple servers with load balancing
45
- this.#loadBalancer = createLoadBalancer(baseUrls, loadBalancingStrategy);
46
- this.#http = createLoadBalancedHttpClient({
47
- baseUrls,
48
- loadBalancer: this.#loadBalancer,
49
- timeout,
50
- enableFailover
51
- });
52
- } else {
53
- // Single server
54
- const singleUrl = baseUrls && baseUrls.length === 1 ? baseUrls[0] : baseUrl;
55
- this.#http = createHttpClient({ baseUrl: singleUrl, timeout });
56
- }
57
-
58
- this.#connected = true;
59
- }
60
-
61
- /**
62
- * Validate if a string is a valid UUID v4
63
- */
64
- #isValidUUID(str) {
65
- 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;
66
- return uuidRegex.test(str);
67
- }
68
-
69
- /**
70
- * Parse address string into components
71
- * Examples:
72
- * - "myqueue" -> { queue: "myqueue" }
73
- * - "myqueue/urgent" -> { queue: "myqueue", partition: "urgent" }
74
- * - "myqueue@workers" -> { queue: "myqueue", consumerGroup: "workers" }
75
- * - "myqueue/urgent@workers" -> { queue: "myqueue", partition: "urgent", consumerGroup: "workers" }
76
- * - "namespace:billing" -> { namespace: "billing" }
77
- * - "task:process" -> { task: "process" }
78
- * - "namespace:billing/task:process" -> { namespace: "billing", task: "process" }
79
- */
80
- #parseAddress(address) {
81
- // Check for namespace/task pattern
82
- if (address.includes('namespace:') || address.includes('task:')) {
83
- const parts = {};
84
-
85
- // Extract consumer group if present (after @)
86
- let workingAddress = address;
87
- const atIndex = address.lastIndexOf('@');
88
- if (atIndex > 0) {
89
- parts.consumerGroup = address.substring(atIndex + 1);
90
- workingAddress = address.substring(0, atIndex);
91
- }
92
-
93
- const segments = workingAddress.split('/');
94
-
95
- for (const segment of segments) {
96
- if (segment.startsWith('namespace:')) {
97
- parts.namespace = segment.substring(10);
98
- } else if (segment.startsWith('task:')) {
99
- parts.task = segment.substring(5);
100
- }
101
- }
102
-
103
- return parts;
104
- }
105
-
106
- // Standard queue[/partition][@group] pattern
107
- const match = address.match(/^([^/@]+)(?:\/([^@]+))?(?:@(.+))?$/);
108
- if (!match) {
109
- throw new Error(`Invalid address format: ${address}`);
110
- }
111
-
112
- return {
113
- queue: match[1],
114
- partition: match[2] || 'Default',
115
- consumerGroup: match[3] || null
116
- };
117
- }
118
-
119
- /**
120
- * Configure a queue with options
121
- * @param {string} name - Queue name
122
- * @param {Object} options - Queue configuration options
123
- * @param {Object} metadata - Optional metadata { namespace, task }
124
- */
125
- async queue(name, options = {}, metadata = {}) {
126
- await this.#ensureConnected();
127
-
128
- const { namespace, task } = metadata;
129
-
130
- const result = await withRetry(
131
- () => this.#http.post('/api/v1/configure', {
132
- queue: name,
133
- namespace: namespace || null,
134
- task: task || null,
135
- options
136
- }),
137
- this.#config.retryAttempts,
138
- this.#config.retryDelay
139
- );
140
-
141
- if (result && result.error) {
142
- throw new Error(result.error);
143
- }
144
-
145
- return result;
146
- }
147
-
148
- /**
149
- * Push messages to a queue
150
- * @param {string} address - Queue address (e.g., "myqueue" or "myqueue/partition")
151
- * @param {Object|Array} payload - Single message or array of messages
152
- * @param {Object} options - Optional message properties { transactionId, traceId, buffer }
153
- */
154
- async push(address, payload, options = {}) {
155
- await this.#ensureConnected();
156
-
157
- const { queue, partition } = this.#parseAddress(address);
158
-
159
- // Handle both single and array inputs
160
- const items = Array.isArray(payload) ? payload : [payload];
161
-
162
- // Format items for the API
163
- const formattedItems = items.map(item => {
164
- // If item is an object with special properties, extract them
165
- const isMessageObject = typeof item === 'object' && item !== null &&
166
- (item._payload || item._transactionId || item._traceId);
167
-
168
- if (isMessageObject) {
169
- const result = {
170
- queue,
171
- partition,
172
- payload: item._payload || item,
173
- transactionId: item._transactionId || item.transactionId || generateUUID() // Always generate if missing
174
- };
175
-
176
- // Include traceId if provided and valid UUID
177
- const traceId = item._traceId || item.traceId;
178
- if (traceId && this.#isValidUUID(traceId)) {
179
- result.traceId = traceId;
180
- }
181
-
182
- return result;
183
- }
184
-
185
- // Otherwise use the item as payload and apply options
186
- const result = {
187
- queue,
188
- partition,
189
- payload: item,
190
- transactionId: options.transactionId ||
191
- (item && typeof item === 'object' ? item.transactionId : undefined) ||
192
- generateUUID() // Always generate if missing
193
- };
194
-
195
- // Include traceId if provided and valid UUID
196
- const traceId = options.traceId || (item && typeof item === 'object' ? item.traceId : undefined);
197
- if (traceId && this.#isValidUUID(traceId)) {
198
- result.traceId = traceId;
199
- }
200
-
201
- return result;
202
- });
203
-
204
- // Check if client-side buffering is enabled
205
- if (options.buffer && typeof options.buffer === 'object') {
206
- return this.#pushBuffered(address, formattedItems, options.buffer);
207
- }
208
-
209
- // Build request body for immediate push
210
- const requestBody = { items: formattedItems };
211
-
212
- // Add server-side buffering flag if specified
213
- if (options.buffer === true || options.qos0 === true) {
214
- requestBody.qos0 = true; // Server still uses 'qos0' internally
215
- }
216
-
217
- // Legacy support: bufferMs/bufferMax are aliases for buffer
218
- if (options.bufferMs !== undefined || options.bufferMax !== undefined) {
219
- requestBody.qos0 = true;
220
- }
221
-
222
- const result = await withRetry(
223
- () => this.#http.post('/api/v1/push', requestBody),
224
- this.#config.retryAttempts,
225
- this.#config.retryDelay
226
- );
227
-
228
- if (result && result.error) {
229
- throw new Error(result.error);
230
- }
231
-
232
- return result;
233
- }
234
-
235
- /**
236
- * Push messages using client-side buffering
237
- * @private
238
- */
239
- #pushBuffered(address, formattedItems, bufferOptions) {
240
- // Validate buffer options
241
- if (!bufferOptions.size || !bufferOptions.time) {
242
- throw new Error('Buffer options must include both size and time');
243
- }
244
-
245
- // Add each formatted message to the buffer
246
- for (const item of formattedItems) {
247
- this.#bufferManager.addMessage(address, item, bufferOptions);
248
- }
249
-
250
- // Return immediately (non-blocking)
251
- return { buffered: true, count: formattedItems.length };
252
- }
253
-
254
- /**
255
- * Internal method that yields batches of messages (arrays)
256
- * @private
257
- * @param {string} address - Queue address
258
- * @param {Object} options - Options for taking messages
259
- * @yields {Array} Arrays of message objects
260
- */
261
- async *#takeInternal(address, options = {}) {
262
- await this.#ensureConnected();
263
-
264
- const { queue, partition, consumerGroup, namespace, task } = this.#parseAddress(address);
265
- const {
266
- limit = null,
267
- batch = 1,
268
- wait = false,
269
- timeout = 30000,
270
- subscriptionMode = null,
271
- subscriptionFrom = null,
272
- idleTimeout = null
273
- } = options;
274
-
275
- let totalCount = 0;
276
- let consecutiveEmptyResponses = 0;
277
- let consecutiveNetworkErrors = 0;
278
- const maxConsecutiveEmpty = 3;
279
- let lastMessageTime = idleTimeout ? Date.now() : null;
280
-
281
- while (true) {
282
- // Check if we've reached the limit
283
- if (limit && totalCount >= limit) break;
284
-
285
- // Check idle timeout
286
- if (idleTimeout && lastMessageTime) {
287
- const idleTime = Date.now() - lastMessageTime;
288
- if (idleTime >= idleTimeout) {
289
- break; // Exit if idle time exceeded
290
- }
291
- }
292
-
293
- // Calculate batch size respecting the limit
294
- const effectiveBatch = limit ? Math.min(batch, limit - totalCount) : batch;
295
-
296
- // Build the request path and parameters
297
- let path;
298
- const params = new URLSearchParams({
299
- wait: wait.toString(),
300
- timeout: timeout.toString(),
301
- batch: effectiveBatch.toString()
302
- });
303
-
304
- // Add consumer group parameters if provided
305
- if (consumerGroup) params.append('consumerGroup', consumerGroup);
306
- if (subscriptionMode) params.append('subscriptionMode', subscriptionMode);
307
- if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
308
-
309
- // Add auto-ack option if specified
310
- if (options.autoAck) params.append('autoAck', 'true');
311
-
312
- // Determine the endpoint based on parameters
313
- if (queue) {
314
- if (partition && partition !== 'Default') {
315
- path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
316
- } else {
317
- path = `/api/v1/pop/queue/${queue}`;
318
- }
319
- } else if (namespace || task) {
320
- // Pop by namespace/task
321
- path = '/api/v1/pop';
322
- if (namespace) params.append('namespace', namespace);
323
- if (task) params.append('task', task);
324
- } else {
325
- throw new Error('Must specify either queue, namespace, or task');
326
- }
327
-
328
- try {
329
- // For long polling, use a slightly longer client timeout
330
- const clientTimeout = wait ? timeout + 5000 : timeout;
331
-
332
- const result = await this.#http.get(`${path}?${params}`, clientTimeout);
333
-
334
- // Handle empty response
335
- if (!result || !result.messages || result.messages.length === 0) {
336
- if (wait) {
337
- // For long polling, immediately retry
338
- continue;
339
- } else {
340
- // For non-waiting mode, stop after several empty responses
341
- consecutiveEmptyResponses++;
342
- if (consecutiveEmptyResponses >= maxConsecutiveEmpty) {
343
- break;
344
- }
345
- // Small delay before retry
346
- await new Promise(resolve => setTimeout(resolve, 100));
347
- continue;
348
- }
349
- }
350
-
351
- // Reset counters on successful fetch
352
- consecutiveEmptyResponses = 0;
353
- consecutiveNetworkErrors = 0;
354
-
355
- // Update last message time if tracking idle timeout
356
- if (idleTimeout) {
357
- lastMessageTime = Date.now();
358
- }
359
-
360
- // Filter out null/undefined messages
361
- const messages = result.messages.filter(msg => msg != null);
362
-
363
- if (messages.length > 0) {
364
- totalCount += messages.length;
365
- yield messages;
366
-
367
- // If we've hit the limit, stop
368
- if (limit && totalCount >= limit) {
369
- break;
370
- }
371
- }
372
-
373
- } catch (error) {
374
- // Check if this is a timeout error (expected for long polling)
375
- const isTimeoutError = error.name === 'AbortError' ||
376
- error.message?.includes('abort') ||
377
- error.message?.includes('timeout');
378
-
379
- if (isTimeoutError && wait) {
380
- // For long polling timeout, immediately retry
381
- continue;
382
- }
383
-
384
- // Check if this is a network error (connection refused, socket closed, etc.)
385
- const isNetworkError = error.message?.includes('fetch failed') ||
386
- error.message?.includes('ECONNREFUSED') ||
387
- error.message?.includes('ECONNRESET') ||
388
- error.message?.includes('closed') ||
389
- error.cause?.code === 'UND_ERR_SOCKET' ||
390
- error.code === 'ECONNREFUSED' ||
391
- error.code === 'ECONNRESET';
392
-
393
- if (isNetworkError) {
394
- // Log the network error
395
- console.warn(`Network error while polling queue: ${error.message}`);
396
-
397
- // Retry with exponential backoff (capped at 30 seconds)
398
- const retryDelay = Math.min(this.#config.retryDelay * Math.pow(2, Math.min(consecutiveNetworkErrors, 10)), 30000);
399
- console.warn(`Retrying in ${retryDelay}ms... (attempt ${consecutiveNetworkErrors + 1})`);
400
- await new Promise(resolve => setTimeout(resolve, retryDelay));
401
-
402
- consecutiveNetworkErrors++;
403
-
404
- // Continue retrying indefinitely
405
- continue;
406
- }
407
-
408
- // For other errors, throw
409
- throw error;
410
- }
411
- }
412
- }
413
-
414
- /**
415
- * Take messages from a queue one at a time (async iterator)
416
- * @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
417
- * @param {Object} options - Options for taking messages
418
- * @yields {Object} Individual message objects
419
- */
420
- async *take(address, options = {}) {
421
- let count = 0;
422
- const { limit = null } = options;
423
-
424
- for await (const messages of this.#takeInternal(address, options)) {
425
- for (const message of messages) {
426
- yield message;
427
- count++;
428
-
429
- // Double-check limit (internal method also checks, but this ensures exact limit)
430
- if (limit && count >= limit) {
431
- return;
432
- }
433
- }
434
- }
435
- }
436
-
437
- /**
438
- * Take messages from a queue in batches (async iterator)
439
- * @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
440
- * @param {Object} options - Options for taking messages
441
- * @yields {Array} Arrays of message objects
442
- */
443
- async *takeBatch(address, options = {}) {
444
- for await (const messages of this.#takeInternal(address, options)) {
445
- // Only yield non-empty batches
446
- if (messages && messages.length > 0) {
447
- yield messages;
448
- }
449
- }
450
- }
451
-
452
- /**
453
- * Take a single batch of messages (convenience method)
454
- * @param {string} address - Queue address
455
- * @param {Object} options - Options for taking messages
456
- * @returns {Promise<Array>} Array of messages
457
- */
458
- async takeSingleBatch(address, options = {}) {
459
- const messages = [];
460
- for await (const batch of this.takeBatch(address, options)) {
461
- messages.push(...batch);
462
- break; // Take only one batch
463
- }
464
- return messages;
465
- }
466
-
467
- /**
468
- * Renew lease for a message or batch of messages
469
- * @param {Object|Array|string} messageOrLeaseId - Message object(s) or lease ID(s)
470
- * @returns {Promise<Object>} Renewal result
471
- */
472
- async renewLease(messageOrLeaseId) {
473
- await this.#ensureConnected();
474
-
475
- // Handle different input types
476
- let leaseIds = [];
477
-
478
- if (typeof messageOrLeaseId === 'string') {
479
- // Direct lease ID
480
- leaseIds = [messageOrLeaseId];
481
- } else if (Array.isArray(messageOrLeaseId)) {
482
- // Array of messages or lease IDs
483
- leaseIds = messageOrLeaseId.map(item =>
484
- typeof item === 'string' ? item : item.leaseId
485
- ).filter(Boolean);
486
- } else if (messageOrLeaseId && typeof messageOrLeaseId === 'object') {
487
- // Single message object
488
- if (messageOrLeaseId.leaseId) {
489
- leaseIds = [messageOrLeaseId.leaseId];
490
- }
491
- }
492
-
493
- if (leaseIds.length === 0) {
494
- throw new Error('No valid lease IDs found for renewal');
495
- }
496
-
497
- // Renew all leases
498
- const results = [];
499
- for (const leaseId of leaseIds) {
500
- try {
501
- const result = await withRetry(
502
- () => this.#http.post(`/api/v1/lease/${leaseId}/extend`, {}),
503
- this.#config.retryAttempts,
504
- this.#config.retryDelay
505
- );
506
- results.push({
507
- leaseId,
508
- success: true,
509
- newExpiresAt: result.leaseId ? result.newExpiresAt : result.lease_expires_at
510
- });
511
- } catch (error) {
512
- results.push({ leaseId, success: false, error: error.message });
513
- }
514
- }
515
-
516
- // Return single result for single input, array for array input
517
- return Array.isArray(messageOrLeaseId) ? results : results[0];
518
- }
519
-
520
- /**
521
- * Acknowledge a message or batch of messages
522
- * @param {Object|string|Array} message - Message object, transaction ID, or array of messages
523
- * @param {boolean|string} status - true for success, false for failure, or 'retry'
524
- * @param {Object} context - Optional context (e.g., { group: 'workers', error: 'reason' })
525
- */
526
- async ack(message, status = true, context = {}) {
527
- await this.#ensureConnected();
528
-
529
- // Handle batch acknowledgment
530
- if (Array.isArray(message)) {
531
- if (message.length === 0) {
532
- return { processed: 0, results: [] };
533
- }
534
-
535
- // Check if messages have individual status (Option B pattern)
536
- const hasIndividualStatus = message.some(msg =>
537
- typeof msg === 'object' && msg !== null && ('_status' in msg || '_error' in msg)
538
- );
539
-
540
- let acknowledgments;
541
-
542
- if (hasIndividualStatus) {
543
- // Option B: Each message has its own status
544
- acknowledgments = message.map(msg => {
545
- // Extract transaction ID, partition ID, and lease ID
546
- let transactionId;
547
- let partitionId = null;
548
- let leaseId = null;
549
-
550
- if (typeof msg === 'string') {
551
- transactionId = msg;
552
- } else if (typeof msg === 'object' && msg !== null) {
553
- transactionId = msg.transactionId || msg.id;
554
- partitionId = msg.partitionId || null;
555
- leaseId = msg.leaseId || null;
556
- if (!transactionId) {
557
- throw new Error('Message object must have transactionId or id property');
558
- }
559
- } else {
560
- throw new Error('Invalid message in batch');
561
- }
562
-
563
- // Get individual status or fall back to parameter
564
- let msgStatus = msg._status !== undefined ? msg._status : status;
565
- let statusStr;
566
- if (typeof msgStatus === 'boolean') {
567
- statusStr = msgStatus ? 'completed' : 'failed';
568
- } else {
569
- statusStr = msgStatus;
570
- }
571
-
572
- const ack = {
573
- transactionId,
574
- status: statusStr,
575
- error: msg._error || context.error || null
576
- };
577
-
578
- // Include partition ID if available
579
- if (partitionId) {
580
- ack.partitionId = partitionId;
581
- }
582
-
583
- // Include lease ID if available
584
- if (leaseId) {
585
- ack.leaseId = leaseId;
586
- }
587
-
588
- return ack;
589
- });
590
- } else {
591
- // Option A: Same status for all messages
592
- const statusStr = typeof status === 'boolean'
593
- ? (status ? 'completed' : 'failed')
594
- : status;
595
-
596
- acknowledgments = message.map(msg => {
597
- // Extract transaction ID, partition ID, and lease ID
598
- let transactionId;
599
- let partitionId = null;
600
- let leaseId = null;
601
-
602
- if (typeof msg === 'string') {
603
- transactionId = msg;
604
- } else if (typeof msg === 'object' && msg !== null) {
605
- transactionId = msg.transactionId || msg.id;
606
- partitionId = msg.partitionId || null;
607
- leaseId = msg.leaseId || null;
608
- if (!transactionId) {
609
- throw new Error('Message object must have transactionId or id property');
610
- }
611
- } else {
612
- throw new Error('Invalid message in batch');
613
- }
614
-
615
- const ack = {
616
- transactionId,
617
- status: statusStr,
618
- error: context.error || null
619
- };
620
-
621
- // Include partition ID if available
622
- if (partitionId) {
623
- ack.partitionId = partitionId;
624
- }
625
-
626
- // Include lease ID if available
627
- if (leaseId) {
628
- ack.leaseId = leaseId;
629
- }
630
-
631
- return ack;
632
- });
633
- }
634
-
635
- // Call batch ack endpoint
636
- const result = await withRetry(
637
- () => this.#http.post('/api/v1/ack/batch', {
638
- acknowledgments,
639
- consumerGroup: context.group || null
640
- }),
641
- this.#config.retryAttempts,
642
- this.#config.retryDelay
643
- );
644
-
645
- if (result && result.error) {
646
- throw new Error(result.error);
647
- }
648
-
649
- return result;
650
- }
651
-
652
- // Handle single message acknowledgment
653
- // Extract transaction ID, partition ID, and lease ID
654
- let transactionId;
655
- let partitionId = null;
656
- let leaseId = null;
657
-
658
- if (typeof message === 'string') {
659
- transactionId = message;
660
- } else if (typeof message === 'object' && message !== null) {
661
- transactionId = message.transactionId || message.id;
662
- partitionId = message.partitionId || null; // Extract partition ID for partition-scoped ACK
663
- leaseId = message.leaseId || null; // Extract lease ID if present
664
- if (!transactionId) {
665
- throw new Error('Message object must have transactionId or id property');
666
- }
667
- } else {
668
- throw new Error('Message must be a string (transaction ID) or object with transactionId');
669
- }
670
-
671
- // Determine status string
672
- let statusStr;
673
- if (typeof status === 'boolean') {
674
- statusStr = status ? 'completed' : 'failed';
675
- } else {
676
- statusStr = status; // Allow custom status like 'retry'
677
- }
678
-
679
- // Build request body
680
- const body = {
681
- transactionId,
682
- status: statusStr,
683
- error: context.error || null,
684
- consumerGroup: context.group || null
685
- };
686
-
687
- // Include partition ID if available for partition-scoped operations
688
- if (partitionId) {
689
- body.partitionId = partitionId;
690
- }
691
-
692
- // Include lease ID if available for lease validation
693
- if (leaseId) {
694
- body.leaseId = leaseId;
695
- }
696
-
697
- const result = await withRetry(
698
- () => this.#http.post('/api/v1/ack', body),
699
- this.#config.retryAttempts,
700
- this.#config.retryDelay
701
- );
702
-
703
- if (result && result.error) {
704
- throw new Error(result.error);
705
- }
706
-
707
- return result;
708
- }
709
-
710
- /**
711
- * Delete a queue (removes queue and all its messages/partitions)
712
- * @param {string} name - Queue name to delete
713
- * @returns {Promise<Object>} Deletion result
714
- */
715
- async queueDelete(name) {
716
- await this.#ensureConnected();
717
-
718
- if (typeof name !== 'string' || !name) {
719
- throw new Error('Queue name must be a non-empty string');
720
- }
721
-
722
- const result = await withRetry(
723
- () => this.#http.delete(`/api/v1/resources/queues/${encodeURIComponent(name)}`),
724
- this.#config.retryAttempts,
725
- this.#config.retryDelay
726
- );
727
-
728
- if (result && result.error) {
729
- throw new Error(result.error);
730
- }
731
-
732
- return result;
733
- }
734
-
735
- /**
736
- * Manually flush a specific buffer
737
- * @param {string} address - Queue address to flush
738
- * @returns {Promise<void>}
739
- */
740
- async flushBuffer(address) {
741
- return this.#bufferManager.flushBuffer(address);
742
- }
743
-
744
- /**
745
- * Manually flush all buffers
746
- * @returns {Promise<void>}
747
- */
748
- async flushAllBuffers() {
749
- return this.#bufferManager.flushAllBuffers();
750
- }
751
-
752
- /**
753
- * Get buffer statistics
754
- * @returns {Object} Buffer statistics
755
- */
756
- getBufferStats() {
757
- return this.#bufferManager.getStats();
758
- }
759
-
760
- /**
761
- * Close the client connection
762
- */
763
- async close() {
764
- // Flush all buffers before closing
765
- try {
766
- await this.#bufferManager.flushAllBuffers();
767
- } catch (error) {
768
- console.warn('Error flushing buffers during close:', error);
769
- }
770
-
771
- // Cleanup buffer manager
772
- this.#bufferManager.cleanup();
773
-
774
- this.#connected = false;
775
- this.#http = null;
776
- this.#loadBalancer = null;
777
- }
778
-
779
- /**
780
- * Create a transaction builder for atomic operations
781
- * @returns {TransactionBuilder} Transaction builder instance
782
- */
783
- transaction() {
784
- return new TransactionBuilder(this);
785
- }
786
-
787
- /**
788
- * Create a pipeline for message processing workflows
789
- * @param {string} queue - Source queue name
790
- * @returns {PipelineBuilder} Pipeline builder instance
791
- */
792
- pipeline(queue) {
793
- return new PipelineBuilder(this, queue);
794
- }
795
-
796
- // Internal helper methods for Transaction and Pipeline
797
- async _ensureConnected() {
798
- return this.#ensureConnected();
799
- }
800
-
801
- get _http() {
802
- return this.#http;
803
- }
804
-
805
- get _config() {
806
- return this.#config;
807
- }
808
- }
809
-
810
- /**
811
- * Message Buffer for client-side buffering
812
- */
813
- class MessageBuffer {
814
- constructor(address, options, flushCallback) {
815
- this.address = address;
816
- this.messages = [];
817
- this.options = options;
818
- this.flushCallback = flushCallback;
819
- this.timer = null;
820
- this.firstMessageTime = null;
821
- this.flushing = false;
822
- }
823
-
824
- add(formattedMessage) {
825
- // Set first message time if this is the first message
826
- if (this.messages.length === 0) {
827
- this.firstMessageTime = Date.now();
828
- this.#startTimer();
829
- }
830
-
831
- this.messages.push(formattedMessage);
832
-
833
- // Check if we should flush based on size
834
- if (this.messages.length >= this.options.size) {
835
- this.#triggerFlush();
836
- }
837
- }
838
-
839
- #startTimer() {
840
- if (this.timer) return; // Timer already running
841
-
842
- this.timer = setTimeout(() => {
843
- this.#triggerFlush();
844
- }, this.options.time);
845
- }
846
-
847
- #triggerFlush() {
848
- if (this.flushing || this.messages.length === 0) return;
849
-
850
- // Clear timer
851
- if (this.timer) {
852
- clearTimeout(this.timer);
853
- this.timer = null;
854
- }
855
-
856
- // Trigger flush via callback
857
- this.flushCallback(this.address);
858
- }
859
-
860
- extractMessages() {
861
- const messages = [...this.messages];
862
- this.messages = [];
863
- this.firstMessageTime = null;
864
- this.flushing = false;
865
-
866
- if (this.timer) {
867
- clearTimeout(this.timer);
868
- this.timer = null;
869
- }
870
-
871
- return messages;
872
- }
873
-
874
- cleanup() {
875
- if (this.timer) {
876
- clearTimeout(this.timer);
877
- this.timer = null;
878
- }
879
- this.messages = [];
880
- this.firstMessageTime = null;
881
- this.flushing = false;
882
- }
883
- }
884
-
885
- /**
886
- * Buffer Manager for client-side message buffering
887
- */
888
- class BufferManager {
889
- constructor(client) {
890
- this.client = client;
891
- this.buffers = new Map(); // address -> MessageBuffer
892
- this.flushCount = 0;
893
- }
894
-
895
- addMessage(address, formattedMessage, bufferOptions) {
896
- const bufferKey = address;
897
-
898
- if (!this.buffers.has(bufferKey)) {
899
- this.buffers.set(bufferKey, new MessageBuffer(
900
- address,
901
- bufferOptions,
902
- (addr) => this.#flushBuffer(addr)
903
- ));
904
- }
905
-
906
- const buffer = this.buffers.get(bufferKey);
907
- buffer.add(formattedMessage);
908
- }
909
-
910
- async #flushBuffer(address) {
911
- const buffer = this.buffers.get(address);
912
- if (!buffer || buffer.flushing || buffer.messages.length === 0) {
913
- return;
914
- }
915
-
916
- buffer.flushing = true;
917
-
918
- try {
919
- const messages = buffer.extractMessages();
920
-
921
- if (messages.length === 0) return;
922
-
923
- // Call the original push logic with the buffered messages
924
- const requestBody = { items: messages };
925
-
926
- await withRetry(
927
- () => this.client._http.post('/api/v1/push', requestBody),
928
- this.client._config.retryAttempts,
929
- this.client._config.retryDelay
930
- );
931
-
932
- this.flushCount++;
933
-
934
- // Call onFlush callback if provided
935
- if (buffer.options.onFlush) {
936
- buffer.options.onFlush(address, messages.length);
937
- }
938
-
939
- // Remove empty buffer
940
- this.buffers.delete(address);
941
-
942
- } catch (error) {
943
- buffer.flushing = false;
944
-
945
- // Call onError callback if provided
946
- if (buffer.options.onError) {
947
- buffer.options.onError(address, error);
948
- }
949
-
950
- throw error;
951
- }
952
- }
953
-
954
- async flushBuffer(address) {
955
- return this.#flushBuffer(address);
956
- }
957
-
958
- async flushAllBuffers() {
959
- const flushPromises = [];
960
- for (const address of this.buffers.keys()) {
961
- flushPromises.push(this.#flushBuffer(address));
962
- }
963
- await Promise.all(flushPromises);
964
- }
965
-
966
- getStats() {
967
- let totalBufferedMessages = 0;
968
- let oldestBufferAge = 0;
969
-
970
- for (const buffer of this.buffers.values()) {
971
- totalBufferedMessages += buffer.messages.length;
972
- if (buffer.firstMessageTime) {
973
- const age = Date.now() - buffer.firstMessageTime;
974
- oldestBufferAge = Math.max(oldestBufferAge, age);
975
- }
976
- }
977
-
978
- return {
979
- activeBuffers: this.buffers.size,
980
- totalBufferedMessages,
981
- oldestBufferAge,
982
- flushesPerformed: this.flushCount
983
- };
984
- }
985
-
986
- cleanup() {
987
- for (const buffer of this.buffers.values()) {
988
- buffer.cleanup();
989
- }
990
- this.buffers.clear();
991
- }
992
- }
993
-
994
- /**
995
- * Transaction Builder for atomic operations
996
- */
997
- class TransactionBuilder {
998
- #client;
999
- #operations = [];
1000
- #requiredLeases = [];
1001
-
1002
- constructor(client) {
1003
- this.#client = client;
1004
- }
1005
-
1006
- /**
1007
- * Add ACK operation to transaction
1008
- * @param {Array|Object} messages - Messages to acknowledge
1009
- * @param {string} status - Status ('completed' or 'failed')
1010
- * @returns {TransactionBuilder} this for chaining
1011
- */
1012
- ack(messages, status = 'completed') {
1013
- const msgs = Array.isArray(messages) ? messages : [messages];
1014
-
1015
- msgs.forEach(msg => {
1016
- const transactionId = msg.transactionId || msg.id || msg;
1017
- const leaseId = msg.leaseId || null;
1018
-
1019
- this.#operations.push({
1020
- type: 'ack',
1021
- transactionId,
1022
- status
1023
- });
1024
-
1025
- if (leaseId) {
1026
- this.#requiredLeases.push(leaseId);
1027
- }
1028
- });
1029
-
1030
- return this;
1031
- }
1032
-
1033
- /**
1034
- * Add PUSH operation to transaction
1035
- * @param {string} queue - Target queue
1036
- * @param {Array|Object} items - Items to push
1037
- * @returns {TransactionBuilder} this for chaining
1038
- */
1039
- push(queue, items) {
1040
- const itemArray = Array.isArray(items) ? items : [items];
1041
-
1042
- this.#operations.push({
1043
- type: 'push',
1044
- items: itemArray.map(item => ({
1045
- queue,
1046
- payload: item
1047
- }))
1048
- });
1049
-
1050
- return this;
1051
- }
1052
-
1053
- /**
1054
- * Add lease extension to transaction
1055
- * @param {string} leaseId - Lease ID to extend
1056
- * @returns {TransactionBuilder} this for chaining
1057
- */
1058
- extend(leaseId) {
1059
- this.#operations.push({
1060
- type: 'extend',
1061
- leaseId
1062
- });
1063
-
1064
- this.#requiredLeases.push(leaseId);
1065
- return this;
1066
- }
1067
-
1068
- /**
1069
- * Execute the transaction
1070
- * @returns {Promise<Object>} Transaction result
1071
- */
1072
- async commit() {
1073
- await this.#client._ensureConnected();
1074
-
1075
- const result = await this.#client._http.post('/api/v1/transaction', {
1076
- operations: this.#operations,
1077
- requiredLeases: [...new Set(this.#requiredLeases)] // Unique leases
1078
- });
1079
-
1080
- if (!result.success) {
1081
- throw new Error(result.error || 'Transaction failed');
1082
- }
1083
-
1084
- return result;
1085
- }
1086
- }
1087
-
1088
- /**
1089
- * Pipeline Builder for message processing workflows
1090
- */
1091
- class PipelineBuilder {
1092
- #client;
1093
- #queue;
1094
- #options = {};
1095
- #processor = null;
1096
- #errorHandler = null;
1097
- #atomicOps = null;
1098
- #leaseRenewal = null;
1099
- #repeatConfig = null;
1100
- #concurrency = 1;
1101
-
1102
- constructor(client, queue) {
1103
- this.#client = client;
1104
- this.#queue = queue;
1105
- }
1106
-
1107
- /**
1108
- * Take messages from the queue
1109
- * @param {number} count - Number of messages to take
1110
- * @param {Object} options - Take options
1111
- * @returns {PipelineBuilder} this for chaining
1112
- */
1113
- take(count, options = {}) {
1114
- this.#options = { ...options, batch: count, limit: count };
1115
- return this;
1116
- }
1117
-
1118
- /**
1119
- * Process messages individually (one at a time)
1120
- * @param {Function} handler - Async function to process a single message
1121
- * @returns {PipelineBuilder} this for chaining
1122
- */
1123
- process(handler) {
1124
- // Wrap the single-message handler to process messages one by one
1125
- this.#processor = async (messages) => {
1126
- const results = [];
1127
- for (const message of messages) {
1128
- const result = await handler(message);
1129
- results.push(result);
1130
- }
1131
- return results;
1132
- };
1133
- return this;
1134
- }
1135
-
1136
- /**
1137
- * Process messages as a batch
1138
- * @param {Function} handler - Async function to process a batch of messages
1139
- * @returns {PipelineBuilder} this for chaining
1140
- */
1141
- processBatch(handler) {
1142
- this.#processor = handler;
1143
- return this;
1144
- }
1145
-
1146
- /**
1147
- * Define atomic operations to execute after processing
1148
- * @param {Function} txBuilder - Function that receives a TransactionBuilder
1149
- * @returns {PipelineBuilder} this for chaining
1150
- */
1151
- atomically(txBuilder) {
1152
- this.#atomicOps = txBuilder;
1153
- return this;
1154
- }
1155
-
1156
- /**
1157
- * Handle errors
1158
- * @param {Function} handler - Error handler function
1159
- * @returns {PipelineBuilder} this for chaining
1160
- */
1161
- onError(handler) {
1162
- this.#errorHandler = handler;
1163
- return this;
1164
- }
1165
-
1166
- /**
1167
- * Enable automatic lease renewal
1168
- * @param {Object} options - Renewal options
1169
- * @returns {PipelineBuilder} this for chaining
1170
- */
1171
- withAutoRenewal(options = {}) {
1172
- this.#leaseRenewal = {
1173
- interval: options.interval || 30000, // Renew every 30s by default
1174
- enabled: true
1175
- };
1176
- return this;
1177
- }
1178
-
1179
- /**
1180
- * Set concurrency level for parallel batch processing
1181
- * @param {number} level - Number of concurrent batches to process
1182
- * @returns {PipelineBuilder} this for chaining
1183
- */
1184
- withConcurrency(level) {
1185
- this.#concurrency = Math.max(1, level);
1186
- return this;
1187
- }
1188
-
1189
- /**
1190
- * Configure repeated execution
1191
- * @param {Object} options - Repeat options
1192
- * @returns {PipelineBuilder} this for chaining
1193
- */
1194
- repeat(options = {}) {
1195
- this.#repeatConfig = {
1196
- maxIterations: options.maxIterations || Infinity,
1197
- delay: options.delay || 0,
1198
- continuous: options.continuous !== undefined ? options.continuous : true, // Default to continuous mode
1199
- waitOnEmpty: options.waitOnEmpty || 1000 // Wait time when no messages
1200
- };
1201
- return this;
1202
- }
1203
-
1204
- /**
1205
- * Execute the pipeline (once or repeatedly based on configuration)
1206
- * @param {Object} options - Execution options
1207
- * @param {boolean} options.returnGenerator - For repeat mode, return generator instead of running loop
1208
- * @returns {Promise<Object>} Result or summary of all iterations
1209
- */
1210
- async execute(options = {}) {
1211
- // If repeat is configured
1212
- if (this.#repeatConfig) {
1213
- // Option to return generator for advanced users
1214
- if (options.returnGenerator) {
1215
- return this.#executeRepeatedly();
1216
- }
1217
-
1218
- // Default: run the loop internally and return summary
1219
- return this.#executeRepeatedlyWithSummary();
1220
- }
1221
-
1222
- // Otherwise execute once
1223
- return this.#executeSingle();
1224
- }
1225
-
1226
- /**
1227
- * Execute the pipeline once (internal)
1228
- * @private
1229
- * @returns {Promise<Object>} Pipeline execution result
1230
- */
1231
- async #executeSingle() {
1232
- // Use local variables to avoid race conditions in parallel execution
1233
- let messages = null;
1234
- let processedMessages = null;
1235
-
1236
- try {
1237
- // 1. Take messages
1238
- messages = await this.#client.takeSingleBatch(this.#queue, this.#options);
1239
-
1240
- if (!messages || messages.length === 0) {
1241
- return { processed: 0, messages: [] };
1242
- }
1243
-
1244
- // 2. Set up auto-renewal if enabled
1245
- let renewalTimer = null;
1246
- if (this.#leaseRenewal?.enabled) {
1247
- renewalTimer = this.#setupAutoRenewal(messages);
1248
- }
1249
-
1250
- try {
1251
- // 3. Process messages
1252
- if (this.#processor) {
1253
- processedMessages = await this.#processor(messages);
1254
- } else {
1255
- processedMessages = messages;
1256
- }
1257
-
1258
- // 4. Execute atomic operations
1259
- if (this.#atomicOps) {
1260
- const tx = new TransactionBuilder(this.#client);
1261
-
1262
- // Apply the atomic operations
1263
- this.#atomicOps(tx, messages, processedMessages);
1264
-
1265
- await tx.commit();
1266
- } else {
1267
- // Default: just ACK the messages
1268
- // Extract consumer group from queue address if present (format: queue@group)
1269
- const atIndex = this.#queue.lastIndexOf('@');
1270
- const consumerGroup = atIndex > 0 ? this.#queue.substring(atIndex + 1) : null;
1271
- const context = consumerGroup ? { group: consumerGroup } : {};
1272
- await this.#client.ack(messages, true, context);
1273
- }
1274
-
1275
- return {
1276
- processed: messages.length,
1277
- messages: processedMessages
1278
- };
1279
-
1280
- } finally {
1281
- // Clean up renewal timer
1282
- if (renewalTimer) {
1283
- clearInterval(renewalTimer);
1284
- }
1285
- }
1286
-
1287
- } catch (error) {
1288
- if (this.#errorHandler) {
1289
- await this.#errorHandler(error, messages || []);
1290
- return { processed: 0, messages: [], error: error.message };
1291
- } else {
1292
- throw error;
1293
- }
1294
- }
1295
- }
1296
-
1297
- /**
1298
- * Execute the pipeline repeatedly (internal)
1299
- * @private
1300
- * @returns {AsyncGenerator} Async generator of results
1301
- */
1302
- async *#executeRepeatedly() {
1303
- const { maxIterations, delay } = this.#repeatConfig;
1304
- let iteration = 0;
1305
-
1306
- while (iteration < maxIterations) {
1307
- const result = await this.#executeSingle();
1308
- yield result;
1309
-
1310
- iteration++;
1311
-
1312
- if (delay > 0) {
1313
- await new Promise(resolve => setTimeout(resolve, delay));
1314
- }
1315
- }
1316
- }
1317
-
1318
- /**
1319
- * Execute the pipeline repeatedly and return summary
1320
- * @private
1321
- * @returns {Promise<Object>} Summary of all iterations
1322
- */
1323
- async #executeRepeatedlyWithSummary() {
1324
- const { maxIterations, delay, continuous, waitOnEmpty } = this.#repeatConfig;
1325
-
1326
- // If concurrency > 1, use parallel workers
1327
- if (this.#concurrency > 1) {
1328
- return this.#executeRepeatedlyParallel();
1329
- }
1330
-
1331
- // Sequential processing
1332
- let iteration = 0;
1333
- let totalProcessed = 0;
1334
- const results = [];
1335
- let consecutiveEmpty = 0;
1336
- const maxConsecutiveEmpty = continuous ? Infinity : 3; // In continuous mode, never stop on empty
1337
-
1338
- while (iteration < maxIterations) {
1339
- const result = await this.#executeSingle();
1340
-
1341
- if (result.processed === 0) {
1342
- consecutiveEmpty++;
1343
-
1344
- // In continuous mode, wait and retry
1345
- if (continuous || consecutiveEmpty < maxConsecutiveEmpty) {
1346
- await new Promise(resolve => setTimeout(resolve, waitOnEmpty));
1347
- continue; // Don't increment iteration for empty results
1348
- } else {
1349
- // Not continuous and too many empty results
1350
- break;
1351
- }
1352
- } else {
1353
- // Reset empty counter on successful batch
1354
- consecutiveEmpty = 0;
1355
- results.push(result);
1356
- totalProcessed += result.processed;
1357
- iteration++;
1358
- }
1359
-
1360
- if (delay > 0 && iteration < maxIterations) {
1361
- await new Promise(resolve => setTimeout(resolve, delay));
1362
- }
1363
- }
1364
-
1365
- return {
1366
- iterations: iteration,
1367
- totalProcessed,
1368
- results,
1369
- summary: {
1370
- averagePerBatch: iteration > 0 ? totalProcessed / iteration : 0,
1371
- completed: iteration >= maxIterations ? 'maxIterations' : 'noMessages',
1372
- concurrency: this.#concurrency,
1373
- continuous
1374
- }
1375
- };
1376
- }
1377
-
1378
- /**
1379
- * Execute pipeline with parallel processing
1380
- * Each worker independently calls take(), allowing them to work on different partitions
1381
- * @private
1382
- * @returns {Promise<Object>} Summary of all parallel executions
1383
- */
1384
- async #executeRepeatedlyParallel() {
1385
- const { maxIterations = Infinity, delay, continuous, waitOnEmpty } = this.#repeatConfig || {};
1386
- const workers = [];
1387
- const results = [];
1388
-
1389
- // Shared state for coordination
1390
- const sharedState = {
1391
- shouldStop: false,
1392
- emptyWorkers: new Set(),
1393
- lock: Promise.resolve()
1394
- };
1395
-
1396
- // Worker function - each worker independently takes and processes messages
1397
- const worker = async (workerId) => {
1398
- let workerIterations = 0;
1399
- let workerProcessed = 0;
1400
- let consecutiveEmpty = 0;
1401
- const maxConsecutiveEmpty = continuous ? Infinity : 10;
1402
-
1403
- while (!sharedState.shouldStop && workerIterations < maxIterations) {
1404
- try {
1405
- // Each worker independently calls executeSingle (which calls take)
1406
- const result = await this.#executeSingle();
1407
-
1408
- if (result.processed === 0) {
1409
- consecutiveEmpty++;
1410
-
1411
- // Add this worker to empty set
1412
- sharedState.emptyWorkers.add(workerId);
1413
-
1414
- // If all workers are empty, consider stopping
1415
- if (!continuous && sharedState.emptyWorkers.size === this.#concurrency) {
1416
- // Double-check with a small delay to avoid race conditions
1417
- await new Promise(resolve => setTimeout(resolve, 200));
1418
-
1419
- // Re-check after delay
1420
- if (sharedState.emptyWorkers.size === this.#concurrency) {
1421
- sharedState.shouldStop = true;
1422
- break;
1423
- }
1424
- }
1425
-
1426
- // Individual worker timeout
1427
- if (!continuous && consecutiveEmpty >= maxConsecutiveEmpty) {
1428
- break;
1429
- }
1430
-
1431
- // Wait before retry
1432
- await new Promise(resolve => setTimeout(resolve, waitOnEmpty || 1000));
1433
- continue; // Don't count empty iterations
1434
- }
1435
-
1436
- // Reset empty counter on successful batch
1437
- consecutiveEmpty = 0;
1438
- sharedState.emptyWorkers.delete(workerId);
1439
-
1440
- // Store result with worker info
1441
- results.push({
1442
- ...result,
1443
- workerId,
1444
- timestamp: new Date().toISOString()
1445
- });
1446
-
1447
- workerProcessed += result.processed;
1448
- workerIterations++;
1449
-
1450
- if (delay > 0) {
1451
- await new Promise(resolve => setTimeout(resolve, delay));
1452
- }
1453
- } catch (error) {
1454
- console.error(`Worker ${workerId} error:`, error);
1455
- if (this.#errorHandler) {
1456
- await this.#errorHandler(error, []);
1457
- } else {
1458
- sharedState.shouldStop = true;
1459
- throw error;
1460
- }
1461
- }
1462
- }
1463
-
1464
- return {
1465
- workerId,
1466
- iterations: workerIterations,
1467
- processed: workerProcessed,
1468
- consecutiveEmpty
1469
- };
1470
- };
1471
-
1472
- // Start parallel workers
1473
- console.log(`🚀 Starting ${this.#concurrency} parallel workers...`);
1474
- for (let i = 0; i < this.#concurrency; i++) {
1475
- workers.push(worker(i));
1476
- }
1477
-
1478
- // Wait for all workers
1479
- const workerResults = await Promise.all(workers);
1480
-
1481
- // Aggregate results
1482
- let totalProcessed = 0;
1483
- let totalIterations = 0;
1484
-
1485
- workerResults.forEach(wr => {
1486
- totalProcessed += wr.processed;
1487
- totalIterations += wr.iterations;
1488
- if (wr.iterations > 0) {
1489
- console.log(`Worker ${wr.workerId}: Processed ${wr.processed} messages in ${wr.iterations} batches`);
1490
- }
1491
- });
1492
-
1493
- return {
1494
- iterations: totalIterations,
1495
- totalProcessed,
1496
- results,
1497
- workers: workerResults,
1498
- summary: {
1499
- averagePerBatch: totalIterations > 0 ? totalProcessed / totalIterations : 0,
1500
- completed: totalIterations >= (maxIterations * this.#concurrency) ? 'maxIterations' : 'noMessages',
1501
- concurrency: this.#concurrency,
1502
- workersUtilized: workerResults.filter(w => w.iterations > 0).length,
1503
- continuous
1504
- }
1505
- };
1506
- }
1507
-
1508
- /**
1509
- * Set up automatic lease renewal
1510
- * @private
1511
- */
1512
- #setupAutoRenewal(messages) {
1513
- const leaseIds = messages
1514
- .map(m => m.leaseId)
1515
- .filter(id => id != null);
1516
-
1517
- if (leaseIds.length === 0) return null;
1518
-
1519
- return setInterval(async () => {
1520
- try {
1521
- // Extend all leases using the client's renewLease method
1522
- for (const leaseId of leaseIds) {
1523
- const result = await this.#client.renewLease(leaseId);
1524
- if (!result.success) {
1525
- console.error('Lease renewal failed:', result.error);
1526
- }
1527
- }
1528
- } catch (error) {
1529
- console.error('Failed to renew lease:', error);
1530
- }
1531
- }, this.#leaseRenewal.interval);
1532
- }
1533
- }
1534
-
1535
- // Export the class as default as well for convenience
1536
- export default Queen;