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.
@@ -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.0",
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": "ISC"
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
  }
@@ -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
- // Match: queue[/partition][@group]
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
- queue,
120
- partition,
121
- payload: item,
122
- transactionId: item.transactionId // Optional, for idempotency
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 (partition && partition !== 'Default') {
182
- path = `/api/v1/pop/queue/${queue}/partition/${partition}`;
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
- path = `/api/v1/pop/queue/${queue}`;
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
- yield message;
216
- count++;
217
- if (limit && count >= limit) return;
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
- const transactionId = typeof message === 'object' ?
248
- (message.transactionId || message.id) :
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;