@vercel/queue 0.0.0-alpha.1 → 0.0.0-alpha.11
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 +226 -564
- package/dist/index.d.mts +115 -478
- package/dist/index.d.ts +115 -478
- package/dist/index.js +276 -554
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +273 -545
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Vercel Queues
|
|
2
2
|
|
|
3
|
-
A TypeScript client library for interacting with the Vercel Queue Service API
|
|
3
|
+
A TypeScript client library for interacting with the Vercel Queue Service API, designed for seamless integration with Vercel deployments.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
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
|
|
7
9
|
- **Generic Payload Support**: Send and receive any type of data with type safety
|
|
8
|
-
- **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
|
|
9
|
-
- **Streaming Support**: Handle large payloads without loading them entirely into memory
|
|
10
10
|
- **Pub/Sub Pattern**: Topic-based messaging with consumer groups
|
|
11
11
|
- **Type Safety**: Full TypeScript support with generic types
|
|
12
|
-
- **
|
|
12
|
+
- **Streaming Support**: Handle large payloads efficiently
|
|
13
|
+
- **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
|
|
13
14
|
|
|
14
15
|
## Installation
|
|
15
16
|
|
|
@@ -19,652 +20,313 @@ npm install @vercel/queue
|
|
|
19
20
|
|
|
20
21
|
## Quick Start
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
import { VQSClient, createTopic, JsonTransport } from "@vercel/queue";
|
|
23
|
+
For local development, you'll need to pull your Vercel environment variables:
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Create a topic with JSON serialization (default)
|
|
30
|
-
const topic = createTopic<{ message: string; timestamp: number }>(
|
|
31
|
-
client,
|
|
32
|
-
"my-topic",
|
|
33
|
-
);
|
|
25
|
+
```bash
|
|
26
|
+
# Install Vercel CLI if you haven't already
|
|
27
|
+
npm i -g vercel
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
timestamp: Date.now(),
|
|
39
|
-
});
|
|
29
|
+
# Pull environment variables from your Vercel project
|
|
30
|
+
vc env pull
|
|
31
|
+
```
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
const consumer = topic.consumerGroup("my-processors");
|
|
33
|
+
### TypeScript Configuration
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
const controller = new AbortController();
|
|
35
|
+
Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
|
|
46
36
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error("Processing stopped due to error:", error);
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"compilerOptions": {
|
|
40
|
+
"moduleResolution": "bundler"
|
|
41
|
+
// ... other options
|
|
42
|
+
}
|
|
55
43
|
}
|
|
56
|
-
|
|
57
|
-
// Stop processing from elsewhere in your code
|
|
58
|
-
// controller.abort();
|
|
59
44
|
```
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
The VQS client supports customizable serialization through the `Transport` interface with **streaming support** for memory-efficient processing. Transport can be configured at the **topic level** when creating a topic, or at the **consumer group level** when creating a consumer group.
|
|
64
|
-
|
|
65
|
-
### Built-in Transports
|
|
66
|
-
|
|
67
|
-
#### JsonTransport (Default)
|
|
46
|
+
### Publishing Messages
|
|
68
47
|
|
|
69
|
-
|
|
48
|
+
The `send` function can be used anywhere in your codebase to publish messages to a queue:
|
|
70
49
|
|
|
71
50
|
```typescript
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
const topic = createTopic<{ data: any }>(
|
|
75
|
-
client,
|
|
76
|
-
"json-topic",
|
|
77
|
-
new JsonTransport(),
|
|
78
|
-
);
|
|
79
|
-
// or simply (JsonTransport is the default):
|
|
80
|
-
const topic = createTopic<{ data: any }>(client, "json-topic");
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
#### BufferTransport
|
|
51
|
+
import { send } from "@vercel/queue";
|
|
84
52
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
53
|
+
// Send a message to a topic
|
|
54
|
+
await send("my-topic", {
|
|
55
|
+
message: "Hello world",
|
|
56
|
+
});
|
|
89
57
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
|
|
58
|
+
// With additional options
|
|
59
|
+
await send(
|
|
60
|
+
"my-topic",
|
|
61
|
+
{
|
|
62
|
+
message: "Hello world",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
idempotencyKey: "unique-key", // Optional: prevent duplicate messages
|
|
66
|
+
retentionSeconds: 3600, // Optional: override retention time (defaults to 24 hours)
|
|
67
|
+
},
|
|
94
68
|
);
|
|
95
|
-
const binaryData = Buffer.from("Binary data", "utf8");
|
|
96
|
-
await topic.publish(binaryData);
|
|
97
69
|
```
|
|
98
70
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
**True streaming support** - passes ReadableStream directly without buffering. Ideal for large files and memory-efficient processing.
|
|
71
|
+
Example usage in an API route:
|
|
102
72
|
|
|
103
73
|
```typescript
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const topic = createTopic<ReadableStream<Uint8Array>>(
|
|
107
|
-
client,
|
|
108
|
-
"streaming-topic",
|
|
109
|
-
new StreamTransport(),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
// Send large file as stream without loading into memory
|
|
113
|
-
const fileStream = new ReadableStream<Uint8Array>({
|
|
114
|
-
start(controller) {
|
|
115
|
-
// Read file in chunks
|
|
116
|
-
for (const chunk of readFileInChunks("large-file.bin")) {
|
|
117
|
-
controller.enqueue(chunk);
|
|
118
|
-
}
|
|
119
|
-
controller.close();
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
await topic.publish(fileStream);
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Custom Transport
|
|
74
|
+
// app/api/send-message/route.ts
|
|
75
|
+
import { send } from "@vercel/queue";
|
|
127
76
|
|
|
128
|
-
|
|
77
|
+
export async function POST(request: Request) {
|
|
78
|
+
const body = await request.json();
|
|
129
79
|
|
|
130
|
-
|
|
131
|
-
|
|
80
|
+
const { messageId } = await send("my-topic", {
|
|
81
|
+
message: body.message,
|
|
82
|
+
});
|
|
132
83
|
|
|
133
|
-
|
|
134
|
-
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
135
|
-
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
136
|
-
contentType: string;
|
|
84
|
+
return Response.json({ messageId });
|
|
137
85
|
}
|
|
138
86
|
```
|
|
139
87
|
|
|
140
|
-
###
|
|
88
|
+
### Consuming Messages
|
|
141
89
|
|
|
142
|
-
|
|
143
|
-
| ---------------------- | --------------------- | ------------ | ----------- |
|
|
144
|
-
| Small JSON objects | `JsonTransport` | Low | High |
|
|
145
|
-
| Binary files < 100MB | `BufferTransport` | Medium | High |
|
|
146
|
-
| Large files > 100MB | `StreamTransport` | Very Low | Medium |
|
|
147
|
-
| Real-time data streams | `StreamTransport` | Very Low | High |
|
|
148
|
-
| Custom protocols | Custom implementation | Varies | Varies |
|
|
90
|
+
Messages are consumed using API routes that Vercel automatically triggers when messages are available.
|
|
149
91
|
|
|
150
|
-
|
|
92
|
+
#### 1. Create API Routes
|
|
151
93
|
|
|
152
|
-
|
|
94
|
+
The recommended approach is to handle multiple topics and consumers in a single API route to keep your `vercel.json` configuration simple:
|
|
153
95
|
|
|
154
96
|
```typescript
|
|
155
|
-
|
|
156
|
-
import {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// Output topic for optimized videos
|
|
172
|
-
const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
173
|
-
client,
|
|
174
|
-
"optimized-videos",
|
|
175
|
-
new StreamTransport(),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// Step 1: Process videos with FFmpeg
|
|
179
|
-
const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
|
|
180
|
-
const processingController = new AbortController();
|
|
97
|
+
// app/api/queue/route.ts
|
|
98
|
+
import { handleCallback } from "@vercel/queue";
|
|
99
|
+
|
|
100
|
+
export const POST = handleCallback({
|
|
101
|
+
// Single topic with one consumer
|
|
102
|
+
"my-topic": {
|
|
103
|
+
"my-consumer": async (message, metadata) => {
|
|
104
|
+
// metadata includes: { messageId, deliveryCount, createdAt }
|
|
105
|
+
console.log("Processing message:", message);
|
|
106
|
+
|
|
107
|
+
// If this throws an error, the message will be automatically retried
|
|
108
|
+
await processMessage(message);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
181
111
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
throw new Error("FFmpeg not available");
|
|
112
|
+
// Multiple consumers for different purposes
|
|
113
|
+
"order-events": {
|
|
114
|
+
fulfillment: async (order, metadata) => {
|
|
115
|
+
// By default, errors will trigger automatic retries
|
|
116
|
+
// But you can control retry timing if needed:
|
|
117
|
+
if (!isSystemReady()) {
|
|
118
|
+
// Override default retry with a 5 minute delay
|
|
119
|
+
return { timeoutSeconds: 300 };
|
|
191
120
|
}
|
|
192
121
|
|
|
193
|
-
|
|
194
|
-
const optimizedStream = new ReadableStream<Uint8Array>({
|
|
195
|
-
start(controller) {
|
|
196
|
-
const ffmpegProcess = spawn(
|
|
197
|
-
ffmpeg,
|
|
198
|
-
[
|
|
199
|
-
"-i",
|
|
200
|
-
"pipe:0", // Input from stdin
|
|
201
|
-
"-c:v",
|
|
202
|
-
"libvpx-vp9", // Video codec
|
|
203
|
-
"-c:a",
|
|
204
|
-
"libopus", // Audio codec
|
|
205
|
-
"-crf",
|
|
206
|
-
"23", // Quality
|
|
207
|
-
"-f",
|
|
208
|
-
"webm", // Output format
|
|
209
|
-
"pipe:1", // Output to stdout
|
|
210
|
-
],
|
|
211
|
-
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// Pipe input stream to FFmpeg
|
|
215
|
-
const reader = inputVideoStream.getReader();
|
|
216
|
-
const pipeInput = async () => {
|
|
217
|
-
while (true) {
|
|
218
|
-
const { done, value } = await reader.read();
|
|
219
|
-
if (done) {
|
|
220
|
-
ffmpegProcess.stdin?.end();
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
ffmpegProcess.stdin?.write(value);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
pipeInput();
|
|
227
|
-
|
|
228
|
-
// Stream FFmpeg output
|
|
229
|
-
ffmpegProcess.stdout?.on("data", (chunk) => {
|
|
230
|
-
controller.enqueue(new Uint8Array(chunk));
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
ffmpegProcess.on("close", (code) => {
|
|
234
|
-
if (code === 0) {
|
|
235
|
-
controller.close();
|
|
236
|
-
} else {
|
|
237
|
-
controller.error(new Error(`FFmpeg failed with code ${code}`));
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Publish optimized video to next topic
|
|
244
|
-
await optimizedVideosTopic.publish(optimizedStream);
|
|
245
|
-
console.log("Video optimized and published");
|
|
122
|
+
await processOrder(order);
|
|
246
123
|
},
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
await blobUploader.subscribe(uploadController.signal, async (message) => {
|
|
258
|
-
const optimizedVideo = message.payload;
|
|
259
|
-
|
|
260
|
-
// Upload to Vercel Blob storage
|
|
261
|
-
const filename = `optimized-${Date.now()}.webm`;
|
|
262
|
-
const blob = await put(filename, optimizedVideo, {
|
|
263
|
-
access: "public",
|
|
264
|
-
contentType: "video/webm",
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
|
|
268
|
-
});
|
|
269
|
-
} catch (error) {
|
|
270
|
-
console.error("Blob upload error:", error);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Graceful shutdown
|
|
274
|
-
process.on("SIGINT", () => {
|
|
275
|
-
processingController.abort();
|
|
276
|
-
uploadController.abort();
|
|
277
|
-
});
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## API Reference
|
|
281
|
-
|
|
282
|
-
### VQSClient
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
const client = new VQSClient({
|
|
286
|
-
token: string;
|
|
287
|
-
baseUrl?: string; // defaults to 'https://@vercel/queue.vercel.sh'
|
|
124
|
+
analytics: async (order, metadata) => {
|
|
125
|
+
try {
|
|
126
|
+
await trackOrder(order);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Optional: Custom exponential backoff instead of default retry timing
|
|
129
|
+
const timeoutSeconds = Math.pow(2, metadata.deliveryCount) * 60;
|
|
130
|
+
return { timeoutSeconds };
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
},
|
|
288
134
|
});
|
|
289
135
|
```
|
|
290
136
|
|
|
291
|
-
|
|
137
|
+
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.
|
|
292
138
|
|
|
293
|
-
|
|
294
|
-
const topic = createTopic<T>(client, topicName, transport?);
|
|
139
|
+
#### 2. Configure vercel.json
|
|
295
140
|
|
|
296
|
-
|
|
297
|
-
await topic.publish(payload, options?);
|
|
141
|
+
Configure which topics and consumers your API route handles:
|
|
298
142
|
|
|
299
|
-
|
|
300
|
-
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"functions": {
|
|
146
|
+
"app/api/queue/route.ts": {
|
|
147
|
+
"experimentalTriggers": [
|
|
148
|
+
{
|
|
149
|
+
"type": "queue/v1beta",
|
|
150
|
+
"topic": "my-topic",
|
|
151
|
+
"consumer": "my-consumer",
|
|
152
|
+
"maxAttempts": 3, // Optional: Maximum number of delivery attempts (default: 3)
|
|
153
|
+
"retryAfterSeconds": 60, // Optional: Delay between retries (default: 60)
|
|
154
|
+
"initialDelaySeconds": 0 // Optional: Initial delay before first delivery (default: 0)
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"type": "queue/v1beta",
|
|
158
|
+
"topic": "order-events",
|
|
159
|
+
"consumer": "fulfillment"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"type": "queue/v1beta",
|
|
163
|
+
"topic": "order-events",
|
|
164
|
+
"consumer": "analytics",
|
|
165
|
+
"maxAttempts": 5, // Retry up to 5 times
|
|
166
|
+
"retryAfterSeconds": 300 // Wait 5 minutes between retries
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
301
172
|
```
|
|
302
173
|
|
|
303
|
-
###
|
|
174
|
+
### Key Concepts
|
|
304
175
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
176
|
+
- **Topics**: Named message channels that can have multiple consumer groups
|
|
177
|
+
- **Consumer Groups**: Named groups of consumers that process messages in parallel
|
|
178
|
+
- Different consumer groups for the same topic each get a copy of every message
|
|
179
|
+
- Multiple consumers in the same group share/split messages for load balancing
|
|
180
|
+
- **Automatic Triggering**: Vercel triggers your API routes when messages are available
|
|
181
|
+
- **Message Processing**: Your API routes receive message metadata via headers
|
|
182
|
+
- **Configuration**: The `vercel.json` file tells Vercel which routes handle which topics/consumers
|
|
308
183
|
|
|
309
|
-
|
|
310
|
-
await consumer.receiveMessage(messageId, handler);
|
|
184
|
+
## Advanced Features
|
|
311
185
|
|
|
312
|
-
|
|
313
|
-
await consumer.receiveNextMessage(handler);
|
|
186
|
+
### Serialization (Transport) System
|
|
314
187
|
|
|
315
|
-
|
|
316
|
-
await consumer.handleMessage(messageId, handler);
|
|
317
|
-
```
|
|
188
|
+
The queue client supports customizable serialization through the `Transport` interface:
|
|
318
189
|
|
|
319
|
-
|
|
190
|
+
#### Built-in Transports
|
|
320
191
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
message: Message<T>,
|
|
325
|
-
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
192
|
+
1. **JsonTransport (Default)**: For structured data that fits in memory
|
|
193
|
+
2. **BufferTransport**: For binary data that fits in memory
|
|
194
|
+
3. **StreamTransport**: For large files and memory-efficient processing
|
|
326
195
|
|
|
327
|
-
|
|
328
|
-
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
329
|
-
|
|
330
|
-
interface MessageTimeoutResult {
|
|
331
|
-
timeoutSeconds: number; // seconds before message becomes available again
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
### Transport Interface
|
|
196
|
+
Example:
|
|
336
197
|
|
|
337
198
|
```typescript
|
|
338
|
-
|
|
339
|
-
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
340
|
-
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
341
|
-
contentType: string;
|
|
342
|
-
}
|
|
343
|
-
```
|
|
199
|
+
import { send, JsonTransport } from "@vercel/queue";
|
|
344
200
|
|
|
345
|
-
|
|
201
|
+
// JsonTransport is the default
|
|
202
|
+
await send("json-topic", { data: "example" });
|
|
346
203
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
queueName: string;
|
|
354
|
-
consumerGroup: string;
|
|
355
|
-
messageId: string;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Error thrown for invalid callback requests
|
|
359
|
-
class InvalidCallbackError extends Error;
|
|
204
|
+
// Explicit transport configuration
|
|
205
|
+
await send(
|
|
206
|
+
"json-topic",
|
|
207
|
+
{ data: "example" },
|
|
208
|
+
{ transport: new JsonTransport() },
|
|
209
|
+
);
|
|
360
210
|
```
|
|
361
211
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
### Basic JSON Processing
|
|
212
|
+
### Transport Selection Guide
|
|
365
213
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const userTopic = createTopic<UserEvent>(client, "user-events");
|
|
374
|
-
|
|
375
|
-
await userTopic.publish({
|
|
376
|
-
userId: "123",
|
|
377
|
-
action: "login",
|
|
378
|
-
timestamp: Date.now(),
|
|
379
|
-
});
|
|
214
|
+
| Use Case | Recommended Transport | Memory Usage | Performance |
|
|
215
|
+
| -------------------- | --------------------- | ------------ | ----------- |
|
|
216
|
+
| Small JSON objects | JsonTransport | Low | High |
|
|
217
|
+
| Binary files < 100MB | BufferTransport | Medium | High |
|
|
218
|
+
| Large files > 100MB | StreamTransport | Very Low | Medium |
|
|
219
|
+
| Real-time streams | StreamTransport | Very Low | High |
|
|
380
220
|
|
|
381
|
-
|
|
382
|
-
const controller = new AbortController();
|
|
221
|
+
## Error Handling
|
|
383
222
|
|
|
384
|
-
|
|
385
|
-
await consumer.subscribe(controller.signal, async (message) => {
|
|
386
|
-
console.log(
|
|
387
|
-
`User ${message.payload.userId} performed ${message.payload.action}`,
|
|
388
|
-
);
|
|
389
|
-
});
|
|
390
|
-
} catch (error) {
|
|
391
|
-
console.error("Processing error:", error);
|
|
392
|
-
}
|
|
223
|
+
The queue client provides specific error types:
|
|
393
224
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
225
|
+
- **`QueueEmptyError`**: No messages available (204)
|
|
226
|
+
- **`MessageLockedError`**: Message temporarily locked (423)
|
|
227
|
+
- **`MessageNotFoundError`**: Message doesn't exist (404)
|
|
228
|
+
- **`MessageNotAvailableError`**: Message exists but unavailable (409)
|
|
229
|
+
- **`MessageCorruptedError`**: Message data corrupted
|
|
230
|
+
- **`BadRequestError`**: Invalid parameters (400)
|
|
231
|
+
- **`UnauthorizedError`**: Authentication failure (401)
|
|
232
|
+
- **`ForbiddenError`**: Access denied (403)
|
|
233
|
+
- **`InternalServerError`**: Server errors (500+)
|
|
397
234
|
|
|
398
|
-
|
|
235
|
+
Example error handling:
|
|
399
236
|
|
|
400
237
|
```typescript
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// Process a specific message if you know its ID
|
|
408
|
-
const messageId = "01234567-89ab-cdef-0123-456789abcdef";
|
|
238
|
+
import {
|
|
239
|
+
BadRequestError,
|
|
240
|
+
ForbiddenError,
|
|
241
|
+
InternalServerError,
|
|
242
|
+
UnauthorizedError,
|
|
243
|
+
} from "@vercel/queue";
|
|
409
244
|
|
|
410
245
|
try {
|
|
411
|
-
await
|
|
412
|
-
console.log(`Processing specific message: ${message.messageId}`);
|
|
413
|
-
console.log(
|
|
414
|
-
`User ${message.payload.userId} performed ${message.payload.action}`,
|
|
415
|
-
);
|
|
416
|
-
});
|
|
417
|
-
console.log("Message processed successfully");
|
|
246
|
+
await send("my-topic", payload);
|
|
418
247
|
} catch (error) {
|
|
419
|
-
if (error
|
|
420
|
-
console.log("
|
|
421
|
-
} else if (error
|
|
422
|
-
console.log("
|
|
423
|
-
} else {
|
|
424
|
-
console.
|
|
248
|
+
if (error instanceof UnauthorizedError) {
|
|
249
|
+
console.log("Invalid token - refresh authentication");
|
|
250
|
+
} else if (error instanceof ForbiddenError) {
|
|
251
|
+
console.log("Environment mismatch - check configuration");
|
|
252
|
+
} else if (error instanceof BadRequestError) {
|
|
253
|
+
console.log("Invalid parameters:", error.message);
|
|
254
|
+
} else if (error instanceof InternalServerError) {
|
|
255
|
+
console.log("Server error - retry with backoff");
|
|
425
256
|
}
|
|
426
257
|
}
|
|
427
258
|
```
|
|
428
259
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
```typescript
|
|
432
|
-
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
433
|
-
client,
|
|
434
|
-
"work-queue",
|
|
435
|
-
);
|
|
436
|
-
const worker = workTopic.consumerGroup("workers");
|
|
437
|
-
|
|
438
|
-
// Process the next available message (one-shot processing)
|
|
439
|
-
try {
|
|
440
|
-
await worker.receiveNextMessage(async (message) => {
|
|
441
|
-
console.log(`Processing task: ${message.payload.taskType}`);
|
|
442
|
-
await processTask(message.payload.taskType, message.payload.data);
|
|
443
|
-
});
|
|
444
|
-
console.log("Message processed successfully");
|
|
445
|
-
} catch (error) {
|
|
446
|
-
if (error instanceof QueueEmptyError) {
|
|
447
|
-
console.log("No messages available");
|
|
448
|
-
} else if (error instanceof MessageLockedError) {
|
|
449
|
-
console.log("Next message is locked (FIFO queue)");
|
|
450
|
-
if (error.retryAfter) {
|
|
451
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
452
|
-
}
|
|
453
|
-
} else {
|
|
454
|
-
console.error("Error processing message:", error);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// You can also use it with timeout results
|
|
459
|
-
await worker.receiveNextMessage(async (message) => {
|
|
460
|
-
if (!canProcessTaskType(message.payload.taskType)) {
|
|
461
|
-
// Return timeout to retry later
|
|
462
|
-
return { timeoutSeconds: 60 };
|
|
463
|
-
}
|
|
260
|
+
## Advanced Usage
|
|
464
261
|
|
|
465
|
-
|
|
466
|
-
});
|
|
467
|
-
```
|
|
262
|
+
### Direct Message Processing
|
|
468
263
|
|
|
469
|
-
|
|
264
|
+
> **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.
|
|
470
265
|
|
|
471
266
|
```typescript
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
"work-queue",
|
|
475
|
-
);
|
|
476
|
-
const worker = workTopic.consumerGroup("workers");
|
|
477
|
-
const controller = new AbortController();
|
|
267
|
+
// Process next available message
|
|
268
|
+
await receive<T>(topicName, consumerGroup, handler);
|
|
478
269
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// Check if we can process this task type right now
|
|
484
|
-
if (taskType === "heavy-computation" && isSystemOverloaded()) {
|
|
485
|
-
// Return timeout to retry later (5 minutes)
|
|
486
|
-
return { timeoutSeconds: 300 };
|
|
487
|
-
}
|
|
270
|
+
// Process specific message by ID
|
|
271
|
+
await receive<T>(topicName, consumerGroup, handler, {
|
|
272
|
+
messageId: "message-id"
|
|
273
|
+
});
|
|
488
274
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
275
|
+
// Process message with options
|
|
276
|
+
await receive<T>(topicName, consumerGroup, handler, {
|
|
277
|
+
messageId?: string; // Process specific message by ID
|
|
278
|
+
skipPayload?: boolean; // Skip payload download (requires messageId)
|
|
279
|
+
transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
|
|
280
|
+
visibilityTimeoutSeconds?: number; // Message visibility timeout
|
|
281
|
+
refreshInterval?: number; // Refresh interval for long-running operations
|
|
282
|
+
});
|
|
494
283
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
} catch (error) {
|
|
501
|
-
console.error("Worker processing error:", error);
|
|
502
|
-
}
|
|
284
|
+
// Handler function signature
|
|
285
|
+
type MessageHandler<T = unknown> = (
|
|
286
|
+
message: T,
|
|
287
|
+
metadata: MessageMetadata
|
|
288
|
+
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
503
289
|
|
|
504
|
-
//
|
|
505
|
-
|
|
290
|
+
// Handler result types
|
|
291
|
+
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
506
292
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const maxRetries = 3;
|
|
510
|
-
const deliveryCount = message.deliveryCount;
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
await processMessage(message.payload);
|
|
514
|
-
// Successful processing - message will be deleted
|
|
515
|
-
} catch (error) {
|
|
516
|
-
if (deliveryCount < maxRetries) {
|
|
517
|
-
// Exponential backoff: 2^deliveryCount minutes
|
|
518
|
-
const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
|
|
519
|
-
console.log(
|
|
520
|
-
`Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
|
|
521
|
-
);
|
|
522
|
-
return { timeoutSeconds: timeoutSeconds };
|
|
523
|
-
} else {
|
|
524
|
-
// Max retries reached, let the message fail and be deleted
|
|
525
|
-
console.error("Max retries reached, message will be discarded:", error);
|
|
526
|
-
throw error;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
} catch (error) {
|
|
531
|
-
console.error("Backoff processing error:", error);
|
|
293
|
+
interface MessageTimeoutResult {
|
|
294
|
+
timeoutSeconds: number; // seconds before message becomes available again
|
|
532
295
|
}
|
|
533
296
|
```
|
|
534
297
|
|
|
535
|
-
##
|
|
536
|
-
|
|
537
|
-
MIT
|
|
538
|
-
|
|
539
|
-
## Error Handling
|
|
298
|
+
## Limits
|
|
540
299
|
|
|
541
|
-
|
|
300
|
+
- **Message Throughput**: Each topic can handle up to 1,000 messages per second
|
|
301
|
+
- **Payload Size**: Maximum payload size is 4.5MB (this limit will be increased soon)
|
|
302
|
+
- **Number of Topics**: No limit on the number of topics you can create
|
|
542
303
|
|
|
543
|
-
###
|
|
304
|
+
### Scaling Beyond Limits
|
|
544
305
|
|
|
545
|
-
|
|
306
|
+
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`:
|
|
546
307
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
- **`MessageNotAvailableError`**: Message exists but isn't available for processing (409 status)
|
|
560
|
-
|
|
561
|
-
- **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status with nextMessageId)
|
|
562
|
-
|
|
563
|
-
- Contains `nextMessageId` property indicating which message to process first
|
|
564
|
-
|
|
565
|
-
- **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424 status)
|
|
566
|
-
|
|
567
|
-
- Contains `nextMessageId` property indicating which message must be processed first
|
|
568
|
-
- Similar to `FifoOrderingViolationError` but specifically for receive-by-ID operations
|
|
569
|
-
|
|
570
|
-
- **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
|
|
571
|
-
|
|
572
|
-
- **`BadRequestError`**: Invalid request parameters (400 status)
|
|
573
|
-
|
|
574
|
-
- Invalid queue names, FIFO limit violations, missing required parameters
|
|
575
|
-
|
|
576
|
-
- **`UnauthorizedError`**: Authentication failure (401 status)
|
|
577
|
-
|
|
578
|
-
- Missing or invalid authentication token
|
|
579
|
-
|
|
580
|
-
- **`ForbiddenError`**: Access denied (403 status)
|
|
581
|
-
|
|
582
|
-
- Queue environment doesn't match token environment
|
|
583
|
-
|
|
584
|
-
- **`InternalServerError`**: Server-side errors (500+ status codes)
|
|
585
|
-
- Unexpected server errors, service unavailable, etc.
|
|
586
|
-
|
|
587
|
-
### Error Handling Examples
|
|
588
|
-
|
|
589
|
-
```typescript
|
|
590
|
-
import {
|
|
591
|
-
QueueEmptyError,
|
|
592
|
-
MessageLockedError,
|
|
593
|
-
FifoOrderingViolationError,
|
|
594
|
-
FailedDependencyError,
|
|
595
|
-
BadRequestError,
|
|
596
|
-
UnauthorizedError,
|
|
597
|
-
ForbiddenError,
|
|
598
|
-
InternalServerError,
|
|
599
|
-
} from "@vercel/queue";
|
|
600
|
-
|
|
601
|
-
// Handle empty queue or locked messages
|
|
602
|
-
try {
|
|
603
|
-
for await (const message of client.receiveMessages(options, transport)) {
|
|
604
|
-
// Process messages
|
|
605
|
-
}
|
|
606
|
-
} catch (error) {
|
|
607
|
-
if (error instanceof QueueEmptyError) {
|
|
608
|
-
console.log("Queue is empty, retry later");
|
|
609
|
-
} else if (error instanceof MessageLockedError) {
|
|
610
|
-
console.log("Next message in FIFO queue is locked");
|
|
611
|
-
if (error.retryAfter) {
|
|
612
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
308
|
+
```json
|
|
309
|
+
{
|
|
310
|
+
"functions": {
|
|
311
|
+
"app/api/queue/route.ts": {
|
|
312
|
+
"experimentalTriggers": [
|
|
313
|
+
{
|
|
314
|
+
"type": "queue/v1beta",
|
|
315
|
+
"topic": "user-*",
|
|
316
|
+
"consumer": "processor"
|
|
317
|
+
}
|
|
318
|
+
]
|
|
613
319
|
}
|
|
614
320
|
}
|
|
615
321
|
}
|
|
322
|
+
```
|
|
616
323
|
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
await consumer.receiveMessage(messageId, handler);
|
|
620
|
-
} catch (error) {
|
|
621
|
-
if (error instanceof MessageLockedError) {
|
|
622
|
-
console.log("Message is locked by another consumer");
|
|
623
|
-
if (error.retryAfter) {
|
|
624
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
625
|
-
setTimeout(() => retry(), error.retryAfter * 1000);
|
|
626
|
-
}
|
|
627
|
-
} else if (error instanceof FailedDependencyError) {
|
|
628
|
-
// FIFO ordering violation for receive by ID
|
|
629
|
-
console.log(`Must process ${error.nextMessageId} first`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
324
|
+
This allows you to:
|
|
632
325
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
} catch (error) {
|
|
637
|
-
if (error instanceof UnauthorizedError) {
|
|
638
|
-
console.log("Invalid token - refresh authentication");
|
|
639
|
-
} else if (error instanceof ForbiddenError) {
|
|
640
|
-
console.log("Environment mismatch - check token/queue configuration");
|
|
641
|
-
} else if (error instanceof BadRequestError) {
|
|
642
|
-
console.log("Invalid parameters:", error.message);
|
|
643
|
-
} else if (error instanceof InternalServerError) {
|
|
644
|
-
console.log("Server error - retry with backoff");
|
|
645
|
-
}
|
|
646
|
-
}
|
|
326
|
+
- Create topics like `user-1`, `user-2`, etc.
|
|
327
|
+
- Process messages from all user topics with a single handler
|
|
328
|
+
- Each topic gets its own 1,000 messages per second quota
|
|
647
329
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
// Transient errors - safe to retry
|
|
652
|
-
console.log("Temporary condition, will retry");
|
|
653
|
-
} else if (
|
|
654
|
-
error instanceof UnauthorizedError ||
|
|
655
|
-
error instanceof ForbiddenError
|
|
656
|
-
) {
|
|
657
|
-
// Authentication/authorization errors - need to fix configuration
|
|
658
|
-
console.log("Auth error - check credentials");
|
|
659
|
-
} else if (error instanceof BadRequestError) {
|
|
660
|
-
// Client error - fix the request
|
|
661
|
-
console.log("Invalid request:", error.message);
|
|
662
|
-
} else if (error instanceof InternalServerError) {
|
|
663
|
-
// Server error - implement exponential backoff
|
|
664
|
-
console.log("Server error - retry with backoff");
|
|
665
|
-
} else {
|
|
666
|
-
// Unknown error
|
|
667
|
-
console.error("Unexpected error:", error);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
```
|
|
330
|
+
## License
|
|
331
|
+
|
|
332
|
+
MIT
|