queen-mq 0.6.4 → 0.7.4

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/README.md CHANGED
@@ -614,7 +614,7 @@ const messages: Message<OrderData>[] = await queen.queue('orders').pop()
614
614
  - **[Complete V2 Guide](client-v2/README.md)** - Full tutorial with all features (94 test examples)
615
615
  - **[HTTP API Reference](https://github.com/smartpricing/queen/blob/master/server/API.md)** - Raw HTTP endpoints
616
616
  - **[Server Guide](https://github.com/smartpricing/queen/blob/master/server/README.md)** - Server setup and configuration
617
- - **[Architecture Guide](https://github.com/smartpricing/queen/blob/master/docs/ARCHITECTURE.md)** - Deep dive into internals
617
+ - **[Architecture Guide](https://github.com/smartpricing/queen/blob/master/documentation/ARCHITECTURE.md)** - Deep dive into internals
618
618
 
619
619
  ---
620
620
 
@@ -75,7 +75,7 @@ export class Queen {
75
75
  }
76
76
 
77
77
  #createHttpClient() {
78
- const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, enableFailover } = this.#config
78
+ const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, affinityHashRing, healthRetryAfterMillis, enableFailover } = this.#config
79
79
 
80
80
  if (urls.length === 1) {
81
81
  // Single server
@@ -88,7 +88,10 @@ export class Queen {
88
88
  }
89
89
 
90
90
  // Multiple servers with load balancing
91
- const loadBalancer = new LoadBalancer(urls, loadBalancingStrategy)
91
+ const loadBalancer = new LoadBalancer(urls, loadBalancingStrategy, {
92
+ affinityHashRing,
93
+ healthRetryAfterMillis
94
+ })
92
95
  return new HttpClient({
93
96
  loadBalancer,
94
97
  timeoutMillis,
@@ -43,16 +43,67 @@ const queen = new Queen(['http://server1:6632', 'http://server2:6632'])
43
43
 
44
44
  // Or with full configuration
45
45
  const queen = new Queen({
46
- urls: ['http://server1:6632', 'http://server2:6632'],
46
+ urls: ['http://server1:6632', 'http://server2:6632', 'http://server3:6632'],
47
47
  timeoutMillis: 30000,
48
48
  retryAttempts: 3,
49
- loadBalancingStrategy: 'round-robin', // or 'session'
49
+ loadBalancingStrategy: 'affinity', // 'affinity', 'round-robin', or 'session'
50
+ affinityHashRing: 150, // Virtual nodes per server (for affinity mode)
50
51
  enableFailover: true
51
52
  })
52
53
  ```
53
54
 
54
55
  That's it! You're connected. Now let's do something fun. 🎉
55
56
 
57
+ ### Load Balancing Strategies
58
+
59
+ When connecting to multiple Queen servers, you can choose how requests are distributed:
60
+
61
+ #### Affinity Mode (Recommended for Production)
62
+
63
+ Uses consistent hashing with virtual nodes to route consumer groups to the same backend server. This optimizes database queries by consolidating poll intentions.
64
+
65
+ ```javascript
66
+ const queen = new Queen({
67
+ urls: ['http://server1:6632', 'http://server2:6632', 'http://server3:6632'],
68
+ loadBalancingStrategy: 'affinity',
69
+ affinityHashRing: 150 // Virtual nodes per server (default: 150)
70
+ })
71
+ ```
72
+
73
+ **Benefits:**
74
+ - ✅ Same consumer group always routes to same server
75
+ - ✅ Poll intentions consolidated → optimized DB queries
76
+ - ✅ Graceful failover (only ~33% of keys move if server fails)
77
+ - ✅ Works great with 3-server HA setup
78
+
79
+ **How it works:**
80
+ 1. Each consumer group generates an affinity key: `queue:partition:consumerGroup`
81
+ 2. Key is hashed and mapped to a virtual node on the ring
82
+ 3. Virtual node maps to a real backend server
83
+ 4. Same key always routes to same server
84
+
85
+ #### Round-Robin Mode
86
+
87
+ Cycles through servers in order. Simple but doesn't optimize for poll intention consolidation.
88
+
89
+ ```javascript
90
+ const queen = new Queen({
91
+ urls: ['http://server1:6632', 'http://server2:6632'],
92
+ loadBalancingStrategy: 'round-robin'
93
+ })
94
+ ```
95
+
96
+ #### Session Mode
97
+
98
+ Sticky sessions - each client instance sticks to one server.
99
+
100
+ ```javascript
101
+ const queen = new Queen({
102
+ urls: ['http://server1:6632', 'http://server2:6632'],
103
+ loadBalancingStrategy: 'session'
104
+ })
105
+ ```
106
+
56
107
  ---
57
108
 
58
109
  ## Part 1: Hello Queue!
@@ -48,6 +48,26 @@ export class QueueBuilder {
48
48
  this.#queueName = queueName
49
49
  }
50
50
 
51
+ // ===========================
52
+ // Affinity Key Generation
53
+ // ===========================
54
+
55
+ /**
56
+ * Generate affinity key for consistent routing
57
+ * Matches server's PollIntention::grouping_key() format
58
+ * Format: queue:partition:consumerGroup or namespace:task:consumerGroup
59
+ */
60
+ #getAffinityKey() {
61
+ if (this.#queueName) {
62
+ // Queue-based routing: queue:partition:consumerGroup
63
+ return `${this.#queueName}:${this.#partition || '*'}:${this.#group || '__QUEUE_MODE__'}`
64
+ } else if (this.#namespace || this.#task) {
65
+ // Namespace/task-based routing: namespace:task:consumerGroup
66
+ return `${this.#namespace || '*'}:${this.#task || '*'}:${this.#group || '__QUEUE_MODE__'}`
67
+ }
68
+ return null
69
+ }
70
+
51
71
  // ===========================
52
72
  // Queue Configuration Methods
53
73
  // ===========================
@@ -270,7 +290,10 @@ export class QueueBuilder {
270
290
  if (this.#subscriptionMode) params.append('subscriptionMode', this.#subscriptionMode)
271
291
  if (this.#subscriptionFrom) params.append('subscriptionFrom', this.#subscriptionFrom)
272
292
 
273
- const result = await this.#httpClient.get(`${path}?${params}`, this.#timeoutMillis + 5000)
293
+ // Generate affinity key for consistent routing to same backend
294
+ const affinityKey = this.#getAffinityKey()
295
+
296
+ const result = await this.#httpClient.get(`${path}?${params}`, this.#timeoutMillis + 5000, affinityKey)
274
297
 
275
298
  if (!result || !result.messages) {
276
299
  logger.log('QueueBuilder.pop', { status: 'no-messages' })
@@ -13,6 +13,22 @@ export class ConsumerManager {
13
13
  this.#queen = queen
14
14
  }
15
15
 
16
+ /**
17
+ * Generate affinity key for consistent routing
18
+ * Matches server's PollIntention::grouping_key() format
19
+ * Format: queue:partition:consumerGroup or namespace:task:consumerGroup
20
+ */
21
+ #getAffinityKey(queue, partition, namespace, task, group) {
22
+ if (queue) {
23
+ // Queue-based routing: queue:partition:consumerGroup
24
+ return `${queue}:${partition || '*'}:${group || '__QUEUE_MODE__'}`
25
+ } else if (namespace || task) {
26
+ // Namespace/task-based routing: namespace:task:consumerGroup
27
+ return `${namespace || '*'}:${task || '*'}:${group || '__QUEUE_MODE__'}`
28
+ }
29
+ return null
30
+ }
31
+
16
32
  async start(handler, options) {
17
33
  const {
18
34
  queue,
@@ -52,6 +68,9 @@ export class ConsumerManager {
52
68
  // Build the path and params for pop requests
53
69
  const path = this.#buildPath(queue, partition, namespace, task)
54
70
  const baseParams = this.#buildParams(batch, wait, timeoutMillis, group, subscriptionMode, subscriptionFrom, namespace, task, autoAck)
71
+
72
+ // Generate affinity key for consistent routing to same backend
73
+ const affinityKey = this.#getAffinityKey(queue, partition, namespace, task, group)
55
74
 
56
75
  // Start workers
57
76
  const workers = []
@@ -67,7 +86,8 @@ export class ConsumerManager {
67
86
  renewLeaseIntervalMillis,
68
87
  each,
69
88
  signal,
70
- group // Pass consumer group to workers
89
+ group, // Pass consumer group to workers
90
+ affinityKey // Pass affinity key to workers
71
91
  }))
72
92
  }
73
93
 
@@ -91,7 +111,8 @@ export class ConsumerManager {
91
111
  renewLeaseIntervalMillis,
92
112
  each,
93
113
  signal,
94
- group
114
+ group,
115
+ affinityKey
95
116
  } = options
96
117
 
97
118
  logger.log('ConsumerManager.worker', { workerId, status: 'started', limit, idleMillis })
@@ -122,9 +143,9 @@ export class ConsumerManager {
122
143
  }
123
144
 
124
145
  try {
125
- // Pop messages
146
+ // Pop messages with affinity key for consistent routing
126
147
  const clientTimeout = wait ? timeoutMillis + 5000 : timeoutMillis
127
- const result = await this.#httpClient.get(`${path}?${baseParams}`, clientTimeout)
148
+ const result = await this.#httpClient.get(`${path}?${baseParams}`, clientTimeout, affinityKey)
128
149
 
129
150
  // Handle empty response
130
151
  if (!result || !result.messages || result.messages.length === 0) {
@@ -145,7 +145,7 @@ export class HttpClient {
145
145
  throw lastError
146
146
  }
147
147
 
148
- async #requestWithFailover(method, path, body = null, requestTimeoutMillis = null) {
148
+ async #requestWithFailover(method, path, body = null, requestTimeoutMillis = null, affinityKey = null) {
149
149
  if (!this.#loadBalancer || !this.#enableFailover) {
150
150
  return this.#requestWithRetry(method, path, body, requestTimeoutMillis)
151
151
  }
@@ -154,10 +154,11 @@ export class HttpClient {
154
154
  const attemptedUrls = new Set()
155
155
  let lastError = null
156
156
 
157
- logger.log('HttpClient.failover', { method, path, totalServers: urls.length })
157
+ logger.log('HttpClient.failover', { method, path, totalServers: urls.length, affinityKey })
158
158
 
159
159
  for (let i = 0; i < urls.length; i++) {
160
- const url = this.#loadBalancer.getNextUrl()
160
+ // Pass affinity key to load balancer for consistent routing
161
+ const url = this.#loadBalancer.getNextUrl(affinityKey)
161
162
 
162
163
  if (attemptedUrls.has(url)) {
163
164
  continue
@@ -166,9 +167,20 @@ export class HttpClient {
166
167
  attemptedUrls.add(url)
167
168
 
168
169
  try {
169
- return await this.#executeRequest(url + path, method, body, requestTimeoutMillis)
170
+ const result = await this.#executeRequest(url + path, method, body, requestTimeoutMillis)
171
+
172
+ // Mark backend as healthy on success
173
+ this.#loadBalancer.markHealthy(url)
174
+
175
+ return result
170
176
  } catch (error) {
171
177
  lastError = error
178
+
179
+ // Mark backend as unhealthy on failure (5xx or network errors)
180
+ if (!error.status || error.status >= 500) {
181
+ this.#loadBalancer.markUnhealthy(url)
182
+ }
183
+
172
184
  logger.warn('HttpClient.failover', { url, method, path, error: error.message })
173
185
  console.warn(`Request failed for ${url}: ${method} ${path} - ${error.message}`)
174
186
 
@@ -192,20 +204,20 @@ export class HttpClient {
192
204
  return this.#baseUrl
193
205
  }
194
206
 
195
- async get(path, requestTimeoutMillis = null) {
196
- return this.#requestWithFailover('GET', path, null, requestTimeoutMillis)
207
+ async get(path, requestTimeoutMillis = null, affinityKey = null) {
208
+ return this.#requestWithFailover('GET', path, null, requestTimeoutMillis, affinityKey)
197
209
  }
198
210
 
199
- async post(path, body = null, requestTimeoutMillis = null) {
200
- return this.#requestWithFailover('POST', path, body, requestTimeoutMillis)
211
+ async post(path, body = null, requestTimeoutMillis = null, affinityKey = null) {
212
+ return this.#requestWithFailover('POST', path, body, requestTimeoutMillis, affinityKey)
201
213
  }
202
214
 
203
- async put(path, body = null, requestTimeoutMillis = null) {
204
- return this.#requestWithFailover('PUT', path, body, requestTimeoutMillis)
215
+ async put(path, body = null, requestTimeoutMillis = null, affinityKey = null) {
216
+ return this.#requestWithFailover('PUT', path, body, requestTimeoutMillis, affinityKey)
205
217
  }
206
218
 
207
- async delete(path, requestTimeoutMillis = null) {
208
- return this.#requestWithFailover('DELETE', path, null, requestTimeoutMillis)
219
+ async delete(path, requestTimeoutMillis = null, affinityKey = null) {
220
+ return this.#requestWithFailover('DELETE', path, null, requestTimeoutMillis, affinityKey)
209
221
  }
210
222
 
211
223
  getLoadBalancer() {
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Load balancer for distributing requests across multiple servers
3
+ * Supports round-robin, session, and affinity-based routing with virtual nodes
3
4
  */
4
5
 
5
6
  export class LoadBalancer {
@@ -8,32 +9,245 @@ export class LoadBalancer {
8
9
  #currentIndex = 0
9
10
  #sessionMap = new Map()
10
11
  #sessionId
12
+ #healthStatus
13
+ #virtualNodes = null
14
+ #affinityHashRing
15
+ #healthRetryAfterMillis
11
16
 
12
- constructor(urls, strategy = 'round-robin') {
17
+ constructor(urls, strategy = 'round-robin', options = {}) {
13
18
  this.#urls = urls.map(url => url.replace(/\/$/, '')) // Remove trailing slashes
14
19
  this.#strategy = strategy
15
20
  this.#sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
21
+
22
+ // Virtual node replicas for affinity strategy (default: 150)
23
+ this.#affinityHashRing = options.affinityHashRing || 150
24
+
25
+ // Health retry interval (default: 30 seconds)
26
+ this.#healthRetryAfterMillis = options.healthRetryAfterMillis || 30000
27
+
28
+ // Initialize health status with last failure time
29
+ this.#healthStatus = new Map(this.#urls.map(url => [url, {
30
+ healthy: true,
31
+ failures: 0,
32
+ lastFailure: null
33
+ }]))
34
+
35
+ // Build virtual node ring for affinity strategy
36
+ if (strategy === 'affinity') {
37
+ this.#buildVirtualNodeRing()
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Build virtual node ring for consistent hashing
43
+ * Each real server gets multiple virtual positions in the hash space
44
+ */
45
+ #buildVirtualNodeRing() {
46
+ this.#virtualNodes = []
47
+
48
+ for (const url of this.#urls) {
49
+ for (let i = 0; i < this.#affinityHashRing; i++) {
50
+ // Create virtual node identifier
51
+ const vnodeKey = `${url}#vnode${i}`
52
+ const hash = this.#hashString(vnodeKey)
53
+
54
+ this.#virtualNodes.push({
55
+ hash,
56
+ realServer: url
57
+ })
58
+ }
59
+ }
60
+
61
+ // Sort by hash value to create the ring
62
+ this.#virtualNodes.sort((a, b) => a.hash - b.hash)
63
+ }
64
+
65
+ /**
66
+ * Rebuild virtual node ring with specified URLs
67
+ * @param {Array<string>} urls - URLs to include in the ring (if not provided, uses all healthy URLs)
68
+ */
69
+ #rebuildVirtualNodeRing(urls = null) {
70
+ const urlsToInclude = urls || this.#urls.filter(url => this.#healthStatus.get(url)?.healthy !== false)
71
+
72
+ this.#virtualNodes = []
73
+
74
+ for (const url of urlsToInclude) {
75
+ for (let i = 0; i < this.#affinityHashRing; i++) {
76
+ const vnodeKey = `${url}#vnode${i}`
77
+ const hash = this.#hashString(vnodeKey)
78
+
79
+ this.#virtualNodes.push({
80
+ hash,
81
+ realServer: url
82
+ })
83
+ }
84
+ }
85
+
86
+ this.#virtualNodes.sort((a, b) => a.hash - b.hash)
16
87
  }
17
88
 
18
89
  getNextUrl(sessionKey = null) {
19
90
  const key = sessionKey || this.#sessionId
91
+
92
+ // Get healthy URLs + unhealthy URLs that are ready for retry
93
+ const now = Date.now()
94
+ let needRingRebuild = false
95
+
96
+ const healthyUrls = this.#urls.filter(url => {
97
+ const status = this.#healthStatus.get(url)
98
+ if (status.healthy) return true
99
+
100
+ // Allow retry of unhealthy backends after configured interval
101
+ if (status.lastFailure && (now - status.lastFailure) >= this.#healthRetryAfterMillis) {
102
+ needRingRebuild = true // Need to rebuild ring to include this server's vnodes
103
+ return true
104
+ }
105
+
106
+ return false
107
+ })
108
+
109
+ // Rebuild vnode ring if we have servers eligible for retry
110
+ // Pass healthyUrls so it includes servers ready for retry
111
+ if (needRingRebuild && this.#virtualNodes) {
112
+ this.#rebuildVirtualNodeRing(healthyUrls)
113
+ }
114
+
115
+ if (healthyUrls.length === 0) {
116
+ // All backends unhealthy, try any backend as fallback
117
+ return this.#urls[this.#currentIndex % this.#urls.length]
118
+ }
20
119
 
120
+ if (this.#strategy === 'affinity') {
121
+ // Virtual node consistent hashing
122
+ return this.#getAffinityUrl(key, healthyUrls)
123
+ }
124
+
21
125
  if (this.#strategy === 'session') {
22
126
  // Session affinity: stick to the same server per session
23
127
  if (!this.#sessionMap.has(key)) {
24
128
  const assignedIndex = this.#currentIndex
25
129
  this.#sessionMap.set(key, assignedIndex)
26
- this.#currentIndex = (this.#currentIndex + 1) % this.#urls.length
130
+ this.#currentIndex = (this.#currentIndex + 1) % healthyUrls.length
27
131
  }
28
- return this.#urls[this.#sessionMap.get(key)]
132
+
133
+ const assignedUrl = this.#urls[this.#sessionMap.get(key)]
134
+ // If assigned URL is unhealthy, reassign
135
+ if (!healthyUrls.includes(assignedUrl)) {
136
+ const newIndex = this.#currentIndex % healthyUrls.length
137
+ this.#sessionMap.set(key, newIndex)
138
+ this.#currentIndex = (this.#currentIndex + 1) % healthyUrls.length
139
+ return healthyUrls[newIndex]
140
+ }
141
+
142
+ return assignedUrl
29
143
  }
30
144
 
31
- // Round robin: cycle through URLs
32
- const url = this.#urls[this.#currentIndex]
33
- this.#currentIndex = (this.#currentIndex + 1) % this.#urls.length
145
+ // Round robin: cycle through healthy URLs only
146
+ const url = healthyUrls[this.#currentIndex % healthyUrls.length]
147
+ this.#currentIndex = (this.#currentIndex + 1) % healthyUrls.length
34
148
  return url
35
149
  }
36
150
 
151
+ /**
152
+ * Affinity-based routing using consistent hashing with virtual nodes
153
+ * Same key always routes to same backend (unless unhealthy)
154
+ */
155
+ #getAffinityUrl(key, healthyUrls) {
156
+ if (!key || !this.#virtualNodes || this.#virtualNodes.length === 0) {
157
+ // No key or no vnodes, fall back to round-robin
158
+ return healthyUrls[this.#currentIndex++ % healthyUrls.length]
159
+ }
160
+
161
+ // Hash the consumer group key
162
+ const keyHash = this.#hashString(key)
163
+
164
+ // Binary search to find the first vnode >= keyHash (clockwise on ring)
165
+ let left = 0
166
+ let right = this.#virtualNodes.length - 1
167
+ let result = 0
168
+
169
+ while (left <= right) {
170
+ const mid = Math.floor((left + right) / 2)
171
+ if (this.#virtualNodes[mid].hash >= keyHash) {
172
+ result = mid
173
+ right = mid - 1
174
+ } else {
175
+ left = mid + 1
176
+ }
177
+ }
178
+
179
+ // Walk forward on the ring to find first healthy server
180
+ const startIdx = result
181
+ for (let i = 0; i < this.#virtualNodes.length; i++) {
182
+ const idx = (startIdx + i) % this.#virtualNodes.length
183
+ const vnode = this.#virtualNodes[idx]
184
+
185
+ if (healthyUrls.includes(vnode.realServer)) {
186
+ return vnode.realServer
187
+ }
188
+ }
189
+
190
+ // Fallback (should never happen if healthyUrls is not empty)
191
+ return healthyUrls[0]
192
+ }
193
+
194
+ /**
195
+ * FNV-1a hash function for consistent hashing
196
+ */
197
+ #hashString(str) {
198
+ let hash = 2166136261 // FNV offset basis
199
+ for (let i = 0; i < str.length; i++) {
200
+ hash ^= str.charCodeAt(i)
201
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
202
+ }
203
+ return hash >>> 0 // Convert to unsigned 32-bit integer
204
+ }
205
+
206
+ /**
207
+ * Mark a backend as unhealthy after failure
208
+ */
209
+ markUnhealthy(url) {
210
+ const status = this.#healthStatus.get(url)
211
+ if (status) {
212
+ status.healthy = false
213
+ status.failures++
214
+ status.lastFailure = Date.now()
215
+
216
+ const retryInSeconds = (this.#healthRetryAfterMillis / 1000).toFixed(0)
217
+
218
+ // Rebuild vnode ring to exclude unhealthy server
219
+ if (this.#virtualNodes) {
220
+ this.#rebuildVirtualNodeRing()
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Mark a backend as healthy after success
227
+ */
228
+ markHealthy(url) {
229
+ const status = this.#healthStatus.get(url)
230
+ if (!status) return
231
+
232
+ const wasUnhealthy = status.healthy === false
233
+
234
+ status.healthy = true
235
+ status.failures = 0
236
+ status.lastFailure = null
237
+
238
+ // Rebuild vnode ring to include recovered server
239
+ if (this.#virtualNodes && wasUnhealthy) {
240
+ this.#rebuildVirtualNodeRing()
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Get health status of all backends
246
+ */
247
+ getHealthStatus() {
248
+ return new Map(this.#healthStatus)
249
+ }
250
+
37
251
  getAllUrls() {
38
252
  return [...this.#urls]
39
253
  }
@@ -41,6 +255,13 @@ export class LoadBalancer {
41
255
  getStrategy() {
42
256
  return this.#strategy
43
257
  }
258
+
259
+ /**
260
+ * Get number of virtual nodes in the ring
261
+ */
262
+ getVirtualNodeCount() {
263
+ return this.#virtualNodes ? this.#virtualNodes.length : 0
264
+ }
44
265
 
45
266
  reset() {
46
267
  this.#currentIndex = 0
@@ -10,8 +10,10 @@ export const CLIENT_DEFAULTS = {
10
10
  timeoutMillis: 30000, // 30 seconds
11
11
  retryAttempts: 3, // 3 retry attempts
12
12
  retryDelayMillis: 1000, // 1 second initial delay (exponential backoff)
13
- loadBalancingStrategy: 'round-robin', // 'round-robin' or 'session'
14
- enableFailover: true // Auto-failover to other servers
13
+ loadBalancingStrategy: 'affinity', // 'round-robin', 'session', or 'affinity'
14
+ affinityHashRing: 128, // Number of virtual nodes per server for affinity strategy
15
+ enableFailover: true, // Auto-failover to other servers
16
+ healthRetryAfterMillis: 5000 // Retry unhealthy backends after 5 seconds
15
17
  }
16
18
 
17
19
  export const QUEUE_DEFAULTS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "queen-mq",
3
- "version": "0.6.4",
3
+ "version": "0.7.4",
4
4
  "type": "module",
5
5
  "description": "High-performance C++ message queue backed by PostgreSQL",
6
6
  "main": "client-v2/index.js",
@@ -8,12 +8,13 @@
8
8
  "test": "node test-v2/run.js human"
9
9
  },
10
10
  "files": [
11
- "client-v2/**/*",
11
+ "client-v2/**/*",
12
12
  "test-v2/**/*",
13
13
  "README.md"
14
14
  ],
15
15
  "dependencies": {
16
16
  "axios": "^1.12.2",
17
+ "pg": "^8.16.3",
17
18
  "uuid": "^13.0.0"
18
19
  },
19
20
  "engines": {
@@ -63,7 +63,7 @@ export async function test_maintenance_mode(client = null) {
63
63
  const consumePromise = (async () => {
64
64
  while (consumerActive) {
65
65
  try {
66
- const messages = await queen.queue(QUEUE_NAME).batch(10).wait(true).limit(1).each().pop();
66
+ const messages = await queen.queue(QUEUE_NAME).batch(10).wait(false).limit(1).each().pop();
67
67
 
68
68
  if (messages && messages.length > 0) {
69
69
  receivedCount += messages.length;