queen-mq 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +212 -144
  2. package/client-js/client-v2/Queen.js +28 -0
  3. package/client-js/client-v2/README.md +6 -2
  4. package/client-js/client-v2/builders/QueueBuilder.js +7 -1
  5. package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
  6. package/client-js/client-v2/stream/StreamBuilder.js +72 -0
  7. package/client-js/client-v2/stream/StreamConsumer.js +156 -0
  8. package/client-js/client-v2/stream/Window.js +140 -0
  9. package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
  10. package/client-js/test-v2/consume.js +1 -0
  11. package/client-js/test-v2/maintenance.js +261 -0
  12. package/client-js/test-v2/retention.js +69 -0
  13. package/client-js/test-v2/run.js +7 -1
  14. package/package.json +1 -1
  15. package/client-js/client/client.js +0 -1536
  16. package/client-js/client/index.js +0 -4
  17. package/client-js/client/utils/http.js +0 -173
  18. package/client-js/client/utils/loadBalancer.js +0 -152
  19. package/client-js/client/utils/retry.js +0 -41
  20. package/client-js/services/encryptionService.js +0 -82
  21. package/client-js/services/evictionService.js +0 -160
  22. package/client-js/services/retentionService.js +0 -162
  23. package/client-js/services/startupSync.js +0 -35
  24. package/client-js/test/README.md +0 -224
  25. package/client-js/test/advanced-client-tests.js +0 -761
  26. package/client-js/test/advanced-pattern-tests.js +0 -1137
  27. package/client-js/test/bus-mode-tests.js +0 -361
  28. package/client-js/test/core-tests.js +0 -457
  29. package/client-js/test/edge-case-tests.js +0 -562
  30. package/client-js/test/enterprise-tests.js +0 -637
  31. package/client-js/test/human.js +0 -162
  32. package/client-js/test/partition-locking-tests.js +0 -545
  33. package/client-js/test/partition-transaction-tests.js +0 -482
  34. package/client-js/test/qos0-tests.js +0 -334
  35. package/client-js/test/test-new.js +0 -370
  36. package/client-js/test/utils.js +0 -169
  37. package/client-js/test/window-buffer-test.js +0 -114
  38. package/client-js/utils/logger.js +0 -44
  39. package/client-js/utils/uuid.js +0 -5
@@ -1,361 +0,0 @@
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.data.id).sort();
51
- const group2Ids = group2Messages.map(m => m.data.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.data.id);
123
- const worker2Ids = worker2Messages.map(m => m.data.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.data.id).sort();
154
- const auditIds = auditMessages.map(m => m.data.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
-