@vercel/queue 0.0.0-alpha.1 → 0.0.0-alpha.10

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