queen-mq 0.1.0 → 0.1.1
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/LICENSE.md +0 -0
- package/README.md +1483 -1052
- package/examples/test-complete-client.js +260 -0
- package/examples/test-traceid.js +147 -0
- package/package.json +14 -3
- package/src/client/client.js +113 -20
- package/src/test/MIGRATION_ISSUES.md +174 -0
- package/src/test/README.md +203 -0
- package/src/test/advanced-pattern-tests.js +1137 -0
- package/src/test/bus-mode-tests.js +348 -0
- package/src/test/core-tests.js +342 -0
- package/src/test/edge-case-tests.js +554 -0
- package/src/test/enterprise-tests.js +604 -0
- package/src/test/partition-locking-tests.js +545 -0
- package/src/test/test-new.js +278 -0
- package/src/test/test.js +6 -3
- package/src/test/utils.js +169 -0
- package/API.md +0 -1116
- package/CACHE.md +0 -519
- package/DASHBOARD-V3.md +0 -478
- package/DASHBOARD.md +0 -382
- package/MOD_QUEUE.md +0 -453
- package/PARTITION_LOCKING_DESIGN.md +0 -989
- package/PLAN.md +0 -707
- package/QUERY_ANALSYS.md +0 -72
- package/QUEUE_BUS.md +0 -334
- package/V2-PLAN.md +0 -236
- package/debug-namespace.js +0 -110
- package/docs/long-polling.md +0 -159
- package/docs/multi-server-cache-solutions.md +0 -185
- package/docs/performance-tuning.md +0 -222
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Complete test of the minimalist Queen client
|
|
5
|
+
* Shows all features including namespace/task and message properties
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Queen } from '../src/client/client.js';
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
console.log('🧪 Testing Complete Minimalist Queen Client Features\n');
|
|
12
|
+
|
|
13
|
+
const client = new Queen({
|
|
14
|
+
baseUrls: ['http://localhost:6632'],
|
|
15
|
+
timeout: 30000
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// ============================================
|
|
20
|
+
// 1. Queue Configuration with Namespace/Task
|
|
21
|
+
// ============================================
|
|
22
|
+
console.log('1️⃣ Configuring queues with namespace and task...');
|
|
23
|
+
|
|
24
|
+
// Configure queue with namespace and task
|
|
25
|
+
await client.queue('orders-processing', {
|
|
26
|
+
leaseTime: 300,
|
|
27
|
+
retryLimit: 3,
|
|
28
|
+
priority: 10
|
|
29
|
+
}, {
|
|
30
|
+
namespace: 'ecommerce',
|
|
31
|
+
task: 'process'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await client.queue('inventory-updates', {
|
|
35
|
+
leaseTime: 300,
|
|
36
|
+
priority: 5
|
|
37
|
+
}, {
|
|
38
|
+
namespace: 'ecommerce',
|
|
39
|
+
task: 'update'
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await client.queue('notifications', {
|
|
43
|
+
priority: 8
|
|
44
|
+
}, {
|
|
45
|
+
namespace: 'ecommerce',
|
|
46
|
+
task: 'notify'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
console.log('✅ Queues configured with namespace/task metadata\n');
|
|
50
|
+
|
|
51
|
+
// ============================================
|
|
52
|
+
// 2. Push with Message Properties
|
|
53
|
+
// ============================================
|
|
54
|
+
console.log('2️⃣ Pushing messages with properties...');
|
|
55
|
+
|
|
56
|
+
// Method 1: Properties in options
|
|
57
|
+
await client.push('orders-processing', {
|
|
58
|
+
orderId: 123,
|
|
59
|
+
amount: 99.99
|
|
60
|
+
}, {
|
|
61
|
+
transactionId: 'txn-123'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Method 2: Properties in the message object
|
|
65
|
+
await client.push('orders-processing', {
|
|
66
|
+
orderId: 124,
|
|
67
|
+
amount: 199.99,
|
|
68
|
+
transactionId: 'txn-124' // Will be extracted
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Method 3: Batch with mixed properties
|
|
72
|
+
await client.push('inventory-updates', [
|
|
73
|
+
{
|
|
74
|
+
sku: 'PROD-001',
|
|
75
|
+
quantity: 5,
|
|
76
|
+
transactionId: 'inv-001'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
sku: 'PROD-002',
|
|
80
|
+
quantity: 10,
|
|
81
|
+
transactionId: 'inv-002'
|
|
82
|
+
}
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
// Method 4: Using special properties format
|
|
86
|
+
await client.push('notifications', {
|
|
87
|
+
_payload: { userId: 789, message: 'Order shipped' },
|
|
88
|
+
_transactionId: 'notif-001'
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log('✅ Messages pushed with various property patterns\n');
|
|
92
|
+
|
|
93
|
+
// ============================================
|
|
94
|
+
// 3. Take by Queue/Partition
|
|
95
|
+
// ============================================
|
|
96
|
+
console.log('3️⃣ Taking messages by queue/partition...');
|
|
97
|
+
|
|
98
|
+
// Take from specific queue
|
|
99
|
+
let count = 0;
|
|
100
|
+
for await (const msg of client.take('orders-processing', { limit: 2 })) {
|
|
101
|
+
count++;
|
|
102
|
+
console.log(` 📩 Order ${count}:`, msg.data);
|
|
103
|
+
console.log(` Transaction ID: ${msg.transactionId}`);
|
|
104
|
+
await client.ack(msg);
|
|
105
|
+
}
|
|
106
|
+
console.log('✅ Processed orders\n');
|
|
107
|
+
|
|
108
|
+
// ============================================
|
|
109
|
+
// 4. Take by Namespace/Task
|
|
110
|
+
// ============================================
|
|
111
|
+
console.log('4️⃣ Taking messages by namespace/task...');
|
|
112
|
+
|
|
113
|
+
// Take all messages from ecommerce namespace
|
|
114
|
+
count = 0;
|
|
115
|
+
for await (const msg of client.take('namespace:ecommerce', { limit: 2 })) {
|
|
116
|
+
count++;
|
|
117
|
+
console.log(` 📦 Namespace message ${count} from ${msg.queue}:`, msg.data);
|
|
118
|
+
await client.ack(msg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Take only 'update' tasks
|
|
122
|
+
for await (const msg of client.take('task:update', { limit: 2 })) {
|
|
123
|
+
console.log(` 🔄 Update task from ${msg.queue}:`, msg.data);
|
|
124
|
+
await client.ack(msg);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Combine namespace and task
|
|
128
|
+
for await (const msg of client.take('namespace:ecommerce/task:notify', { limit: 1 })) {
|
|
129
|
+
console.log(` 🔔 Notification:`, msg.data);
|
|
130
|
+
await client.ack(msg);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('✅ Processed messages by namespace/task\n');
|
|
134
|
+
|
|
135
|
+
// ============================================
|
|
136
|
+
// 5. Consumer Groups with Subscription Modes
|
|
137
|
+
// ============================================
|
|
138
|
+
console.log('5️⃣ Testing consumer groups with subscription modes...');
|
|
139
|
+
|
|
140
|
+
// Configure events queue first
|
|
141
|
+
await client.queue('events', {
|
|
142
|
+
leaseTime: 300,
|
|
143
|
+
retryLimit: 3
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Push test events
|
|
147
|
+
await client.push('events', [
|
|
148
|
+
{ event: 'user.signup', userId: 1 },
|
|
149
|
+
{ event: 'user.login', userId: 2 },
|
|
150
|
+
{ event: 'user.purchase', userId: 3 }
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
// Consumer group starting from all messages
|
|
154
|
+
for await (const event of client.take('events@analytics', {
|
|
155
|
+
limit: 1,
|
|
156
|
+
subscriptionMode: 'all'
|
|
157
|
+
})) {
|
|
158
|
+
console.log(' 📊 Analytics (all):', event.data);
|
|
159
|
+
await client.ack(event, true, { group: 'analytics' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Consumer group starting from new messages only
|
|
163
|
+
for await (const event of client.take('events@realtime', {
|
|
164
|
+
limit: 1,
|
|
165
|
+
subscriptionMode: 'new'
|
|
166
|
+
})) {
|
|
167
|
+
console.log(' ⚡ Realtime (new only):', event.data);
|
|
168
|
+
await client.ack(event, true, { group: 'realtime' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log('✅ Consumer groups tested\n');
|
|
172
|
+
|
|
173
|
+
// ============================================
|
|
174
|
+
// 6. Advanced Patterns
|
|
175
|
+
// ============================================
|
|
176
|
+
console.log('6️⃣ Testing advanced patterns...');
|
|
177
|
+
|
|
178
|
+
// Configure tasks queue
|
|
179
|
+
await client.queue('tasks', {
|
|
180
|
+
leaseTime: 300,
|
|
181
|
+
priority: 8
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Pattern 1: Partitioned messages with properties
|
|
185
|
+
await client.push('tasks/urgent', {
|
|
186
|
+
taskId: 'TASK-001',
|
|
187
|
+
priority: 'critical'
|
|
188
|
+
}, {
|
|
189
|
+
transactionId: 'task-txn-001'
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Pattern 2: Consumer group on specific partition
|
|
193
|
+
for await (const task of client.take('tasks/urgent@workers', { limit: 1 })) {
|
|
194
|
+
console.log(' 🚨 Urgent task for worker group:', task.data);
|
|
195
|
+
await client.ack(task, true, { group: 'workers' });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Configure logs queue
|
|
199
|
+
await client.queue('logs', {
|
|
200
|
+
leaseTime: 300,
|
|
201
|
+
retentionSeconds: 86400 // Keep for 1 day
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Pattern 3: Batch push with partition
|
|
205
|
+
await client.push('logs/audit', [
|
|
206
|
+
{ action: 'login', user: 'alice', timestamp: Date.now() },
|
|
207
|
+
{ action: 'update', user: 'bob', timestamp: Date.now() },
|
|
208
|
+
{ action: 'delete', user: 'charlie', timestamp: Date.now() }
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
console.log('✅ Advanced patterns tested\n');
|
|
212
|
+
|
|
213
|
+
// ============================================
|
|
214
|
+
// 7. Error Handling
|
|
215
|
+
// ============================================
|
|
216
|
+
console.log('7️⃣ Testing error handling...');
|
|
217
|
+
|
|
218
|
+
// Configure error-test queue
|
|
219
|
+
await client.queue('error-test', {
|
|
220
|
+
leaseTime: 60,
|
|
221
|
+
retryLimit: 2
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Push a message that will fail processing
|
|
225
|
+
await client.push('error-test', {
|
|
226
|
+
willFail: true,
|
|
227
|
+
reason: 'Testing failure path'
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
for await (const msg of client.take('error-test', { limit: 1 })) {
|
|
231
|
+
try {
|
|
232
|
+
// Simulate processing error
|
|
233
|
+
throw new Error('Simulated processing error');
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.log(' ❌ Failed to process:', msg.data);
|
|
236
|
+
// Acknowledge with failure and error context
|
|
237
|
+
await client.ack(msg, false, {
|
|
238
|
+
error: error.message,
|
|
239
|
+
retryable: true
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log('✅ Error handling tested\n');
|
|
245
|
+
|
|
246
|
+
console.log('🎉 All features tested successfully!');
|
|
247
|
+
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error('❌ Test failed:', error.message);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
} finally {
|
|
252
|
+
await client.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Run tests
|
|
257
|
+
main().catch(error => {
|
|
258
|
+
console.error('Fatal error:', error);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test client-provided traceId functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Queen } from '../src/client/client.js';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
|
|
10
|
+
// Helper to generate a valid UUID v4
|
|
11
|
+
function generateUUID() {
|
|
12
|
+
return crypto.randomUUID();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function main() {
|
|
16
|
+
console.log('🧪 Testing Client-Provided TraceId\n');
|
|
17
|
+
|
|
18
|
+
const client = new Queen({
|
|
19
|
+
baseUrls: ['http://localhost:6632'],
|
|
20
|
+
timeout: 30000
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Configure test queue
|
|
25
|
+
await client.queue('trace-test', {
|
|
26
|
+
leaseTime: 300,
|
|
27
|
+
retryLimit: 3
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Test 1: Push with valid UUID traceId
|
|
31
|
+
console.log('1️⃣ Testing valid UUID traceId...');
|
|
32
|
+
const validTraceId = generateUUID();
|
|
33
|
+
console.log(` Generated UUID: ${validTraceId}`);
|
|
34
|
+
|
|
35
|
+
await client.push('trace-test', {
|
|
36
|
+
message: 'Test with valid traceId'
|
|
37
|
+
}, {
|
|
38
|
+
transactionId: 'txn-001',
|
|
39
|
+
traceId: validTraceId
|
|
40
|
+
});
|
|
41
|
+
console.log('✅ Message pushed with valid traceId\n');
|
|
42
|
+
|
|
43
|
+
// Test 2: Push with invalid traceId (should be ignored)
|
|
44
|
+
console.log('2️⃣ Testing invalid traceId (should be ignored)...');
|
|
45
|
+
const invalidTraceId = 'not-a-valid-uuid';
|
|
46
|
+
console.log(` Invalid traceId: ${invalidTraceId}`);
|
|
47
|
+
|
|
48
|
+
await client.push('trace-test', {
|
|
49
|
+
message: 'Test with invalid traceId'
|
|
50
|
+
}, {
|
|
51
|
+
transactionId: 'txn-002',
|
|
52
|
+
traceId: invalidTraceId // Will be ignored, server will generate one
|
|
53
|
+
});
|
|
54
|
+
console.log('✅ Message pushed (invalid traceId ignored)\n');
|
|
55
|
+
|
|
56
|
+
// Test 3: Push without traceId (server generates)
|
|
57
|
+
console.log('3️⃣ Testing without traceId (server generates)...');
|
|
58
|
+
await client.push('trace-test', {
|
|
59
|
+
message: 'Test without traceId'
|
|
60
|
+
}, {
|
|
61
|
+
transactionId: 'txn-003'
|
|
62
|
+
});
|
|
63
|
+
console.log('✅ Message pushed (server generated traceId)\n');
|
|
64
|
+
|
|
65
|
+
// Test 4: Using special properties format
|
|
66
|
+
console.log('4️⃣ Testing special properties format...');
|
|
67
|
+
const anotherValidTraceId = generateUUID();
|
|
68
|
+
|
|
69
|
+
await client.push('trace-test', {
|
|
70
|
+
_payload: { message: 'Special format test' },
|
|
71
|
+
_transactionId: 'txn-004',
|
|
72
|
+
_traceId: anotherValidTraceId
|
|
73
|
+
});
|
|
74
|
+
console.log('✅ Message pushed with special format\n');
|
|
75
|
+
|
|
76
|
+
// Test 5: Batch with mixed traceIds
|
|
77
|
+
console.log('5️⃣ Testing batch with mixed traceIds...');
|
|
78
|
+
const batchTraceId1 = generateUUID();
|
|
79
|
+
const batchTraceId2 = generateUUID();
|
|
80
|
+
|
|
81
|
+
await client.push('trace-test', [
|
|
82
|
+
{
|
|
83
|
+
message: 'Batch message 1',
|
|
84
|
+
transactionId: 'batch-001',
|
|
85
|
+
traceId: batchTraceId1 // Valid UUID
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
message: 'Batch message 2',
|
|
89
|
+
transactionId: 'batch-002',
|
|
90
|
+
traceId: 'invalid-trace' // Invalid, will be ignored
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
message: 'Batch message 3',
|
|
94
|
+
transactionId: 'batch-003'
|
|
95
|
+
// No traceId, server will generate
|
|
96
|
+
}
|
|
97
|
+
]);
|
|
98
|
+
console.log('✅ Batch pushed with mixed traceId scenarios\n');
|
|
99
|
+
|
|
100
|
+
// Retrieve and display messages
|
|
101
|
+
console.log('📥 Retrieving messages to verify traceIds...\n');
|
|
102
|
+
let count = 0;
|
|
103
|
+
for await (const msg of client.take('trace-test', { limit: 7 })) {
|
|
104
|
+
count++;
|
|
105
|
+
console.log(`Message ${count}:`);
|
|
106
|
+
console.log(` Transaction ID: ${msg.transactionId}`);
|
|
107
|
+
console.log(` Trace ID: ${msg.traceId}`);
|
|
108
|
+
console.log(` Data: ${JSON.stringify(msg.data)}`);
|
|
109
|
+
|
|
110
|
+
// Check if our valid traceIds were preserved
|
|
111
|
+
if (msg.transactionId === 'txn-001') {
|
|
112
|
+
if (msg.traceId === validTraceId) {
|
|
113
|
+
console.log(' ✅ Client-provided traceId was preserved!');
|
|
114
|
+
} else {
|
|
115
|
+
console.log(' ❌ Client-provided traceId was not preserved');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (msg.transactionId === 'txn-002') {
|
|
120
|
+
if (msg.traceId !== invalidTraceId) {
|
|
121
|
+
console.log(' ✅ Invalid traceId was correctly replaced');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await client.ack(msg);
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('🎉 TraceId test completed successfully!');
|
|
130
|
+
console.log('\nSummary:');
|
|
131
|
+
console.log('- Valid UUID traceIds are accepted and preserved');
|
|
132
|
+
console.log('- Invalid traceIds are ignored (server generates new ones)');
|
|
133
|
+
console.log('- Missing traceIds are auto-generated by server');
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('❌ Test failed:', error.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
} finally {
|
|
139
|
+
await client.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Run test
|
|
144
|
+
main().catch(error => {
|
|
145
|
+
console.error('Fatal error:', error);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "queen-mq",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "High-performance message queue backed by PostgreSQL",
|
|
6
6
|
"main": "src/client/index.js",
|
|
@@ -31,6 +31,17 @@
|
|
|
31
31
|
"engines": {
|
|
32
32
|
"node": ">=22.0.0"
|
|
33
33
|
},
|
|
34
|
-
"author": "",
|
|
35
|
-
"license": "
|
|
34
|
+
"author": "Smartness",
|
|
35
|
+
"license": "Apache-2.0",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/smartpricing/queen"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"message-queue",
|
|
42
|
+
"queue",
|
|
43
|
+
"broker",
|
|
44
|
+
"message-queue-system",
|
|
45
|
+
"fifo"
|
|
46
|
+
]
|
|
36
47
|
}
|
package/src/client/client.js
CHANGED
|
@@ -55,6 +55,14 @@ export class Queen {
|
|
|
55
55
|
this.#connected = true;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Validate if a string is a valid UUID v4
|
|
60
|
+
*/
|
|
61
|
+
#isValidUUID(str) {
|
|
62
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
63
|
+
return uuidRegex.test(str);
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
/**
|
|
59
67
|
* Parse address string into components
|
|
60
68
|
* Examples:
|
|
@@ -62,9 +70,37 @@ export class Queen {
|
|
|
62
70
|
* - "myqueue/urgent" -> { queue: "myqueue", partition: "urgent" }
|
|
63
71
|
* - "myqueue@workers" -> { queue: "myqueue", consumerGroup: "workers" }
|
|
64
72
|
* - "myqueue/urgent@workers" -> { queue: "myqueue", partition: "urgent", consumerGroup: "workers" }
|
|
73
|
+
* - "namespace:billing" -> { namespace: "billing" }
|
|
74
|
+
* - "task:process" -> { task: "process" }
|
|
75
|
+
* - "namespace:billing/task:process" -> { namespace: "billing", task: "process" }
|
|
65
76
|
*/
|
|
66
77
|
#parseAddress(address) {
|
|
67
|
-
//
|
|
78
|
+
// Check for namespace/task pattern
|
|
79
|
+
if (address.includes('namespace:') || address.includes('task:')) {
|
|
80
|
+
const parts = {};
|
|
81
|
+
|
|
82
|
+
// Extract consumer group if present (after @)
|
|
83
|
+
let workingAddress = address;
|
|
84
|
+
const atIndex = address.lastIndexOf('@');
|
|
85
|
+
if (atIndex > 0) {
|
|
86
|
+
parts.consumerGroup = address.substring(atIndex + 1);
|
|
87
|
+
workingAddress = address.substring(0, atIndex);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const segments = workingAddress.split('/');
|
|
91
|
+
|
|
92
|
+
for (const segment of segments) {
|
|
93
|
+
if (segment.startsWith('namespace:')) {
|
|
94
|
+
parts.namespace = segment.substring(10);
|
|
95
|
+
} else if (segment.startsWith('task:')) {
|
|
96
|
+
parts.task = segment.substring(5);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Standard queue[/partition][@group] pattern
|
|
68
104
|
const match = address.match(/^([^/@]+)(?:\/([^@]+))?(?:@(.+))?$/);
|
|
69
105
|
if (!match) {
|
|
70
106
|
throw new Error(`Invalid address format: ${address}`);
|
|
@@ -81,13 +117,18 @@ export class Queen {
|
|
|
81
117
|
* Configure a queue with options
|
|
82
118
|
* @param {string} name - Queue name
|
|
83
119
|
* @param {Object} options - Queue configuration options
|
|
120
|
+
* @param {Object} metadata - Optional metadata { namespace, task }
|
|
84
121
|
*/
|
|
85
|
-
async queue(name, options = {}) {
|
|
122
|
+
async queue(name, options = {}, metadata = {}) {
|
|
86
123
|
await this.#ensureConnected();
|
|
87
124
|
|
|
125
|
+
const { namespace, task } = metadata;
|
|
126
|
+
|
|
88
127
|
const result = await withRetry(
|
|
89
128
|
() => this.#http.post('/api/v1/configure', {
|
|
90
129
|
queue: name,
|
|
130
|
+
namespace: namespace || null,
|
|
131
|
+
task: task || null,
|
|
91
132
|
options
|
|
92
133
|
}),
|
|
93
134
|
this.#config.retryAttempts,
|
|
@@ -105,8 +146,9 @@ export class Queen {
|
|
|
105
146
|
* Push messages to a queue
|
|
106
147
|
* @param {string} address - Queue address (e.g., "myqueue" or "myqueue/partition")
|
|
107
148
|
* @param {Object|Array} payload - Single message or array of messages
|
|
149
|
+
* @param {Object} options - Optional message properties { transactionId, traceId }
|
|
108
150
|
*/
|
|
109
|
-
async push(address, payload) {
|
|
151
|
+
async push(address, payload, options = {}) {
|
|
110
152
|
await this.#ensureConnected();
|
|
111
153
|
|
|
112
154
|
const { queue, partition } = this.#parseAddress(address);
|
|
@@ -115,12 +157,44 @@ export class Queen {
|
|
|
115
157
|
const items = Array.isArray(payload) ? payload : [payload];
|
|
116
158
|
|
|
117
159
|
// Format items for the API
|
|
118
|
-
const formattedItems = items.map(item =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
160
|
+
const formattedItems = items.map(item => {
|
|
161
|
+
// If item is an object with special properties, extract them
|
|
162
|
+
const isMessageObject = typeof item === 'object' && item !== null &&
|
|
163
|
+
(item._payload || item._transactionId || item._traceId);
|
|
164
|
+
|
|
165
|
+
if (isMessageObject) {
|
|
166
|
+
const result = {
|
|
167
|
+
queue,
|
|
168
|
+
partition,
|
|
169
|
+
payload: item._payload || item,
|
|
170
|
+
transactionId: item._transactionId || item.transactionId
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Include traceId if provided and valid UUID
|
|
174
|
+
const traceId = item._traceId || item.traceId;
|
|
175
|
+
if (traceId && this.#isValidUUID(traceId)) {
|
|
176
|
+
result.traceId = traceId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Otherwise use the item as payload and apply options
|
|
183
|
+
const result = {
|
|
184
|
+
queue,
|
|
185
|
+
partition,
|
|
186
|
+
payload: item,
|
|
187
|
+
transactionId: options.transactionId || (item && typeof item === 'object' ? item.transactionId : undefined)
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Include traceId if provided and valid UUID
|
|
191
|
+
const traceId = options.traceId || (item && typeof item === 'object' ? item.traceId : undefined);
|
|
192
|
+
if (traceId && this.#isValidUUID(traceId)) {
|
|
193
|
+
result.traceId = traceId;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
});
|
|
124
198
|
|
|
125
199
|
const result = await withRetry(
|
|
126
200
|
() => this.#http.post('/api/v1/push', {
|
|
@@ -146,7 +220,7 @@ export class Queen {
|
|
|
146
220
|
async *take(address, options = {}) {
|
|
147
221
|
await this.#ensureConnected();
|
|
148
222
|
|
|
149
|
-
const { queue, partition, consumerGroup } = this.#parseAddress(address);
|
|
223
|
+
const { queue, partition, consumerGroup, namespace, task } = this.#parseAddress(address);
|
|
150
224
|
const {
|
|
151
225
|
limit = null,
|
|
152
226
|
batch = 1,
|
|
@@ -178,10 +252,19 @@ export class Queen {
|
|
|
178
252
|
if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
|
|
179
253
|
|
|
180
254
|
// Determine the endpoint based on parameters
|
|
181
|
-
if (
|
|
182
|
-
|
|
255
|
+
if (queue) {
|
|
256
|
+
if (partition && partition !== 'Default') {
|
|
257
|
+
path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
|
|
258
|
+
} else {
|
|
259
|
+
path = `/api/v1/pop/queue/${queue}`;
|
|
260
|
+
}
|
|
261
|
+
} else if (namespace || task) {
|
|
262
|
+
// Pop by namespace/task
|
|
263
|
+
path = '/api/v1/pop';
|
|
264
|
+
if (namespace) params.append('namespace', namespace);
|
|
265
|
+
if (task) params.append('task', task);
|
|
183
266
|
} else {
|
|
184
|
-
|
|
267
|
+
throw new Error('Must specify either queue, namespace, or task');
|
|
185
268
|
}
|
|
186
269
|
|
|
187
270
|
try {
|
|
@@ -210,11 +293,13 @@ export class Queen {
|
|
|
210
293
|
// Reset empty counter on successful fetch
|
|
211
294
|
consecutiveEmptyResponses = 0;
|
|
212
295
|
|
|
213
|
-
// Yield each message
|
|
296
|
+
// Yield each message (filter out any null/undefined values)
|
|
214
297
|
for (const message of result.messages) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
298
|
+
if (message) { // Skip null/undefined messages
|
|
299
|
+
yield message;
|
|
300
|
+
count++;
|
|
301
|
+
if (limit && count >= limit) return;
|
|
302
|
+
}
|
|
218
303
|
}
|
|
219
304
|
|
|
220
305
|
} catch (error) {
|
|
@@ -244,9 +329,17 @@ export class Queen {
|
|
|
244
329
|
await this.#ensureConnected();
|
|
245
330
|
|
|
246
331
|
// Extract transaction ID
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
message;
|
|
332
|
+
let transactionId;
|
|
333
|
+
if (typeof message === 'string') {
|
|
334
|
+
transactionId = message;
|
|
335
|
+
} else if (typeof message === 'object' && message !== null) {
|
|
336
|
+
transactionId = message.transactionId || message.id;
|
|
337
|
+
if (!transactionId) {
|
|
338
|
+
throw new Error('Message object must have transactionId or id property');
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
throw new Error('Message must be a string (transaction ID) or object with transactionId');
|
|
342
|
+
}
|
|
250
343
|
|
|
251
344
|
// Determine status string
|
|
252
345
|
let statusStr;
|