queen-mq 0.7.4 → 0.12.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.
@@ -10,6 +10,7 @@ import { QueueBuilder } from './builders/QueueBuilder.js'
10
10
  import { TransactionBuilder } from './builders/TransactionBuilder.js'
11
11
  import { StreamBuilder } from './stream/StreamBuilder.js'
12
12
  import { StreamConsumer } from './stream/StreamConsumer.js'
13
+ import { Admin } from './admin/Admin.js'
13
14
  import { CLIENT_DEFAULTS } from './utils/defaults.js'
14
15
  import { validateUrl, validateUrls } from './utils/validation.js'
15
16
  import * as logger from './utils/logger.js'
@@ -19,6 +20,7 @@ export class Queen {
19
20
  #bufferManager
20
21
  #config
21
22
  #shutdownHandlers = []
23
+ #admin = null
22
24
 
23
25
  constructor(config = {}) {
24
26
  logger.log('Queen.constructor', { config: typeof config === 'object' && !Array.isArray(config) ? { ...config, urls: config.urls?.length || 0 } : { type: typeof config } })
@@ -75,7 +77,7 @@ export class Queen {
75
77
  }
76
78
 
77
79
  #createHttpClient() {
78
- const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, affinityHashRing, healthRetryAfterMillis, enableFailover } = this.#config
80
+ const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, affinityHashRing, healthRetryAfterMillis, enableFailover, bearerToken } = this.#config
79
81
 
80
82
  if (urls.length === 1) {
81
83
  // Single server
@@ -83,7 +85,8 @@ export class Queen {
83
85
  baseUrl: urls[0],
84
86
  timeoutMillis,
85
87
  retryAttempts,
86
- retryDelayMillis
88
+ retryDelayMillis,
89
+ bearerToken
87
90
  })
88
91
  }
89
92
 
@@ -97,11 +100,17 @@ export class Queen {
97
100
  timeoutMillis,
98
101
  retryAttempts,
99
102
  retryDelayMillis,
100
- enableFailover
103
+ enableFailover,
104
+ bearerToken
101
105
  })
102
106
  }
103
107
 
104
108
  #setupGracefulShutdown() {
109
+ // Skip signal handlers in browser environment
110
+ if (typeof process === 'undefined' || typeof process.on !== 'function') {
111
+ return
112
+ }
113
+
105
114
  let signalReceivedCount = 0;
