queen-mq 0.1.2 → 0.1.4

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.
@@ -1,518 +0,0 @@
1
- import { httpRequest, createHttpClient, createLoadBalancedHttpClient } from './utils/http.js';
2
- import { withRetry } from './utils/retry.js';
3
- import { createLoadBalancer, LoadBalancingStrategy } from './utils/loadBalancer.js';
4
-
5
- export const createQueenClient = (options = {}) => {
6
- const {
7
- baseUrl = 'http://localhost:6632',
8
- baseUrls = null, // Array of base URLs for load balancing
9
- loadBalancingStrategy = LoadBalancingStrategy.ROUND_ROBIN,
10
- enableFailover = true,
11
- timeout = 30000,
12
- retryAttempts = 3,
13
- retryDelay = 1000
14
- } = options;
15
-
16
- // Determine if we're using load balancing or single server
17
- let http;
18
- let loadBalancer = null;
19
-
20
- if (baseUrls && Array.isArray(baseUrls) && baseUrls.length > 0) {
21
- // Multiple servers with load balancing
22
- loadBalancer = createLoadBalancer(baseUrls, loadBalancingStrategy);
23
- http = createLoadBalancedHttpClient({
24
- baseUrls,
25
- loadBalancer,
26
- timeout,
27
- enableFailover
28
- });
29
- } else {
30
- // Single server (backward compatibility)
31
- const singleUrl = baseUrls && baseUrls.length === 1 ? baseUrls[0] : baseUrl;
32
- http = createHttpClient({ baseUrl: singleUrl, timeout });
33
- }
34
-
35
- const configure = async ({ queue, namespace, task, options = {} }) => {
36
- const result = await withRetry(
37
- () => http.post('/api/v1/configure', { queue, namespace, task, options }),
38
- retryAttempts,
39
- retryDelay
40
- );
41
-
42
- // Check if the response contains an error
43
- if (result && result.error) {
44
- throw new Error(result.error);
45
- }
46
-
47
- return result;
48
- };
49
-
50
- // Push messages to queue
51
- const push = async ({ items, config = {} }) => {
52
- if (!Array.isArray(items)) {
53
- items = [items];
54
- }
55
-
56
- // Ensure each item has the correct structure for V2
57
- const v2Items = items.map(item => ({
58
- queue: item.queue,
59
- partition: item.partition ?? 'Default',
60
- // Use 'payload' if it exists (even if null), otherwise fall back to 'data'
61
- payload: 'payload' in item ? item.payload : item.data,
62
- transactionId: item.transactionId
63
- }));
64
-
65
- const result = await withRetry(
66
- () => http.post('/api/v1/push', { items: v2Items, config }),
67
- retryAttempts,
68
- retryDelay
69
- );
70
-
71
- // Check if the response contains an error
72
- if (result && result.error) {
73
- throw new Error(result.error);
74
- }
75
-
76
- return result;
77
- };
78
-
79
- // Pop messages from queue (with consumer group support)
80
- const pop = async (options = {}) => {
81
- const {
82
- queue,
83
- partition,
84
- namespace,
85
- task,
86
- consumerGroup,
87
- subscriptionMode,
88
- subscriptionFrom,
89
- wait = false,
90
- timeout = 30000,
91
- batch = 1
92
- } = options;
93
-
94
- let path;
95
- const params = new URLSearchParams({
96
- wait: wait.toString(),
97
- timeout: timeout.toString(),
98
- batch: batch.toString()
99
- });
100
-
101
- // Add consumer group parameters if provided
102
- if (consumerGroup) params.append('consumerGroup', consumerGroup);
103
- if (subscriptionMode) params.append('subscriptionMode', subscriptionMode);
104
- if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
105
-
106
- // Determine the appropriate endpoint based on parameters
107
- if (queue && partition) {
108
- // Pop from specific partition
109
- path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
110
- } else if (queue) {
111
- // Pop from any partition in queue
112
- path = `/api/v1/pop/queue/${queue}`;
113
- } else if (namespace || task) {
114
- // Pop with filters
115
- path = '/api/v1/pop';
116
- if (namespace) params.append('namespace', namespace);
117
- if (task) params.append('task', task);
118
- } else {
119
- throw new Error('Must specify either queue, namespace, or task for pop operation');
120
- }
121
-
122
- // For long polling, use a slightly longer client timeout than server timeout
123
- const clientTimeout = wait ? timeout + 5000 : timeout;
124
-
125
- const result = await http.get(`${path}?${params}`, clientTimeout);
126
-
127
- // Return empty array if no messages (204 response)
128
- if (!result) {
129
- return { messages: [] };
130
- }
131
-
132
- return result;
133
- };
134
-
135
- // Acknowledge a message (with consumer group support)
136
- const ack = async (transactionId, status = 'completed', error = null, consumerGroup = null) => {
137
- const result = await withRetry(
138
- () => http.post('/api/v1/ack', { transactionId, status, error, consumerGroup }),
139
- retryAttempts,
140
- retryDelay
141
- );
142
-
143
- // Check if the response contains an error
144
- if (result && result.error) {
145
- throw new Error(result.error);
146
- }
147
-
148
- return result;
149
- };
150
-
151
- // Batch acknowledge messages (with consumer group support)
152
- const ackBatch = async (acknowledgments, consumerGroup = null) => {
153
- const result = await withRetry(
154
- () => http.post('/api/v1/ack/batch', { acknowledgments, consumerGroup }),
155
- retryAttempts,
156
- retryDelay
157
- );
158
-
159
- // Check if the response contains an error
160
- if (result && result.error) {
161
- throw new Error(result.error);
162
- }
163
-
164
- return result;
165
- };
166
-
167
- // Message management
168
- const messages = {
169
- // List messages with filters
170
- list: async (filters = {}) => {
171
- const params = new URLSearchParams();
172
- if (filters.queue) params.append('queue', filters.queue);
173
- if (filters.partition) params.append('partition', filters.partition);
174
- if (filters.namespace) params.append('namespace', filters.namespace);
175
- if (filters.task) params.append('task', filters.task);
176
- if (filters.status) params.append('status', filters.status);
177
- if (filters.limit) params.append('limit', filters.limit);
178
- if (filters.offset) params.append('offset', filters.offset);
179
-
180
- return http.get(`/api/v1/messages?${params}`);
181
- },
182
-
183
- // Get single message
184
- get: async (transactionId) => {
185
- return http.get(`/api/v1/messages/${transactionId}`);
186
- },
187
-
188
- // Delete message
189
- delete: async (transactionId) => {
190
- return http.delete(`/api/v1/messages/${transactionId}`);
191
- },
192
-
193
- // Retry failed message
194
- retry: async (transactionId) => {
195
- return http.post(`/api/v1/messages/${transactionId}/retry`);
196
- },
197
-
198
- // Move to DLQ
199
- moveToDLQ: async (transactionId) => {
200
- return http.post(`/api/v1/messages/${transactionId}/dlq`);
201
- },
202
-
203
- // Get related messages
204
- getRelated: async (transactionId) => {
205
- return http.get(`/api/v1/messages/${transactionId}/related`);
206
- }
207
- };
208
-
209
- // Queue management
210
- const queues = {
211
- // Clear queue or partition
212
- clear: async (queue, partition = null) => {
213
- const params = partition ? `?partition=${partition}` : '';
214
- return http.delete(`/api/v1/queues/${queue}/clear${params}`);
215
- },
216
-
217
- // Delete a queue (removes queue and all its messages/partitions)
218
- delete: async (queue) => {
219
- return http.delete(`/api/v1/resources/queues/${encodeURIComponent(queue)}`);
220
- }
221
- };
222
-
223
- // Analytics
224
- const analytics = {
225
- // Get all queues overview
226
- queues: async (filters = {}) => {
227
- const params = new URLSearchParams();
228
- if (filters.namespace) params.append('namespace', filters.namespace);
229
- if (filters.task) params.append('task', filters.task);
230
- const query = params.toString();
231
- return http.get(`/api/v1/analytics/queues${query ? '?' + query : ''}`);
232
- },
233
-
234
- // Get specific queue statistics
235
- queue: async (queue) => {
236
- return http.get(`/api/v1/analytics/queue/${queue}`);
237
- },
238
-
239
- // Get stats by namespace
240
- namespace: async (namespace) => {
241
- return http.get(`/api/v1/analytics?namespace=${namespace}`);
242
- },
243
-
244
- // Get stats by task
245
- task: async (task) => {
246
- return http.get(`/api/v1/analytics?task=${task}`);
247
- },
248
-
249
- // Get queue depths
250
- queueDepths: async (filters = {}) => {
251
- const params = new URLSearchParams();
252
- if (filters.namespace) params.append('namespace', filters.namespace);
253
- if (filters.task) params.append('task', filters.task);
254
- const query = params.toString();
255
- return http.get(`/api/v1/analytics/queue-depths${query ? '?' + query : ''}`);
256
- },
257
-
258
- // Get throughput metrics
259
- throughput: async () => {
260
- return http.get('/api/v1/analytics/throughput');
261
- },
262
-
263
- // Get queue stats with filters
264
- queueStats: async (filters = {}) => {
265
- const params = new URLSearchParams();
266
- if (filters.queue) params.append('queue', filters.queue);
267
- if (filters.namespace) params.append('namespace', filters.namespace);
268
- if (filters.task) params.append('task', filters.task);
269
- return http.get(`/api/v1/analytics/queue-stats?${params}`);
270
- },
271
-
272
- // Get queue lag metrics
273
- queueLag: async (filters = {}) => {
274
- const params = new URLSearchParams();
275
- if (filters.queue) params.append('queue', filters.queue);
276
- if (filters.namespace) params.append('namespace', filters.namespace);
277
- if (filters.task) params.append('task', filters.task);
278
- const query = params.toString();
279
- return http.get(`/api/v1/analytics/queue-lag${query ? '?' + query : ''}`);
280
- }
281
- };
282
-
283
- // System health
284
- const health = {
285
- check: async () => http.get('/health'),
286
- metrics: async () => http.get('/metrics')
287
- };
288
-
289
- // Consumer helper - continuously pop and process messages (with consumer group support)
290
- const consume = ({ queue, partition, namespace, task, consumerGroup, handler, handlerBatch, options = {} }) => {
291
- const {
292
- batch = 1,
293
- wait = true,
294
- timeout = 30000,
295
- stopOnError = false,
296
- subscriptionMode = null,
297
- subscriptionFrom = null
298
- } = options;
299
-
300
- // Validate that only one handler type is provided
301
- if (handler && handlerBatch) {
302
- throw new Error('Cannot specify both handler and handlerBatch. Choose one processing mode.');
303
- }
304
-
305
- if (!handler && !handlerBatch) {
306
- throw new Error('Must specify either handler (for individual processing) or handlerBatch (for batch processing).');
307
- }
308
-
309
- const isBatchMode = !!handlerBatch;
310
- const messageHandler = handler || handlerBatch;
311
-
312
- let running = true;
313
-
314
- const stop = () => {
315
- running = false;
316
- };
317
-
318
- // Start the consume loop asynchronously
319
- (async () => {
320
- while (running) {
321
- try {
322
- const result = await pop({
323
- queue,
324
- partition,
325
- namespace,
326
- task,
327
- consumerGroup,
328
- subscriptionMode,
329
- subscriptionFrom,
330
- wait,
331
- timeout,
332
- batch
333
- });
334
-
335
- if (result.messages && result.messages.length > 0) {
336
- if (isBatchMode) {
337
- // Batch processing mode
338
- if (!running) return;
339
-
340
- try {
341
- await messageHandler(result.messages);
342
-
343
- // Batch acknowledge all messages as completed
344
- const acknowledgments = result.messages.map(msg => ({
345
- transactionId: msg.transactionId,
346
- status: 'completed'
347
- }));
348
-
349
- await ackBatch(acknowledgments, consumerGroup);
350
- } catch (error) {
351
- console.error('Error processing message batch:', error);
352
-
353
- // Batch acknowledge all messages as failed
354
- const acknowledgments = result.messages.map(msg => ({
355
- transactionId: msg.transactionId,
356
- status: 'failed',
357
- error: error.message
358
- }));
359
-
360
- await ackBatch(acknowledgments, consumerGroup);
361
-
362
- if (stopOnError) {
363
- running = false;
364
- }
365
- }
366
- } else {
367
- // Individual processing mode (original behavior)
368
- for (const message of result.messages) {
369
- if (!running) break; // Check if stopped
370
-
371
- try {
372
- await messageHandler(message);
373
- await ack(message.transactionId, 'completed', null, consumerGroup);
374
- } catch (error) {
375
- console.error('Error processing message:', error);
376
- await ack(message.transactionId, 'failed', error.message, consumerGroup);
377
-
378
- if (stopOnError) {
379
- running = false;
380
- break;
381
- }
382
- }
383
- }
384
- }
385
- }
386
- // If no messages received (timeout on server side), immediately continue
387
- // No delay needed - this is expected behavior for long polling
388
- } catch (error) {
389
- // Check if this is a timeout/abort error from the HTTP client
390
- const isTimeoutError = error.name === 'AbortError' ||
391
- error.message?.includes('abort') ||
392
- error.message?.includes('timeout');
393
-
394
- if (isTimeoutError) {
395
- // For timeout errors, immediately retry without delay
396
- console.debug('Long poll timeout, immediately retrying...');
397
- continue;
398
- }
399
-
400
- console.error('Error in consume loop:', error);
401
- if (stopOnError) {
402
- running = false;
403
- } else {
404
- // For other errors (network issues, server errors), wait before retrying
405
- await new Promise(resolve => setTimeout(resolve, retryDelay));
406
- }
407
- }
408
- }
409
- })();
410
-
411
- // Return the stop function immediately
412
- return stop;
413
- };
414
-
415
- // WebSocket support for real-time updates
416
- const createWebSocketConnection = (path = '/ws/dashboard') => {
417
- // For WebSocket, we'll use the first available server or round-robin
418
- let wsBaseUrl;
419
-
420
- if (loadBalancer) {
421
- // Get a URL from the load balancer for WebSocket connection
422
- wsBaseUrl = loadBalancer.getNextUrl();
423
- } else {
424
- // Use single server URL
425
- wsBaseUrl = baseUrls && baseUrls.length === 1 ? baseUrls[0] : baseUrl;
426
- }
427
-
428
- const wsUrl = wsBaseUrl.replace('http://', 'ws://').replace('https://', 'wss://');
429
- const ws = new WebSocket(`${wsUrl}${path}`);
430
-
431
- // Store the server URL for reconnection purposes
432
- ws._serverUrl = wsBaseUrl;
433
-
434
- return {
435
- ws,
436
- serverUrl: wsBaseUrl,
437
- onMessage: (handler) => {
438
- ws.addEventListener('message', (event) => {
439
- try {
440
- const data = JSON.parse(event.data);
441
- handler(data);
442
- } catch (error) {
443
- console.error('Error parsing WebSocket message:', error);
444
- }
445
- });
446
- },
447
- onError: (handler) => ws.addEventListener('error', handler),
448
- onClose: (handler) => ws.addEventListener('close', handler),
449
- onOpen: (handler) => ws.addEventListener('open', handler),
450
- close: () => ws.close(),
451
- reconnect: () => {
452
- // For reconnection, try a different server if using load balancing
453
- if (loadBalancer) {
454
- return createWebSocketConnection(path);
455
- }
456
- // For single server, reconnect to the same
457
- const newWs = new WebSocket(`${wsUrl}${path}`);
458
- newWs._serverUrl = wsBaseUrl;
459
- return {
460
- ws: newWs,
461
- serverUrl: wsBaseUrl,
462
- onMessage: (handler) => {
463
- newWs.addEventListener('message', (event) => {
464
- try {
465
- const data = JSON.parse(event.data);
466
- handler(data);
467
- } catch (error) {
468
- console.error('Error parsing WebSocket message:', error);
469
- }
470
- });
471
- },
472
- onError: (handler) => newWs.addEventListener('error', handler),
473
- onClose: (handler) => newWs.addEventListener('close', handler),
474
- onOpen: (handler) => newWs.addEventListener('open', handler),
475
- close: () => newWs.close()
476
- };
477
- }
478
- };
479
- };
480
-
481
- // Get load balancer stats (useful for debugging)
482
- const getLoadBalancerStats = () => {
483
- if (loadBalancer) {
484
- return loadBalancer.getStats();
485
- }
486
- return null;
487
- };
488
-
489
- return {
490
- configure,
491
- push,
492
- pop,
493
- ack,
494
- ackBatch,
495
- messages,
496
- queues,
497
- analytics,
498
- health,
499
- consume,
500
- createWebSocketConnection,
501
- getLoadBalancerStats
502
- };
503
- };
504
-
505
- // Export helper for creating a simple consumer
506
- export const createConsumer = (options) => {
507
- const client = createQueenClient(options);
508
- return client.consume;
509
- };
510
-
511
- // Export helper for creating a producer
512
- export const createProducer = (options) => {
513
- const client = createQueenClient(options);
514
- return {
515
- push: client.push,
516
- configure: client.configure
517
- };
518
- };