@vercel/queue 0.0.0-alpha.12 → 0.0.0-alpha.3
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 +823 -204
- package/dist/index.d.mts +507 -132
- package/dist/index.d.ts +507 -132
- package/dist/index.js +589 -291
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +580 -288
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
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
|
|
4
|
+
with customizable serialization/deserialization (transport) support, including
|
|
5
|
+
**streaming support** for memory-efficient processing of large payloads.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
- **
|
|
8
|
-
|
|
9
|
-
- **
|
|
9
|
+
- **Generic Payload Support**: Send and receive any type of data with type
|
|
10
|
+
safety
|
|
11
|
+
- **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream)
|
|
12
|
+
or create your own
|
|
13
|
+
- **Streaming Support**: Handle large payloads without loading them entirely
|
|
14
|
+
into memory
|
|
10
15
|
- **Pub/Sub Pattern**: Topic-based messaging with consumer groups
|
|
11
16
|
- **Type Safety**: Full TypeScript support with generic types
|
|
12
|
-
- **
|
|
13
|
-
- **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
|
|
17
|
+
- **Automatic Retries**: Built-in visibility timeout management
|
|
14
18
|
|
|
15
19
|
## Installation
|
|
16
20
|
|
|
@@ -20,7 +24,8 @@ npm install @vercel/queue
|
|
|
20
24
|
|
|
21
25
|
## Quick Start
|
|
22
26
|
|
|
23
|
-
For local development, you'll need to pull your Vercel environment variables
|
|
27
|
+
For local development, you'll need to pull your Vercel environment variables
|
|
28
|
+
(including the OIDC token):
|
|
24
29
|
|
|
25
30
|
```bash
|
|
26
31
|
# Install Vercel CLI if you haven't already
|
|
@@ -30,9 +35,68 @@ npm i -g vercel
|
|
|
30
35
|
vc env pull
|
|
31
36
|
```
|
|
32
37
|
|
|
38
|
+
Publishing and consuming messages on a queue
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// index.ts
|
|
42
|
+
import { createTopic, JsonTransport, QueueClient } from "@vercel/queue";
|
|
43
|
+
|
|
44
|
+
// Create a client - automatically authenticated using the OIDC token
|
|
45
|
+
const client = await QueueClient.fromVercelFunction();
|
|
46
|
+
|
|
47
|
+
// Create a topic with JSON serialization (default)
|
|
48
|
+
const topic = createTopic<{ message: string; timestamp: number }>(
|
|
49
|
+
client,
|
|
50
|
+
"my-topic",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Publish a message
|
|
54
|
+
await topic.publish({
|
|
55
|
+
message: "Hello, World!",
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Create a consumer group
|
|
60
|
+
const consumer = topic.consumerGroup("my-processors");
|
|
61
|
+
|
|
62
|
+
// Process messages continuously with cancellation support
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
|
|
65
|
+
// Start processing (blocks until aborted or error)
|
|
66
|
+
try {
|
|
67
|
+
await consumer.subscribe(controller.signal, async (message) => {
|
|
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
|
+
```
|
|
78
|
+
|
|
79
|
+
Run the script
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Using dotenv to load the OIDC token
|
|
83
|
+
dotenv -e .env.local node index.ts
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Usage with Vercel
|
|
87
|
+
|
|
88
|
+
When deploying on Vercel, rather than having a persistent server subscribed to a
|
|
89
|
+
queue, Vercel can trigger a callback route when a message is ready for
|
|
90
|
+
consumption.
|
|
91
|
+
|
|
92
|
+
To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
|
|
93
|
+
existing app or create one using
|
|
94
|
+
[create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
95
|
+
|
|
33
96
|
### TypeScript Configuration
|
|
34
97
|
|
|
35
|
-
Update your `tsconfig.json` to use `"bundler"` module resolution for proper
|
|
98
|
+
Update your `tsconfig.json` to use `"bundler"` module resolution for proper
|
|
99
|
+
package export resolution:
|
|
36
100
|
|
|
37
101
|
```json
|
|
38
102
|
{
|
|
@@ -43,248 +107,326 @@ Update your `tsconfig.json` to use `"bundler"` module resolution for proper pack
|
|
|
43
107
|
}
|
|
44
108
|
```
|
|
45
109
|
|
|
46
|
-
### Publishing
|
|
110
|
+
### Publishing messages to a queue
|
|
47
111
|
|
|
48
|
-
|
|
112
|
+
Create a new server function to publish messages
|
|
49
113
|
|
|
50
114
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
115
|
+
// app/action.ts
|
|
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
|
+
);
|
|
57
142
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
},
|
|
68
|
-
);
|
|
143
|
+
console.log(`Published message ${messageId}`);
|
|
144
|
+
}
|
|
69
145
|
```
|
|
70
146
|
|
|
71
|
-
|
|
147
|
+
Now wire up the server function to your app
|
|
72
148
|
|
|
73
|
-
```
|
|
74
|
-
// app/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
export async function POST(request: Request) {
|
|
78
|
-
const body = await request.json();
|
|
79
|
-
|
|
80
|
-
const { messageId } = await send("my-topic", {
|
|
81
|
-
message: body.message,
|
|
82
|
-
});
|
|
149
|
+
```jsx
|
|
150
|
+
// app/some/page.tsx
|
|
151
|
+
"use client";
|
|
152
|
+
import { publishTestMessage } from "./actions";
|
|
83
153
|
|
|
84
|
-
|
|
154
|
+
export default function Button() {
|
|
155
|
+
return (
|
|
156
|
+
// ...
|
|
157
|
+
<Button onClick={() => publishTestMessage("Hello world")} >
|
|
158
|
+
Publish Test Message
|
|
159
|
+
</a>
|
|
160
|
+
);
|
|
85
161
|
}
|
|
86
162
|
```
|
|
87
163
|
|
|
88
|
-
### Consuming
|
|
164
|
+
### Consuming the queue
|
|
89
165
|
|
|
90
|
-
|
|
166
|
+
Instead of running a persistent server that subscribes to the queue, we use the
|
|
167
|
+
callback functionality of Vercel queues to consume messages on the fly, when a
|
|
168
|
+
message is ready to be processed.
|
|
91
169
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
The recommended approach is to handle multiple topics and consumers in a single API route to keep your `vercel.json` configuration simple:
|
|
170
|
+
The `handleCallback` helper function simplifies queue callback handling in NextJS:
|
|
95
171
|
|
|
96
172
|
```typescript
|
|
97
|
-
// app/api/queue/route.ts
|
|
173
|
+
// app/api/queue/handle/route.ts
|
|
98
174
|
import { handleCallback } from "@vercel/queue";
|
|
99
175
|
|
|
100
176
|
export const POST = handleCallback({
|
|
101
|
-
//
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// If this throws an error, the message will be automatically retried
|
|
108
|
-
await processMessage(message);
|
|
109
|
-
},
|
|
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 }
|
|
110
182
|
},
|
|
183
|
+
});
|
|
111
184
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return { timeoutSeconds: 300 };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
await processOrder(order);
|
|
185
|
+
// Or, specify separate handlers for separate consumer groups
|
|
186
|
+
export const POST = handleCallback({
|
|
187
|
+
// topic: "my-topic"
|
|
188
|
+
"my-topic": {
|
|
189
|
+
// consumer group: "compress"
|
|
190
|
+
compress: (message, metadata) => {
|
|
191
|
+
console.log("Compressing image:", message);
|
|
123
192
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
193
|
+
// consumer group: "resize"
|
|
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);
|
|
132
200
|
},
|
|
133
201
|
},
|
|
134
202
|
});
|
|
135
203
|
```
|
|
136
204
|
|
|
137
|
-
|
|
205
|
+
## Key Features
|
|
138
206
|
|
|
139
|
-
|
|
207
|
+
### Streaming Support
|
|
140
208
|
|
|
141
|
-
|
|
209
|
+
Handle large files and data streams without loading them into memory:
|
|
142
210
|
|
|
143
|
-
```
|
|
144
|
-
{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
}
|
|
211
|
+
```typescript
|
|
212
|
+
import { StreamTransport } from "@vercel/queue";
|
|
213
|
+
|
|
214
|
+
const videoTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
215
|
+
client,
|
|
216
|
+
"video-processing",
|
|
217
|
+
new StreamTransport(),
|
|
218
|
+
);
|
|
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);
|
|
170
230
|
}
|
|
171
|
-
}
|
|
231
|
+
});
|
|
172
232
|
```
|
|
173
233
|
|
|
174
|
-
###
|
|
234
|
+
### Consumer Groups
|
|
175
235
|
|
|
176
|
-
|
|
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
|
|
236
|
+
Multiple consumers can process messages from the same topic in parallel:
|
|
183
237
|
|
|
184
|
-
|
|
238
|
+
```typescript
|
|
239
|
+
// Multiple workers in the same group - they share/split messages
|
|
240
|
+
const worker1 = topic.consumerGroup("workers");
|
|
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
|
+
```
|
|
185
249
|
|
|
186
|
-
|
|
250
|
+
## Architecture
|
|
187
251
|
|
|
188
|
-
|
|
252
|
+
- **Topics**: Named message channels with configurable serialization
|
|
253
|
+
- **Consumer Groups**: Named groups of consumers that process messages in
|
|
254
|
+
parallel
|
|
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
|
|
189
263
|
|
|
190
|
-
|
|
264
|
+
## Performance
|
|
191
265
|
|
|
192
|
-
|
|
193
|
-
2. **BufferTransport**: For binary data that fits in memory
|
|
194
|
-
3. **StreamTransport**: For large files and memory-efficient processing
|
|
266
|
+
The multipart parser is optimized for high-throughput scenarios:
|
|
195
267
|
|
|
196
|
-
|
|
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
|
|
197
273
|
|
|
198
|
-
|
|
199
|
-
|
|
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)
|
|
200
284
|
|
|
201
|
-
|
|
202
|
-
|
|
285
|
+
Buffers data for JSON parsing - suitable for structured data that fits in
|
|
286
|
+
memory.
|
|
203
287
|
|
|
204
|
-
|
|
205
|
-
|
|
288
|
+
```typescript
|
|
289
|
+
import { createTopic, JsonTransport } from "@vercel/queue";
|
|
290
|
+
|
|
291
|
+
const topic = createTopic<{ data: any }>(
|
|
292
|
+
client,
|
|
206
293
|
"json-topic",
|
|
207
|
-
|
|
208
|
-
{ transport: new JsonTransport() },
|
|
294
|
+
new JsonTransport(),
|
|
209
295
|
);
|
|
296
|
+
// or simply (JsonTransport is the default):
|
|
297
|
+
const topic = createTopic<{ data: any }>(client, "json-topic");
|
|
210
298
|
```
|
|
211
299
|
|
|
212
|
-
|
|
300
|
+
#### BufferTransport
|
|
213
301
|
|
|
214
|
-
|
|
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 |
|
|
302
|
+
Buffers the entire payload into memory as a Buffer - suitable for binary data
|
|
303
|
+
that fits in memory.
|
|
220
304
|
|
|
221
|
-
|
|
305
|
+
```typescript
|
|
306
|
+
import { BufferTransport, createTopic } from "@vercel/queue";
|
|
222
307
|
|
|
223
|
-
|
|
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
|
+
```
|
|
224
316
|
|
|
225
|
-
|
|
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+)
|
|
317
|
+
#### StreamTransport
|
|
234
318
|
|
|
235
|
-
|
|
319
|
+
**True streaming support** - passes ReadableStream directly without buffering.
|
|
320
|
+
Ideal for large files and memory-efficient processing.
|
|
236
321
|
|
|
237
322
|
```typescript
|
|
238
|
-
import {
|
|
239
|
-
BadRequestError,
|
|
240
|
-
ForbiddenError,
|
|
241
|
-
InternalServerError,
|
|
242
|
-
UnauthorizedError,
|
|
243
|
-
} from "@vercel/queue";
|
|
323
|
+
import { createTopic, StreamTransport } from "@vercel/queue";
|
|
244
324
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
325
|
+
const topic = createTopic<ReadableStream<Uint8Array>>(
|
|
326
|
+
client,
|
|
327
|
+
"streaming-topic",
|
|
328
|
+
new StreamTransport(),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Send large file as stream without loading into memory
|
|
332
|
+
const fileStream = new ReadableStream<Uint8Array>({
|
|
333
|
+
start(controller) {
|
|
334
|
+
// Read file in chunks
|
|
335
|
+
for (const chunk of readFileInChunks("large-file.bin")) {
|
|
336
|
+
controller.enqueue(chunk);
|
|
337
|
+
}
|
|
338
|
+
controller.close();
|
|
339
|
+
},
|
|
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;
|
|
257
357
|
}
|
|
258
358
|
```
|
|
259
359
|
|
|
260
|
-
|
|
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
|
+
|
|
372
|
+
### QueueClient
|
|
261
373
|
|
|
262
|
-
|
|
374
|
+
```typescript
|
|
375
|
+
const client = new QueueClient({
|
|
376
|
+
token: string;
|
|
377
|
+
baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
|
|
378
|
+
});
|
|
379
|
+
```
|
|
263
380
|
|
|
264
|
-
|
|
381
|
+
### Topic
|
|
265
382
|
|
|
266
383
|
```typescript
|
|
267
|
-
|
|
268
|
-
await receive<T>(topicName, consumerGroup, handler);
|
|
384
|
+
const topic = createTopic<T>(client, topicName, transport?);
|
|
269
385
|
|
|
270
|
-
//
|
|
271
|
-
await
|
|
272
|
-
|
|
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" }
|
|
273
393
|
});
|
|
274
394
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
395
|
+
// Or provide multiple callbacks (each URL is called
|
|
396
|
+
// with a separate consumer group)
|
|
397
|
+
await topic.publish(payload, {
|
|
398
|
+
callback: {
|
|
399
|
+
group1: { url: "https://example.com/webhook1" },
|
|
400
|
+
group2: { url: "https://example.com/webhook2", delay: 30 }
|
|
401
|
+
}
|
|
282
402
|
});
|
|
283
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
|
+
```
|
|
423
|
+
|
|
424
|
+
### Message Handler
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
284
427
|
// Handler function signature
|
|
285
|
-
type MessageHandler<T
|
|
286
|
-
message: T
|
|
287
|
-
metadata: MessageMetadata
|
|
428
|
+
type MessageHandler<T> = (
|
|
429
|
+
message: Message<T>,
|
|
288
430
|
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
289
431
|
|
|
290
432
|
// Handler result types
|
|
@@ -295,37 +437,514 @@ interface MessageTimeoutResult {
|
|
|
295
437
|
}
|
|
296
438
|
```
|
|
297
439
|
|
|
298
|
-
|
|
440
|
+
### Transport Interface
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
interface Transport<T = unknown> {
|
|
444
|
+
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
445
|
+
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
446
|
+
contentType: string;
|
|
447
|
+
}
|
|
448
|
+
```
|
|
299
449
|
|
|
300
|
-
|
|
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
|
|
450
|
+
### Callback Utilities
|
|
303
451
|
|
|
304
|
-
|
|
452
|
+
```typescript
|
|
453
|
+
// Parse queue callback request headers
|
|
454
|
+
function parseCallbackRequest(request: Request): CallbackMessageOptions;
|
|
455
|
+
|
|
456
|
+
// Callback options type
|
|
457
|
+
interface CallbackMessageOptions {
|
|
458
|
+
queueName: string;
|
|
459
|
+
consumerGroup: string;
|
|
460
|
+
messageId: string;
|
|
461
|
+
}
|
|
462
|
+
// Create a callback handler for NextJS route handlers
|
|
463
|
+
function handleCallback(handlers: CallbackHandlers): (request: Request) => Promise<Response>;
|
|
305
464
|
|
|
306
|
-
|
|
465
|
+
// Handler function signature for callbacks
|
|
466
|
+
type Handler<T = unknown> = (
|
|
467
|
+
payload: T,
|
|
468
|
+
metadata: MessageMetadata
|
|
469
|
+
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
307
470
|
|
|
308
|
-
|
|
309
|
-
{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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);
|
|
490
|
+
},
|
|
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
|
+
});
|
|
498
|
+
|
|
499
|
+
// Error thrown for invalid callback requests
|
|
500
|
+
class InvalidCallbackError extends Error;
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Examples
|
|
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");
|
|
515
|
+
|
|
516
|
+
await userTopic.publish({
|
|
517
|
+
userId: "123",
|
|
518
|
+
action: "login",
|
|
519
|
+
timestamp: Date.now(),
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const consumer = userTopic.consumerGroup("processors");
|
|
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");
|
|
547
|
+
|
|
548
|
+
// Process a specific message if you know its ID
|
|
549
|
+
const messageId = "01234567-89ab-cdef-0123-456789abcdef";
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
await consumer.receiveMessage(messageId, async (message) => {
|
|
553
|
+
console.log(`Processing specific message: ${message.messageId}`);
|
|
554
|
+
console.log(
|
|
555
|
+
`User ${message.payload.userId} performed ${message.payload.action}`,
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
console.log("Message processed successfully");
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (error.message.includes("not found or not available")) {
|
|
561
|
+
console.log("Message was already processed or does not exist");
|
|
562
|
+
} else if (error.message.includes("FIFO ordering violation")) {
|
|
563
|
+
console.log("FIFO queue requires processing messages in order");
|
|
564
|
+
} else {
|
|
565
|
+
console.error("Error processing message:", error);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Processing Next Available Message
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
574
|
+
client,
|
|
575
|
+
"work-queue",
|
|
576
|
+
);
|
|
577
|
+
const worker = workTopic.consumerGroup("workers");
|
|
578
|
+
|
|
579
|
+
// Process the next available message (one-shot processing)
|
|
580
|
+
try {
|
|
581
|
+
await worker.receiveNextMessage(async (message) => {
|
|
582
|
+
console.log(`Processing task: ${message.payload.taskType}`);
|
|
583
|
+
await processTask(message.payload.taskType, message.payload.data);
|
|
584
|
+
});
|
|
585
|
+
console.log("Message processed successfully");
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (error instanceof QueueEmptyError) {
|
|
588
|
+
console.log("No messages available");
|
|
589
|
+
} else if (error instanceof MessageLockedError) {
|
|
590
|
+
console.log("Next message is locked (FIFO queue)");
|
|
591
|
+
if (error.retryAfter) {
|
|
592
|
+
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
319
593
|
}
|
|
594
|
+
} else {
|
|
595
|
+
console.error("Error processing message:", error);
|
|
320
596
|
}
|
|
321
597
|
}
|
|
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
|
+
```
|
|
609
|
+
|
|
610
|
+
### Timing Out Messages
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
614
|
+
client,
|
|
615
|
+
"work-queue",
|
|
616
|
+
);
|
|
617
|
+
const worker = workTopic.consumerGroup("workers");
|
|
618
|
+
const controller = new AbortController();
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
await worker.subscribe(controller.signal, async (message) => {
|
|
622
|
+
const { taskType, data } = message.payload;
|
|
623
|
+
|
|
624
|
+
// Check if we can process this task type right now
|
|
625
|
+
if (taskType === "heavy-computation" && isSystemOverloaded()) {
|
|
626
|
+
// Return timeout to retry later (5 minutes)
|
|
627
|
+
return { timeoutSeconds: 300 };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Check if we have required resources
|
|
631
|
+
if (taskType === "external-api" && !isExternalServiceAvailable()) {
|
|
632
|
+
// Return timeout to retry in 1 minute
|
|
633
|
+
return { timeoutSeconds: 60 };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Process the message normally
|
|
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
|
+
}
|
|
644
|
+
|
|
645
|
+
// Example with exponential backoff
|
|
646
|
+
const backoffController = new AbortController();
|
|
647
|
+
|
|
648
|
+
try {
|
|
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
|
+
}
|
|
322
674
|
```
|
|
323
675
|
|
|
324
|
-
|
|
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:
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
import { createTopic, QueueClient, StreamTransport } from "@vercel/queue";
|
|
683
|
+
import { spawn } from "child_process";
|
|
684
|
+
import ffmpeg from "ffmpeg-static";
|
|
685
|
+
import { put } from "@vercel/blob";
|
|
686
|
+
|
|
687
|
+
const client = new QueueClient({
|
|
688
|
+
token: "your-vercel-oidc-token",
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Input topic with unoptimized videos
|
|
692
|
+
const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
693
|
+
client,
|
|
694
|
+
"unoptimized-videos",
|
|
695
|
+
new StreamTransport(),
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
// Output topic for optimized videos
|
|
699
|
+
const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
700
|
+
client,
|
|
701
|
+
"optimized-videos",
|
|
702
|
+
new StreamTransport(),
|
|
703
|
+
);
|
|
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
|
+
}
|
|
325
778
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
```
|
|
806
|
+
|
|
807
|
+
## Error Handling
|
|
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
|
|
838
|
+
|
|
839
|
+
- **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424
|
|
840
|
+
status)
|
|
841
|
+
|
|
842
|
+
- Contains `nextMessageId` property indicating which message must be processed
|
|
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)
|
|
858
|
+
|
|
859
|
+
- Queue environment doesn't match token environment
|
|
860
|
+
|
|
861
|
+
- **`InternalServerError`**: Server-side errors (500+ status codes)
|
|
862
|
+
- Unexpected server errors, service unavailable, etc.
|
|
863
|
+
|
|
864
|
+
### Error Handling Examples
|
|
865
|
+
|
|
866
|
+
```typescript
|
|
867
|
+
import {
|
|
868
|
+
BadRequestError,
|
|
869
|
+
FailedDependencyError,
|
|
870
|
+
FifoOrderingViolationError,
|
|
871
|
+
ForbiddenError,
|
|
872
|
+
InternalServerError,
|
|
873
|
+
MessageLockedError,
|
|
874
|
+
QueueEmptyError,
|
|
875
|
+
UnauthorizedError,
|
|
876
|
+
} from "@vercel/queue";
|
|
877
|
+
|
|
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
|
+
try {
|
|
896
|
+
await consumer.receiveMessage(messageId, handler);
|
|
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);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
if (error instanceof UnauthorizedError) {
|
|
915
|
+
console.log("Invalid token - refresh authentication");
|
|
916
|
+
} else if (error instanceof ForbiddenError) {
|
|
917
|
+
console.log("Environment mismatch - check token/queue configuration");
|
|
918
|
+
} else if (error instanceof BadRequestError) {
|
|
919
|
+
console.log("Invalid parameters:", error.message);
|
|
920
|
+
} else if (error instanceof InternalServerError) {
|
|
921
|
+
console.log("Server error - retry with backoff");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Complete error handling pattern
|
|
926
|
+
function handleQueueError(error: unknown): void {
|
|
927
|
+
if (error instanceof QueueEmptyError || error instanceof MessageLockedError) {
|
|
928
|
+
// Transient errors - safe to retry
|
|
929
|
+
console.log("Temporary condition, will retry");
|
|
930
|
+
} else if (
|
|
931
|
+
error instanceof UnauthorizedError ||
|
|
932
|
+
error instanceof ForbiddenError
|
|
933
|
+
) {
|
|
934
|
+
// Authentication/authorization errors - need to fix configuration
|
|
935
|
+
console.log("Auth error - check credentials");
|
|
936
|
+
} else if (error instanceof BadRequestError) {
|
|
937
|
+
// Client error - fix the request
|
|
938
|
+
console.log("Invalid request:", error.message);
|
|
939
|
+
} else if (error instanceof InternalServerError) {
|
|
940
|
+
// Server error - implement exponential backoff
|
|
941
|
+
console.log("Server error - retry with backoff");
|
|
942
|
+
} else {
|
|
943
|
+
// Unknown error
|
|
944
|
+
console.error("Unexpected error:", error);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
```
|
|
329
948
|
|
|
330
949
|
## License
|
|
331
950
|
|