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.
- package/README.md +162 -136
- 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 +2 -2
|
@@ -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
|
+
|