queen-mq 0.1.2 → 0.1.3
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.
- package/PROGRAMMATIC_SERVER.md +190 -0
- package/README.md +27 -0
- package/examples/batch-processing.js +2 -2
- package/examples/programmatic-server.js +58 -0
- package/examples/test-cache-multi-server.js +2 -2
- package/examples/test-connection-recovery.js +66 -0
- package/package.json +1 -1
- package/src/client/client.js +27 -1
- package/src/client/index.js +4 -3
- package/src/managers/queueManagerOptimized.js +64 -35
- package/src/routes/messages.js +8 -8
- package/src/routes/status.js +4 -4
- package/src/server.js +530 -504
- package/src/services/startupSync.js +1 -1
- package/src/test/advanced-pattern-tests.js +1 -1
- package/src/test/core-tests.js +110 -0
- package/src/test/test-new.js +3 -1
- package/src/test/test.js +1 -1
- package/src/test/window-buffer-test.js +114 -0
- package/webapp/src/components/messages/MessageFilters.vue +1 -14
- package/webapp/src/views/Analytics.vue +5 -5
- package/webapp/src/views/Messages.vue +4 -2
- package/src/client/queenClient.js +0 -518
|
@@ -22,7 +22,7 @@ export async function syncSystemEvents(client, eventManager, consumerGroup) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
for (const message of result.messages) {
|
|
25
|
-
await eventManager.processSystemEvent(message.
|
|
25
|
+
await eventManager.processSystemEvent(message.data);
|
|
26
26
|
await client.ack(message.transactionId, 'completed', null, consumerGroup);
|
|
27
27
|
processed++;
|
|
28
28
|
}
|
|
@@ -1000,7 +1000,7 @@ export async function testMultipleConsumersSinglePartitionOrdering(client) {
|
|
|
1000
1000
|
limit: batchSize,
|
|
1001
1001
|
batch: batchSize
|
|
1002
1002
|
})) {
|
|
1003
|
-
const sequenceNumber = msg.
|
|
1003
|
+
const sequenceNumber = msg.data.sequenceNumber;
|
|
1004
1004
|
consumerMessages.push({
|
|
1005
1005
|
consumerId,
|
|
1006
1006
|
sequenceNumber,
|
package/src/test/core-tests.js
CHANGED
|
@@ -340,3 +340,113 @@ export async function testPartitionFIFOOrdering(client) {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Test 8: Window Buffer
|
|
345
|
+
* Verifies that windowBuffer delays message consumption across all access modes
|
|
346
|
+
*/
|
|
347
|
+
export async function testWindowBuffer(client) {
|
|
348
|
+
startTest('Window Buffer');
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const timestamp = Date.now();
|
|
352
|
+
const queueName = 'test-window-buffer-' + timestamp;
|
|
353
|
+
const partition = 'test-partition';
|
|
354
|
+
|
|
355
|
+
// Test CASE 1: Direct partition access
|
|
356
|
+
await client.queue(queueName, { windowBuffer: 5 });
|
|
357
|
+
|
|
358
|
+
// Push a message
|
|
359
|
+
await client.push(`${queueName}/${partition}`, { test: 'case1' });
|
|
360
|
+
|
|
361
|
+
// Try to take immediately - should get nothing
|
|
362
|
+
let gotImmediate = false;
|
|
363
|
+
for await (const msg of client.take(`${queueName}/${partition}`, { limit: 1 })) {
|
|
364
|
+
gotImmediate = true;
|
|
365
|
+
await client.ack(msg);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (gotImmediate) {
|
|
369
|
+
throw new Error('Got message immediately in direct access - windowBuffer not working');
|
|
370
|
+
}
|
|
371
|
+
log('Direct partition access: windowBuffer correctly delayed message');
|
|
372
|
+
|
|
373
|
+
// Wait for window to pass
|
|
374
|
+
await sleep(6000);
|
|
375
|
+
|
|
376
|
+
// Now should get the message
|
|
377
|
+
let gotDelayed = false;
|
|
378
|
+
for await (const msg of client.take(`${queueName}/${partition}`, { limit: 1 })) {
|
|
379
|
+
gotDelayed = true;
|
|
380
|
+
await client.ack(msg);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!gotDelayed) {
|
|
384
|
+
throw new Error('Did not get message after window expired');
|
|
385
|
+
}
|
|
386
|
+
log('Message available after window buffer expired');
|
|
387
|
+
|
|
388
|
+
// Test CASE 2: Queue-level access
|
|
389
|
+
const queue2 = 'test-window-buffer-q2-' + timestamp;
|
|
390
|
+
await client.queue(queue2, { windowBuffer: 5 });
|
|
391
|
+
await client.push(`${queue2}/p1`, { test: 'case2' });
|
|
392
|
+
|
|
393
|
+
let gotCase2Immediate = false;
|
|
394
|
+
for await (const msg of client.take(queue2, { limit: 1 })) {
|
|
395
|
+
gotCase2Immediate = true;
|
|
396
|
+
await client.ack(msg);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (gotCase2Immediate) {
|
|
400
|
+
throw new Error('Got message immediately in queue access - windowBuffer not working');
|
|
401
|
+
}
|
|
402
|
+
log('Queue-level access: windowBuffer correctly delayed message');
|
|
403
|
+
|
|
404
|
+
await sleep(6000);
|
|
405
|
+
|
|
406
|
+
let gotCase2Delayed = false;
|
|
407
|
+
for await (const msg of client.take(queue2, { limit: 1 })) {
|
|
408
|
+
gotCase2Delayed = true;
|
|
409
|
+
await client.ack(msg);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!gotCase2Delayed) {
|
|
413
|
+
throw new Error('Did not get message after window expired (queue access)');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Test CASE 3: Namespace/task filtered access
|
|
417
|
+
const ns = `test-ns-${timestamp}`;
|
|
418
|
+
const task = `test-task-${timestamp}`;
|
|
419
|
+
const queue3 = 'test-window-buffer-filtered-' + timestamp;
|
|
420
|
+
|
|
421
|
+
await client.queue(queue3, { windowBuffer: 5 }, ns, task);
|
|
422
|
+
await client.push(`${queue3}/p1`, { test: 'case3' });
|
|
423
|
+
|
|
424
|
+
let gotCase3Immediate = false;
|
|
425
|
+
for await (const msg of client.take(`namespace:${ns}/task:${task}`, { limit: 1 })) {
|
|
426
|
+
gotCase3Immediate = true;
|
|
427
|
+
await client.ack(msg);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (gotCase3Immediate) {
|
|
431
|
+
throw new Error('Got message immediately in filtered access - windowBuffer not working');
|
|
432
|
+
}
|
|
433
|
+
log('Namespace/task filtered access: windowBuffer correctly delayed message');
|
|
434
|
+
|
|
435
|
+
await sleep(6000);
|
|
436
|
+
|
|
437
|
+
let gotCase3Delayed = false;
|
|
438
|
+
for await (const msg of client.take(`namespace:${ns}/task:${task}`, { limit: 1 })) {
|
|
439
|
+
gotCase3Delayed = true;
|
|
440
|
+
await client.ack(msg);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (!gotCase3Delayed) {
|
|
444
|
+
throw new Error('Did not get message after window expired (filtered access)');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
passTest('Window buffer works correctly across all access modes');
|
|
448
|
+
} catch (error) {
|
|
449
|
+
failTest(error);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
package/src/test/test-new.js
CHANGED
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
testQueueConfiguration,
|
|
33
33
|
testTakeAndAcknowledgment,
|
|
34
34
|
testDelayedProcessing,
|
|
35
|
-
testPartitionFIFOOrdering
|
|
35
|
+
testPartitionFIFOOrdering,
|
|
36
|
+
testWindowBuffer
|
|
36
37
|
} from './core-tests.js';
|
|
37
38
|
|
|
38
39
|
import {
|
|
@@ -168,6 +169,7 @@ async function runTests() {
|
|
|
168
169
|
await runTest(() => testTakeAndAcknowledgment(client));
|
|
169
170
|
await runTest(() => testDelayedProcessing(client));
|
|
170
171
|
await runTest(() => testPartitionFIFOOrdering(client));
|
|
172
|
+
await runTest(() => testWindowBuffer(client));
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
// ============================================
|
package/src/test/test.js
CHANGED
|
@@ -4147,7 +4147,7 @@ async function testMultipleConsumersSinglePartitionOrdering() {
|
|
|
4147
4147
|
|
|
4148
4148
|
// Process messages and track their sequence numbers
|
|
4149
4149
|
for (const msg of result.messages) {
|
|
4150
|
-
const sequenceNumber = msg.
|
|
4150
|
+
const sequenceNumber = msg.data.sequenceNumber;
|
|
4151
4151
|
consumerMessages.push({
|
|
4152
4152
|
consumerId,
|
|
4153
4153
|
sequenceNumber,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test to validate windowBuffer functionality
|
|
3
|
+
* Ensures windowBuffer works correctly for all access modes:
|
|
4
|
+
* - Direct partition access
|
|
5
|
+
* - Queue-level access
|
|
6
|
+
* - Namespace/task filtered access
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Queen } from '../client/index.js';
|
|
10
|
+
import { sleep } from './utils.js';
|
|
11
|
+
|
|
12
|
+
const client = new Queen({
|
|
13
|
+
baseUrl: 'http://localhost:6632'
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
async function testWindowBuffer() {
|
|
17
|
+
console.log('\n🧪 Testing windowBuffer functionality...\n');
|
|
18
|
+
|
|
19
|
+
const timestamp = Date.now();
|
|
20
|
+
const queueName = 'test-window-buffer-' + timestamp;
|
|
21
|
+
const partition = 'test-partition';
|
|
22
|
+
|
|
23
|
+
// Delete queue if it exists (cleanup from previous runs)
|
|
24
|
+
try {
|
|
25
|
+
await client.queueDelete(queueName);
|
|
26
|
+
console.log(`0. Deleted existing queue "${queueName}" (cleanup)`);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
// Queue doesn't exist, that's fine
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Configure queue with 10 second window buffer
|
|
32
|
+
console.log(`1. Creating queue "${queueName}" with windowBuffer: 10 seconds`);
|
|
33
|
+
await client.queue(queueName, {
|
|
34
|
+
windowBuffer: 10
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Push first message
|
|
38
|
+
console.log('2. Pushing first message at', new Date().toISOString());
|
|
39
|
+
await client.push(`${queueName}/${partition}`, { message: 'Message 1' });
|
|
40
|
+
|
|
41
|
+
// Wait 2 seconds and push second message
|
|
42
|
+
await sleep(2000);
|
|
43
|
+
console.log('3. Pushing second message at', new Date().toISOString());
|
|
44
|
+
await client.push(`${queueName}/${partition}`, { message: 'Message 2' });
|
|
45
|
+
|
|
46
|
+
// Try to pop immediately (should get NOTHING due to windowBuffer)
|
|
47
|
+
console.log('\n4. Trying to pop immediately (should get NOTHING due to 10s windowBuffer)...');
|
|
48
|
+
|
|
49
|
+
// Test CASE 1: Direct partition access
|
|
50
|
+
console.log('\n TEST CASE 1: Direct partition access');
|
|
51
|
+
let case1Result = 0;
|
|
52
|
+
for await (const msg of client.take(`${queueName}/${partition}`, { limit: 10, batch: 10 })) {
|
|
53
|
+
case1Result++;
|
|
54
|
+
await client.ack(msg, true);
|
|
55
|
+
}
|
|
56
|
+
const case1Status = case1Result === 0 ? '✅' : '❌';
|
|
57
|
+
console.log(` ${case1Status} DIRECT ACCESS: Got ${case1Result} messages (expected 0)`);
|
|
58
|
+
|
|
59
|
+
// Test CASE 2: Queue-level access
|
|
60
|
+
console.log('\n TEST CASE 2: Queue-level access');
|
|
61
|
+
let case2Result = 0;
|
|
62
|
+
for await (const msg of client.take(queueName, { limit: 10, batch: 10 })) {
|
|
63
|
+
case2Result++;
|
|
64
|
+
await client.ack(msg, true);
|
|
65
|
+
}
|
|
66
|
+
const case2Status = case2Result === 0 ? '✅' : '❌';
|
|
67
|
+
console.log(` ${case2Status} QUEUE ACCESS: Got ${case2Result} messages (expected 0)`);
|
|
68
|
+
|
|
69
|
+
// Test CASE 3: Namespace/task filtered access (should work correctly)
|
|
70
|
+
console.log('\n TEST CASE 3: Namespace/task filtered access');
|
|
71
|
+
|
|
72
|
+
// Use unique namespace/task per test run to avoid collisions
|
|
73
|
+
const testNamespace = `test-ns-${timestamp}`;
|
|
74
|
+
const testTask = `test-task-${timestamp}`;
|
|
75
|
+
const filteredQueue = `${queueName}-filtered`;
|
|
76
|
+
|
|
77
|
+
// Delete queue if it exists
|
|
78
|
+
try {
|
|
79
|
+
await client.queueDelete(filteredQueue);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Queue doesn't exist, that's fine
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await client.queue(filteredQueue, {
|
|
85
|
+
windowBuffer: 10
|
|
86
|
+
}, testNamespace, testTask);
|
|
87
|
+
|
|
88
|
+
await client.push(`${filteredQueue}/${partition}`, { message: 'Filtered message' });
|
|
89
|
+
|
|
90
|
+
let case3Result = 0;
|
|
91
|
+
for await (const msg of client.take(`namespace:${testNamespace}/task:${testTask}`, { limit: 10, batch: 10 })) {
|
|
92
|
+
case3Result++;
|
|
93
|
+
await client.ack(msg, true);
|
|
94
|
+
}
|
|
95
|
+
const case3Status = case3Result === 0 ? '✅' : '❌';
|
|
96
|
+
console.log(` ${case3Status} FILTERED ACCESS: Got ${case3Result} messages (expected 0)`);
|
|
97
|
+
|
|
98
|
+
console.log('\n📊 SUMMARY:');
|
|
99
|
+
const allPassed = case1Result === 0 && case2Result === 0 && case3Result === 0;
|
|
100
|
+
|
|
101
|
+
if (allPassed) {
|
|
102
|
+
console.log(' ✅ All tests PASSED! windowBuffer is working correctly in all access modes.');
|
|
103
|
+
} else {
|
|
104
|
+
console.log(' Issues found:');
|
|
105
|
+
if (case1Result !== 0) console.log(' ❌ Direct partition access: windowBuffer NOT enforced');
|
|
106
|
+
if (case2Result !== 0) console.log(' ❌ Queue-level access: windowBuffer NOT enforced');
|
|
107
|
+
if (case3Result !== 0) console.log(' ❌ Namespace/task filtered access: windowBuffer NOT enforced');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('\n' + (allPassed ? '✅ Test complete. All fixed!' : '❌ Test complete. Issues remain.') + '\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
testWindowBuffer().catch(console.error);
|
|
114
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div>
|
|
3
3
|
<div class="flex flex-col sm:flex-row gap-3">
|
|
4
4
|
<!-- Search by Transaction ID -->
|
|
5
5
|
<div class="flex-1">
|
|
@@ -49,19 +49,6 @@
|
|
|
49
49
|
</div>
|
|
50
50
|
</template>
|
|
51
51
|
|
|
52
|
-
<style scoped>
|
|
53
|
-
.filter-container {
|
|
54
|
-
background: #ffffff;
|
|
55
|
-
border: none;
|
|
56
|
-
box-shadow: none;
|
|
57
|
-
border-radius: 0.75rem;
|
|
58
|
-
padding: 1rem;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.dark .filter-container {
|
|
62
|
-
background: #0a0d14;
|
|
63
|
-
}
|
|
64
|
-
</style>
|
|
65
52
|
|
|
66
53
|
<script setup>
|
|
67
54
|
import { ref, watch } from 'vue';
|
|
@@ -317,27 +317,27 @@ onUnmounted(() => {
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
|
|
320
|
-
|
|
320
|
+
.filter-flat :deep(.input) {
|
|
321
321
|
background: transparent;
|
|
322
322
|
border: 1px solid rgba(156, 163, 175, 0.15);
|
|
323
323
|
transition: all 0.2s ease;
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
|
|
326
|
+
.filter-flat :deep(.input:hover) {
|
|
327
327
|
border-color: rgba(156, 163, 175, 0.25);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
.filter-flat :deep(.input:focus) {
|
|
331
331
|
background: rgba(244, 63, 94, 0.02);
|
|
332
332
|
border-color: rgba(244, 63, 94, 0.4);
|
|
333
333
|
box-shadow: 0 0 0 3px rgba(244, 63, 94, 0.05);
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
.dark
|
|
336
|
+
.dark .filter-flat :deep(.input) {
|
|
337
337
|
border-color: rgba(156, 163, 175, 0.1);
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
.dark
|
|
340
|
+
.dark .filter-flat :deep(.input:focus) {
|
|
341
341
|
background: rgba(244, 63, 94, 0.03);
|
|
342
342
|
border-color: rgba(244, 63, 94, 0.5);
|
|
343
343
|
box-shadow: 0 0 0 3px rgba(244, 63, 94, 0.08);
|
|
@@ -18,12 +18,14 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
<!-- Filters -->
|
|
21
|
-
<
|
|
21
|
+
<div class="filter-flat">
|
|
22
|
+
<MessageFilters
|
|
22
23
|
v-model:search="searchQuery"
|
|
23
24
|
v-model:queue="queueFilter"
|
|
24
25
|
v-model:status="statusFilter"
|
|
25
26
|
:queues="queues"
|
|
26
|
-
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
27
29
|
|
|
28
30
|
<LoadingSpinner v-if="loading && !messages.length" />
|
|
29
31
|
|