@vercel/queue 0.0.0-alpha.5 → 0.0.0-alpha.7
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 +277 -501
- package/dist/index.d.mts +22 -458
- package/dist/index.d.ts +22 -458
- package/dist/index.js +110 -303
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +110 -295
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,7 @@ type Message = {
|
|
|
46
46
|
timestamp: number;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
// Automatically uses default client and JSON transport
|
|
49
|
+
// Send a message to a topic
|
|
51
50
|
await send<Message>("my-topic", {
|
|
52
51
|
message: "Hello, World!",
|
|
53
52
|
timestamp: Date.now(),
|
|
@@ -55,55 +54,19 @@ await send<Message>("my-topic", {
|
|
|
55
54
|
|
|
56
55
|
// Consume a single message off the queue
|
|
57
56
|
// (Often wrapped in a loop to keep polling messages off the queue)
|
|
58
|
-
await receive<Message>("my-topic", "my-consumer-group", (
|
|
59
|
-
console.log(
|
|
57
|
+
await receive<Message>("my-topic", "my-consumer-group", (message, metadata) => {
|
|
58
|
+
console.log("Received:", message.message);
|
|
59
|
+
console.log("Timestamp:", new Date(message.timestamp));
|
|
60
|
+
console.log("Message Metadata", metadata);
|
|
61
|
+
// => { messageId, deliveryCount, timestamp }
|
|
60
62
|
});
|
|
61
|
-
|
|
62
|
-
// Option 2: Using createTopic for more control
|
|
63
|
-
|
|
64
|
-
import { createTopic } from "@vercel/queue";
|
|
65
|
-
|
|
66
|
-
// Create a topic with JSON serialization (default)
|
|
67
|
-
// Uses default QueueClient automatically authenticated from Vercel environment
|
|
68
|
-
const topic = createTopic<Message>("my-topic");
|
|
69
|
-
|
|
70
|
-
// Publish a message
|
|
71
|
-
await topic.publish({
|
|
72
|
-
message: "Hello, World!",
|
|
73
|
-
timestamp: Date.now(),
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// Create a consumer group
|
|
77
|
-
const consumer = topic.consumerGroup("my-consumer-group");
|
|
78
|
-
|
|
79
|
-
// Process next available message (one-shot processing)
|
|
80
|
-
try {
|
|
81
|
-
await consumer.consume(async (message, metadata) => {
|
|
82
|
-
console.log("Received:", message.message);
|
|
83
|
-
console.log("Timestamp:", new Date(message.timestamp));
|
|
84
|
-
console.log("Message Metadata", metadata);
|
|
85
|
-
// => { messageId, deliveryCount, timestamp }
|
|
86
|
-
});
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error("Processing error:", error);
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Run the script
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
# Install dotenv-cli and ts-node if you need it
|
|
96
|
-
npm i -g dotenv-cli ts-node typescript
|
|
97
|
-
|
|
98
|
-
# Run the script with the OIDC token
|
|
99
|
-
dotenv -e .env.local ts-node index.ts
|
|
100
63
|
```
|
|
101
64
|
|
|
102
65
|
## Usage with Vercel
|
|
103
66
|
|
|
104
67
|
When deploying on Vercel, rather than having a persistent server subscribed to a
|
|
105
|
-
queue, Vercel can trigger
|
|
106
|
-
consumption.
|
|
68
|
+
queue, Vercel can automatically trigger your API routes when messages are ready for
|
|
69
|
+
consumption based on your vercel.json configuration.
|
|
107
70
|
|
|
108
71
|
To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
|
|
109
72
|
existing app or create one using
|
|
@@ -134,69 +97,13 @@ Create a new server function to publish messages
|
|
|
134
97
|
import { send } from "@vercel/queue";
|
|
135
98
|
|
|
136
99
|
export async function publishTestMessage(message: string) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
// Provide a callback URL to invoke a consumer when the message is ready to be processed
|
|
143
|
-
callback: {
|
|
144
|
-
url: getCallbackUrl() // implementation below
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
console.log(`Published message ${messageId}`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Option 2: Customize the topic, transport, consumer groups, etc.
|
|
153
|
-
import { createTopic } from "@vercel/queue";
|
|
154
|
-
|
|
155
|
-
export async function publishTestMessage(message: string) {
|
|
156
|
-
// Create a topic with JSON serialization (default)
|
|
157
|
-
const topic = createTopic<{ message: string; timestamp: number }>("my-topic");
|
|
158
|
-
|
|
159
|
-
// Publish the message
|
|
160
|
-
const { messageId } = await topic.publish(
|
|
161
|
-
{ message, timestamp: Date.now() },
|
|
162
|
-
{
|
|
163
|
-
// Provide multiple callback URLs to invoke multiple consumer groups
|
|
164
|
-
callback: {
|
|
165
|
-
{
|
|
166
|
-
"consumer-group-1": {
|
|
167
|
-
url: getCallbackUrl()
|
|
168
|
-
},
|
|
169
|
-
"consumer-group-2": {
|
|
170
|
-
url: getCallbackUrl()
|
|
171
|
-
delay: 5 // Delay callback by 5 seconds
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
);
|
|
100
|
+
const { messageId } = await send("my-topic", {
|
|
101
|
+
message,
|
|
102
|
+
timestamp: Date.now(),
|
|
103
|
+
});
|
|
177
104
|
|
|
178
105
|
console.log(`Published message ${messageId}`);
|
|
179
106
|
}
|
|
180
|
-
|
|
181
|
-
// Helper function to generate a local callback URL
|
|
182
|
-
function getCallbackUrl() {
|
|
183
|
-
const callbackUrl = new URL(
|
|
184
|
-
process.env.VERCEL_URL
|
|
185
|
-
? `https://${process.env.VERCEL_URL}/api/queue/handle`
|
|
186
|
-
: "http://localhost:3000/api/queue/handle"
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
// Add Vercel automation bypass secret if available (for preview deployments)
|
|
190
|
-
if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) {
|
|
191
|
-
callbackUrl.searchParams.set(
|
|
192
|
-
"x-vercel-protection-bypass",
|
|
193
|
-
process.env.VERCEL_AUTOMATION_BYPASS_SECRET
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return callbackUrl.toString();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
107
|
```
|
|
201
108
|
|
|
202
109
|
Now wire up the server function to your app
|
|
@@ -218,86 +125,175 @@ export default function Page() {
|
|
|
218
125
|
|
|
219
126
|
### Consuming the queue
|
|
220
127
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
128
|
+
Messages are consumed using consumer groups, which provide load balancing and parallel processing capabilities.
|
|
129
|
+
|
|
130
|
+
## Usage with Vercel
|
|
224
131
|
|
|
225
|
-
|
|
132
|
+
To consume queue messages in a Vercel deployment, you need to create (Next.js) API routes and configure them in your `vercel.json` file.
|
|
133
|
+
|
|
134
|
+
### 1. Create API Routes
|
|
135
|
+
|
|
136
|
+
Create API routes to handle incoming queue messages using the `handleCallback` helper:
|
|
226
137
|
|
|
227
138
|
```typescript
|
|
228
139
|
// app/api/queue/handle/route.ts
|
|
229
140
|
import { handleCallback } from "@vercel/queue";
|
|
230
141
|
|
|
231
|
-
// Option 1:
|
|
142
|
+
// Option 1: Single topic with multiple consumer groups
|
|
232
143
|
export const POST = handleCallback({
|
|
233
|
-
"my-topic":
|
|
234
|
-
|
|
235
|
-
|
|
144
|
+
"my-topic": {
|
|
145
|
+
"consumer-group-1": async (message, metadata) => {
|
|
146
|
+
console.log(`Consumer group 1 processing:`, message, metadata);
|
|
147
|
+
// Handle consumer group 1 logic
|
|
148
|
+
await processGroup1(message);
|
|
149
|
+
},
|
|
150
|
+
"consumer-group-2": async (message, metadata) => {
|
|
151
|
+
console.log(`Consumer group 2 processing:`, message, metadata);
|
|
152
|
+
// Handle consumer group 2 logic
|
|
153
|
+
await processGroup2(message);
|
|
154
|
+
},
|
|
236
155
|
},
|
|
237
|
-
|
|
238
|
-
// .. more topic handlers can be provided here
|
|
239
156
|
});
|
|
240
157
|
|
|
241
|
-
|
|
242
|
-
//
|
|
158
|
+
async function processGroup1(message: any) {
|
|
159
|
+
// Consumer group 1 specific logic
|
|
160
|
+
}
|
|
243
161
|
|
|
244
|
-
|
|
162
|
+
async function processGroup2(message: any) {
|
|
163
|
+
// Consumer group 2 specific logic
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Alternative: Multiple topics in one handler
|
|
245
169
|
export const POST = handleCallback({
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
170
|
+
"user-events": {
|
|
171
|
+
welcome: async (message, metadata) => {
|
|
172
|
+
console.log(`New user event:`, message, metadata);
|
|
173
|
+
await sendWelcomeEmail(message.email);
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
"order-events": {
|
|
177
|
+
fulfillment: async (order, metadata) => {
|
|
178
|
+
console.log(`Processing order:`, order, metadata);
|
|
179
|
+
await fulfillOrder(order);
|
|
251
180
|
},
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
181
|
+
analytics: async (order, metadata) => {
|
|
182
|
+
console.log(`Tracking order:`, order, metadata);
|
|
183
|
+
await trackOrder(order);
|
|
255
184
|
},
|
|
256
185
|
},
|
|
257
186
|
});
|
|
258
187
|
```
|
|
259
188
|
|
|
260
|
-
|
|
189
|
+
### 2. Configure vercel.json
|
|
190
|
+
|
|
191
|
+
Create a `vercel.json` file in your project root to declare which topics and consumer groups each API route handles:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"functions": {
|
|
196
|
+
"app/api/queue/handle/route.ts": {
|
|
197
|
+
"experimentalTriggers": [
|
|
198
|
+
{
|
|
199
|
+
"type": "queue/v1beta",
|
|
200
|
+
"topic": "my-topic",
|
|
201
|
+
"consumer": "consumer-group-1"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"type": "queue/v1beta",
|
|
205
|
+
"topic": "my-topic",
|
|
206
|
+
"consumer": "consumer-group-2"
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
261
213
|
|
|
262
|
-
###
|
|
214
|
+
### 3. Multiple API Routes
|
|
263
215
|
|
|
264
|
-
|
|
216
|
+
You can also create separate API routes for different topics:
|
|
265
217
|
|
|
266
218
|
```typescript
|
|
267
|
-
|
|
219
|
+
// app/api/queue/users/route.ts - Handle user events
|
|
220
|
+
import { handleCallback } from "@vercel/queue";
|
|
268
221
|
|
|
269
|
-
const
|
|
270
|
-
"
|
|
271
|
-
|
|
272
|
-
);
|
|
222
|
+
export const POST = handleCallback({
|
|
223
|
+
"user-events": {
|
|
224
|
+
processors: async (user, metadata) => {
|
|
225
|
+
console.log(`Processing user event:`, user, metadata);
|
|
226
|
+
await sendWelcomeEmail(user.email);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
273
231
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
232
|
+
```typescript
|
|
233
|
+
// app/api/queue/orders/route.ts - Handle order events
|
|
234
|
+
import { handleCallback } from "@vercel/queue";
|
|
235
|
+
|
|
236
|
+
export const POST = handleCallback({
|
|
237
|
+
"order-events": {
|
|
238
|
+
fulfillment: async (order, metadata) => {
|
|
239
|
+
console.log(`Processing order:`, order, metadata);
|
|
240
|
+
await fulfillOrder(order);
|
|
241
|
+
},
|
|
242
|
+
},
|
|
284
243
|
});
|
|
285
244
|
```
|
|
286
245
|
|
|
246
|
+
With corresponding `vercel.json`:
|
|
247
|
+
|
|
248
|
+
```json
|
|
249
|
+
{
|
|
250
|
+
"functions": {
|
|
251
|
+
"app/api/queue/users/route.ts": {
|
|
252
|
+
"experimentalTriggers": [
|
|
253
|
+
{
|
|
254
|
+
"type": "queue/v1beta",
|
|
255
|
+
"topic": "user-events",
|
|
256
|
+
"consumer": "processors"
|
|
257
|
+
}
|
|
258
|
+
]
|
|
259
|
+
},
|
|
260
|
+
"app/api/queue/orders/route.ts": {
|
|
261
|
+
"experimentalTriggers": [
|
|
262
|
+
{
|
|
263
|
+
"type": "queue/v1beta",
|
|
264
|
+
"topic": "order-events",
|
|
265
|
+
"consumer": "fulfillment"
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Key Points
|
|
274
|
+
|
|
275
|
+
- **Automatic Triggering**: Vercel automatically triggers your API routes when messages are available for the configured topic/consumer combinations
|
|
276
|
+
- **Message Processing**: Your API routes receive the message ID and other metadata via headers, then use the queue client to process the specific message
|
|
277
|
+
- **Configuration Required**: The `vercel.json` file is essential - it tells Vercel which topics and consumers each route should handle
|
|
278
|
+
- **No Polling**: Unlike traditional queue consumers, you don't need to poll for messages - Vercel handles the triggering automatically
|
|
279
|
+
|
|
280
|
+
## Key Features
|
|
281
|
+
|
|
287
282
|
### Consumer Groups
|
|
288
283
|
|
|
289
284
|
Multiple consumers can process messages from the same topic in parallel:
|
|
290
285
|
|
|
291
286
|
```typescript
|
|
292
287
|
// Multiple workers in the same group - they share/split messages
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
288
|
+
// Using the same consumer group name means they will load balance messages
|
|
289
|
+
await receive("my-topic", "workers", handler1);
|
|
290
|
+
await receive("my-topic", "workers", handler2);
|
|
291
|
+
// handler1 and handler2 will receive different messages (load balancing)
|
|
296
292
|
|
|
297
293
|
// Different consumer groups - each gets copies of ALL messages
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
//
|
|
294
|
+
await receive("my-topic", "analytics", analyticsHandler);
|
|
295
|
+
await receive("my-topic", "webhooks", webhooksHandler);
|
|
296
|
+
// analyticsHandler and webhooksHandler will both receive every message
|
|
301
297
|
```
|
|
302
298
|
|
|
303
299
|
## Architecture
|
|
@@ -305,8 +301,8 @@ const webhooks = topic.consumerGroup("webhooks");
|
|
|
305
301
|
- **Topics**: Named message channels with configurable serialization
|
|
306
302
|
- **Consumer Groups**: Named groups of consumers that process messages in
|
|
307
303
|
parallel
|
|
308
|
-
- `
|
|
309
|
-
-
|
|
304
|
+
- `receive()`: Process messages with flexible consumption patterns
|
|
305
|
+
- Basic usage: Process next available message
|
|
310
306
|
- With `messageId`: Process specific message by ID
|
|
311
307
|
- With `skipPayload: true`: Process message metadata only (without payload)
|
|
312
308
|
- **Transports**: Pluggable serialization/deserialization for different data
|
|
@@ -328,8 +324,7 @@ The multipart parser is optimized for high-throughput scenarios:
|
|
|
328
324
|
|
|
329
325
|
The queue client supports customizable serialization through the `Transport`
|
|
330
326
|
interface with **streaming support** for memory-efficient processing. Transport
|
|
331
|
-
can be configured
|
|
332
|
-
**consumer group level** when creating a consumer group.
|
|
327
|
+
can be configured using the `transport` option when calling `send()` or `receive()`.
|
|
333
328
|
|
|
334
329
|
### Built-in Transports
|
|
335
330
|
|
|
@@ -339,11 +334,15 @@ Buffers data for JSON parsing - suitable for structured data that fits in
|
|
|
339
334
|
memory.
|
|
340
335
|
|
|
341
336
|
```typescript
|
|
342
|
-
import {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
337
|
+
import { send, JsonTransport } from "@vercel/queue";
|
|
338
|
+
|
|
339
|
+
// JsonTransport is the default, so these are equivalent:
|
|
340
|
+
await send("json-topic", { data: "example" });
|
|
341
|
+
await send(
|
|
342
|
+
"json-topic",
|
|
343
|
+
{ data: "example" },
|
|
344
|
+
{ transport: new JsonTransport() },
|
|
345
|
+
);
|
|
347
346
|
```
|
|
348
347
|
|
|
349
348
|
#### BufferTransport
|
|
@@ -351,55 +350,15 @@ const topic = createTopic<{ data: any }>("json-topic");
|
|
|
351
350
|
Buffers the entire payload into memory as a Buffer - suitable for binary data
|
|
352
351
|
that fits in memory.
|
|
353
352
|
|
|
354
|
-
```typescript
|
|
355
|
-
import { BufferTransport, createTopic } from "@vercel/queue";
|
|
356
|
-
|
|
357
|
-
const topic = createTopic<Buffer>("binary-topic", new BufferTransport());
|
|
358
|
-
const binaryData = Buffer.from("Binary data", "utf8");
|
|
359
|
-
await topic.publish(binaryData);
|
|
360
|
-
```
|
|
361
|
-
|
|
362
353
|
#### StreamTransport
|
|
363
354
|
|
|
364
355
|
**True streaming support** - passes ReadableStream directly without buffering.
|
|
365
356
|
Ideal for large files and memory-efficient processing.
|
|
366
357
|
|
|
367
|
-
```typescript
|
|
368
|
-
import { createTopic, StreamTransport } from "@vercel/queue";
|
|
369
|
-
|
|
370
|
-
const topic = createTopic<ReadableStream<Uint8Array>>(
|
|
371
|
-
"streaming-topic",
|
|
372
|
-
new StreamTransport(),
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
// Send large file as stream without loading into memory
|
|
376
|
-
const fileStream = new ReadableStream<Uint8Array>({
|
|
377
|
-
start(controller) {
|
|
378
|
-
// Read file in chunks
|
|
379
|
-
for (const chunk of readFileInChunks("large-file.bin")) {
|
|
380
|
-
controller.enqueue(chunk);
|
|
381
|
-
}
|
|
382
|
-
controller.close();
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
await topic.publish(fileStream);
|
|
387
|
-
```
|
|
388
|
-
|
|
389
358
|
### Custom Transport
|
|
390
359
|
|
|
391
360
|
You can create your own serialization format by implementing the `Transport`
|
|
392
|
-
interface
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
import { Transport } from "@vercel/queue";
|
|
396
|
-
|
|
397
|
-
interface Transport<T = unknown> {
|
|
398
|
-
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
399
|
-
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
400
|
-
contentType: string;
|
|
401
|
-
}
|
|
402
|
-
```
|
|
361
|
+
interface.
|
|
403
362
|
|
|
404
363
|
### Choosing the Right Transport
|
|
405
364
|
|
|
@@ -413,52 +372,7 @@ interface Transport<T = unknown> {
|
|
|
413
372
|
|
|
414
373
|
## API Reference
|
|
415
374
|
|
|
416
|
-
###
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
// Simple usage - automatically gets OIDC token from Vercel environment
|
|
420
|
-
const client = new QueueClient();
|
|
421
|
-
|
|
422
|
-
// Or with options
|
|
423
|
-
const client = new QueueClient({
|
|
424
|
-
token?: string; // Optional - will auto-detect if not provided
|
|
425
|
-
baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
|
|
426
|
-
});
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
### Topic
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
// Simple usage with default client
|
|
433
|
-
const topic = createTopic<T>(topicName, transport?);
|
|
434
|
-
|
|
435
|
-
// For custom client configuration, use Topic constructor directly
|
|
436
|
-
const customClient = new QueueClient({ baseUrl: "https://custom.vqs.vercel.sh" });
|
|
437
|
-
const topic = new Topic<T>(customClient, topicName, transport?);
|
|
438
|
-
|
|
439
|
-
// Publish a message (uses topic's transport)
|
|
440
|
-
await topic.publish(payload, options?);
|
|
441
|
-
|
|
442
|
-
// Trigger a callback URL when the message is
|
|
443
|
-
// ready for consumption
|
|
444
|
-
await topic.publish(payload, {
|
|
445
|
-
callback: { url: "https://example.com/webhook" }
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Or provide multiple callbacks (each URL is called
|
|
449
|
-
// with a separate consumer group)
|
|
450
|
-
await topic.publish(payload, {
|
|
451
|
-
callback: {
|
|
452
|
-
group1: { url: "https://example.com/webhook1" },
|
|
453
|
-
group2: { url: "https://example.com/webhook2", delay: 30 }
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// Create a consumer group (can override transport)
|
|
458
|
-
const consumer = topic.consumerGroup<U>(groupName, options?);
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
### Send (Shorthand)
|
|
375
|
+
### Send Function
|
|
462
376
|
|
|
463
377
|
```typescript
|
|
464
378
|
// Simple send - automatically uses default client and JSON transport
|
|
@@ -469,7 +383,6 @@ await send<T>(topicName, payload, {
|
|
|
469
383
|
transport?: Transport<T>;
|
|
470
384
|
idempotencyKey?: string;
|
|
471
385
|
retentionSeconds?: number;
|
|
472
|
-
callback?: Record<string, CallbackConfig> | CallbackConfig;
|
|
473
386
|
});
|
|
474
387
|
|
|
475
388
|
// Examples:
|
|
@@ -477,31 +390,31 @@ await send("notifications", { userId: "123", message: "Welcome!" });
|
|
|
477
390
|
|
|
478
391
|
await send("images", imageBuffer, {
|
|
479
392
|
transport: new BufferTransport(),
|
|
480
|
-
callback: { url: "https://example.com/process-image" }
|
|
481
393
|
});
|
|
482
394
|
|
|
483
395
|
await send("events", eventData, {
|
|
484
396
|
idempotencyKey: "unique-key-123",
|
|
485
397
|
retentionSeconds: 3600,
|
|
486
|
-
callback: {
|
|
487
|
-
analytics: { url: "https://analytics.example.com/webhook" },
|
|
488
|
-
notifications: { url: "https://notifications.example.com/webhook", delay: 30 }
|
|
489
|
-
}
|
|
490
398
|
});
|
|
491
399
|
```
|
|
492
400
|
|
|
493
|
-
###
|
|
401
|
+
### Receive Function
|
|
494
402
|
|
|
495
403
|
```typescript
|
|
496
404
|
// Process next available message (simplest form)
|
|
497
|
-
await consumer
|
|
405
|
+
await receive("topic-name", "consumer-group", handler);
|
|
498
406
|
|
|
499
407
|
// Process specific message by ID with payload
|
|
500
|
-
await
|
|
408
|
+
await receive("topic-name", "consumer-group", handler, {
|
|
409
|
+
messageId: "message-id",
|
|
410
|
+
});
|
|
501
411
|
|
|
502
412
|
// Process specific message by ID without payload (metadata only)
|
|
503
413
|
// handler will be called with `undefined` as the payload
|
|
504
|
-
await
|
|
414
|
+
await receive("topic-name", "consumer-group", handler, {
|
|
415
|
+
messageId: "message-id",
|
|
416
|
+
skipPayload: true,
|
|
417
|
+
});
|
|
505
418
|
```
|
|
506
419
|
|
|
507
420
|
### Message Handler
|
|
@@ -528,12 +441,16 @@ interface MessageMetadata {
|
|
|
528
441
|
}
|
|
529
442
|
```
|
|
530
443
|
|
|
531
|
-
###
|
|
444
|
+
### Receive Options
|
|
532
445
|
|
|
533
446
|
```typescript
|
|
534
|
-
|
|
447
|
+
// Options for the receive function
|
|
448
|
+
interface ReceiveOptions<T = unknown> {
|
|
535
449
|
messageId?: string; // Process specific message by ID
|
|
536
450
|
skipPayload?: boolean; // Skip payload download (requires messageId)
|
|
451
|
+
transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
|
|
452
|
+
visibilityTimeoutSeconds?: number; // Message visibility timeout
|
|
453
|
+
refreshInterval?: number; // Refresh interval for long-running operations
|
|
537
454
|
}
|
|
538
455
|
```
|
|
539
456
|
|
|
@@ -547,44 +464,33 @@ interface Transport<T = unknown> {
|
|
|
547
464
|
}
|
|
548
465
|
```
|
|
549
466
|
|
|
550
|
-
### Callback
|
|
467
|
+
### Callback Handler
|
|
551
468
|
|
|
552
469
|
```typescript
|
|
553
|
-
//
|
|
554
|
-
function
|
|
470
|
+
// Create a callback handler for Next.js route handlers
|
|
471
|
+
function handleCallback(
|
|
472
|
+
handlers: CallbackHandlers,
|
|
473
|
+
): (request: Request) => Promise<Response>;
|
|
555
474
|
|
|
556
|
-
//
|
|
557
|
-
interface CallbackMessageOptions {
|
|
558
|
-
queueName: string;
|
|
559
|
-
consumerGroup: string;
|
|
560
|
-
messageId: string;
|
|
561
|
-
}
|
|
562
|
-
// Create a callback handler for NextJS route handlers
|
|
563
|
-
function handleCallback(handlers: CallbackHandlers): (request: Request) => Promise<Response>;
|
|
564
|
-
|
|
565
|
-
// Configuration object with handlers for different topics
|
|
475
|
+
// Configuration object with handlers for different topics and consumer groups
|
|
566
476
|
type CallbackHandlers = {
|
|
567
|
-
[topicName: string]:
|
|
568
|
-
| MessageHandler // Single handler (uses 'default' consumer group)
|
|
569
|
-
| { [consumerGroup: string]: MessageHandler }; // Multiple consumer group handlers
|
|
477
|
+
[topicName: string]: { [consumerGroup: string]: MessageHandler };
|
|
570
478
|
};
|
|
571
479
|
|
|
572
480
|
// Example usage:
|
|
573
481
|
export const POST = handleCallback({
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
482
|
+
"user-events": {
|
|
483
|
+
welcome: (message, metadata) => {
|
|
484
|
+
console.log(`New user event:`, message, metadata);
|
|
485
|
+
},
|
|
577
486
|
},
|
|
578
487
|
|
|
579
|
-
//
|
|
488
|
+
// Multiple consumer groups per topic
|
|
580
489
|
"image-processing": {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
490
|
+
compress: (message, metadata) => console.log("Compressing image", message),
|
|
491
|
+
resize: (message, metadata) => console.log("Resizing image", message),
|
|
492
|
+
},
|
|
584
493
|
});
|
|
585
|
-
|
|
586
|
-
// Error thrown for invalid callback requests
|
|
587
|
-
class InvalidCallbackError extends Error;
|
|
588
494
|
```
|
|
589
495
|
|
|
590
496
|
## Examples
|
|
@@ -598,27 +504,16 @@ interface UserEvent {
|
|
|
598
504
|
timestamp: number;
|
|
599
505
|
}
|
|
600
506
|
|
|
601
|
-
//
|
|
507
|
+
// Send a message
|
|
602
508
|
await send<UserEvent>("user-events", {
|
|
603
509
|
userId: "123",
|
|
604
510
|
action: "login",
|
|
605
511
|
timestamp: Date.now(),
|
|
606
512
|
});
|
|
607
513
|
|
|
608
|
-
//
|
|
609
|
-
const userTopic = createTopic<UserEvent>("user-events");
|
|
610
|
-
|
|
611
|
-
await userTopic.publish({
|
|
612
|
-
userId: "123",
|
|
613
|
-
action: "login",
|
|
614
|
-
timestamp: Date.now(),
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
const consumer = userTopic.consumerGroup("processors");
|
|
618
|
-
|
|
619
|
-
// Process next available message
|
|
514
|
+
// Receive and process a message
|
|
620
515
|
try {
|
|
621
|
-
await
|
|
516
|
+
await receive<UserEvent>("user-events", "processors", async (message) => {
|
|
622
517
|
console.log(`User ${message.userId} performed ${message.action}`);
|
|
623
518
|
});
|
|
624
519
|
} catch (error) {
|
|
@@ -629,16 +524,13 @@ try {
|
|
|
629
524
|
### Processing Specific Messages by ID
|
|
630
525
|
|
|
631
526
|
```typescript
|
|
632
|
-
const userTopic = createTopic<{ userId: string; action: string }>(
|
|
633
|
-
"user-events",
|
|
634
|
-
);
|
|
635
|
-
const consumer = userTopic.consumerGroup("processors");
|
|
636
|
-
|
|
637
527
|
// Process a specific message if you know its ID
|
|
638
528
|
const messageId = "01234567-89ab-cdef-0123-456789abcdef";
|
|
639
529
|
|
|
640
530
|
try {
|
|
641
|
-
await
|
|
531
|
+
await receive<{ userId: string; action: string }>(
|
|
532
|
+
"user-events",
|
|
533
|
+
"processors",
|
|
642
534
|
async (message, { messageId }) => {
|
|
643
535
|
console.log(`Processing specific message: ${messageId}`);
|
|
644
536
|
console.log(`User ${message.userId} performed ${message.action}`);
|
|
@@ -649,8 +541,6 @@ try {
|
|
|
649
541
|
} catch (error) {
|
|
650
542
|
if (error.message.includes("not found or not available")) {
|
|
651
543
|
console.log("Message was already processed or does not exist");
|
|
652
|
-
} else if (error.message.includes("FIFO ordering violation")) {
|
|
653
|
-
console.log("FIFO queue requires processing messages in order");
|
|
654
544
|
} else {
|
|
655
545
|
console.error("Error processing message:", error);
|
|
656
546
|
}
|
|
@@ -660,21 +550,22 @@ try {
|
|
|
660
550
|
### Processing Next Available Message
|
|
661
551
|
|
|
662
552
|
```typescript
|
|
663
|
-
const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
|
|
664
|
-
const worker = workTopic.consumerGroup("workers");
|
|
665
|
-
|
|
666
553
|
// Process the next available message (one-shot processing)
|
|
667
554
|
try {
|
|
668
|
-
await
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
555
|
+
await receive<{ taskType: string; data: any }>(
|
|
556
|
+
"work-queue",
|
|
557
|
+
"workers",
|
|
558
|
+
async (message) => {
|
|
559
|
+
console.log(`Processing task: ${message.taskType}`);
|
|
560
|
+
await processTask(message.taskType, message.data);
|
|
561
|
+
},
|
|
562
|
+
);
|
|
672
563
|
console.log("Message processed successfully");
|
|
673
564
|
} catch (error) {
|
|
674
565
|
if (error instanceof QueueEmptyError) {
|
|
675
566
|
console.log("No messages available");
|
|
676
567
|
} else if (error instanceof MessageLockedError) {
|
|
677
|
-
console.log("Next message is locked
|
|
568
|
+
console.log("Next message is locked");
|
|
678
569
|
if (error.retryAfter) {
|
|
679
570
|
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
680
571
|
}
|
|
@@ -684,17 +575,23 @@ try {
|
|
|
684
575
|
}
|
|
685
576
|
|
|
686
577
|
// Handle conditional timeouts
|
|
687
|
-
await
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
578
|
+
await receive<{ taskType: string; data: any }>(
|
|
579
|
+
"work-queue",
|
|
580
|
+
"workers",
|
|
581
|
+
async (message) => {
|
|
582
|
+
if (!canProcessTaskType(message.taskType)) {
|
|
583
|
+
// Return timeout to retry later
|
|
584
|
+
return { timeoutSeconds: 60 };
|
|
585
|
+
}
|
|
692
586
|
|
|
693
|
-
|
|
694
|
-
}
|
|
587
|
+
await processTask(message.taskType, message.data);
|
|
588
|
+
},
|
|
589
|
+
);
|
|
695
590
|
|
|
696
591
|
// Process specific message metadata only (no payload download)
|
|
697
|
-
await
|
|
592
|
+
await receive<{ taskType: string; data: any }>(
|
|
593
|
+
"work-queue",
|
|
594
|
+
"workers",
|
|
698
595
|
async (_, metadata) => {
|
|
699
596
|
console.log(`Message ID: ${metadata.messageId}`);
|
|
700
597
|
console.log(`Delivery count: ${metadata.deliveryCount}`);
|
|
@@ -708,172 +605,69 @@ await worker.consume(
|
|
|
708
605
|
### Timing Out Messages
|
|
709
606
|
|
|
710
607
|
```typescript
|
|
711
|
-
const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
|
|
712
|
-
const worker = workTopic.consumerGroup("workers");
|
|
713
|
-
|
|
714
608
|
// Process a message with conditional timeout
|
|
715
609
|
try {
|
|
716
|
-
await
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
610
|
+
await receive<{ taskType: string; data: any }>(
|
|
611
|
+
"work-queue",
|
|
612
|
+
"workers",
|
|
613
|
+
async ({ taskType, data }) => {
|
|
614
|
+
// Check if we can process this task type right now
|
|
615
|
+
if (taskType === "heavy-computation" && isSystemOverloaded()) {
|
|
616
|
+
// Return timeout to retry later (5 minutes)
|
|
617
|
+
return { timeoutSeconds: 300 };
|
|
618
|
+
}
|
|
722
619
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
620
|
+
// Check if we have required resources
|
|
621
|
+
if (taskType === "external-api" && !isExternalServiceAvailable()) {
|
|
622
|
+
// Return timeout to retry in 1 minute
|
|
623
|
+
return { timeoutSeconds: 60 };
|
|
624
|
+
}
|
|
728
625
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
626
|
+
// Process the message normally
|
|
627
|
+
console.log(`Processing ${taskType} task`);
|
|
628
|
+
await processTask(taskType, data);
|
|
629
|
+
// Message will be automatically deleted on successful completion
|
|
630
|
+
},
|
|
631
|
+
);
|
|
734
632
|
} catch (error) {
|
|
735
633
|
console.error("Worker processing error:", error);
|
|
736
634
|
}
|
|
737
635
|
|
|
738
636
|
// Example with exponential backoff
|
|
739
637
|
try {
|
|
740
|
-
await
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
638
|
+
await receive<{ taskType: string; data: any }>(
|
|
639
|
+
"work-queue",
|
|
640
|
+
"workers",
|
|
641
|
+
async (message, { deliveryCount }) => {
|
|
642
|
+
const maxRetries = 3;
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
await processMessage(message);
|
|
646
|
+
// Successful processing - message will be deleted
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (deliveryCount < maxRetries) {
|
|
649
|
+
// Exponential backoff: 2^deliveryCount minutes
|
|
650
|
+
const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
|
|
651
|
+
console.log(
|
|
652
|
+
`Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
|
|
653
|
+
);
|
|
654
|
+
return { timeoutSeconds: timeoutSeconds };
|
|
655
|
+
} else {
|
|
656
|
+
// Max retries reached, let the message fail and be deleted
|
|
657
|
+
console.error(
|
|
658
|
+
"Max retries reached, message will be discarded:",
|
|
659
|
+
error,
|
|
660
|
+
);
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
758
663
|
}
|
|
759
|
-
}
|
|
760
|
-
|
|
664
|
+
},
|
|
665
|
+
);
|
|
761
666
|
} catch (error) {
|
|
762
667
|
console.error("Backoff processing error:", error);
|
|
763
668
|
}
|
|
764
669
|
```
|
|
765
670
|
|
|
766
|
-
### Complete Example: Video Processing Pipeline
|
|
767
|
-
|
|
768
|
-
Here's a comprehensive example showing a video processing pipeline that
|
|
769
|
-
processes videos with FFmpeg and stores the results in Vercel Blob:
|
|
770
|
-
|
|
771
|
-
```typescript
|
|
772
|
-
import { createTopic, StreamTransport } from "@vercel/queue";
|
|
773
|
-
import { spawn } from "child_process";
|
|
774
|
-
import ffmpeg from "ffmpeg-static";
|
|
775
|
-
import { put } from "@vercel/blob";
|
|
776
|
-
|
|
777
|
-
// Input topic with unoptimized videos
|
|
778
|
-
const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
779
|
-
"unoptimized-videos",
|
|
780
|
-
new StreamTransport(),
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
// Output topic for optimized videos
|
|
784
|
-
const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
785
|
-
"optimized-videos",
|
|
786
|
-
new StreamTransport(),
|
|
787
|
-
);
|
|
788
|
-
|
|
789
|
-
// Step 1: Process videos with FFmpeg
|
|
790
|
-
const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
|
|
791
|
-
|
|
792
|
-
try {
|
|
793
|
-
await videoProcessor.consume(async (inputVideoStream) => {
|
|
794
|
-
console.log("Processing video...");
|
|
795
|
-
|
|
796
|
-
if (!ffmpeg) {
|
|
797
|
-
throw new Error("FFmpeg not available");
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Create optimized video stream using FFmpeg
|
|
801
|
-
const optimizedStream = new ReadableStream<Uint8Array>({
|
|
802
|
-
start(controller) {
|
|
803
|
-
const ffmpegProcess = spawn(
|
|
804
|
-
ffmpeg,
|
|
805
|
-
[
|
|
806
|
-
"-i",
|
|
807
|
-
"pipe:0", // Input from stdin
|
|
808
|
-
"-c:v",
|
|
809
|
-
"libvpx-vp9", // Video codec
|
|
810
|
-
"-c:a",
|
|
811
|
-
"libopus", // Audio codec
|
|
812
|
-
"-crf",
|
|
813
|
-
"23", // Quality
|
|
814
|
-
"-f",
|
|
815
|
-
"webm", // Output format
|
|
816
|
-
"pipe:1", // Output to stdout
|
|
817
|
-
],
|
|
818
|
-
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
819
|
-
);
|
|
820
|
-
|
|
821
|
-
// Pipe input stream to FFmpeg
|
|
822
|
-
const reader = inputVideoStream.getReader();
|
|
823
|
-
const pipeInput = async () => {
|
|
824
|
-
while (true) {
|
|
825
|
-
const { done, value } = await reader.read();
|
|
826
|
-
if (done) {
|
|
827
|
-
ffmpegProcess.stdin?.end();
|
|
828
|
-
break;
|
|
829
|
-
}
|
|
830
|
-
ffmpegProcess.stdin?.write(value);
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
pipeInput();
|
|
834
|
-
|
|
835
|
-
// Stream FFmpeg output
|
|
836
|
-
ffmpegProcess.stdout?.on("data", (chunk) => {
|
|
837
|
-
controller.enqueue(new Uint8Array(chunk));
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
ffmpegProcess.on("close", (code) => {
|
|
841
|
-
if (code === 0) {
|
|
842
|
-
controller.close();
|
|
843
|
-
} else {
|
|
844
|
-
controller.error(new Error(`FFmpeg failed with code ${code}`));
|
|
845
|
-
}
|
|
846
|
-
});
|
|
847
|
-
},
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
// Publish optimized video to next topic
|
|
851
|
-
await optimizedVideosTopic.publish(optimizedStream);
|
|
852
|
-
console.log("Video optimized and published");
|
|
853
|
-
});
|
|
854
|
-
} catch (error) {
|
|
855
|
-
console.error("Video processing error:", error);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Step 2: Store optimized videos in Vercel Blob
|
|
859
|
-
const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
|
|
860
|
-
|
|
861
|
-
try {
|
|
862
|
-
await blobUploader.consume(async (optimizedVideo) => {
|
|
863
|
-
// Upload to Vercel Blob storage
|
|
864
|
-
const filename = `optimized-${Date.now()}.webm`;
|
|
865
|
-
const blob = await put(filename, optimizedVideo, {
|
|
866
|
-
access: "public",
|
|
867
|
-
contentType: "video/webm",
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
|
|
871
|
-
});
|
|
872
|
-
} catch (error) {
|
|
873
|
-
console.error("Blob upload error:", error);
|
|
874
|
-
}
|
|
875
|
-
```
|
|
876
|
-
|
|
877
671
|
## Error Handling
|
|
878
672
|
|
|
879
673
|
The queue client provides specific error types for different failure scenarios:
|
|
@@ -883,39 +677,25 @@ The queue client provides specific error types for different failure scenarios:
|
|
|
883
677
|
- **`QueueEmptyError`**: Thrown when attempting to receive messages from an
|
|
884
678
|
empty queue (204 status)
|
|
885
679
|
|
|
886
|
-
- Thrown by `
|
|
887
|
-
- Also thrown when directly using `client.receiveMessages()`
|
|
680
|
+
- Thrown by `receive()` when no messages are available
|
|
888
681
|
|
|
889
682
|
- **`MessageLockedError`**: Thrown when a message is temporarily locked (423
|
|
890
683
|
status)
|
|
891
684
|
|
|
892
685
|
- Contains optional `retryAfter` property with seconds to wait before retry
|
|
893
|
-
- For `
|
|
894
|
-
- For `
|
|
686
|
+
- For `receive()` without options: the next message is locked
|
|
687
|
+
- For `receive()` with messageId: the requested message is locked
|
|
895
688
|
|
|
896
689
|
- **`MessageNotFoundError`**: Message doesn't exist (404 status)
|
|
897
690
|
|
|
898
691
|
- **`MessageNotAvailableError`**: Message exists but isn't available for
|
|
899
692
|
processing (409 status)
|
|
900
693
|
|
|
901
|
-
- **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status
|
|
902
|
-
with nextMessageId)
|
|
903
|
-
|
|
904
|
-
- Contains `nextMessageId` property indicating which message to process first
|
|
905
|
-
|
|
906
|
-
- **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424
|
|
907
|
-
status)
|
|
908
|
-
|
|
909
|
-
- Contains `nextMessageId` property indicating which message must be processed
|
|
910
|
-
first
|
|
911
|
-
- Similar to `FifoOrderingViolationError` but specifically for receive-by-ID
|
|
912
|
-
operations
|
|
913
|
-
|
|
914
694
|
- **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
|
|
915
695
|
|
|
916
696
|
- **`BadRequestError`**: Invalid request parameters (400 status)
|
|
917
697
|
|
|
918
|
-
- Invalid queue names,
|
|
698
|
+
- Invalid queue names, missing required parameters
|
|
919
699
|
|
|
920
700
|
- **`UnauthorizedError`**: Authentication failure (401 status)
|
|
921
701
|
|
|
@@ -933,8 +713,6 @@ The queue client provides specific error types for different failure scenarios:
|
|
|
933
713
|
```typescript
|
|
934
714
|
import {
|
|
935
715
|
BadRequestError,
|
|
936
|
-
FailedDependencyError,
|
|
937
|
-
FifoOrderingViolationError,
|
|
938
716
|
ForbiddenError,
|
|
939
717
|
InternalServerError,
|
|
940
718
|
MessageLockedError,
|
|
@@ -944,14 +722,15 @@ import {
|
|
|
944
722
|
|
|
945
723
|
// Handle empty queue or locked messages
|
|
946
724
|
try {
|
|
947
|
-
|
|
948
|
-
// Process
|
|
949
|
-
|
|
725
|
+
await receive("my-topic", "my-consumer", async (message) => {
|
|
726
|
+
// Process message
|
|
727
|
+
console.log("Processing message:", message);
|
|
728
|
+
});
|
|
950
729
|
} catch (error) {
|
|
951
730
|
if (error instanceof QueueEmptyError) {
|
|
952
731
|
console.log("Queue is empty, retry later");
|
|
953
732
|
} else if (error instanceof MessageLockedError) {
|
|
954
|
-
console.log("Next message
|
|
733
|
+
console.log("Next message is locked");
|
|
955
734
|
if (error.retryAfter) {
|
|
956
735
|
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
957
736
|
}
|
|
@@ -960,7 +739,7 @@ try {
|
|
|
960
739
|
|
|
961
740
|
// Handle locked message with retry
|
|
962
741
|
try {
|
|
963
|
-
await consumer
|
|
742
|
+
await receive("my-topic", "my-consumer", handler, { messageId });
|
|
964
743
|
} catch (error) {
|
|
965
744
|
if (error instanceof MessageLockedError) {
|
|
966
745
|
console.log("Message is locked by another consumer");
|
|
@@ -968,15 +747,12 @@ try {
|
|
|
968
747
|
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
969
748
|
setTimeout(() => retry(), error.retryAfter * 1000);
|
|
970
749
|
}
|
|
971
|
-
} else if (error instanceof FailedDependencyError) {
|
|
972
|
-
// FIFO ordering violation for receive by ID
|
|
973
|
-
console.log(`Must process ${error.nextMessageId} first`);
|
|
974
750
|
}
|
|
975
751
|
}
|
|
976
752
|
|
|
977
753
|
// Handle authentication and authorization errors
|
|
978
754
|
try {
|
|
979
|
-
await topic
|
|
755
|
+
await send("my-topic", payload);
|
|
980
756
|
} catch (error) {
|
|
981
757
|
if (error instanceof UnauthorizedError) {
|
|
982
758
|
console.log("Invalid token - refresh authentication");
|