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,1883 @@
|
|
|
1
|
+
# 👑 Queen Client
|
|
2
|
+
|
|
3
|
+
Welcome to Queen client! This is your friendly guide to mastering message queues without losing your sanity. We'll start simple and gradually unlock the superpowers. 🚀
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Getting Started](#getting-started)
|
|
8
|
+
- [Part 1: Hello Queue!](#part-1-hello-queue)
|
|
9
|
+
- [Part 2: Push & Consume Basics](#part-2-push--consume-basics)
|
|
10
|
+
- [Part 3: Pop vs Consume (Choose Your Adventure)](#part-3-pop-vs-consume-choose-your-adventure)
|
|
11
|
+
- [Part 4: Partitions - Organize Your World](#part-4-partitions---organize-your-world)
|
|
12
|
+
- [Part 5: Consumer Groups - Share the Load](#part-5-consumer-groups---share-the-load)
|
|
13
|
+
- [Part 5.5: Subscription Modes - Control Message History](#part-55-subscription-modes---control-message-history)
|
|
14
|
+
- [Part 6: Namespaces & Tasks - The Wildcard Way](#part-6-namespaces--tasks---the-wildcard-way)
|
|
15
|
+
- [Part 7: Transactions - All or Nothing](#part-7-transactions---all-or-nothing)
|
|
16
|
+
- [Part 8: Client-Side Buffering - Speed Demon Mode](#part-8-client-side-buffering---speed-demon-mode)
|
|
17
|
+
- [Part 9: Dead Letter Queue - When Things Go Wrong](#part-9-dead-letter-queue---when-things-go-wrong)
|
|
18
|
+
- [Part 10: Lease Renewal - Keep It Locked](#part-10-lease-renewal---keep-it-locked)
|
|
19
|
+
- [Part 11: Queue Configuration - Fine Tuning](#part-11-queue-configuration---fine-tuning)
|
|
20
|
+
- [Part 12: Message Tracing - Debug Your Workflows](#part-12-message-tracing---debug-your-workflows)
|
|
21
|
+
- [Part 13: Callbacks & Error Handling](#part-13-callbacks--error-handling)
|
|
22
|
+
- [Part 14: Graceful Shutdown](#part-14-graceful-shutdown)
|
|
23
|
+
- [Cheat Sheet](#cheat-sheet)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
First, install and import:
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
import { Queen } from './client-js/client-v2/index.js'
|
|
33
|
+
|
|
34
|
+
// Connect to your Queen server
|
|
35
|
+
const queen = new Queen('http://localhost:6632')
|
|
36
|
+
|
|
37
|
+
// Or with multiple servers for high availability
|
|
38
|
+
const queen = new Queen(['http://server1:6632', 'http://server2:6632'])
|
|
39
|
+
|
|
40
|
+
// Or with full configuration
|
|
41
|
+
const queen = new Queen({
|
|
42
|
+
urls: ['http://server1:6632', 'http://server2:6632'],
|
|
43
|
+
timeoutMillis: 30000,
|
|
44
|
+
retryAttempts: 3,
|
|
45
|
+
loadBalancingStrategy: 'round-robin', // or 'session'
|
|
46
|
+
enableFailover: true
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it! You're connected. Now let's do something fun. 🎉
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Part 1: Hello Queue!
|
|
55
|
+
|
|
56
|
+
Every journey starts with a queue. Let's create one:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
// Create a simple queue
|
|
60
|
+
await queen.queue('my-tasks').create()
|
|
61
|
+
|
|
62
|
+
// That's it! The queue exists now with sensible defaults
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Need to customize it? We'll get to that later. For now, let's keep it simple.
|
|
66
|
+
|
|
67
|
+
Want to delete a queue? (Be careful! ⚠️)
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
await queen.queue('my-tasks').delete()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Part 2: Push & Consume Basics
|
|
76
|
+
|
|
77
|
+
### Pushing Messages (aka "Adding Work to Do")
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
// Push a single message
|
|
81
|
+
await queen.queue('my-tasks').push([
|
|
82
|
+
{ data: { job: 'send-email', to: 'alice@example.com' } }
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
// Push multiple messages at once
|
|
86
|
+
await queen.queue('my-tasks').push([
|
|
87
|
+
{ data: { job: 'send-email', to: 'alice@example.com' } },
|
|
88
|
+
{ data: { job: 'send-email', to: 'bob@example.com' } },
|
|
89
|
+
{ data: { job: 'resize-image', id: 123 } }
|
|
90
|
+
])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Pro tip:** Notice the array? Always wrap your messages in an array, even for a single message.
|
|
94
|
+
|
|
95
|
+
### Consuming Messages (aka "Getting Work Done")
|
|
96
|
+
|
|
97
|
+
The easiest way to process messages:
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
await queen.queue('my-tasks').consume(async (message) => {
|
|
101
|
+
console.log('Processing:', message.data)
|
|
102
|
+
|
|
103
|
+
// Do your work here
|
|
104
|
+
await sendEmail(message.data.to)
|
|
105
|
+
|
|
106
|
+
// That's it! If your function succeeds, the message is automatically acknowledged
|
|
107
|
+
// If it throws an error, the message is automatically rejected and will retry
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**What just happened?**
|
|
112
|
+
1. The consumer pulls messages from the queue
|
|
113
|
+
2. Your function processes each message
|
|
114
|
+
3. If successful → message is marked as complete ✅
|
|
115
|
+
4. If error → message goes back to the queue for retry 🔄
|
|
116
|
+
|
|
117
|
+
This runs **forever** by default. Perfect for background workers!
|
|
118
|
+
|
|
119
|
+
Want to process just a few messages and stop?
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// Process exactly 10 messages then stop
|
|
123
|
+
await queen
|
|
124
|
+
.queue('my-tasks')
|
|
125
|
+
.limit(10)
|
|
126
|
+
.consume(async (message) => {
|
|
127
|
+
console.log('Processing:', message.data)
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Part 3: Pop vs Consume (Choose Your Adventure)
|
|
134
|
+
|
|
135
|
+
### The Consume Way (Recommended for Workers)
|
|
136
|
+
|
|
137
|
+
**Use when:** You want a long-running worker that continuously processes messages.
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Runs forever, processing messages as they arrive
|
|
141
|
+
await queen.queue('my-tasks').consume(async (message) => {
|
|
142
|
+
// Your processing logic
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### The Pop Way (Good for On-Demand Processing)
|
|
147
|
+
|
|
148
|
+
**Use when:** You want to grab messages manually and control everything yourself.
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// Grab one message right now
|
|
152
|
+
const messages = await queen.queue('my-tasks').pop()
|
|
153
|
+
|
|
154
|
+
if (messages.length > 0) {
|
|
155
|
+
const message = messages[0]
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Do your work
|
|
159
|
+
await processMessage(message.data)
|
|
160
|
+
|
|
161
|
+
// Tell Queen it succeeded
|
|
162
|
+
await queen.ack(message, true)
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Tell Queen it failed
|
|
165
|
+
await queen.ack(message, false, { error: error.message })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Key differences:**
|
|
171
|
+
- `consume()` = Long-running, auto-ack, loops automatically
|
|
172
|
+
- `pop()` = One-shot, manual-ack, you control the loop
|
|
173
|
+
|
|
174
|
+
Want to pop multiple messages?
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// Grab up to 10 messages at once
|
|
178
|
+
const messages = await queen
|
|
179
|
+
.queue('my-tasks')
|
|
180
|
+
.batch(10)
|
|
181
|
+
.pop()
|
|
182
|
+
|
|
183
|
+
console.log(`Got ${messages.length} messages`)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Want to wait if no messages are available?
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
// Wait up to 30 seconds for messages to arrive
|
|
190
|
+
const messages = await queen
|
|
191
|
+
.queue('my-tasks')
|
|
192
|
+
.batch(10)
|
|
193
|
+
.wait(true) // Enable long polling
|
|
194
|
+
.pop()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Part 4: Partitions - Organize Your World
|
|
200
|
+
|
|
201
|
+
Think of partitions like lanes on a highway. Each lane processes independently.
|
|
202
|
+
|
|
203
|
+
**Why use partitions?**
|
|
204
|
+
- Process different types of work in parallel
|
|
205
|
+
- Ensure order within a partition
|
|
206
|
+
- Isolate failures
|
|
207
|
+
|
|
208
|
+
### Creating Partitioned Messages
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
// Send messages to specific partitions
|
|
212
|
+
await queen
|
|
213
|
+
.queue('user-events')
|
|
214
|
+
.partition('user-123')
|
|
215
|
+
.push([
|
|
216
|
+
{ data: { event: 'login', timestamp: Date.now() } }
|
|
217
|
+
])
|
|
218
|
+
|
|
219
|
+
await queen
|
|
220
|
+
.queue('user-events')
|
|
221
|
+
.partition('user-456')
|
|
222
|
+
.push([
|
|
223
|
+
{ data: { event: 'logout', timestamp: Date.now() } }
|
|
224
|
+
])
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Important:** Messages in the same partition are **ordered**. Messages in different partitions are **independent**.
|
|
228
|
+
|
|
229
|
+
### Consuming from a Specific Partition
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
// Process only messages from user-123's partition
|
|
233
|
+
await queen
|
|
234
|
+
.queue('user-events')
|
|
235
|
+
.partition('user-123')
|
|
236
|
+
.consume(async (message) => {
|
|
237
|
+
console.log('User 123 did:', message.data.event)
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Real-World Example: Per-User Processing
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
// Each user gets their own partition for ordered processing
|
|
245
|
+
const userId = 'alice-007'
|
|
246
|
+
|
|
247
|
+
// Push user-specific events
|
|
248
|
+
await queen
|
|
249
|
+
.queue('user-commands')
|
|
250
|
+
.partition(userId)
|
|
251
|
+
.push([
|
|
252
|
+
{ data: { action: 'create-post', title: 'Hello World' } },
|
|
253
|
+
{ data: { action: 'like-post', postId: 123 } },
|
|
254
|
+
{ data: { action: 'comment', postId: 123, text: 'Nice!' } }
|
|
255
|
+
])
|
|
256
|
+
|
|
257
|
+
// Process user's commands in order
|
|
258
|
+
await queen
|
|
259
|
+
.queue('user-commands')
|
|
260
|
+
.partition(userId)
|
|
261
|
+
.consume(async (message) => {
|
|
262
|
+
// These will be processed in exact order
|
|
263
|
+
console.log(`${userId} doing:`, message.data.action)
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Part 5: Consumer Groups - Share the Load
|
|
270
|
+
|
|
271
|
+
Consumer groups let multiple workers share the same queue while ensuring each message is processed exactly once.
|
|
272
|
+
|
|
273
|
+
**Use cases:**
|
|
274
|
+
- Scale horizontally (run multiple workers)
|
|
275
|
+
- A/B testing (send copies to different systems)
|
|
276
|
+
- Fan-out patterns (process each message multiple ways)
|
|
277
|
+
|
|
278
|
+
### Basic Consumer Groups
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
// Worker 1 in group "processors"
|
|
282
|
+
await queen
|
|
283
|
+
.queue('emails')
|
|
284
|
+
.group('processors')
|
|
285
|
+
.consume(async (message) => {
|
|
286
|
+
console.log('Worker 1 processing:', message.data)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
// Worker 2 in the SAME group (shares the load)
|
|
290
|
+
await queen
|
|
291
|
+
.queue('emails')
|
|
292
|
+
.group('processors')
|
|
293
|
+
.consume(async (message) => {
|
|
294
|
+
console.log('Worker 2 processing:', message.data)
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Result:** Messages are distributed between Worker 1 and Worker 2. Each message goes to only ONE worker.
|
|
299
|
+
|
|
300
|
+
### Multiple Consumer Groups (Fan-Out)
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
// Group 1: Send emails
|
|
304
|
+
await queen
|
|
305
|
+
.queue('notifications')
|
|
306
|
+
.group('email-sender')
|
|
307
|
+
.consume(async (message) => {
|
|
308
|
+
await sendEmail(message.data)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// Group 2: Log to analytics (processes THE SAME messages)
|
|
312
|
+
await queen
|
|
313
|
+
.queue('notifications')
|
|
314
|
+
.group('analytics')
|
|
315
|
+
.consume(async (message) => {
|
|
316
|
+
await trackEvent(message.data)
|
|
317
|
+
})
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Result:** Every message is processed by BOTH groups independently! 🎉
|
|
321
|
+
|
|
322
|
+
### Real-World Example: Order Processing
|
|
323
|
+
|
|
324
|
+
```javascript
|
|
325
|
+
// Main order processor (high priority)
|
|
326
|
+
await queen
|
|
327
|
+
.queue('orders')
|
|
328
|
+
.group('order-fulfillment')
|
|
329
|
+
.concurrency(5) // Run 5 workers in parallel
|
|
330
|
+
.consume(async (message) => {
|
|
331
|
+
await processOrder(message.data)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
// Analytics processor (separate group, same messages)
|
|
335
|
+
await queen
|
|
336
|
+
.queue('orders')
|
|
337
|
+
.group('analytics')
|
|
338
|
+
.consume(async (message) => {
|
|
339
|
+
await logOrderMetrics(message.data)
|
|
340
|
+
})
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Part 5.5: Subscription Modes - Control Message History
|
|
346
|
+
|
|
347
|
+
When a consumer group first subscribes to a queue, should it process **all historical messages** or only **new messages** that arrive after subscription? Subscription modes give you control!
|
|
348
|
+
|
|
349
|
+
**Use cases:**
|
|
350
|
+
- Start fresh without processing old backlog
|
|
351
|
+
- Subscribe to real-time events only
|
|
352
|
+
- Join a stream at a specific point in time
|
|
353
|
+
- Skip historical data for new analytics consumers
|
|
354
|
+
|
|
355
|
+
### Default Behavior (All Messages)
|
|
356
|
+
|
|
357
|
+
By default, consumer groups start from the **beginning** and process all messages:
|
|
358
|
+
|
|
359
|
+
```javascript
|
|
360
|
+
// This consumer group gets ALL messages, including historical ones
|
|
361
|
+
await queen
|
|
362
|
+
.queue('events')
|
|
363
|
+
.group('new-analytics')
|
|
364
|
+
.consume(async (message) => {
|
|
365
|
+
console.log('Processing:', message.data)
|
|
366
|
+
})
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Subscription Mode: 'new'
|
|
370
|
+
|
|
371
|
+
Skip all historical messages and only process messages that arrive **after** subscription:
|
|
372
|
+
|
|
373
|
+
```javascript
|
|
374
|
+
// Only process NEW messages, skip historical backlog
|
|
375
|
+
await queen
|
|
376
|
+
.queue('events')
|
|
377
|
+
.group('realtime-monitor')
|
|
378
|
+
.subscriptionMode('new') // 👈 Skip history
|
|
379
|
+
.consume(async (message) => {
|
|
380
|
+
console.log('New event:', message.data)
|
|
381
|
+
})
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**What happens:**
|
|
385
|
+
1. Consumer subscribes at `T0`
|
|
386
|
+
2. All messages before `T0` are skipped
|
|
387
|
+
3. Only messages arriving after `T0` are processed
|
|
388
|
+
|
|
389
|
+
### Subscription Mode: 'new-only'
|
|
390
|
+
|
|
391
|
+
Alias for `'new'` - same behavior:
|
|
392
|
+
|
|
393
|
+
```javascript
|
|
394
|
+
await queen
|
|
395
|
+
.queue('events')
|
|
396
|
+
.group('fresh-start')
|
|
397
|
+
.subscriptionMode('new-only')
|
|
398
|
+
.consume(async (message) => {
|
|
399
|
+
// Only new messages
|
|
400
|
+
})
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Subscription From: 'now'
|
|
404
|
+
|
|
405
|
+
Alternative syntax using `subscriptionFrom('now')`:
|
|
406
|
+
|
|
407
|
+
```javascript
|
|
408
|
+
await queen
|
|
409
|
+
.queue('events')
|
|
410
|
+
.group('from-now')
|
|
411
|
+
.subscriptionFrom('now') // 👈 Start from now
|
|
412
|
+
.consume(async (message) => {
|
|
413
|
+
console.log('New event:', message.data)
|
|
414
|
+
})
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Subscription From: Timestamp
|
|
418
|
+
|
|
419
|
+
Start consuming from a **specific timestamp**:
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
// Start from a specific point in time
|
|
423
|
+
const startTime = '2025-10-28T10:00:00.000Z'
|
|
424
|
+
|
|
425
|
+
await queen
|
|
426
|
+
.queue('events')
|
|
427
|
+
.group('replay-from-10am')
|
|
428
|
+
.subscriptionFrom(startTime) // 👈 ISO 8601 timestamp
|
|
429
|
+
.consume(async (message) => {
|
|
430
|
+
// Process messages from 10am onwards
|
|
431
|
+
})
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Dynamic timestamp example:**
|
|
435
|
+
|
|
436
|
+
```javascript
|
|
437
|
+
// Start from 1 hour ago
|
|
438
|
+
const oneHourAgo = new Date(Date.now() - 3600000).toISOString()
|
|
439
|
+
|
|
440
|
+
await queen
|
|
441
|
+
.queue('events')
|
|
442
|
+
.group('last-hour')
|
|
443
|
+
.subscriptionFrom(oneHourAgo)
|
|
444
|
+
.consume(async (message) => {
|
|
445
|
+
console.log('Processing recent event:', message.data)
|
|
446
|
+
})
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Real-World Example: Multi-Consumer Setup
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
// Group 1: Process ALL messages (including backlog)
|
|
453
|
+
await queen
|
|
454
|
+
.queue('user-actions')
|
|
455
|
+
.group('batch-analytics')
|
|
456
|
+
.consume(async (message) => {
|
|
457
|
+
await generateFullReport(message.data)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
// Group 2: Only NEW messages (real-time monitoring)
|
|
461
|
+
await queen
|
|
462
|
+
.queue('user-actions')
|
|
463
|
+
.group('realtime-alerts')
|
|
464
|
+
.subscriptionMode('new')
|
|
465
|
+
.consume(async (message) => {
|
|
466
|
+
await sendRealtimeAlert(message.data)
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// Group 3: Replay from specific time (debugging)
|
|
470
|
+
await queen
|
|
471
|
+
.queue('user-actions')
|
|
472
|
+
.group('debug-replay')
|
|
473
|
+
.subscriptionFrom('2025-10-28T15:30:00.000Z')
|
|
474
|
+
.consume(async (message) => {
|
|
475
|
+
await debugSpecificTimeframe(message.data)
|
|
476
|
+
})
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**Result:**
|
|
480
|
+
- `batch-analytics`: Processes all 10,000 historical messages + new ones
|
|
481
|
+
- `realtime-alerts`: Skips 10,000 historical messages, only processes new ones
|
|
482
|
+
- `debug-replay`: Starts from 3:30 PM, processes everything after that
|
|
483
|
+
|
|
484
|
+
### Important Notes
|
|
485
|
+
|
|
486
|
+
⚠️ **Subscription modes only work with consumer groups:**
|
|
487
|
+
- Requires `.group('name')`
|
|
488
|
+
- Does NOT work with default queue mode (no group)
|
|
489
|
+
- Each consumer group maintains its own subscription position
|
|
490
|
+
|
|
491
|
+
🎯 **First subscription matters:**
|
|
492
|
+
- Subscription mode is set when the consumer group **first subscribes**
|
|
493
|
+
- Subsequent consumers in the same group inherit the same position
|
|
494
|
+
- To change subscription mode, use a different group name
|
|
495
|
+
|
|
496
|
+
💡 **Best Practices:**
|
|
497
|
+
- Use `'new'` for real-time monitoring and alerting
|
|
498
|
+
- Use default (all) for batch processing and analytics
|
|
499
|
+
- Use timestamps for replay/debugging scenarios
|
|
500
|
+
- Name groups descriptively based on their subscription mode
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Part 6: Namespaces & Tasks - The Wildcard Way
|
|
505
|
+
|
|
506
|
+
Sometimes you don't care about specific queues. You want to process messages based on **what they do** or **where they belong**.
|
|
507
|
+
|
|
508
|
+
### Namespaces (Logical Grouping)
|
|
509
|
+
|
|
510
|
+
Think of namespaces as folders for your queues.
|
|
511
|
+
|
|
512
|
+
```javascript
|
|
513
|
+
// Create queues with namespaces
|
|
514
|
+
await queen.queue('billing-invoices').namespace('accounting').create()
|
|
515
|
+
await queen.queue('billing-receipts').namespace('accounting').create()
|
|
516
|
+
await queen.queue('user-emails').namespace('notifications').create()
|
|
517
|
+
|
|
518
|
+
// Push to specific queues
|
|
519
|
+
await queen.queue('billing-invoices').push([
|
|
520
|
+
{ data: { invoice: 'INV-001' } }
|
|
521
|
+
])
|
|
522
|
+
|
|
523
|
+
// Consume from ALL queues in the 'accounting' namespace
|
|
524
|
+
await queen
|
|
525
|
+
.queue()
|
|
526
|
+
.namespace('accounting')
|
|
527
|
+
.consume(async (message) => {
|
|
528
|
+
// This will receive messages from BOTH billing-invoices AND billing-receipts
|
|
529
|
+
console.log('Accounting message:', message.data)
|
|
530
|
+
})
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
**Why this is cool:** Add new queues to the namespace later, and existing consumers automatically process them! 🎯
|
|
534
|
+
|
|
535
|
+
### Tasks (Processing Types)
|
|
536
|
+
|
|
537
|
+
Tasks are like tags that describe what needs to be done.
|
|
538
|
+
|
|
539
|
+
```javascript
|
|
540
|
+
// Create queues with tasks
|
|
541
|
+
await queen.queue('video-uploads').task('video-processing').create()
|
|
542
|
+
await queen.queue('image-uploads').task('image-processing').create()
|
|
543
|
+
|
|
544
|
+
// Consume by task type
|
|
545
|
+
await queen
|
|
546
|
+
.queue()
|
|
547
|
+
.task('video-processing')
|
|
548
|
+
.consume(async (message) => {
|
|
549
|
+
// Only video processing messages
|
|
550
|
+
await processVideo(message.data)
|
|
551
|
+
})
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Combining Namespace + Task
|
|
555
|
+
|
|
556
|
+
```javascript
|
|
557
|
+
// Super specific filtering!
|
|
558
|
+
await queen
|
|
559
|
+
.queue()
|
|
560
|
+
.namespace('media')
|
|
561
|
+
.task('urgent-processing')
|
|
562
|
+
.consume(async (message) => {
|
|
563
|
+
// Only urgent media processing from the media namespace
|
|
564
|
+
})
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Part 7: Transactions - All or Nothing
|
|
570
|
+
|
|
571
|
+
Transactions are atomic operations. Either **everything** succeeds or **nothing** does.
|
|
572
|
+
|
|
573
|
+
**Use cases:**
|
|
574
|
+
- Ack one message and push to another queue (pipeline pattern)
|
|
575
|
+
- Process multiple messages atomically
|
|
576
|
+
- Ensure consistency across operations
|
|
577
|
+
|
|
578
|
+
### Basic Transaction: Ack + Push
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
// Pop a message
|
|
582
|
+
const messages = await queen.queue('raw-data').batch(1).pop()
|
|
583
|
+
|
|
584
|
+
if (messages.length > 0) {
|
|
585
|
+
const message = messages[0]
|
|
586
|
+
|
|
587
|
+
// Process it
|
|
588
|
+
const processed = await transformData(message.data)
|
|
589
|
+
|
|
590
|
+
// Atomically: ack the input AND push the output
|
|
591
|
+
await queen
|
|
592
|
+
.transaction()
|
|
593
|
+
.ack(message) // Complete the input message
|
|
594
|
+
.queue('processed-data')
|
|
595
|
+
.push([{ data: processed }]) // Add to next queue
|
|
596
|
+
.commit()
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// If commit fails, NOTHING happens. Message stays in raw-data queue!
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Multi-Queue Pipeline
|
|
603
|
+
|
|
604
|
+
```javascript
|
|
605
|
+
// Pop from queue A
|
|
606
|
+
const messages = await queen.queue('queue-a').batch(1).pop()
|
|
607
|
+
|
|
608
|
+
// Transaction: ack from A, push to B and C
|
|
609
|
+
await queen
|
|
610
|
+
.transaction()
|
|
611
|
+
.ack(messages[0])
|
|
612
|
+
.queue('queue-b')
|
|
613
|
+
.push([{ data: { step: 2, value: messages[0].data.value * 2 } }])
|
|
614
|
+
.queue('queue-c')
|
|
615
|
+
.push([{ data: { step: 2, value: messages[0].data.value * 2 } }])
|
|
616
|
+
.commit()
|
|
617
|
+
|
|
618
|
+
// Atomic! Either all three operations succeed, or none do
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Batch Processing Transaction
|
|
622
|
+
|
|
623
|
+
```javascript
|
|
624
|
+
// Pop multiple messages
|
|
625
|
+
const messages = await queen.queue('inputs').batch(10).pop()
|
|
626
|
+
|
|
627
|
+
// Process them
|
|
628
|
+
const results = messages.map(m => process(m.data))
|
|
629
|
+
|
|
630
|
+
// Atomically ack all inputs and push all outputs
|
|
631
|
+
const txn = queen.transaction()
|
|
632
|
+
|
|
633
|
+
// Ack all inputs
|
|
634
|
+
for (const message of messages) {
|
|
635
|
+
txn.ack(message)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Push all outputs
|
|
639
|
+
txn.queue('outputs').push(results.map(r => ({ data: r })))
|
|
640
|
+
|
|
641
|
+
await txn.commit()
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Transaction with Consumer
|
|
645
|
+
|
|
646
|
+
Want to consume with transactions? Easy:
|
|
647
|
+
|
|
648
|
+
```javascript
|
|
649
|
+
await queen
|
|
650
|
+
.queue('source')
|
|
651
|
+
.autoAck(false) // Must disable auto-ack for manual transaction
|
|
652
|
+
.consume(async (message) => {
|
|
653
|
+
// Do work
|
|
654
|
+
const result = await processMessage(message.data)
|
|
655
|
+
|
|
656
|
+
// Transactionally ack and push result
|
|
657
|
+
await queen
|
|
658
|
+
.transaction()
|
|
659
|
+
.ack(message)
|
|
660
|
+
.queue('destination')
|
|
661
|
+
.push([{ data: result }])
|
|
662
|
+
.commit()
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
## Part 8: Client-Side Buffering - Speed Demon Mode
|
|
669
|
+
|
|
670
|
+
Pushing messages one-at-a-time is slow. Buffering batches them up for massive speed boosts! 🚄
|
|
671
|
+
|
|
672
|
+
### How Buffering Works
|
|
673
|
+
|
|
674
|
+
Instead of sending messages immediately:
|
|
675
|
+
1. Messages collect in a local buffer
|
|
676
|
+
2. Buffer flushes when it reaches a **count** or **time** threshold
|
|
677
|
+
3. All buffered messages are sent in one HTTP request
|
|
678
|
+
|
|
679
|
+
**Result:** 10x-100x faster throughput!
|
|
680
|
+
|
|
681
|
+
### Basic Buffering
|
|
682
|
+
|
|
683
|
+
```javascript
|
|
684
|
+
// Buffer up to 100 messages OR 1 second (whichever comes first)
|
|
685
|
+
await queen
|
|
686
|
+
.queue('logs')
|
|
687
|
+
.buffer({ messageCount: 100, timeMillis: 1000 })
|
|
688
|
+
.push([
|
|
689
|
+
{ data: { level: 'info', message: 'User logged in' } }
|
|
690
|
+
])
|
|
691
|
+
|
|
692
|
+
// Message is now buffered, not sent yet
|
|
693
|
+
// Will send when 100 messages accumulate OR 1 second passes
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### High-Throughput Example
|
|
697
|
+
|
|
698
|
+
```javascript
|
|
699
|
+
// Send 10,000 messages super fast
|
|
700
|
+
for (let i = 0; i < 10000; i++) {
|
|
701
|
+
await queen
|
|
702
|
+
.queue('events')
|
|
703
|
+
.buffer({ messageCount: 500, timeMillis: 100 })
|
|
704
|
+
.push([
|
|
705
|
+
{ data: { id: i, timestamp: Date.now() } }
|
|
706
|
+
])
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Flush any remaining buffered messages
|
|
710
|
+
await queen.flushAllBuffers()
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Performance:** This might take seconds instead of minutes! ⚡
|
|
714
|
+
|
|
715
|
+
### Manual Flush
|
|
716
|
+
|
|
717
|
+
```javascript
|
|
718
|
+
// Flush all buffers for all queues
|
|
719
|
+
await queen.flushAllBuffers()
|
|
720
|
+
|
|
721
|
+
// Flush a specific queue's buffer
|
|
722
|
+
await queen.queue('my-queue').flushBuffer()
|
|
723
|
+
|
|
724
|
+
// Get buffer statistics
|
|
725
|
+
const stats = queen.getBufferStats()
|
|
726
|
+
console.log('Buffers:', stats)
|
|
727
|
+
// Example output: { 'my-queue/Default': { count: 45, size: 1234 } }
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### Real-World Example: Log Aggregation
|
|
731
|
+
|
|
732
|
+
```javascript
|
|
733
|
+
// High-frequency logging with buffering
|
|
734
|
+
class Logger {
|
|
735
|
+
constructor(queen) {
|
|
736
|
+
this.queen = queen
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async log(level, message) {
|
|
740
|
+
await this.queen
|
|
741
|
+
.queue('application-logs')
|
|
742
|
+
.buffer({ messageCount: 1000, timeMillis: 5000 })
|
|
743
|
+
.push([
|
|
744
|
+
{ data: { level, message, timestamp: Date.now() } }
|
|
745
|
+
])
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async flush() {
|
|
749
|
+
await this.queen.flushAllBuffers()
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const logger = new Logger(queen)
|
|
754
|
+
await logger.log('info', 'Server started')
|
|
755
|
+
await logger.log('debug', 'Processing request...')
|
|
756
|
+
// Logs are buffered and sent in batches!
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Part 9: Dead Letter Queue - When Things Go Wrong
|
|
762
|
+
|
|
763
|
+
Not all messages can be processed. Some are just... problematic. The DLQ is where failed messages go to be examined.
|
|
764
|
+
|
|
765
|
+
### How DLQ Works
|
|
766
|
+
|
|
767
|
+
1. Message fails (your handler throws an error)
|
|
768
|
+
2. Message retries (up to `retryLimit`)
|
|
769
|
+
3. After max retries → moves to Dead Letter Queue
|
|
770
|
+
4. You can query DLQ to see what went wrong
|
|
771
|
+
|
|
772
|
+
### Enable DLQ
|
|
773
|
+
|
|
774
|
+
```javascript
|
|
775
|
+
// Create queue with DLQ enabled
|
|
776
|
+
await queen
|
|
777
|
+
.queue('risky-business')
|
|
778
|
+
.config({
|
|
779
|
+
retryLimit: 3, // Try 3 times
|
|
780
|
+
dlqAfterMaxRetries: true // Send to DLQ after 3 failures
|
|
781
|
+
})
|
|
782
|
+
.create()
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Process Messages (Some Will Fail)
|
|
786
|
+
|
|
787
|
+
```javascript
|
|
788
|
+
await queen
|
|
789
|
+
.queue('risky-business')
|
|
790
|
+
.consume(async (message) => {
|
|
791
|
+
if (message.data.value < 0) {
|
|
792
|
+
throw new Error('Negative values not allowed!')
|
|
793
|
+
}
|
|
794
|
+
// Process normally
|
|
795
|
+
})
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Query the DLQ
|
|
799
|
+
|
|
800
|
+
```javascript
|
|
801
|
+
// Get failed messages
|
|
802
|
+
const dlq = await queen
|
|
803
|
+
.queue('risky-business')
|
|
804
|
+
.dlq()
|
|
805
|
+
.limit(10)
|
|
806
|
+
.get()
|
|
807
|
+
|
|
808
|
+
console.log(`Found ${dlq.total} failed messages`)
|
|
809
|
+
|
|
810
|
+
for (const message of dlq.messages) {
|
|
811
|
+
console.log('Failed message:', message.data)
|
|
812
|
+
console.log('Error was:', message.errorMessage)
|
|
813
|
+
console.log('Failed at:', message.dlqTimestamp)
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### DLQ with Consumer Groups
|
|
818
|
+
|
|
819
|
+
```javascript
|
|
820
|
+
// Check DLQ for a specific consumer group
|
|
821
|
+
const dlq = await queen
|
|
822
|
+
.queue('risky-business')
|
|
823
|
+
.dlq('my-consumer-group')
|
|
824
|
+
.limit(100)
|
|
825
|
+
.get()
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### Advanced DLQ Queries
|
|
829
|
+
|
|
830
|
+
```javascript
|
|
831
|
+
// Query with time range
|
|
832
|
+
const dlq = await queen
|
|
833
|
+
.queue('risky-business')
|
|
834
|
+
.dlq()
|
|
835
|
+
.from('2025-01-01')
|
|
836
|
+
.to('2025-01-31')
|
|
837
|
+
.limit(100)
|
|
838
|
+
.offset(0) // Pagination
|
|
839
|
+
.get()
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Part 10: Lease Renewal - Keep It Locked
|
|
845
|
+
|
|
846
|
+
When you pop a message, you get a "lease" (a lock). The lease expires after `leaseTime` seconds. If your processing takes longer, you need to **renew** the lease.
|
|
847
|
+
|
|
848
|
+
### Why Lease Renewal?
|
|
849
|
+
|
|
850
|
+
Imagine processing a video that takes 10 minutes, but your lease is 5 minutes. After 5 minutes, Queen thinks you died and gives the message to someone else. Oops! 😱
|
|
851
|
+
|
|
852
|
+
### Automatic Lease Renewal (Easy Mode)
|
|
853
|
+
|
|
854
|
+
```javascript
|
|
855
|
+
await queen
|
|
856
|
+
.queue('long-tasks')
|
|
857
|
+
.renewLease(true, 60000) // Renew every 60 seconds
|
|
858
|
+
.consume(async (message) => {
|
|
859
|
+
// Even if this takes 30 minutes, the lease keeps renewing automatically!
|
|
860
|
+
await processVeryLongTask(message.data)
|
|
861
|
+
})
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
**What happens:** Every 60 seconds, Queen automatically extends your lease. Your function can take as long as needed!
|
|
865
|
+
|
|
866
|
+
### Manual Lease Renewal
|
|
867
|
+
|
|
868
|
+
```javascript
|
|
869
|
+
// Pop a message
|
|
870
|
+
const messages = await queen.queue('long-tasks').pop()
|
|
871
|
+
const message = messages[0]
|
|
872
|
+
|
|
873
|
+
// Start long processing
|
|
874
|
+
const timer = setInterval(async () => {
|
|
875
|
+
await queen.renew(message) // Extend lease
|
|
876
|
+
console.log('Lease renewed!')
|
|
877
|
+
}, 30000) // Every 30 seconds
|
|
878
|
+
|
|
879
|
+
try {
|
|
880
|
+
await processVeryLongTask(message.data)
|
|
881
|
+
await queen.ack(message, true)
|
|
882
|
+
} finally {
|
|
883
|
+
clearInterval(timer)
|
|
884
|
+
}
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Batch Lease Renewal
|
|
888
|
+
|
|
889
|
+
```javascript
|
|
890
|
+
// Renew multiple messages at once
|
|
891
|
+
const messages = await queen.queue('tasks').batch(10).pop()
|
|
892
|
+
|
|
893
|
+
// Renew all of them
|
|
894
|
+
await queen.renew(messages)
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### Using Just the Lease ID
|
|
898
|
+
|
|
899
|
+
```javascript
|
|
900
|
+
const message = messages[0]
|
|
901
|
+
|
|
902
|
+
// Renew by lease ID
|
|
903
|
+
await queen.renew(message.leaseId)
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## Part 11: Queue Configuration - Fine Tuning
|
|
909
|
+
|
|
910
|
+
Queues have lots of knobs to turn. Let's explore them all!
|
|
911
|
+
|
|
912
|
+
### Complete Configuration Example
|
|
913
|
+
|
|
914
|
+
```javascript
|
|
915
|
+
await queen
|
|
916
|
+
.queue('super-queue')
|
|
917
|
+
.config({
|
|
918
|
+
// Lease & Retry
|
|
919
|
+
leaseTime: 300, // 5 minutes to process (seconds)
|
|
920
|
+
retryLimit: 3, // Retry 3 times before giving up
|
|
921
|
+
retryDelay: 5000, // Wait 5 seconds between retries (milliseconds)
|
|
922
|
+
|
|
923
|
+
// Dead Letter Queue
|
|
924
|
+
dlqAfterMaxRetries: true, // Move to DLQ after max retries
|
|
925
|
+
|
|
926
|
+
// Priority
|
|
927
|
+
priority: 5, // Higher number = higher priority (0-10)
|
|
928
|
+
|
|
929
|
+
// Delays & Buffers
|
|
930
|
+
delayedProcessing: 60, // Messages become available after 60 seconds
|
|
931
|
+
windowBuffer: 30, // Hold messages for 30 seconds to batch them
|
|
932
|
+
|
|
933
|
+
// Capacity
|
|
934
|
+
maxSize: 10000, // Max 10,000 messages in queue
|
|
935
|
+
|
|
936
|
+
// Retention
|
|
937
|
+
retentionSeconds: 86400, // Keep pending messages for 24 hours
|
|
938
|
+
completedRetentionSeconds: 3600, // Keep completed messages for 1 hour
|
|
939
|
+
ttl: 86400, // Message expires after 24 hours (seconds)
|
|
940
|
+
|
|
941
|
+
// Security
|
|
942
|
+
encryptionEnabled: true // Encrypt message payloads at rest
|
|
943
|
+
})
|
|
944
|
+
.create()
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
### Priority Queues
|
|
948
|
+
|
|
949
|
+
Higher priority queues are processed first!
|
|
950
|
+
|
|
951
|
+
```javascript
|
|
952
|
+
// High priority queue
|
|
953
|
+
await queen
|
|
954
|
+
.queue('urgent-alerts')
|
|
955
|
+
.config({ priority: 10 })
|
|
956
|
+
.create()
|
|
957
|
+
|
|
958
|
+
// Normal priority
|
|
959
|
+
await queen
|
|
960
|
+
.queue('regular-tasks')
|
|
961
|
+
.config({ priority: 5 })
|
|
962
|
+
.create()
|
|
963
|
+
|
|
964
|
+
// Low priority
|
|
965
|
+
await queen
|
|
966
|
+
.queue('background-jobs')
|
|
967
|
+
.config({ priority: 1 })
|
|
968
|
+
.create()
|
|
969
|
+
|
|
970
|
+
// Consumer processes urgent-alerts first, then regular-tasks, then background-jobs
|
|
971
|
+
await queen.queue().namespace('all').consume(async (message) => {
|
|
972
|
+
console.log('Processing:', message)
|
|
973
|
+
})
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
### Delayed Processing
|
|
977
|
+
|
|
978
|
+
Messages don't become available until the delay passes.
|
|
979
|
+
|
|
980
|
+
```javascript
|
|
981
|
+
// Messages are invisible for 60 seconds
|
|
982
|
+
await queen
|
|
983
|
+
.queue('scheduled-tasks')
|
|
984
|
+
.config({ delayedProcessing: 60 })
|
|
985
|
+
.create()
|
|
986
|
+
|
|
987
|
+
// Push a message
|
|
988
|
+
await queen.queue('scheduled-tasks').push([
|
|
989
|
+
{ data: { task: 'send-reminder' } }
|
|
990
|
+
])
|
|
991
|
+
|
|
992
|
+
// Pop immediately: gets nothing!
|
|
993
|
+
const now = await queen.queue('scheduled-tasks').pop()
|
|
994
|
+
console.log(now) // []
|
|
995
|
+
|
|
996
|
+
// Wait 60 seconds...
|
|
997
|
+
await new Promise(r => setTimeout(r, 61000))
|
|
998
|
+
|
|
999
|
+
// Pop again: now we get the message!
|
|
1000
|
+
const later = await queen.queue('scheduled-tasks').pop()
|
|
1001
|
+
console.log(later) // [{ data: { task: 'send-reminder' } }]
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Window Buffering (Server-Side Batching)
|
|
1005
|
+
|
|
1006
|
+
Holds messages server-side to create natural batches.
|
|
1007
|
+
|
|
1008
|
+
```javascript
|
|
1009
|
+
// Hold messages for 5 seconds to batch them
|
|
1010
|
+
await queen
|
|
1011
|
+
.queue('events')
|
|
1012
|
+
.config({ windowBuffer: 5 })
|
|
1013
|
+
.create()
|
|
1014
|
+
|
|
1015
|
+
// Push 10 messages quickly
|
|
1016
|
+
for (let i = 0; i < 10; i++) {
|
|
1017
|
+
await queen.queue('events').push([{ data: { id: i } }])
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Consumer gets them all at once!
|
|
1021
|
+
await queen
|
|
1022
|
+
.queue('events')
|
|
1023
|
+
.batch(100)
|
|
1024
|
+
.consume(async (messages) => {
|
|
1025
|
+
console.log(`Got ${messages.length} messages in one batch!`)
|
|
1026
|
+
// Likely: "Got 10 messages in one batch!"
|
|
1027
|
+
})
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
### Message TTL (Time To Live)
|
|
1031
|
+
|
|
1032
|
+
Messages expire and are deleted automatically.
|
|
1033
|
+
|
|
1034
|
+
```javascript
|
|
1035
|
+
// Messages live for 1 hour max
|
|
1036
|
+
await queen
|
|
1037
|
+
.queue('temporary-data')
|
|
1038
|
+
.config({ ttl: 3600 })
|
|
1039
|
+
.create()
|
|
1040
|
+
|
|
1041
|
+
// Messages older than 1 hour are automatically deleted
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### Encryption
|
|
1045
|
+
|
|
1046
|
+
Sensitive data? Enable encryption!
|
|
1047
|
+
|
|
1048
|
+
```javascript
|
|
1049
|
+
await queen
|
|
1050
|
+
.queue('customer-pii')
|
|
1051
|
+
.config({ encryptionEnabled: true })
|
|
1052
|
+
.create()
|
|
1053
|
+
|
|
1054
|
+
// Messages are encrypted at rest
|
|
1055
|
+
// Decrypted automatically when consumed
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
---
|
|
1059
|
+
|
|
1060
|
+
## Part 12: Message Tracing - Debug Your Workflows
|
|
1061
|
+
|
|
1062
|
+
Ever wondered what's happening to your messages as they flow through your system? Message tracing lets you record breadcrumbs as messages are processed, perfect for debugging distributed workflows!
|
|
1063
|
+
|
|
1064
|
+
### Basic Tracing
|
|
1065
|
+
|
|
1066
|
+
```javascript
|
|
1067
|
+
await queen.queue('orders').consume(async (msg) => {
|
|
1068
|
+
// Record a trace event
|
|
1069
|
+
await msg.trace({
|
|
1070
|
+
data: { text: 'Order processing started' }
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
// Do some work
|
|
1074
|
+
const order = await processOrder(msg.data)
|
|
1075
|
+
|
|
1076
|
+
// Record another trace
|
|
1077
|
+
await msg.trace({
|
|
1078
|
+
data: {
|
|
1079
|
+
text: 'Order processed successfully',
|
|
1080
|
+
orderId: order.id,
|
|
1081
|
+
total: order.total
|
|
1082
|
+
}
|
|
1083
|
+
})
|
|
1084
|
+
}, { autoAck: true })
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
**What you get:**
|
|
1088
|
+
- Timeline of processing events
|
|
1089
|
+
- View traces in the frontend (Messages → Click message → Processing Timeline)
|
|
1090
|
+
- Never crashes your consumer (safe error handling built-in)
|
|
1091
|
+
|
|
1092
|
+
### Trace Names - Connect the Dots
|
|
1093
|
+
|
|
1094
|
+
The real power comes from **trace names** - they let you correlate traces across multiple messages!
|
|
1095
|
+
|
|
1096
|
+
```javascript
|
|
1097
|
+
// Service 1: Order Service
|
|
1098
|
+
await queen.queue('orders').consume(async (msg) => {
|
|
1099
|
+
const orderId = msg.data.orderId
|
|
1100
|
+
|
|
1101
|
+
await msg.trace({
|
|
1102
|
+
traceName: `order-${orderId}`, // 👈 Link traces with this name
|
|
1103
|
+
data: { text: 'Order created', service: 'orders' }
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
// Create inventory check
|
|
1107
|
+
await queen.queue('inventory').push([{
|
|
1108
|
+
data: { orderId, items: msg.data.items }
|
|
1109
|
+
}])
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
// Service 2: Inventory Service
|
|
1113
|
+
await queen.queue('inventory').consume(async (msg) => {
|
|
1114
|
+
const orderId = msg.data.orderId
|
|
1115
|
+
|
|
1116
|
+
await msg.trace({
|
|
1117
|
+
traceName: `order-${orderId}`, // 👈 Same name = connected!
|
|
1118
|
+
data: { text: 'Stock checked', service: 'inventory' }
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
// Create payment
|
|
1122
|
+
await queen.queue('payments').push([{
|
|
1123
|
+
data: { orderId }
|
|
1124
|
+
}])
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
// Service 3: Payment Service
|
|
1128
|
+
await queen.queue('payments').consume(async (msg) => {
|
|
1129
|
+
const orderId = msg.data.orderId
|
|
1130
|
+
|
|
1131
|
+
await msg.trace({
|
|
1132
|
+
traceName: `order-${orderId}`, // 👈 All connected!
|
|
1133
|
+
data: { text: 'Payment processed', service: 'payments' }
|
|
1134
|
+
})
|
|
1135
|
+
})
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
**Now in the frontend:**
|
|
1139
|
+
- Go to **Traces** page
|
|
1140
|
+
- Search for `order-12345`
|
|
1141
|
+
- See the ENTIRE workflow across all 3 services! 🎉
|
|
1142
|
+
|
|
1143
|
+
### Multi-Category Tracing
|
|
1144
|
+
|
|
1145
|
+
You can add multiple trace names to organize by different dimensions:
|
|
1146
|
+
|
|
1147
|
+
```javascript
|
|
1148
|
+
await queen.queue('chat-messages').consume(async (msg) => {
|
|
1149
|
+
const { tenantId, roomId, userId } = msg.data
|
|
1150
|
+
|
|
1151
|
+
await msg.trace({
|
|
1152
|
+
traceName: [
|
|
1153
|
+
`tenant-${tenantId}`, // Track by tenant
|
|
1154
|
+
`room-${roomId}`, // Track by room
|
|
1155
|
+
`user-${userId}` // Track by user
|
|
1156
|
+
],
|
|
1157
|
+
data: { text: 'Message sent' }
|
|
1158
|
+
})
|
|
1159
|
+
})
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
**Query any dimension:**
|
|
1163
|
+
- Search `tenant-acme` → See all tenant activity
|
|
1164
|
+
- Search `room-123` → See all room activity
|
|
1165
|
+
- Search `user-456` → See all user activity
|
|
1166
|
+
|
|
1167
|
+
### Event Types
|
|
1168
|
+
|
|
1169
|
+
Organize traces by event type for better visualization:
|
|
1170
|
+
|
|
1171
|
+
```javascript
|
|
1172
|
+
await msg.trace({
|
|
1173
|
+
eventType: 'info', // Blue in UI
|
|
1174
|
+
data: { text: 'Started processing' }
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
await msg.trace({
|
|
1178
|
+
eventType: 'step', // Purple in UI
|
|
1179
|
+
data: { text: 'Validated data' }
|
|
1180
|
+
})
|
|
1181
|
+
|
|
1182
|
+
await msg.trace({
|
|
1183
|
+
eventType: 'error', // Red in UI
|
|
1184
|
+
data: { text: 'Validation failed', reason: 'Invalid email' }
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
await msg.trace({
|
|
1188
|
+
eventType: 'processing', // Green in UI
|
|
1189
|
+
data: { text: 'Sending email' }
|
|
1190
|
+
})
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
### Error Tracking
|
|
1194
|
+
|
|
1195
|
+
Traces are perfect for tracking errors without breaking your flow:
|
|
1196
|
+
|
|
1197
|
+
```javascript
|
|
1198
|
+
await queen.queue('analytics').consume(async (msg) => {
|
|
1199
|
+
try {
|
|
1200
|
+
await msg.trace({ data: { text: 'Job started' } })
|
|
1201
|
+
|
|
1202
|
+
const result = await computeAnalytics(msg.data)
|
|
1203
|
+
|
|
1204
|
+
await msg.trace({
|
|
1205
|
+
data: {
|
|
1206
|
+
text: 'Job completed',
|
|
1207
|
+
recordsProcessed: result.count
|
|
1208
|
+
}
|
|
1209
|
+
})
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
// Record the error (this won't crash!)
|
|
1212
|
+
await msg.trace({
|
|
1213
|
+
eventType: 'error',
|
|
1214
|
+
data: {
|
|
1215
|
+
text: 'Job failed',
|
|
1216
|
+
error: error.message,
|
|
1217
|
+
stack: error.stack
|
|
1218
|
+
}
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
throw error // Still fail the message for retry
|
|
1222
|
+
}
|
|
1223
|
+
}, { autoAck: true })
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Performance Tracking
|
|
1227
|
+
|
|
1228
|
+
Track timing and metrics:
|
|
1229
|
+
|
|
1230
|
+
```javascript
|
|
1231
|
+
await queen.queue('reports').consume(async (msg) => {
|
|
1232
|
+
const start = Date.now()
|
|
1233
|
+
|
|
1234
|
+
await msg.trace({ data: { text: 'Report generation started' } })
|
|
1235
|
+
|
|
1236
|
+
const data = await fetchData(msg.data)
|
|
1237
|
+
const fetchTime = Date.now() - start
|
|
1238
|
+
|
|
1239
|
+
await msg.trace({
|
|
1240
|
+
data: {
|
|
1241
|
+
text: 'Data fetched',
|
|
1242
|
+
durationMs: fetchTime,
|
|
1243
|
+
rowCount: data.length
|
|
1244
|
+
}
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
const report = await generatePDF(data)
|
|
1248
|
+
|
|
1249
|
+
await msg.trace({
|
|
1250
|
+
data: {
|
|
1251
|
+
text: 'Report generated',
|
|
1252
|
+
totalDurationMs: Date.now() - start,
|
|
1253
|
+
sizeKB: Math.round(report.size / 1024)
|
|
1254
|
+
}
|
|
1255
|
+
})
|
|
1256
|
+
})
|
|
1257
|
+
```
|
|
1258
|
+
|
|
1259
|
+
### Viewing Traces in the UI
|
|
1260
|
+
|
|
1261
|
+
**Method 1: From Message Details**
|
|
1262
|
+
1. Go to **Messages** page
|
|
1263
|
+
2. Click on any message
|
|
1264
|
+
3. See "Processing Timeline" with all traces
|
|
1265
|
+
|
|
1266
|
+
**Method 2: Search by Trace Name**
|
|
1267
|
+
1. Go to **Traces** page
|
|
1268
|
+
2. Enter a trace name (e.g., `order-12345`)
|
|
1269
|
+
3. See timeline across ALL messages with that name
|
|
1270
|
+
|
|
1271
|
+
**Method 3: Browse Available Traces**
|
|
1272
|
+
1. Go to **Traces** page
|
|
1273
|
+
2. See list of all trace names with statistics
|
|
1274
|
+
3. Click any trace name to view
|
|
1275
|
+
|
|
1276
|
+
### Important Notes
|
|
1277
|
+
|
|
1278
|
+
⚡ **Safe & Non-Blocking**
|
|
1279
|
+
- Traces are awaited but NEVER crash your consumer
|
|
1280
|
+
- Failures are logged but don't throw errors
|
|
1281
|
+
- Your message processing continues normally
|
|
1282
|
+
|
|
1283
|
+
🎯 **Best Practices**
|
|
1284
|
+
- Use descriptive trace names for easy searching
|
|
1285
|
+
- Include relevant data (IDs, counts, durations)
|
|
1286
|
+
- Use event types for visual organization
|
|
1287
|
+
- Trace both successes and failures
|
|
1288
|
+
|
|
1289
|
+
🔍 **Use Cases**
|
|
1290
|
+
- Debug distributed workflows
|
|
1291
|
+
- Track multi-tenant operations
|
|
1292
|
+
- Monitor user journeys
|
|
1293
|
+
- Performance analysis
|
|
1294
|
+
- Error investigation
|
|
1295
|
+
- Audit trails
|
|
1296
|
+
|
|
1297
|
+
---
|
|
1298
|
+
|
|
1299
|
+
## Part 13: Callbacks & Error Handling
|
|
1300
|
+
|
|
1301
|
+
Sometimes you need more control over what happens when messages succeed or fail.
|
|
1302
|
+
|
|
1303
|
+
### Success Callback
|
|
1304
|
+
|
|
1305
|
+
```javascript
|
|
1306
|
+
await queen
|
|
1307
|
+
.queue('tasks')
|
|
1308
|
+
.consume(async (message) => {
|
|
1309
|
+
return await processMessage(message.data)
|
|
1310
|
+
})
|
|
1311
|
+
.onSuccess(async (message, result) => {
|
|
1312
|
+
console.log('Success! Result:', result)
|
|
1313
|
+
// Custom ack logic could go here
|
|
1314
|
+
})
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
### Error Callback
|
|
1318
|
+
|
|
1319
|
+
```javascript
|
|
1320
|
+
await queen
|
|
1321
|
+
.queue('tasks')
|
|
1322
|
+
.consume(async (message) => {
|
|
1323
|
+
throw new Error('Something went wrong!')
|
|
1324
|
+
})
|
|
1325
|
+
.onError(async (message, error) => {
|
|
1326
|
+
console.error('Failed:', error.message)
|
|
1327
|
+
// Log to external service, send alert, etc.
|
|
1328
|
+
})
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
### Both Callbacks (Full Control)
|
|
1332
|
+
|
|
1333
|
+
```javascript
|
|
1334
|
+
await queen
|
|
1335
|
+
.queue('tasks')
|
|
1336
|
+
.autoAck(false) // Disable auto-ack to manually control it
|
|
1337
|
+
.consume(async (message) => {
|
|
1338
|
+
return await riskyOperation(message.data)
|
|
1339
|
+
})
|
|
1340
|
+
.onSuccess(async (message, result) => {
|
|
1341
|
+
console.log('Success!')
|
|
1342
|
+
await queen.ack(message, true)
|
|
1343
|
+
})
|
|
1344
|
+
.onError(async (message, error) => {
|
|
1345
|
+
console.error('Error:', error.message)
|
|
1346
|
+
|
|
1347
|
+
// Custom logic: retry or DLQ?
|
|
1348
|
+
if (error.message.includes('temporary')) {
|
|
1349
|
+
// Retry
|
|
1350
|
+
await queen.ack(message, false)
|
|
1351
|
+
} else {
|
|
1352
|
+
// Send to DLQ immediately
|
|
1353
|
+
await queen.ack(message, 'failed', { error: error.message })
|
|
1354
|
+
}
|
|
1355
|
+
})
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
### Push Callbacks
|
|
1359
|
+
|
|
1360
|
+
```javascript
|
|
1361
|
+
await queen
|
|
1362
|
+
.queue('tasks')
|
|
1363
|
+
.push([
|
|
1364
|
+
{ data: { id: 1 } },
|
|
1365
|
+
{ data: { id: 2 } }
|
|
1366
|
+
])
|
|
1367
|
+
.onSuccess(async (messages) => {
|
|
1368
|
+
console.log('Pushed successfully!')
|
|
1369
|
+
})
|
|
1370
|
+
.onError(async (messages, error) => {
|
|
1371
|
+
console.error('Push failed:', error)
|
|
1372
|
+
})
|
|
1373
|
+
.onDuplicate(async (messages, error) => {
|
|
1374
|
+
console.warn('Duplicate transaction IDs detected')
|
|
1375
|
+
})
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
### Batch Ack with Mixed Results
|
|
1379
|
+
|
|
1380
|
+
```javascript
|
|
1381
|
+
const messages = await queen.queue('tasks').batch(10).pop()
|
|
1382
|
+
|
|
1383
|
+
// Process and mark each message individually
|
|
1384
|
+
for (const message of messages) {
|
|
1385
|
+
try {
|
|
1386
|
+
await processMessage(message.data)
|
|
1387
|
+
message._status = true // Mark as success
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
message._status = false // Mark as failure
|
|
1390
|
+
message._error = error.message
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Batch ack with individual statuses
|
|
1395
|
+
await queen.ack(messages)
|
|
1396
|
+
// Queen will ack some and nack others based on _status
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
---
|
|
1400
|
+
|
|
1401
|
+
## Part 14: Graceful Shutdown
|
|
1402
|
+
|
|
1403
|
+
Always clean up properly when shutting down!
|
|
1404
|
+
|
|
1405
|
+
### Why Graceful Shutdown?
|
|
1406
|
+
|
|
1407
|
+
When you kill a process:
|
|
1408
|
+
1. Buffered messages need to be flushed
|
|
1409
|
+
2. In-progress messages need to finish
|
|
1410
|
+
3. Connections need to close properly
|
|
1411
|
+
|
|
1412
|
+
### Automatic Shutdown (Built-In)
|
|
1413
|
+
|
|
1414
|
+
Queen automatically handles `SIGINT` and `SIGTERM`:
|
|
1415
|
+
|
|
1416
|
+
```javascript
|
|
1417
|
+
const queen = new Queen('http://localhost:6632')
|
|
1418
|
+
|
|
1419
|
+
// Your app runs...
|
|
1420
|
+
|
|
1421
|
+
// User presses Ctrl+C or Docker sends SIGTERM:
|
|
1422
|
+
// Queen automatically flushes buffers and closes cleanly!
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
### Manual Shutdown
|
|
1426
|
+
|
|
1427
|
+
```javascript
|
|
1428
|
+
const queen = new Queen('http://localhost:6632')
|
|
1429
|
+
|
|
1430
|
+
// Do work...
|
|
1431
|
+
|
|
1432
|
+
// Shutdown manually
|
|
1433
|
+
await queen.close()
|
|
1434
|
+
console.log('Queen shut down cleanly')
|
|
1435
|
+
```
|
|
1436
|
+
|
|
1437
|
+
### Shutdown with AbortController
|
|
1438
|
+
|
|
1439
|
+
For consumers, use signals to stop them gracefully:
|
|
1440
|
+
|
|
1441
|
+
```javascript
|
|
1442
|
+
const controller = new AbortController()
|
|
1443
|
+
|
|
1444
|
+
// Start consumer with abort signal
|
|
1445
|
+
const consumerPromise = queen
|
|
1446
|
+
.queue('tasks')
|
|
1447
|
+
.consume(async (message) => {
|
|
1448
|
+
await processMessage(message.data)
|
|
1449
|
+
}, { signal: controller.signal })
|
|
1450
|
+
|
|
1451
|
+
// Later... stop the consumer
|
|
1452
|
+
controller.abort()
|
|
1453
|
+
|
|
1454
|
+
// Wait for consumer to finish current message and stop
|
|
1455
|
+
await consumerPromise
|
|
1456
|
+
|
|
1457
|
+
// Close Queen
|
|
1458
|
+
await queen.close()
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
---
|
|
1462
|
+
|
|
1463
|
+
## Cheat Sheet
|
|
1464
|
+
|
|
1465
|
+
### Connection
|
|
1466
|
+
|
|
1467
|
+
```javascript
|
|
1468
|
+
const queen = new Queen('http://localhost:6632')
|
|
1469
|
+
const queen = new Queen(['http://server1:6632', 'http://server2:6632'])
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
### Queue Operations
|
|
1473
|
+
|
|
1474
|
+
```javascript
|
|
1475
|
+
// Create
|
|
1476
|
+
await queen.queue('my-queue').create()
|
|
1477
|
+
await queen.queue('my-queue').config({ priority: 5 }).create()
|
|
1478
|
+
|
|
1479
|
+
// Delete
|
|
1480
|
+
await queen.queue('my-queue').delete()
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
### Push
|
|
1484
|
+
|
|
1485
|
+
```javascript
|
|
1486
|
+
// Simple
|
|
1487
|
+
await queen.queue('q').push([{ data: { value: 1 } }])
|
|
1488
|
+
|
|
1489
|
+
// With partition
|
|
1490
|
+
await queen.queue('q').partition('p1').push([{ data: { value: 1 } }])
|
|
1491
|
+
|
|
1492
|
+
// With buffering
|
|
1493
|
+
await queen.queue('q').buffer({ messageCount: 100, timeMillis: 1000 }).push([{ data: { value: 1 } }])
|
|
1494
|
+
|
|
1495
|
+
// With custom transaction ID
|
|
1496
|
+
await queen.queue('q').push([{ transactionId: 'my-id', data: { value: 1 } }])
|
|
1497
|
+
```
|
|
1498
|
+
|
|
1499
|
+
### Pop
|
|
1500
|
+
|
|
1501
|
+
```javascript
|
|
1502
|
+
// Simple pop
|
|
1503
|
+
const msgs = await queen.queue('q').pop()
|
|
1504
|
+
|
|
1505
|
+
// Pop multiple
|
|
1506
|
+
const msgs = await queen.queue('q').batch(10).pop()
|
|
1507
|
+
|
|
1508
|
+
// Pop with long polling
|
|
1509
|
+
const msgs = await queen.queue('q').batch(10).wait(true).pop()
|
|
1510
|
+
|
|
1511
|
+
// Pop from partition
|
|
1512
|
+
const msgs = await queen.queue('q').partition('p1').pop()
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
### Consume
|
|
1516
|
+
|
|
1517
|
+
```javascript
|
|
1518
|
+
// Simple consume (runs forever)
|
|
1519
|
+
await queen.queue('q').consume(async (msg) => { /* process */ })
|
|
1520
|
+
|
|
1521
|
+
// Consume with limit
|
|
1522
|
+
await queen.queue('q').limit(10).consume(async (msg) => { /* process */ })
|
|
1523
|
+
|
|
1524
|
+
// Consume batches
|
|
1525
|
+
await queen.queue('q').batch(10).consume(async (msgs) => { /* process array */ })
|
|
1526
|
+
|
|
1527
|
+
// Consume with concurrency
|
|
1528
|
+
await queen.queue('q').concurrency(5).consume(async (msg) => { /* 5 parallel workers */ })
|
|
1529
|
+
|
|
1530
|
+
// Consume from partition
|
|
1531
|
+
await queen.queue('q').partition('p1').consume(async (msg) => { /* process */ })
|
|
1532
|
+
|
|
1533
|
+
// Consume with consumer group
|
|
1534
|
+
await queen.queue('q').group('my-group').consume(async (msg) => { /* process */ })
|
|
1535
|
+
|
|
1536
|
+
// Consume by namespace
|
|
1537
|
+
await queen.queue().namespace('my-ns').consume(async (msg) => { /* process */ })
|
|
1538
|
+
|
|
1539
|
+
// Consume by task
|
|
1540
|
+
await queen.queue().task('my-task').consume(async (msg) => { /* process */ })
|
|
1541
|
+
```
|
|
1542
|
+
|
|
1543
|
+
### Subscription Modes
|
|
1544
|
+
|
|
1545
|
+
```javascript
|
|
1546
|
+
// Default (all messages, including historical)
|
|
1547
|
+
await queen.queue('q').group('my-group').consume(async (msg) => { /* all messages */ })
|
|
1548
|
+
|
|
1549
|
+
// Skip historical messages, only new ones
|
|
1550
|
+
await queen.queue('q').group('my-group').subscriptionMode('new').consume(async (msg) => { /* new only */ })
|
|
1551
|
+
|
|
1552
|
+
// Alternative: subscriptionMode('new-only')
|
|
1553
|
+
await queen.queue('q').group('my-group').subscriptionMode('new-only').consume(async (msg) => { /* new only */ })
|
|
1554
|
+
|
|
1555
|
+
// Subscribe from 'now'
|
|
1556
|
+
await queen.queue('q').group('my-group').subscriptionFrom('now').consume(async (msg) => { /* from now */ })
|
|
1557
|
+
|
|
1558
|
+
// Subscribe from timestamp
|
|
1559
|
+
const timestamp = '2025-10-28T10:00:00.000Z'
|
|
1560
|
+
await queen.queue('q').group('my-group').subscriptionFrom(timestamp).consume(async (msg) => { /* from timestamp */ })
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
### Acknowledgment
|
|
1564
|
+
|
|
1565
|
+
```javascript
|
|
1566
|
+
// Ack success
|
|
1567
|
+
await queen.ack(message, true)
|
|
1568
|
+
|
|
1569
|
+
// Ack failure (will retry)
|
|
1570
|
+
await queen.ack(message, false)
|
|
1571
|
+
|
|
1572
|
+
// Ack with error details
|
|
1573
|
+
await queen.ack(message, false, { error: 'Something went wrong' })
|
|
1574
|
+
|
|
1575
|
+
// Batch ack
|
|
1576
|
+
await queen.ack([msg1, msg2, msg3], true)
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
### Transactions
|
|
1580
|
+
|
|
1581
|
+
```javascript
|
|
1582
|
+
await queen
|
|
1583
|
+
.transaction()
|
|
1584
|
+
.ack(message)
|
|
1585
|
+
.queue('output-queue')
|
|
1586
|
+
.push([{ data: { result: 'processed' } }])
|
|
1587
|
+
.commit()
|
|
1588
|
+
```
|
|
1589
|
+
|
|
1590
|
+
### Lease Renewal
|
|
1591
|
+
|
|
1592
|
+
```javascript
|
|
1593
|
+
// Manual renewal
|
|
1594
|
+
await queen.renew(message)
|
|
1595
|
+
await queen.renew([msg1, msg2, msg3])
|
|
1596
|
+
await queen.renew(message.leaseId)
|
|
1597
|
+
|
|
1598
|
+
// Auto renewal
|
|
1599
|
+
await queen.queue('q').renewLease(true, 60000).consume(async (msg) => { /* process */ })
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
### Buffering
|
|
1603
|
+
|
|
1604
|
+
```javascript
|
|
1605
|
+
// Flush all buffers
|
|
1606
|
+
await queen.flushAllBuffers()
|
|
1607
|
+
|
|
1608
|
+
// Flush specific queue
|
|
1609
|
+
await queen.queue('q').flushBuffer()
|
|
1610
|
+
|
|
1611
|
+
// Get buffer stats
|
|
1612
|
+
const stats = queen.getBufferStats()
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
### Message Tracing
|
|
1616
|
+
|
|
1617
|
+
```javascript
|
|
1618
|
+
// Basic trace
|
|
1619
|
+
await msg.trace({ data: { text: 'Processing started' } })
|
|
1620
|
+
|
|
1621
|
+
// Trace with name (for cross-message correlation)
|
|
1622
|
+
await msg.trace({
|
|
1623
|
+
traceName: 'order-12345',
|
|
1624
|
+
data: { text: 'Order created' }
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
// Multiple trace names (multi-dimensional tracking)
|
|
1628
|
+
await msg.trace({
|
|
1629
|
+
traceName: ['tenant-acme', 'room-123', 'user-456'],
|
|
1630
|
+
data: { text: 'Message sent' }
|
|
1631
|
+
})
|
|
1632
|
+
|
|
1633
|
+
// With event type
|
|
1634
|
+
await msg.trace({
|
|
1635
|
+
eventType: 'error', // info, error, step, processing, warning
|
|
1636
|
+
data: { text: 'Processing failed', reason: 'timeout' }
|
|
1637
|
+
})
|
|
1638
|
+
|
|
1639
|
+
// Rich data
|
|
1640
|
+
await msg.trace({
|
|
1641
|
+
traceName: 'report-gen-789',
|
|
1642
|
+
data: {
|
|
1643
|
+
text: 'Report generated',
|
|
1644
|
+
durationMs: 1500,
|
|
1645
|
+
sizeKB: 250
|
|
1646
|
+
}
|
|
1647
|
+
})
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
### DLQ
|
|
1651
|
+
|
|
1652
|
+
```javascript
|
|
1653
|
+
// Query DLQ
|
|
1654
|
+
const dlq = await queen.queue('q').dlq().limit(10).get()
|
|
1655
|
+
const dlq = await queen.queue('q').dlq('consumer-group').limit(10).get()
|
|
1656
|
+
const dlq = await queen.queue('q').dlq().from('2025-01-01').to('2025-01-31').get()
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
### Shutdown
|
|
1660
|
+
|
|
1661
|
+
```javascript
|
|
1662
|
+
await queen.close()
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
---
|
|
1666
|
+
|
|
1667
|
+
## Configuration Defaults
|
|
1668
|
+
|
|
1669
|
+
### Client Defaults
|
|
1670
|
+
```javascript
|
|
1671
|
+
{
|
|
1672
|
+
timeoutMillis: 30000, // 30 seconds
|
|
1673
|
+
retryAttempts: 3,
|
|
1674
|
+
retryDelayMillis: 1000,
|
|
1675
|
+
loadBalancingStrategy: 'round-robin',
|
|
1676
|
+
enableFailover: true
|
|
1677
|
+
}
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
### Queue Defaults
|
|
1681
|
+
```javascript
|
|
1682
|
+
{
|
|
1683
|
+
leaseTime: 300, // 5 minutes
|
|
1684
|
+
retryLimit: 3,
|
|
1685
|
+
priority: 0,
|
|
1686
|
+
delayedProcessing: 0,
|
|
1687
|
+
windowBuffer: 0,
|
|
1688
|
+
maxSize: 0, // Unlimited
|
|
1689
|
+
retentionSeconds: 0, // Keep forever
|
|
1690
|
+
completedRetentionSeconds: 0,
|
|
1691
|
+
encryptionEnabled: false
|
|
1692
|
+
}
|
|
1693
|
+
```
|
|
1694
|
+
|
|
1695
|
+
### Consume Defaults
|
|
1696
|
+
```javascript
|
|
1697
|
+
{
|
|
1698
|
+
concurrency: 1,
|
|
1699
|
+
batch: 1,
|
|
1700
|
+
autoAck: true,
|
|
1701
|
+
wait: true, // Long polling
|
|
1702
|
+
timeoutMillis: 30000,
|
|
1703
|
+
limit: null, // Run forever
|
|
1704
|
+
idleMillis: null, // No idle timeout
|
|
1705
|
+
renewLease: false
|
|
1706
|
+
}
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
### Pop Defaults
|
|
1710
|
+
```javascript
|
|
1711
|
+
{
|
|
1712
|
+
batch: 1,
|
|
1713
|
+
wait: false, // No long polling
|
|
1714
|
+
autoAck: false // Manual ack required
|
|
1715
|
+
}
|
|
1716
|
+
```
|
|
1717
|
+
|
|
1718
|
+
---
|
|
1719
|
+
|
|
1720
|
+
## Logging
|
|
1721
|
+
|
|
1722
|
+
Enable detailed logging for debugging:
|
|
1723
|
+
|
|
1724
|
+
```bash
|
|
1725
|
+
export QUEEN_CLIENT_LOG=true
|
|
1726
|
+
node your-app.js
|
|
1727
|
+
```
|
|
1728
|
+
|
|
1729
|
+
Example log output:
|
|
1730
|
+
```
|
|
1731
|
+
[2025-10-28T10:30:45.123Z] [INFO] [Queen.constructor] {"status":"initialized","urls":1}
|
|
1732
|
+
[2025-10-28T10:30:45.234Z] [INFO] [QueueBuilder.push] {"queue":"tasks","partition":"Default","count":5}
|
|
1733
|
+
[2025-10-28T10:30:46.789Z] [INFO] [HttpClient.request] {"method":"POST","url":"http://localhost:6632/api/v1/push"}
|
|
1734
|
+
```
|
|
1735
|
+
|
|
1736
|
+
---
|
|
1737
|
+
|
|
1738
|
+
## Tips & Best Practices
|
|
1739
|
+
|
|
1740
|
+
1. **Use `consume()` for workers** - It's simpler and handles retries automatically
|
|
1741
|
+
2. **Use `pop()` for control** - When you need precise control over acking and timing
|
|
1742
|
+
3. **Buffer for speed** - Always use buffering when pushing many messages
|
|
1743
|
+
4. **Partitions for order** - Use partitions when message order matters
|
|
1744
|
+
5. **Consumer groups for scale** - Run multiple workers in the same group
|
|
1745
|
+
6. **Transactions for consistency** - Use transactions when operations must be atomic
|
|
1746
|
+
7. **Enable DLQ** - Always enable DLQ in production to catch failures
|
|
1747
|
+
8. **Renew long leases** - Use auto-renewal for long-running tasks
|
|
1748
|
+
9. **Graceful shutdown** - Always call `queen.close()` before exiting
|
|
1749
|
+
10. **Monitor DLQ** - Regularly check your DLQ for failed messages
|
|
1750
|
+
|
|
1751
|
+
---
|
|
1752
|
+
|
|
1753
|
+
## Real-World Example: Complete Pipeline
|
|
1754
|
+
|
|
1755
|
+
Here's a complete example showing many features together:
|
|
1756
|
+
|
|
1757
|
+
```javascript
|
|
1758
|
+
import { Queen } from './client-js/client-v2/index.js'
|
|
1759
|
+
|
|
1760
|
+
const queen = new Queen('http://localhost:6632')
|
|
1761
|
+
|
|
1762
|
+
// Setup queues
|
|
1763
|
+
await queen.queue('raw-events').config({ priority: 5 }).create()
|
|
1764
|
+
await queen.queue('processed-events').config({ priority: 10 }).create()
|
|
1765
|
+
await queen.queue('notifications').config({ delayedProcessing: 60 }).create()
|
|
1766
|
+
|
|
1767
|
+
// Stage 1: Ingest raw events with buffering
|
|
1768
|
+
async function ingestEvents() {
|
|
1769
|
+
for (let i = 0; i < 10000; i++) {
|
|
1770
|
+
await queen
|
|
1771
|
+
.queue('raw-events')
|
|
1772
|
+
.partition(`user-${i % 100}`) // Partition by user
|
|
1773
|
+
.buffer({ messageCount: 500, timeMillis: 1000 })
|
|
1774
|
+
.push([{
|
|
1775
|
+
data: {
|
|
1776
|
+
userId: i % 100,
|
|
1777
|
+
event: 'page_view',
|
|
1778
|
+
timestamp: Date.now()
|
|
1779
|
+
}
|
|
1780
|
+
}])
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
await queen.flushAllBuffers()
|
|
1784
|
+
console.log('Ingestion complete!')
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// Stage 2: Process events with transaction
|
|
1788
|
+
async function processEvents() {
|
|
1789
|
+
await queen
|
|
1790
|
+
.queue('raw-events')
|
|
1791
|
+
.group('processors')
|
|
1792
|
+
.concurrency(5)
|
|
1793
|
+
.batch(10)
|
|
1794
|
+
.autoAck(false) // Manual ack for transactions
|
|
1795
|
+
.consume(async (messages) => {
|
|
1796
|
+
// Process batch
|
|
1797
|
+
const processed = messages.map(m => ({
|
|
1798
|
+
userId: m.data.userId,
|
|
1799
|
+
processed: true,
|
|
1800
|
+
timestamp: Date.now()
|
|
1801
|
+
}))
|
|
1802
|
+
|
|
1803
|
+
// Atomic: ack inputs and push outputs
|
|
1804
|
+
const txn = queen.transaction()
|
|
1805
|
+
|
|
1806
|
+
for (const msg of messages) {
|
|
1807
|
+
txn.ack(msg)
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
txn.queue('processed-events').push(
|
|
1811
|
+
processed.map(p => ({ data: p }))
|
|
1812
|
+
)
|
|
1813
|
+
|
|
1814
|
+
await txn.commit()
|
|
1815
|
+
})
|
|
1816
|
+
.onError(async (messages, error) => {
|
|
1817
|
+
console.error('Processing failed:', error)
|
|
1818
|
+
await queen.ack(messages, false)
|
|
1819
|
+
})
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Stage 3: Send notifications (delayed)
|
|
1823
|
+
async function sendNotifications() {
|
|
1824
|
+
await queen
|
|
1825
|
+
.queue('processed-events')
|
|
1826
|
+
.group('notifiers')
|
|
1827
|
+
.renewLease(true, 30000) // Auto-renew every 30s
|
|
1828
|
+
.consume(async (message) => {
|
|
1829
|
+
// Queue delayed notification
|
|
1830
|
+
await queen
|
|
1831
|
+
.queue('notifications')
|
|
1832
|
+
.push([{
|
|
1833
|
+
data: {
|
|
1834
|
+
userId: message.data.userId,
|
|
1835
|
+
message: 'Your data has been processed!'
|
|
1836
|
+
}
|
|
1837
|
+
}])
|
|
1838
|
+
|
|
1839
|
+
console.log(`Notification queued for user ${message.data.userId}`)
|
|
1840
|
+
})
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Stage 4: Check DLQ periodically
|
|
1844
|
+
setInterval(async () => {
|
|
1845
|
+
const dlq = await queen
|
|
1846
|
+
.queue('raw-events')
|
|
1847
|
+
.dlq()
|
|
1848
|
+
.limit(10)
|
|
1849
|
+
.get()
|
|
1850
|
+
|
|
1851
|
+
if (dlq.total > 0) {
|
|
1852
|
+
console.warn(`⚠️ ${dlq.total} failed messages in DLQ!`)
|
|
1853
|
+
}
|
|
1854
|
+
}, 60000) // Check every minute
|
|
1855
|
+
|
|
1856
|
+
// Run the pipeline
|
|
1857
|
+
await ingestEvents()
|
|
1858
|
+
await Promise.all([
|
|
1859
|
+
processEvents(),
|
|
1860
|
+
sendNotifications()
|
|
1861
|
+
])
|
|
1862
|
+
|
|
1863
|
+
// Graceful shutdown
|
|
1864
|
+
process.on('SIGINT', async () => {
|
|
1865
|
+
await queen.close()
|
|
1866
|
+
process.exit(0)
|
|
1867
|
+
})
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
---
|
|
1871
|
+
|
|
1872
|
+
## What's Next?
|
|
1873
|
+
|
|
1874
|
+
You now know everything about Queen v2! 🎉
|
|
1875
|
+
|
|
1876
|
+
**Additional resources:**
|
|
1877
|
+
- [API Documentation](../../API.md) - Complete API reference
|
|
1878
|
+
- [Test Examples](../test-v2/) - 94 working test cases
|
|
1879
|
+
- [Architecture Guide](../../docs/) - Deep dive into Queen's internals
|
|
1880
|
+
|
|
1881
|
+
**Need help?** Check out the test files in `test-v2/` - they're full of working examples!
|
|
1882
|
+
|
|
1883
|
+
Happy queuing! 👑✨
|