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
@@ -5,37 +5,66 @@ import config from '../config.js';
5
5
  const EVICTION_INTERVAL = config.JOBS.EVICTION_INTERVAL;
6
6
 
7
7
  // Evict messages that exceeded max wait time for a queue
8
+ // Note: Eviction is already handled by POP query filtering old messages
9
+ // This service optionally moves them to DLQ for visibility
8
10
  const evictMessages = async (client, queueName, maxWaitTimeSeconds) => {
9
11
  if (!maxWaitTimeSeconds || maxWaitTimeSeconds <= 0) return 0;
10
12
 
11
- // For the new schema, we need to update or create status entries for evicted messages
12
- const result = await client.query(`
13
- WITH queue_partitions AS (
14
- SELECT p.id
15
- FROM queen.partitions p
16
- JOIN queen.queues q ON p.queue_id = q.id
17
- WHERE q.name = $1
18
- ),
19
- eligible_messages AS (
20
- SELECT m.id
21
- FROM queen.messages m
22
- JOIN queue_partitions qp ON m.partition_id = qp.id
23
- LEFT JOIN queen.messages_status ms ON m.id = ms.message_id AND ms.consumer_group IS NULL
24
- WHERE m.created_at < NOW() - INTERVAL '1 second' * $2
25
- AND (ms.status = 'pending' OR ms.status IS NULL)
26
- )
27
- INSERT INTO queen.messages_status (message_id, consumer_group, status, completed_at, error_message)
28
- SELECT id, NULL, 'evicted', NOW(), 'Message exceeded maximum wait time'
29
- FROM eligible_messages
30
- ON CONFLICT (message_id, consumer_group)
31
- DO UPDATE SET
32
- status = 'evicted',
33
- completed_at = NOW(),
34
- error_message = 'Message exceeded maximum wait time'
35
- RETURNING message_id
36
- `, [queueName, maxWaitTimeSeconds]);
37
-
38
- return result.rowCount || 0;
13
+ try {
14
+ // Find old messages that haven't been consumed and move to DLQ
15
+ // Use a WHERE EXISTS check to ensure message still exists before inserting into DLQ
16
+ const result = await client.query(`
17
+ WITH queue_partitions AS (
18
+ SELECT p.id, p.queue_id
19
+ FROM queen.partitions p
20
+ JOIN queen.queues q ON p.queue_id = q.id
21
+ WHERE q.name = $1
22
+ ),
23
+ old_unconsumed_messages AS (
24
+ SELECT m.id, m.partition_id, m.created_at
25
+ FROM queen.messages m
26
+ JOIN queue_partitions qp ON m.partition_id = qp.id
27
+ LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id
28
+ AND pc.consumer_group = '__QUEUE_MODE__'
29
+ WHERE m.created_at < NOW() - INTERVAL '1 second' * $2
30
+ -- Message has not been consumed yet
31
+ AND ((m.created_at, m.id) > (COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz),
32
+ COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
33
+ OR pc.last_consumed_id IS NULL)
34
+ )
35
+ INSERT INTO queen.dead_letter_queue (
36
+ message_id,
37
+ partition_id,
38
+ consumer_group,
39
+ error_message,
40
+ original_created_at
41
+ )
42
+ SELECT
43
+ om.id,
44
+ om.partition_id,
45
+ '__QUEUE_MODE__',
46
+ 'Message exceeded maximum wait time',
47
+ om.created_at
48
+ FROM old_unconsumed_messages om
49
+ WHERE EXISTS (
50
+ SELECT 1 FROM queen.messages m WHERE m.id = om.id
51
+ )
52
+ ON CONFLICT DO NOTHING
53
+ RETURNING message_id
54
+ `, [queueName, maxWaitTimeSeconds]);
55
+
56
+ return result.rowCount || 0;
57
+ } catch (error) {
58
+ // Catch foreign key violations (messages deleted between SELECT and INSERT)
59
+ if (error.code === '23503') {
60
+ // Foreign key constraint violation - message was deleted
61
+ // This is a harmless race condition, just log it
62
+ log(`${LogTypes.EVICTION} | Queue: ${queueName} | Warning: Message deleted during eviction (race condition)`);
63
+ return 0;
64
+ }
65
+ // Re-throw other errors
66
+ throw error;
67
+ }
39
68
  };
