queen-mq 0.1.0 → 0.1.2

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 (160) hide show
  1. package/API.md +862 -752
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +1705 -1051
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-complete-client.js +260 -0
  17. package/examples/test-dashboard-api.js +200 -0
  18. package/examples/test-traceid.js +147 -0
  19. package/package.json +17 -4
  20. package/server.log +1 -0
  21. package/src/benchmark/consumer.js +207 -0
  22. package/src/benchmark/consumer_multi.js +216 -0
  23. package/src/benchmark/producer.js +75 -0
  24. package/src/benchmark/producer_multi.js +115 -0
  25. package/src/client/client.js +300 -31
  26. package/src/client/queenClient.js +5 -0
  27. package/src/cluster-server.js +242 -0
  28. package/src/config.js +19 -5
  29. package/src/database/connection.js +42 -16
  30. package/src/database/poolManager.js +7 -0
  31. package/src/database/schema-v2.sql +194 -130
  32. package/src/managers/queueManagerOptimized.js +823 -933
  33. package/src/managers/systemEventManager.js +8 -3
  34. package/src/routes/messages.js +127 -57
  35. package/src/routes/pop.js +27 -43
  36. package/src/routes/resources.js +61 -27
  37. package/src/routes/status.js +1037 -0
  38. package/src/server.js +308 -272
  39. package/src/services/evictionService.js +57 -28
  40. package/src/services/retentionService.js +44 -11
  41. package/src/test/MIGRATION_ISSUES.md +174 -0
  42. package/src/test/README.md +203 -0
  43. package/src/test/advanced-pattern-tests.js +1137 -0
  44. package/src/test/bus-mode-tests.js +361 -0
  45. package/src/test/core-tests.js +342 -0
  46. package/src/test/edge-case-tests.js +561 -0
  47. package/src/test/enterprise-tests.js +637 -0
  48. package/src/test/partition-locking-tests.js +545 -0
  49. package/src/test/test-new.js +278 -0
  50. package/src/test/test.js +6 -3
  51. package/src/test/utils.js +169 -0
  52. package/src/utils/streaming.js +231 -0
  53. package/src/utils/uuid.js +2 -2
  54. package/src/websocket/wsServer.js +10 -3
  55. package/test-keepalive-v2.sh +22 -0
  56. package/webapp/COLOR_GUIDE.md +118 -0
  57. package/webapp/README.md +143 -0
  58. package/webapp/index.html +14 -0
  59. package/webapp/package-lock.json +3184 -0
  60. package/webapp/package.json +25 -0
  61. package/webapp/postcss.config.js +7 -0
  62. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  63. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  64. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  65. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  66. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  67. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  68. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  69. package/webapp/public/assets/queen-logo.svg +263 -0
  70. package/webapp/src/App.vue +19 -0
  71. package/webapp/src/api/analytics.js +10 -0
  72. package/webapp/src/api/client.js +29 -0
  73. package/webapp/src/api/consumers.js +52 -0
  74. package/webapp/src/api/health.js +7 -0
  75. package/webapp/src/api/messages.js +26 -0
  76. package/webapp/src/api/queues.js +14 -0
  77. package/webapp/src/api/resources.js +8 -0
  78. package/webapp/src/assets/styles/main.css +357 -0
  79. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  80. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  81. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  82. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  83. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  84. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  85. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  86. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  87. package/webapp/src/components/common/MetricCard.vue +43 -0
  88. package/webapp/src/components/common/StatusBadge.vue +45 -0
  89. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  90. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  91. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  92. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  93. package/webapp/src/components/layout/AppLayout.vue +110 -0
  94. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  95. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  96. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  97. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  98. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  99. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  100. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  101. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  102. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  103. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  104. package/webapp/src/composables/useApi.js +34 -0
  105. package/webapp/src/composables/useTheme.js +36 -0
  106. package/webapp/src/main.js +11 -0
  107. package/webapp/src/router/index.js +42 -0
  108. package/webapp/src/utils/colors.js +96 -0
  109. package/webapp/src/utils/formatters.js +49 -0
  110. package/webapp/src/views/Analytics.vue +377 -0
  111. package/webapp/src/views/ConsumerGroups.vue +433 -0
  112. package/webapp/src/views/Dashboard.vue +418 -0
  113. package/webapp/src/views/Messages.vue +361 -0
  114. package/webapp/src/views/QueueDetail.vue +582 -0
  115. package/webapp/src/views/Queues.vue +496 -0
  116. package/webapp/tailwind.config.js +25 -0
  117. package/webapp/vite.config.js +10 -0
  118. package/CACHE.md +0 -519
  119. package/DASHBOARD-V3.md +0 -478
  120. package/DASHBOARD.md +0 -382
  121. package/MOD_QUEUE.md +0 -453
  122. package/PARTITION_LOCKING_DESIGN.md +0 -989
  123. package/PLAN.md +0 -707
  124. package/QUERY_ANALSYS.md +0 -72
  125. package/QUEUE_BUS.md +0 -334
  126. package/V2-PLAN.md +0 -236
  127. package/dashboard/.vscode/extensions.json +0 -3
  128. package/dashboard/README.md +0 -5
  129. package/dashboard/index.html +0 -14
  130. package/dashboard/package-lock.json +0 -1458
  131. package/dashboard/package.json +0 -25
  132. package/dashboard/public/vite.svg +0 -1
  133. package/dashboard/src/App.vue +0 -29
  134. package/dashboard/src/assets/styles/main.css +0 -908
  135. package/dashboard/src/assets/vue.svg +0 -1
  136. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  137. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  138. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  139. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  140. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  141. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  142. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  143. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  144. package/dashboard/src/main.js +0 -44
  145. package/dashboard/src/router.js +0 -54
  146. package/dashboard/src/services/api.js +0 -187
  147. package/dashboard/src/services/websocket.js +0 -167
  148. package/dashboard/src/utils/constants.js +0 -56
  149. package/dashboard/src/utils/helpers.js +0 -118
  150. package/dashboard/src/views/Analytics.vue +0 -912
  151. package/dashboard/src/views/Dashboard.vue +0 -906
  152. package/dashboard/src/views/Messages.vue +0 -437
  153. package/dashboard/src/views/QueueDetail.vue +0 -501
  154. package/dashboard/src/views/Queues.vue +0 -333
  155. package/dashboard/vite.config.js +0 -30
  156. package/debug-namespace.js +0 -110
  157. package/docs/long-polling.md +0 -159
  158. package/docs/multi-server-cache-solutions.md +0 -185
  159. package/docs/performance-tuning.md +0 -222
  160. package/src/routes/analytics.js +0 -812
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Complete Test Suite for Queen Message Queue System
5
+ * Using the new minimalist Queen client interface
6
+ *
7
+ * Test Categories:
8
+ * 1. Core Features
9
+ * 2. Partition Locking
10
+ * 3. Enterprise Features (coming soon)
11
+ * 4. Bus Mode Features (coming soon)
12
+ * 5. Edge Cases (coming soon)
13
+ * 6. Advanced Scenarios & Patterns (coming soon)
14
+ */
15
+
16
+ import { Queen } from '../client/client.js';
17
+ import {
18
+ TEST_CONFIG,
19
+ initDb,
20
+ closeDb,
21
+ cleanupTestData,
22
+ printResults,
23
+ log,
24
+ resetResults
25
+ } from './utils.js';
26
+
27
+ // Import test modules
28
+ import {
29
+ testQueueCreationPolicy,
30
+ testSingleMessagePush,
31
+ testBatchMessagePush,
32
+ testQueueConfiguration,
33
+ testTakeAndAcknowledgment,
34
+ testDelayedProcessing,
35
+ testPartitionFIFOOrdering
36
+ } from './core-tests.js';
37
+
38
+ import {
39
+ testPartitionLocking,
40
+ testBusPartitionLocking,
41
+ testSpecificPartitionLocking,
42
+ testNamespaceTaskFiltering,
43
+ testNamespaceTaskBusMode
44
+ } from './partition-locking-tests.js';
45
+
46
+ import {
47
+ testMessageEncryption,
48
+ testRetentionPendingMessages,
49
+ testRetentionCompletedMessages,
50
+ testPartitionRetention,
51
+ testConsumerEncryption,
52
+ testMessageEviction,
53
+ testCombinedEnterpriseFeatures,
54
+ testEnterpriseErrorHandling
55
+ } from './enterprise-tests.js';
56
+
57
+ import {
58
+ testBusConsumerGroups,
59
+ testMixedMode,
60
+ testConsumerGroupSubscriptionModes,
61
+ testConsumerGroupIsolation
62
+ } from './bus-mode-tests.js';
63
+
64
+ import {
65
+ testEmptyAndNullPayloads,
66
+ testVeryLargePayloads,
67
+ testConcurrentPushOperations,
68
+ testConcurrentTakeOperations,
69
+ testManyConsumerGroups,
70
+ testRetryLimitExhaustion,
71
+ testLeaseExpiration,
72
+ testSQLInjectionPrevention,
73
+ testXSSPrevention
74
+ } from './edge-case-tests.js';
75
+
76
+ import {
77
+ testMultiStagePipeline,
78
+ testFanOutFanIn,
79
+ testComplexPriorityScenarios,
80
+ testDynamicPriorityAdjustment,
81
+ testSagaPattern,
82
+ testRateLimiting,
83
+ testTimeBatchProcessing,
84
+ testEventSourcing,
85
+ testQueueMetrics,
86
+ testMultipleConsumersSinglePartitionOrdering,
87
+ testDeadLetterQueuePattern,
88
+ testCircuitBreaker,
89
+ testMessageDeduplication
90
+ } from './advanced-pattern-tests.js';
91
+
92
+ // Helper to run a test with error handling and delay
93
+ async function runTest(testFn) {
94
+ try {
95
+ await testFn();
96
+ } catch (error) {
97
+ log(`Unexpected error in test: ${error.message}`, 'error');
98
+ }
99
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay between tests
100
+ }
101
+
102
+ // Parse command-line arguments
103
+ const args = process.argv.slice(2);
104
+ const testFilter = args[0]; // e.g., 'core', 'bus', 'edge', 'enterprise', 'partition', 'advanced'
105
+
106
+ // Show help if requested
107
+ if (testFilter === 'help' || testFilter === '--help' || testFilter === '-h') {
108
+ console.log('Usage: node src/test/test-new.js [category]');
109
+ console.log('\nAvailable test categories:');
110
+ console.log(' core - Core features (queue creation, push, take, ack, etc.)');
111
+ console.log(' partition - Partition locking tests');
112
+ console.log(' enterprise - Enterprise features (encryption, retention, eviction)');
113
+ console.log(' bus - Bus mode features (consumer groups, mixed mode)');
114
+ console.log(' edge - Edge cases (null payloads, large payloads, SQL injection, etc.)');
115
+ console.log(' advanced - Advanced patterns (pipeline, fan-out, DLQ, circuit breaker)');
116
+ console.log('\nExamples:');
117
+ console.log(' node src/test/test-new.js # Run all tests');
118
+ console.log(' node src/test/test-new.js core # Run only core tests');
119
+ console.log(' node src/test/test-new.js bus # Run only bus mode tests');
120
+ console.log(' node src/test/test-new.js edge # Run only edge case tests');
121
+ process.exit(0);
122
+ }
123
+
124
+ async function runTests() {
125
+ console.log('🚀 Starting Queen Message Queue Test Suite');
126
+ console.log(' Using the new minimalist Queen client interface\n');
127
+ if (testFilter) {
128
+ console.log(` 🎯 Running only: ${testFilter} tests\n`);
129
+ }
130
+ console.log('='.repeat(80));
131
+
132
+ let client;
133
+
134
+ try {
135
+ // Initialize database connection
136
+ await initDb();
137
+ log('Database connection established');
138
+
139
+ // Initialize client
140
+ client = new Queen({
141
+ baseUrls: TEST_CONFIG.baseUrls,
142
+ timeout: 30000,
143
+ retryAttempts: 3
144
+ });
145
+ log('Queen client initialized');
146
+
147
+ // Check for encryption key
148
+ if (process.env.QUEEN_ENCRYPTION_KEY) {
149
+ log('Encryption key detected - encryption tests will run', 'info');
150
+ } else {
151
+ log('No encryption key found - encryption tests will be skipped', 'warning');
152
+ }
153
+
154
+ // Clean up any existing test data
155
+ await cleanupTestData();
156
+
157
+ // ============================================
158
+ // CORE FEATURE TESTS
159
+ // ============================================
160
+ if (!testFilter || testFilter === 'core') {
161
+ console.log('\n📦 CORE FEATURES');
162
+ console.log('-'.repeat(40));
163
+
164
+ await runTest(() => testQueueCreationPolicy(client));
165
+ await runTest(() => testSingleMessagePush(client));
166
+ await runTest(() => testBatchMessagePush(client));
167
+ await runTest(() => testQueueConfiguration(client));
168
+ await runTest(() => testTakeAndAcknowledgment(client));
169
+ await runTest(() => testDelayedProcessing(client));
170
+ await runTest(() => testPartitionFIFOOrdering(client));
171
+ }
172
+
173
+ // ============================================
174
+ // PARTITION LOCKING TESTS
175
+ // ============================================
176
+ if (!testFilter || testFilter === 'partition' || testFilter === 'locking') {
177
+ console.log('\n🔒 PARTITION LOCKING');
178
+ console.log('-'.repeat(40));
179
+
180
+ await runTest(() => testPartitionLocking(client));
181
+ await runTest(() => testBusPartitionLocking(client));
182
+ await runTest(() => testSpecificPartitionLocking(client));
183
+ await runTest(() => testNamespaceTaskFiltering(client));
184
+ await runTest(() => testNamespaceTaskBusMode(client));
185
+ }
186
+
187
+ // ============================================
188
+ // ENTERPRISE FEATURE TESTS
189
+ // ============================================
190
+ if (!testFilter || testFilter === 'enterprise') {
191
+ console.log('\n🏢 ENTERPRISE FEATURES');
192
+ console.log('-'.repeat(40));
193
+
194
+ await runTest(() => testMessageEncryption(client));
195
+ await runTest(() => testRetentionPendingMessages(client));
196
+ await runTest(() => testRetentionCompletedMessages(client));
197
+ await runTest(() => testPartitionRetention(client));
198
+ await runTest(() => testConsumerEncryption(client));
199
+ await runTest(() => testMessageEviction(client));
200
+ await runTest(() => testCombinedEnterpriseFeatures(client));
201
+ await runTest(() => testEnterpriseErrorHandling(client));
202
+ }
203
+
204
+ // ============================================
205
+ // BUS MODE FEATURE TESTS
206
+ // ============================================
207
+ if (!testFilter || testFilter === 'bus') {
208
+ console.log('\n🚌 BUS MODE FEATURES');
209
+ console.log('-'.repeat(40));
210
+
211
+ await runTest(() => testBusConsumerGroups(client));
212
+ await runTest(() => testMixedMode(client));
213
+ await runTest(() => testConsumerGroupSubscriptionModes(client));
214
+ await runTest(() => testConsumerGroupIsolation(client));
215
+ }
216
+
217
+ // ============================================
218
+ // EDGE CASE TESTS
219
+ // ============================================
220
+ if (!testFilter || testFilter === 'edge') {
221
+ console.log('\n🔍 EDGE CASES');
222
+ console.log('-'.repeat(40));
223
+
224
+ await runTest(() => testEmptyAndNullPayloads(client));
225
+ await runTest(() => testVeryLargePayloads(client));
226
+ await runTest(() => testConcurrentPushOperations(client));
227
+ await runTest(() => testConcurrentTakeOperations(client));
228
+ await runTest(() => testManyConsumerGroups(client));
229
+ await runTest(() => testRetryLimitExhaustion(client));
230
+ await runTest(() => testLeaseExpiration(client));
231
+ await runTest(() => testSQLInjectionPrevention(client));
232
+ await runTest(() => testXSSPrevention(client));
233
+ }
234
+
235
+ // ============================================
236
+ // ADVANCED SCENARIO TESTS
237
+ // ============================================
238
+ if (!testFilter || testFilter === 'advanced' || testFilter === 'pattern') {
239
+ console.log('\n🎯 ADVANCED SCENARIOS');
240
+ console.log('-'.repeat(40));
241
+
242
+ await runTest(() => testMultiStagePipeline(client));
243
+ await runTest(() => testFanOutFanIn(client));
244
+ await runTest(() => testComplexPriorityScenarios(client));
245
+ await runTest(() => testDynamicPriorityAdjustment(client));
246
+ await runTest(() => testDeadLetterQueuePattern(client));
247
+ await runTest(() => testSagaPattern(client));
248
+ await runTest(() => testRateLimiting(client));
249
+ await runTest(() => testMessageDeduplication(client));
250
+ await runTest(() => testTimeBatchProcessing(client));
251
+ await runTest(() => testEventSourcing(client));
252
+ await runTest(() => testQueueMetrics(client));
253
+ await runTest(() => testCircuitBreaker(client));
254
+ await runTest(() => testMultipleConsumersSinglePartitionOrdering(client));
255
+ }
256
+
257
+ } catch (error) {
258
+ log(`Test suite setup failed: ${error.message}`, 'error');
259
+ console.error(error);
260
+ } finally {
261
+ // Cleanup
262
+ if (client) {
263
+ await client.close();
264
+ }
265
+ await cleanupTestData();
266
+ await closeDb();
267
+ }
268
+
269
+ // Print results
270
+ printResults();
271
+ }
272
+
273
+ // Run the test suite
274
+ runTests().catch(error => {
275
+ console.error('💥 Test suite crashed:', error);
276
+ process.exit(1);
277
+ });
278
+
package/src/test/test.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * 5. Advanced Scenarios & Patterns
13
13
  */
