@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 CHANGED
@@ -1,16 +1,20 @@
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
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
- - **Automatic Queue Triggering**: Vercel automatically triggers your API routes when messages are ready
8
- - **Next.js Integration**: Built-in support for Next.js API routes and Server Actions
9
- - **Generic Payload Support**: Send and receive any type of data with type safety
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
- - **Streaming Support**: Handle large payloads efficiently
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 package export resolution:
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 Messages
110
+ ### Publishing messages to a queue
47
111
 
48
- The `send` function can be used anywhere in your codebase to publish messages to a queue:
112
+ Create a new server function to publish messages
49
113
 
50
114
  ```typescript
51
- import { send } from "@vercel/queue";
52
-
53
- // Send a message to a topic
54
- await send("my-topic", {
55
- message: "Hello world",
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
- // 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
- },
68
- );
143
+ console.log(`Published message ${messageId}`);
144
+ }
69
145
  ```
70
146
 
71
- Example usage in an API route:
147
+ Now wire up the server function to your app
72
148
 
73
- ```typescript
74
- // app/api/send-message/route.ts
75
- import { send } from "@vercel/queue";
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
- return Response.json({ messageId });
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 Messages
164
+ ### Consuming the queue
89
165
 
90
- Messages are consumed using API routes that Vercel automatically triggers when messages are available.
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
- #### 1. Create API Routes
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
- // 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
- },
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
- // 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
- }
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
- 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
- }
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
- 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.
205
+ ## Key Features
138
206
 
139
- #### 2. Configure vercel.json
207
+ ### Streaming Support
140
208
 
141
- Configure which topics and consumers your API route handles:
209
+ Handle large files and data streams without loading them into memory:
142
210
 
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
- }
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
- ### Key Concepts
234
+ ### Consumer Groups
175
235
 
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
236
+ Multiple consumers can process messages from the same topic in parallel:
183
237
 
184
- ## Advanced Features
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
- ### Serialization (Transport) System
250
+ ## Architecture
187
251
 
188
- The queue client supports customizable serialization through the `Transport` interface:
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
- #### Built-in Transports
264
+ ## Performance
191
265
 
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
266
+ The multipart parser is optimized for high-throughput scenarios:
195
267
 
196
- Example:
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
- ```typescript
199
- import { send, JsonTransport } from "@vercel/queue";
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
- // JsonTransport is the default
202
- await send("json-topic", { data: "example" });
285
+ Buffers data for JSON parsing - suitable for structured data that fits in
286
+ memory.
203
287
 
204
- // Explicit transport configuration
205
- await send(
288
+ ```typescript
289
+ import { createTopic, JsonTransport } from "@vercel/queue";
290
+
291
+ const topic = createTopic<{ data: any }>(
292
+ client,
206
293
  "json-topic",
207
- { data: "example" },
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
- ### Transport Selection Guide
300
+ #### BufferTransport
213
301
 
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 |
302
+ Buffers the entire payload into memory as a Buffer - suitable for binary data
303
+ that fits in memory.
220
304
 
221
- ## Error Handling
305
+ ```typescript
306
+ import { BufferTransport, createTopic } from "@vercel/queue";
222
307
 
223
- The queue client provides specific error types:
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
- - **`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+)
317
+ #### StreamTransport
234
318
 
235
- Example error handling:
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
- try {
246
- await send("my-topic", payload);
247
- } 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
- }
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
- ## Advanced Usage
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
- ### Direct Message Processing
374
+ ```typescript
375
+ const client = new QueueClient({
376
+ token: string;
377
+ baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
378
+ });
379
+ ```
263
380
 
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.
381
+ ### Topic
265
382
 
266
383
  ```typescript
267
- // Process next available message
268
- await receive<T>(topicName, consumerGroup, handler);
384
+ const topic = createTopic<T>(client, topicName, transport?);
269
385
 
270
- // Process specific message by ID
271
- await receive<T>(topicName, consumerGroup, handler, {
272
- messageId: "message-id"
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
- // 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
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 = unknown> = (
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
- ## Limits
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
- - **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
450
+ ### Callback Utilities
303
451
 
304
- ### Scaling Beyond Limits
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
- 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`:
465
+ // Handler function signature for callbacks
466
+ type Handler<T = unknown> = (
467
+ payload: T,
468
+ metadata: MessageMetadata
469
+ ) => Promise<MessageHandlerResult> | MessageHandlerResult;
307
470
 
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
- ]
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
- This allows you to:
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
- - 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
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