queen-mq 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/API.md +1116 -0
  2. package/CACHE.md +519 -0
  3. package/DASHBOARD-V3.md +478 -0
  4. package/DASHBOARD.md +382 -0
  5. package/MOD_QUEUE.md +453 -0
  6. package/PARTITION_LOCKING_DESIGN.md +989 -0
  7. package/PLAN.md +707 -0
  8. package/QUERY_ANALSYS.md +72 -0
  9. package/QUEUE_BUS.md +334 -0
  10. package/README.md +1495 -0
  11. package/V2-PLAN.md +236 -0
  12. package/assets/dashboard.png +0 -0
  13. package/dashboard/.vscode/extensions.json +3 -0
  14. package/dashboard/README.md +5 -0
  15. package/dashboard/index.html +14 -0
  16. package/dashboard/package-lock.json +1458 -0
  17. package/dashboard/package.json +25 -0
  18. package/dashboard/public/vite.svg +1 -0
  19. package/dashboard/src/App.vue +29 -0
  20. package/dashboard/src/assets/styles/main.css +908 -0
  21. package/dashboard/src/assets/vue.svg +1 -0
  22. package/dashboard/src/components/cards/MetricCard.vue +298 -0
  23. package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
  24. package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
  25. package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
  26. package/dashboard/src/components/common/ActivityFeed.vue +251 -0
  27. package/dashboard/src/components/layout/AppHeader.vue +208 -0
  28. package/dashboard/src/components/layout/AppLayout.vue +88 -0
  29. package/dashboard/src/components/layout/AppSidebar.vue +261 -0
  30. package/dashboard/src/main.js +44 -0
  31. package/dashboard/src/router.js +54 -0
  32. package/dashboard/src/services/api.js +187 -0
  33. package/dashboard/src/services/websocket.js +167 -0
  34. package/dashboard/src/utils/constants.js +56 -0
  35. package/dashboard/src/utils/helpers.js +118 -0
  36. package/dashboard/src/views/Analytics.vue +912 -0
  37. package/dashboard/src/views/Dashboard.vue +906 -0
  38. package/dashboard/src/views/Messages.vue +437 -0
  39. package/dashboard/src/views/QueueDetail.vue +501 -0
  40. package/dashboard/src/views/Queues.vue +333 -0
  41. package/dashboard/vite.config.js +30 -0
  42. package/debug-namespace.js +110 -0
  43. package/docs/long-polling.md +159 -0
  44. package/docs/multi-server-cache-solutions.md +185 -0
  45. package/docs/performance-tuning.md +222 -0
  46. package/examples/bus-mode.js +239 -0
  47. package/examples/continuous-consumer-optimized.js +215 -0
  48. package/examples/continuous-consumer.js +159 -0
  49. package/examples/continuous-producer.js +343 -0
  50. package/examples/mixed-mode.js +277 -0
  51. package/examples/multi-server-test.js +305 -0
  52. package/examples/single.js +64 -0
  53. package/examples/smartchat-dealyed.js +42 -0
  54. package/examples/smartchat.js +52 -0
  55. package/examples/test-cache-invalidation.js +119 -0
  56. package/examples/test-cache-multi-server.js +245 -0
  57. package/examples/test-minimal-client.js +112 -0
  58. package/examples/test-queue-creation-policy.js +137 -0
  59. package/init-db.js +20 -0
  60. package/package.json +36 -0
  61. package/src/client/client.js +291 -0
  62. package/src/client/index.js +6 -0
  63. package/src/client/queenClient.js +513 -0
  64. package/src/client/utils/http.js +172 -0
  65. package/src/client/utils/loadBalancer.js +152 -0
  66. package/src/client/utils/retry.js +35 -0
  67. package/src/config.js +215 -0
  68. package/src/database/connection.js +103 -0
  69. package/src/database/poolManager.js +192 -0
  70. package/src/database/schema-v2.sql +214 -0
  71. package/src/managers/eventManager.js +59 -0
  72. package/src/managers/queueManagerOptimized.js +1512 -0
  73. package/src/managers/resourceCache.js +96 -0
  74. package/src/managers/systemEventManager.js +127 -0
  75. package/src/routes/ack.js +26 -0
  76. package/src/routes/analytics.js +812 -0
  77. package/src/routes/configure.js +46 -0
  78. package/src/routes/messages.js +298 -0
  79. package/src/routes/pop.js +85 -0
  80. package/src/routes/push.js +28 -0
  81. package/src/routes/resources.js +296 -0
  82. package/src/server.js +1286 -0
  83. package/src/services/encryptionService.js +82 -0
  84. package/src/services/evictionService.js +131 -0
  85. package/src/services/retentionService.js +129 -0
  86. package/src/services/startupSync.js +35 -0
  87. package/src/test/test.js +4521 -0
  88. package/src/utils/logger.js +44 -0
  89. package/src/utils/uuid.js +5 -0
  90. package/src/websocket/wsServer.js +221 -0