14
14
 
15
- import { Queen } from '../client/client.js';
15
+ import { createQueenClient } from '../client/queenClient.js';
16
16
  import pg from 'pg';
17
17
  import crypto from 'crypto';
18
18
 
@@ -124,7 +124,10 @@ const getMessageCount = async (queueName, partitionName = null) => {
124
124
  // Helper function to ensure queue exists
125
125
  const ensureQueue = async (queueName, options = {}) => {
126
126
  try {
127
- await client.queue(queueName, options);
127
+ await client.configure({
128
+ queue: queueName,
129
+ options: options
130
+ });
128
131
  } catch (error) {
129
132
  // Queue might already exist, that's okay
130
133
  if (!error.message.includes('already exists')) {
@@ -4301,7 +4304,7 @@ async function runAllTests() {
4301
4304
 
4302
4305
  try {
4303
4306
  // Initialize client and database connection
4304
- client = new Queen({ baseUrls: TEST_CONFIG.baseUrls });
4307
+ client = createQueenClient({ baseUrls: TEST_CONFIG.baseUrls });
4305
4308
  dbPool = new pg.Pool(TEST_CONFIG.dbConfig);
4306
4309
 
4307
4310
  // Test database connection
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Shared test utilities for Queen test suite
3
+ */
4
+
5
+ import pg from 'pg';
6
+
7
+ // Test configuration
8
+ export const TEST_CONFIG = {
9
+ baseUrls: ['http://localhost:6632'],
10
+ dbConfig: {
11
+ host: process.env.PG_HOST || 'localhost',
12
+ port: process.env.PG_PORT || 5432,
13
+ database: process.env.PG_DB || 'postgres',
14
+ user: process.env.PG_USER || 'postgres',
15
+ password: process.env.PG_PASSWORD || 'postgres'
16
+ }
17
+ };
18
+
19
+ // Global test state
20
+ export let dbPool;
21
+ export let testResults = [];
22
+ export let currentTest = '';
23
+
24
+ // Initialize database pool
25
+ export async function initDb() {
26
+ dbPool = new pg.Pool(TEST_CONFIG.dbConfig);
27
+ await dbPool.query('SELECT 1');
28
+ return dbPool;
29
+ }
30
+
31
+ // Close database pool
32
+ export async function closeDb() {
33
+ if (dbPool) {
34
+ await dbPool.end();
35
+ }
36
+ }
37
+
38
+ // Test logging utilities
39
+ export const log = (message, type = 'info') => {
40
+ const timestamp = new Date().toISOString().substring(11, 23);
41
+ const prefix = {
42
+ info: '📝',
43
+ success: '✅',
44
+ error: '❌',
45
+ warning: '⚠️',
46
+ test: '🧪',
47
+ enterprise: '🏢',
48
+ edge: '🔍',
49
+ pattern: '🎯',
50
+ workflow: '🔄',
51
+ priority: '⚡'
52
+ }[type] || '📝';
53
+
54
+ console.log(`[${timestamp}] ${prefix} ${message}`);
55
+ };
56
+
57
+ export const startTest = (testName, category = 'test') => {
58
+ currentTest = testName;
59
+ log(`Starting: ${testName}`, category);
60
+ };
61
+
62
+ export const passTest = (message = '') => {
63
+ const result = { test: currentTest, status: 'PASS', message };
64
+ testResults.push(result);
65
+ log(`PASS: ${currentTest} ${message}`, 'success');
66
+ };
67
+
68
+ export const failTest = (error) => {
69
+ const result = { test: currentTest, status: 'FAIL', error: error.message };
70
+ testResults.push(result);
71
+ log(`FAIL: ${currentTest} - ${error.message}`, 'error');
72
+ };
73
+
74
+ export const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
75
+
76
+ // Database utilities
77
+ export const cleanupTestData = async () => {
78
+ try {
79
+ await dbPool.query(`
80
+ DELETE FROM queen.messages
81
+ WHERE partition_id IN (
82
+ SELECT p.id FROM queen.partitions p
83
+ JOIN queen.queues q ON p.queue_id = q.id
84
+ WHERE q.name LIKE 'test-%' OR q.name LIKE 'edge-%' OR q.name LIKE 'pattern-%' OR q.name LIKE 'workflow-%'
85
+ )
86
+ `);
87
+
88
+ await dbPool.query(`
89
+ DELETE FROM queen.partitions
90
+ WHERE queue_id IN (
91
+ SELECT id FROM queen.queues WHERE name LIKE 'test-%' OR name LIKE 'edge-%' OR name LIKE 'pattern-%' OR name LIKE 'workflow-%'
92
+ )
93
+ `);
94
+
95
+ await dbPool.query(`DELETE FROM queen.queues WHERE name LIKE 'test-%' OR name LIKE 'edge-%' OR name LIKE 'pattern-%' OR name LIKE 'workflow-%'`);
96
+
97
+ await dbPool.query(`
98
+ DELETE FROM queen.retention_history
99
+ WHERE partition_id NOT IN (SELECT id FROM queen.partitions)
100
+ `);
101
+
102
+ log('Test data cleaned up');
103
+ } catch (error) {
104
+ log(`Cleanup error: ${error.message}`, 'warning');
105
+ }
106
+ };
107
+
108
+ export const getMessageCount = async (queueName, partitionName = null) => {
109
+ const query = partitionName
110
+ ? `SELECT COUNT(*) as count FROM queen.messages m
111
+ JOIN queen.partitions p ON m.partition_id = p.id
112
+ JOIN queen.queues q ON p.queue_id = q.id
113
+ WHERE q.name = $1 AND p.name = $2`
114
+ : `SELECT COUNT(*) as count FROM queen.messages m
115
+ JOIN queen.partitions p ON m.partition_id = p.id
116
+ JOIN queen.queues q ON p.queue_id = q.id
117
+ WHERE q.name = $1`;
118
+
119
+ const params = partitionName ? [queueName, partitionName] : [queueName];
120
+ const result = await dbPool.query(query, params);
121
+ return parseInt(result.rows[0].count);
122
+ };
123
+
124
+ // Print test results summary
125
+ export function printResults() {
126
+ console.log('\n' + '='.repeat(80));
127
+ console.log('📊 TEST RESULTS SUMMARY');
128
+ console.log('='.repeat(80));
129
+
130
+ const passed = testResults.filter(r => r.status === 'PASS').length;
131
+ const failed = testResults.filter(r => r.status === 'FAIL').length;
132
+ const total = testResults.length;
133
+
134
+ console.log(`\n📈 Overall Results: ${passed}/${total} tests passed`);
135
+
136
+ if (failed > 0) {
137
+ console.log('\n❌ Failed Tests:');
138
+ testResults
139
+ .filter(r => r.status === 'FAIL')
140
+ .forEach(r => {
141
+ console.log(` • ${r.test}: ${r.error}`);
142
+ });
143
+ }
144
+
145
+ if (passed > 0) {
146
+ console.log('\n✅ Passed Tests:');
147
+ testResults
148
+ .filter(r => r.status === 'PASS')
149
+ .forEach(r => {
150
+ console.log(` • ${r.test}${r.message ? ': ' + r.message : ''}`);
151
+ });
152
+ }
153
+
154
+ console.log('\n' + '='.repeat(80));
155
+
156
+ if (failed === 0) {
157
+ console.log('🎉 ALL TESTS PASSED! Queen Message Queue System is working correctly.');
158
+ console.log(' All features are operational.');
159
+ } else {
160
+ console.log(`⚠️ ${failed} test(s) failed. Please review the failures above.`);
161
+ process.exit(1);
162
+ }
163
+ }
164
+
165
+ // Reset test results (useful for running tests in batches)
166
+ export function resetResults() {
167
+ testResults = [];
168
+ }
169
+
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Streaming utilities for uWebSockets.js with backpressure handling
3
+ *
4
+ * When sending large responses (>1MB), we need to handle backpressure to prevent:
5
+ * - Event loop blocking
6
+ * - Memory buildup
7
+ * - Slow client stalling the server
8
+ *
9
+ * uWebSockets.js provides efficient backpressure handling via:
10
+ * - res.write() - Returns false if buffer is full
11
+ * - res.onWritable() - Called when buffer has space again
12
+ * - res.cork() - Batches syscalls for efficiency
13
+ */
14
+
15
+ import { log } from './logger.js';
16
+
17
+ // Thresholds for streaming decisions
18
+ const SMALL_RESPONSE_THRESHOLD = 128 * 1024; // 128KB - send directly
19
+ const CHUNK_SIZE = 64 * 1024; // 64KB - stream in chunks
20
+
21
+ /**
22
+ * Send JSON response with automatic backpressure handling
23
+ *
24
+ * For small responses (<128KB): Sends directly via res.end()
25
+ * For large responses (>=128KB): Streams with backpressure via res.write() + res.onWritable()
26
+ *
27
+ * @param {Object} res - uWebSockets.js response object
28
+ * @param {Object} data - Data to serialize and send
29
+ * @param {Object} abortedRef - Reference object with 'aborted' flag
30
+ * @param {Object} options - Optional settings
31
+ * @returns {boolean} - True if sent successfully, false if aborted
32
+ */
33
+ export const streamJSON = (res, data, abortedRef = null, options = {}) => {
34
+ // Check if already aborted
35
+ if (abortedRef && abortedRef.aborted) {
36
+ return false;
37
+ }
38
+
39
+ const jsonStr = JSON.stringify(data);
40
+ const dataSize = jsonStr.length;
41
+
42
+ // Small response - send directly (fast path)
43
+ if (dataSize < SMALL_RESPONSE_THRESHOLD) {
44
+ try {
45
+ res.cork(() => {
46
+ res.end(jsonStr);
47
+ });
48
+ return true;
49
+ } catch (error) {
50
+ log(`Error sending small response: ${error.message}`);
51
+ return false;
52
+ }
53
+ }
54
+
55
+ // Large response - stream with backpressure handling
56
+ // uWebSockets.js requires all writes to be synchronous within cork callbacks
57
+ log(`Streaming large response: ${(dataSize / 1024 / 1024).toFixed(2)}MB`);
58
+
59
+ let offset = 0;
60
+
61
+ // Start sending - write as much as possible in first cork
62
+ res.cork(() => {
63
+ // Write chunks until buffer is full or done
64
+ while (offset < dataSize) {
65
+ const chunkEnd = Math.min(offset + CHUNK_SIZE, dataSize);
66
+ const chunk = jsonStr.slice(offset, chunkEnd);
67
+ const isLastChunk = chunkEnd >= dataSize;
68
+
69
+ if (isLastChunk) {
70
+ // Last chunk - end the response
71
+ res.end(chunk);
72
+ log(`Streamed response complete: ${(dataSize / 1024).toFixed(2)}KB sent`);
73
+ return; // All done!
74
+ }
75
+
76
+ const ok = res.write(chunk);
77
+ offset = chunkEnd;
78
+
79
+ if (!ok) {
80
+ // Buffer full - stop and wait for drain
81
+ log(`Backpressure detected at offset ${offset}/${dataSize} (${((offset/dataSize)*100).toFixed(1)}%)`);
82
+ break; // Exit while loop, set up onWritable below
83
+ }
84
+
85
+ // Successfully wrote, continue to next chunk
86
+ }
87
+
88
+ // If we exited due to backpressure (offset < dataSize), set up handler
89
+ if (offset < dataSize) {
90
+ // Set up backpressure handler for remaining chunks
91
+ // onWritable is called when the socket buffer has space again
92
+ res.onWritable((writeOffset) => {
93
+ if (abortedRef && abortedRef.aborted) {
94
+ return true; // true = done, stop calling
95
+ }
96
+
97
+ // CRITICAL: All writes inside onWritable must also be corked!
98
+ let isDone = true; // Will be set to false if we need to continue
99
+
100
+ res.cork(() => {
101
+ // Write as many chunks as possible while buffer has space
102
+ while (offset < dataSize) {
103
+ const chunkEnd = Math.min(offset + CHUNK_SIZE, dataSize);
104
+ const chunk = jsonStr.slice(offset, chunkEnd);
105
+ const isLastChunk = chunkEnd >= dataSize;
106
+
107
+ if (isLastChunk) {
108
+ // Last chunk - end the response
109
+ res.end(chunk);
110
+ log(`Streamed response complete: ${(dataSize / 1024).toFixed(2)}KB sent`);
111
+ isDone = true; // true = done, stop calling onWritable
112
+ return;
113
+ }
114
+
115
+ const ok = res.write(chunk);
116
+ offset = chunkEnd;
117
+
118
+ if (!ok) {
119
+ // Buffer full again - stop and wait for next onWritable call
120
+ log(`Backpressure detected at offset ${offset}/${dataSize} (${((offset/dataSize)*100).toFixed(1)}%)`);
121
+ isDone = false; // false = not done, call me again when writable
122
+ return;
123
+ }
124
+
125
+ // Successfully wrote, try next chunk in this iteration
126
+ }
127
+
128
+ // If we get here, all chunks sent
129
+ isDone = true; // Done
130
+ });
131
+
132
+ return isDone;
133
+ });
134
+ }
135
+ });
136
+
137
+ return true;
138
+ };
139
+
140
+ /**
141
+ * Send a simple JSON response (for small data)
142
+ * Always uses res.cork() for efficiency
143
+ */
144
+ export const sendJSON = (res, data, statusCode = 200) => {
145
+ res.cork(() => {
146
+ res.writeStatus(statusCode.toString());
147
+ res.end(JSON.stringify(data));
148
+ });
149
+ };
150
+
151
+ /**
152
+ * Send error response with proper status code
153
+ * Note: CORS headers should be set by caller before calling this
154
+ */
155
+ export const sendError = (res, error, statusCode = 500) => {
156
+ res.cork(() => {
157
+ res.writeStatus(statusCode.toString());
158
+ res.end(JSON.stringify({
159
+ error: error.message || error,
160
+ code: error.code || 'INTERNAL_ERROR'
161
+ }));
162
+ });
163
+ };
164
+
165
+ /**
166
+ * Stream large array in chunks (alternative approach for very large arrays)
167
+ * Serializes chunks separately to avoid large JSON.stringify() blocking
168
+ */
169
+ export const streamJSONArray = (res, items, abortedRef = null) => {
170
+ if (abortedRef && abortedRef.aborted) return false;
171
+
172
+ const ITEMS_PER_CHUNK = 1000;
173
+ let sent = 0;
174
+
175
+ const sendNextChunk = () => {
176
+ if (abortedRef && abortedRef.aborted) return true;
177
+
178
+ const chunk = items.slice(sent, sent + ITEMS_PER_CHUNK);
179
+ if (chunk.length === 0) {
180
+ // All sent
181
+ res.end(']');
182
+ return true;
183
+ }
184
+
185
+ const isFirst = sent === 0;
186
+ const isLast = sent + chunk.length >= items.length;
187
+
188
+ // Build chunk JSON
189
+ let chunkStr;
190
+ if (isFirst) {
191
+ chunkStr = '[' + chunk.map(item => JSON.stringify(item)).join(',');
192
+ } else {
193
+ chunkStr = ',' + chunk.map(item => JSON.stringify(item)).join(',');
194
+ }
195
+
196
+ const ok = isLast
197
+ ? res.end(chunkStr + ']')
198
+ : res.write(chunkStr);
199
+
200
+ sent += chunk.length;
201
+
202
+ if (!ok) {
203
+ // Backpressure
204
+ res.onWritable(() => sendNextChunk());
205
+ return false;
206
+ }
207
+
208
+ if (!isLast) {
209
+ setImmediate(sendNextChunk);
210
+ }
211
+
212
+ return true;
213
+ };
214
+
215
+ res.cork(() => {
216
+ const ok = sendNextChunk();
217
+ if (!ok) {
218
+ res.onWritable(sendNextChunk);
219
+ }
220
+ });
221
+
222
+ return true;
223
+ };
224
+
225
+ export default {
226
+ streamJSON,
227
+ sendJSON,
228
+ sendError,
229
+ streamJSONArray
230
+ };
231
+