@vercel/queue 0.0.0-alpha.11 → 0.0.0-alpha.2

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 CHANGED
@@ -1,16 +1,15 @@
1
1
  # Vercel Queues
2
2
 
3
- A TypeScript client library for interacting with the Vercel Queue Service API, designed for seamless integration with Vercel deployments.
3
+ A TypeScript client library for interacting with the Vercel Queue Service API with customizable serialization/deserialization (transport) support, including **streaming support** for memory-efficient processing of large payloads.
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
9
7
  - **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
- - **Streaming Support**: Handle large payloads efficiently
13
- - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
12
+ - **Automatic Retries**: Built-in visibility timeout management
14
13
 
15
14
  ## Installation
16
15
 
@@ -20,7 +19,7 @@ npm install @vercel/queue
20
19
 
21
20
  ## Quick Start
22
21
 
23
- For local development, you'll need to pull your Vercel environment variables:
22
+ For local development, you'll need to pull your Vercel environment variables (including the OIDC token):
24
23
 
25
24
  ```bash
26
25
  # Install Vercel CLI if you haven't already
@@ -30,6 +29,60 @@ npm i -g vercel
30
29
  vc env pull
31
30
  ```
32
31
 
32
+ Publishing and consuming messages on a queue
33
+
34
+ ```typescript
35
+ // index.ts
36
+ import { QueueClient, createTopic, JsonTransport } from "@vercel/queue";
37
+
38
+ // Create a client - automatically authenticated using the OIDC token
39
+ const client = QueueClient.fromVercelFunction();
40
+
41
+ // Create a topic with JSON serialization (default)
42
+ const topic = createTopic<{ message: string; timestamp: number }>(
43
+ client,
44
+ "my-topic",
45
+ );
46
+
47
+ // Publish a message
48
+ await topic.publish({
49
+ message: "Hello, World!",
50
+ timestamp: Date.now(),
51
+ });
52
+
53
+ // Create a consumer group
54
+ const consumer = topic.consumerGroup("my-processors");
55
+
56
+ // Process messages continuously with cancellation support
57
+ const controller = new AbortController();
58
+
59
+ // Start processing (blocks until aborted or error)
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
+ }
68
+
69
+ // Stop processing from elsewhere in your code
70
+ // controller.abort();
71
+ ```
72
+
73
+ Run the script
74
+
75
+ ```bash
76
+ # Using dotenv to load the OIDC token
77
+ dotenv -e .env.local node index.ts
78
+ ```
79
+
80
+ ## Usage with Vercel
81
+
82
+ When deploying on Vercel, rather than having a persistent server subscribed to a queue, Vercel can trigger a callback route when a message is ready for consumption.
83
+
84
+ To demonstrate using queues on Vercel, let's use a Next.js app. You can use an existing app or create one using [create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
85
+
33
86
  ### TypeScript Configuration
34
87
 
35
88
  Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
@@ -43,289 +96,795 @@ Update your `tsconfig.json` to use `"bundler"` module resolution for proper pack
43
96
  }
44
97
  ```
45
98
 
46
- ### Publishing Messages
99
+ ### Publishing messages to a queue
47
100
 
48
- The `send` function can be used anywhere in your codebase to publish messages to a queue:
101
+ Create a new server function to publish messages
49
102
 
50
103
  ```typescript
51
- import { send } from "@vercel/queue";
104
+ // app/action.ts
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
+ );
52
133
 
53
- // Send a message to a topic
54
- await send("my-topic", {
55
- message: "Hello world",
56
- });
134
+ console.log(`Published message ${messageId}`);
135
+ }
136
+ ```
57
137
 
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
- },
138
+ Now wire up the server function to your app
139
+
140
+ ```jsx
141
+ // app/some/page.tsx
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";
224
+
225
+ const videoTopic = createTopic<ReadableStream<Uint8Array>>(
226
+ client,
227
+ "video-processing",
228
+ new StreamTransport(),
68
229
  );
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
+ });
69
243
  ```
70
244
 
71
- Example usage in an API route:
245
+ ### Consumer Groups
246
+
247
+ Multiple consumers can process messages from the same topic in parallel:
72
248
 
73
249
  ```typescript
