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