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