@vercel/queue 0.0.0-alpha.3 → 0.0.0-alpha.30
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 +288 -800
- package/bin/local-discover.js +196 -0
- package/dist/index.d.mts +101 -717
- package/dist/index.d.ts +101 -717
- package/dist/index.js +490 -603
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +486 -593
- package/dist/index.mjs.map +1 -1
- package/dist/pages.d.mts +47 -0
- package/dist/pages.d.ts +47 -0
- package/dist/pages.js +1250 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +1223 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/types-JvOenjfT.d.mts +256 -0
- package/dist/types-JvOenjfT.d.ts +256 -0
- package/package.json +23 -6
package/README.md
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
# Vercel Queues
|
|
2
2
|
|
|
3
|
-
A TypeScript client library for interacting with the Vercel Queue Service API
|
|
4
|
-
with customizable serialization/deserialization (transport) support, including
|
|
5
|
-
**streaming support** for memory-efficient processing of large payloads.
|
|
3
|
+
A TypeScript client library for interacting with the Vercel Queue Service API, designed for seamless integration with Vercel deployments.
|
|
6
4
|
|
|
7
5
|
## Features
|
|
8
6
|
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
or create your own
|
|
13
|
-
- **Streaming Support**: Handle large payloads without loading them entirely
|
|
14
|
-
into memory
|
|
7
|
+
- **Automatic Queue Triggering**: Vercel automatically triggers your API routes when messages are ready
|
|
8
|
+
- **Next.js Integration**: Built-in support for Next.js API routes and Server Actions
|
|
9
|
+
- **Generic Payload Support**: Send and receive any type of data with type safety
|
|
15
10
|
- **Pub/Sub Pattern**: Topic-based messaging with consumer groups
|
|
16
11
|
- **Type Safety**: Full TypeScript support with generic types
|
|
17
|
-
- **
|
|
12
|
+
- **Streaming Support**: Handle large payloads efficiently
|
|
13
|
+
- **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
|
|
18
14
|
|
|
19
15
|
## Installation
|
|
20
16
|
|
|
@@ -22,930 +18,422 @@ with customizable serialization/deserialization (transport) support, including
|
|
|
22
18
|
npm install @vercel/queue
|
|
23
19
|
```
|
|
24
20
|
|
|
21
|
+
The package includes:
|
|
22
|
+
|
|
23
|
+
- **Main Library**: Queue client and utilities for production and development
|
|
24
|
+
- **CLI Tool**: `npx vercel-queue-local-init` for local development handler initialization
|
|
25
|
+
|
|
25
26
|
## Quick Start
|
|
26
27
|
|
|
27
|
-
For local development, you'll need to
|
|
28
|
-
(including the OIDC token):
|
|
28
|
+
For local development, you'll need to set up your Vercel project:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
# Install Vercel CLI if you haven't already
|
|
32
32
|
npm i -g vercel
|
|
33
33
|
|
|
34
|
+
# Link your project to Vercel
|
|
35
|
+
vc link
|
|
36
|
+
|
|
34
37
|
# Pull environment variables from your Vercel project
|
|
35
38
|
vc env pull
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
## Local Development
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
// index.ts
|
|
42
|
-
import { createTopic, JsonTransport, QueueClient } from "@vercel/queue";
|
|
43
|
+
**Queues just work locally.** After you have setup your Vercel project, when you `send()` messages in development mode, they automatically trigger your handlers locally - no external queue infrastructure needed.
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
const client = await QueueClient.fromVercelFunction();
|
|
45
|
+
### Next.js Lazy Loading
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
const topic = createTopic<{ message: string; timestamp: number }>(
|
|
49
|
-
client,
|
|
50
|
-
"my-topic",
|
|
51
|
-
);
|
|
47
|
+
For Next.js API routes (or others that are lazy-loaded), run this simple command to initialize handlers:
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
timestamp: Date.now(),
|
|
57
|
-
});
|
|
49
|
+
```bash
|
|
50
|
+
npx vercel-queue-local-init
|
|
51
|
+
```
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
const consumer = topic.consumerGroup("my-processors");
|
|
53
|
+
That's it! The script reads your `vercel.json`, finds your queue handlers, and triggers Next.js to load them.
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
const controller = new AbortController();
|
|
55
|
+
### Example Workflow
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
console.log("Received:", message.payload.message);
|
|
69
|
-
console.log("Timestamp:", new Date(message.payload.timestamp));
|
|
70
|
-
});
|
|
71
|
-
} catch (error) {
|
|
72
|
-
console.error("Processing stopped due to error:", error);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Stop processing from elsewhere in your code
|
|
76
|
-
// controller.abort();
|
|
77
|
-
```
|
|
57
|
+
```bash
|
|
58
|
+
# Start your dev server
|
|
59
|
+
npm run dev
|
|
78
60
|
|
|
79
|
-
|
|
61
|
+
# Initialize handlers (only needed for frameworks that lazy load routes in dev)
|
|
62
|
+
npx vercel-queue-local-init
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
# Using dotenv to load the OIDC token
|
|
83
|
-
dotenv -e .env.local node index.ts
|
|
64
|
+
# Send messages - they process locally automatically!
|
|
84
65
|
```
|
|
85
66
|
|
|
86
|
-
|
|
67
|
+
### CLI Options
|
|
87
68
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
69
|
+
```bash
|
|
70
|
+
# Custom port
|
|
71
|
+
npx vercel-queue-local-init --port 3001
|
|
91
72
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
73
|
+
# Different config file
|
|
74
|
+
npx vercel-queue-local-init --config ./my-vercel.json
|
|
75
|
+
```
|
|
95
76
|
|
|
96
77
|
### TypeScript Configuration
|
|
97
78
|
|
|
98
|
-
Update your `tsconfig.json` to use `"bundler"` module resolution for proper
|
|
99
|
-
package export resolution:
|
|
79
|
+
Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
|
|
100
80
|
|
|
101
81
|
```json
|
|
102
82
|
{
|
|
103
83
|
"compilerOptions": {
|
|
104
84
|
"moduleResolution": "bundler"
|
|
105
|
-
// ... other options
|
|
106
85
|
}
|
|
107
86
|
}
|
|
108
87
|
```
|
|
109
88
|
|
|
110
|
-
### Publishing
|
|
89
|
+
### Publishing Messages
|
|
111
90
|
|
|
112
|
-
|
|
91
|
+
The `send` function can be used anywhere in your codebase to publish messages to a queue:
|
|
113
92
|
|
|
114
93
|
```typescript
|
|
115
|
-
|
|
116
|
-
"use server";
|
|
117
|
-
|
|
118
|
-
import { createTopic, QueueClient } from "@vercel/queue";
|
|
119
|
-
|
|
120
|
-
export async function publishTestMessage(message: string) {
|
|
121
|
-
// Initialize a queue client
|
|
122
|
-
const client = await QueueClient.fromVercelFunction();
|
|
123
|
-
|
|
124
|
-
// Create a topic with JSON serialization (default)
|
|
125
|
-
const topic = createTopic<{ message: string; timestamp: number }>(
|
|
126
|
-
client,
|
|
127
|
-
"my-topic",
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
// Publish the message
|
|
131
|
-
const { messageId } = await topic.publish(
|
|
132
|
-
{ message, timestamp: Date.now() },
|
|
133
|
-
{
|
|
134
|
-
// Provide a callback URL to invoke a consumer when the message is ready to be processed
|
|
135
|
-
callback: {
|
|
136
|
-
url: process.env.VERCEL_PROJECT_PRODUCTION_URL
|
|
137
|
-
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}/api/queue/handle`
|
|
138
|
-
: "http://localhost:3000/api/queue/handle",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
console.log(`Published message ${messageId}`);
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Now wire up the server function to your app
|
|
148
|
-
|
|
149
|
-
```jsx
|
|
150
|
-
// app/some/page.tsx
|
|
151
|
-
"use client";
|
|
152
|
-
import { publishTestMessage } from "./actions";
|
|
153
|
-
|
|
154
|
-
export default function Button() {
|
|
155
|
-
return (
|
|
156
|
-
// ...
|
|
157
|
-
<Button onClick={() => publishTestMessage("Hello world")} >
|
|
158
|
-
Publish Test Message
|
|
159
|
-
</a>
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
```
|
|
94
|
+
import { send } from "@vercel/queue";
|
|
163
95
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
callback functionality of Vercel queues to consume messages on the fly, when a
|
|
168
|
-
message is ready to be processed.
|
|
169
|
-
|
|
170
|
-
The `handleCallback` helper function simplifies queue callback handling in NextJS:
|
|
171
|
-
|
|
172
|
-
```typescript
|
|
173
|
-
// app/api/queue/handle/route.ts
|
|
174
|
-
import { handleCallback } from "@vercel/queue";
|
|
175
|
-
|
|
176
|
-
export const POST = handleCallback({
|
|
177
|
-
// Handle messages sent on the "new-users" topic (the consumer
|
|
178
|
-
// group "default" will be used)
|
|
179
|
-
"my-topic": (message, metadata) => {
|
|
180
|
-
console.log(`Received message:`, message, metadata);
|
|
181
|
-
// metadata: { messageId, deliveryCount, timestamp }
|
|
182
|
-
},
|
|
96
|
+
// Send a message to a topic
|
|
97
|
+
await send("my-topic", {
|
|
98
|
+
message: "Hello world",
|
|
183
99
|
});
|
|
184
100
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
resize: (message, metadata) => {
|
|
195
|
-
console.log("Resizing image:", message);
|
|
196
|
-
},
|
|
197
|
-
// consumer group: "watermark"
|
|
198
|
-
watermark: (message, metadata) => {
|
|
199
|
-
console.log("Adding watermark:", message);
|
|
200
|
-
},
|
|
101
|
+
// With additional options
|
|
102
|
+
await send(
|
|
103
|
+
"my-topic",
|
|
104
|
+
{
|
|
105
|
+
message: "Hello world",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
idempotencyKey: "unique-key", // Optional: prevent duplicate messages
|
|
109
|
+
retentionSeconds: 3600, // Optional: override retention time (defaults to 24 hours)
|
|
201
110
|
},
|
|
202
|
-
});
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## Key Features
|
|
206
|
-
|
|
207
|
-
### Streaming Support
|
|
208
|
-
|
|
209
|
-
Handle large files and data streams without loading them into memory:
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
import { StreamTransport } from "@vercel/queue";
|
|
213
|
-
|
|
214
|
-
const videoTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
215
|
-
client,
|
|
216
|
-
"video-processing",
|
|
217
|
-
new StreamTransport(),
|
|
218
111
|
);
|
|
219
|
-
|
|
220
|
-
// Process large video files efficiently
|
|
221
|
-
const processor = videoTopic.consumerGroup("processors");
|
|
222
|
-
await processor.subscribe(signal, async (message) => {
|
|
223
|
-
const videoStream = message.payload;
|
|
224
|
-
// Process stream chunk by chunk
|
|
225
|
-
const reader = videoStream.getReader();
|
|
226
|
-
while (true) {
|
|
227
|
-
const { done, value } = await reader.read();
|
|
228
|
-
if (done) break;
|
|
229
|
-
await processChunk(value);
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
112
|
```
|
|
233
113
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
Multiple consumers can process messages from the same topic in parallel:
|
|
114
|
+
Example usage in an API route:
|
|
237
115
|
|
|
238
116
|
```typescript
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
const worker2 = topic.consumerGroup("workers"); // Same group name
|
|
242
|
-
// worker1 and worker2 will receive different messages (load balancing)
|
|
243
|
-
|
|
244
|
-
// Different consumer groups - each gets copies of ALL messages
|
|
245
|
-
const analytics = topic.consumerGroup("analytics");
|
|
246
|
-
const webhooks = topic.consumerGroup("webhooks");
|
|
247
|
-
// analytics and webhooks will both receive every message
|
|
248
|
-
```
|
|
117
|
+
// app/api/send-message/route.ts
|
|
118
|
+
import { send } from "@vercel/queue";
|
|
249
119
|
|
|
250
|
-
|
|
120
|
+
export async function POST(request: Request) {
|
|
121
|
+
const body = await request.json();
|
|
251
122
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
- `subscribe()`: Continuously process messages with automatic polling
|
|
256
|
-
- `receiveMessage()`: Process a specific message by ID
|
|
257
|
-
- `receiveNextMessage()`: Process the next available message (one-shot)
|
|
258
|
-
- `handleMessage()`: Process message metadata only (without payload)
|
|
259
|
-
- **Transports**: Pluggable serialization/deserialization for different data
|
|
260
|
-
types
|
|
261
|
-
- **Streaming**: Memory-efficient processing of large payloads
|
|
262
|
-
- **Visibility Timeouts**: Automatic message lifecycle management
|
|
263
|
-
|
|
264
|
-
## Performance
|
|
265
|
-
|
|
266
|
-
The multipart parser is optimized for high-throughput scenarios:
|
|
267
|
-
|
|
268
|
-
- **Streaming**: Messages are yielded immediately as headers are parsed
|
|
269
|
-
- **Memory Efficient**: No buffering of complete payloads
|
|
270
|
-
- **Fast Parsing**: Native Buffer operations for ~50% performance improvement
|
|
271
|
-
- **Scalable**: Can handle arbitrarily large responses without memory
|
|
272
|
-
constraints
|
|
273
|
-
|
|
274
|
-
## Serialization (Transport) System
|
|
275
|
-
|
|
276
|
-
The queue client supports customizable serialization through the `Transport`
|
|
277
|
-
interface with **streaming support** for memory-efficient processing. Transport
|
|
278
|
-
can be configured at the **topic level** when creating a topic, or at the
|
|
279
|
-
**consumer group level** when creating a consumer group.
|
|
280
|
-
|
|
281
|
-
### Built-in Transports
|
|
282
|
-
|
|
283
|
-
#### JsonTransport (Default)
|
|
284
|
-
|
|
285
|
-
Buffers data for JSON parsing - suitable for structured data that fits in
|
|
286
|
-
memory.
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
289
|
-
import { createTopic, JsonTransport } from "@vercel/queue";
|
|
123
|
+
const { messageId } = await send("my-topic", {
|
|
124
|
+
message: body.message,
|
|
125
|
+
});
|
|
290
126
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"json-topic",
|
|
294
|
-
new JsonTransport(),
|
|
295
|
-
);
|
|
296
|
-
// or simply (JsonTransport is the default):
|
|
297
|
-
const topic = createTopic<{ data: any }>(client, "json-topic");
|
|
127
|
+
return Response.json({ messageId });
|
|
128
|
+
}
|
|
298
129
|
```
|
|
299
130
|
|
|
300
|
-
|
|
131
|
+
### Consuming Messages
|
|
301
132
|
|
|
302
|
-
|
|
303
|
-
that fits in memory.
|
|
133
|
+
Messages are consumed using API routes that Vercel automatically triggers when messages are available.
|
|
304
134
|
|
|
305
|
-
|
|
306
|
-
import { BufferTransport, createTopic } from "@vercel/queue";
|
|
307
|
-
|
|
308
|
-
const topic = createTopic<Buffer>(
|
|
309
|
-
client,
|
|
310
|
-
"binary-topic",
|
|
311
|
-
new BufferTransport(),
|
|
312
|
-
);
|
|
313
|
-
const binaryData = Buffer.from("Binary data", "utf8");
|
|
314
|
-
await topic.publish(binaryData);
|
|
315
|
-
```
|
|
135
|
+
#### 1. Create API Routes
|
|
316
136
|
|
|
317
|
-
|
|
137
|
+
##### App Router (Recommended)
|
|
318
138
|
|
|
319
|
-
|
|
320
|
-
Ideal for large files and memory-efficient processing.
|
|
139
|
+
The recommended approach is to handle multiple topics and consumers in a single API route to keep your `vercel.json` configuration simple:
|
|
321
140
|
|
|
322
141
|
```typescript
|
|
323
|
-
|
|
142
|
+
// app/api/queue/route.ts
|
|
143
|
+
import { handleCallback } from "@vercel/queue";
|
|
324
144
|
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
"
|
|
328
|
-
|
|
329
|
-
|
|
145
|
+
export const POST = handleCallback({
|
|
146
|
+
// Single topic with one consumer
|
|
147
|
+
"my-topic": {
|
|
148
|
+
"my-consumer": async (message, metadata) => {
|
|
149
|
+
// metadata includes: { messageId, deliveryCount, createdAt }
|
|
150
|
+
console.log("Processing message:", message);
|
|
330
151
|
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
// Read file in chunks
|
|
335
|
-
for (const chunk of readFileInChunks("large-file.bin")) {
|
|
336
|
-
controller.enqueue(chunk);
|
|
337
|
-
}
|
|
338
|
-
controller.close();
|
|
152
|
+
// If this throws an error, the message will be automatically retried
|
|
153
|
+
await processMessage(message);
|
|
154
|
+
},
|
|
339
155
|
},
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
await topic.publish(fileStream);
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Custom Transport
|
|
346
|
-
|
|
347
|
-
You can create your own serialization format by implementing the `Transport`
|
|
348
|
-
interface:
|
|
349
|
-
|
|
350
|
-
```typescript
|
|
351
|
-
import { Transport } from "@vercel/queue";
|
|
352
|
-
|
|
353
|
-
interface Transport<T = unknown> {
|
|
354
|
-
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
355
|
-
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
356
|
-
contentType: string;
|
|
357
|
-
}
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### Choosing the Right Transport
|
|
361
|
-
|
|
362
|
-
| Use Case | Recommended Transport | Memory Usage | Performance |
|
|
363
|
-
| ---------------------- | --------------------- | ------------ | ----------- |
|
|
364
|
-
| Small JSON objects | `JsonTransport` | Low | High |
|
|
365
|
-
| Binary files < 100MB | `BufferTransport` | Medium | High |
|
|
366
|
-
| Large files > 100MB | `StreamTransport` | Very Low | Medium |
|
|
367
|
-
| Real-time data streams | `StreamTransport` | Very Low | High |
|
|
368
|
-
| Custom protocols | Custom implementation | Varies | Varies |
|
|
369
|
-
|
|
370
|
-
## API Reference
|
|
371
156
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
### Topic
|
|
382
|
-
|
|
383
|
-
```typescript
|
|
384
|
-
const topic = createTopic<T>(client, topicName, transport?);
|
|
385
|
-
|
|
386
|
-
// Publish a message (uses topic's transport)
|
|
387
|
-
await topic.publish(payload, options?);
|
|
388
|
-
|
|
389
|
-
// Trigger a callback URL when the message is
|
|
390
|
-
// ready for consumption
|
|
391
|
-
await topic.publish(payload, {
|
|
392
|
-
callback: { url: "https://example.com/webhook" }
|
|
393
|
-
});
|
|
157
|
+
// Multiple consumers for different purposes
|
|
158
|
+
"order-events": {
|
|
159
|
+
fulfillment: async (order, metadata) => {
|
|
160
|
+
// By default, errors will trigger automatic retries
|
|
161
|
+
// But you can control retry timing if needed:
|
|
162
|
+
if (!isSystemReady()) {
|
|
163
|
+
// Override default retry with a 5 minute delay
|
|
164
|
+
return { timeoutSeconds: 300 };
|
|
165
|
+
}
|
|
394
166
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
167
|
+
await processOrder(order);
|
|
168
|
+
},
|
|
169
|
+
analytics: async (order, metadata) => {
|
|
170
|
+
try {
|
|
171
|
+
await trackOrder(order);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Optional: Custom exponential backoff instead of default retry timing
|
|
174
|
+
const timeoutSeconds = Math.pow(2, metadata.deliveryCount) * 60;
|
|
175
|
+
return { timeoutSeconds };
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
},
|
|
402
179
|
});
|
|
403
|
-
|
|
404
|
-
// Create a consumer group (can override transport)
|
|
405
|
-
const consumer = topic.consumerGroup<U>(groupName, options?);
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
### ConsumerGroup
|
|
409
|
-
|
|
410
|
-
```typescript
|
|
411
|
-
// Start continuous processing (blocks until signal is aborted or error occurs)
|
|
412
|
-
await consumer.subscribe(signal, handler, options?);
|
|
413
|
-
|
|
414
|
-
// Process a specific message by ID
|
|
415
|
-
await consumer.receiveMessage(messageId, handler);
|
|
416
|
-
|
|
417
|
-
// Process the next available message
|
|
418
|
-
await consumer.receiveNextMessage(handler);
|
|
419
|
-
|
|
420
|
-
// Handle a specific message by ID without payload
|
|
421
|
-
await consumer.handleMessage(messageId, handler);
|
|
422
180
|
```
|
|
423
181
|
|
|
424
|
-
|
|
182
|
+
While you can split handlers into separate routes if needed (e.g., for code organization or deployment flexibility), consolidating them in one route is recommended for simpler configuration.
|
|
425
183
|
|
|
426
|
-
|
|
427
|
-
// Handler function signature
|
|
428
|
-
type MessageHandler<T> = (
|
|
429
|
-
message: Message<T>,
|
|
430
|
-
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
184
|
+
##### Pages Router
|
|
431
185
|
|
|
432
|
-
|
|
433
|
-
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
434
|
-
|
|
435
|
-
interface MessageTimeoutResult {
|
|
436
|
-
timeoutSeconds: number; // seconds before message becomes available again
|
|
437
|
-
}
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
### Transport Interface
|
|
186
|
+
For Next.js Pages Router, import from `@vercel/queue/pages` to get a handler compatible with the Pages Router API (`NextApiRequest`/`NextApiResponse`):
|
|
441
187
|
|
|
442
188
|
```typescript
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
446
|
-
contentType: string;
|
|
447
|
-
}
|
|
448
|
-
```
|
|
189
|
+
// pages/api/queue.ts
|
|
190
|
+
import { handleCallback } from "@vercel/queue/pages";
|
|
449
191
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
payload: T,
|
|
468
|
-
metadata: MessageMetadata
|
|
469
|
-
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
470
|
-
|
|
471
|
-
// Message metadata provided to handlers
|
|
472
|
-
interface MessageMetadata {
|
|
473
|
-
messageId: string;
|
|
474
|
-
deliveryCount: number;
|
|
475
|
-
timestamp: string;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Configuration object with handlers for different topics
|
|
479
|
-
type CallbackHandlers = {
|
|
480
|
-
[topicName: string]:
|
|
481
|
-
| Handler // Single handler (uses 'default' consumer group)
|
|
482
|
-
| { [consumerGroup: string]: Handler }; // Multiple consumer group handlers
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
// Example usage:
|
|
486
|
-
export const POST = handleCallback({
|
|
487
|
-
// Topic handler (uses 'default' consumer group)
|
|
488
|
-
"new-users": (message, metadata) => {
|
|
489
|
-
console.log(`New user event:`, message, metadata);
|
|
192
|
+
export default handleCallback({
|
|
193
|
+
"my-topic": {
|
|
194
|
+
"my-consumer": async (message, metadata) => {
|
|
195
|
+
console.log("Processing message:", message);
|
|
196
|
+
await processMessage(message);
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
"order-events": {
|
|
200
|
+
fulfillment: async (order, metadata) => {
|
|
201
|
+
if (!isSystemReady()) {
|
|
202
|
+
return { timeoutSeconds: 300 };
|
|
203
|
+
}
|
|
204
|
+
await processOrder(order);
|
|
205
|
+
},
|
|
206
|
+
analytics: async (order, metadata) => {
|
|
207
|
+
await trackOrder(order);
|
|
208
|
+
},
|
|
490
209
|
},
|
|
491
|
-
|
|
492
|
-
// Consumer group specific handlers
|
|
493
|
-
"image-processing": {
|
|
494
|
-
"compress": (message, metadata) => console.log("Compressing image", message),
|
|
495
|
-
"resize": (message, metadata) => console.log("Resizing image", message),
|
|
496
|
-
}
|
|
497
210
|
});
|
|
498
|
-
|
|
499
|
-
// Error thrown for invalid callback requests
|
|
500
|
-
class InvalidCallbackError extends Error;
|
|
501
211
|
```
|
|
502
212
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
### Basic JSON Processing
|
|
506
|
-
|
|
507
|
-
```typescript
|
|
508
|
-
interface UserEvent {
|
|
509
|
-
userId: string;
|
|
510
|
-
action: string;
|
|
511
|
-
timestamp: number;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const userTopic = createTopic<UserEvent>(client, "user-events");
|
|
213
|
+
The `/pages` subpath export automatically adapts the handler to work with the Pages Router API.
|
|
515
214
|
|
|
516
|
-
|
|
517
|
-
userId: "123",
|
|
518
|
-
action: "login",
|
|
519
|
-
timestamp: Date.now(),
|
|
520
|
-
});
|
|
215
|
+
#### 2. Configure vercel.json
|
|
521
216
|
|
|
522
|
-
|
|
523
|
-
const controller = new AbortController();
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
await consumer.subscribe(controller.signal, async (message) => {
|
|
527
|
-
console.log(
|
|
528
|
-
`User ${message.payload.userId} performed ${message.payload.action}`,
|
|
529
|
-
);
|
|
530
|
-
});
|
|
531
|
-
} catch (error) {
|
|
532
|
-
console.error("Processing error:", error);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Stop processing when needed
|
|
536
|
-
// controller.abort();
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
### Processing Specific Messages by ID
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
const userTopic = createTopic<{ userId: string; action: string }>(
|
|
543
|
-
client,
|
|
544
|
-
"user-events",
|
|
545
|
-
);
|
|
546
|
-
const consumer = userTopic.consumerGroup("processors");
|
|
217
|
+
Configure which topics and consumers your API route handles.
|
|
547
218
|
|
|
548
|
-
|
|
549
|
-
const messageId = "01234567-89ab-cdef-0123-456789abcdef";
|
|
219
|
+
**For App Router:**
|
|
550
220
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"functions": {
|
|
224
|
+
"app/api/queue/route.ts": {
|
|
225
|
+
"experimentalTriggers": [
|
|
226
|
+
{
|
|
227
|
+
"type": "queue/v1beta",
|
|
228
|
+
"topic": "my-topic",
|
|
229
|
+
"consumer": "my-consumer",
|
|
230
|
+
"retryAfterSeconds": 60,
|
|
231
|
+
"initialDelaySeconds": 0
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"type": "queue/v1beta",
|
|
235
|
+
"topic": "order-events",
|
|
236
|
+
"consumer": "fulfillment"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"type": "queue/v1beta",
|
|
240
|
+
"topic": "order-events",
|
|
241
|
+
"consumer": "analytics",
|
|
242
|
+
"retryAfterSeconds": 300
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
566
246
|
}
|
|
567
247
|
}
|
|
568
248
|
```
|
|
569
249
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
```typescript
|
|
573
|
-
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
574
|
-
client,
|
|
575
|
-
"work-queue",
|
|
576
|
-
);
|
|
577
|
-
const worker = workTopic.consumerGroup("workers");
|
|
250
|
+
**For Pages Router:**
|
|
578
251
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"functions": {
|
|
255
|
+
"pages/api/queue.ts": {
|
|
256
|
+
"experimentalTriggers": [
|
|
257
|
+
{
|
|
258
|
+
"type": "queue/v1beta",
|
|
259
|
+
"topic": "my-topic",
|
|
260
|
+
"consumer": "my-consumer",
|
|
261
|
+
"retryAfterSeconds": 60,
|
|
262
|
+
"initialDelaySeconds": 0
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"type": "queue/v1beta",
|
|
266
|
+
"topic": "order-events",
|
|
267
|
+
"consumer": "fulfillment"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"type": "queue/v1beta",
|
|
271
|
+
"topic": "order-events",
|
|
272
|
+
"consumer": "analytics",
|
|
273
|
+
"retryAfterSeconds": 300
|
|
274
|
+
}
|
|
275
|
+
]
|
|
593
276
|
}
|
|
594
|
-
} else {
|
|
595
|
-
console.error("Error processing message:", error);
|
|
596
277
|
}
|
|
597
278
|
}
|
|
598
|
-
|
|
599
|
-
// You can also use it with timeout results
|
|
600
|
-
await worker.receiveNextMessage(async (message) => {
|
|
601
|
-
if (!canProcessTaskType(message.payload.taskType)) {
|
|
602
|
-
// Return timeout to retry later
|
|
603
|
-
return { timeoutSeconds: 60 };
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
await processTask(message.payload.taskType, message.payload.data);
|
|
607
|
-
});
|
|
608
279
|
```
|
|
609
280
|
|
|
610
|
-
###
|
|
281
|
+
### Key Concepts
|
|
611
282
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
283
|
+
- **Topics**: Named message channels that can have multiple consumer groups
|
|
284
|
+
- **Consumer Groups**: Named groups of consumers that process messages in parallel
|
|
285
|
+
- Different consumer groups for the same topic each get a copy of every message
|
|
286
|
+
- Multiple consumers in the same group share/split messages for load balancing
|
|
287
|
+
- **Automatic Triggering**: Vercel triggers your API routes when messages are available
|
|
288
|
+
- **Message Processing**: Your API routes receive message metadata via headers
|
|
289
|
+
- **Configuration**: The `vercel.json` file tells Vercel which routes handle which topics/consumers
|
|
619
290
|
|
|
620
|
-
|
|
621
|
-
await worker.subscribe(controller.signal, async (message) => {
|
|
622
|
-
const { taskType, data } = message.payload;
|
|
291
|
+
## Advanced Features
|
|
623
292
|
|
|
624
|
-
|
|
625
|
-
if (taskType === "heavy-computation" && isSystemOverloaded()) {
|
|
626
|
-
// Return timeout to retry later (5 minutes)
|
|
627
|
-
return { timeoutSeconds: 300 };
|
|
628
|
-
}
|
|
293
|
+
### Serialization (Transport) System
|
|
629
294
|
|
|
630
|
-
|
|
631
|
-
if (taskType === "external-api" && !isExternalServiceAvailable()) {
|
|
632
|
-
// Return timeout to retry in 1 minute
|
|
633
|
-
return { timeoutSeconds: 60 };
|
|
634
|
-
}
|
|
295
|
+
The queue client supports customizable serialization through the `Transport` interface:
|
|
635
296
|
|
|
636
|
-
|
|
637
|
-
console.log(`Processing ${taskType} task`);
|
|
638
|
-
await processTask(taskType, data);
|
|
639
|
-
// Message will be automatically deleted on successful completion
|
|
640
|
-
});
|
|
641
|
-
} catch (error) {
|
|
642
|
-
console.error("Worker processing error:", error);
|
|
643
|
-
}
|
|
297
|
+
#### Built-in Transports
|
|
644
298
|
|
|
645
|
-
|
|
646
|
-
|
|
299
|
+
1. **JsonTransport (Default)**: For structured data that fits in memory
|
|
300
|
+
2. **BufferTransport**: For binary data that fits in memory
|
|
301
|
+
3. **StreamTransport**: For large files and memory-efficient processing
|
|
647
302
|
|
|
648
|
-
|
|
649
|
-
await worker.subscribe(backoffController.signal, async (message) => {
|
|
650
|
-
const maxRetries = 3;
|
|
651
|
-
const deliveryCount = message.deliveryCount;
|
|
652
|
-
|
|
653
|
-
try {
|
|
654
|
-
await processMessage(message.payload);
|
|
655
|
-
// Successful processing - message will be deleted
|
|
656
|
-
} catch (error) {
|
|
657
|
-
if (deliveryCount < maxRetries) {
|
|
658
|
-
// Exponential backoff: 2^deliveryCount minutes
|
|
659
|
-
const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
|
|
660
|
-
console.log(
|
|
661
|
-
`Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
|
|
662
|
-
);
|
|
663
|
-
return { timeoutSeconds: timeoutSeconds };
|
|
664
|
-
} else {
|
|
665
|
-
// Max retries reached, let the message fail and be deleted
|
|
666
|
-
console.error("Max retries reached, message will be discarded:", error);
|
|
667
|
-
throw error;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
} catch (error) {
|
|
672
|
-
console.error("Backoff processing error:", error);
|
|
673
|
-
}
|
|
674
|
-
```
|
|
675
|
-
|
|
676
|
-
### Complete Example: Video Processing Pipeline
|
|
677
|
-
|
|
678
|
-
Here's a comprehensive example showing a video processing pipeline that
|
|
679
|
-
processes videos with FFmpeg and stores the results in Vercel Blob:
|
|
303
|
+
Example:
|
|
680
304
|
|
|
681
305
|
```typescript
|
|
682
|
-
import {
|
|
683
|
-
import { spawn } from "child_process";
|
|
684
|
-
import ffmpeg from "ffmpeg-static";
|
|
685
|
-
import { put } from "@vercel/blob";
|
|
306
|
+
import { send, JsonTransport } from "@vercel/queue";
|
|
686
307
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
// Input topic with unoptimized videos
|
|
692
|
-
const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
693
|
-
client,
|
|
694
|
-
"unoptimized-videos",
|
|
695
|
-
new StreamTransport(),
|
|
696
|
-
);
|
|
308
|
+
// JsonTransport is the default
|
|
309
|
+
await send("json-topic", { data: "example" });
|
|
697
310
|
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
"
|
|
702
|
-
new
|
|
311
|
+
// Explicit transport configuration
|
|
312
|
+
await send(
|
|
313
|
+
"json-topic",
|
|
314
|
+
{ data: "example" },
|
|
315
|
+
{ transport: new JsonTransport() },
|
|
703
316
|
);
|
|
704
|
-
|
|
705
|
-
// Step 1: Process videos with FFmpeg
|
|
706
|
-
const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
|
|
707
|
-
const processingController = new AbortController();
|
|
708
|
-
|
|
709
|
-
try {
|
|
710
|
-
await videoProcessor.subscribe(
|
|
711
|
-
processingController.signal,
|
|
712
|
-
async (message) => {
|
|
713
|
-
const inputVideoStream = message.payload;
|
|
714
|
-
console.log("Processing video...");
|
|
715
|
-
|
|
716
|
-
if (!ffmpeg) {
|
|
717
|
-
throw new Error("FFmpeg not available");
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// Create optimized video stream using FFmpeg
|
|
721
|
-
const optimizedStream = new ReadableStream<Uint8Array>({
|
|
722
|
-
start(controller) {
|
|
723
|
-
const ffmpegProcess = spawn(
|
|
724
|
-
ffmpeg,
|
|
725
|
-
[
|
|
726
|
-
"-i",
|
|
727
|
-
"pipe:0", // Input from stdin
|
|
728
|
-
"-c:v",
|
|
729
|
-
"libvpx-vp9", // Video codec
|
|
730
|
-
"-c:a",
|
|
731
|
-
"libopus", // Audio codec
|
|
732
|
-
"-crf",
|
|
733
|
-
"23", // Quality
|
|
734
|
-
"-f",
|
|
735
|
-
"webm", // Output format
|
|
736
|
-
"pipe:1", // Output to stdout
|
|
737
|
-
],
|
|
738
|
-
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
739
|
-
);
|
|
740
|
-
|
|
741
|
-
// Pipe input stream to FFmpeg
|
|
742
|
-
const reader = inputVideoStream.getReader();
|
|
743
|
-
const pipeInput = async () => {
|
|
744
|
-
while (true) {
|
|
745
|
-
const { done, value } = await reader.read();
|
|
746
|
-
if (done) {
|
|
747
|
-
ffmpegProcess.stdin?.end();
|
|
748
|
-
break;
|
|
749
|
-
}
|
|
750
|
-
ffmpegProcess.stdin?.write(value);
|
|
751
|
-
}
|
|
752
|
-
};
|
|
753
|
-
pipeInput();
|
|
754
|
-
|
|
755
|
-
// Stream FFmpeg output
|
|
756
|
-
ffmpegProcess.stdout?.on("data", (chunk) => {
|
|
757
|
-
controller.enqueue(new Uint8Array(chunk));
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
ffmpegProcess.on("close", (code) => {
|
|
761
|
-
if (code === 0) {
|
|
762
|
-
controller.close();
|
|
763
|
-
} else {
|
|
764
|
-
controller.error(new Error(`FFmpeg failed with code ${code}`));
|
|
765
|
-
}
|
|
766
|
-
});
|
|
767
|
-
},
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
// Publish optimized video to next topic
|
|
771
|
-
await optimizedVideosTopic.publish(optimizedStream);
|
|
772
|
-
console.log("Video optimized and published");
|
|
773
|
-
},
|
|
774
|
-
);
|
|
775
|
-
} catch (error) {
|
|
776
|
-
console.error("Video processing error:", error);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// Step 2: Store optimized videos in Vercel Blob
|
|
780
|
-
const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
|
|
781
|
-
const uploadController = new AbortController();
|
|
782
|
-
|
|
783
|
-
try {
|
|
784
|
-
await blobUploader.subscribe(uploadController.signal, async (message) => {
|
|
785
|
-
const optimizedVideo = message.payload;
|
|
786
|
-
|
|
787
|
-
// Upload to Vercel Blob storage
|
|
788
|
-
const filename = `optimized-${Date.now()}.webm`;
|
|
789
|
-
const blob = await put(filename, optimizedVideo, {
|
|
790
|
-
access: "public",
|
|
791
|
-
contentType: "video/webm",
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
|
|
795
|
-
});
|
|
796
|
-
} catch (error) {
|
|
797
|
-
console.error("Blob upload error:", error);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Graceful shutdown
|
|
801
|
-
process.on("SIGINT", () => {
|
|
802
|
-
processingController.abort();
|
|
803
|
-
uploadController.abort();
|
|
804
|
-
});
|
|
805
317
|
```
|
|
806
318
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
The queue client provides specific error types for different failure scenarios:
|
|
810
|
-
|
|
811
|
-
### Error Types
|
|
812
|
-
|
|
813
|
-
- **`QueueEmptyError`**: Thrown when attempting to receive messages from an
|
|
814
|
-
empty queue (204 status)
|
|
815
|
-
|
|
816
|
-
- Only thrown when directly using `client.receiveMessages()`
|
|
817
|
-
- `ConsumerGroup.subscribe()` handles this error internally and continues
|
|
818
|
-
polling
|
|
819
|
-
|
|
820
|
-
- **`MessageLockedError`**: Thrown when a message is temporarily locked (423
|
|
821
|
-
status)
|
|
822
|
-
|
|
823
|
-
- Contains optional `retryAfter` property with seconds to wait before retry
|
|
824
|
-
- For `receiveMessages()` on FIFO queues: the next message in sequence is
|
|
825
|
-
locked
|
|
826
|
-
- For `receiveMessageById()`: the requested message is locked
|
|
827
|
-
- `ConsumerGroup.subscribe()` handles this error internally when polling
|
|
828
|
-
|
|
829
|
-
- **`MessageNotFoundError`**: Message doesn't exist (404 status)
|
|
830
|
-
|
|
831
|
-
- **`MessageNotAvailableError`**: Message exists but isn't available for
|
|
832
|
-
processing (409 status)
|
|
833
|
-
|
|
834
|
-
- **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status
|
|
835
|
-
with nextMessageId)
|
|
836
|
-
|
|
837
|
-
- Contains `nextMessageId` property indicating which message to process first
|
|
319
|
+
### Transport Selection Guide
|
|
838
320
|
|
|
839
|
-
|
|
840
|
-
|
|
321
|
+
| Use Case | Recommended Transport | Memory Usage | Performance |
|
|
322
|
+
| -------------------- | --------------------- | ------------ | ----------- |
|
|
323
|
+
| Small JSON objects | JsonTransport | Low | High |
|
|
324
|
+
| Binary files < 100MB | BufferTransport | Medium | High |
|
|
325
|
+
| Large files > 100MB | StreamTransport | Very Low | Medium |
|
|
326
|
+
| Real-time streams | StreamTransport | Very Low | High |
|
|
841
327
|
|
|
842
|
-
|
|
843
|
-
first
|
|
844
|
-
- Similar to `FifoOrderingViolationError` but specifically for receive-by-ID
|
|
845
|
-
operations
|
|
846
|
-
|
|
847
|
-
- **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
|
|
848
|
-
|
|
849
|
-
- **`BadRequestError`**: Invalid request parameters (400 status)
|
|
850
|
-
|
|
851
|
-
- Invalid queue names, FIFO limit violations, missing required parameters
|
|
852
|
-
|
|
853
|
-
- **`UnauthorizedError`**: Authentication failure (401 status)
|
|
854
|
-
|
|
855
|
-
- Missing or invalid authentication token
|
|
856
|
-
|
|
857
|
-
- **`ForbiddenError`**: Access denied (403 status)
|
|
328
|
+
## Error Handling
|
|
858
329
|
|
|
859
|
-
|
|
330
|
+
The queue client provides specific error types:
|
|
860
331
|
|
|
861
|
-
- **`
|
|
862
|
-
|
|
332
|
+
- **`QueueEmptyError`**: No messages available (204)
|
|
333
|
+
- **`MessageLockedError`**: Message temporarily locked (423)
|
|
334
|
+
- **`MessageNotFoundError`**: Message doesn't exist (404)
|
|
335
|
+
- **`MessageNotAvailableError`**: Message exists but unavailable (409)
|
|
336
|
+
- **`MessageCorruptedError`**: Message data corrupted
|
|
337
|
+
- **`BadRequestError`**: Invalid parameters (400)
|
|
338
|
+
- **`UnauthorizedError`**: Authentication failure (401)
|
|
339
|
+
- **`ForbiddenError`**: Access denied (403)
|
|
340
|
+
- **`InternalServerError`**: Server errors (500+)
|
|
863
341
|
|
|
864
|
-
|
|
342
|
+
Example error handling:
|
|
865
343
|
|
|
866
344
|
```typescript
|
|
867
345
|
import {
|
|
868
346
|
BadRequestError,
|
|
869
|
-
FailedDependencyError,
|
|
870
|
-
FifoOrderingViolationError,
|
|
871
347
|
ForbiddenError,
|
|
872
348
|
InternalServerError,
|
|
873
|
-
MessageLockedError,
|
|
874
|
-
QueueEmptyError,
|
|
875
349
|
UnauthorizedError,
|
|
876
350
|
} from "@vercel/queue";
|
|
877
351
|
|
|
878
|
-
// Handle empty queue or locked messages
|
|
879
|
-
try {
|
|
880
|
-
for await (const message of client.receiveMessages(options, transport)) {
|
|
881
|
-
// Process messages
|
|
882
|
-
}
|
|
883
|
-
} catch (error) {
|
|
884
|
-
if (error instanceof QueueEmptyError) {
|
|
885
|
-
console.log("Queue is empty, retry later");
|
|
886
|
-
} else if (error instanceof MessageLockedError) {
|
|
887
|
-
console.log("Next message in FIFO queue is locked");
|
|
888
|
-
if (error.retryAfter) {
|
|
889
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Handle locked message with retry
|
|
895
352
|
try {
|
|
896
|
-
await
|
|
897
|
-
} catch (error) {
|
|
898
|
-
if (error instanceof MessageLockedError) {
|
|
899
|
-
console.log("Message is locked by another consumer");
|
|
900
|
-
if (error.retryAfter) {
|
|
901
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
902
|
-
setTimeout(() => retry(), error.retryAfter * 1000);
|
|
903
|
-
}
|
|
904
|
-
} else if (error instanceof FailedDependencyError) {
|
|
905
|
-
// FIFO ordering violation for receive by ID
|
|
906
|
-
console.log(`Must process ${error.nextMessageId} first`);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// Handle authentication and authorization errors
|
|
911
|
-
try {
|
|
912
|
-
await topic.publish(payload);
|
|
353
|
+
await send("my-topic", payload);
|
|
913
354
|
} catch (error) {
|
|
914
355
|
if (error instanceof UnauthorizedError) {
|
|
915
356
|
console.log("Invalid token - refresh authentication");
|
|
916
357
|
} else if (error instanceof ForbiddenError) {
|
|
917
|
-
console.log("Environment mismatch - check
|
|
358
|
+
console.log("Environment mismatch - check configuration");
|
|
918
359
|
} else if (error instanceof BadRequestError) {
|
|
919
360
|
console.log("Invalid parameters:", error.message);
|
|
920
361
|
} else if (error instanceof InternalServerError) {
|
|
921
362
|
console.log("Server error - retry with backoff");
|
|
922
363
|
}
|
|
923
364
|
}
|
|
365
|
+
```
|
|
924
366
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
367
|
+
## Advanced Usage
|
|
368
|
+
|
|
369
|
+
### Direct Message Processing
|
|
370
|
+
|
|
371
|
+
> **Note**: The `receive` function is not intended for use in Vercel deployments. It's designed for use in the Vercel Sandbox environment or alternative server setups where you need direct message processing control.
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
// Process next available message
|
|
375
|
+
await receive<T>(topicName, consumerGroup, handler);
|
|
376
|
+
|
|
377
|
+
// Process specific message by ID
|
|
378
|
+
await receive<T>(topicName, consumerGroup, handler, {
|
|
379
|
+
messageId: "message-id"
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Process message with options
|
|
383
|
+
await receive<T>(topicName, consumerGroup, handler, {
|
|
384
|
+
messageId?: string; // Process specific message by ID
|
|
385
|
+
skipPayload?: boolean; // Skip payload download (requires messageId)
|
|
386
|
+
transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
|
|
387
|
+
visibilityTimeoutSeconds?: number; // Message visibility timeout
|
|
388
|
+
refreshInterval?: number; // Refresh interval for long-running operations
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Handler function signature
|
|
392
|
+
type MessageHandler<T = unknown> = (
|
|
393
|
+
message: T,
|
|
394
|
+
metadata: MessageMetadata
|
|
395
|
+
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
396
|
+
|
|
397
|
+
// Handler result types
|
|
398
|
+
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
399
|
+
|
|
400
|
+
interface MessageTimeoutResult {
|
|
401
|
+
timeoutSeconds: number; // seconds before message becomes available again
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Limits
|
|
406
|
+
|
|
407
|
+
- **Message Throughput**: Each topic can handle up to 1,000 messages per second
|
|
408
|
+
- **Payload Size**: Maximum payload size is 4.5MB (this limit will be increased soon)
|
|
409
|
+
- **Number of Topics**: No limit on the number of topics you can create
|
|
410
|
+
|
|
411
|
+
### Scaling Beyond Limits
|
|
412
|
+
|
|
413
|
+
If you need more than 1,000 messages per second, you can create multiple topics (e.g., user-specific or shard-based topics) and handle them with a single consumer using wildcards in your `vercel.json`:
|
|
414
|
+
|
|
415
|
+
```json
|
|
416
|
+
{
|
|
417
|
+
"functions": {
|
|
418
|
+
"app/api/queue/route.ts": {
|
|
419
|
+
"experimentalTriggers": [
|
|
420
|
+
{
|
|
421
|
+
"type": "queue/v1beta",
|
|
422
|
+
"topic": "user-*",
|
|
423
|
+
"consumer": "processor"
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
}
|
|
945
427
|
}
|
|
946
428
|
}
|
|
947
429
|
```
|
|
948
430
|
|
|
431
|
+
This allows you to:
|
|
432
|
+
|
|
433
|
+
- Create topics like `user-1`, `user-2`, etc.
|
|
434
|
+
- Process messages from all user topics with a single handler
|
|
435
|
+
- Each topic gets its own 1,000 messages per second quota
|
|
436
|
+
|
|
949
437
|
## License
|
|
950
438
|
|
|
951
439
|
MIT
|