queen-mq 0.2.23 → 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/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,724 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue builder for fluent API
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { generateUUID } from '../../utils/uuid.js'
|
|
6
|
+
import { isValidUUID } from '../utils/validation.js'
|
|
7
|
+
import { QUEUE_DEFAULTS, CONSUME_DEFAULTS, POP_DEFAULTS } from '../utils/defaults.js'
|
|
8
|
+
import * as logger from '../utils/logger.js'
|
|
9
|
+
|
|
10
|
+
export class QueueBuilder {
|
|
11
|
+
#queen
|
|
12
|
+
#httpClient
|
|
13
|
+
#bufferManager
|
|
14
|
+
#queueName = null
|
|
15
|
+
#partition = 'Default'
|
|
16
|
+
#namespace = null
|
|
17
|
+
#task = null
|
|
18
|
+
#group = null
|
|
19
|
+
#config = {}
|
|
20
|
+
|
|
21
|
+
// Consume options
|
|
22
|
+
#concurrency = CONSUME_DEFAULTS.concurrency
|
|
23
|
+
#batch = CONSUME_DEFAULTS.batch
|
|
24
|
+
#limit = CONSUME_DEFAULTS.limit
|
|
25
|
+
#idleMillis = CONSUME_DEFAULTS.idleMillis
|
|
26
|
+
#autoAck = CONSUME_DEFAULTS.autoAck
|
|
27
|
+
#wait = CONSUME_DEFAULTS.wait
|
|
28
|
+
#timeoutMillis = CONSUME_DEFAULTS.timeoutMillis
|
|
29
|
+
#renewLease = CONSUME_DEFAULTS.renewLease
|
|
30
|
+
#renewLeaseIntervalMillis = CONSUME_DEFAULTS.renewLeaseIntervalMillis
|
|
31
|
+
#subscriptionMode = CONSUME_DEFAULTS.subscriptionMode
|
|
32
|
+
#subscriptionFrom = CONSUME_DEFAULTS.subscriptionFrom
|
|
33
|
+
#each = false
|
|
34
|
+
|
|
35
|
+
// Buffer options
|
|
36
|
+
#bufferOptions = null
|
|
37
|
+
|
|
38
|
+
constructor(queen, httpClient, bufferManager, queueName = null) {
|
|
39
|
+
this.#queen = queen
|
|
40
|
+
this.#httpClient = httpClient
|
|
41
|
+
this.#bufferManager = bufferManager
|
|
42
|
+
this.#queueName = queueName
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ===========================
|
|
46
|
+
// Queue Configuration Methods
|
|
47
|
+
// ===========================
|
|
48
|
+
|
|
49
|
+
namespace(name) {
|
|
50
|
+
this.#namespace = name
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
task(name) {
|
|
55
|
+
this.#task = name
|
|
56
|
+
return this
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
config(options) {
|
|
60
|
+
this.#config = { ...QUEUE_DEFAULTS, ...options }
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
create() {
|
|
65
|
+
// Always merge with QUEUE_DEFAULTS to ensure all options are sent
|
|
66
|
+
const fullConfig = Object.keys(this.#config).length > 0
|
|
67
|
+
? this.#config
|
|
68
|
+
: QUEUE_DEFAULTS
|
|
69
|
+
|
|
70
|
+
const payload = {
|
|
71
|
+
queue: this.#queueName,
|
|
72
|
+
namespace: this.#namespace,
|
|
73
|
+
task: this.#task,
|
|
74
|
+
options: fullConfig
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logger.log('QueueBuilder.create', { queue: this.#queueName, namespace: this.#namespace, task: this.#task })
|
|
78
|
+
return new OperationBuilder(this.#httpClient, 'POST', '/api/v1/configure', payload)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
delete() {
|
|
82
|
+
if (!this.#queueName) {
|
|
83
|
+
throw new Error('Queue name is required for delete operation')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.log('QueueBuilder.delete', { queue: this.#queueName })
|
|
87
|
+
return new OperationBuilder(
|
|
88
|
+
this.#httpClient,
|
|
89
|
+
'DELETE',
|
|
90
|
+
`/api/v1/resources/queues/${encodeURIComponent(this.#queueName)}`,
|
|
91
|
+
null
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ===========================
|
|
96
|
+
// Push Methods
|
|
97
|
+
// ===========================
|
|
98
|
+
|
|
99
|
+
partition(name) {
|
|
100
|
+
this.#partition = name
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
buffer(options) {
|
|
105
|
+
this.#bufferOptions = options
|
|
106
|
+
return this
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
push(payload) {
|
|
110
|
+
if (!this.#queueName) {
|
|
111
|
+
throw new Error('Queue name is required for push operation')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
logger.log('QueueBuilder.push', { queue: this.#queueName, partition: this.#partition, count: Array.isArray(payload) ? payload.length : 1, buffered: !!this.#bufferOptions })
|
|
115
|
+
|
|
116
|
+
// Format items
|
|
117
|
+
const items = Array.isArray(payload) ? payload : [payload]
|
|
118
|
+
const formattedItems = items.map(item => {
|
|
119
|
+
// Determine the payload - check if property exists, not just truthy
|
|
120
|
+
let payloadValue
|
|
121
|
+
if ('data' in item) {
|
|
122
|
+
payloadValue = item.data
|
|
123
|
+
} else if ('payload' in item) {
|
|
124
|
+
payloadValue = item.payload
|
|
125
|
+
} else {
|
|
126
|
+
payloadValue = item
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result = {
|
|
130
|
+
queue: this.#queueName,
|
|
131
|
+
partition: this.#partition,
|
|
132
|
+
payload: payloadValue,
|
|
133
|
+
transactionId: item.transactionId || generateUUID()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Include traceId if provided and valid UUID
|
|
137
|
+
if (item.traceId && isValidUUID(item.traceId)) {
|
|
138
|
+
result.traceId = item.traceId
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// Return a PushBuilder for chaining callbacks
|
|
145
|
+
return new PushBuilder(this.#httpClient, this.#bufferManager, this.#queueName, this.#partition, formattedItems, this.#bufferOptions)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ===========================
|
|
149
|
+
// Consume Configuration Methods
|
|
150
|
+
// ===========================
|
|
151
|
+
|
|
152
|
+
group(name) {
|
|
153
|
+
this.#group = name
|
|
154
|
+
return this
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
concurrency(count) {
|
|
158
|
+
this.#concurrency = Math.max(1, count)
|
|
159
|
+
return this
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
batch(size) {
|
|
163
|
+
this.#batch = Math.max(1, size)
|
|
164
|
+
return this
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
limit(count) {
|
|
168
|
+
this.#limit = count
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
idleMillis(millis) {
|
|
173
|
+
this.#idleMillis = millis
|
|
174
|
+
return this
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
autoAck(enabled) {
|
|
178
|
+
this.#autoAck = enabled
|
|
179
|
+
return this
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
renewLease(enabled, intervalMillis) {
|
|
183
|
+
this.#renewLease = enabled
|
|
184
|
+
if (intervalMillis) {
|
|
185
|
+
this.#renewLeaseIntervalMillis = intervalMillis
|
|
186
|
+
}
|
|
187
|
+
return this
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
subscriptionMode(mode) {
|
|
191
|
+
this.#subscriptionMode = mode
|
|
192
|
+
return this
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
subscriptionFrom(from) {
|
|
196
|
+
this.#subscriptionFrom = from
|
|
197
|
+
return this
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
each() {
|
|
201
|
+
this.#each = true
|
|
202
|
+
return this
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ===========================
|
|
206
|
+
// Consume Method
|
|
207
|
+
// ===========================
|
|
208
|
+
|
|
209
|
+
consume(handler, options = {}) {
|
|
210
|
+
const consumeOptions = {
|
|
211
|
+
queue: this.#queueName,
|
|
212
|
+
partition: this.#partition !== 'Default' ? this.#partition : null,
|
|
213
|
+
namespace: this.#namespace,
|
|
214
|
+
task: this.#task,
|
|
215
|
+
group: this.#group,
|
|
216
|
+
concurrency: this.#concurrency,
|
|
217
|
+
batch: this.#batch,
|
|
218
|
+
limit: this.#limit,
|
|
219
|
+
idleMillis: this.#idleMillis,
|
|
220
|
+
autoAck: this.#autoAck,
|
|
221
|
+
wait: this.#wait,
|
|
222
|
+
timeoutMillis: this.#timeoutMillis,
|
|
223
|
+
renewLease: this.#renewLease,
|
|
224
|
+
renewLeaseIntervalMillis: this.#renewLeaseIntervalMillis,
|
|
225
|
+
subscriptionMode: this.#subscriptionMode,
|
|
226
|
+
subscriptionFrom: this.#subscriptionFrom,
|
|
227
|
+
each: this.#each,
|
|
228
|
+
signal: options.signal
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return new ConsumeBuilder(this.#httpClient, this.#queen, handler, consumeOptions)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ===========================
|
|
235
|
+
// Pop Methods
|
|
236
|
+
// ===========================
|
|
237
|
+
|
|
238
|
+
wait(enabled) {
|
|
239
|
+
this.#wait = enabled
|
|
240
|
+
return this
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async pop() {
|
|
244
|
+
logger.log('QueueBuilder.pop', { queue: this.#queueName, partition: this.#partition, namespace: this.#namespace, task: this.#task, batch: this.#batch, wait: this.#wait, group: this.#group })
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const path = this.#buildPopPath()
|
|
248
|
+
|
|
249
|
+
// For pop(), use POP defaults (not CONSUME defaults)
|
|
250
|
+
// Override autoAck to false unless explicitly set
|
|
251
|
+
const effectiveAutoAck = this.#autoAck !== CONSUME_DEFAULTS.autoAck ? this.#autoAck : POP_DEFAULTS.autoAck
|
|
252
|
+
|
|
253
|
+
// Build params with correct autoAck for pop
|
|
254
|
+
const params = new URLSearchParams({
|
|
255
|
+
batch: this.#batch.toString(),
|
|
256
|
+
wait: this.#wait.toString(),
|
|
257
|
+
timeout: this.#timeoutMillis.toString()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
if (this.#group) params.append('consumerGroup', this.#group)
|
|
261
|
+
if (this.#namespace) params.append('namespace', this.#namespace)
|
|
262
|
+
if (this.#task) params.append('task', this.#task)
|
|
263
|
+
if (effectiveAutoAck) params.append('autoAck', 'true')
|
|
264
|
+
if (this.#subscriptionMode) params.append('subscriptionMode', this.#subscriptionMode)
|
|
265
|
+
if (this.#subscriptionFrom) params.append('subscriptionFrom', this.#subscriptionFrom)
|
|
266
|
+
|
|
267
|
+
const result = await this.#httpClient.get(`${path}?${params}`, this.#timeoutMillis + 5000)
|
|
268
|
+
|
|
269
|
+
if (!result || !result.messages) {
|
|
270
|
+
logger.log('QueueBuilder.pop', { status: 'no-messages' })
|
|
271
|
+
return []
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const messages = result.messages.filter(msg => msg != null)
|
|
275
|
+
logger.log('QueueBuilder.pop', { status: 'success', count: messages.length })
|
|
276
|
+
return messages
|
|
277
|
+
} catch (error) {
|
|
278
|
+
// Return empty array on error instead of throwing
|
|
279
|
+
logger.error('QueueBuilder.pop', { error: error.message })
|
|
280
|
+
console.warn('Pop failed:', error.message)
|
|
281
|
+
return []
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#buildPopPath() {
|
|
286
|
+
if (this.#queueName) {
|
|
287
|
+
if (this.#partition && this.#partition !== 'Default') {
|
|
288
|
+
return `/api/v1/pop/queue/${this.#queueName}/partition/${this.#partition}`
|
|
289
|
+
}
|
|
290
|
+
return `/api/v1/pop/queue/${this.#queueName}`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (this.#namespace || this.#task) {
|
|
294
|
+
return '/api/v1/pop'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
throw new Error('Must specify queue, namespace, or task for pop operation')
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#buildPopParams() {
|
|
301
|
+
const params = new URLSearchParams({
|
|
302
|
+
batch: this.#batch.toString(),
|
|
303
|
+
wait: this.#wait.toString(),
|
|
304
|
+
timeout: this.#timeoutMillis.toString() // Server expects 'timeout', not 'timeoutMillis'
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
if (this.#group) params.append('consumerGroup', this.#group)
|
|
308
|
+
if (this.#namespace) params.append('namespace', this.#namespace)
|
|
309
|
+
if (this.#task) params.append('task', this.#task)
|
|
310
|
+
if (this.#autoAck) params.append('autoAck', 'true')
|
|
311
|
+
if (this.#subscriptionMode) params.append('subscriptionMode', this.#subscriptionMode)
|
|
312
|
+
if (this.#subscriptionFrom) params.append('subscriptionFrom', this.#subscriptionFrom)
|
|
313
|
+
|
|
314
|
+
return params
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ===========================
|
|
318
|
+
// Buffer Management Methods
|
|
319
|
+
// ===========================
|
|
320
|
+
|
|
321
|
+
async flushBuffer() {
|
|
322
|
+
if (!this.#queueName) {
|
|
323
|
+
throw new Error('Queue name is required for buffer flush')
|
|
324
|
+
}
|
|
325
|
+
const queueAddress = `${this.#queueName}/${this.#partition}`
|
|
326
|
+
logger.log('QueueBuilder.flushBuffer', { queueAddress })
|
|
327
|
+
await this.#bufferManager.flushBuffer(queueAddress)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ===========================
|
|
331
|
+
// Dead Letter Queue Methods
|
|
332
|
+
// ===========================
|
|
333
|
+
|
|
334
|
+
dlq(consumerGroup = null) {
|
|
335
|
+
if (!this.#queueName) {
|
|
336
|
+
throw new Error('Queue name is required for DLQ operations')
|
|
337
|
+
}
|
|
338
|
+
logger.log('QueueBuilder.dlq', { queue: this.#queueName, consumerGroup, partition: this.#partition })
|
|
339
|
+
return new DLQBuilder(this.#httpClient, this.#queueName, consumerGroup, this.#partition)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Operation builder for create/delete operations with callbacks
|
|
345
|
+
*/
|
|
346
|
+
class OperationBuilder {
|
|
347
|
+
#httpClient
|
|
348
|
+
#method
|
|
349
|
+
#path
|
|
350
|
+
#body
|
|
351
|
+
#onSuccessCallback = null
|
|
352
|
+
#onErrorCallback = null
|
|
353
|
+
#executed = false
|
|
354
|
+
|
|
355
|
+
constructor(httpClient, method, path, body) {
|
|
356
|
+
this.#httpClient = httpClient
|
|
357
|
+
this.#method = method
|
|
358
|
+
this.#path = path
|
|
359
|
+
this.#body = body
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
onSuccess(callback) {
|
|
363
|
+
this.#onSuccessCallback = callback
|
|
364
|
+
return this
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
onError(callback) {
|
|
368
|
+
this.#onErrorCallback = callback
|
|
369
|
+
return this
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Auto-execute when awaited
|
|
373
|
+
then(resolve, reject) {
|
|
374
|
+
if (this.#executed) {
|
|
375
|
+
return Promise.resolve().then(resolve, reject)
|
|
376
|
+
}
|
|
377
|
+
this.#executed = true
|
|
378
|
+
return this.#execute().then(resolve, reject)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async #execute() {
|
|
382
|
+
logger.log('OperationBuilder.execute', { method: this.#method, path: this.#path })
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
let result
|
|
386
|
+
|
|
387
|
+
if (this.#method === 'GET') {
|
|
388
|
+
result = await this.#httpClient.get(this.#path)
|
|
389
|
+
} else if (this.#method === 'POST') {
|
|
390
|
+
result = await this.#httpClient.post(this.#path, this.#body)
|
|
391
|
+
} else if (this.#method === 'PUT') {
|
|
392
|
+
result = await this.#httpClient.put(this.#path, this.#body)
|
|
393
|
+
} else if (this.#method === 'DELETE') {
|
|
394
|
+
result = await this.#httpClient.delete(this.#path)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (result && result.error) {
|
|
398
|
+
const error = new Error(result.error)
|
|
399
|
+
logger.error('OperationBuilder.execute', { method: this.#method, path: this.#path, error: result.error })
|
|
400
|
+
if (this.#onErrorCallback) {
|
|
401
|
+
await this.#onErrorCallback(error)
|
|
402
|
+
return { success: false, error: result.error }
|
|
403
|
+
}
|
|
404
|
+
throw error
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
logger.log('OperationBuilder.execute', { method: this.#method, path: this.#path, status: 'success' })
|
|
408
|
+
if (this.#onSuccessCallback) {
|
|
409
|
+
await this.#onSuccessCallback(result)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
} catch (error) {
|
|
415
|
+
logger.error('OperationBuilder.execute', { method: this.#method, path: this.#path, error: error.message })
|
|
416
|
+
if (this.#onErrorCallback) {
|
|
417
|
+
await this.#onErrorCallback(error)
|
|
418
|
+
return { success: false, error: error.message }
|
|
419
|
+
}
|
|
420
|
+
throw error
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Consume builder for chaining callbacks
|
|
427
|
+
*/
|
|
428
|
+
class ConsumeBuilder {
|
|
429
|
+
#httpClient
|
|
430
|
+
#queen
|
|
431
|
+
#handler
|
|
432
|
+
#options
|
|
433
|
+
#onSuccessCallback = null
|
|
434
|
+
#onErrorCallback = null
|
|
435
|
+
#executed = false
|
|
436
|
+
|
|
437
|
+
constructor(httpClient, queen, handler, options) {
|
|
438
|
+
this.#httpClient = httpClient
|
|
439
|
+
this.#queen = queen
|
|
440
|
+
this.#handler = handler
|
|
441
|
+
this.#options = options
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
onSuccess(callback) {
|
|
445
|
+
this.#onSuccessCallback = callback
|
|
446
|
+
return this
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
onError(callback) {
|
|
450
|
+
this.#onErrorCallback = callback
|
|
451
|
+
return this
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Auto-execute when awaited
|
|
455
|
+
then(resolve, reject) {
|
|
456
|
+
if (this.#executed) {
|
|
457
|
+
return Promise.resolve().then(resolve, reject)
|
|
458
|
+
}
|
|
459
|
+
this.#executed = true
|
|
460
|
+
return this.#execute().then(resolve, reject)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async #execute() {
|
|
464
|
+
// Import ConsumerManager lazily to avoid circular dependency
|
|
465
|
+
const { ConsumerManager } = await import('../consumer/ConsumerManager.js')
|
|
466
|
+
const consumerManager = new ConsumerManager(this.#httpClient, this.#queen)
|
|
467
|
+
|
|
468
|
+
// Wrap the handler to include callback logic
|
|
469
|
+
const wrappedHandler = async (msgOrMsgs) => {
|
|
470
|
+
try {
|
|
471
|
+
const result = await this.#handler(msgOrMsgs)
|
|
472
|
+
|
|
473
|
+
// Call onSuccess if defined
|
|
474
|
+
if (this.#onSuccessCallback) {
|
|
475
|
+
await this.#onSuccessCallback(msgOrMsgs, result)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return result
|
|
479
|
+
} catch (error) {
|
|
480
|
+
// Call onError if defined
|
|
481
|
+
if (this.#onErrorCallback) {
|
|
482
|
+
await this.#onErrorCallback(msgOrMsgs, error)
|
|
483
|
+
// Don't re-throw if callback is defined
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
// Re-throw if no error callback
|
|
487
|
+
throw error
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// IMPORTANT: If callbacks are defined, auto-ack must be disabled
|
|
492
|
+
// to prevent double-acking (auto-ack + manual ack in callback)
|
|
493
|
+
const hasCallbacks = this.#onSuccessCallback || this.#onErrorCallback
|
|
494
|
+
const effectiveAutoAck = hasCallbacks ? false : this.#options.autoAck
|
|
495
|
+
|
|
496
|
+
const updatedOptions = {
|
|
497
|
+
...this.#options,
|
|
498
|
+
autoAck: effectiveAutoAck
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return consumerManager.start(wrappedHandler, updatedOptions)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Push builder for chaining callbacks
|
|
507
|
+
*/
|
|
508
|
+
class PushBuilder {
|
|
509
|
+
#httpClient
|
|
510
|
+
#bufferManager
|
|
511
|
+
#queueName
|
|
512
|
+
#partition
|
|
513
|
+
#formattedItems
|
|
514
|
+
#bufferOptions
|
|
515
|
+
#onSuccessCallback = null
|
|
516
|
+
#onErrorCallback = null
|
|
517
|
+
#onDuplicateCallback = null
|
|
518
|
+
#executed = false
|
|
519
|
+
|
|
520
|
+
constructor(httpClient, bufferManager, queueName, partition, formattedItems, bufferOptions) {
|
|
521
|
+
this.#httpClient = httpClient
|
|
522
|
+
this.#bufferManager = bufferManager
|
|
523
|
+
this.#queueName = queueName
|
|
524
|
+
this.#partition = partition
|
|
525
|
+
this.#formattedItems = formattedItems
|
|
526
|
+
this.#bufferOptions = bufferOptions
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
onSuccess(callback) {
|
|
530
|
+
this.#onSuccessCallback = callback
|
|
531
|
+
return this
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
onError(callback) {
|
|
535
|
+
this.#onErrorCallback = callback
|
|
536
|
+
return this
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
onDuplicate(callback) {
|
|
540
|
+
this.#onDuplicateCallback = callback
|
|
541
|
+
return this
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Auto-execute when awaited
|
|
545
|
+
then(resolve, reject) {
|
|
546
|
+
if (this.#executed) {
|
|
547
|
+
return Promise.resolve().then(resolve, reject)
|
|
548
|
+
}
|
|
549
|
+
this.#executed = true
|
|
550
|
+
return this.#execute().then(resolve, reject)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async #execute() {
|
|
554
|
+
logger.log('PushBuilder.execute', { queue: this.#queueName, partition: this.#partition, count: this.#formattedItems.length, buffered: !!this.#bufferOptions })
|
|
555
|
+
|
|
556
|
+
// Client-side buffering
|
|
557
|
+
if (this.#bufferOptions) {
|
|
558
|
+
for (const item of this.#formattedItems) {
|
|
559
|
+
const queueAddress = `${this.#queueName}/${this.#partition}`
|
|
560
|
+
this.#bufferManager.addMessage(queueAddress, item, this.#bufferOptions)
|
|
561
|
+
}
|
|
562
|
+
const result = { buffered: true, count: this.#formattedItems.length }
|
|
563
|
+
|
|
564
|
+
logger.log('PushBuilder.execute', { status: 'buffered', count: this.#formattedItems.length })
|
|
565
|
+
|
|
566
|
+
if (this.#onSuccessCallback) {
|
|
567
|
+
await this.#onSuccessCallback(this.#formattedItems)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return result
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Immediate push
|
|
574
|
+
try {
|
|
575
|
+
const results = await this.#httpClient.post('/api/v1/push', { items: this.#formattedItems })
|
|
576
|
+
|
|
577
|
+
// Server returns an array of results with status for each item
|
|
578
|
+
if (Array.isArray(results)) {
|
|
579
|
+
// Separate results by status
|
|
580
|
+
const successful = []
|
|
581
|
+
const duplicates = []
|
|
582
|
+
const failed = []
|
|
583
|
+
|
|
584
|
+
for (let i = 0; i < results.length; i++) {
|
|
585
|
+
const result = results[i]
|
|
586
|
+
const originalItem = this.#formattedItems[i]
|
|
587
|
+
|
|
588
|
+
if (result.status === 'duplicate') {
|
|
589
|
+
duplicates.push({ ...originalItem, result })
|
|
590
|
+
} else if (result.status === 'failed') {
|
|
591
|
+
failed.push({ ...originalItem, result, error: result.error })
|
|
592
|
+
} else if (result.status === 'queued') {
|
|
593
|
+
successful.push({ ...originalItem, result })
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Call appropriate callbacks
|
|
598
|
+
if (duplicates.length > 0 && this.#onDuplicateCallback) {
|
|
599
|
+
await this.#onDuplicateCallback(duplicates, new Error('Duplicate transaction IDs detected'))
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (failed.length > 0 && this.#onErrorCallback) {
|
|
603
|
+
const error = new Error(failed[0].error || 'Push failed')
|
|
604
|
+
await this.#onErrorCallback(failed, error)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (successful.length > 0 && this.#onSuccessCallback) {
|
|
608
|
+
await this.#onSuccessCallback(successful)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Only throw if no error callback is defined
|
|
612
|
+
if (failed.length > 0 && !this.#onErrorCallback) {
|
|
613
|
+
logger.error('PushBuilder.execute', { status: 'failed', count: failed.length })
|
|
614
|
+
throw new Error(failed[0].error || 'Push failed')
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
logger.log('PushBuilder.execute', { status: 'success', successful: successful.length, duplicates: duplicates.length, failed: failed.length })
|
|
618
|
+
return results
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Fallback for non-array responses
|
|
622
|
+
if (results && results.error) {
|
|
623
|
+
const error = new Error(results.error)
|
|
624
|
+
if (this.#onErrorCallback) {
|
|
625
|
+
await this.#onErrorCallback(this.#formattedItems, error)
|
|
626
|
+
return results // Don't throw if callback is defined
|
|
627
|
+
}
|
|
628
|
+
throw error
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (this.#onSuccessCallback) {
|
|
632
|
+
await this.#onSuccessCallback(this.#formattedItems)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return results
|
|
636
|
+
|
|
637
|
+
} catch (error) {
|
|
638
|
+
// Network or HTTP errors
|
|
639
|
+
if (this.#onErrorCallback) {
|
|
640
|
+
await this.#onErrorCallback(this.#formattedItems, error)
|
|
641
|
+
return null // Don't throw if callback is defined
|
|
642
|
+
}
|
|
643
|
+
throw error
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* DLQ (Dead Letter Queue) builder for querying failed messages
|
|
650
|
+
*/
|
|
651
|
+
class DLQBuilder {
|
|
652
|
+
#httpClient
|
|
653
|
+
#queueName
|
|
654
|
+
#consumerGroup
|
|
655
|
+
#partition
|
|
656
|
+
#limit = 100
|
|
657
|
+
#offset = 0
|
|
658
|
+
#from = null
|
|
659
|
+
#to = null
|
|
660
|
+
|
|
661
|
+
constructor(httpClient, queueName, consumerGroup, partition) {
|
|
662
|
+
this.#httpClient = httpClient
|
|
663
|
+
this.#queueName = queueName
|
|
664
|
+
this.#consumerGroup = consumerGroup
|
|
665
|
+
this.#partition = partition !== 'Default' ? partition : null
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
limit(count) {
|
|
669
|
+
this.#limit = Math.max(1, count)
|
|
670
|
+
return this
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
offset(count) {
|
|
674
|
+
this.#offset = Math.max(0, count)
|
|
675
|
+
return this
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
from(timestamp) {
|
|
679
|
+
this.#from = timestamp
|
|
680
|
+
return this
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
to(timestamp) {
|
|
684
|
+
this.#to = timestamp
|
|
685
|
+
return this
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async get() {
|
|
689
|
+
const params = new URLSearchParams()
|
|
690
|
+
|
|
691
|
+
params.append('queue', this.#queueName)
|
|
692
|
+
params.append('limit', this.#limit.toString())
|
|
693
|
+
params.append('offset', this.#offset.toString())
|
|
694
|
+
|
|
695
|
+
if (this.#consumerGroup) {
|
|
696
|
+
params.append('consumerGroup', this.#consumerGroup)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (this.#partition) {
|
|
700
|
+
params.append('partition', this.#partition)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (this.#from) {
|
|
704
|
+
params.append('from', this.#from)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (this.#to) {
|
|
708
|
+
params.append('to', this.#to)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
logger.log('DLQBuilder.get', { queue: this.#queueName, consumerGroup: this.#consumerGroup, partition: this.#partition, limit: this.#limit, offset: this.#offset })
|
|
712
|
+
|
|
713
|
+
try {
|
|
714
|
+
const result = await this.#httpClient.get(`/api/v1/dlq?${params}`)
|
|
715
|
+
logger.log('DLQBuilder.get', { status: 'success', total: result?.total || 0, messages: result?.messages?.length || 0 })
|
|
716
|
+
return result || { messages: [], total: 0 }
|
|
717
|
+
} catch (error) {
|
|
718
|
+
logger.error('DLQBuilder.get', { error: error.message })
|
|
719
|
+
console.warn('DLQ query failed:', error.message)
|
|
720
|
+
return { messages: [], total: 0 }
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|