40
69
 
41
70
  // Evict messages during pop operation (inline eviction)
@@ -16,30 +16,44 @@ const retentionQueue = async (client, queue) => {
16
16
 
17
17
  let totalDeleted = 0;
18
18
 
19
- // Delete old pending messages from all partitions in this queue
19
+ // Delete old unconsumed messages from all partitions in this queue
20
20
  if (retentionSeconds > 0) {
21
21
  const result = await client.query(`
22
- DELETE FROM queen.messages
23
- WHERE partition_id IN (
22
+ DELETE FROM queen.messages m
23
+ WHERE m.partition_id IN (
24
24
  SELECT id FROM queen.partitions WHERE queue_id = $1
25
25
  )
26
- AND status = 'pending'
27
- AND created_at < NOW() - INTERVAL '1 second' * $2
26
+ AND m.created_at < NOW() - INTERVAL '1 second' * $2
27
+ AND NOT EXISTS (
28
+ -- Message has been consumed by at least one consumer
29
+ SELECT 1
30
+ FROM queen.partition_consumers pc
31
+ WHERE pc.partition_id = m.partition_id
32
+ AND (m.created_at, m.id) <= (pc.last_consumed_created_at, pc.last_consumed_id)
33
+ )
28
34
  RETURNING id
29
35
  `, [queue.id, retentionSeconds]);
30
36
 
31
37
  totalDeleted += result.rowCount || 0;
32
38
  }
33
39
 
34
- // Delete completed/failed/evicted messages from all partitions in this queue
40
+ // Delete old consumed messages from all partitions in this queue
41
+ // (messages consumed by ALL consumer groups on that partition)
35
42
  if (completedRetentionSeconds > 0) {
36
43
  const result = await client.query(`
37
- DELETE FROM queen.messages
38
- WHERE partition_id IN (
44
+ DELETE FROM queen.messages m
45
+ WHERE m.partition_id IN (
39
46
  SELECT id FROM queen.partitions WHERE queue_id = $1
40
47
  )
41
- AND status IN ('completed', 'failed', 'evicted')
42
- AND COALESCE(completed_at, failed_at, created_at) < NOW() - INTERVAL '1 second' * $2
48
+ AND m.created_at < NOW() - INTERVAL '1 second' * $2
49
+ AND NOT EXISTS (
50
+ -- No consumer group that hasn't consumed this message yet
51
+ SELECT 1
52
+ FROM queen.partition_consumers pc
53
+ WHERE pc.partition_id = m.partition_id
54
+ AND ((m.created_at, m.id) > (pc.last_consumed_created_at, pc.last_consumed_id)
55
+ OR pc.last_consumed_id IS NULL)
56
+ )
43
57
  RETURNING id
44
58
  `, [queue.id, completedRetentionSeconds]);
45
59
 
@@ -48,7 +62,6 @@ const retentionQueue = async (client, queue) => {
48
62
 
49
63
  // Log retention if messages were deleted
50
64
  if (totalDeleted > 0) {
51
- // Note: We might need to update retention_history table to use queue_id instead
52
65
  log(`${LogTypes.RETENTION} | Queue: ${queue.name} | Count: ${totalDeleted} | RetentionSeconds: ${retentionSeconds} | CompletedRetentionSeconds: ${completedRetentionSeconds}`);
53
66
  }
54
67
 
@@ -76,6 +89,23 @@ const cleanupEmptyPartitions = async (client) => {
76
89
  return result.rowCount;
77
90
  };
78
91
 
92
+ // Clean up old metrics data (messages_consumed table)
93
+ const cleanupOldMetrics = async (client) => {
94
+ const retentionDays = config.JOBS.METRICS_RETENTION_DAYS;
95
+
96
+ // Delete metrics data older than the retention period
97
+ const result = await client.query(`
98
+ DELETE FROM queen.messages_consumed
99
+ WHERE acked_at < NOW() - INTERVAL '1 day' * $1
100
+ `, [retentionDays]);
101
+
102
+ if (result.rowCount > 0) {
103
+ log(`📊 Deleted ${result.rowCount} old metrics records (older than ${retentionDays} days)`);
104
+ }
105
+
106
+ return result.rowCount;
107
+ };
108
+
79
109
  // Main retention function
80
110
  const performRetention = async (pool) => {
81
111
  const client = await pool.connect();
@@ -97,6 +127,9 @@ const performRetention = async (pool) => {
97
127
  // Cleanup empty partitions
98
128
  await cleanupEmptyPartitions(client);
99
129
 
130
+ // Cleanup old metrics data
131
+ await cleanupOldMetrics(client);
132
+
100
133
  return totalDeleted;
101
134
  } catch (error) {
102
135
  log('Retention error:', error);
@@ -0,0 +1,174 @@
1
+ # Test Migration Issues and Fixes
2
+
3
+ ## Summary
4
+
5
+ When migrating tests from the old `queenClient` interface to the new minimalist `Queen` client interface, we discovered client bugs that needed fixing. The system is transactional - ACKs immediately release partition locks when awaited.
6
+
7
+ ## Client Bugs Fixed
8
+
9
+ ### 1. Null Payload Handling in `push()`
10
+ **Issue**: `typeof null === 'object'` is `true` in JavaScript, causing the code to try to access properties on `null` payloads.
11
+
12
+ **Location**: `src/client/client.js` line 153
13
+
14
+ **Fix**:
15
+ ```javascript
16
+ // Before
17
+ const isMessageObject = typeof item === 'object' &&
18
+ (item._payload || item._transactionId || item._traceId);
19
+
20
+ // After
21
+ const isMessageObject = typeof item === 'object' && item !== null &&
22
+ (item._payload || item._transactionId || item._traceId);
23
+ ```
24
+
25
+ **Why**: Tests correctly validate edge cases including null payloads. The client must handle them gracefully.
26
+
27
+ ### 2. Null Handling in `ack()`
28
+ **Issue**: Same null-checking issue when extracting transaction ID from message object.
29
+
30
+ **Location**: `src/client/client.js` line 330
31
+
32
+ **Fix**:
33
+ ```javascript
34
+ // Before
35
+ const transactionId = typeof message === 'object' ?
36
+ (message.transactionId || message.id) :
37
+ message;
38
+
39
+ // After
40
+ const transactionId = typeof message === 'object' && message !== null ?
41
+ (message.transactionId || message.id) :
42
+ message;
43
+ ```
44
+
45
+ ### 3. Consumer Group Parsing for Namespace/Task Addresses
46
+ **Issue**: The `#parseAddress()` method didn't extract consumer groups from namespace/task patterns like `namespace:billing@group-a`.
47
+
48
+ **Location**: `src/client/client.js` line 77-101
49
+
50
+ **Fix**: Extract `@group` portion before parsing namespace/task segments:
51
+ ```javascript
52
+ #parseAddress(address) {
53
+ if (address.includes('namespace:') || address.includes('task:')) {
54
+ const parts = {};
55
+
56
+ // Extract consumer group if present (after @)
57
+ let workingAddress = address;
58
+ const atIndex = address.lastIndexOf('@');
59
+ if (atIndex > 0) {
60
+ parts.consumerGroup = address.substring(atIndex + 1);
61
+ workingAddress = address.substring(0, atIndex);
62
+ }
63
+
64
+ const segments = workingAddress.split('/');
65
+ for (const segment of segments) {
66
+ if (segment.startsWith('namespace:')) {
67
+ parts.namespace = segment.substring(10);
68
+ } else if (segment.startsWith('task:')) {
69
+ parts.task = segment.substring(5);
70
+ }
71
+ }
72
+ return parts;
73
+ }
74
+ // ...
75
+ }
76
+ ```
77
+
78
+ ### 4. Defensive Filtering of Null Messages
79
+ **Issue**: Server might return null values in the `messages` array response.
80
+
81
+ **Location**: `src/client/client.js` in `take()` method
82
+
83
+ **Fix**: Filter out null/undefined messages when yielding:
84
+ ```javascript
85
+ // Yield each message (filter out any null/undefined values)
86
+ for (const message of result.messages) {
87
+ if (message) { // Skip null/undefined messages
88
+ yield message;
89
+ count++;
90
+ if (limit && count >= limit) return;
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Test Fixes
96
+
97
+ ### 1. Consumer Group Isolation Test
98
+ **Problem**: Test was ACKing all messages as completed for both groups, but the test expects group2 to have some failed messages to verify isolation.
99
+
100
+ **Solution**: Actually fail the first 2 messages for group2:
101
+ ```javascript
102
+ // Fail first 2 messages for group2, complete the rest
103
+ for (let i = 0; i < group2Messages.length; i++) {
104
+ if (i < 2) {
105
+ await client.ack(group2Messages[i], false, {
106
+ error: 'Test failure',
107
+ group: 'group2'
108
+ });
109
+ } else {
110
+ await client.ack(group2Messages[i], true, { group: 'group2' });
111
+ }
112
+ }
113
+ ```
114
+
115
+ ### 2. Empty and Null Payloads Test
116
+ **Problem**: If server returns null message objects in the array, the test would crash.
117
+
118
+ **Solution**: Add defensive check (though client now filters these):
119
+ ```javascript
120
+ for await (const msg of client.take(queue, { limit: 10 })) {
121
+ if (!msg) {
122
+ throw new Error('Received null/undefined message from take()');
123
+ }
124
+ messages.push(msg);
125
+ await client.ack(msg);
126
+ }
127
+ ```
128
+
129
+ ## Key Learnings
130
+
131
+ ### System is Transactional ✅
132
+
133
+ The Queen system is **transactional**. When you `await client.ack(msg)`:
134
+ - The ACK completes atomically
135
+ - The partition lock is released immediately
136
+ - No "timing issues" or delays needed
137
+
138
+ ### Correct Pattern
139
+ ```javascript
140
+ // ✅ This works correctly:
141
+ for await (const msg of client.take(queue, { limit: 10 })) {
142
+ await client.ack(msg); // Partition unlocked when this completes
143
+ }
144
+
145
+ // Next consumer can immediately access the partition:
146
+ for await (const msg of client.take(queue, { limit: 10 })) {
147
+ // Gets messages right away
148
+ }
149
+ ```
150
+
151
+ ### Edge Case Handling
152
+
153
+ 1. **Null payloads are valid**: Tests should validate that the system handles `null`, `undefined`, empty objects, and empty strings as payloads.
154
+
155
+ 2. **Client must be defensive**: Check for `typeof x === 'object' && x !== null` when you need to access properties.
156
+
157
+ 3. **Server responses should be filtered**: If server returns null messages in an array, filter them out client-side.
158
+
159
+ ### Test Design Principles
160
+
161
+ 1. **Test real edge cases**: null, undefined, empty strings, large payloads, etc.
162
+
163
+ 2. **Don't add artificial delays**: The system is transactional. If you need a delay, something is wrong.
164
+
165
+ 3. **Verify isolation**: When testing consumer groups, actually test that different groups can have different statuses for the same messages.
166
+
167
+ 4. **Use appropriate batch sizes**: Match batch size to expected message count for efficient testing.
168
+
169
+ ## Final Results
170
+
171
+ After fixing client bugs and test issues:
172
+ - 33/33 tests should pass
173
+ - No timing sensitivity
174
+ - Clean, transactional behavior throughout
@@ -0,0 +1,203 @@
1
+ # Queen Message Queue Test Suite
2
+
3
+ This directory contains the comprehensive test suite for the Queen Message Queue system, using the new minimalist `Queen` client interface.
4
+
5
+ ## Test Structure
6
+
7
+ The test suite is organized into focused, modular files:
8
+
9
+ ### Core Files
10
+
11
+ - **`test-new.js`** - Main test runner that orchestrates all tests
12
+ - **`utils.js`** - Shared utilities (logging, database helpers, result tracking)
13
+
14
+ ### Test Categories
15
+
16
+ #### 1. Core Features (`core-tests.js`)
17
+ - Queue creation policy
18
+ - Single/batch message push
19
+ - Queue configuration
20
+ - Take and acknowledgment
21
+ - Delayed processing
22
+ - FIFO ordering within partitions
23
+
24
+ #### 2. Partition Locking (`partition-locking-tests.js`)
25
+ - Partition locking in queue mode
26
+ - Partition locking in bus mode
27
+ - Specific partition requests with locking
28
+ - Namespace/task filtering with partition locking
29
+
30
+ #### 3. Enterprise Features (`enterprise-tests.js`)
31
+ - Message encryption/decryption
32
+ - Retention policies (pending & completed messages)
33
+ - Message eviction
34
+ - Combined enterprise features
35
+ - Enterprise error handling
36
+
37
+ #### 4. Bus Mode Features (`bus-mode-tests.js`)
38
+ - Consumer groups
39
+ - Mixed mode (queue + bus)
40
+ - Subscription modes (all vs new messages)
41
+ - Consumer group isolation
42
+
43
+ #### 5. Edge Cases (`edge-case-tests.js`)
44
+ - Empty and null payloads
45
+ - Very large payloads
46
+ - Concurrent push/take operations
47
+ - Retry limit exhaustion
48
+ - Lease expiration and redelivery
49
+ - SQL injection prevention
50
+ - XSS prevention
51
+
52
+ #### 6. Advanced Patterns (`advanced-pattern-tests.js`)
53
+ - Multi-stage pipeline workflow
54
+ - Fan-out/fan-in pattern
55
+ - Dead letter queue pattern
56
+ - Circuit breaker pattern
57
+ - Message deduplication
58
+
59
+ ## New vs Old Interface
60
+
61
+ ### Old Interface (test.js)
62
+ ```javascript
63
+ import { createQueenClient } from '../client/queenClient.js';
64
+
65
+ const client = createQueenClient({ baseUrls: [...] });
66
+
67
+ // Configure
68
+ await client.configure({ queue: 'myqueue', options: {} });
69
+
70
+ // Push
71
+ await client.push({ items: [{ queue: 'myqueue', partition: 'Default', payload: {...} }] });
72
+
73
+ // Pop
74
+ const result = await client.pop({ queue: 'myqueue', batch: 10 });
75
+
76
+ // Ack
77
+ await client.ack(transactionId, 'completed', null, consumerGroup);
78
+ ```
79
+
80
+ ### New Interface (test-new.js)
81
+ ```javascript
82
+ import { Queen } from '../client/client.js';
83
+
84
+ const client = new Queen({ baseUrls: [...] });
85
+
86
+ // Configure
87
+ await client.queue('myqueue', {}, { namespace, task });
88
+
89
+ // Push
90
+ await client.push('myqueue/partition', payload);
91
+
92
+ // Take (async iterator)
93
+ for await (const msg of client.take('myqueue/partition@group', { limit: 10 })) {
94
+ // Process message
95
+ await client.ack(msg, true, { group: 'mygroup' });
96
+ }
97
+ ```
98
+
99
+ ## Key Differences
100
+
101
+ 1. **Address Format**: The new interface uses a unified address format:
102
+ - `"queue"` - Simple queue
103
+ - `"queue/partition"` - Specific partition
104
+ - `"queue@group"` - Consumer group
105
+ - `"queue/partition@group"` - Partition + group
106
+ - `"namespace:name"` - Namespace filter
107
+ - `"task:name"` - Task filter
108
+ - `"namespace:billing/task:process"` - Combined filters
109
+
110
+ 2. **Async Iterator**: `pop()` → `take()` using async iteration
111
+ ```javascript
112
+ for await (const msg of client.take(address, options)) {
113
+ // Handle message
114
+ }
115
+ ```
116
+
117
+ 3. **Simplified Methods**: 4 core methods instead of many:
118
+ - `client.queue(name, options, metadata)` - Configure
119
+ - `client.push(address, payload, options)` - Send
120
+ - `client.take(address, options)` - Receive (async iterator)
121
+ - `client.ack(message, status, context)` - Acknowledge
122
+
123
+ 4. **Message Properties**: Can be in payload or options:
124
+ ```javascript
125
+ // In options
126
+ await client.push('queue', { data }, { transactionId: 'txn-1' });
127
+
128
+ // In payload
129
+ await client.push('queue', { data, transactionId: 'txn-1' });
130
+ ```
131
+
132
+ ## Running Tests
133
+
134
+ ```bash
135
+ # Make sure Queen servers are running first
136
+ # And database is accessible
137
+
138
+ cd /Users/alice/Work/queen
139
+
140
+ # Run all tests
141
+ nvm use 22 && node src/test/test-new.js
142
+
143
+ # Run specific test category
144
+ node src/test/test-new.js core # Core features only
145
+ node src/test/test-new.js partition # Partition locking tests only
146
+ node src/test/test-new.js enterprise # Enterprise features only
147
+ node src/test/test-new.js bus # Bus mode tests only
148
+ node src/test/test-new.js edge # Edge cases only
149
+ node src/test/test-new.js advanced # Advanced patterns only
150
+
151
+ # Show help
152
+ node src/test/test-new.js help
153
+ ```
154
+
155
+ ### Available Test Categories
156
+
157
+ - **`core`** - Core features (queue creation, push, take, ack, delayed processing, FIFO)
158
+ - **`partition`** (or `locking`) - Partition locking in queue and bus modes
159
+ - **`enterprise`** - Enterprise features (encryption, retention, eviction)
160
+ - **`bus`** - Bus mode features (consumer groups, mixed mode, subscription modes, isolation)
161
+ - **`edge`** - Edge cases (null payloads, large payloads, concurrency, SQL injection, XSS)
162
+ - **`advanced`** (or `pattern`) - Advanced patterns (pipeline, fan-out/fan-in, DLQ, circuit breaker, deduplication)
163
+
164
+ ## Test Configuration
165
+
166
+ Tests use environment variables for configuration:
167
+ - `PG_HOST` - PostgreSQL host (default: localhost)
168
+ - `PG_PORT` - PostgreSQL port (default: 5432)
169
+ - `PG_DB` - Database name (default: postgres)
170
+ - `PG_USER` - Database user (default: postgres)
171
+ - `PG_PASSWORD` - Database password (default: postgres)
172
+ - `QUEEN_ENCRYPTION_KEY` - Optional encryption key for encryption tests
173
+
174
+ ## Adding New Tests
175
+
176
+ 1. Create a test function in the appropriate category file:
177
+ ```javascript
178
+ export async function testMyFeature(client) {
179
+ startTest('My Feature Test', 'category');
180
+ try {
181
+ // Test code here
182
+ passTest('Feature works correctly');
183
+ } catch (error) {
184
+ failTest(error);
185
+ }
186
+ }
187
+ ```
188
+
189
+ 2. Import and add to `test-new.js`:
190
+ ```javascript
191
+ import { testMyFeature } from './category-tests.js';
192
+
193
+ // In runTests():
194
+ await runTest(() => testMyFeature(client));
195
+ ```
196
+
197
+ ## Notes
198
+
199
+ - All tests automatically clean up test data before and after running
200
+ - Tests use the `test-*`, `edge-*`, `pattern-*`, and `workflow-*` queue name prefixes
201
+ - The test suite supports both single-server and multi-server configurations
202
+ - Some enterprise tests (encryption, retention) are skipped if not configured
203
+