queen-mq 0.2.0 → 0.2.22

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 (85) hide show
  1. package/README.md +186 -763
  2. package/{src → client-js}/benchmark/consumer.js +6 -4
  3. package/{src → client-js}/benchmark/producer.js +9 -4
  4. package/{src → client-js}/client/client.js +277 -7
  5. package/{src → client-js}/client/index.js +1 -4
  6. package/{src → client-js}/client/utils/http.js +3 -2
  7. package/{src → client-js}/client/utils/retry.js +7 -1
  8. package/{src → client-js}/test/README.md +30 -9
  9. package/{src → client-js}/test/core-tests.js +7 -2
  10. package/{src → client-js}/test/edge-case-tests.js +2 -1
  11. package/client-js/test/human.js +162 -0
  12. package/client-js/test/partition-transaction-tests.js +482 -0
  13. package/client-js/test/qos0-tests.js +334 -0
  14. package/{src → client-js}/test/test-new.js +55 -0
  15. package/package.json +8 -17
  16. package/init-db.js +0 -20
  17. package/src/cluster-server.js +0 -242
  18. package/src/config.js +0 -229
  19. package/src/database/connection.js +0 -129
  20. package/src/database/poolManager.js +0 -199
  21. package/src/database/schema-v2.sql +0 -278
  22. package/src/managers/eventManager.js +0 -59
  23. package/src/managers/queueManagerOptimized.js +0 -1634
  24. package/src/managers/resourceCache.js +0 -96
  25. package/src/managers/systemEventManager.js +0 -132
  26. package/src/routes/ack.js +0 -26
  27. package/src/routes/configure.js +0 -46
  28. package/src/routes/messages.js +0 -368
  29. package/src/routes/pop.js +0 -69
  30. package/src/routes/push.js +0 -28
  31. package/src/routes/resources.js +0 -330
  32. package/src/routes/status.js +0 -1037
  33. package/src/server.js +0 -1633
  34. package/src/test/MIGRATION_ISSUES.md +0 -174
  35. package/src/test/test.js +0 -4524
  36. package/src/utils/streaming.js +0 -231
  37. package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
  38. package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
  39. package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
  40. package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
  41. package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
  42. package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
  43. package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
  44. package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
  45. package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
  46. package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
  47. package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
  48. package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
  49. package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
  50. package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
  51. package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
  52. package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
  53. package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
  54. package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
  55. package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
  56. package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
  57. package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
  58. package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
  59. package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
  60. package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
  61. package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
  62. package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
  63. package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
  64. package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
  65. package/src/webapp-dist/assets/queen-logo.svg +0 -263
  66. package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
  67. package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
  68. package/src/webapp-dist/index.html +0 -15
  69. package/src/websocket/wsServer.js +0 -228
  70. /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
  71. /package/{src → client-js}/benchmark/producer_multi.js +0 -0
  72. /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
  73. /package/{src → client-js}/services/encryptionService.js +0 -0
  74. /package/{src → client-js}/services/evictionService.js +0 -0
  75. /package/{src → client-js}/services/retentionService.js +0 -0
  76. /package/{src → client-js}/services/startupSync.js +0 -0
  77. /package/{src → client-js}/test/advanced-client-tests.js +0 -0
  78. /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
  79. /package/{src → client-js}/test/bus-mode-tests.js +0 -0
  80. /package/{src → client-js}/test/enterprise-tests.js +0 -0
  81. /package/{src → client-js}/test/partition-locking-tests.js +0 -0
  82. /package/{src → client-js}/test/utils.js +0 -0
  83. /package/{src → client-js}/test/window-buffer-test.js +0 -0
  84. /package/{src → client-js}/utils/logger.js +0 -0
  85. /package/{src → client-js}/utils/uuid.js +0 -0
