queen-mq 0.1.6 → 0.2.11
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 +166 -2078
- package/{src → client-js}/benchmark/consumer.js +5 -3
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/client-js/client/client.js +1511 -0
- package/{src → client-js}/client/index.js +1 -4
- package/{src → client-js}/client/utils/http.js +3 -2
- package/{src → client-js}/client/utils/retry.js +7 -1
- package/client-js/test/advanced-client-tests.js +761 -0
- package/{src → client-js}/test/advanced-pattern-tests.js +2 -2
- package/{src → client-js}/test/bus-mode-tests.js +6 -6
- package/{src → client-js}/test/core-tests.js +8 -3
- package/{src → client-js}/test/edge-case-tests.js +4 -3
- package/client-js/test/human.js +162 -0
- package/client-js/test/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +78 -1
- package/package.json +8 -29
- package/init-db.js +0 -20
- package/src/client/client.js +0 -586
- package/src/cluster-server.js +0 -242
- package/src/config.js +0 -229
- package/src/database/connection.js +0 -129
- package/src/database/poolManager.js +0 -199
- package/src/database/schema-v2.sql +0 -278
- package/src/managers/eventManager.js +0 -59
- package/src/managers/queueManagerOptimized.js +0 -1431
- package/src/managers/resourceCache.js +0 -96
- package/src/managers/systemEventManager.js +0 -132
- package/src/routes/ack.js +0 -26
- package/src/routes/configure.js +0 -46
- package/src/routes/messages.js +0 -368
- package/src/routes/pop.js +0 -69
- package/src/routes/push.js +0 -28
- package/src/routes/resources.js +0 -330
- package/src/routes/status.js +0 -1037
- package/src/server.js +0 -1528
- package/src/test/MIGRATION_ISSUES.md +0 -174
- package/src/test/test.js +0 -4524
- package/src/utils/streaming.js +0 -231
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
- package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
- package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
- package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
- package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
- package/src/webapp-dist/assets/queen-logo.svg +0 -263
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
- package/src/webapp-dist/index.html +0 -15
- package/src/websocket/wsServer.js +0 -228
- /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
- /package/{src → client-js}/benchmark/producer_multi.js +0 -0
- /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
- /package/{src → client-js}/services/encryptionService.js +0 -0
- /package/{src → client-js}/services/evictionService.js +0 -0
- /package/{src → client-js}/services/retentionService.js +0 -0
- /package/{src → client-js}/services/startupSync.js +0 -0
- /package/{src → client-js}/test/README.md +0 -0
- /package/{src → client-js}/test/enterprise-tests.js +0 -0
- /package/{src → client-js}/test/partition-locking-tests.js +0 -0
- /package/{src → client-js}/test/utils.js +0 -0
- /package/{src → client-js}/test/window-buffer-test.js +0 -0
- /package/{src → client-js}/utils/logger.js +0 -0
- /package/{src → client-js}/utils/uuid.js +0 -0
package/src/client/client.js
DELETED
|
@@ -1,586 +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
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Minimalist Queen Message Queue Client
|
|
7
|
-
*
|
|
8
|
-
* Simple, powerful API with just 4 methods:
|
|
9
|
-
* - queue: Configure a queue
|
|
10
|
-
* - push: Send messages
|
|
11
|
-
* - take: Receive messages (async iterator)
|
|
12
|
-
* - ack: Acknowledge messages
|
|
13
|
-
*/
|
|
14
|
-
export class Queen {
|
|
15
|
-
#config;
|
|
16
|
-
#http;
|
|
17
|
-
#loadBalancer;
|
|
18
|
-
#connected = false;
|
|
19
|
-
|
|
20
|
-
constructor(config = {}) {
|
|
21
|
-
this.#config = {
|
|
22
|
-
baseUrls: null,
|
|
23
|
-
loadBalancingStrategy: LoadBalancingStrategy.ROUND_ROBIN,
|
|
24
|
-
enableFailover: true,
|
|
25
|
-
timeout: 30000,
|
|
26
|
-
retryAttempts: 3,
|
|
27
|
-
retryDelay: 1000,
|
|
28
|
-
...config
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Ensure HTTP client is connected
|
|
34
|
-
*/
|
|
35
|
-
async #ensureConnected() {
|
|
36
|
-
if (this.#connected) return;
|
|
37
|
-
|
|
38
|
-
const { baseUrl, baseUrls, loadBalancingStrategy, enableFailover, timeout } = this.#config;
|
|
39
|
-
|
|
40
|
-
if (baseUrls && Array.isArray(baseUrls) && baseUrls.length > 0) {
|
|
41
|
-
// Multiple servers with load balancing
|
|
42
|
-
this.#loadBalancer = createLoadBalancer(baseUrls, loadBalancingStrategy);
|
|
43
|
-
this.#http = createLoadBalancedHttpClient({
|
|
44
|
-
baseUrls,
|
|
45
|
-
loadBalancer: this.#loadBalancer,
|
|
46
|
-
timeout,
|
|
47
|
-
enableFailover
|
|
48
|
-
});
|
|
49
|
-
} else {
|
|
50
|
-
// Single server
|
|
51
|
-
const singleUrl = baseUrls && baseUrls.length === 1 ? baseUrls[0] : baseUrl;
|
|
52
|
-
this.#http = createHttpClient({ baseUrl: singleUrl, timeout });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
this.#connected = true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Validate if a string is a valid UUID v4
|
|
60
|
-
*/
|
|
61
|
-
#isValidUUID(str) {
|
|
62
|
-
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
63
|
-
return uuidRegex.test(str);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parse address string into components
|
|
68
|
-
* Examples:
|
|
69
|
-
* - "myqueue" -> { queue: "myqueue" }
|
|
70
|
-
* - "myqueue/urgent" -> { queue: "myqueue", partition: "urgent" }
|
|
71
|
-
* - "myqueue@workers" -> { queue: "myqueue", consumerGroup: "workers" }
|
|
72
|
-
* - "myqueue/urgent@workers" -> { queue: "myqueue", partition: "urgent", consumerGroup: "workers" }
|
|
73
|
-
* - "namespace:billing" -> { namespace: "billing" }
|
|
74
|
-
* - "task:process" -> { task: "process" }
|
|
75
|
-
* - "namespace:billing/task:process" -> { namespace: "billing", task: "process" }
|
|
76
|
-
*/
|
|
77
|
-
#parseAddress(address) {
|
|
78
|
-
// Check for namespace/task pattern
|
|
79
|
-
if (address.includes('namespace:') || address.includes('task:')) {
|
|
80
|
-
const parts = {};
|
|
81
|
-
|
|
82
|
-
// Extract consumer group if present (after @)
|
|
83
|
-
let workingAddress = address;
|
|
84
|
-
const atIndex = address.lastIndexOf('@');
|
|
85
|
-
if (atIndex > 0) {
|
|
86
|
-
parts.consumerGroup = address.substring(atIndex + 1);
|
|
87
|
-
workingAddress = address.substring(0, atIndex);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const segments = workingAddress.split('/');
|
|
91
|
-
|
|
92
|
-
for (const segment of segments) {
|
|
93
|
-
if (segment.startsWith('namespace:')) {
|
|
94
|
-
parts.namespace = segment.substring(10);
|
|
95
|
-
} else if (segment.startsWith('task:')) {
|
|
96
|
-
parts.task = segment.substring(5);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return parts;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Standard queue[/partition][@group] pattern
|
|
104
|
-
const match = address.match(/^([^/@]+)(?:\/([^@]+))?(?:@(.+))?$/);
|
|
105
|
-
if (!match) {
|
|
106
|
-
throw new Error(`Invalid address format: ${address}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
queue: match[1],
|
|
111
|
-
partition: match[2] || 'Default',
|
|
112
|
-
consumerGroup: match[3] || null
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Configure a queue with options
|
|
118
|
-
* @param {string} name - Queue name
|
|
119
|
-
* @param {Object} options - Queue configuration options
|
|
120
|
-
* @param {Object} metadata - Optional metadata { namespace, task }
|
|
121
|
-
*/
|
|
122
|
-
async queue(name, options = {}, metadata = {}) {
|
|
123
|
-
await this.#ensureConnected();
|
|
124
|
-
|
|
125
|
-
const { namespace, task } = metadata;
|
|
126
|
-
|
|
127
|
-
const result = await withRetry(
|
|
128
|
-
() => this.#http.post('/api/v1/configure', {
|
|
129
|
-
queue: name,
|
|
130
|
-
namespace: namespace || null,
|
|
131
|
-
task: task || null,
|
|
132
|
-
options
|
|
133
|
-
}),
|
|
134
|
-
this.#config.retryAttempts,
|
|
135
|
-
this.#config.retryDelay
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
if (result && result.error) {
|
|
139
|
-
throw new Error(result.error);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Push messages to a queue
|
|
147
|
-
* @param {string} address - Queue address (e.g., "myqueue" or "myqueue/partition")
|
|
148
|
-
* @param {Object|Array} payload - Single message or array of messages
|
|
149
|
-
* @param {Object} options - Optional message properties { transactionId, traceId }
|
|
150
|
-
*/
|
|
151
|
-
async push(address, payload, options = {}) {
|
|
152
|
-
await this.#ensureConnected();
|
|
153
|
-
|
|
154
|
-
const { queue, partition } = this.#parseAddress(address);
|
|
155
|
-
|
|
156
|
-
// Handle both single and array inputs
|
|
157
|
-
const items = Array.isArray(payload) ? payload : [payload];
|
|
158
|
-
|
|
159
|
-
// Format items for the API
|
|
160
|
-
const formattedItems = items.map(item => {
|
|
161
|
-
// If item is an object with special properties, extract them
|
|
162
|
-
const isMessageObject = typeof item === 'object' && item !== null &&
|
|
163
|
-
(item._payload || item._transactionId || item._traceId);
|
|
164
|
-
|
|
165
|
-
if (isMessageObject) {
|
|
166
|
-
const result = {
|
|
167
|
-
queue,
|
|
168
|
-
partition,
|
|
169
|
-
payload: item._payload || item,
|
|
170
|
-
transactionId: item._transactionId || item.transactionId
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// Include traceId if provided and valid UUID
|
|
174
|
-
const traceId = item._traceId || item.traceId;
|
|
175
|
-
if (traceId && this.#isValidUUID(traceId)) {
|
|
176
|
-
result.traceId = traceId;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return result;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Otherwise use the item as payload and apply options
|
|
183
|
-
const result = {
|
|
184
|
-
queue,
|
|
185
|
-
partition,
|
|
186
|
-
payload: item,
|
|
187
|
-
transactionId: options.transactionId || (item && typeof item === 'object' ? item.transactionId : undefined)
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Include traceId if provided and valid UUID
|
|
191
|
-
const traceId = options.traceId || (item && typeof item === 'object' ? item.traceId : undefined);
|
|
192
|
-
if (traceId && this.#isValidUUID(traceId)) {
|
|
193
|
-
result.traceId = traceId;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return result;
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const result = await withRetry(
|
|
200
|
-
() => this.#http.post('/api/v1/push', {
|
|
201
|
-
items: formattedItems
|
|
202
|
-
}),
|
|
203
|
-
this.#config.retryAttempts,
|
|
204
|
-
this.#config.retryDelay
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
if (result && result.error) {
|
|
208
|
-
throw new Error(result.error);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return result;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Internal method that yields batches of messages (arrays)
|
|
216
|
-
* @private
|
|
217
|
-
* @param {string} address - Queue address
|
|
218
|
-
* @param {Object} options - Options for taking messages
|
|
219
|
-
* @yields {Array} Arrays of message objects
|
|
220
|
-
*/
|
|
221
|
-
async *#takeInternal(address, options = {}) {
|
|
222
|
-
await this.#ensureConnected();
|
|
223
|
-
|
|
224
|
-
const { queue, partition, consumerGroup, namespace, task } = this.#parseAddress(address);
|
|
225
|
-
const {
|
|
226
|
-
limit = null,
|
|
227
|
-
batch = 1,
|
|
228
|
-
wait = false,
|
|
229
|
-
timeout = 30000,
|
|
230
|
-
subscriptionMode = null,
|
|
231
|
-
subscriptionFrom = null,
|
|
232
|
-
idleTimeout = null
|
|
233
|
-
} = options;
|
|
234
|
-
|
|
235
|
-
let totalCount = 0;
|
|
236
|
-
let consecutiveEmptyResponses = 0;
|
|
237
|
-
let consecutiveNetworkErrors = 0;
|
|
238
|
-
const maxConsecutiveEmpty = 3;
|
|
239
|
-
let lastMessageTime = idleTimeout ? Date.now() : null;
|
|
240
|
-
|
|
241
|
-
while (true) {
|
|
242
|
-
// Check if we've reached the limit
|
|
243
|
-
if (limit && totalCount >= limit) break;
|
|
244
|
-
|
|
245
|
-
// Check idle timeout
|
|
246
|
-
if (idleTimeout && lastMessageTime) {
|
|
247
|
-
const idleTime = Date.now() - lastMessageTime;
|
|
248
|
-
if (idleTime >= idleTimeout) {
|
|
249
|
-
break; // Exit if idle time exceeded
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Calculate batch size respecting the limit
|
|
254
|
-
const effectiveBatch = limit ? Math.min(batch, limit - totalCount) : batch;
|
|
255
|
-
|
|
256
|
-
// Build the request path and parameters
|
|
257
|
-
let path;
|
|
258
|
-
const params = new URLSearchParams({
|
|
259
|
-
wait: wait.toString(),
|
|
260
|
-
timeout: timeout.toString(),
|
|
261
|
-
batch: effectiveBatch.toString()
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// Add consumer group parameters if provided
|
|
265
|
-
if (consumerGroup) params.append('consumerGroup', consumerGroup);
|
|
266
|
-
if (subscriptionMode) params.append('subscriptionMode', subscriptionMode);
|
|
267
|
-
if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
|
|
268
|
-
|
|
269
|
-
// Determine the endpoint based on parameters
|
|
270
|
-
if (queue) {
|
|
271
|
-
if (partition && partition !== 'Default') {
|
|
272
|
-
path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
|
|
273
|
-
} else {
|
|
274
|
-
path = `/api/v1/pop/queue/${queue}`;
|
|
275
|
-
}
|
|
276
|
-
} else if (namespace || task) {
|
|
277
|
-
// Pop by namespace/task
|
|
278
|
-
path = '/api/v1/pop';
|
|
279
|
-
if (namespace) params.append('namespace', namespace);
|
|
280
|
-
if (task) params.append('task', task);
|
|
281
|
-
} else {
|
|
282
|
-
throw new Error('Must specify either queue, namespace, or task');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
// For long polling, use a slightly longer client timeout
|
|
287
|
-
const clientTimeout = wait ? timeout + 5000 : timeout;
|
|
288
|
-
|
|
289
|
-
const result = await this.#http.get(`${path}?${params}`, clientTimeout);
|
|
290
|
-
|
|
291
|
-
// Handle empty response
|
|
292
|
-
if (!result || !result.messages || result.messages.length === 0) {
|
|
293
|
-
if (wait) {
|
|
294
|
-
// For long polling, immediately retry
|
|
295
|
-
continue;
|
|
296
|
-
} else {
|
|
297
|
-
// For non-waiting mode, stop after several empty responses
|
|
298
|
-
consecutiveEmptyResponses++;
|
|
299
|
-
if (consecutiveEmptyResponses >= maxConsecutiveEmpty) {
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
// Small delay before retry
|
|
303
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Reset counters on successful fetch
|
|
309
|
-
consecutiveEmptyResponses = 0;
|
|
310
|
-
consecutiveNetworkErrors = 0;
|
|
311
|
-
|
|
312
|
-
// Update last message time if tracking idle timeout
|
|
313
|
-
if (idleTimeout) {
|
|
314
|
-
lastMessageTime = Date.now();
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Filter out null/undefined messages
|
|
318
|
-
const messages = result.messages.filter(msg => msg != null);
|
|
319
|
-
|
|
320
|
-
if (messages.length > 0) {
|
|
321
|
-
totalCount += messages.length;
|
|
322
|
-
yield messages;
|
|
323
|
-
|
|
324
|
-
// If we've hit the limit, stop
|
|
325
|
-
if (limit && totalCount >= limit) {
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
} catch (error) {
|
|
331
|
-
// Check if this is a timeout error (expected for long polling)
|
|
332
|
-
const isTimeoutError = error.name === 'AbortError' ||
|
|
333
|
-
error.message?.includes('abort') ||
|
|
334
|
-
error.message?.includes('timeout');
|
|
335
|
-
|
|
336
|
-
if (isTimeoutError && wait) {
|
|
337
|
-
// For long polling timeout, immediately retry
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Check if this is a network error (connection refused, socket closed, etc.)
|
|
342
|
-
const isNetworkError = error.message?.includes('fetch failed') ||
|
|
343
|
-
error.message?.includes('ECONNREFUSED') ||
|
|
344
|
-
error.message?.includes('ECONNRESET') ||
|
|
345
|
-
error.message?.includes('closed') ||
|
|
346
|
-
error.cause?.code === 'UND_ERR_SOCKET' ||
|
|
347
|
-
error.code === 'ECONNREFUSED' ||
|
|
348
|
-
error.code === 'ECONNRESET';
|
|
349
|
-
|
|
350
|
-
if (isNetworkError) {
|
|
351
|
-
// Log the network error
|
|
352
|
-
console.warn(`Network error while polling queue: ${error.message}`);
|
|
353
|
-
|
|
354
|
-
// Retry with exponential backoff (capped at 30 seconds)
|
|
355
|
-
const retryDelay = Math.min(this.#config.retryDelay * Math.pow(2, Math.min(consecutiveNetworkErrors, 10)), 30000);
|
|
356
|
-
console.warn(`Retrying in ${retryDelay}ms... (attempt ${consecutiveNetworkErrors + 1})`);
|
|
357
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
358
|
-
|
|
359
|
-
consecutiveNetworkErrors++;
|
|
360
|
-
|
|
361
|
-
// Continue retrying indefinitely
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// For other errors, throw
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Take messages from a queue one at a time (async iterator)
|
|
373
|
-
* @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
|
|
374
|
-
* @param {Object} options - Options for taking messages
|
|
375
|
-
* @yields {Object} Individual message objects
|
|
376
|
-
*/
|
|
377
|
-
async *take(address, options = {}) {
|
|
378
|
-
let count = 0;
|
|
379
|
-
const { limit = null } = options;
|
|
380
|
-
|
|
381
|
-
for await (const messages of this.#takeInternal(address, options)) {
|
|
382
|
-
for (const message of messages) {
|
|
383
|
-
yield message;
|
|
384
|
-
count++;
|
|
385
|
-
|
|
386
|
-
// Double-check limit (internal method also checks, but this ensures exact limit)
|
|
387
|
-
if (limit && count >= limit) {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Take messages from a queue in batches (async iterator)
|
|
396
|
-
* @param {string} address - Queue address (e.g., "myqueue", "myqueue/partition", "myqueue@group")
|
|
397
|
-
* @param {Object} options - Options for taking messages
|
|
398
|
-
* @yields {Array} Arrays of message objects
|
|
399
|
-
*/
|
|
400
|
-
async *takeBatch(address, options = {}) {
|
|
401
|
-
for await (const messages of this.#takeInternal(address, options)) {
|
|
402
|
-
// Only yield non-empty batches
|
|
403
|
-
if (messages && messages.length > 0) {
|
|
404
|
-
yield messages;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Acknowledge a message or batch of messages
|
|
411
|
-
* @param {Object|string|Array} message - Message object, transaction ID, or array of messages
|
|
412
|
-
* @param {boolean|string} status - true for success, false for failure, or 'retry'
|
|
413
|
-
* @param {Object} context - Optional context (e.g., { group: 'workers', error: 'reason' })
|
|
414
|
-
*/
|
|
415
|
-
async ack(message, status = true, context = {}) {
|
|
416
|
-
await this.#ensureConnected();
|
|
417
|
-
|
|
418
|
-
// Handle batch acknowledgment
|
|
419
|
-
if (Array.isArray(message)) {
|
|
420
|
-
if (message.length === 0) {
|
|
421
|
-
return { processed: 0, results: [] };
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Check if messages have individual status (Option B pattern)
|
|
425
|
-
const hasIndividualStatus = message.some(msg =>
|
|
426
|
-
typeof msg === 'object' && msg !== null && ('_status' in msg || '_error' in msg)
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
let acknowledgments;
|
|
430
|
-
|
|
431
|
-
if (hasIndividualStatus) {
|
|
432
|
-
// Option B: Each message has its own status
|
|
433
|
-
acknowledgments = message.map(msg => {
|
|
434
|
-
// Extract transaction ID
|
|
435
|
-
let transactionId;
|
|
436
|
-
if (typeof msg === 'string') {
|
|
437
|
-
transactionId = msg;
|
|
438
|
-
} else if (typeof msg === 'object' && msg !== null) {
|
|
439
|
-
transactionId = msg.transactionId || msg.id;
|
|
440
|
-
if (!transactionId) {
|
|
441
|
-
throw new Error('Message object must have transactionId or id property');
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
throw new Error('Invalid message in batch');
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Get individual status or fall back to parameter
|
|
448
|
-
let msgStatus = msg._status !== undefined ? msg._status : status;
|
|
449
|
-
let statusStr;
|
|
450
|
-
if (typeof msgStatus === 'boolean') {
|
|
451
|
-
statusStr = msgStatus ? 'completed' : 'failed';
|
|
452
|
-
} else {
|
|
453
|
-
statusStr = msgStatus;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
transactionId,
|
|
458
|
-
status: statusStr,
|
|
459
|
-
error: msg._error || context.error || null
|
|
460
|
-
};
|
|
461
|
-
});
|
|
462
|
-
} else {
|
|
463
|
-
// Option A: Same status for all messages
|
|
464
|
-
const statusStr = typeof status === 'boolean'
|
|
465
|
-
? (status ? 'completed' : 'failed')
|
|
466
|
-
: status;
|
|
467
|
-
|
|
468
|
-
acknowledgments = message.map(msg => {
|
|
469
|
-
// Extract transaction ID
|
|
470
|
-
let transactionId;
|
|
471
|
-
if (typeof msg === 'string') {
|
|
472
|
-
transactionId = msg;
|
|
473
|
-
} else if (typeof msg === 'object' && msg !== null) {
|
|
474
|
-
transactionId = msg.transactionId || msg.id;
|
|
475
|
-
if (!transactionId) {
|
|
476
|
-
throw new Error('Message object must have transactionId or id property');
|
|
477
|
-
}
|
|
478
|
-
} else {
|
|
479
|
-
throw new Error('Invalid message in batch');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
transactionId,
|
|
484
|
-
status: statusStr,
|
|
485
|
-
error: context.error || null
|
|
486
|
-
};
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Call batch ack endpoint
|
|
491
|
-
const result = await withRetry(
|
|
492
|
-
() => this.#http.post('/api/v1/ack/batch', {
|
|
493
|
-
acknowledgments,
|
|
494
|
-
consumerGroup: context.group || null
|
|
495
|
-
}),
|
|
496
|
-
this.#config.retryAttempts,
|
|
497
|
-
this.#config.retryDelay
|
|
498
|
-
);
|
|
499
|
-
|
|
500
|
-
if (result && result.error) {
|
|
501
|
-
throw new Error(result.error);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return result;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Handle single message acknowledgment
|
|
508
|
-
// Extract transaction ID
|
|
509
|
-
let transactionId;
|
|
510
|
-
if (typeof message === 'string') {
|
|
511
|
-
transactionId = message;
|
|
512
|
-
} else if (typeof message === 'object' && message !== null) {
|
|
513
|
-
transactionId = message.transactionId || message.id;
|
|
514
|
-
if (!transactionId) {
|
|
515
|
-
throw new Error('Message object must have transactionId or id property');
|
|
516
|
-
}
|
|
517
|
-
} else {
|
|
518
|
-
throw new Error('Message must be a string (transaction ID) or object with transactionId');
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Determine status string
|
|
522
|
-
let statusStr;
|
|
523
|
-
if (typeof status === 'boolean') {
|
|
524
|
-
statusStr = status ? 'completed' : 'failed';
|
|
525
|
-
} else {
|
|
526
|
-
statusStr = status; // Allow custom status like 'retry'
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Build request body
|
|
530
|
-
const body = {
|
|
531
|
-
transactionId,
|
|
532
|
-
status: statusStr,
|
|
533
|
-
error: context.error || null,
|
|
534
|
-
consumerGroup: context.group || null
|
|
535
|
-
};
|
|
536
|
-
|
|
537
|
-
const result = await withRetry(
|
|
538
|
-
() => this.#http.post('/api/v1/ack', body),
|
|
539
|
-
this.#config.retryAttempts,
|
|
540
|
-
this.#config.retryDelay
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
if (result && result.error) {
|
|
544
|
-
throw new Error(result.error);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return result;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Delete a queue (removes queue and all its messages/partitions)
|
|
552
|
-
* @param {string} name - Queue name to delete
|
|
553
|
-
* @returns {Promise<Object>} Deletion result
|
|
554
|
-
*/
|
|
555
|
-
async queueDelete(name) {
|
|
556
|
-
await this.#ensureConnected();
|
|
557
|
-
|
|
558
|
-
if (typeof name !== 'string' || !name) {
|
|
559
|
-
throw new Error('Queue name must be a non-empty string');
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
const result = await withRetry(
|
|
563
|
-
() => this.#http.delete(`/api/v1/resources/queues/${encodeURIComponent(name)}`),
|
|
564
|
-
this.#config.retryAttempts,
|
|
565
|
-
this.#config.retryDelay
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
if (result && result.error) {
|
|
569
|
-
throw new Error(result.error);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
return result;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Close the client connection
|
|
577
|
-
*/
|
|
578
|
-
async close() {
|
|
579
|
-
this.#connected = false;
|
|
580
|
-
this.#http = null;
|
|
581
|
-
this.#loadBalancer = null;
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Export the class as default as well for convenience
|
|
586
|
-
export default Queen;
|