74
- // app/api/send-message/route.ts
75
- import { send } from "@vercel/queue";
250
+ // Multiple workers in the same group - they share/split messages
251
+ const worker1 = topic.consumerGroup("workers");
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
+ ```
76
260
 
77
- export async function POST(request: Request) {
78
- const body = await request.json();
261
+ ## Architecture
79
262
 
80
- const { messageId } = await send("my-topic", {
81
- message: body.message,
82
- });
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
83
272
 
84
- return Response.json({ messageId });
85
- }
273
+ ## Performance
274
+
275
+ The multipart parser is optimized for high-throughput scenarios:
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";
294
+
295
+ const topic = createTopic<{ data: any }>(
296
+ client,
297
+ "json-topic",
298
+ new JsonTransport(),
299
+ );
300
+ // or simply (JsonTransport is the default):
301
+ const topic = createTopic<{ data: any }>(client, "json-topic");
86
302
  ```
87
303
 
88
- ### Consuming Messages
304
+ #### BufferTransport
305
+
306
+ Buffers the entire payload into memory as a Buffer - suitable for binary data that fits in memory.
89
307
 
90
- Messages are consumed using API routes that Vercel automatically triggers when messages are available.
308
+ ```typescript
309
+ import { BufferTransport, createTopic } from "@vercel/queue";
91
310
 
92
- #### 1. Create API Routes
311
+ const topic = createTopic<Buffer>(
312
+ client,
313
+ "binary-topic",
314
+ new BufferTransport(),
315
+ );
316
+ const binaryData = Buffer.from("Binary data", "utf8");
317
+ await topic.publish(binaryData);
318
+ ```
93
319
 
94
- The recommended approach is to handle multiple topics and consumers in a single API route to keep your `vercel.json` configuration simple:
320
+ #### StreamTransport
321
+
322
+ **True streaming support** - passes ReadableStream directly without buffering. Ideal for large files and memory-efficient processing.
95
323
 
96
324
  ```typescript
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
- },
325
+ import { StreamTransport, createTopic } from "@vercel/queue";
111
326
 
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 };
120
- }
327
+ const topic = createTopic<ReadableStream<Uint8Array>>(
328
+ client,
329
+ "streaming-topic",
330
+ new StreamTransport(),
331
+ );
121
332
 
122
- await processOrder(order);
123
- },
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
- },
333
+ // Send large file as stream without loading into memory
334
+ const fileStream = new ReadableStream<Uint8Array>({
335
+ start(controller) {
336
+ // Read file in chunks
337
+ for (const chunk of readFileInChunks("large-file.bin")) {
338
+ controller.enqueue(chunk);
339
+ }
340
+ controller.close();
133
341
  },
134
342
  });
343
+
344
+ await topic.publish(fileStream);
135
345
  ```
136
346
 
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.
347
+ ### Custom Transport
138
348
 
139
- #### 2. Configure vercel.json
349
+ You can create your own serialization format by implementing the `Transport` interface:
140
350
 
141
- Configure which topics and consumers your API route handles:
351
+ ```typescript
352
+ import { Transport } from "@vercel/queue";
142
353
 
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
- }
354
+ interface Transport<T = unknown> {
355
+ serialize(value: T): Buffer | ReadableStream<Uint8Array>;
356
+ deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
357
+ contentType: string;
171
358
  }
172
359
  ```
173
360
 
174
- ### Key Concepts
361
+ ### Choosing the Right Transport
175
362
 
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
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 |
183
370
 
184
- ## Advanced Features
371
+ ## API Reference
185
372
 
186
- ### Serialization (Transport) System
373
+ ### QueueClient
187
374
 
188
- The queue client supports customizable serialization through the `Transport` interface:
375
+ ```typescript
376
+ const client = new QueueClient({
377
+ token: string;
378
+ baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
379
+ });
380
+ ```
189
381
 
190
- #### Built-in Transports
382
+ ### Topic
191
383
 
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
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
+ ```
195
393
 
196
- Example:
394
+ ### ConsumerGroup
197
395
 
198
396
  ```typescript
199
- import { send, JsonTransport } from "@vercel/queue";
397
+ // Start continuous processing (blocks until signal is aborted or error occurs)
398
+ await consumer.subscribe(signal, handler, options?);
200
399
 
201
- // JsonTransport is the default
202
- await send("json-topic", { data: "example" });
400
+ // Process a specific message by ID
401
+ await consumer.receiveMessage(messageId, handler);
203
402
 
