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
package/src/server.js ADDED
@@ -0,0 +1,1286 @@
1
+ import uWS from 'uWebSockets.js';
2
+ import pg from 'pg';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import config from './config.js';
7
+ import { initDatabase } from './database/connection.js';
8
+ import { createOptimizedQueueManager } from './managers/queueManagerOptimized.js';
9
+ import { createResourceCache } from './managers/resourceCache.js';
10
+ import { createEventManager } from './managers/eventManager.js';
11
+ import { SystemEventManager, SYSTEM_QUEUE } from './managers/systemEventManager.js';
12
+ import { syncSystemEvents } from './services/startupSync.js';
13
+ import { createQueenClient } from './client/queenClient.js';
14
+ import { createPushRoute } from './routes/push.js';
15
+ import { createPopRoute } from './routes/pop.js';
16
+ import { createAckRoute } from './routes/ack.js';
17
+ import { createConfigureRoute } from './routes/configure.js';
18
+ import { createAnalyticsRoutes } from './routes/analytics.js';
19
+ import { createMessagesRoutes } from './routes/messages.js';
20
+ import { createResourcesRoutes } from './routes/resources.js';
21
+ import { createWebSocketServer } from './websocket/wsServer.js';
22
+ import { initEncryption } from './services/encryptionService.js';
23
+ import { startRetentionJob } from './services/retentionService.js';
24
+ import { startEvictionJob } from './services/evictionService.js';
25
+ import { log } from './utils/logger.js';
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+
29
+ const PORT = config.SERVER.PORT;
30
+ const HOST = config.SERVER.HOST;
31
+
32
+ // Use WORKER_ID from config as the server instance ID for consumer groups
33
+ // This ensures the same server (identified by WORKER_ID) maintains its position in the event stream
34
+ const SERVER_INSTANCE_ID = config.SERVER.WORKER_ID;
35
+
36
+ // Performance monitoring
37
+ let requestCount = 0;
38
+ let messageCount = 0;
39
+ const startTime = Date.now();
40
+
41
+ // Create optimized connection pool
42
+ const { Pool } = pg;
43
+ const createOptimizedPool = () => {
44
+ return new Pool({
45
+ user: config.DATABASE.USER,
46
+ host: config.DATABASE.HOST,
47
+ database: config.DATABASE.DATABASE,
48
+ password: config.DATABASE.PASSWORD,
49
+ port: config.DATABASE.PORT,
50
+ max: config.DATABASE.POOL_SIZE,
51
+ idleTimeoutMillis: config.DATABASE.IDLE_TIMEOUT,
52
+ connectionTimeoutMillis: config.DATABASE.CONNECTION_TIMEOUT,
53
+ statement_timeout: config.DATABASE.STATEMENT_TIMEOUT,
54
+ query_timeout: config.DATABASE.QUERY_TIMEOUT,
55
+ application_name: config.SERVER.APPLICATION_NAME
56
+ });
57
+ };
58
+
59
+ // Initialize components
60
+ const pool = createOptimizedPool();
61
+ const resourceCache = createResourceCache();
62
+ const eventManager = createEventManager();
63
+
64
+ // Initialize system event manager
65
+ const systemEventManager = new SystemEventManager(pool, SERVER_INSTANCE_ID);
66
+
67
+ // Register cache event handlers
68
+ resourceCache.registerEventHandlers(systemEventManager);
69
+
70
+ // Always use optimized queue manager - it's better for all scenarios
71
+ const queueManager = createOptimizedQueueManager(pool, resourceCache, eventManager, systemEventManager);
72
+ const analyticsRoutes = createAnalyticsRoutes(queueManager);
73
+ const messagesRoutes = createMessagesRoutes(pool, queueManager);
74
+ const resourcesRoutes = createResourcesRoutes(pool);
75
+
76
+ // Initialize database with migrations
77
+ const initDatabaseWithMigrations = async () => {
78
+ // Run base schema
79
+ await initDatabase(pool);
80
+
81
+ // Run extension migrations
82
+ const migrationPath = path.join(__dirname, '..', 'migrations', '001-extend-features.sql');
83
+ if (fs.existsSync(migrationPath)) {
84
+ const migration = fs.readFileSync(migrationPath, 'utf8');
85
+ try {
86
+ await pool.query(migration);
87
+ log('āœ… Extension features migration applied');
88
+ } catch (error) {
89
+ if (!error.message.includes('already exists')) {
90
+ log('Migration error:', error);
91
+ }
92
+ }
93
+ }
94
+ };
95
+
96
+ await initDatabaseWithMigrations();
97
+
98
+ // Initialize system queue
99
+ async function initializeSystemQueue() {
100
+ try {
101
+ // Direct database operation to create system queue
102
+ await pool.query(`
103
+ INSERT INTO queen.queues (name, ttl, priority, max_queue_size)
104
+ VALUES ($1, 300, 100, 10000)
105
+ ON CONFLICT (name) DO UPDATE SET
106
+ ttl = EXCLUDED.ttl,
107
+ priority = EXCLUDED.priority,
108
+ max_queue_size = EXCLUDED.max_queue_size
109
+ `, [SYSTEM_QUEUE]);
110
+
111
+ await pool.query(`
112
+ INSERT INTO queen.partitions (queue_id, name)
113
+ SELECT id, 'Default' FROM queen.queues WHERE name = $1
114
+ ON CONFLICT (queue_id, name) DO NOTHING
115
+ `, [SYSTEM_QUEUE]);
116
+
117
+ log('āœ… System event queue initialized');
118
+ } catch (error) {
119
+ console.error('Failed to initialize system queue:', error);
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ await initializeSystemQueue();
125
+
126
+ // Initialize extended features
127
+ log('šŸš€ Initializing Queen Services...');
128
+
129
+ // 1. Encryption service
130
+ const encryptionEnabled = initEncryption();
131
+
132
+ // 2. Retention service
133
+ const stopRetention = startRetentionJob(pool);
134
+
135
+ // 3. Eviction service
136
+ const stopEviction = startEvictionJob(pool, eventManager);
137
+
138
+ // Background job for lease reclamation
139
+ setInterval(async () => {
140
+ try {
141
+ const reclaimed = await queueManager.reclaimExpiredLeases();
142
+ if (reclaimed > 0) {
143
+ log(`Reclaimed ${reclaimed} expired leases`);
144
+ }
145
+ } catch (error) {
146
+ log('Error reclaiming leases:', error);
147
+ }
148
+ }, config.JOBS.LEASE_RECLAIM_INTERVAL);
149
+
150
+ // Create server
151
+ const app = uWS.App();
152
+
153
+ // Initialize WebSocket server
154
+ const wsServer = createWebSocketServer(app, eventManager);
155
+
156
+ // Background job for queue depth updates
157
+ setInterval(async () => {
158
+ await wsServer.updateQueueDepths(queueManager);
159
+ }, config.JOBS.QUEUE_DEPTH_UPDATE_INTERVAL);
160
+
161
+ // Background job for system stats
162
+ setInterval(async () => {
163
+ await wsServer.sendSystemStats(pool);
164
+ }, config.JOBS.SYSTEM_STATS_UPDATE_INTERVAL);
165
+
166
+ // CORS headers helper
167
+ const setCorsHeaders = (res) => {
168
+ res.writeHeader('Access-Control-Allow-Origin', config.API.CORS_ALLOWED_ORIGINS);
169
+ res.writeHeader('Access-Control-Allow-Methods', config.API.CORS_ALLOWED_METHODS);
170
+ res.writeHeader('Access-Control-Allow-Headers', config.API.CORS_ALLOWED_HEADERS);
171
+ res.writeHeader('Access-Control-Max-Age', config.API.CORS_MAX_AGE.toString());
172
+ };
173
+
174
+ // Helper to read JSON body
175
+ const readJson = (res, cb, abortedRef) => {
176
+ let buffer;
177
+ const maxBodySize = config.API.MAX_BODY_SIZE; // 10MB max body size
178
+
179
+ res.onData((ab, isLast) => {
180
+ const chunk = Buffer.from(ab);
181
+
182
+ // Check size limit
183
+ const currentSize = (buffer ? buffer.length : 0) + chunk.length;
184
+ if (currentSize > maxBodySize) {
185
+ if (abortedRef && abortedRef.aborted) return;
186
+ res.writeStatus(config.HTTP_STATUS.BAD_REQUEST.toString()).end(JSON.stringify({ error: 'Request body too large' }));
187
+ return;
188
+ }
189
+
190
+ if (isLast) {
191
+ let json;
192
+ try {
193
+ let fullData;
194
+ if (buffer) {
195
+ fullData = Buffer.concat([buffer, chunk]);
196
+ } else {
197
+ fullData = chunk;
198
+ }
199
+
200
+ const jsonString = fullData.toString('utf8');
201
+
202
+ // Debug large payloads
203
+ if (jsonString.length > 1000000) {
204
+ log(`Parsing large JSON: ${jsonString.length} bytes, starts with: ${jsonString.substring(0, 100)}`);
205
+ }
206
+
207
+ json = JSON.parse(jsonString);
208
+ } catch (e) {
209
+ if (abortedRef && abortedRef.aborted) return;
210
+ // Log the actual error for debugging
211
+ log(`JSON parse error: ${e.message}`);
212
+
213
+ // Log what we're trying to parse for debugging
214
+ let sample = '';
215
+ if (buffer) {
216
+ const fullData = Buffer.concat([buffer, chunk]);
217
+ sample = fullData.toString('utf8').substring(0, 200);
218
+ } else {
219
+ sample = chunk.toString('utf8').substring(0, 200);
220
+ }
221
+ log(`Failed to parse data starting with: ${sample}`);
222
+
223
+ res.writeStatus(config.HTTP_STATUS.BAD_REQUEST.toString()).end(JSON.stringify({ error: 'Invalid JSON' }));
224
+ return;
225
+ }
226
+ cb(json);
227
+ } else {
228
+ if (buffer) {
229
+ buffer = Buffer.concat([buffer, chunk]);
230
+ } else {
231
+ buffer = Buffer.from(chunk); // Ensure it's a Buffer
232
+ }
233
+ }
234
+ });
235
+ };
236
+
237
+ // Handle OPTIONS requests for CORS preflight
238
+ app.options('/*', (res, req) => {
239
+ res.onAborted(() => {
240
+ log('OPTIONS request aborted');
241
+ });
242
+
243
+ res.cork(() => {
244
+ setCorsHeaders(res);
245
+ res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
246
+ });
247
+ });
248
+
249
+ // Configure route
250
+ app.post('/api/v1/configure', (res, req) => {
251
+ const abortedRef = { aborted: false };
252
+ res.onAborted(() => {
253
+ abortedRef.aborted = true;
254
+ log('Configure request aborted');
255
+ });
256
+
257
+ readJson(res, async (body) => {
258
+ if (abortedRef.aborted) return;
259
+ try {
260
+ const result = await createConfigureRoute(queueManager)(body);
261
+ if (abortedRef.aborted) return;
262
+ eventManager.emit('queue.created', result);
263
+ res.cork(() => {
264
+ setCorsHeaders(res);
265
+ res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
266
+ });
267
+ } catch (error) {
268
+ if (abortedRef.aborted) return;
269
+ log('Configure error:', error);
270
+ res.cork(() => {
271
+ setCorsHeaders(res);
272
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
273
+ });
274
+ }
275
+ }, abortedRef);
276
+ });
277
+
278
+ // Push route
279
+ app.post('/api/v1/push', (res, req) => {
280
+ const abortedRef = { aborted: false };
281
+ res.onAborted(() => {
282
+ abortedRef.aborted = true;
283
+ log('Push request aborted');
284
+ });
285
+
286
+ readJson(res, async (body) => {
287
+ if (abortedRef.aborted) return;
288
+ try {
289
+ const result = await createPushRoute(queueManager)(body);
290
+ messageCount += result.messages.length;
291
+ requestCount++;
292
+ if (abortedRef.aborted) return;
293
+
294
+ // Emit events for pushed messages
295
+ for (const msg of result.messages) {
296
+ if (msg.status === 'queued') {
297
+ const item = body.items.find(i =>
298
+ i.transactionId === msg.transactionId || !i.transactionId
299
+ );
300
+ if (item) {
301
+ const partition = item.partition ?? 'Default';
302
+ const queuePath = `${item.queue}/${partition}`;
303
+ eventManager.emit('message.pushed', {
304
+ queue: item.queue,
305
+ partition: partition,
306
+ transactionId: msg.transactionId
307
+ });
308
+ eventManager.notifyMessageAvailable(queuePath);
309
+ }
310
+ }
311
+ }
312
+
313
+ res.cork(() => {
314
+ setCorsHeaders(res);
315
+ res.writeStatus(config.HTTP_STATUS.CREATED.toString()).end(JSON.stringify(result));
316
+ });
317
+ } catch (error) {
318
+ if (abortedRef.aborted) return;
319
+ log('Push error:', error.message || error);
320
+ res.cork(() => {
321
+ setCorsHeaders(res);
322
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
323
+ });
324
+ }
325
+ }, abortedRef);
326
+ });
327
+
328
+ // Pop routes with new structure
329
+ app.get('/api/v1/pop/queue/:queue/partition/:partition', (res, req) => {
330
+ const queue = req.getParameter(0);
331
+ const partition = req.getParameter(1);
332
+ const query = new URLSearchParams(req.getQuery());
333
+
334
+ let aborted = false;
335
+ res.onAborted(() => {
336
+ aborted = true;
337
+ log('Pop request aborted');
338
+ });
339
+
340
+ createPopRoute(queueManager, eventManager)(
341
+ {
342
+ queue,
343
+ partition,
344
+ consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
345
+ },
346
+ {
347
+ wait: query.get('wait') === 'true',
348
+ timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
349
+ batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
350
+ subscriptionMode: query.get('subscriptionMode'),
351
+ subscriptionFrom: query.get('subscriptionFrom')
352
+ }
353
+ ).then(result => {
354
+ if (aborted) return;
355
+
356
+ // Emit events for processing messages
357
+ for (const msg of result.messages) {
358
+ eventManager.emit('message.processing', {
359
+ queue: msg.queue,
360
+ partition: msg.partition,
361
+ transactionId: msg.transactionId,
362
+ workerId: `worker-${process.pid}`
363
+ });
364
+ }
365
+
366
+ res.cork(() => {
367
+ setCorsHeaders(res);
368
+ if (result.messages.length > 0) {
369
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
370
+ } else {
371
+ res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
372
+ }
373
+ });
374
+ }).catch(error => {
375
+ if (aborted) return;
376
+ log('Pop error:', error);
377
+ res.cork(() => {
378
+ setCorsHeaders(res);
379
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
380
+ });
381
+ });
382
+ });
383
+
384
+ app.get('/api/v1/pop/queue/:queue', (res, req) => {
385
+ const queue = req.getParameter(0);
386
+ const query = new URLSearchParams(req.getQuery());
387
+
388
+ let aborted = false;
389
+ res.onAborted(() => {
390
+ aborted = true;
391
+ log('Pop request aborted');
392
+ });
393
+
394
+ createPopRoute(queueManager, eventManager)(
395
+ {
396
+ queue,
397
+ consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
398
+ },
399
+ {
400
+ wait: query.get('wait') === 'true',
401
+ timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
402
+ batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
403
+ subscriptionMode: query.get('subscriptionMode'),
404
+ subscriptionFrom: query.get('subscriptionFrom')
405
+ }
406
+ ).then(result => {
407
+ if (aborted) return;
408
+
409
+ for (const msg of result.messages) {
410
+ eventManager.emit('message.processing', {
411
+ queue: msg.queue,
412
+ partition: msg.partition,
413
+ transactionId: msg.transactionId,
414
+ workerId: `worker-${process.pid}`
415
+ });
416
+ }
417
+
418
+ res.cork(() => {
419
+ setCorsHeaders(res);
420
+ if (result.messages.length > 0) {
421
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
422
+ } else {
423
+ res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
424
+ }
425
+ });
426
+ }).catch(error => {
427
+ if (aborted) return;
428
+ log('Pop error:', error);
429
+ res.cork(() => {
430
+ setCorsHeaders(res);
431
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
432
+ });
433
+ });
434
+ });
435
+
436
+ // Pop with filters (namespace or task)
437
+ app.get('/api/v1/pop', (res, req) => {
438
+ const query = new URLSearchParams(req.getQuery());
439
+ const namespace = query.get('namespace');
440
+ const task = query.get('task');
441
+
442
+ let aborted = false;
443
+ res.onAborted(() => {
444
+ aborted = true;
445
+ log('Pop request aborted');
446
+ });
447
+
448
+ createPopRoute(queueManager, eventManager)(
449
+ {
450
+ namespace,
451
+ task,
452
+ consumerGroup: query.get('consumerGroup') || query.get('consumer_group')
453
+ },
454
+ {
455
+ wait: query.get('wait') === 'true',
456
+ timeout: parseInt(query.get('timeout') || config.QUEUE.DEFAULT_TIMEOUT.toString()),
457
+ batch: parseInt(query.get('batch') || config.QUEUE.DEFAULT_BATCH_SIZE.toString()),
458
+ subscriptionMode: query.get('subscriptionMode'),
459
+ subscriptionFrom: query.get('subscriptionFrom')
460
+ }
461
+ ).then(result => {
462
+ if (aborted) return;
463
+
464
+ for (const msg of result.messages) {
465
+ eventManager.emit('message.processing', {
466
+ queue: msg.queue,
467
+ partition: msg.partition,
468
+ transactionId: msg.transactionId,
469
+ workerId: `worker-${process.pid}`
470
+ });
471
+ }
472
+
473
+ res.cork(() => {
474
+ setCorsHeaders(res);
475
+ if (result.messages.length > 0) {
476
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
477
+ } else {
478
+ res.writeStatus(config.HTTP_STATUS.NO_CONTENT.toString()).end();
479
+ }
480
+ });
481
+ }).catch(error => {
482
+ if (aborted) return;
483
+ log('Pop error:', error);
484
+ res.cork(() => {
485
+ setCorsHeaders(res);
486
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
487
+ });
488
+ });
489
+ });
490
+
491
+ // ACK route
492
+ app.post('/api/v1/ack', (res, req) => {
493
+ const abortedRef = { aborted: false };
494
+ res.onAborted(() => {
495
+ abortedRef.aborted = true;
496
+ log('ACK request aborted');
497
+ });
498
+
499
+ readJson(res, async (body) => {
500
+ if (abortedRef.aborted) return;
501
+ try {
502
+ const result = await createAckRoute(queueManager)(body);
503
+ if (abortedRef.aborted) return;
504
+
505
+ // Emit completion or failure event
506
+ if (body.status === 'completed') {
507
+ eventManager.emit('message.completed', {
508
+ transactionId: body.transactionId
509
+ });
510
+ } else {
511
+ eventManager.emit('message.failed', {
512
+ transactionId: body.transactionId,
513
+ error: body.error
514
+ });
515
+ }
516
+
517
+ res.cork(() => {
518
+ setCorsHeaders(res);
519
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
520
+ });
521
+ } catch (error) {
522
+ if (abortedRef.aborted) return;
523
+ log('ACK error:', error);
524
+ res.cork(() => {
525
+ setCorsHeaders(res);
526
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
527
+ });
528
+ }
529
+ }, abortedRef);
530
+ });
531
+
532
+ // Batch ACK route
533
+ app.post('/api/v1/ack/batch', (res, req) => {
534
+ const abortedRef = { aborted: false };
535
+ res.onAborted(() => {
536
+ abortedRef.aborted = true;
537
+ log('Batch ACK request aborted');
538
+ });
539
+
540
+ readJson(res, async (body) => {
541
+ if (abortedRef.aborted) return;
542
+ try {
543
+ const results = await queueManager.acknowledgeMessages(body.acknowledgments, body.consumerGroup);
544
+ if (abortedRef.aborted) return;
545
+
546
+ // Emit events for each acknowledgment
547
+ for (const ack of body.acknowledgments) {
548
+ if (ack.status === 'completed') {
549
+ eventManager.emit('message.completed', {
550
+ transactionId: ack.transactionId
551
+ });
552
+ } else {
553
+ eventManager.emit('message.failed', {
554
+ transactionId: ack.transactionId,
555
+ error: ack.error
556
+ });
557
+ }
558
+ }
559
+
560
+ res.cork(() => {
561
+ setCorsHeaders(res);
562
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({
563
+ processed: results.length,
564
+ results
565
+ }));
566
+ });
567
+ } catch (error) {
568
+ if (abortedRef.aborted) return;
569
+ log('Batch ACK error:', error);
570
+ res.cork(() => {
571
+ setCorsHeaders(res);
572
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
573
+ });
574
+ }
575
+ }, abortedRef);
576
+ });
577
+
578
+ // Analytics routes
579
+ app.get('/api/v1/analytics/queues', (res, req) => {
580
+ const query = new URLSearchParams(req.getQuery());
581
+ const fromDateTime = query.get('fromDateTime');
582
+ const toDateTime = query.get('toDateTime');
583
+ const queue = query.get('queue');
584
+ const namespace = query.get('namespace');
585
+ const task = query.get('task');
586
+
587
+ let aborted = false;
588
+ res.onAborted(() => {
589
+ aborted = true;
590
+ log('Analytics request aborted');
591
+ });
592
+
593
+ analyticsRoutes.getQueues({ fromDateTime, toDateTime, queue, namespace, task }).then(result => {
594
+ if (aborted) return;
595
+ res.cork(() => {
596
+ setCorsHeaders(res);
597
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
598
+ });
599
+ }).catch(error => {
600
+ if (aborted) return;
601
+ log('Analytics error:', error.message || error);
602
+ res.cork(() => {
603
+ setCorsHeaders(res);
604
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
605
+ });
606
+ });
607
+ });
608
+
609
+ app.get('/api/v1/analytics/queue/:queue', (res, req) => {
610
+ const queue = req.getParameter(0);
611
+ const query = new URLSearchParams(req.getQuery());
612
+ const fromDateTime = query.get('fromDateTime');
613
+ const toDateTime = query.get('toDateTime');
614
+
615
+ let aborted = false;
616
+ res.onAborted(() => {
617
+ aborted = true;
618
+ log('Analytics request aborted');
619
+ });
620
+
621
+ analyticsRoutes.getQueueStats(queue, { fromDateTime, toDateTime }).then(result => {
622
+ if (aborted) return;
623
+ res.cork(() => {
624
+ setCorsHeaders(res);
625
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
626
+ });
627
+ }).catch(error => {
628
+ if (aborted) return;
629
+ log('Analytics error:', error.message || error);
630
+ res.cork(() => {
631
+ setCorsHeaders(res);
632
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
633
+ });
634
+ });
635
+ });
636
+
637
+ // Analytics with filters
638
+ app.get('/api/v1/analytics', (res, req) => {
639
+ const query = new URLSearchParams(req.getQuery());
640
+ const namespace = query.get('namespace');
641
+ const task = query.get('task');
642
+ const fromDateTime = query.get('fromDateTime');
643
+ const toDateTime = query.get('toDateTime');
644
+ const queue = query.get('queue');
645
+
646
+ let aborted = false;
647
+ res.onAborted(() => {
648
+ aborted = true;
649
+ log('Analytics request aborted');
650
+ });
651
+
652
+ const filters = { fromDateTime, toDateTime };
653
+ const getStats = namespace
654
+ ? analyticsRoutes.getNamespaceStats(namespace, filters)
655
+ : task
656
+ ? analyticsRoutes.getTaskStats(task, filters)
657
+ : analyticsRoutes.getQueues({ ...filters, queue, namespace, task });
658
+
659
+ getStats.then(result => {
660
+ if (aborted) return;
661
+ res.cork(() => {
662
+ setCorsHeaders(res);
663
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
664
+ });
665
+ }).catch(error => {
666
+ if (aborted) return;
667
+ log('Analytics error:', error.message || error);
668
+ res.cork(() => {
669
+ setCorsHeaders(res);
670
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
671
+ });
672
+ });
673
+ });
674
+
675
+ app.get('/api/v1/analytics/queue-depths', (res, req) => {
676
+ const query = new URLSearchParams(req.getQuery());
677
+ const fromDateTime = query.get('fromDateTime');
678
+ const toDateTime = query.get('toDateTime');
679
+ const queue = query.get('queue');
680
+ const namespace = query.get('namespace');
681
+ const task = query.get('task');
682
+
683
+ let aborted = false;
684
+ res.onAborted(() => {
685
+ aborted = true;
686
+ log('Analytics request aborted');
687
+ });
688
+
689
+ analyticsRoutes.getQueueDepths({ fromDateTime, toDateTime, queue, namespace, task }).then(result => {
690
+ if (aborted) return;
691
+ res.cork(() => {
692
+ setCorsHeaders(res);
693
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
694
+ });
695
+ }).catch(error => {
696
+ if (aborted) return;
697
+ log('Analytics error:', error.message || error);
698
+ res.cork(() => {
699
+ setCorsHeaders(res);
700
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
701
+ });
702
+ });
703
+ });
704
+
705
+ app.get('/api/v1/analytics/throughput', (res, req) => {
706
+ const query = new URLSearchParams(req.getQuery());
707
+ const fromDateTime = query.get('fromDateTime');
708
+ const toDateTime = query.get('toDateTime');
709
+ const queue = query.get('queue');
710
+ const namespace = query.get('namespace');
711
+ const task = query.get('task');
712
+
713
+ let aborted = false;
714
+ res.onAborted(() => {
715
+ aborted = true;
716
+ log('Analytics request aborted');
717
+ });
718
+
719
+ analyticsRoutes.getThroughput(pool, { fromDateTime, toDateTime, queue, namespace, task }).then(result => {
720
+ if (aborted) return;
721
+ res.cork(() => {
722
+ setCorsHeaders(res);
723
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
724
+ });
725
+ }).catch(error => {
726
+ if (aborted) return;
727
+ log('Analytics error:', error.message || error);
728
+ res.cork(() => {
729
+ setCorsHeaders(res);
730
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
731
+ });
732
+ });
733
+ });
734
+
735
+ app.get('/api/v1/analytics/queue-lag', (res, req) => {
736
+ const query = new URLSearchParams(req.getQuery());
737
+ const queue = query.get('queue');
738
+ const namespace = query.get('namespace');
739
+ const task = query.get('task');
740
+ const fromDateTime = query.get('fromDateTime');
741
+ const toDateTime = query.get('toDateTime');
742
+
743
+ let aborted = false;
744
+ res.onAborted(() => {
745
+ aborted = true;
746
+ log('Queue lag request aborted');
747
+ });
748
+
749
+ analyticsRoutes.getQueueLag({ queue, namespace, task, fromDateTime, toDateTime }).then(result => {
750
+ if (aborted) return;
751
+ res.cork(() => {
752
+ setCorsHeaders(res);
753
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
754
+ });
755
+ }).catch(error => {
756
+ if (aborted) return;
757
+ log('Queue lag error:', error);
758
+ res.cork(() => {
759
+ setCorsHeaders(res);
760
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
761
+ });
762
+ });
763
+ });
764
+
765
+ // Queue stats route
766
+ app.get('/api/v1/analytics/queue-stats', (res, req) => {
767
+ const query = new URLSearchParams(req.getQuery());
768
+ const queue = query.get('queue');
769
+ const namespace = query.get('namespace');
770
+ const task = query.get('task');
771
+ const fromDateTime = query.get('fromDateTime');
772
+ const toDateTime = query.get('toDateTime');
773
+
774
+ let aborted = false;
775
+ res.onAborted(() => {
776
+ aborted = true;
777
+ log('Queue stats request aborted');
778
+ });
779
+
780
+ queueManager.getQueueStats({ queue, namespace, task, fromDateTime, toDateTime }).then(result => {
781
+ if (aborted) return;
782
+ res.cork(() => {
783
+ setCorsHeaders(res);
784
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
785
+ });
786
+ }).catch(error => {
787
+ if (aborted) return;
788
+ log('Queue stats error:', error);
789
+ res.cork(() => {
790
+ setCorsHeaders(res);
791
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
792
+ });
793
+ });
794
+ });
795
+
796
+ // Messages routes
797
+ app.get('/api/v1/messages', (res, req) => {
798
+ const query = new URLSearchParams(req.getQuery());
799
+
800
+ let aborted = false;
801
+ res.onAborted(() => {
802
+ aborted = true;
803
+ log('Messages list request aborted');
804
+ });
805
+
806
+ const filters = {
807
+ ns: query.get('ns'),
808
+ task: query.get('task'),
809
+ queue: query.get('queue'),
810
+ status: query.get('status'),
811
+ limit: parseInt(query.get('limit') || config.API.DEFAULT_LIMIT.toString()),
812
+ offset: parseInt(query.get('offset') || config.API.DEFAULT_OFFSET.toString())
813
+ };
814
+
815
+ messagesRoutes.listMessages(filters).then(result => {
816
+ if (aborted) return;
817
+ res.cork(() => {
818
+ setCorsHeaders(res);
819
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ messages: result }));
820
+ });
821
+ }).catch(error => {
822
+ if (aborted) return;
823
+ log('List messages error:', error);
824
+ res.cork(() => {
825
+ setCorsHeaders(res);
826
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
827
+ });
828
+ });
829
+ });
830
+
831
+ app.get('/api/v1/messages/:transactionId', (res, req) => {
832
+ const transactionId = req.getParameter(0);
833
+
834
+ let aborted = false;
835
+ res.onAborted(() => {
836
+ aborted = true;
837
+ log('Get message request aborted');
838
+ });
839
+
840
+ messagesRoutes.getMessage(transactionId).then(result => {
841
+ if (aborted) return;
842
+ res.cork(() => {
843
+ setCorsHeaders(res);
844
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
845
+ });
846
+ }).catch(error => {
847
+ if (aborted) return;
848
+ log('Get message error:', error);
849
+ res.cork(() => {
850
+ setCorsHeaders(res);
851
+ res.writeStatus(error.message === 'Message not found' ? config.HTTP_STATUS.NOT_FOUND.toString() : config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString())
852
+ .end(JSON.stringify({ error: error.message }));
853
+ });
854
+ });
855
+ });
856
+
857
+ app.del('/api/v1/messages/:transactionId', (res, req) => {
858
+ const transactionId = req.getParameter(0);
859
+
860
+ let aborted = false;
861
+ res.onAborted(() => {
862
+ aborted = true;
863
+ log('Delete message request aborted');
864
+ });
865
+
866
+ messagesRoutes.deleteMessage(transactionId).then(result => {
867
+ if (aborted) return;
868
+ res.cork(() => {
869
+ setCorsHeaders(res);
870
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
871
+ });
872
+ }).catch(error => {
873
+ if (aborted) return;
874
+ log('Delete message error:', error);
875
+ res.cork(() => {
876
+ setCorsHeaders(res);
877
+ res.writeStatus(error.message === 'Message not found' ? config.HTTP_STATUS.NOT_FOUND.toString() : config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString())
878
+ .end(JSON.stringify({ error: error.message }));
879
+ });
880
+ });
881
+ });
882
+
883
+ app.post('/api/v1/messages/:transactionId/retry', (res, req) => {
884
+ const transactionId = req.getParameter(0);
885
+
886
+ let aborted = false;
887
+ res.onAborted(() => {
888
+ aborted = true;
889
+ log('Retry message request aborted');
890
+ });
891
+
892
+ messagesRoutes.retryMessage(transactionId).then(result => {
893
+ if (aborted) return;
894
+ res.cork(() => {
895
+ setCorsHeaders(res);
896
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
897
+ });
898
+ }).catch(error => {
899
+ if (aborted) return;
900
+ log('Retry message error:', error);
901
+ res.cork(() => {
902
+ setCorsHeaders(res);
903
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
904
+ });
905
+ });
906
+ });
907
+
908
+ app.post('/api/v1/messages/:transactionId/dlq', (res, req) => {
909
+ const transactionId = req.getParameter(0);
910
+
911
+ let aborted = false;
912
+ res.onAborted(() => {
913
+ aborted = true;
914
+ log('Move to DLQ request aborted');
915
+ });
916
+
917
+ messagesRoutes.moveToDLQ(transactionId).then(result => {
918
+ if (aborted) return;
919
+ res.cork(() => {
920
+ setCorsHeaders(res);
921
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
922
+ });
923
+ }).catch(error => {
924
+ if (aborted) return;
925
+ log('Move to DLQ error:', error);
926
+ res.cork(() => {
927
+ setCorsHeaders(res);
928
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
929
+ });
930
+ });
931
+ });
932
+
933
+ app.get('/api/v1/messages/:transactionId/related', (res, req) => {
934
+ const transactionId = req.getParameter(0);
935
+
936
+ let aborted = false;
937
+ res.onAborted(() => {
938
+ aborted = true;
939
+ log('Get related messages request aborted');
940
+ });
941
+
942
+ messagesRoutes.getRelatedMessages(transactionId).then(result => {
943
+ if (aborted) return;
944
+ res.cork(() => {
945
+ setCorsHeaders(res);
946
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ messages: result }));
947
+ });
948
+ }).catch(error => {
949
+ if (aborted) return;
950
+ log('Get related messages error:', error);
951
+ res.cork(() => {
952
+ setCorsHeaders(res);
953
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
954
+ });
955
+ });
956
+ });
957
+
958
+ app.del('/api/v1/queues/:queue/clear', (res, req) => {
959
+ const queue = req.getParameter(0);
960
+ const query = new URLSearchParams(req.getQuery());
961
+ const partition = query.get('partition');
962
+
963
+ let aborted = false;
964
+ res.onAborted(() => {
965
+ aborted = true;
966
+ log('Clear queue request aborted');
967
+ });
968
+
969
+ messagesRoutes.clearQueue(queue, partition).then(result => {
970
+ if (aborted) return;
971
+ res.cork(() => {
972
+ setCorsHeaders(res);
973
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
974
+ });
975
+ }).catch(error => {
976
+ if (aborted) return;
977
+ log('Clear queue error:', error);
978
+ res.cork(() => {
979
+ setCorsHeaders(res);
980
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
981
+ });
982
+ });
983
+ });
984
+
985
+ // Resources routes
986
+ app.get('/api/v1/resources/queues', (res, req) => {
987
+ const query = new URLSearchParams(req.getQuery());
988
+ const namespace = query.get('namespace');
989
+ const task = query.get('task');
990
+
991
+ let aborted = false;
992
+ res.onAborted(() => {
993
+ aborted = true;
994
+ log('Get queues request aborted');
995
+ });
996
+
997
+ resourcesRoutes.getQueues({ namespace, task }).then(result => {
998
+ if (aborted) return;
999
+ res.cork(() => {
1000
+ setCorsHeaders(res);
1001
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ queues: result }));
1002
+ });
1003
+ }).catch(error => {
1004
+ if (aborted) return;
1005
+ log('Get queues error:', error);
1006
+ res.cork(() => {
1007
+ setCorsHeaders(res);
1008
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
1009
+ });
1010
+ });
1011
+ });
1012
+
1013
+ app.get('/api/v1/resources/queues/:queue', (res, req) => {
1014
+ const queue = req.getParameter(0);
1015
+
1016
+ let aborted = false;
1017
+ res.onAborted(() => {
1018
+ aborted = true;
1019
+ log('Get queue request aborted');
1020
+ });
1021
+
1022
+ resourcesRoutes.getQueue(queue).then(result => {
1023
+ if (aborted) return;
1024
+ res.cork(() => {
1025
+ setCorsHeaders(res);
1026
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
1027
+ });
1028
+ }).catch(error => {
1029
+ if (aborted) return;
1030
+ log('Get queue error:', error);
1031
+ res.cork(() => {
1032
+ setCorsHeaders(res);
1033
+ res.writeStatus(error.message === 'Queue not found' ? config.HTTP_STATUS.NOT_FOUND.toString() : config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString())
1034
+ .end(JSON.stringify({ error: error.message }));
1035
+ });
1036
+ });
1037
+ });
1038
+
1039
+ app.get('/api/v1/resources/partitions', (res, req) => {
1040
+ const query = new URLSearchParams(req.getQuery());
1041
+ const queue = query.get('queue');
1042
+ const minDepth = query.get('minDepth');
1043
+
1044
+ let aborted = false;
1045
+ res.onAborted(() => {
1046
+ aborted = true;
1047
+ log('Get partitions request aborted');
1048
+ });
1049
+
1050
+ resourcesRoutes.getPartitions({ queue, minDepth: minDepth ? parseInt(minDepth) : null }).then(result => {
1051
+ if (aborted) return;
1052
+ res.cork(() => {
1053
+ setCorsHeaders(res);
1054
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ partitions: result }));
1055
+ });
1056
+ }).catch(error => {
1057
+ if (aborted) return;
1058
+ log('Get partitions error:', error);
1059
+ res.cork(() => {
1060
+ setCorsHeaders(res);
1061
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
1062
+ });
1063
+ });
1064
+ });
1065
+
1066
+ app.get('/api/v1/resources/namespaces', (res, req) => {
1067
+ let aborted = false;
1068
+ res.onAborted(() => {
1069
+ aborted = true;
1070
+ log('Get namespaces request aborted');
1071
+ });
1072
+
1073
+ resourcesRoutes.getNamespaces().then(result => {
1074
+ if (aborted) return;
1075
+ res.cork(() => {
1076
+ setCorsHeaders(res);
1077
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ namespaces: result }));
1078
+ });
1079
+ }).catch(error => {
1080
+ if (aborted) return;
1081
+ log('Get namespaces error:', error);
1082
+ res.cork(() => {
1083
+ setCorsHeaders(res);
1084
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
1085
+ });
1086
+ });
1087
+ });
1088
+
1089
+ app.get('/api/v1/resources/tasks', (res, req) => {
1090
+ let aborted = false;
1091
+ res.onAborted(() => {
1092
+ aborted = true;
1093
+ log('Get tasks request aborted');
1094
+ });
1095
+
1096
+ resourcesRoutes.getTasks().then(result => {
1097
+ if (aborted) return;
1098
+ res.cork(() => {
1099
+ setCorsHeaders(res);
1100
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({ tasks: result }));
1101
+ });
1102
+ }).catch(error => {
1103
+ if (aborted) return;
1104
+ log('Get tasks error:', error);
1105
+ res.cork(() => {
1106
+ setCorsHeaders(res);
1107
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
1108
+ });
1109
+ });
1110
+ });
1111
+
1112
+ app.get('/api/v1/resources/overview', (res, req) => {
1113
+ let aborted = false;
1114
+ res.onAborted(() => {
1115
+ aborted = true;
1116
+ log('Get overview request aborted');
1117
+ });
1118
+
1119
+ resourcesRoutes.getSystemOverview().then(result => {
1120
+ if (aborted) return;
1121
+ res.cork(() => {
1122
+ setCorsHeaders(res);
1123
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(result));
1124
+ });
1125
+ }).catch(error => {
1126
+ if (aborted) return;
1127
+ log('Get overview error:', error);
1128
+ res.cork(() => {
1129
+ setCorsHeaders(res);
1130
+ res.writeStatus(config.HTTP_STATUS.INTERNAL_SERVER_ERROR.toString()).end(JSON.stringify({ error: error.message }));
1131
+ });
1132
+ });
1133
+ });
1134
+
1135
+ // Health check with performance stats
1136
+ app.get('/health', async (res, req) => {
1137
+ let aborted = false;
1138
+ res.onAborted(() => {
1139
+ aborted = true;
1140
+ log('Health check aborted');
1141
+ });
1142
+
1143
+ try {
1144
+ await pool.query('SELECT 1');
1145
+ if (aborted) return;
1146
+
1147
+ const uptime = (Date.now() - startTime) / 1000;
1148
+ res.cork(() => {
1149
+ setCorsHeaders(res);
1150
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify({
1151
+ status: 'healthy',
1152
+ uptime: `${Math.floor(uptime)}s`,
1153
+ connections: wsServer.getConnectionCount(),
1154
+ stats: {
1155
+ requests: requestCount,
1156
+ messages: messageCount,
1157
+ requestsPerSecond: (requestCount / uptime).toFixed(2),
1158
+ messagesPerSecond: (messageCount / uptime).toFixed(2),
1159
+ pool: {
1160
+ total: pool.totalCount,
1161
+ idle: pool.idleCount,
1162
+ waiting: pool.waitingCount
1163
+ }
1164
+ }
1165
+ }));
1166
+ });
1167
+ } catch (error) {
1168
+ if (aborted) return;
1169
+ res.cork(() => {
1170
+ setCorsHeaders(res);
1171
+ res.writeStatus(config.HTTP_STATUS.SERVICE_UNAVAILABLE.toString()).end(JSON.stringify({
1172
+ status: 'unhealthy',
1173
+ error: error.message
1174
+ }));
1175
+ });
1176
+ }
1177
+ });
1178
+
1179
+ // Metrics endpoint for performance monitoring
1180
+ app.get('/metrics', (res, req) => {
1181
+ res.onAborted(() => {
1182
+ log('Metrics request aborted');
1183
+ });
1184
+
1185
+ const uptime = (Date.now() - startTime) / 1000;
1186
+ const metrics = {
1187
+ uptime: uptime,
1188
+ requests: {
1189
+ total: requestCount,
1190
+ rate: requestCount / uptime
1191
+ },
1192
+ messages: {
1193
+ total: messageCount,
1194
+ rate: messageCount / uptime
1195
+ },
1196
+ database: {
1197
+ poolSize: pool.totalCount,
1198
+ idleConnections: pool.idleCount,
1199
+ waitingRequests: pool.waitingCount
1200
+ },
1201
+ memory: process.memoryUsage(),
1202
+ cpu: process.cpuUsage()
1203
+ };
1204
+
1205
+ res.cork(() => {
1206
+ setCorsHeaders(res);
1207
+ res.writeStatus(config.HTTP_STATUS.OK.toString()).end(JSON.stringify(metrics));
1208
+ });
1209
+ });
1210
+
1211
+ // Variable to hold system event consumer stop function
1212
+ let stopSystemEventConsumer = null;
1213
+
1214
+ // Start server
1215
+ app.listen(HOST, PORT, async (token) => {
1216
+ if (token) {
1217
+ log(`āœ… Queen Server running at http://${HOST}:${PORT}`);
1218
+ log(`šŸ“Š Features enabled:`);
1219
+ log(` - Encryption: ${encryptionEnabled ? 'āœ…' : 'āŒ (set QUEEN_ENCRYPTION_KEY)'}`);
1220
+ log(` - Retention: āœ…`);
1221
+ log(` - Eviction: āœ…`);
1222
+ log(` - WebSocket Dashboard: ws://${HOST}:${PORT}/ws/dashboard`);
1223
+ log(` - Server Instance ID: ${SERVER_INSTANCE_ID}`);
1224
+
1225
+ // Start system event synchronization and consumption
1226
+ try {
1227
+ // Create temporary client for synchronization
1228
+ const syncClient = createQueenClient({
1229
+ baseUrl: `http://localhost:${PORT}`
1230
+ });
1231
+
1232
+ // Synchronize system events (catch up on missed events)
1233
+ await syncSystemEvents(syncClient, systemEventManager, SERVER_INSTANCE_ID);
1234
+
1235
+ // Start consuming system events
1236
+ // Note: consume() returns the stop function directly
1237
+ stopSystemEventConsumer = syncClient.consume({
1238
+ queue: SYSTEM_QUEUE,
1239
+ consumerGroup: SERVER_INSTANCE_ID,
1240
+ handler: async (message) => {
1241
+ await systemEventManager.processSystemEvent(message.payload);
1242
+ },
1243
+ options: {
1244
+ batch: 10,
1245
+ wait: true
1246
+ }
1247
+ });
1248
+
1249
+ log(`āœ… System event consumer started`);
1250
+ } catch (error) {
1251
+ log(`āš ļø Failed to start system event consumer: ${error.message}`);
1252
+ }
1253
+
1254
+ log(`šŸŽÆ Ready to process messages with high performance!`);
1255
+ } else {
1256
+ log(`Failed to start server on port ${PORT}`);
1257
+ process.exit(1);
1258
+ }
1259
+ });
1260
+
1261
+ // Graceful shutdown
1262
+ const shutdown = async (signal) => {
1263
+ log(`\nšŸ›‘ Received ${signal}, shutting down gracefully...`);
1264
+
1265
+ // Stop system event consumer
1266
+ if (stopSystemEventConsumer) {
1267
+ stopSystemEventConsumer();
1268
+ log('āœ… System event consumer stopped');
1269
+ }
1270
+
1271
+ // Stop background jobs
1272
+ if (stopRetention) stopRetention();
1273
+ if (stopEviction) stopEviction();
1274
+
1275
+ const uptime = (Date.now() - startTime) / 1000;
1276
+ if (messageCount > 0) {
1277
+ log(`šŸ“Š Final stats: ${messageCount} messages processed in ${uptime.toFixed(1)}s`);
1278
+ log(` Average: ${(messageCount / uptime).toFixed(2)} messages/second`);
1279
+ }
1280
+
1281
+ await pool.end();
1282
+ process.exit(0);
1283
+ };
1284
+
1285
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
1286
+ process.on('SIGINT', () => shutdown('SIGINT'));