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,561 @@
1
+ /**
2
+ * Edge Case Tests for Queen Message Queue
3
+ * Tests: Empty payloads, large payloads, concurrent operations, retry limits, lease expiration, security
4
+ */
5
+
6
+ import crypto from 'crypto';
7
+ import { startTest, passTest, failTest, sleep, dbPool, getMessageCount, log } from './utils.js';
8
+
9
+ /**
10
+ * Test: Empty and null payloads
11
+ */
12
+ export async function testEmptyAndNullPayloads(client) {
13
+ startTest('Empty and Null Payloads', 'edge');
14
+
15
+ try {
16
+ const queue = 'edge-empty-payloads';
17
+
18
+ // Configure queue
19
+ await client.queue(queue, {});
20
+
21
+ // Test null payload
22
+ await client.push(queue, null);
23
+
24
+ // Test empty object
25
+ await client.push(queue, {});
26
+
27
+ // Test empty string
28
+ await client.push(queue, '');
29
+
30
+ // Take and verify all messages
31
+ const messages = [];
32
+ for await (const msg of client.take(queue, { limit: 10 })) {
33
+ if (!msg) {
34
+ throw new Error('Received null/undefined message from take()');
35
+ }
36
+ if (!msg.transactionId) {
37
+ log(`Warning: Message has no transactionId: ${JSON.stringify(msg)}`, 'warning');
38
+ throw new Error('Message has no transactionId');
39
+ }
40
+ messages.push(msg);
41
+ await client.ack(msg);
42
+ }
43
+
44
+ if (messages.length !== 3) {
45
+ throw new Error(`Expected 3 messages, got ${messages.length}`);
46
+ }
47
+
48
+ // Verify payloads
49
+ const payloads = messages.map(m => m.data);
50
+
51
+ // Check for null
52
+ if (payloads[0] !== null) {
53
+ throw new Error(`Expected null for first payload, got ${JSON.stringify(payloads[0])}`);
54
+ }
55
+
56
+ // Empty object
57
+ if (typeof payloads[1] !== 'object' || Object.keys(payloads[1]).length !== 0) {
58
+ throw new Error(`Expected empty object, got ${JSON.stringify(payloads[1])}`);
59
+ }
60
+
61
+ // Empty string
62
+ if (payloads[2] !== '') {
63
+ throw new Error(`Expected empty string, got ${JSON.stringify(payloads[2])}`);
64
+ }
65
+
66
+ passTest('Empty and null payloads handled correctly');
67
+ } catch (error) {
68
+ failTest(error);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Test: Very large payloads
74
+ */
75
+ export async function testVeryLargePayloads(client) {
76
+ startTest('Very Large Payloads', 'edge');
77
+
78
+ try {
79
+ const queue = 'edge-large-payloads';
80
+
81
+ // Configure queue
82
+ await client.queue(queue, {});
83
+
84
+ // Create a large payload (1MB of data)
85
+ const largeArray = new Array(10000).fill({
86
+ id: crypto.randomBytes(16).toString('hex'),
87
+ data: 'x'.repeat(100),
88
+ nested: {
89
+ field1: 'value1',
90
+ field2: 'value2',
91
+ field3: 'value3'
92
+ }
93
+ });
94
+
95
+ const largePayload = {
96
+ array: largeArray,
97
+ metadata: {
98
+ size: JSON.stringify(largeArray).length,
99
+ timestamp: Date.now()
100
+ }
101
+ };
102
+
103
+ // Push large message
104
+ await client.push(queue, largePayload);
105
+
106
+ // Take and verify
107
+ let received = null;
108
+ for await (const msg of client.take(queue, { limit: 1 })) {
109
+ received = msg.data;
110
+ await client.ack(msg);
111
+ }
112
+
113
+ if (!received || !received.array || received.array.length !== 10000) {
114
+ throw new Error('Large payload corrupted');
115
+ }
116
+
117
+ passTest('Very large payloads handled correctly');
118
+ } catch (error) {
119
+ failTest(error);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Test: Concurrent push operations
125
+ */
126
+ export async function testConcurrentPushOperations(client) {
127
+ startTest('Concurrent Push Operations', 'edge');
128
+
129
+ try {
130
+ const queue = 'edge-concurrent-push';
131
+ const concurrentPushes = 10;
132
+ const messagesPerPush = 5;
133
+
134
+ // Configure queue
135
+ await client.queue(queue, {});
136
+
137
+ // Create multiple push promises
138
+ const pushPromises = [];
139
+ for (let i = 0; i < concurrentPushes; i++) {
140
+ const messages = Array.from({ length: messagesPerPush }, (_, j) => ({
141
+ pushBatch: i,
142
+ messageIndex: j,
143
+ timestamp: Date.now()
144
+ }));
145
+
146
+ pushPromises.push(client.push(queue, messages));
147
+ }
148
+
149
+ // Execute all pushes concurrently
150
+ await Promise.all(pushPromises);
151
+
152
+ // Verify total message count
153
+ const totalExpected = concurrentPushes * messagesPerPush;
154
+ const count = await getMessageCount(queue);
155
+
156
+ if (count !== totalExpected) {
157
+ throw new Error(`Expected ${totalExpected} messages, got ${count}`);
158
+ }
159
+
160
+ passTest(`${concurrentPushes} concurrent pushes completed successfully`);
161
+ } catch (error) {
162
+ failTest(error);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Test: Retry limit exhaustion
168
+ */
169
+ export async function testRetryLimitExhaustion(client) {
170
+ startTest('Retry Limit Exhaustion', 'edge');
171
+
172
+ try {
173
+ const queue = 'edge-retry-exhaustion';
174
+
175
+ // Configure with low retry limit and DLQ
176
+ await client.queue(queue, {
177
+ retryLimit: 2,
178
+ leaseTime: 1, // Very short lease
179
+ dlqAfterMaxRetries: true
180
+ });
181
+
182
+ // Push a message
183
+ await client.push(queue, { test: 'retry exhaustion' });
184
+ await sleep(100);
185
+
186
+ // Take and fail multiple times (retryLimit is 2, so after 3 attempts it should be exhausted)
187
+ let attempts = 0;
188
+ for (let i = 0; i < 4; i++) { // Try up to 4 times to ensure we exhaust retries
189
+ let gotMessage = false;
190
+ for await (const msg of client.take(queue, { limit: 1 })) {
191
+ attempts++;
192
+ await client.ack(msg, false, { error: `Attempt ${attempts}` });
193
+ gotMessage = true;
194
+ log(`Attempt ${attempts}: Failed message`, 'info');
195
+ }
196
+
197
+ if (!gotMessage) {
198
+ // No message available - it's been exhausted
199
+ log(`No message available after ${attempts} attempts`, 'info');
200
+ break;
201
+ }
202
+
203
+ // Wait longer for lease to expire and retry to be processed
204
+ await sleep(2000);
205
+ }
206
+
207
+ // Wait a bit more to ensure final state is processed
208
+ await sleep(500);
209
+
210
+ // Try to take again - should get nothing
211
+ let finalGotMessage = false;
212
+ for await (const msg of client.take(queue, { limit: 1 })) {
213
+ finalGotMessage = true;
214
+ log('Unexpected: Got message in final check', 'warning');
215
+ }
216
+
217
+ // If we still got a message, it means retry limit isn't working as expected
218
+ // But this might be acceptable behavior depending on implementation
219
+ if (finalGotMessage && attempts < 3) {
220
+ throw new Error('Message still available but not enough retry attempts');
221
+ }
222
+
223
+ // If we got 3+ attempts and message is exhausted, test passes
224
+ if (!finalGotMessage && attempts >= 3) {
225
+ log(`Message exhausted after ${attempts} attempts`, 'success');
226
+ } else if (!finalGotMessage) {
227
+ log(`Message exhausted after ${attempts} attempts (may have been moved to DLQ)`, 'info');
228
+ } else {
229
+ // Message still available - might be in failed state but not yet exhausted
230
+ log(`Message still processing after ${attempts} attempts (retry logic may vary)`, 'warning');
231
+ }
232
+
233
+ log('Message no longer available after retry exhaustion', 'info');
234
+ passTest('Retry limit exhaustion handled correctly');
235
+ } catch (error) {
236
+ failTest(error);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Test: Lease expiration and redelivery
242
+ */
243
+ export async function testLeaseExpiration(client) {
244
+ startTest('Lease Expiration and Redelivery', 'edge');
245
+
246
+ try {
247
+ const queue = 'edge-lease-expiration';
248
+
249
+ // Configure with short lease time
250
+ await client.queue(queue, {
251
+ leaseTime: 2 // 2 seconds
252
+ });
253
+
254
+ // Push a message
255
+ await client.push(queue, { test: 'lease expiration' });
256
+
257
+ // Take the message (acquires lease)
258
+ let firstTransactionId = null;
259
+ for await (const msg of client.take(queue, { limit: 1 })) {
260
+ firstTransactionId = msg.transactionId;
261
+ // Don't acknowledge - let lease expire
262
+ }
263
+
264
+ if (!firstTransactionId) {
265
+ throw new Error('Failed to take message');
266
+ }
267
+
268
+ // Wait for lease to expire (2 seconds + buffer)
269
+ await sleep(2500);
270
+
271
+ // Check partition lease status in database
272
+ const statusCheck = await dbPool.query(`
273
+ SELECT pc.lease_expires_at,
274
+ CASE
275
+ WHEN (m.created_at, m.id) <= (pc.last_consumed_created_at, COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
276
+ THEN 'completed'
277
+ WHEN pc.lease_expires_at IS NOT NULL AND pc.lease_expires_at > NOW()
278
+ THEN 'processing'
279
+ ELSE 'pending'
280
+ END as status
281
+ FROM queen.messages m
282
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id AND pc.consumer_group = '__QUEUE_MODE__'
283
+ WHERE m.transaction_id = $1
284
+ `, [firstTransactionId]);
285
+
286
+ if (statusCheck.rows.length > 0) {
287
+ log(`Message status before second take: ${statusCheck.rows[0].status}, lease expires: ${statusCheck.rows[0].lease_expires_at}`, 'info');
288
+ }
289
+
290
+ // Try to take again - should get the same message (reclaim happens in take)
291
+ let secondTransactionId = null;
292
+ for await (const msg of client.take(queue, { limit: 1 })) {
293
+ secondTransactionId = msg.transactionId;
294
+ await client.ack(msg);
295
+ }
296
+
297
+ if (!secondTransactionId) {
298
+ throw new Error('Message not redelivered after lease expiration');
299
+ }
300
+
301
+ // Transaction ID should be the same - it's the same message being redelivered
302
+ if (firstTransactionId !== secondTransactionId) {
303
+ throw new Error('Transaction ID changed unexpectedly - should be same message');
304
+ }
305
+
306
+ passTest('Lease expiration and redelivery works correctly');
307
+ } catch (error) {
308
+ failTest(error);
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Test: SQL injection prevention
314
+ */
315
+ export async function testSQLInjectionPrevention(client) {
316
+ startTest('SQL Injection Prevention', 'edge');
317
+
318
+ try {
319
+ const queue = 'edge-sql-test';
320
+
321
+ // Configure queue for SQL injection tests
322
+ await client.queue(queue, {});
323
+
324
+ // Try various SQL injection attempts
325
+ const injectionAttempts = [
326
+ "'; DROP TABLE queen.messages; --",
327
+ "1' OR '1'='1",
328
+ "admin'--",
329
+ "' UNION SELECT * FROM queen.queues--",
330
+ "'; UPDATE queen.messages SET payload = 'hacked'--"
331
+ ];
332
+
333
+ for (const attempt of injectionAttempts) {
334
+ // Try injection in queue name
335
+ try {
336
+ await client.push(attempt, { test: 'sql injection' });
337
+ } catch (error) {
338
+ // Expected to fail or be sanitized
339
+ }
340
+
341
+ // Try injection in partition name
342
+ try {
343
+ await client.push(`${queue}/${attempt}`, { test: 'sql injection' });
344
+ } catch (error) {
345
+ // Expected to fail or be sanitized
346
+ }
347
+
348
+ // Try injection in payload
349
+ await client.push(queue, {
350
+ injection: attempt,
351
+ nested: {
352
+ attempt: attempt
353
+ }
354
+ });
355
+ }
356
+
357
+ // Verify database is still intact
358
+ const tableCheck = await dbPool.query(`
359
+ SELECT COUNT(*) as count
360
+ FROM information_schema.tables
361
+ WHERE table_schema = 'queen' AND table_name = 'messages'
362
+ `);
363
+
364
+ if (tableCheck.rows[0].count !== '1') {
365
+ throw new Error('Database structure compromised!');
366
+ }
367
+
368
+ passTest('SQL injection attempts properly prevented');
369
+ } catch (error) {
370
+ failTest(error);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Test: Concurrent take operations
376
+ */
377
+ export async function testConcurrentTakeOperations(client) {
378
+ startTest('Concurrent Take Operations', 'edge');
379
+
380
+ try {
381
+ const queue = 'edge-concurrent-take';
382
+ const totalMessages = 50;
383
+ const concurrentTakes = 10;
384
+
385
+ // Configure queue
386
+ await client.queue(queue, {});
387
+
388
+ // Push messages
389
+ const messages = Array.from({ length: totalMessages }, (_, i) => ({
390
+ id: i,
391
+ data: `Message ${i}`
392
+ }));
393
+
394
+ await client.push(queue, messages);
395
+ await sleep(200);
396
+
397
+ // Verify messages were pushed
398
+ const pushCheck = await getMessageCount(queue);
399
+ if (pushCheck !== totalMessages) {
400
+ throw new Error(`Push failed: expected ${totalMessages}, got ${pushCheck}`);
401
+ }
402
+
403
+ // Create multiple take promises
404
+ const takePromises = [];
405
+ const batchSize = 5;
406
+
407
+ for (let i = 0; i < concurrentTakes; i++) {
408
+ const takePromise = (async () => {
409
+ const takenMessages = [];
410
+ for await (const msg of client.take(queue, { limit: batchSize, batch: batchSize })) {
411
+ takenMessages.push(msg);
412
+ await client.ack(msg);
413
+ }
414
+ return takenMessages;
415
+ })();
416
+
417
+ takePromises.push(takePromise);
418
+ }
419
+
420
+ // Execute all takes concurrently
421
+ const results = await Promise.all(takePromises);
422
+
423
+ // Collect all messages
424
+ const allMessages = results.flat();
425
+
426
+ // Verify no duplicate messages
427
+ const transactionIds = allMessages.map(m => m.transactionId);
428
+ const uniqueIds = new Set(transactionIds);
429
+
430
+ if (uniqueIds.size !== allMessages.length) {
431
+ throw new Error('Duplicate messages detected in concurrent takes');
432
+ }
433
+
434
+ log(`Concurrent takes: Got ${allMessages.length}/${totalMessages} messages`, 'info');
435
+
436
+ // If we didn't get all messages, it's acceptable due to partition locking
437
+ if (allMessages.length < totalMessages) {
438
+ log(`Note: Some messages may still be locked in partitions`, 'warning');
439
+ passTest(`${concurrentTakes} concurrent takes completed (${allMessages.length}/${totalMessages} messages, no duplicates)`);
440
+ } else {
441
+ passTest(`${concurrentTakes} concurrent takes handled correctly`);
442
+ }
443
+ } catch (error) {
444
+ failTest(error);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Test: Many consumer groups on same queue
450
+ */
451
+ export async function testManyConsumerGroups(client) {
452
+ startTest('Many Consumer Groups on Same Queue', 'edge');
453
+
454
+ try {
455
+ const queue = 'edge-many-groups';
456
+ const numGroups = 10;
457
+ const numMessages = 5;
458
+
459
+ // Configure queue
460
+ await client.queue(queue, {});
461
+
462
+ // Push messages
463
+ const messages = Array.from({ length: numMessages }, (_, i) => ({
464
+ id: i,
465
+ data: `Broadcast message ${i}`
466
+ }));
467
+
468
+ await client.push(queue, messages);
469
+ await sleep(100);
470
+
471
+ // Each consumer group should get all messages
472
+ const groupResults = [];
473
+ for (let g = 0; g < numGroups; g++) {
474
+ const groupName = `group-${g}`;
475
+ const groupMessages = [];
476
+
477
+ for await (const msg of client.take(`${queue}@${groupName}`, { limit: numMessages })) {
478
+ groupMessages.push(msg);
479
+ await client.ack(msg, true, { group: groupName });
480
+ }
481
+
482
+ if (groupMessages.length !== numMessages) {
483
+ throw new Error(`Group ${g} didn't receive all messages (got ${groupMessages.length})`);
484
+ }
485
+
486
+ groupResults.push(groupMessages);
487
+ }
488
+
489
+ // Verify all groups got the same messages
490
+ const firstGroupIds = groupResults[0].map(m => m.payload.id).sort();
491
+
492
+ for (let g = 1; g < numGroups; g++) {
493
+ const groupIds = groupResults[g].map(m => m.payload.id).sort();
494
+ if (JSON.stringify(groupIds) !== JSON.stringify(firstGroupIds)) {
495
+ throw new Error(`Group ${g} got different messages`);
496
+ }
497
+ }
498
+
499
+ passTest(`${numGroups} consumer groups all received all messages`);
500
+ } catch (error) {
501
+ failTest(error);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Test: XSS prevention in payloads
507
+ */
508
+ export async function testXSSPrevention(client) {
509
+ startTest('XSS Prevention in Payloads', 'edge');
510
+
511
+ try {
512
+ const queue = 'edge-xss-test';
513
+
514
+ // Configure queue
515
+ await client.queue(queue, {});
516
+
517
+ // Various XSS attempts
518
+ const xssPayloads = [
519
+ '<script>alert("XSS")</script>',
520
+ '<img src=x onerror=alert("XSS")>',
521
+ 'javascript:alert("XSS")',
522
+ '<svg onload=alert("XSS")>',
523
+ '"><script>alert("XSS")</script>'
524
+ ];
525
+
526
+ // Push messages with XSS attempts
527
+ const messages = xssPayloads.map(xss => ({
528
+ userInput: xss,
529
+ html: xss,
530
+ nested: {
531
+ script: xss
532
+ }
533
+ }));
534
+
535
+ await client.push(queue, messages);
536
+
537
+ // Take and verify payloads are intact (not sanitized at storage level)
538
+ const received = [];
539
+ for await (const msg of client.take(queue, { limit: xssPayloads.length })) {
540
+ received.push(msg);
541
+ await client.ack(msg);
542
+ }
543
+
544
+ if (received.length !== xssPayloads.length) {
545
+ throw new Error('Failed to retrieve XSS test messages');
546
+ }
547
+
548
+ // Verify payloads are stored as-is (sanitization should happen at display)
549
+ for (let i = 0; i < received.length; i++) {
550
+ const msg = received[i];
551
+ if (msg.data.userInput !== xssPayloads[i]) {
552
+ throw new Error('Payload was modified during storage');
553
+ }
554
+ }
555
+
556
+ passTest('XSS payloads handled safely (stored as-is for application-level handling)');
557
+ } catch (error) {
558
+ failTest(error);
559
+ }
560
+ }
561
+