queen-mq 0.2.23 → 0.3.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.
Files changed (35) hide show
  1. package/README.md +162 -136
  2. package/client-js/client-v2/LOGGING.md +240 -0
  3. package/client-js/client-v2/Queen.js +389 -0
  4. package/client-js/client-v2/README.md +1883 -0
  5. package/client-js/client-v2/buffer/BufferManager.js +215 -0
  6. package/client-js/client-v2/buffer/MessageBuffer.js +132 -0
  7. package/client-js/client-v2/builders/QueueBuilder.js +724 -0
  8. package/client-js/client-v2/builders/TransactionBuilder.js +110 -0
  9. package/client-js/client-v2/consumer/ConsumerManager.js +390 -0
  10. package/client-js/client-v2/http/HttpClient.js +215 -0
  11. package/client-js/client-v2/http/LoadBalancer.js +50 -0
  12. package/client-js/client-v2/index.js +7 -0
  13. package/client-js/client-v2/utils/defaults.js +54 -0
  14. package/client-js/client-v2/utils/logger.js +54 -0
  15. package/client-js/client-v2/utils/validation.js +31 -0
  16. package/client-js/test-v2/AI_TEST_SUMMARY.md +226 -0
  17. package/client-js/test-v2/GETTING_STARTED.md +154 -0
  18. package/client-js/test-v2/ai_buffering.js +194 -0
  19. package/client-js/test-v2/ai_error_handling.js +223 -0
  20. package/client-js/test-v2/ai_lease_renewal.js +206 -0
  21. package/client-js/test-v2/ai_mixed_scenarios.js +278 -0
  22. package/client-js/test-v2/ai_priority.js +169 -0
  23. package/client-js/test-v2/ai_resources.js +217 -0
  24. package/client-js/test-v2/ai_ttl_retention.js +170 -0
  25. package/client-js/test-v2/complete.js +59 -0
  26. package/client-js/test-v2/consume.js +655 -0
  27. package/client-js/test-v2/dlq.js +82 -0
  28. package/client-js/test-v2/load.js +177 -0
  29. package/client-js/test-v2/pop.js +114 -0
  30. package/client-js/test-v2/push.js +333 -0
  31. package/client-js/test-v2/queue.js +39 -0
  32. package/client-js/test-v2/run.js +187 -0
  33. package/client-js/test-v2/subscription.js +354 -0
  34. package/client-js/test-v2/transaction.js +443 -0
  35. package/package.json +2 -2
@@ -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
+