queen-mq 0.3.1 → 0.6.3
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 +309 -167
- package/client-js/client-v2/Queen.js +66 -0
- package/client-js/client-v2/README.md +67 -10
- package/client-js/client-v2/builders/QueueBuilder.js +7 -1
- package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
- package/client-js/client-v2/stream/StreamBuilder.js +72 -0
- package/client-js/client-v2/stream/StreamConsumer.js +156 -0
- package/client-js/client-v2/stream/Window.js +140 -0
- package/client-js/test-v2/GETTING_STARTED.md +27 -0
- package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
- package/client-js/test-v2/README_SUBSCRIPTION_TESTS.md +201 -0
- package/client-js/test-v2/consume.js +11 -0
- package/client-js/test-v2/dlq.js +1 -1
- package/client-js/test-v2/load.js +2 -0
- package/client-js/test-v2/maintenance.js +261 -0
- package/client-js/test-v2/retention.js +69 -0
- package/client-js/test-v2/run.js +7 -1
- package/client-js/test-v2/subscription.js +198 -18
- package/client-js/test-v2/transaction.js +66 -4
- package/package.json +2 -2
- package/client-js/client/client.js +0 -1536
- package/client-js/client/index.js +0 -4
- package/client-js/client/utils/http.js +0 -173
- package/client-js/client/utils/loadBalancer.js +0 -152
- package/client-js/client/utils/retry.js +0 -41
- package/client-js/services/encryptionService.js +0 -82
- package/client-js/services/evictionService.js +0 -160
- package/client-js/services/retentionService.js +0 -162
- package/client-js/services/startupSync.js +0 -35
- package/client-js/test/README.md +0 -224
- package/client-js/test/advanced-client-tests.js +0 -761
- package/client-js/test/advanced-pattern-tests.js +0 -1137
- package/client-js/test/bus-mode-tests.js +0 -361
- package/client-js/test/core-tests.js +0 -457
- package/client-js/test/edge-case-tests.js +0 -562
- package/client-js/test/enterprise-tests.js +0 -637
- package/client-js/test/human.js +0 -162
- package/client-js/test/partition-locking-tests.js +0 -545
- package/client-js/test/partition-transaction-tests.js +0 -482
- package/client-js/test/qos0-tests.js +0 -334
- package/client-js/test/test-new.js +0 -370
- package/client-js/test/utils.js +0 -169
- package/client-js/test/window-buffer-test.js +0 -114
- package/client-js/utils/logger.js +0 -44
- 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;
|