queen-mq 0.2.22 → 0.3.0
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 +162 -136
- package/client-js/benchmark/consumer.js +1 -1
- package/client-js/client/client.js +24 -3
- package/client-js/client-v2/LOGGING.md +240 -0
- package/client-js/client-v2/Queen.js +389 -0
- package/client-js/client-v2/README.md +1883 -0
- package/client-js/client-v2/buffer/BufferManager.js +215 -0
- package/client-js/client-v2/buffer/MessageBuffer.js +132 -0
- package/client-js/client-v2/builders/QueueBuilder.js +724 -0
- package/client-js/client-v2/builders/TransactionBuilder.js +110 -0
- package/client-js/client-v2/consumer/ConsumerManager.js +390 -0
- package/client-js/client-v2/http/HttpClient.js +215 -0
- package/client-js/client-v2/http/LoadBalancer.js +50 -0
- package/client-js/client-v2/index.js +7 -0
- package/client-js/client-v2/utils/defaults.js +54 -0
- package/client-js/client-v2/utils/logger.js +54 -0
- package/client-js/client-v2/utils/validation.js +31 -0
- package/client-js/test-v2/AI_TEST_SUMMARY.md +226 -0
- package/client-js/test-v2/GETTING_STARTED.md +154 -0
- package/client-js/test-v2/ai_buffering.js +194 -0
- package/client-js/test-v2/ai_error_handling.js +223 -0
- package/client-js/test-v2/ai_lease_renewal.js +206 -0
- package/client-js/test-v2/ai_mixed_scenarios.js +278 -0
- package/client-js/test-v2/ai_priority.js +169 -0
- package/client-js/test-v2/ai_resources.js +217 -0
- package/client-js/test-v2/ai_ttl_retention.js +170 -0
- package/client-js/test-v2/complete.js +59 -0
- package/client-js/test-v2/consume.js +655 -0
- package/client-js/test-v2/dlq.js +82 -0
- package/client-js/test-v2/load.js +177 -0
- package/client-js/test-v2/pop.js +114 -0
- package/client-js/test-v2/push.js +333 -0
- package/client-js/test-v2/queue.js +39 -0
- package/client-js/test-v2/run.js +187 -0
- package/client-js/test-v2/subscription.js +354 -0
- package/client-js/test-v2/transaction.js +443 -0
- package/package.json +1 -1
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queen Message Queue Client - Version 2
|
|
3
|
+
* Clean, fluent API with smart defaults
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { HttpClient } from './http/HttpClient.js'
|
|
7
|
+
import { LoadBalancer } from './http/LoadBalancer.js'
|
|
8
|
+
import { BufferManager } from './buffer/BufferManager.js'
|
|
9
|
+
import { QueueBuilder } from './builders/QueueBuilder.js'
|
|
10
|
+
import { TransactionBuilder } from './builders/TransactionBuilder.js'
|
|
11
|
+
import { CLIENT_DEFAULTS } from './utils/defaults.js'
|
|
12
|
+
import { validateUrl, validateUrls } from './utils/validation.js'
|
|
13
|
+
import * as logger from './utils/logger.js'
|
|
14
|
+
|
|
15
|
+
export class Queen {
|
|
16
|
+
#httpClient
|
|
17
|
+
#bufferManager
|
|
18
|
+
#config
|
|
19
|
+
#shutdownHandlers = []
|
|
20
|
+
|
|
21
|
+
constructor(config = {}) {
|
|
22
|
+
logger.log('Queen.constructor', { config: typeof config === 'object' && !Array.isArray(config) ? { ...config, urls: config.urls?.length || 0 } : { type: typeof config } })
|
|
23
|
+
|
|
24
|
+
// Normalize config
|
|
25
|
+
this.#config = this.#normalizeConfig(config)
|
|
26
|
+
|
|
27
|
+
// Create HTTP client
|
|
28
|
+
this.#httpClient = this.#createHttpClient()
|
|
29
|
+
|
|
30
|
+
// Create buffer manager
|
|
31
|
+
this.#bufferManager = new BufferManager(this.#httpClient)
|
|
32
|
+
|
|
33
|
+
// Setup graceful shutdown
|
|
34
|
+
this.#setupGracefulShutdown()
|
|
35
|
+
|
|
36
|
+
logger.log('Queen.constructor', { status: 'initialized', urls: this.#config.urls.length })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#normalizeConfig(config) {
|
|
40
|
+
// Handle different input formats
|
|
41
|
+
if (typeof config === 'string') {
|
|
42
|
+
// Single URL string
|
|
43
|
+
return {
|
|
44
|
+
...CLIENT_DEFAULTS,
|
|
45
|
+
urls: [validateUrl(config)]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(config)) {
|
|
50
|
+
// Array of URLs
|
|
51
|
+
return {
|
|
52
|
+
...CLIENT_DEFAULTS,
|
|
53
|
+
urls: validateUrls(config)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Object config
|
|
58
|
+
const normalized = {
|
|
59
|
+
...CLIENT_DEFAULTS,
|
|
60
|
+
...config
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Ensure URLs are validated
|
|
64
|
+
if (normalized.urls) {
|
|
65
|
+
normalized.urls = validateUrls(normalized.urls)
|
|
66
|
+
} else if (normalized.url) {
|
|
67
|
+
normalized.urls = [validateUrl(normalized.url)]
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error('Must provide urls or url in configuration')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return normalized
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#createHttpClient() {
|
|
76
|
+
const { urls, timeoutMillis, retryAttempts, retryDelayMillis, loadBalancingStrategy, enableFailover } = this.#config
|
|
77
|
+
|
|
78
|
+
if (urls.length === 1) {
|
|
79
|
+
// Single server
|
|
80
|
+
return new HttpClient({
|
|
81
|
+
baseUrl: urls[0],
|
|
82
|
+
timeoutMillis,
|
|
83
|
+
retryAttempts,
|
|
84
|
+
retryDelayMillis
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Multiple servers with load balancing
|
|
89
|
+
const loadBalancer = new LoadBalancer(urls, loadBalancingStrategy)
|
|
90
|
+
return new HttpClient({
|
|
91
|
+
loadBalancer,
|
|
92
|
+
timeoutMillis,
|
|
93
|
+
retryAttempts,
|
|
94
|
+
retryDelayMillis,
|
|
95
|
+
enableFailover
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#setupGracefulShutdown() {
|
|
100
|
+
let signalReceivedCount = 0;
|
|
101
|
+
const shutdown = async (signal) => {
|
|
102
|
+
console.log(`\nReceived ${signal}, shutting down gracefully...`)
|
|
103
|
+
try {
|
|
104
|
+
signalReceivedCount++;
|
|
105
|
+
if (signalReceivedCount > 1) {
|
|
106
|
+
console.log('Received multiple shutdown signals, exiting immediately')
|
|
107
|
+
process.exit(1)
|
|
108
|
+
}
|
|
109
|
+
await this.close()
|
|
110
|
+
process.exit(0)
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Error during shutdown:', error)
|
|
113
|
+
process.exit(1)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sigintHandler = () => shutdown('SIGINT')
|
|
118
|
+
const sigtermHandler = () => shutdown('SIGTERM')
|
|
119
|
+
|
|
120
|
+
process.on('SIGINT', sigintHandler)
|
|
121
|
+
process.on('SIGTERM', sigtermHandler)
|
|
122
|
+
|
|
123
|
+
// Store handlers for cleanup
|
|
124
|
+
this.#shutdownHandlers.push(
|
|
125
|
+
() => process.off('SIGINT', sigintHandler),
|
|
126
|
+
() => process.off('SIGTERM', sigtermHandler)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ===========================
|
|
131
|
+
// Queue Builder Entry Point
|
|
132
|
+
// ===========================
|
|
133
|
+
|
|
134
|
+
queue(name = null) {
|
|
135
|
+
return new QueueBuilder(this, this.#httpClient, this.#bufferManager, name)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ===========================
|
|
139
|
+
// Transaction API
|
|
140
|
+
// ===========================
|
|
141
|
+
|
|
142
|
+
transaction() {
|
|
143
|
+
return new TransactionBuilder(this.#httpClient)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ===========================
|
|
147
|
+
// Direct ACK API
|
|
148
|
+
// ===========================
|
|
149
|
+
|
|
150
|
+
async ack(message, status = true, context = {}) {
|
|
151
|
+
const isBatch = Array.isArray(message)
|
|
152
|
+
logger.log('Queen.ack', { isBatch, count: isBatch ? message.length : 1, status, context })
|
|
153
|
+
|
|
154
|
+
// Handle batch acknowledgment
|
|
155
|
+
if (Array.isArray(message)) {
|
|
156
|
+
if (message.length === 0) {
|
|
157
|
+
return { processed: 0, results: [] }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if messages have individual status
|
|
161
|
+
const hasIndividualStatus = message.some(msg =>
|
|
162
|
+
typeof msg === 'object' && msg !== null && ('_status' in msg || '_error' in msg)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
let acknowledgments
|
|
166
|
+
|
|
167
|
+
if (hasIndividualStatus) {
|
|
168
|
+
// Each message has its own status
|
|
169
|
+
acknowledgments = message.map(msg => {
|
|
170
|
+
const transactionId = typeof msg === 'string' ? msg : (msg.transactionId || msg.id)
|
|
171
|
+
const partitionId = typeof msg === 'object' ? msg.partitionId : null
|
|
172
|
+
const leaseId = typeof msg === 'object' ? msg.leaseId : null
|
|
173
|
+
|
|
174
|
+
if (!transactionId) {
|
|
175
|
+
throw new Error('Message must have transactionId or id property')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// CRITICAL: partitionId is now MANDATORY to prevent acking wrong message
|
|
179
|
+
if (!partitionId) {
|
|
180
|
+
throw new Error('Message must have partitionId property to ensure message uniqueness')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const msgStatus = msg._status !== undefined ? msg._status : status
|
|
184
|
+
const statusStr = typeof msgStatus === 'boolean'
|
|
185
|
+
? (msgStatus ? 'completed' : 'failed')
|
|
186
|
+
: msgStatus
|
|
187
|
+
|
|
188
|
+
const ack = {
|
|
189
|
+
transactionId,
|
|
190
|
+
partitionId,
|
|
191
|
+
status: statusStr,
|
|
192
|
+
error: msg._error || context.error || null
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (leaseId) ack.leaseId = leaseId
|
|
196
|
+
|
|
197
|
+
return ack
|
|
198
|
+
})
|
|
199
|
+
} else {
|
|
200
|
+
// Same status for all messages
|
|
201
|
+
const statusStr = typeof status === 'boolean'
|
|
202
|
+
? (status ? 'completed' : 'failed')
|
|
203
|
+
: status
|
|
204
|
+
|
|
205
|
+
acknowledgments = message.map(msg => {
|
|
206
|
+
const transactionId = typeof msg === 'string' ? msg : (msg.transactionId || msg.id)
|
|
207
|
+
const partitionId = typeof msg === 'object' ? msg.partitionId : null
|
|
208
|
+
const leaseId = typeof msg === 'object' ? msg.leaseId : null
|
|
209
|
+
|
|
210
|
+
if (!transactionId) {
|
|
211
|
+
throw new Error('Message must have transactionId or id property')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// CRITICAL: partitionId is now MANDATORY to prevent acking wrong message
|
|
215
|
+
if (!partitionId) {
|
|
216
|
+
throw new Error('Message must have partitionId property to ensure message uniqueness')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ack = {
|
|
220
|
+
transactionId,
|
|
221
|
+
partitionId,
|
|
222
|
+
status: statusStr,
|
|
223
|
+
error: context.error || null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (leaseId) ack.leaseId = leaseId
|
|
227
|
+
|
|
228
|
+
return ack
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Call batch ack endpoint
|
|
233
|
+
try {
|
|
234
|
+
const result = await this.#httpClient.post('/api/v1/ack/batch', {
|
|
235
|
+
acknowledgments,
|
|
236
|
+
consumerGroup: context.group || null
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
if (result && result.error) {
|
|
240
|
+
logger.error('Queen.ack', { type: 'batch', error: result.error })
|
|
241
|
+
return { success: false, error: result.error }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logger.log('Queen.ack', { type: 'batch', success: true, count: acknowledgments.length })
|
|
245
|
+
return { success: true, ...result }
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.error('Queen.ack', { type: 'batch', error: error.message })
|
|
248
|
+
return { success: false, error: error.message }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Handle single message acknowledgment
|
|
253
|
+
const transactionId = typeof message === 'string' ? message : (message.transactionId || message.id)
|
|
254
|
+
const partitionId = typeof message === 'object' ? message.partitionId : null
|
|
255
|
+
const leaseId = typeof message === 'object' ? message.leaseId : null
|
|
256
|
+
|
|
257
|
+
if (!transactionId) {
|
|
258
|
+
return { success: false, error: 'Message must have transactionId or id property' }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// CRITICAL: partitionId is now MANDATORY to prevent acking wrong message
|
|
262
|
+
if (!partitionId) {
|
|
263
|
+
return { success: false, error: 'Message must have partitionId property to ensure message uniqueness' }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const statusStr = typeof status === 'boolean'
|
|
267
|
+
? (status ? 'completed' : 'failed')
|
|
268
|
+
: status
|
|
269
|
+
|
|
270
|
+
const body = {
|
|
271
|
+
transactionId,
|
|
272
|
+
partitionId,
|
|
273
|
+
status: statusStr,
|
|
274
|
+
error: context.error || null,
|
|
275
|
+
consumerGroup: context.group || null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (leaseId) body.leaseId = leaseId
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const result = await this.#httpClient.post('/api/v1/ack', body)
|
|
282
|
+
|
|
283
|
+
if (result && result.error) {
|
|
284
|
+
logger.error('Queen.ack', { type: 'single', transactionId, error: result.error })
|
|
285
|
+
return { success: false, error: result.error }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
logger.log('Queen.ack', { type: 'single', transactionId, success: true })
|
|
289
|
+
return { success: true, ...result }
|
|
290
|
+
} catch (error) {
|
|
291
|
+
logger.error('Queen.ack', { type: 'single', transactionId, error: error.message })
|
|
292
|
+
return { success: false, error: error.message }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ===========================
|
|
297
|
+
// Lease Renewal API
|
|
298
|
+
// ===========================
|
|
299
|
+
|
|
300
|
+
async renew(messageOrLeaseId) {
|
|
301
|
+
let leaseIds = []
|
|
302
|
+
|
|
303
|
+
if (typeof messageOrLeaseId === 'string') {
|
|
304
|
+
leaseIds = [messageOrLeaseId]
|
|
305
|
+
} else if (Array.isArray(messageOrLeaseId)) {
|
|
306
|
+
leaseIds = messageOrLeaseId.map(item =>
|
|
307
|
+
typeof item === 'string' ? item : item.leaseId
|
|
308
|
+
).filter(Boolean)
|
|
309
|
+
} else if (messageOrLeaseId && typeof messageOrLeaseId === 'object') {
|
|
310
|
+
if (messageOrLeaseId.leaseId) {
|
|
311
|
+
leaseIds = [messageOrLeaseId.leaseId]
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (leaseIds.length === 0) {
|
|
316
|
+
logger.warn('Queen.renew', 'No valid lease IDs found for renewal')
|
|
317
|
+
return { success: false, error: 'No valid lease IDs found for renewal' }
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
logger.log('Queen.renew', { count: leaseIds.length })
|
|
321
|
+
|
|
322
|
+
const results = []
|
|
323
|
+
for (const leaseId of leaseIds) {
|
|
324
|
+
try {
|
|
325
|
+
const result = await this.#httpClient.post(`/api/v1/lease/${leaseId}/extend`, {})
|
|
326
|
+
results.push({
|
|
327
|
+
leaseId,
|
|
328
|
+
success: true,
|
|
329
|
+
newExpiresAt: result.leaseId ? result.newExpiresAt : result.lease_expires_at
|
|
330
|
+
})
|
|
331
|
+
logger.log('Queen.renew', { leaseId, success: true })
|
|
332
|
+
} catch (error) {
|
|
333
|
+
results.push({ leaseId, success: false, error: error.message })
|
|
334
|
+
logger.error('Queen.renew', { leaseId, error: error.message })
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
logger.log('Queen.renew', { total: results.length, successful: results.filter(r => r.success).length })
|
|
339
|
+
return Array.isArray(messageOrLeaseId) ? results : results[0]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ===========================
|
|
343
|
+
// Buffer Management API
|
|
344
|
+
// ===========================
|
|
345
|
+
|
|
346
|
+
async flushAllBuffers() {
|
|
347
|
+
logger.log('Queen.flushAllBuffers', 'Starting flush of all buffers')
|
|
348
|
+
await this.#bufferManager.flushAllBuffers()
|
|
349
|
+
logger.log('Queen.flushAllBuffers', 'Completed')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
getBufferStats() {
|
|
353
|
+
const stats = this.#bufferManager.getStats()
|
|
354
|
+
logger.log('Queen.getBufferStats', stats)
|
|
355
|
+
return stats
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ===========================
|
|
359
|
+
// Graceful Shutdown
|
|
360
|
+
// ===========================
|
|
361
|
+
|
|
362
|
+
async close() {
|
|
363
|
+
logger.log('Queen.close', 'Starting shutdown')
|
|
364
|
+
console.log('Closing Queen client...')
|
|
365
|
+
|
|
366
|
+
// Flush all buffers
|
|
367
|
+
try {
|
|
368
|
+
await this.#bufferManager.flushAllBuffers()
|
|
369
|
+
logger.log('Queen.close', 'All buffers flushed')
|
|
370
|
+
console.log('All buffers flushed')
|
|
371
|
+
} catch (error) {
|
|
372
|
+
logger.error('Queen.close', { error: error.message, phase: 'buffer-flush' })
|
|
373
|
+
console.warn('Error flushing buffers:', error)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Cleanup buffer manager
|
|
377
|
+
this.#bufferManager.cleanup()
|
|
378
|
+
|
|
379
|
+
// Remove shutdown handlers
|
|
380
|
+
for (const cleanup of this.#shutdownHandlers) {
|
|
381
|
+
cleanup()
|
|
382
|
+
}
|
|
383
|
+
this.#shutdownHandlers = []
|
|
384
|
+
|
|
385
|
+
logger.log('Queen.close', 'Client closed successfully')
|
|
386
|
+
console.log('Queen client closed')
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|