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.
- package/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- 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
|
+
};
|