106
115
  const shutdown = async (signal) => {
107
116
  console.log(`\nReceived ${signal}, shutting down gracefully...`)
@@ -140,6 +149,21 @@ export class Queen {
140
149
  return new QueueBuilder(this, this.#httpClient, this.#bufferManager, name)
141
150
  }
142
151
 
152
+ // ===========================
153
+ // Admin API Entry Point
154
+ // ===========================
155
+
156
+ /**
157
+ * Get the Admin API for administrative and observability operations
158
+ * @returns {Admin} Admin API instance (lazily initialized, singleton)
159
+ */
160
+ get admin() {
161
+ if (!this.#admin) {
162
+ this.#admin = new Admin(this.#httpClient)
163
+ }
164
+ return this.#admin
165
+ }
166
+
143
167
  // ===========================
144
168
  // Transaction API
145
169
  // ===========================
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Admin API for Queen - Administrative and observability endpoints
3
+ * These APIs are typically used by dashboards and admin tools, not regular applications
4
+ */
5
+
6
+ import * as logger from '../utils/logger.js'
7
+
8
+ export class Admin {
9
+ #httpClient
10
+
11
+ constructor(httpClient) {
12
+ this.#httpClient = httpClient
13
+ }
14
+
15
+ // ===========================
16
+ // Resources API
17
+ // ===========================
18
+
19
+ /**
20
+ * Get system overview with queue counts, message stats, etc.
21
+ * @returns {Promise<object>}
22
+ */
23
+ async getOverview() {
24
+ logger.log('Admin.getOverview', {})
25
+ return this.#httpClient.get('/api/v1/resources/overview')
26
+ }
27
+
28
+ /**
29
+ * Get all namespaces
30
+ * @returns {Promise<object>}
31
+ */
32
+ async getNamespaces() {
33
+ logger.log('Admin.getNamespaces', {})
34
+ return this.#httpClient.get('/api/v1/resources/namespaces')
35
+ }
36
+
37
+ /**
38
+ * Get all tasks
39
+ * @returns {Promise<object>}
40
+ */
41
+ async getTasks() {
42
+ logger.log('Admin.getTasks', {})
43
+ return this.#httpClient.get('/api/v1/resources/tasks')
44
+ }
45
+
46
+ // ===========================
47
+ // Queues API
48
+ // ===========================
49
+
50
+ /**
51
+ * List all queues
52
+ * @param {object} params - Query parameters (limit, offset, search, etc.)
53
+ * @returns {Promise<object>}
54
+ */
55
+ async listQueues(params = {}) {
56
+ logger.log('Admin.listQueues', { params })
57
+ const queryString = this.#buildQueryString(params)
58
+ return this.#httpClient.get(`/api/v1/resources/queues${queryString}`)
59
+ }
60
+
61
+ /**
62
+ * Get queue details
63
+ * @param {string} name - Queue name
64
+ * @returns {Promise<object>}
65
+ */
66
+ async getQueue(name) {
67
+ logger.log('Admin.getQueue', { name })
68
+ return this.#httpClient.get(`/api/v1/resources/queues/${encodeURIComponent(name)}`)
69
+ }
70
+
71
+ /**
72
+ * Clear all messages from a queue
73
+ * @param {string} name - Queue name
74
+ * @param {string} [partition] - Optional partition to clear
75
+ * @returns {Promise<object>}
76
+ */
77
+ async clearQueue(name, partition = null) {
78
+ logger.log('Admin.clearQueue', { name, partition })
79
+ const queryString = partition ? `?partition=${encodeURIComponent(partition)}` : ''
80
+ return this.#httpClient.delete(`/api/v1/queues/${encodeURIComponent(name)}/clear${queryString}`)
81
+ }
82
+
83
+ /**
84
+ * Get all partitions
85
+ * @param {object} params - Query parameters (queue, limit, etc.)
86
+ * @returns {Promise<object>}
87
+ */
88
+ async getPartitions(params = {}) {
89
+ logger.log('Admin.getPartitions', { params })
90
+ const queryString = this.#buildQueryString(params)
91
+ return this.#httpClient.get(`/api/v1/resources/partitions${queryString}`)
92
+ }
93
+
94
+ // ===========================
95
+ // Messages API
96
+ // ===========================
97
+
98
+ /**
99
+ * List messages with filters
100
+ * @param {object} params - Query parameters (queue, partition, status, limit, offset, etc.)
101
+ * @returns {Promise<object>}
102
+ */
103
+ async listMessages(params = {}) {
104
+ logger.log('Admin.listMessages', { params })
105
+ const queryString = this.#buildQueryString(params)
106
+ return this.#httpClient.get(`/api/v1/messages${queryString}`)
107
+ }
108
+
109
+ /**
110
+ * Get a specific message
111
+ * @param {string} partitionId - Partition ID
112
+ * @param {string} transactionId - Transaction ID
113
+ * @returns {Promise<object>}
114
+ */
115
+ async getMessage(partitionId, transactionId) {
116
+ logger.log('Admin.getMessage', { partitionId, transactionId })
117
+ return this.#httpClient.get(`/api/v1/messages/${partitionId}/${transactionId}`)
118
+ }
119
+
120
+ /**
121
+ * Delete a specific message
122
+ * @param {string} partitionId - Partition ID
123
+ * @param {string} transactionId - Transaction ID
124
+ * @returns {Promise<object>}
125
+ */
126
+ async deleteMessage(partitionId, transactionId) {
127
+ logger.log('Admin.deleteMessage', { partitionId, transactionId })
128
+ return this.#httpClient.delete(`/api/v1/messages/${partitionId}/${transactionId}`)
129
+ }
130
+
131
+ /**
132
+ * Retry a failed message
133
+ * @param {string} partitionId - Partition ID
134
+ * @param {string} transactionId - Transaction ID
135
+ * @returns {Promise<object>}
136
+ */
137
+ async retryMessage(partitionId, transactionId) {
138
+ logger.log('Admin.retryMessage', { partitionId, transactionId })
139
+ return this.#httpClient.post(`/api/v1/messages/${partitionId}/${transactionId}/retry`, {})
140
+ }
141
+
142
+ /**
143
+ * Move a message to the Dead Letter Queue
144
+ * @param {string} partitionId - Partition ID
145
+ * @param {string} transactionId - Transaction ID
146
+ * @returns {Promise<object>}
147
+ */
148
+ async moveMessageToDLQ(partitionId, transactionId) {
149
+ logger.log('Admin.moveMessageToDLQ', { partitionId, transactionId })
150
+ return this.#httpClient.post(`/api/v1/messages/${partitionId}/${transactionId}/dlq`, {})
151
+ }
152
+
153
+ // ===========================
154
+ // Traces API
155
+ // ===========================
156
+
157
+ /**
158
+ * Get traces by name
159
+ * @param {string} traceName - Trace name to search for
160
+ * @param {object} params - Query parameters (limit, offset, from, to, etc.)
161
+ * @returns {Promise<object>}
162
+ */
163
+ async getTracesByName(traceName, params = {}) {
164
+ logger.log('Admin.getTracesByName', { traceName, params })
165
+ const queryString = this.#buildQueryString(params)
166
+ return this.#httpClient.get(`/api/v1/traces/by-name/${encodeURIComponent(traceName)}${queryString}`)
167
+ }
168
+
169
+ /**
170
+ * Get available trace names
171
+ * @param {object} params - Query parameters (limit, search, etc.)
172
+ * @returns {Promise<object>}
173
+ */
174
+ async getTraceNames(params = {}) {
175
+ logger.log('Admin.getTraceNames', { params })
176
+ const queryString = this.#buildQueryString(params)
177
+ return this.#httpClient.get(`/api/v1/traces/names${queryString}`)
178
+ }
179
+
180
+ /**
181
+ * Get traces for a specific message
182
+ * @param {string} partitionId - Partition ID
183
+ * @param {string} transactionId - Transaction ID
184
+ * @returns {Promise<object>}
185
+ */
186
+ async getTracesForMessage(partitionId, transactionId) {
187
+ logger.log('Admin.getTracesForMessage', { partitionId, transactionId })
188
+ return this.#httpClient.get(`/api/v1/traces/${partitionId}/${transactionId}`)
189
+ }
190
+
191
+ // ===========================
192
+ // Analytics/Status API
193
+ // ===========================
194
+
195
+ /**
196
+ * Get system status
197
+ * @param {object} params - Query parameters
198
+ * @returns {Promise<object>}
199
+ */
200
+ async getStatus(params = {}) {
201
+ logger.log('Admin.getStatus', { params })
202
+ const queryString = this.#buildQueryString(params)
203
+ return this.#httpClient.get(`/api/v1/status${queryString}`)
204
+ }
205
+
206
+ /**
207
+ * Get queue statistics
208
+ * @param {object} params - Query parameters (limit, offset, etc.)
209
+ * @returns {Promise<object>}
210
+ */
211
+ async getQueueStats(params = {}) {
212
+ logger.log('Admin.getQueueStats', { params })
213
+ const queryString = this.#buildQueryString(params)
214
+ return this.#httpClient.get(`/api/v1/status/queues${queryString}`)
215
+ }
216
+
217
+ /**
218
+ * Get detailed statistics for a specific queue
219
+ * @param {string} name - Queue name
220
+ * @param {object} params - Query parameters
221
+ * @returns {Promise<object>}
222
+ */
223
+ async getQueueDetail(name, params = {}) {
224
+ logger.log('Admin.getQueueDetail', { name, params })
225
+ const queryString = this.#buildQueryString(params)
226
+ return this.#httpClient.get(`/api/v1/status/queues/${encodeURIComponent(name)}${queryString}`)
227
+ }
228
+
229
+ /**
230
+ * Get analytics data
231
+ * @param {object} params - Query parameters (from, to, interval, etc.)
232
+ * @returns {Promise<object>}
233
+ */
234
+ async getAnalytics(params = {}) {
235
+ logger.log('Admin.getAnalytics', { params })
236
+ const queryString = this.#buildQueryString(params)
237
+ return this.#httpClient.get(`/api/v1/status/analytics${queryString}`)
238
+ }
239
+
240
+ // ===========================
241
+ // Consumer Groups API
242
+ // ===========================
243
+
244
+ /**
245
+ * List all consumer groups
246
+ * @returns {Promise<object>}
247
+ */
248
+ async listConsumerGroups() {
249
+ logger.log('Admin.listConsumerGroups', {})
250
+ return this.#httpClient.get('/api/v1/consumer-groups')
251
+ }
252
+
253
+ /**
254
+ * Refresh consumer group statistics
255
+ * @returns {Promise<object>}
256
+ */
257
+ async refreshConsumerStats() {
258
+ logger.log('Admin.refreshConsumerStats', {})
259
+ return this.#httpClient.post('/api/v1/stats/refresh', {})
260
+ }
261
+
262
+ /**
263
+ * Get consumer group details
264
+ * @param {string} name - Consumer group name
265
+ * @returns {Promise<object>}
266
+ */
267
+ async getConsumerGroup(name) {
268
+ logger.log('Admin.getConsumerGroup', { name })
269
+ return this.#httpClient.get(`/api/v1/consumer-groups/${encodeURIComponent(name)}`)
270
+ }
271
+
272
+ /**
273
+ * Get lagging consumer groups
274
+ * @param {number} [minLagSeconds=60] - Minimum lag in seconds to be considered lagging
275
+ * @returns {Promise<object>}
276
+ */
277
+ async getLaggingConsumers(minLagSeconds = 60) {
278
+ logger.log('Admin.getLaggingConsumers', { minLagSeconds })
279
+ return this.#httpClient.get(`/api/v1/consumer-groups/lagging?minLagSeconds=${minLagSeconds}`)
280
+ }
281
+
282
+ /**
283
+ * Delete a consumer group for a specific queue
284
+ * @param {string} consumerGroup - Consumer group name
285
+ * @param {string} queueName - Queue name
286
+ * @param {boolean} [deleteMetadata=true] - Whether to delete subscription metadata
287
+ * @returns {Promise<object>}
288
+ */
289
+ async deleteConsumerGroupForQueue(consumerGroup, queueName, deleteMetadata = true) {
290
+ logger.log('Admin.deleteConsumerGroupForQueue', { consumerGroup, queueName, deleteMetadata })
291
+ const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}/queues/${encodeURIComponent(queueName)}?deleteMetadata=${deleteMetadata}`
292
+ return this.#httpClient.delete(url)
293
+ }
294
+
295
+ /**
296
+ * Seek consumer group offset for a queue
297
+ * @param {string} consumerGroup - Consumer group name
298
+ * @param {string} queueName - Queue name
299
+ * @param {object} options - Seek options (timestamp, position, etc.)
300
+ * @returns {Promise<object>}
301
+ */
302
+ async seekConsumerGroup(consumerGroup, queueName, options = {}) {
303
+ logger.log('Admin.seekConsumerGroup', { consumerGroup, queueName, options })
304
+ const url = `/api/v1/consumer-groups/${encodeURIComponent(consumerGroup)}/queues/${encodeURIComponent(queueName)}/seek`
305
+ return this.#httpClient.post(url, options)
306
+ }
307
+
308
+ // ===========================
309
+ // System API
310
+ // ===========================
311
+
312
+ /**
313
+ * Health check
314
+ * @returns {Promise<object>}
315
+ */
316
+ async health() {
317
+ logger.log('Admin.health', {})
318
+ return this.#httpClient.get('/health')
319
+ }
320
+
321
+ /**
322
+ * Get Prometheus metrics
323
+ * @returns {Promise<string>} Raw metrics text
324
+ */
325
+ async metrics() {
326
+ logger.log('Admin.metrics', {})
327
+ return this.#httpClient.get('/metrics')
328
+ }
329
+
330
+ /**
331
+ * Get push maintenance mode status
332
+ * @returns {Promise<object>}
333
+ */
334
+ async getMaintenanceMode() {
335
+ logger.log('Admin.getMaintenanceMode', {})
336
+ return this.#httpClient.get('/api/v1/system/maintenance')
337
+ }
338
+
339
+ /**
340
+ * Set push maintenance mode
341
+ * @param {boolean} enabled - Enable or disable maintenance mode
342
+ * @returns {Promise<object>}
343
+ */
344
+ async setMaintenanceMode(enabled) {
345
+ logger.log('Admin.setMaintenanceMode', { enabled })
346
+ return this.#httpClient.post('/api/v1/system/maintenance', { enabled })
347
+ }
348
+
349
+ /**
350
+ * Get pop maintenance mode status
351
+ * @returns {Promise<object>}
352
+ */
353
+ async getPopMaintenanceMode() {
354
+ logger.log('Admin.getPopMaintenanceMode', {})
355
+ return this.#httpClient.get('/api/v1/system/maintenance/pop')
356
+ }
357
+
358
+ /**
359
+ * Set pop maintenance mode
360
+ * @param {boolean} enabled - Enable or disable pop maintenance mode
361
+ * @returns {Promise<object>}
362
+ */
363
+ async setPopMaintenanceMode(enabled) {
364
+ logger.log('Admin.setPopMaintenanceMode', { enabled })
365
+ return this.#httpClient.post('/api/v1/system/maintenance/pop', { enabled })
366
+ }
367
+
368
+ /**
369
+ * Get system metrics (CPU, memory, connections, etc.)
370
+ * @param {object} params - Query parameters (from, to, etc.)
371
+ * @returns {Promise<object>}
372
+ */
373
+ async getSystemMetrics(params = {}) {
374
+ logger.log('Admin.getSystemMetrics', { params })
375
+ const queryString = this.#buildQueryString(params)
376
+ return this.#httpClient.get(`/api/v1/analytics/system-metrics${queryString}`)
377
+ }
378
+
379
+ /**
380
+ * Get worker metrics
381
+ * @param {object} params - Query parameters
382
+ * @returns {Promise<object>}
383
+ */
384
+ async getWorkerMetrics(params = {}) {
385
+ logger.log('Admin.getWorkerMetrics', { params })
386
+ const queryString = this.#buildQueryString(params)
387
+ return this.#httpClient.get(`/api/v1/analytics/worker-metrics${queryString}`)
388
+ }
389
+
390
+ /**
391
+ * Get PostgreSQL statistics
392
+ * @returns {Promise<object>}
393
+ */
394
+ async getPostgresStats() {
395
+ logger.log('Admin.getPostgresStats', {})
396
+ return this.#httpClient.get('/api/v1/analytics/postgres-stats')
397
+ }
398
+
399
+ // ===========================
400
+ // Helper Methods
401
+ // ===========================
402
+
403
+ #buildQueryString(params) {
404
+ const searchParams = new URLSearchParams()
405
+ for (const [key, value] of Object.entries(params)) {
406
+ if (value !== undefined && value !== null) {
407
+ searchParams.append(key, String(value))
408
+ }
409
+ }
410
+ const queryString = searchParams.toString()
411
+ return queryString ? `?${queryString}` : ''
412
+ }
413
+ }
414
+
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Admin module exports
3
+ */
4
+
5
+ export { Admin } from './Admin.js'
6
+
@@ -13,10 +13,10 @@ export class TransactionBuilder {
13
13
  this.#httpClient = httpClient
14
14
  }
15
15
 
16
- ack(messages, status = 'completed') {
16
+ ack(messages, status = 'completed', context = {}) {
17
17
  const msgs = Array.isArray(messages) ? messages : [messages]
18
18
 
19
- logger.log('TransactionBuilder.ack', { count: msgs.length, status })
19
+ logger.log('TransactionBuilder.ack', { count: msgs.length, status, consumerGroup: context.consumerGroup })
20
20
 
21
21
  msgs.forEach(msg => {
22
22
  const transactionId = typeof msg === 'string' ? msg : (msg.transactionId || msg.id)
@@ -32,12 +32,19 @@ export class TransactionBuilder {
32
32
  throw new Error('Message must have partitionId property to ensure message uniqueness')
33
33
  }
34
34
 
35
- this.#operations.push({
35
+ const operation = {
36
36
  type: 'ack',
37
37
  transactionId,
38
38
  partitionId,
39
39
  status
40
- })
40
+ }
41
+
42
+ // Add consumerGroup if provided in context
43
+ if (context.consumerGroup) {
44
+ operation.consumerGroup = context.consumerGroup
45
+ }
46
+
47
+ this.#operations.push(operation)
41
48
 
42
49
  if (leaseId) {
43
50
  this.#requiredLeases.push(leaseId)
@@ -11,6 +11,7 @@ export class HttpClient {
11
11
  #retryAttempts
12
12
  #retryDelayMillis
13
13
  #enableFailover
14
+ #bearerToken
14
15
 
15
16
  constructor(options = {}) {
16
17
  const {
@@ -19,7 +20,8 @@ export class HttpClient {
19
20
  timeoutMillis = 30000,
20
21
  retryAttempts = 3,
21
22
  retryDelayMillis = 1000,
22
- enableFailover = true
23
+ enableFailover = true,
24
+ bearerToken = null
23
25
  } = options
24
26
 
25
27
  this.#baseUrl = baseUrl
@@ -28,13 +30,15 @@ export class HttpClient {
28
30
  this.#retryAttempts = retryAttempts
29
31
  this.#retryDelayMillis = retryDelayMillis
30
32
  this.#enableFailover = enableFailover
33
+ this.#bearerToken = bearerToken
31
34
 
32
35
  logger.log('HttpClient.constructor', {
33
36
  hasLoadBalancer: !!loadBalancer,
34
37
  baseUrl: baseUrl || 'load-balanced',
35
38
  timeoutMillis,
36
39
  retryAttempts,
37
- enableFailover
40
+ enableFailover,
41
+ hasAuth: !!bearerToken
38
42
  })
39
43
  }
40
44
 
@@ -46,10 +50,15 @@ export class HttpClient {
46
50
  const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout)
47
51
 
48
52
  try {
53
+ const headers = { 'Content-Type': 'application/json' }
54
+ if (this.#bearerToken) {
55
+ headers['Authorization'] = `Bearer ${this.#bearerToken}`
56
+ }
57
+
49
58
  const options = {
50
59
  method,
51
60
  signal: controller.signal,
52
- headers: { 'Content-Type': 'application/json' }
61
+ headers
53
62
  }
54
63
 
55
64
  if (body) {
@@ -3,5 +3,6 @@
3
3
  */
4
4
 
5
5
  export { Queen } from './Queen.js'
6
+ export { Admin } from './admin/Admin.js'
6
7
  export { CLIENT_DEFAULTS, QUEUE_DEFAULTS, CONSUME_DEFAULTS, POP_DEFAULTS, BUFFER_DEFAULTS } from './utils/defaults.js'
7
8
 
@@ -13,7 +13,8 @@ export const CLIENT_DEFAULTS = {
13
13
  loadBalancingStrategy: 'affinity', // 'round-robin', 'session', or 'affinity'
14
14
  affinityHashRing: 128, // Number of virtual nodes per server for affinity strategy
15
15
  enableFailover: true, // Auto-failover to other servers
16
- healthRetryAfterMillis: 5000 // Retry unhealthy backends after 5 seconds
16
+ healthRetryAfterMillis: 5000, // Retry unhealthy backends after 5 seconds
17
+ bearerToken: null // Bearer token for proxy authentication
17
18
  }
18
19
 
19
20
  export const QUEUE_DEFAULTS = {
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * Logger utility for Queen Client v2
3
- * Controlled by QUEEN_CLIENT_LOG environment variable
3
+ * Controlled by QUEEN_CLIENT_LOG environment variable (Node.js)
4
+ * or window.QUEEN_CLIENT_LOG (Browser)
4
5
  */
5
6
 
6
- const LOG_ENABLED = process.env.QUEEN_CLIENT_LOG === 'true'
7
+ const LOG_ENABLED = (() => {
8
+ // Node.js environment
9
+ if (typeof process !== 'undefined' && process.env) {
10
+ return process.env.QUEEN_CLIENT_LOG === 'true'
11
+ }
12
+ // Browser environment
13
+ if (typeof window !== 'undefined') {
14
+ return window.QUEEN_CLIENT_LOG === true
15
+ }
16
+ return false
17
+ })()
7
18
 
8
19
  /**
9
20
  * Get formatted timestamp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "queen-mq",
3
- "version": "0.7.4",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "description": "High-performance C++ message queue backed by PostgreSQL",
6
6
  "main": "client-v2/index.js",
@@ -51,60 +51,6 @@ export async function testConsumerTrace(client) {
51
51
  return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
52
52
  }
53
53
 
54
- export async function testConsumerNamespace(client) {
55
- const queue = await client.queue('test-queue-v2-namespace')
56
- .namespace('test-namespace')
57
- .create()
58
- if (!queue.configured) {
59
- return { success: false, message: 'Queue not created' }
60
- }
61
-
62
- await client
63
- .queue('test-queue-v2-namespace')
64
- .push([{ data: { message: 'Hello, world!' } }])
65
-
66
- let msgToReturn = null
67
-
68
- await client
69
- .queue()
70
- .namespace('test-namespace')
71
- .batch(1)
72
- .limit(1)
73
- .each()
74
- .consume(async msg => {
75
- msgToReturn = msg
76
- })
77
-
78
- return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
79
- }
80
-
81
- export async function testConsumerTask(client) {
82
- const queue = await client.queue('test-queue-v2-task')
83
- .task('test-task')
84
- .create()
85
- if (!queue.configured) {
86
- return { success: false, message: 'Queue not created' }
87
- }
88
-
89
- await client
90
- .queue('test-queue-v2-task')
91
- .push([{ data: { message: 'Hello, world!' } }])
92
-
93
- let msgToReturn = null
94
-
95
- await client
96
- .queue()
97
- .task('test-task')
98
- .batch(1)
99
- .limit(1)
100
- .each()
101
- .consume(async msg => {
102
- msgToReturn = msg
103
- })
104
-
105
- return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
106
- }
107
-
108
54
  export async function testConsumerWithPartition(client) {
109
55
  const queue = await client.queue('test-queue-v2-consume-with-partition').create()
110
56
  if (!queue.configured) {
@@ -252,6 +198,7 @@ export async function testConsumerOrderingBatch(client) {
252
198
 
253
199
 
254
200
  export async function testConsumerBatchConsumeBatch(client) {
201
+ await client.queue('test-queue-v2-consume-batch').delete()
255
202
  const queue = await client.queue('test-queue-v2-consume-batch').create()
256
203
  if (!queue.configured) {
257
204
  return { success: false, message: 'Queue not created' }
@@ -271,7 +218,7 @@ export async function testConsumerBatchConsumeBatch(client) {
271
218
  .queue('test-queue-v2-consume-batch')
272
219
  .batch(10)
273
220
  .wait(false)
274
- .limit(1)
221
+ .limit(3)
275
222
  .consume(async msgs => {
276
223
  msgLength = msgs.length
277
224
  })
@@ -465,12 +412,13 @@ export async function manualAck(client) {
465
412
 
466
413
  await client
467
414
  .queue('test-queue-v2-manual-ack')
468
- .concurrency(10)
415
+ .concurrency(1)
469
416
  .subscriptionMode('from_beginning')
470
417
  .batch(1000)
471
418
  .wait(false)
472
- .limit(1)
419
+ .limit(10000)
473
420
  .consume(async msgs => {
421
+ console.log('Messages: ' + msgs.length)
474
422
  for (const msg of msgs) {
475
423
  if (lastId === null) {
476
424
  lastId = msg.data.id
@@ -525,6 +473,7 @@ export async function retries(client) {
525
473
  .limit(300) // Allow up to 300 messages (3 batches of 100)
526
474
  .consume(async msgs => {
527
475
  attemptCount++;
476
+ console.log('Attempt: ' + attemptCount)
528
477
  if (attemptCount < 3) {
529
478
  // Fail first 2 attempts, succeed on 3rd
530
479
  throw new Error('Test error - triggering retry')
package/test-v2/load.js CHANGED
@@ -11,7 +11,7 @@ export async function testLoad(client) {
11
11
  let messages = [];
12
12
  for (let i = 0; i < messagesToPush; i++) {
13
13
  messages.push({ data: { id: i } })
14
- if (messages.length >= 10000) {
14
+ if (messages.length >= 100) {
15
15
  await client.queue('test-queue-v2-load').push(messages)
16
16
  messages = [];
17
17
  }
@@ -24,11 +24,10 @@ export async function testLoad(client) {
24
24
  await client
25
25
  .queue('test-queue-v2-load')
26
26
  .concurrency(10)
27
- .batch(10000)
27
+ .batch(100)
28
28
  .wait(false)
29
- .limit(1)
29
+ .limit(10000)
30
30
  .consume(async msgs => {
31
- console.log(`Messages: ${msgs.length}`)
32
31
  for (const msg of msgs) {
33
32
  if (lastId === null) {
34
33
  lastId = msg.data.id
@@ -60,35 +59,37 @@ export async function testLoadPartition(client) {
60
59
 
61
60
  let messages = [];
62
61
  let k = 0
63
- for (let i = 0; i < messagesToPush; i++) {
62
+ for (let k = 0; k < 10; k++) {
63
+ for (let i = 0; i < messagesToPush / 10; i++) {
64
64
  messages.push({ data: { id: i } })
65
- if (messages.length >= 10000) {
65
+ if (messages.length >= 100) {
66
66
  await client
67
67
  .queue('test-queue-v2-load-partition')
68
68
  .partition(k.toString())
69
69
  .push(messages)
70
70
  messages = [];
71
- k++;
72
71
  }
73
72
  }
74
- await client.queue('test-queue-v2-load-partition').push(messages)
73
+ }
75
74
 
76
75
  let uniqueIds = new Set();
77
- let lastId = null;
76
+
78
77
 
79
78
  for (let i = 0; i < 10; i++) {
79
+ let lastId = null;
80
80
  await client
81
81
  .queue('test-queue-v2-load-partition')
82
82
  .partition(i.toString())
83
- .batch(10000)
83
+ .batch(100)
84
84
  .wait(false)
85
- .limit(1)
85
+ .limit(10000)
86
86
  .consume(async msgs => {
87
87
  for (const msg of msgs) {
88
88
  if (lastId === null) {
89
89
  lastId = msg.data.id
90
90
  } else {
91
91
  if (msg.data.id !== lastId + 1) {
92
+ console.log(`Message ordering violation: ${msg.data.id} !== ${lastId + 1}`)
92
93
  throw new Error('Message ordering violation')
93
94
  }
94
95
  lastId = msg.data.id
@@ -98,14 +99,15 @@ export async function testLoadPartition(client) {
98
99
  })
99
100
  }
100
101
 
102
+ console.log(uniqueIds.size, messagesToPush)
101
103
  const uniqueIdsCount = uniqueIds.size;
102
104
 
103
- return { success: uniqueIdsCount === messagesToPush, message: 'Load test completed successfully' }
105
+ return { success: uniqueIdsCount === messagesToPush / 10, message: 'Load test completed successfully' }
104
106
  }
105
107
 
106
108
  export async function testLoadConsumerGroup(client) {
107
109
  const queue = await client
108
- .queue('test-queue-v2-load-consumer-group')
110
+ .queue('')
109
111
  .create()
110
112
  if (!queue.configured) {
111
113
  return { success: false, message: 'Queue not created' }
@@ -116,7 +118,7 @@ export async function testLoadConsumerGroup(client) {
116
118
  let messages = [];
117
119
  for (let i = 0; i < messagesToPush; i++) {
118
120
  messages.push({ data: { id: i } })
119
- if (messages.length >= 10000) {
121
+ if (messages.length >= 100) {
120
122
  await client.queue('test-queue-v2-load-consumer-group').push(messages)
121
123
  messages = [];
122
124
  }
@@ -129,11 +131,11 @@ export async function testLoadConsumerGroup(client) {
129
131
  await client
130
132
  .queue('test-queue-v2-load-consumer-group')
131
133
  .group('test-consumer-group-a')
132
- .subscriptionMode('from_beginning')
134
+ .subscriptionMode('all')
133
135
  .concurrency(10)
134
- .batch(10000)
136
+ .batch(100)
135
137
  .wait(false)
136
- .limit(1)
138
+ .limit(10000)
137
139
  .consume(async msgs => {
138
140
  for (const msg of msgs) {
139
141
  if (lastIdA === null) {
@@ -154,11 +156,11 @@ export async function testLoadConsumerGroup(client) {
154
156
  await client
155
157
  .queue('test-queue-v2-load-consumer-group')
156
158
  .group('test-consumer-group-b')
157
- .subscriptionMode('from_beginning')
159
+ .subscriptionMode('all')
158
160
  .concurrency(10)
159
- .batch(10000)
161
+ .batch(100)
160
162
  .wait(false)
161
- .limit(1)
163
+ .limit(10000)
162
164
  .consume(async msgs => {
163
165
  for (const msg of msgs) {
164
166
  if (lastIdB === null) {
@@ -71,12 +71,14 @@ export async function test_maintenance_mode(client = null) {
71
71
 
72
72
  // Acknowledge messages
73
73
  await queen.ack(messages);
74
+
74
75
  }
75
76
  } catch (err) {
76
77
  if (consumerActive) {
77
78
  console.error(' ❌ Consumer error:', err.message);
78
79
  }
79
80
  }
81
+ await sleep(10);
80
82
  }
81
83
  })();
82
84
 
package/test-v2/push.js CHANGED
@@ -115,35 +115,6 @@ export async function pushBufferedMessage(client) {
115
115
  return { success: true }
116
116
  }
117
117
 
118
- export async function pushMaxQueueSize(client) {
119
- const queue = await client.queue('test-queue-max-size').config({ maxSize: 10 }).create()
120
- if (!queue.configured) {
121
- return { success: false, message: 'Queue not created' }
122
- }
123
-
124
- // Necessary to create a partition consumer to trigger the capacity check
125
- const popToCreateParititonConsumer = await client
126
- .queue('test-queue-max-size')
127
- .batch(1)
128
- .wait(false)
129
- .pop()
130
-
131
- try {
132
- for (let i = 0; i < 20; i++) {
133
- const result = await client
134
- .queue('test-queue-max-size')
135
- .push([{ data: { message: i } }])
136
- }
137
- } catch (error) {
138
- if (!error.message.includes(' Queue "test-queue-max-size" would exceed max capacity (10).')) {
139
- return { success: true, message: 'Correct error message' }
140
- } else {
141
- return { success: false, message: 'Incorrect error message' }
142
- }
143
- }
144
- return { success: false }
145
- }
146
-
147
118
  export async function pushDelayedMessage(client) {
148
119
  const queue = await client.queue('test-queue-delayed')
149
120
  .config({ delayedProcessing: 2 })
package/test-v2/run.js CHANGED
@@ -18,11 +18,26 @@ import * as bufferingTests from './ai_buffering.js'
18
18
  import * as priorityTests from './ai_priority.js'
19
19
  import * as ttlRetentionTests from './ai_ttl_retention.js'
20
20
  import * as mixedScenariosTests from './ai_mixed_scenarios.js'
21
+ import { LoadBalancer } from '../client-v2/http/LoadBalancer.js';
21
22
 
22
23
 
23
24
  // Test configuration
24
- export const TEST_CONFIG = {
25
- baseUrls: ['http://localhost:6632'],
25
+
26
+ export const TEST_CONFIG_SINGLE = {
27
+ baseUrls: ['http://localhost:6632'],
28
+ loadBalancingStrategy: 'affinity',
29
+ dbConfig: {
30
+ host: process.env.PG_HOST || 'localhost',
31
+ port: process.env.PG_PORT || 5432,
32
+ database: process.env.PG_DB || 'postgres',
33
+ user: process.env.PG_USER || 'postgres',
34
+ password: process.env.PG_PASSWORD || 'postgres'
35
+ }
36
+ };
37
+
38
+ export const TEST_CONFIG_MULTIPLE = {
39
+ baseUrls: ['http://localhost:6632','http://localhost:6633'],
40
+ loadBalancingStrategy: 'round-robin',
26
41
  dbConfig: {
27
42
  host: process.env.PG_HOST || 'localhost',
28
43
  port: process.env.PG_PORT || 5432,
@@ -32,6 +47,9 @@ export const TEST_CONFIG = {
32
47
  }
33
48
  };
34
49
 
50
+ export const TEST_CONFIG = process.env.TEST_CONFIG === 'multiple' ? TEST_CONFIG_MULTIPLE : TEST_CONFIG_SINGLE;
51
+ console.log('TEST_CONFIG:', TEST_CONFIG);
52
+
35
53
  // Global test state
36
54
  export let dbPool;
37
55
 
@@ -82,7 +100,10 @@ export const cleanupTestData = async () => {
82
100
  };
83
101
 
84
102
  async function main() {
85
- const client = new Queen(TEST_CONFIG.baseUrls)
103
+ const client = new Queen({
104
+ urls: TEST_CONFIG.baseUrls,
105
+ loadBalancingStrategy: TEST_CONFIG.loadBalancingStrategy
106
+ })
86
107
  await initDb()
87
108
 
88
109
  // Separate human and AI tests
@@ -503,3 +503,69 @@ export async function transactionRollback(client) {
503
503
  }
504
504
  }
505
505
 
506
+ export async function transactionAckWithConsumerGroup(client) {
507
+ const queueA = await client.queue('test-queue-v2-txn-cg-a').create()
508
+ const queueB = await client.queue('test-queue-v2-txn-cg-b').create()
509
+ if (!queueA.configured || !queueB.configured) {
510
+ return { success: false, message: 'Queues not created' }
511
+ }
512
+
513
+ const consumerGroup = 'test-consumer-group-txn'
514
+
515
+ // Push messages to queue A
516
+ await client.queue('test-queue-v2-txn-cg-a').push([
517
+ { data: { value: 1 } },
518
+ { data: { value: 2 } }
519
+ ])
520
+
521
+ // Pop with consumer group
522
+ const messages = await client.queue('test-queue-v2-txn-cg-a')
523
+ .group(consumerGroup)
524
+ .batch(2)
525
+ .wait(false)
526
+ .pop()
527
+
528
+ if (messages.length !== 2) {
529
+ return { success: false, message: `Expected 2 messages, got ${messages.length}` }
530
+ }
531
+
532
+ // Transaction: ACK with consumer group context, push to B
533
+ await client
534
+ .transaction()
535
+ .ack(messages[0], 'completed', { consumerGroup })
536
+ .ack(messages[1], 'completed', { consumerGroup })
537
+ .queue('test-queue-v2-txn-cg-b')
538
+ .push([{ data: { sum: messages[0].data.value + messages[1].data.value } }])
539
+ .commit()
540
+
541
+ // Verify: Pop again with same consumer group should get no messages
542
+ // (consumer group cursor should have advanced)
543
+ const messagesAfterAck = await client.queue('test-queue-v2-txn-cg-a')
544
+ .group(consumerGroup)
545
+ .batch(2)
546
+ .wait(false)
547
+ .pop()
548
+
549
+ // Verify: Queue B has the result
550
+ const resultB = await client.queue('test-queue-v2-txn-cg-b').batch(1).wait(false).pop()
551
+
552
+ // Verify: Using a different consumer group should still see the messages
553
+ const messagesOtherGroup = await client.queue('test-queue-v2-txn-cg-a')
554
+ .group('other-consumer-group')
555
+ .batch(2)
556
+ .wait(false)
557
+ .pop()
558
+
559
+ const success = messagesAfterAck.length === 0 &&
560
+ resultB.length === 1 &&
561
+ resultB[0].data.sum === 3 &&
562
+ messagesOtherGroup.length === 2
563
+
564
+ return {
565
+ success,
566
+ message: success
567
+ ? 'Transaction ack with consumer group completed - cursor advanced correctly'
568
+ : `Failed: afterAck=${messagesAfterAck.length} (expected 0), resultB=${resultB.length} (expected 1), otherGroup=${messagesOtherGroup.length} (expected 2)`
569
+ }
570
+ }
571
+