@@ -0,0 +1,334 @@
1
+ import { startTest, passTest, failTest } from './utils.js';
2
+
3
+ /**
4
+ * Test server-side buffering
5
+ * Uses file buffer for fast writes, server flushes every 100ms (configurable via FILE_BUFFER_FLUSH_MS)
6
+ */
7
+ export async function testQoS0Buffering(client) {
8
+ startTest('Server-side buffering', 'qos0');
9
+
10
+ try {
11
+ const queue = 'test-qos0-buffer-' + Date.now();
12
+ await client.queue(queue);
13
+
14
+ // Push 10 events with server-side buffering
15
+ const start = Date.now();
16
+ for (let i = 0; i < 10; i++) {
17
+ await client.push(queue, { index: i }, { buffer: true });
18
+ }
19
+ const pushTime = Date.now() - start;
20
+
21
+ // Should return quickly (buffered to file, server-side batching)
22
+ // With file buffering, push should be fast (< 500ms for 10 messages)
23
+ if (pushTime >= 500) {
24
+ throw new Error(`Push should be fast (file buffered), took ${pushTime}ms`);
25
+ }
26
+
27
+ // Wait for server to flush buffer (FILE_BUFFER_FLUSH_MS default is 100ms)
28
+ // Add extra margin for processing
29
+ await new Promise(resolve => setTimeout(resolve, 300));
30
+
31
+ // Consume all events
32
+ const messages = [];
33
+ for await (const msg of client.take(queue, { batch: 20 })) {
34
+ messages.push(msg);
35
+ await client.ack(msg);
36
+ if (messages.length >= 10) break;
37
+ }
38
+
39
+ if (messages.length !== 10) {
40
+ throw new Error(`Should receive all 10 events, got ${messages.length}`);
41
+ }
42
+
43
+ // Verify all events received
44
+ const indices = messages.map(m => m.data.index).sort((a, b) => a - b);
45
+ for (let i = 0; i < 10; i++) {
46
+ if (!indices.includes(i)) {
47
+ throw new Error(`Should have event with index ${i}`);
48
+ }
49
+ }
50
+
51
+ passTest('Server-side buffering works correctly');
52
+ } catch (error) {
53
+ failTest(error);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Test auto-acknowledgment
59
+ */
60
+ export async function testAutoAck(client) {
61
+ startTest('Auto-acknowledgment', 'qos0');
62
+
63
+ try {
64
+ const queue = 'test-autoack-' + Date.now();
65
+ await client.queue(queue);
66
+
67
+ // Push message
68
+ await client.push(queue, { test: 'autoack-data' });
69
+
70
+ // Take with auto-ack
71
+ let msg;
72
+ for await (const m of client.take(queue, { autoAck: true })) {
73
+ msg = m;
74
+ break;
75
+ }
76
+
77
+ if (msg === undefined) {
78
+ throw new Error('Should receive message');
79
+ }
80
+ if (msg.data.test !== 'autoack-data') {
81
+ throw new Error('Should have correct data');
82
+ }
83
+
84
+ // Message should NOT be available again (already auto-acked)
85
+ await new Promise(resolve => setTimeout(resolve, 100));
86
+
87
+ let count = 0;
88
+ for await (const m of client.take(queue, { wait: false, batch: 10 })) {
89
+ count++;
90
+ await client.ack(m);
91
+ break;
92
+ }
93
+
94
+ if (count !== 0) {
95
+ throw new Error('Message should not be available again (auto-acked)');
96
+ }
97
+
98
+ passTest('Auto-ack works correctly');
99
+ } catch (error) {
100
+ failTest(error);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Test consumer group with auto-ack
106
+ */
107
+ export async function testConsumerGroupAutoAck(client) {
108
+ startTest('Consumer group with auto-ack', 'qos0');
109
+
110
+ try {
111
+ const queue = 'test-cg-autoack-' + Date.now();
112
+ await client.queue(queue);
113
+
114
+ // Push message
115
+ await client.push(queue, { value: 42, type: 'test' });
116
+
117
+ // Wait a bit for message to be available
118
+ await new Promise(resolve => setTimeout(resolve, 100));
119
+
120
+ // Two consumer groups consume same message
121
+ let msgA, msgB;
122
+
123
+ for await (const msg of client.take(`${queue}@groupA`, { autoAck: true })) {
124
+ msgA = msg;
125
+ break;
126
+ }
127
+
128
+ for await (const msg of client.take(`${queue}@groupB`, { autoAck: true })) {
129
+ msgB = msg;
130
+ break;
131
+ }
132
+
133
+ if (msgA === undefined) {
134
+ throw new Error('Group A should receive message');
135
+ }
136
+ if (msgB === undefined) {
137
+ throw new Error('Group B should receive message');
138
+ }
139
+ if (msgA.data.value !== 42) {
140
+ throw new Error('Group A should have correct data');
141
+ }
142
+ if (msgB.data.value !== 42) {
143
+ throw new Error('Group B should have correct data');
144
+ }
145
+
146
+ // Both groups should have auto-acked - message not available again
147
+ await new Promise(resolve => setTimeout(resolve, 100));
148
+
149
+ let countA = 0, countB = 0;
150
+
151
+ for await (const msg of client.take(`${queue}@groupA`, { wait: false })) {
152
+ countA++;
153
+ await client.ack(msg, true, { group: 'groupA' });
154
+ break;
155
+ }
156
+
157
+ for await (const msg of client.take(`${queue}@groupB`, { wait: false })) {
158
+ countB++;
159
+ await client.ack(msg, true, { group: 'groupB' });
160
+ break;
161
+ }
162
+
163
+ if (countA !== 0) {
164
+ throw new Error('Group A should have auto-acked');
165
+ }
166
+ if (countB !== 0) {
167
+ throw new Error('Group B should have auto-acked');
168
+ }
169
+
170
+ passTest('Consumer group auto-ack works correctly');
171
+ } catch (error) {
172
+ failTest(error);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Test normal push preserves FIFO ordering
178
+ */
179
+ export async function testFIFOOrdering(client) {
180
+ startTest('Normal push preserves FIFO', 'qos0');
181
+
182
+ try {
183
+ const queue = 'test-fifo-' + Date.now();
184
+ await client.queue(queue);
185
+
186
+ // Push without buffering (normal mode)
187
+ for (let i = 0; i < 20; i++) {
188
+ await client.push(queue, { order: i });
189
+ }
190
+
191
+ // Consume and verify order
192
+ const messages = [];
193
+ for await (const msg of client.take(queue, { batch: 20 })) {
194
+ messages.push(msg);
195
+ await client.ack(msg);
196
+ if (messages.length >= 20) break;
197
+ }
198
+
199
+ if (messages.length !== 20) {
200
+ throw new Error(`Should have 20 messages, got ${messages.length}`);
201
+ }
202
+
203
+ // Verify FIFO ordering
204
+ for (let i = 0; i < 20; i++) {
205
+ if (messages[i].data.order !== i) {
206
+ throw new Error(`Message ${i} out of order: got ${messages[i].data.order}`);
207
+ }
208
+ }
209
+
210
+ passTest('FIFO ordering preserved for normal push');
211
+ } catch (error) {
212
+ failTest(error);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Test mixed buffered and non-buffered operations
218
+ */
219
+ export async function testMixedOperations(client) {
220
+ startTest('Mixed buffered and normal operations', 'qos0');
221
+
222
+ try {
223
+ const queue = 'test-mixed-' + Date.now();
224
+ await client.queue(queue);
225
+
226
+ // Push with buffering (file buffered)
227
+ await client.push(queue, { type: 'buffered', id: 1 }, { buffer: true });
228
+
229
+ // Push normal (direct DB)
230
+ await client.push(queue, { type: 'normal', id: 2 });
231
+
232
+ // Wait for buffer flush (100ms + margin)
233
+ await new Promise(resolve => setTimeout(resolve, 200));
234
+
235
+ // Should have both messages
236
+ const messages = [];
237
+ for await (const msg of client.take(queue, { batch: 10 })) {
238
+ messages.push(msg);
239
+ await client.ack(msg);
240
+ if (messages.length >= 2) break;
241
+ }
242
+
243
+ if (messages.length !== 2) {
244
+ throw new Error(`Should have 2 messages, got ${messages.length}`);
245
+ }
246
+
247
+ const types = messages.map(m => m.data.type);
248
+ if (!types.includes('buffered')) {
249
+ throw new Error('Should have buffered message');
250
+ }
251
+ if (!types.includes('normal')) {
252
+ throw new Error('Should have normal message');
253
+ }
254
+
255
+ passTest('Mixed buffered and normal operations work correctly');
256
+ } catch (error) {
257
+ failTest(error);
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Test buffered events are stored individually
263
+ * Note: Buffering is a server-side optimization for DB writes,
264
+ * not for combining events into a single message
265
+ */
266
+ export async function testBatchedPayload(client) {
267
+ startTest('Buffered events stored individually', 'qos0');
268
+
269
+ try {
270
+ const queue = 'test-batch-payload-' + Date.now();
271
+ await client.queue(queue);
272
+
273
+ // Push multiple events with buffering
274
+ for (let i = 0; i < 5; i++) {
275
+ await client.push(queue, { index: i, timestamp: Date.now() }, { buffer: true });
276
+ }
277
+
278
+ // Wait for server flush (100ms + margin)
279
+ await new Promise(resolve => setTimeout(resolve, 200));
280
+
281
+ // Consume all messages - each event should be a separate message
282
+ const messages = [];
283
+ for await (const msg of client.take(queue, { batch: 10 })) {
284
+ messages.push(msg);
285
+ await client.ack(msg);
286
+ if (messages.length >= 5) break;
287
+ }
288
+
289
+ if (messages.length !== 5) {
290
+ throw new Error(`Should receive 5 separate messages, got ${messages.length}`);
291
+ }
292
+
293
+ // Verify each message has individual payload (not batched into array)
294
+ for (const msg of messages) {
295
+ if (typeof msg.data !== 'object' || Array.isArray(msg.data)) {
296
+ throw new Error(`Each message should have object payload, got ${typeof msg.data}`);
297
+ }
298
+ if (typeof msg.data.index !== 'number') {
299
+ throw new Error('Each message should have index property');
300
+ }
301
+ }
302
+
303
+ // Verify all indices are present
304
+ const indices = messages.map(m => m.data.index).sort((a, b) => a - b);
305
+ for (let i = 0; i < 5; i++) {
306
+ if (!indices.includes(i)) {
307
+ throw new Error(`Should have message with index ${i}`);
308
+ }
309
+ }
310
+
311
+ passTest('Buffered events stored and retrieved individually');
312
+ } catch (error) {
313
+ failTest(error);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Run all QoS 0 tests
319
+ */
320
+ export async function runQoS0Tests(client) {
321
+ console.log('\n╔════════════════════════════════════════╗');
322
+ console.log('║ Queen QoS 0 Test Suite ║');
323
+ console.log('╚════════════════════════════════════════╝\n');
324
+
325
+ await testQoS0Buffering(client);
326
+ await testAutoAck(client);
327
+ await testConsumerGroupAutoAck(client);
328
+ await testFIFOOrdering(client);
329
+ await testMixedOperations(client);
330
+ await testBatchedPayload(client);
331
+
332
+ console.log('\n✅ All QoS 0 tests passed!\n');
333
+ }
334
+
@@ -24,6 +24,12 @@ import {
24
24
  resetResults
25
25
  } from './utils.js';
26
26
 
27
+ // Import human tests
28
+ import {
29
+ testCreateQueue,
30
+ workerQueueTest
31
+ } from './human.js';
32
+
27
33
  // Import test modules
28
34
  import {
29
35
  testQueueCreationPolicy,
@@ -74,6 +80,13 @@ import {
74
80
  testXSSPrevention
75
81
  } from './edge-case-tests.js';
76
82
 
83
+ import {
84
+ testDuplicateTransactionIdsAcrossPartitions,
85
+ testBatchAckWithDuplicateTransactionIds,
86
+ testDLQWithDuplicateTransactionIds,
87
+ testAnalyticsAPIWithDuplicateTransactionIds
88
+ } from './partition-transaction-tests.js';
89
+
77
90
  import {
78
91
  testMultiStagePipeline,
79
92
  testFanOutFanIn,
@@ -105,6 +118,15 @@ import {
105
118
  runAdvancedClientTests
106
119
  } from './advanced-client-tests.js';
107
120
 
121
+ import {
122
+ testQoS0Buffering,
123
+ testAutoAck,
124
+ testConsumerGroupAutoAck,
125
+ testFIFOOrdering,
126
+ testMixedOperations,
127
+ testBatchedPayload
128
+ } from './qos0-tests.js';
129
+
108
130
  // Helper to run a test with error handling and delay
109
131
  async function runTest(testFn) {
110
132
  try {
@@ -128,12 +150,14 @@ if (testFilter === 'help' || testFilter === '--help' || testFilter === '-h') {
128
150
  console.log(' enterprise - Enterprise features (encryption, retention, eviction)');
129
151
  console.log(' bus - Bus mode features (consumer groups, mixed mode)');
130
152
  console.log(' edge - Edge cases (null payloads, large payloads, SQL injection, etc.)');
153
+ console.log(' qos0 - QoS 0 features (buffering, auto-ack, FIFO ordering)');
131
154
  console.log(' advanced - Advanced patterns (pipeline, fan-out, DLQ, circuit breaker)');
132
155
  console.log('\nExamples:');
133
156
  console.log(' node src/test/test-new.js # Run all tests');
134
157
  console.log(' node src/test/test-new.js core # Run only core tests');
135
158
  console.log(' node src/test/test-new.js bus # Run only bus mode tests');
136
159
  console.log(' node src/test/test-new.js edge # Run only edge case tests');
160
+ console.log(' node src/test/test-new.js qos0 # Run only QoS 0 tests');
137
161
  process.exit(0);
138
162
  }
139
163
 
@@ -169,6 +193,16 @@ async function runTests() {
169
193
 
170
194
  // Clean up any existing test data
171
195
  await cleanupTestData();
196
+
197
+ // ============================================
198
+ // HUMAN TESTS
199
+ // ============================================
200
+ if (!testFilter || testFilter === 'human') {
201
+ console.log('\n👤 HUMAN TESTS');
202
+ console.log('-'.repeat(40));
203
+ await runTest(() => testCreateQueue(client));
204
+ await runTest(() => workerQueueTest(client));
205
+ }
172
206
 
173
207
  // ============================================
174
208
  // CORE FEATURE TESTS
@@ -247,6 +281,27 @@ async function runTests() {
247
281
  await runTest(() => testLeaseExpiration(client));
248
282
  await runTest(() => testSQLInjectionPrevention(client));
249
283
  await runTest(() => testXSSPrevention(client));
284
+
285
+ // Partition-scoped transaction_id tests
286
+ await runTest(() => testDuplicateTransactionIdsAcrossPartitions(client));
287
+ await runTest(() => testBatchAckWithDuplicateTransactionIds(client));
288
+ await runTest(() => testDLQWithDuplicateTransactionIds(client));
289
+ await runTest(() => testAnalyticsAPIWithDuplicateTransactionIds(client));
290
+ }
291
+
292
+ // ============================================
293
+ // QOS 0 FEATURE TESTS
294
+ // ============================================
295
+ if (!testFilter || testFilter === 'qos0') {
296
+ console.log('\n⚡ QOS 0 FEATURES');
297
+ console.log('-'.repeat(40));
298
+
299
+ await runTest(() => testQoS0Buffering(client));
300
+ await runTest(() => testAutoAck(client));
301
+ await runTest(() => testConsumerGroupAutoAck(client));
302
+ await runTest(() => testFIFOOrdering(client));
303
+ await runTest(() => testMixedOperations(client));
304
+ await runTest(() => testBatchedPayload(client));
250
305
  }
251
306
 
252
307
  // ============================================
package/package.json CHANGED
@@ -1,32 +1,23 @@
1
1
  {
2
2
  "name": "queen-mq",
3
- "version": "0.2.0",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
- "description": "High-performance message queue backed by PostgreSQL",
6
- "main": "src/client/index.js",
5
+ "description": "High-performance C++ message queue backed by PostgreSQL",
6
+ "main": "client-js/client/index.js",
7
7
  "scripts": {
8
- "start": "node src/server.js",
9
- "start:cluster": "node src/cluster-server.js",
8
+ "start": "./server/bin/queen-server",
10
9
  "build:webapp": "cd webapp && npm install && npm run build",
11
- "copy:webapp": "rm -rf src/webapp-dist && cp -r webapp/dist src/webapp-dist",
12
- "prepublishOnly": "npm run build:webapp && npm run copy:webapp",
13
- "publish:public": "npm run build:webapp && npm run copy:webapp && npm publish --access public",
14
- "test": "node src/test/test-new.js",
15
- "init": "node init-db.js"
10
+ "publish:public": "npm publish --access public",
11
+ "test": "node client-js/test/test-new.js"
16
12
  },
17
13
  "files": [
18
- "src/",
19
- "init-db.js",
14
+ "client-js/",
20
15
  "LICENSE.md",
21
16
  "README.md"
22
17
  ],
23
18
  "dependencies": {
24
19
  "axios": "^1.12.2",
25
- "chart.js": "^4.5.1",
26
- "pg": "^8.13.3",
27
- "uuid": "^13.0.0",
28
- "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0",
29
- "ws": "^8.18.3"
20
+ "uuid": "^13.0.0"
30
21
  },
31
22
  "engines": {
32
23
  "node": ">=22.0.0"
package/init-db.js DELETED
@@ -1,20 +0,0 @@
1
- #!/usr/bin/env node
2
- import { createPool, initDatabase } from './src/database/connection.js';
3
-
4
- const init = async () => {
5
- console.log('Initializing Queen database schema...');
6
-
7
- const pool = createPool();
8
-
9
- try {
10
- await initDatabase(pool);
11
- console.log('✅ Database schema initialized successfully');
12
- } catch (error) {
13
- console.error('❌ Failed to initialize database:', error.message);
14
- process.exit(1);
15
- } finally {
16
- await pool.end();
17
- }
18
- };
19
-
20
- init();