204
- // Explicit transport configuration
205
- await send(
206
- "json-topic",
207
- { data: "example" },
208
- { transport: new JsonTransport() },
209
- );
403
+ // Process the next available message
404
+ await consumer.receiveNextMessage(handler);
405
+
406
+ // Handle a specific message by ID without payload
407
+ await consumer.handleMessage(messageId, handler);
408
+ ```
409
+
410
+ ### Message Handler
411
+
412
+ ```typescript
413
+ // Handler function signature
414
+ type MessageHandler<T> = (
415
+ message: Message<T>,
416
+ ) => Promise<MessageHandlerResult> | MessageHandlerResult;
417
+
418
+ // Handler result types
419
+ type MessageHandlerResult = void | MessageTimeoutResult;
420
+
421
+ interface MessageTimeoutResult {
422
+ timeoutSeconds: number; // seconds before message becomes available again
423
+ }
210
424
  ```
211
425
 
212
- ### Transport Selection Guide
426
+ ### Transport Interface
427
+
428
+ ```typescript
429
+ interface Transport<T = unknown> {
430
+ serialize(value: T): Buffer | ReadableStream<Uint8Array>;
431
+ deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
432
+ contentType: string;
433
+ }
434
+ ```
213
435
 
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 |
436
+ ### Callback Utilities
220
437
 
221
- ## Error Handling
438
+ ```typescript
439
+ // Parse queue callback request headers
440
+ function parseCallbackRequest(request: Request): CallbackMessageOptions;
441
+
442
+ // Callback options type
443
+ interface CallbackMessageOptions {
444
+ queueName: string;
445
+ consumerGroup: string;
446
+ messageId: string;
447
+ }
222
448
 
223
- The queue client provides specific error types:
449
+ // Error thrown for invalid callback requests
450
+ class InvalidCallbackError extends Error;
451
+ ```
224
452
 
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+)
453
+ ## Examples
234
454
 
235
- Example error handling:
455
+ ### Basic JSON Processing
236
456
 
237
457
  ```typescript
238
- import {
239
- BadRequestError,
240
- ForbiddenError,
241
- InternalServerError,
242
- UnauthorizedError,
243
- } from "@vercel/queue";
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();
244
474
 
245
475
  try {
246
- await send("my-topic", payload);
476
+ await consumer.subscribe(controller.signal, async (message) => {
477
+ console.log(
478
+ `User ${message.payload.userId} performed ${message.payload.action}`,
479
+ );
480
+ });
247
481
  } catch (error) {
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");
256
- }
482
+ console.error("Processing error:", error);
257
483
  }
484
+
485
+ // Stop processing when needed
486
+ // controller.abort();
258
487
  ```
259
488
 
260
- ## Advanced Usage
489
+ ### Processing Specific Messages by ID
490
+
491
+ ```typescript
492
+ const userTopic = createTopic<{ userId: string; action: string }>(
493
+ client,
494
+ "user-events",
495
+ );
496
+ const consumer = userTopic.consumerGroup("processors");
497
+
498
+ // Process a specific message if you know its ID
499
+ const messageId = "01234567-89ab-cdef-0123-456789abcdef";
261
500
 
262
- ### Direct Message Processing
501
+ try {
502
+ await consumer.receiveMessage(messageId, async (message) => {
503
+ console.log(`Processing specific message: ${message.messageId}`);
504
+ console.log(
505
+ `User ${message.payload.userId} performed ${message.payload.action}`,
506
+ );
507
+ });
508
+ console.log("Message processed successfully");
509
+ } catch (error) {
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
+ ```
263
519
 
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.
520
+ ### Processing Next Available Message
265
521
 
266
522
  ```typescript
267
- // Process next available message
268
- await receive<T>(topicName, consumerGroup, handler);
523
+ const workTopic = createTopic<{ taskType: string; data: any }>(
524
+ client,
525
+ "work-queue",
526
+ );
527
+ const worker = workTopic.consumerGroup("workers");
269
528
 
270
- // Process specific message by ID
271
- await receive<T>(topicName, consumerGroup, handler, {
272
- messageId: "message-id"
529
+ // Process the next available message (one-shot processing)
530
+ try {
531
+ await worker.receiveNextMessage(async (message) => {
532
+ console.log(`Processing task: ${message.payload.taskType}`);
533
+ await processTask(message.payload.taskType, message.payload.data);
534
+ });
535
+ console.log("Message processed successfully");
536
+ } catch (error) {
537
+ if (error instanceof QueueEmptyError) {
538
+ console.log("No messages available");
539
+ } else if (error instanceof MessageLockedError) {
540
+ console.log("Next message is locked (FIFO queue)");
541
+ if (error.retryAfter) {
542
+ console.log(`Retry after ${error.retryAfter} seconds`);
543
+ }
544
+ } else {
545
+ console.error("Error processing message:", error);
546
+ }
547
+ }
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);
273
557
  });
