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.
Files changed (37) hide show
  1. package/README.md +162 -136
  2. package/client-js/benchmark/consumer.js +1 -1
  3. package/client-js/client/client.js +24 -3
  4. package/client-js/client-v2/LOGGING.md +240 -0
  5. package/client-js/client-v2/Queen.js +389 -0
  6. package/client-js/client-v2/README.md +1883 -0
  7. package/client-js/client-v2/buffer/BufferManager.js +215 -0
  8. package/client-js/client-v2/buffer/MessageBuffer.js +132 -0
  9. package/client-js/client-v2/builders/QueueBuilder.js +724 -0
  10. package/client-js/client-v2/builders/TransactionBuilder.js +110 -0
  11. package/client-js/client-v2/consumer/ConsumerManager.js +390 -0
  12. package/client-js/client-v2/http/HttpClient.js +215 -0
  13. package/client-js/client-v2/http/LoadBalancer.js +50 -0
  14. package/client-js/client-v2/index.js +7 -0
  15. package/client-js/client-v2/utils/defaults.js +54 -0
  16. package/client-js/client-v2/utils/logger.js +54 -0
  17. package/client-js/client-v2/utils/validation.js +31 -0
  18. package/client-js/test-v2/AI_TEST_SUMMARY.md +226 -0
  19. package/client-js/test-v2/GETTING_STARTED.md +154 -0
  20. package/client-js/test-v2/ai_buffering.js +194 -0
  21. package/client-js/test-v2/ai_error_handling.js +223 -0
  22. package/client-js/test-v2/ai_lease_renewal.js +206 -0
  23. package/client-js/test-v2/ai_mixed_scenarios.js +278 -0
  24. package/client-js/test-v2/ai_priority.js +169 -0
  25. package/client-js/test-v2/ai_resources.js +217 -0
  26. package/client-js/test-v2/ai_ttl_retention.js +170 -0
  27. package/client-js/test-v2/complete.js +59 -0
  28. package/client-js/test-v2/consume.js +655 -0
  29. package/client-js/test-v2/dlq.js +82 -0
  30. package/client-js/test-v2/load.js +177 -0
  31. package/client-js/test-v2/pop.js +114 -0
  32. package/client-js/test-v2/push.js +333 -0
  33. package/client-js/test-v2/queue.js +39 -0
  34. package/client-js/test-v2/run.js +187 -0
  35. package/client-js/test-v2/subscription.js +354 -0
  36. package/client-js/test-v2/transaction.js +443 -0
  37. package/package.json +1 -1
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Buffer manager for client-side message buffering across queues
3
+ */
4
+
5
+ import { MessageBuffer } from './MessageBuffer.js'
6
+ import { BUFFER_DEFAULTS } from '../utils/defaults.js'
7
+ import * as logger from '../utils/logger.js'
8
+
9
+ export class BufferManager {
10
+ #httpClient
11
+ #buffers = new Map() // queueAddress -> MessageBuffer
12
+ #pendingFlushes = new Set() // Track in-flight flush promises
13
+ #flushCount = 0
14
+
15
+ constructor(httpClient) {
16
+ this.#httpClient = httpClient
17
+ }
18
+
19
+ addMessage(queueAddress, formattedMessage, bufferOptions) {
20
+ const options = { ...BUFFER_DEFAULTS, ...bufferOptions }
21
+
22
+ if (!this.#buffers.has(queueAddress)) {
23
+ logger.log('BufferManager.createBuffer', { queueAddress, options })
24
+ this.#buffers.set(queueAddress, new MessageBuffer(
25
+ queueAddress,
26
+ options,
27
+ (addr) => this.#flushBuffer(addr)
28
+ ))
29
+ }
30
+
31
+ const buffer = this.#buffers.get(queueAddress)
32
+ buffer.add(formattedMessage)
33
+ logger.log('BufferManager.addMessage', { queueAddress, messageCount: buffer.messageCount })
34
+ }
35
+
36
+ async #flushBuffer(queueAddress) {
37
+ const buffer = this.#buffers.get(queueAddress)
38
+ if (!buffer || buffer.messageCount === 0) {
39
+ logger.log('BufferManager.flushBuffer', { queueAddress, status: 'empty' })
40
+ console.log(`No buffer or empty buffer for ${queueAddress}`)
41
+ return
42
+ }
43
+
44
+ logger.log('BufferManager.flushBuffer', { queueAddress, messageCount: buffer.messageCount })
45
+ console.log(`Flushing ${buffer.messageCount} messages for ${queueAddress}`)
46
+ buffer.setFlushing(true)
47
+
48
+ // Create a promise for this flush and track it
49
+ const flushPromise = (async () => {
50
+ try {
51
+ const messages = buffer.extractMessages()
52
+
53
+ console.log(`Extracted ${messages.length} messages, sending to server...`)
54
+
55
+ if (messages.length === 0) return
56
+
57
+ // Send to server
58
+ const result = await this.#httpClient.post('/api/v1/push', { items: messages })
59
+ console.log(`Server responded:`, result ? `${result.length || 'N/A'} items` : 'null')
60
+
61
+ this.#flushCount++
62
+ logger.log('BufferManager.flushBuffer', { queueAddress, status: 'success', messagesSent: messages.length })
63
+
64
+ // Remove empty buffer
65
+ this.#buffers.delete(queueAddress)
66
+
67
+ } catch (error) {
68
+ logger.error('BufferManager.flushBuffer', { queueAddress, error: error.message })
69
+ console.error(`Flush error for ${queueAddress}:`, error.message)
70
+ buffer.setFlushing(false)
71
+ throw error
72
+ } finally {
73
+ // Remove from pending flushes
74
+ this.#pendingFlushes.delete(flushPromise)
75
+ }
76
+ })()
77
+
78
+ // Track this flush
79
+ this.#pendingFlushes.add(flushPromise)
80
+
81
+ return flushPromise
82
+ }
83
+
84
+ async #flushBufferBatch(queueAddress, batchSize) {
85
+ const buffer = this.#buffers.get(queueAddress)
86
+ if (!buffer || buffer.messageCount === 0) {
87
+ return
88
+ }
89
+
90
+ buffer.setFlushing(true)
91
+
92
+ // Create a promise for this flush and track it
93
+ const flushPromise = (async () => {
94
+ try {
95
+ const messages = buffer.extractMessages(batchSize)
96
+
97
+ console.log(`Extracted ${messages.length} messages, sending to server...`)
98
+
99
+ if (messages.length === 0) return
100
+
101
+ // Send to server
102
+ const result = await this.#httpClient.post('/api/v1/push', { items: messages })
103
+ console.log(`Server responded:`, result ? `${result.length || 'N/A'} items` : 'null')
104
+
105
+ this.#flushCount++
106
+
107
+ // Remove empty buffer if no more messages
108
+ if (buffer.messageCount === 0) {
109
+ this.#buffers.delete(queueAddress)
110
+ } else {
111
+ buffer.setFlushing(false)
112
+ }
113
+
114
+ } catch (error) {
115
+ console.error(`Flush error for ${queueAddress}:`, error.message)
116
+ buffer.setFlushing(false)
117
+ throw error
118
+ } finally {
119
+ // Remove from pending flushes
120
+ this.#pendingFlushes.delete(flushPromise)
121
+ }
122
+ })()
123
+
124
+ // Track this flush
125
+ this.#pendingFlushes.add(flushPromise)
126
+
127
+ return flushPromise
128
+ }
129
+
130
+ async flushBuffer(queueAddress) {
131
+ logger.log('BufferManager.flushBuffer', { queueAddress, activeBuffers: this.#buffers.size, pendingFlushes: this.#pendingFlushes.size })
132
+ console.log(`flushBuffer called for address: ${queueAddress}`)
133
+ console.log(`Active buffers:`, Array.from(this.#buffers.keys()))
134
+ console.log(`Pending flushes:`, this.#pendingFlushes.size)
135
+
136
+ const buffer = this.#buffers.get(queueAddress)
137
+ if (!buffer) {
138
+ logger.log('BufferManager.flushBuffer', { queueAddress, status: 'not-found' })
139
+ console.log(`No buffer found for ${queueAddress}`)
140
+ await this.#waitForPendingFlushes()
141
+ return
142
+ }
143
+
144
+ // Cancel timer to prevent time-based flush
145
+ buffer.cancelTimer()
146
+
147
+ // Get the batch size from buffer options
148
+ const batchSize = buffer.options.messageCount
149
+
150
+ // Flush all messages in batches
151
+ while (buffer.messageCount > 0) {
152
+ console.log(`Flushing batch of up to ${batchSize} messages (${buffer.messageCount} remaining)`)
153
+ await this.#flushBufferBatch(queueAddress, batchSize)
154
+ }
155
+
156
+ // Wait for all pending flushes to complete
157
+ await this.#waitForPendingFlushes()
158
+
159
+ console.log(`flushBuffer completed for ${queueAddress}`)
160
+ }
161
+
162
+ async flushAllBuffers() {
163
+ const queueAddresses = Array.from(this.#buffers.keys())
164
+ logger.log('BufferManager.flushAllBuffers', { bufferCount: queueAddresses.length, pendingFlushes: this.#pendingFlushes.size })
165
+ console.log(`flushAllBuffers called, pending flushes: ${this.#pendingFlushes.size}`)
166
+
167
+ // Get all queue addresses that have buffers
168
+
169
+ // Flush each buffer in batches
170
+ for (const queueAddress of queueAddresses) {
171
+ await this.flushBuffer(queueAddress)
172
+ }
173
+
174
+ logger.log('BufferManager.flushAllBuffers', { status: 'completed' })
175
+ console.log(`flushAllBuffers completed`)
176
+ }
177
+
178
+ async #waitForPendingFlushes() {
179
+ if (this.#pendingFlushes.size === 0) return
180
+
181
+ console.log(`Waiting for ${this.#pendingFlushes.size} pending flushes...`)
182
+ await Promise.all(Array.from(this.#pendingFlushes))
183
+ console.log(`All pending flushes completed`)
184
+ }
185
+
186
+ getStats() {
187
+ let totalBufferedMessages = 0
188
+ let oldestBufferAge = 0
189
+
190
+ for (const buffer of this.#buffers.values()) {
191
+ totalBufferedMessages += buffer.messageCount
192
+ const age = buffer.firstMessageAge
193
+ oldestBufferAge = Math.max(oldestBufferAge, age)
194
+ }
195
+
196
+ const stats = {
197
+ activeBuffers: this.#buffers.size,
198
+ totalBufferedMessages,
199
+ oldestBufferAge,
200
+ flushesPerformed: this.#flushCount
201
+ }
202
+
203
+ logger.log('BufferManager.getStats', stats)
204
+ return stats
205
+ }
206
+
207
+ cleanup() {
208
+ logger.log('BufferManager.cleanup', { bufferCount: this.#buffers.size })
209
+ for (const buffer of this.#buffers.values()) {
210
+ buffer.cleanup()
211
+ }
212
+ this.#buffers.clear()
213
+ }
214
+ }
215
+
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Message buffer for a single queue
3
+ */
4
+
5
+ export class MessageBuffer {
6
+ #queueAddress
7
+ #messages = []
8
+ #options
9
+ #flushCallback
10
+ #timer = null
11
+ #firstMessageTime = null
12
+ #flushing = false
13
+
14
+ constructor(queueAddress, options, flushCallback) {
15
+ this.#queueAddress = queueAddress
16
+ this.#options = options
17
+ this.#flushCallback = flushCallback
18
+ }
19
+
20
+ add(formattedMessage) {
21
+ // Set first message time if this is the first message
22
+ if (this.#messages.length === 0) {
23
+ this.#firstMessageTime = Date.now()
24
+ this.#startTimer()
25
+ }
26
+
27
+ this.#messages.push(formattedMessage)
28
+
29
+ // Check if we should flush based on size
30
+ if (this.#messages.length >= this.#options.messageCount) {
31
+ this.#triggerFlush()
32
+ }
33
+ }
34
+
35
+ #startTimer() {
36
+ if (this.#timer) return // Timer already running
37
+
38
+ this.#timer = setTimeout(() => {
39
+ this.#triggerFlush()
40
+ }, this.#options.timeMillis)
41
+ }
42
+
43
+ #triggerFlush() {
44
+ if (this.#flushing || this.#messages.length === 0) return
45
+
46
+ // Clear timer
47
+ if (this.#timer) {
48
+ clearTimeout(this.#timer)
49
+ this.#timer = null
50
+ }
51
+
52
+ // Trigger flush via callback
53
+ this.#flushCallback(this.#queueAddress)
54
+ }
55
+
56
+ extractMessages(batchSize = null) {
57
+ // If no batch size specified, extract all messages
58
+ if (batchSize === null || batchSize >= this.#messages.length) {
59
+ const messages = [...this.#messages]
60
+ this.#messages = []
61
+ this.#firstMessageTime = null
62
+ this.#flushing = false
63
+
64
+ if (this.#timer) {
65
+ clearTimeout(this.#timer)
66
+ this.#timer = null
67
+ }
68
+
69
+ return messages
70
+ }
71
+
72
+ // Extract a batch of messages
73
+ const messages = this.#messages.splice(0, batchSize)
74
+
75
+ // If buffer is now empty, reset state
76
+ if (this.#messages.length === 0) {
77
+ this.#firstMessageTime = null
78
+ this.#flushing = false
79
+
80
+ if (this.#timer) {
81
+ clearTimeout(this.#timer)
82
+ this.#timer = null
83
+ }
84
+ }
85
+
86
+ return messages
87
+ }
88
+
89
+ setFlushing(value) {
90
+ this.#flushing = value
91
+ }
92
+
93
+ forceFlush() {
94
+ // Immediately trigger flush, ignoring timers
95
+ if (this.#timer) {
96
+ clearTimeout(this.#timer)
97
+ this.#timer = null
98
+ }
99
+ this.#triggerFlush()
100
+ }
101
+
102
+ cancelTimer() {
103
+ // Cancel the timer without triggering a flush
104
+ if (this.#timer) {
105
+ clearTimeout(this.#timer)
106
+ this.#timer = null
107
+ }
108
+ }
109
+
110
+ get messageCount() {
111
+ return this.#messages.length
112
+ }
113
+
114
+ get options() {
115
+ return this.#options
116
+ }
117
+
118
+ get firstMessageAge() {
119
+ return this.#firstMessageTime ? Date.now() - this.#firstMessageTime : 0
120
+ }
121
+
122
+ cleanup() {
123
+ if (this.#timer) {
124
+ clearTimeout(this.#timer)
125
+ this.#timer = null
126
+ }
127
+ this.#messages = []
128
+ this.#firstMessageTime = null
129
+ this.#flushing = false
130
+ }
131
+ }
132
+