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,361 @@
1
+ /**
2
+ * Bus Mode Tests for Queen Message Queue
3
+ * Tests: Consumer groups, mixed mode, subscription modes, consumer group isolation
4
+ */
5
+
6
+ import { startTest, passTest, failTest, sleep, dbPool, log } from './utils.js';
7
+
8
+ /**
9
+ * Test: Bus Mode - Consumer Groups
10
+ */
11
+ export async function testBusConsumerGroups(client) {
12
+ startTest('Bus Mode - Consumer Groups', 'enterprise');
13
+
14
+ try {
15
+ const queue = 'test-bus-mode';
16
+
17
+ // Configure queue
18
+ await client.queue(queue, {});
19
+
20
+ // Push test messages
21
+ const messages = Array.from({ length: 5 }, (_, i) => ({
22
+ id: i + 1,
23
+ data: `Message ${i + 1}`
24
+ }));
25
+
26
+ await client.push(queue, messages);
27
+ await sleep(100);
28
+
29
+ // Consumer Group 1: Take messages
30
+ const group1Messages = [];
31
+ for await (const msg of client.take(`${queue}@group1`, { limit: 3, batch: 3 })) {
32
+ group1Messages.push(msg);
33
+ }
34
+
35
+ if (group1Messages.length !== 3) {
36
+ throw new Error(`Group1 expected 3 messages, got ${group1Messages.length}`);
37
+ }
38
+
39
+ // Consumer Group 2: Should get the same messages
40
+ const group2Messages = [];
41
+ for await (const msg of client.take(`${queue}@group2`, { limit: 3, batch: 3 })) {
42
+ group2Messages.push(msg);
43
+ }
44
+
45
+ if (group2Messages.length !== 3) {
46
+ throw new Error(`Group2 expected 3 messages, got ${group2Messages.length}`);
47
+ }
48
+
49
+ // Verify both groups got the same message IDs
50
+ const group1Ids = group1Messages.map(m => m.payload.id).sort();
51
+ const group2Ids = group2Messages.map(m => m.payload.id).sort();
52
+
53
+ if (JSON.stringify(group1Ids) !== JSON.stringify(group2Ids)) {
54
+ throw new Error('Consumer groups did not receive the same messages');
55
+ }
56
+
57
+ // Acknowledge messages for both groups
58
+ for (const msg of group1Messages) {
59
+ await client.ack(msg, true, { group: 'group1' });
60
+ }
61
+
62
+ for (const msg of group2Messages) {
63
+ await client.ack(msg, true, { group: 'group2' });
64
+ }
65
+
66
+ // Take remaining messages
67
+ const group1Remaining = [];
68
+ for await (const msg of client.take(`${queue}@group1`, { limit: 5 })) {
69
+ group1Remaining.push(msg);
70
+ await client.ack(msg, true, { group: 'group1' });
71
+ }
72
+
73
+ if (group1Remaining.length !== 2) {
74
+ throw new Error(`Expected 2 remaining messages for group1, got ${group1Remaining.length}`);
75
+ }
76
+
77
+ passTest('Consumer groups receive all messages independently');
78
+ } catch (error) {
79
+ failTest(error);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Test: Mixed Mode - Queue and Bus Together
85
+ */
86
+ export async function testMixedMode(client) {
87
+ startTest('Mixed Mode - Queue and Bus Together', 'enterprise');
88
+
89
+ try {
90
+ const queue = 'test-mixed-mode';
91
+
92
+ // Configure queue
93
+ await client.queue(queue, {});
94
+
95
+ // Push test messages
96
+ const messages = Array.from({ length: 6 }, (_, i) => ({
97
+ id: i + 1,
98
+ data: `Mixed message ${i + 1}`
99
+ }));
100
+
101
+ await client.push(queue, messages);
102
+ await sleep(100);
103
+
104
+ // Queue Mode: Workers compete for messages
105
+ const worker1Messages = [];
106
+ for await (const msg of client.take(queue, { limit: 2, batch: 2 })) {
107
+ worker1Messages.push(msg);
108
+ await client.ack(msg);
109
+ }
110
+
111
+ const worker2Messages = [];
112
+ for await (const msg of client.take(queue, { limit: 2, batch: 2 })) {
113
+ worker2Messages.push(msg);
114
+ await client.ack(msg);
115
+ }
116
+
117
+ if (!worker1Messages.length || !worker2Messages.length) {
118
+ throw new Error('Workers did not receive messages');
119
+ }
120
+
121
+ // Workers should get different messages (competing)
122
+ const worker1Ids = worker1Messages.map(m => m.payload.id);
123
+ const worker2Ids = worker2Messages.map(m => m.payload.id);
124
+ const intersection = worker1Ids.filter(id => worker2Ids.includes(id));
125
+
126
+ if (intersection.length > 0) {
127
+ throw new Error('Workers received the same messages - should be competing');
128
+ }
129
+
130
+ // Bus Mode: Consumer groups get all messages
131
+ const analyticsMessages = [];
132
+ for await (const msg of client.take(`${queue}@analytics`, { limit: 10 })) {
133
+ analyticsMessages.push(msg);
134
+ await client.ack(msg, true, { group: 'analytics' });
135
+ }
136
+
137
+ const auditMessages = [];
138
+ for await (const msg of client.take(`${queue}@audit`, { limit: 10 })) {
139
+ auditMessages.push(msg);
140
+ await client.ack(msg, true, { group: 'audit' });
141
+ }
142
+
143
+ // Both groups should get all 6 messages
144
+ if (analyticsMessages.length !== 6) {
145
+ throw new Error(`Analytics group expected 6 messages, got ${analyticsMessages.length}`);
146
+ }
147
+
148
+ if (auditMessages.length !== 6) {
149
+ throw new Error(`Audit group expected 6 messages, got ${auditMessages.length}`);
150
+ }
151
+
152
+ // Verify groups got all messages
153
+ const analyticsIds = analyticsMessages.map(m => m.payload.id).sort();
154
+ const auditIds = auditMessages.map(m => m.payload.id).sort();
155
+
156
+ if (JSON.stringify(analyticsIds) !== JSON.stringify([1, 2, 3, 4, 5, 6])) {
157
+ throw new Error('Analytics group did not receive all messages');
158
+ }
159
+
160
+ if (JSON.stringify(auditIds) !== JSON.stringify([1, 2, 3, 4, 5, 6])) {
161
+ throw new Error('Audit group did not receive all messages');
162
+ }
163
+
164
+ passTest('Mixed mode works: workers compete, groups get all messages');
165
+ } catch (error) {
166
+ failTest(error);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Test: Consumer Group Subscription Modes
172
+ */
173
+ export async function testConsumerGroupSubscriptionModes(client) {
174
+ startTest('Consumer Group Subscription Modes', 'enterprise');
175
+
176
+ try {
177
+ const queue = 'test-subscription-modes';
178
+
179
+ // Configure queue
180
+ await client.queue(queue, {});
181
+
182
+ // Push historical messages
183
+ const historicalMessages = Array.from({ length: 3 }, (_, i) => ({
184
+ id: i + 1,
185
+ type: 'historical',
186
+ data: `Historical ${i + 1}`
187
+ }));
188
+
189
+ await client.push(queue, historicalMessages);
190
+ await sleep(100);
191
+
192
+ // Group 1: Default mode (consume all)
193
+ const allMessages = [];
194
+ for await (const msg of client.take(`${queue}@all-messages`, { limit: 10 })) {
195
+ allMessages.push(msg);
196
+ // ACK immediately to release partition lock
197
+ await client.ack(msg, true, { group: 'all-messages' });
198
+ }
199
+
200
+ if (allMessages.length !== 3) {
201
+ throw new Error(`All-messages group expected 3 messages, got ${allMessages.length}`);
202
+ }
203
+
204
+ // Group 2: New messages only (subscription mode = 'new')
205
+ const newOnlyMessages = [];
206
+ for await (const msg of client.take(`${queue}@new-only`, {
207
+ limit: 10,
208
+ subscriptionMode: 'new'
209
+ })) {
210
+ newOnlyMessages.push(msg);
211
+ }
212
+
213
+ if (newOnlyMessages.length !== 0) {
214
+ throw new Error(`New-only group should get 0 historical messages, got ${newOnlyMessages.length}`);
215
+ }
216
+
217
+ // Wait to ensure subscription timestamp is in the past
218
+ await sleep(1000);
219
+
220
+ // Push new messages
221
+ const newMessages = Array.from({ length: 3 }, (_, i) => ({
222
+ id: i + 4,
223
+ type: 'new',
224
+ data: `New ${i + 4}`
225
+ }));
226
+
227
+ await client.push(queue, newMessages);
228
+ await sleep(100);
229
+
230
+ // All-messages group should get the new messages
231
+ const allMessagesNew = [];
232
+ for await (const msg of client.take(`${queue}@all-messages`, { limit: 10 })) {
233
+ allMessagesNew.push(msg);
234
+ // ACK immediately
235
+ await client.ack(msg, true, { group: 'all-messages' });
236
+ }
237
+
238
+ if (allMessagesNew.length !== 3) {
239
+ throw new Error(`All-messages group expected 3 new messages, got ${allMessagesNew.length}`);
240
+ }
241
+
242
+ // New-only group should also get the new messages
243
+ const newOnlyNew = [];
244
+ for await (const msg of client.take(`${queue}@new-only`, { limit: 10 })) {
245
+ newOnlyNew.push(msg);
246
+ await client.ack(msg, true, { group: 'new-only' });
247
+ }
248
+
249
+ if (newOnlyNew.length !== 3) {
250
+ throw new Error(`New-only group expected 3 new messages, got ${newOnlyNew.length}`);
251
+ }
252
+
253
+ passTest('Consumer group subscription modes work correctly');
254
+ } catch (error) {
255
+ failTest(error);
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Test: Consumer Group Isolation
261
+ */
262
+ export async function testConsumerGroupIsolation(client) {
263
+ startTest('Consumer Group Isolation', 'enterprise');
264
+
265
+ try {
266
+ const queue = 'test-group-isolation';
267
+
268
+ // Configure queue
269
+ await client.queue(queue, {});
270
+
271
+ // Push messages
272
+ const messages = Array.from({ length: 4 }, (_, i) => ({
273
+ id: i + 1,
274
+ data: `Isolation test ${i + 1}`
275
+ }));
276
+
277
+ await client.push(queue, messages);
278
+ await sleep(100);
279
+
280
+ // Group 1: Take some messages first
281
+ const group1Messages = [];
282
+ for await (const msg of client.take(`${queue}@group1`, { limit: 2, batch: 2 })) {
283
+ group1Messages.push(msg);
284
+ await client.ack(msg, true, { group: 'group1' });
285
+ }
286
+
287
+ if (group1Messages.length !== 2) {
288
+ throw new Error(`Group1 expected 2 messages, got ${group1Messages.length}`);
289
+ }
290
+
291
+ // Group 2: Should get ALL 4 messages (different consumer group, independent)
292
+ // Consumer groups don't race - they each get all messages
293
+ const group2Messages = [];
294
+ for await (const msg of client.take(`${queue}@group2`, { limit: 10, batch: 10 })) {
295
+ group2Messages.push(msg);
296
+ }
297
+
298
+ if (group2Messages.length !== 4) {
299
+ log(`Group2 got ${group2Messages.length} messages, expected 4 - consumer group isolation may not be working`, 'warning');
300
+ throw new Error(`Group2 expected all 4 messages, got ${group2Messages.length}`);
301
+ }
302
+
303
+ // Group 1: Take remaining messages
304
+ const group1Remaining = [];
305
+ for await (const msg of client.take(`${queue}@group1`, { limit: 10, batch: 10 })) {
306
+ group1Remaining.push(msg);
307
+ await client.ack(msg, true, { group: 'group1' });
308
+ }
309
+
310
+ if (group1Remaining.length !== 2) {
311
+ throw new Error(`Group1 expected 2 remaining messages, got ${group1Remaining.length}`);
312
+ }
313
+
314
+ // Verify isolation: Fail first 2 messages for group2, complete the rest
315
+ for (let i = 0; i < group2Messages.length; i++) {
316
+ if (i < 2) {
317
+ await client.ack(group2Messages[i], false, { error: 'Test failure', group: 'group2' });
318
+ } else {
319
+ await client.ack(group2Messages[i], true, { group: 'group2' });
320
+ }
321
+ }
322
+
323
+ // Verify isolation: Check that group1's first message is completed (cursor advanced) and group2 failed (in DLQ)
324
+ const statusCheck = await dbPool.query(`
325
+ SELECT
326
+ m.id as message_id,
327
+ m.transaction_id,
328
+ CASE
329
+ WHEN dlq1.id IS NOT NULL THEN 'failed'
330
+ WHEN m.id <= COALESCE(pc1.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid) THEN 'completed'
331
+ ELSE 'pending'
332
+ END as group1_status,
333
+ CASE
334
+ WHEN dlq2.id IS NOT NULL THEN 'failed'
335
+ WHEN m.id <= COALESCE(pc2.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid) THEN 'completed'
336
+ ELSE 'pending'
337
+ END as group2_status
338
+ FROM queen.messages m
339
+ LEFT JOIN queen.partition_consumers pc1 ON pc1.partition_id = m.partition_id AND pc1.consumer_group = 'group1'
340
+ LEFT JOIN queen.partition_consumers pc2 ON pc2.partition_id = m.partition_id AND pc2.consumer_group = 'group2'
341
+ LEFT JOIN queen.dead_letter_queue dlq1 ON dlq1.message_id = m.id AND dlq1.consumer_group = 'group1'
342
+ LEFT JOIN queen.dead_letter_queue dlq2 ON dlq2.message_id = m.id AND dlq2.consumer_group = 'group2'
343
+ WHERE m.transaction_id = $1
344
+ `, [group1Messages[0].transactionId]);
345
+
346
+ const result = statusCheck.rows[0];
347
+
348
+ if (result?.group1_status !== 'completed') {
349
+ throw new Error(`Group1 status should be completed, got ${result?.group1_status}`);
350
+ }
351
+
352
+ if (result?.group2_status !== 'failed') {
353
+ throw new Error(`Group2 status should be failed, got ${result?.group2_status}`);
354
+ }
355
+
356
+ passTest('Consumer groups are properly isolated');
357
+ } catch (error) {
358
+ failTest(error);
359
+ }
360
+ }
361
+
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Core Feature Tests for Queen Message Queue
3
+ * Tests: Queue configuration, push, take, acknowledgment, delayed processing, FIFO ordering
4
+ */
5
+
6
+ import { startTest, passTest, failTest, sleep, dbPool, getMessageCount, log } from './utils.js';
7
+
8
+ /**
9
+ * Test 1: Queue Creation Policy
10
+ * Verifies that queues must be created via queue() method
11
+ */
12
+ export async function testQueueCreationPolicy(client) {
13
+ startTest('Queue Creation Policy');
14
+
15
+ try {
16
+ // Test 1: Push to non-existent queue should fail
17
+ const nonExistentQueue = 'queue-that-does-not-exist-' + Date.now();
18
+ try {
19
+ await client.push(nonExistentQueue, { test: 'should fail' });
20
+ throw new Error('Push to non-existent queue should have failed');
21
+ } catch (error) {
22
+ if (!error.message.includes('does not exist')) {
23
+ throw error;
24
+ }
25
+ log('Push to non-existent queue correctly failed', 'success');
26
+ }
27
+
28
+ // Test 2: Create queue via queue() method
29
+ const testQueue = 'test-creation-policy-' + Date.now();
30
+ await client.queue(testQueue, {
31
+ retryLimit: 3,
32
+ priority: 5
33
+ }, {
34
+ namespace: 'test',
35
+ task: 'creation-policy'
36
+ });
37
+ log('Queue created via queue() method', 'success');
38
+
39
+ // Test 3: Push to configured queue should succeed
40
+ await client.push(testQueue, { test: 'message 1' });
41
+ log('Push to configured queue succeeded', 'success');
42
+
43
+ // Test 4: Push to new partition (should create on-demand)
44
+ await client.push(`${testQueue}/NewPartition`, { test: 'message 2' });
45
+ log('Partition created on-demand', 'success');
46
+
47
+ passTest('Queue creation policy working correctly');
48
+ } catch (error) {
49
+ failTest(error);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Test 2: Single Message Push
55
+ */
56
+ export async function testSingleMessagePush(client) {
57
+ startTest('Single Message Push');
58
+
59
+ try {
60
+ const queue = 'test-single-push';
61
+
62
+ // Configure queue first
63
+ await client.queue(queue, {});
64
+
65
+ // Push single message
66
+ await client.push(queue, {
67
+ message: 'Single test message',
68
+ timestamp: Date.now()
69
+ });
70
+
71
+ // Verify message was stored
72
+ const count = await getMessageCount(queue);
73
+ if (count !== 1) {
74
+ throw new Error(`Expected 1 message in database, got ${count}`);
75
+ }
76
+
77
+ // Take and verify the message
78
+ let messageReceived = false;
79
+ for await (const msg of client.take(queue, { limit: 1 })) {
80
+ if (msg.data && msg.data.message === 'Single test message') {
81
+ messageReceived = true;
82
+ await client.ack(msg);
83
+ }
84
+ }
85
+
86
+ if (!messageReceived) {
87
+ throw new Error('Failed to receive pushed message');
88
+ }
89
+
90
+ passTest('Single message pushed successfully');
91
+ } catch (error) {
92
+ failTest(error);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Test 3: Batch Message Push
98
+ */
99
+ export async function testBatchMessagePush(client) {
100
+ startTest('Batch Message Push');
101
+
102
+ try {
103
+ const queue = 'test-batch-push';
104
+
105
+ // Configure queue first
106
+ await client.queue(queue, {});
107
+
108
+ const batchSize = 5;
109
+ const messages = Array.from({ length: batchSize }, (_, i) => ({
110
+ message: `Batch message ${i + 1}`,
111
+ index: i + 1
112
+ }));
113
+
114
+ // Push batch
115
+ await client.push(`${queue}/batch-partition`, messages);
116
+
117
+ // Verify messages were stored
118
+ const count = await getMessageCount(queue, 'batch-partition');
119
+ if (count !== batchSize) {
120
+ throw new Error(`Expected ${batchSize} messages in database, got ${count}`);
121
+ }
122
+
123
+ // Take all messages
124
+ const received = [];
125
+ for await (const msg of client.take(`${queue}/batch-partition`, { limit: batchSize })) {
126
+ received.push(msg);
127
+ await client.ack(msg);
128
+ }
129
+
130
+ if (received.length !== batchSize) {
131
+ throw new Error(`Expected ${batchSize} messages, got ${received.length}`);
132
+ }
133
+
134
+ passTest(`Batch of ${batchSize} messages pushed successfully`);
135
+ } catch (error) {
136
+ failTest(error);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Test 4: Queue Configuration
142
+ */
143
+ export async function testQueueConfiguration(client) {
144
+ startTest('Queue Configuration');
145
+
146
+ try {
147
+ const queue = 'test-config-queue';
148
+
149
+ await client.queue(queue, {
150
+ leaseTime: 600,
151
+ retryLimit: 5,
152
+ priority: 8,
153
+ delayedProcessing: 2,
154
+ windowBuffer: 3
155
+ });
156
+
157
+ // Verify configuration was applied by checking database
158
+ const result = await dbPool.query(`
159
+ SELECT lease_time, retry_limit, priority, delayed_processing, window_buffer
160
+ FROM queen.queues
161
+ WHERE name = $1
162
+ `, [queue]);
163
+
164
+ if (result.rows.length === 0) {
165
+ throw new Error('Configured queue not found in database');
166
+ }
167
+
168
+ const row = result.rows[0];
169
+ if (row.lease_time !== 600 || row.retry_limit !== 5) {
170
+ throw new Error('Configuration options not saved correctly');
171
+ }
172
+
173
+ passTest('Queue configuration applied successfully');
174
+ } catch (error) {
175
+ failTest(error);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Test 5: Take and Acknowledgment
181
+ */
182
+ export async function testTakeAndAcknowledgment(client) {
183
+ startTest('Take and Acknowledgment');
184
+
185
+ try {
186
+ const queue = 'test-take-ack';
187
+
188
+ // Configure queue first
189
+ await client.queue(queue, {});
190
+
191
+ // Push test messages
192
+ await client.push(`${queue}/ack-partition`, [
193
+ { message: 'Message to complete' },
194
+ { message: 'Message to fail' }
195
+ ]);
196
+
197
+ await sleep(100);
198
+
199
+ // Take messages
200
+ const messages = [];
201
+ for await (const msg of client.take(`${queue}/ack-partition`, { limit: 2, batch: 2 })) {
202
+ messages.push(msg);
203
+ }
204
+
205
+ if (messages.length !== 2) {
206
+ throw new Error('Expected 2 messages from take');
207
+ }
208
+
209
+ // Acknowledge first as completed
210
+ await client.ack(messages[0], true);
211
+
212
+ // Acknowledge second as failed
213
+ await client.ack(messages[1], false, {
214
+ error: 'Test failure'
215
+ });
216
+
217
+ passTest('Take and acknowledgment work correctly');
218
+ } catch (error) {
219
+ failTest(error);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Test 6: Delayed Processing
225
+ */
226
+ export async function testDelayedProcessing(client) {
227
+ startTest('Delayed Processing');
228
+
229
+ try {
230
+ const queue = 'test-delayed-queue';
231
+
232
+ // Configure queue with delayed processing
233
+ await client.queue(queue, {
234
+ delayedProcessing: 2 // 2 seconds delay
235
+ });
236
+
237
+ const startTime = Date.now();
238
+
239
+ // Push message
240
+ await client.push(queue, {
241
+ message: 'Delayed message',
242
+ sentAt: startTime
243
+ });
244
+
245
+ // Try to take immediately (should get nothing)
246
+ let gotImmediate = false;
247
+ for await (const msg of client.take(queue, { limit: 1 })) {
248
+ gotImmediate = true;
249
+ }
250
+
251
+ if (gotImmediate) {
252
+ throw new Error('Got message immediately when it should be delayed');
253
+ }
254
+
255
+ // Wait for delay period plus buffer
256
+ await sleep(2500);
257
+
258
+ // Try to take again (should get the message now)
259
+ let gotDelayed = false;
260
+ let processingDelay = 0;
261
+
262
+ for await (const msg of client.take(queue, { limit: 1 })) {
263
+ gotDelayed = true;
264
+ processingDelay = Date.now() - startTime;
265
+ await client.ack(msg);
266
+ }
267
+
268
+ if (!gotDelayed) {
269
+ throw new Error('Did not get delayed message after delay period');
270
+ }
271
+
272
+ if (processingDelay < 2000) {
273
+ throw new Error(`Message processed too early: ${processingDelay}ms`);
274
+ }
275
+
276
+ passTest(`Delayed processing works correctly (${processingDelay}ms delay)`);
277
+ } catch (error) {
278
+ failTest(error);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Test 7: FIFO Ordering Within Partitions
284
+ */
285
+ export async function testPartitionFIFOOrdering(client) {
286
+ startTest('FIFO Ordering Within Partitions');
287
+
288
+ try {
289
+ const queue = 'test-partition-fifo';
290
+
291
+ // Configure queue
292
+ await client.queue(queue, {
293
+ priority: 0
294
+ });
295
+
296
+ const partitions = [
297
+ { name: 'ultra-high', priority: 100 },
298
+ { name: 'high', priority: 10 },
299
+ { name: 'medium', priority: 5 },
300
+ { name: 'low', priority: 1 }
301
+ ];
302
+
303
+ // Create partitions manually in DB
304
+ for (const partition of partitions) {
305
+ await dbPool.query(`
306
+ INSERT INTO queen.partitions (queue_id, name)
307
+ SELECT q.id, $1
308
+ FROM queen.queues q
309
+ WHERE q.name = $2
310
+ ON CONFLICT (queue_id, name) DO NOTHING
311
+ `, [partition.name, queue]);
312
+ }
313
+
314
+ // Push messages to partitions
315
+ for (const p of partitions) {
316
+ await client.push(`${queue}/${p.name}`, {
317
+ message: `${p.name} priority message`,
318
+ priority: p.priority
319
+ });
320
+ }
321
+
322
+ await sleep(100);
323
+
324
+ // Take messages from different partitions
325
+ const allMessages = [];
326
+ for (let i = 0; i < partitions.length; i++) {
327
+ for await (const msg of client.take(queue, { limit: 1, batch: 1 })) {
328
+ allMessages.push(msg);
329
+ await client.ack(msg);
330
+ }
331
+ }
332
+
333
+ if (allMessages.length !== partitions.length) {
334
+ throw new Error(`Expected ${partitions.length} messages, got ${allMessages.length}`);
335
+ }
336
+
337
+ passTest('FIFO ordering within partitions works correctly');
338
+ } catch (error) {
339
+ failTest(error);
340
+ }
341
+ }
342
+