558
+ ```
559
+
560
+ ### Timing Out Messages
561
+
562
+ ```typescript
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();
569
+
570
+ try {
571
+ await worker.subscribe(controller.signal, async (message) => {
572
+ const { taskType, data } = message.payload;
274
573
 
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
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:
629
+
630
+ ```typescript
631
+ import { QueueClient, createTopic, StreamTransport } from "@vercel/queue";
632
+ import { spawn } from "child_process";
633
+ import ffmpeg from "ffmpeg-static";
634
+ import { put } from "@vercel/blob";
635
+
636
+ const client = new QueueClient({
637
+ token: "your-vercel-oidc-token",
282
638
  });
283
639
 
284
- // Handler function signature
285
- type MessageHandler<T = unknown> = (
286
- message: T,
287
- metadata: MessageMetadata
288
- ) => Promise<MessageHandlerResult> | MessageHandlerResult;
640
+ // Input topic with unoptimized videos
641
+ const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
642
+ client,
643
+ "unoptimized-videos",
644
+ new StreamTransport(),
645
+ );
289
646
 
290
- // Handler result types
291
- type MessageHandlerResult = void | MessageTimeoutResult;
647
+ // Output topic for optimized videos
648
+ const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
649
+ client,
650
+ "optimized-videos",
651
+ new StreamTransport(),
652
+ );
292
653
 
293
- interface MessageTimeoutResult {
294
- timeoutSeconds: number; // seconds before message becomes available again
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);
295
726
  }
727
+
728
+ // Step 2: Store optimized videos in Vercel Blob
729
+ const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
730
+ const uploadController = new AbortController();
731
+
732
+ try {
733
+ await blobUploader.subscribe(uploadController.signal, async (message) => {
734
+ const optimizedVideo = message.payload;
735
+
736
+ // Upload to Vercel Blob storage
737
+ const filename = `optimized-${Date.now()}.webm`;
738
+ const blob = await put(filename, optimizedVideo, {
739
+ access: "public",
740
+ contentType: "video/webm",
741
+ });
742
+
743
+ console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
744
+ });
745
+ } catch (error) {
746
+ console.error("Blob upload error:", error);
747
+ }
748
+
749
+ // Graceful shutdown
750
+ process.on("SIGINT", () => {
751
+ processingController.abort();
752
+ uploadController.abort();
753
+ });
296
754
  ```
297
755
 
298
- ## Limits
756
+ ## Error Handling
757
+
758
+ The queue client provides specific error types for different failure scenarios:
299
759
 
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
760
+ ### Error Types
303
761
 
304
- ### Scaling Beyond Limits
762
+ - **`QueueEmptyError`**: Thrown when attempting to receive messages from an empty queue (204 status)
305
763
 
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`:
764
+ - Only thrown when directly using `client.receiveMessages()`
765
+ - `ConsumerGroup.subscribe()` handles this error internally and continues polling
307
766
 
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
- ]
767
+ - **`MessageLockedError`**: Thrown when a message is temporarily locked (423 status)
768
+
769
+ - Contains optional `retryAfter` property with seconds to wait before retry
770
+ - For `receiveMessages()` on FIFO queues: the next message in sequence is locked
771
+ - For `receiveMessageById()`: the requested message is locked
772
+ - `ConsumerGroup.subscribe()` handles this error internally when polling
773
+
774
+ - **`MessageNotFoundError`**: Message doesn't exist (404 status)
775
+
776
+ - **`MessageNotAvailableError`**: Message exists but isn't available for processing (409 status)
777
+
778
+ - **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status with nextMessageId)
779
+
780
+ - Contains `nextMessageId` property indicating which message to process first
781
+
782
+ - **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424 status)
783
+
784
+ - Contains `nextMessageId` property indicating which message must be processed first
785
+ - Similar to `FifoOrderingViolationError` but specifically for receive-by-ID operations
786
+
787
+ - **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
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`);
319
830
  }
320
831
  }
321
832
  }
322
- ```
323
833
 
324
- This allows you to:
834
+ // Handle locked message with retry
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
+ }
849
+
850
+ // Handle authentication and authorization errors
851
+ try {
852
+ await topic.publish(payload);
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
+ }
325
864
 
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
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
+ ```
329
888
 
330
889
  ## License
331
890