@@ -0,0 +1,513 @@
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
+
218
+ // Analytics
219
+ const analytics = {
220
+ // Get all queues overview
221
+ queues: async (filters = {}) => {
222
+ const params = new URLSearchParams();
223
+ if (filters.namespace) params.append('namespace', filters.namespace);
224
+ if (filters.task) params.append('task', filters.task);
225
+ const query = params.toString();
226
+ return http.get(`/api/v1/analytics/queues${query ? '?' + query : ''}`);
227
+ },
228
+
229
+ // Get specific queue statistics
230
+ queue: async (queue) => {
231
+ return http.get(`/api/v1/analytics/queue/${queue}`);
232
+ },
233
+
234
+ // Get stats by namespace
235
+ namespace: async (namespace) => {
236
+ return http.get(`/api/v1/analytics?namespace=${namespace}`);
237
+ },
238
+
239
+ // Get stats by task
240
+ task: async (task) => {
241
+ return http.get(`/api/v1/analytics?task=${task}`);
242
+ },
243
+
244
+ // Get queue depths
245
+ queueDepths: async (filters = {}) => {
246
+ const params = new URLSearchParams();
247
+ if (filters.namespace) params.append('namespace', filters.namespace);
248
+ if (filters.task) params.append('task', filters.task);
249
+ const query = params.toString();
250
+ return http.get(`/api/v1/analytics/queue-depths${query ? '?' + query : ''}`);
251
+ },
252
+
253
+ // Get throughput metrics
254
+ throughput: async () => {
255
+ return http.get('/api/v1/analytics/throughput');
256
+ },
257
+
258
+ // Get queue stats with filters
259
+ queueStats: async (filters = {}) => {
260
+ const params = new URLSearchParams();
261
+ if (filters.queue) params.append('queue', filters.queue);
262
+ if (filters.namespace) params.append('namespace', filters.namespace);
263
+ if (filters.task) params.append('task', filters.task);
264
+ return http.get(`/api/v1/analytics/queue-stats?${params}`);
265
+ },
266
+
267
+ // Get queue lag metrics
268
+ queueLag: async (filters = {}) => {
269
+ const params = new URLSearchParams();
270
+ if (filters.queue) params.append('queue', filters.queue);
271
+ if (filters.namespace) params.append('namespace', filters.namespace);
272
+ if (filters.task) params.append('task', filters.task);
273
+ const query = params.toString();
274
+ return http.get(`/api/v1/analytics/queue-lag${query ? '?' + query : ''}`);
275
+ }
276
+ };
277
+
278
+ // System health
279
+ const health = {
280
+ check: async () => http.get('/health'),
281
+ metrics: async () => http.get('/metrics')
282
+ };
283
+
284
+ // Consumer helper - continuously pop and process messages (with consumer group support)
285
+ const consume = ({ queue, partition, namespace, task, consumerGroup, handler, handlerBatch, options = {} }) => {
286
+ const {
287
+ batch = 1,
288
+ wait = true,
289
+ timeout = 30000,
290
+ stopOnError = false,
291
+ subscriptionMode = null,
292
+ subscriptionFrom = null
293
+ } = options;
294
+
295
+ // Validate that only one handler type is provided
296
+ if (handler && handlerBatch) {
297
+ throw new Error('Cannot specify both handler and handlerBatch. Choose one processing mode.');
298
+ }
299
+
300
+ if (!handler && !handlerBatch) {
301
+ throw new Error('Must specify either handler (for individual processing) or handlerBatch (for batch processing).');
302
+ }
303
+
304
+ const isBatchMode = !!handlerBatch;
305
+ const messageHandler = handler || handlerBatch;
306
+
307
+ let running = true;
308
+
309
+ const stop = () => {
310
+ running = false;
311
+ };
312
+
313
+ // Start the consume loop asynchronously
314
+ (async () => {
315
+ while (running) {
316
+ try {
317
+ const result = await pop({
318
+ queue,
319
+ partition,
320
+ namespace,
321
+ task,
322
+ consumerGroup,
323
+ subscriptionMode,
324
+ subscriptionFrom,
325
+ wait,
326
+ timeout,
327
+ batch
328
+ });
329
+
330
+ if (result.messages && result.messages.length > 0) {
331
+ if (isBatchMode) {
332
+ // Batch processing mode
333
+ if (!running) return;
334
+
335
+ try {
336
+ await messageHandler(result.messages);
337
+
338
+ // Batch acknowledge all messages as completed
339
+ const acknowledgments = result.messages.map(msg => ({
340
+ transactionId: msg.transactionId,
341
+ status: 'completed'
342
+ }));
343
+
344
+ await ackBatch(acknowledgments, consumerGroup);
345
+ } catch (error) {
346
+ console.error('Error processing message batch:', error);
347
+
348
+ // Batch acknowledge all messages as failed
349
+ const acknowledgments = result.messages.map(msg => ({
350
+ transactionId: msg.transactionId,
351
+ status: 'failed',
352
+ error: error.message
353
+ }));
354
+
355
+ await ackBatch(acknowledgments, consumerGroup);
356
+
357
+ if (stopOnError) {
358
+ running = false;
359
+ }
360
+ }
361
+ } else {
362
+ // Individual processing mode (original behavior)
363
+ for (const message of result.messages) {
364
+ if (!running) break; // Check if stopped
365
+
366
+ try {
367
+ await messageHandler(message);
368
+ await ack(message.transactionId, 'completed', null, consumerGroup);
369
+ } catch (error) {
370
+ console.error('Error processing message:', error);
371
+ await ack(message.transactionId, 'failed', error.message, consumerGroup);
372
+
373
+ if (stopOnError) {
374
+ running = false;
375
+ break;
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ // If no messages received (timeout on server side), immediately continue
382
+ // No delay needed - this is expected behavior for long polling
383
+ } catch (error) {
384
+ // Check if this is a timeout/abort error from the HTTP client
385
+ const isTimeoutError = error.name === 'AbortError' ||
386
+ error.message?.includes('abort') ||
387
+ error.message?.includes('timeout');
388
+
389
+ if (isTimeoutError) {
390
+ // For timeout errors, immediately retry without delay
391
+ console.debug('Long poll timeout, immediately retrying...');
392
+ continue;
393
+ }
394
+
395
+ console.error('Error in consume loop:', error);
396
+ if (stopOnError) {
397
+ running = false;
398
+ } else {
399
+ // For other errors (network issues, server errors), wait before retrying
400
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
401
+ }
402
+ }
403
+ }
404
+ })();
405
+
406
+ // Return the stop function immediately
407
+ return stop;
408
+ };
409
+
410
+ // WebSocket support for real-time updates
411
+ const createWebSocketConnection = (path = '/ws/dashboard') => {
412
+ // For WebSocket, we'll use the first available server or round-robin
413
+ let wsBaseUrl;
414
+
415
+ if (loadBalancer) {
416
+ // Get a URL from the load balancer for WebSocket connection
417
+ wsBaseUrl = loadBalancer.getNextUrl();
418
+ } else {
419
+ // Use single server URL
420
+ wsBaseUrl = baseUrls && baseUrls.length === 1 ? baseUrls[0] : baseUrl;
421
+ }
422
+
423
+ const wsUrl = wsBaseUrl.replace('http://', 'ws://').replace('https://', 'wss://');
424
+ const ws = new WebSocket(`${wsUrl}${path}`);
425
+
426
+ // Store the server URL for reconnection purposes
427
+ ws._serverUrl = wsBaseUrl;
428
+
429
+ return {
430
+ ws,
431
+ serverUrl: wsBaseUrl,
432
+ onMessage: (handler) => {
433
+ ws.addEventListener('message', (event) => {
434
+ try {
435
+ const data = JSON.parse(event.data);
436
+ handler(data);
437
+ } catch (error) {
438
+ console.error('Error parsing WebSocket message:', error);
439
+ }
440
+ });
441
+ },
442
+ onError: (handler) => ws.addEventListener('error', handler),
443
+ onClose: (handler) => ws.addEventListener('close', handler),
444
+ onOpen: (handler) => ws.addEventListener('open', handler),
445
+ close: () => ws.close(),
446
+ reconnect: () => {
447
+ // For reconnection, try a different server if using load balancing
448
+ if (loadBalancer) {
449
+ return createWebSocketConnection(path);
450
+ }
451
+ // For single server, reconnect to the same
452
+ const newWs = new WebSocket(`${wsUrl}${path}`);
453
+ newWs._serverUrl = wsBaseUrl;
454
+ return {
455
+ ws: newWs,
456
+ serverUrl: wsBaseUrl,
457
+ onMessage: (handler) => {
458
+ newWs.addEventListener('message', (event) => {
459
+ try {
460
+ const data = JSON.parse(event.data);
461
+ handler(data);
462
+ } catch (error) {
463
+ console.error('Error parsing WebSocket message:', error);
464
+ }
465
+ });
466
+ },
467
+ onError: (handler) => newWs.addEventListener('error', handler),
468
+ onClose: (handler) => newWs.addEventListener('close', handler),
469
+ onOpen: (handler) => newWs.addEventListener('open', handler),
470
+ close: () => newWs.close()
471
+ };
472
+ }
473
+ };
474
+ };
475
+
476
+ // Get load balancer stats (useful for debugging)
477
+ const getLoadBalancerStats = () => {
478
+ if (loadBalancer) {
479
+ return loadBalancer.getStats();
480
+ }
481
+ return null;
482
+ };
483
+
484
+ return {
485
+ configure,
486
+ push,
487
+ pop,
488
+ ack,
489
+ ackBatch,
490
+ messages,
491
+ queues,
492
+ analytics,
493
+ health,
494
+ consume,
495
+ createWebSocketConnection,
496
+ getLoadBalancerStats
497
+ };
498
+ };
499
+
500
+ // Export helper for creating a simple consumer
501
+ export const createConsumer = (options) => {
502
+ const client = createQueenClient(options);
503
+ return client.consume;
504
+ };
505
+
506
+ // Export helper for creating a producer
507
+ export const createProducer = (options) => {
508
+ const client = createQueenClient(options);
509
+ return {
510
+ push: client.push,
511
+ configure: client.configure
512
+ };
513
+ };
@@ -0,0 +1,172 @@
1
+ export const httpRequest = async (url, options = {}) => {
2
+ const response = await fetch(url, {
3
+ ...options,
4
+ headers: {
5
+ 'Content-Type': 'application/json',
6
+ ...options.headers
7
+ }
8
+ });
9
+
10
+ // Handle 204 No Content
11
+ if (response.status === 204) {
12
+ return null; // No content
13
+ }
14
+
15
+ // Handle errors
16
+ if (!response.ok) {
17
+ const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
18
+ error.status = response.status;
19
+
20
+ try {
21
+ const text = await response.text();
22
+ if (text) {
23
+ const body = JSON.parse(text);
24
+ error.message = body.error || error.message;
25
+ }
26
+ } catch (e) {
27
+ // Ignore JSON parse errors
28
+ }
29
+
30
+ throw error;
31
+ }
32
+
33
+ // Handle successful responses
34
+ const contentType = response.headers.get('content-type');
35
+ const contentLength = response.headers.get('content-length');
36
+
37
+ // Check if there's actually content to parse
38
+ if (!contentType || !contentType.includes('application/json') || contentLength === '0') {
39
+ const text = await response.text();
40
+ if (!text || text.length === 0) {
41
+ return null; // Empty response
42
+ }
43
+ try {
44
+ return JSON.parse(text);
45
+ } catch (e) {
46
+ console.warn('Failed to parse response as JSON:', text);
47
+ return null;
48
+ }
49
+ }
50
+
51
+ return response.json();
52
+ };
53
+
54
+ export const createHttpClient = ({ baseUrl, timeout = 30000 }) => {
55
+ const request = (method, path, body = null, requestTimeout = null) => {
56
+ const effectiveTimeout = requestTimeout || timeout;
57
+ const controller = new AbortController();
58
+ const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
59
+
60
+ const options = {
61
+ method,
62
+ signal: controller.signal
63
+ };
64
+
65
+ if (body) {
66
+ options.body = JSON.stringify(body);
67
+ }
68
+
69
+ return httpRequest(`${baseUrl}${path}`, options)
70
+ .catch(error => {
71
+ // Enhance abort errors with more context
72
+ if (error.name === 'AbortError') {
73
+ const timeoutError = new Error(`Request timeout after ${effectiveTimeout}ms`);
74
+ timeoutError.name = 'AbortError';
75
+ timeoutError.timeout = effectiveTimeout;
76
+ throw timeoutError;
77
+ }
78
+ throw error;
79
+ })
80
+ .finally(() => clearTimeout(timeoutId));
81
+ };
82
+
83
+ return {
84
+ get: (path, requestTimeout) => request('GET', path, null, requestTimeout),
85
+ post: (path, body, requestTimeout) => request('POST', path, body, requestTimeout),
86
+ put: (path, body, requestTimeout) => request('PUT', path, body, requestTimeout),
87
+ delete: (path, requestTimeout) => request('DELETE', path, null, requestTimeout)
88
+ };
89
+ };
90
+
91
+ /**
92
+ * Create an HTTP client with load balancing support
93
+ */
94
+ export const createLoadBalancedHttpClient = ({ baseUrls, loadBalancer, timeout = 30000, enableFailover = true }) => {
95
+ const request = async (method, path, body = null, requestTimeout = null) => {
96
+ const effectiveTimeout = requestTimeout || timeout;
97
+
98
+ const executeRequest = async (baseUrl) => {
99
+ const controller = new AbortController();
100
+ const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout);
101
+
102
+ const options = {
103
+ method,
104
+ signal: controller.signal
105
+ };
106
+
107
+ if (body) {
108
+ options.body = JSON.stringify(body);
109
+ }
110
+
111
+ try {
112
+ return await httpRequest(`${baseUrl}${path}`, options);
113
+ } catch (error) {
114
+ // Enhance abort errors with more context
115
+ if (error.name === 'AbortError') {
116
+ const timeoutError = new Error(`Request timeout after ${effectiveTimeout}ms`);
117
+ timeoutError.name = 'AbortError';
118
+ timeoutError.timeout = effectiveTimeout;
119
+ throw timeoutError;
120
+ }
121
+ throw error;
122
+ } finally {
123
+ clearTimeout(timeoutId);
124
+ }
125
+ };
126
+
127
+ if (enableFailover) {
128
+ // Try with failover to different servers
129
+ const attemptedUrls = new Set();
130
+ const urls = loadBalancer.getAllUrls();
131
+ let lastError = null;
132
+
133
+ for (let attempt = 0; attempt < urls.length; attempt++) {
134
+ const url = loadBalancer.getNextUrl();
135
+
136
+ if (attemptedUrls.has(url)) {
137
+ continue;
138
+ }
139
+
140
+ attemptedUrls.add(url);
141
+
142
+ try {
143
+ return await executeRequest(url);
144
+ } catch (error) {
145
+ lastError = error;
146
+ console.warn(`Request failed for ${url}: ${error.message}`);
147
+
148
+ // Don't retry on client errors (4xx)
149
+ if (error.status && error.status >= 400 && error.status < 500) {
150
+ throw error;
151
+ }
152
+
153
+ // Continue to next server for server errors or network issues
154
+ }
155
+ }
156
+
157
+ throw lastError || new Error('All servers failed');
158
+ } else {
159
+ // No failover, just use the next URL from load balancer
160
+ const baseUrl = loadBalancer.getNextUrl();
161
+ return executeRequest(baseUrl);
162
+ }
163
+ };
164
+
165
+ return {
166
+ get: (path, requestTimeout) => request('GET', path, null, requestTimeout),
167
+ post: (path, body, requestTimeout) => request('POST', path, body, requestTimeout),
168
+ put: (path, body, requestTimeout) => request('PUT', path, body, requestTimeout),
169
+ delete: (path, requestTimeout) => request('DELETE', path, null, requestTimeout),
170
+ getLoadBalancer: () => loadBalancer
171
+ };
172
+ };