@vercel/queue 0.0.0-alpha.36 → 0.0.0-alpha.38

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
@@ -5,12 +5,13 @@ A TypeScript client library for interacting with the Vercel Queue Service API, d
5
5
  ## Features
6
6
 
7
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
8
+ - **Next.js Integration**: Built-in support for Next.js App Router and Pages Router
9
9
  - **Generic Payload Support**: Send and receive any type of data with type safety
10
10
  - **Pub/Sub Pattern**: Topic-based messaging with consumer groups
11
11
  - **Type Safety**: Full TypeScript support with generic types
12
12
  - **Streaming Support**: Handle large payloads efficiently
13
13
  - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
14
+ - **Framework Adapters**: Web API, Next.js App Router, and Pages Router support
14
15
 
15
16
  ## Installation
16
17
 
@@ -39,6 +40,8 @@ vc env pull
39
40
 
40
41
  The library reads your `vercel.json` configuration, discovers your queue handlers, and triggers them automatically when messages are sent.
41
42
 
43
+ > **Note:** Local dev mode is enabled when `NODE_ENV=development`. Most frameworks (Next.js, etc.) set this automatically when running `npm run dev`.
44
+
42
45
  ### Example Workflow
43
46
 
44
47
  ```bash
@@ -48,18 +51,6 @@ npm run dev
48
51
  # Send messages - they process locally automatically!
49
52
  ```
50
53
 
51
- ### TypeScript Configuration
52
-
53
- Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
54
-
55
- ```json
56
- {
57
- "compilerOptions": {
58
- "moduleResolution": "bundler"
59
- }
60
- }
61
- ```
62
-
63
54
  ### Publishing Messages
64
55
 
65
56
  The `send` function can be used anywhere in your codebase to publish messages to a queue:
@@ -109,124 +100,108 @@ Messages are consumed using API routes that Vercel automatically triggers when m
109
100
 
110
101
  #### 1. Create API Routes
111
102
 
112
- ##### App Router (Recommended)
103
+ ##### Web API (`@vercel/queue/web`)
113
104
 
114
- The recommended approach is to handle multiple topics and consumers in a single API route to keep your `vercel.json` configuration simple:
105
+ The `handleCallback` from `@vercel/queue/web` returns a standard `(Request) => Promise<Response>` handler. It works with any framework that uses the Web API `Request`/`Response` types, including Next.js App Router, Hono, and others.
106
+
107
+ **Next.js App Router:**
115
108
 
116
109
  ```typescript
117
- // app/api/queue/route.ts
118
- import { handleCallback } from "@vercel/queue";
119
-
120
- export const POST = handleCallback({
121
- // Single topic with one consumer
122
- "my-topic": {
123
- "my-consumer": async (message, metadata) => {
124
- // metadata includes: { messageId, deliveryCount, createdAt, topicName, consumerGroup }
125
- console.log("Processing message:", message);
126
-
127
- // If this throws an error, the message will be automatically retried
128
- await processMessage(message);
129
- },
130
- },
110
+ // app/api/queue/my-topic/route.ts
111
+ import { handleCallback } from "@vercel/queue/web";
131
112
 
132
- // Multiple consumers for different purposes
133
- "order-events": {
134
- fulfillment: async (order, metadata) => {
135
- await processOrder(order);
136
- },
137
- analytics: async (order, metadata) => {
138
- await trackOrder(order);
139
- },
140
- },
113
+ export const POST = handleCallback(async (message, metadata) => {
114
+ // metadata includes: { messageId, deliveryCount, createdAt, topicName, consumerGroup }
115
+ console.log("Processing message:", message);
116
+
117
+ // If this throws an error, the message will be automatically retried
118
+ await processMessage(message);
141
119
  });
142
120
  ```
143
121
 
144
- 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.
122
+ **Hono:**
145
123
 
146
- ##### Pages Router
124
+ ```typescript
125
+ import { Hono } from "hono";
126
+ import { handleCallback } from "@vercel/queue/web";
147
127
 
148
- For Next.js Pages Router, import from `@vercel/queue/nextjs/pages` to get a handler compatible with the Pages Router API (`NextApiRequest`/`NextApiResponse`):
128
+ const app = new Hono();
129
+
130
+ app.post(
131
+ "/api/queue",
132
+ handleCallback(async (message, metadata) => {
133
+ console.log("Processing:", message);
134
+ }),
135
+ );
136
+
137
+ export default app;
138
+ ```
139
+
140
+ For multiple topics/consumers, create separate route files:
149
141
 
150
142
  ```typescript
151
- // pages/api/queue.ts
152
- import { handleCallback } from "@vercel/queue/nextjs/pages";
143
+ // app/api/queue/orders/fulfillment/route.ts
144
+ import { handleCallback } from "@vercel/queue/web";
153
145
 
154
- export default handleCallback({
155
- "my-topic": {
156
- "my-consumer": async (message, metadata) => {
157
- console.log("Processing message:", message);
158
- await processMessage(message);
159
- },
160
- },
161
- "order-events": {
162
- fulfillment: async (order, metadata) => {
163
- await processOrder(order);
164
- },
165
- analytics: async (order, metadata) => {
166
- await trackOrder(order);
167
- },
168
- },
146
+ export const POST = handleCallback(async (order, metadata) => {
147
+ await processOrder(order);
169
148
  });
170
149
  ```
171
150
 
172
- The `/nextjs/pages` subpath export automatically adapts the handler to work with the Pages Router API.
151
+ ```typescript
152
+ // app/api/queue/orders/analytics/route.ts
153
+ import { handleCallback } from "@vercel/queue/web";
173
154
 
174
- #### 2. Configure vercel.json
155
+ export const POST = handleCallback(async (order, metadata) => {
156
+ await trackOrder(order);
157
+ });
158
+ ```
175
159
 
176
- Configure which topics and consumers your API route handles.
160
+ ##### Pages Router (`@vercel/queue/nextjs/pages`)
177
161
 
178
- **For App Router:**
162
+ For Next.js Pages Router, import from `@vercel/queue/nextjs/pages`. This returns a `(req, res) => Promise<void>` handler:
179
163
 
180
- ```json
181
- {
182
- "functions": {
183
- "app/api/queue/route.ts": {
184
- "experimentalTriggers": [
185
- {
186
- "type": "queue/v1beta",
187
- "topic": "my-topic",
188
- "consumer": "my-consumer",
189
- "retryAfterSeconds": 60,
190
- "initialDelaySeconds": 0
191
- },
192
- {
193
- "type": "queue/v1beta",
194
- "topic": "order-events",
195
- "consumer": "fulfillment"
196
- },
197
- {
198
- "type": "queue/v1beta",
199
- "topic": "order-events",
200
- "consumer": "analytics",
201
- "retryAfterSeconds": 300
202
- }
203
- ]
204
- }
205
- }
206
- }
164
+ ```typescript
165
+ // pages/api/queue/my-topic.ts
166
+ import { handleCallback } from "@vercel/queue/nextjs/pages";
167
+
168
+ export default handleCallback(async (message, metadata) => {
169
+ console.log("Processing message:", message);
170
+ await processMessage(message);
171
+ });
207
172
  ```
208
173
 
209
- **For Pages Router:**
174
+ #### 2. Configure vercel.json
175
+
176
+ Configure which topics and consumers your API routes handle.
210
177
 
211
178
  ```json
212
179
  {
213
180
  "functions": {
214
- "pages/api/queue.ts": {
181
+ "app/api/queue/my-topic/route.ts": {
215
182
  "experimentalTriggers": [
216
183
  {
217
- "type": "queue/v1beta",
184
+ "type": "queue/v2beta",
218
185
  "topic": "my-topic",
219
186
  "consumer": "my-consumer",
220
187
  "retryAfterSeconds": 60,
221
188
  "initialDelaySeconds": 0
222
- },
189
+ }
190
+ ]
191
+ },
192
+ "app/api/queue/orders/fulfillment/route.ts": {
193
+ "experimentalTriggers": [
223
194
  {
224
- "type": "queue/v1beta",
195
+ "type": "queue/v2beta",
225
196
  "topic": "order-events",
226
197
  "consumer": "fulfillment"
227
- },
198
+ }
199
+ ]
200
+ },
201
+ "app/api/queue/orders/analytics/route.ts": {
202
+ "experimentalTriggers": [
228
203
  {
229
- "type": "queue/v1beta",
204
+ "type": "queue/v2beta",
230
205
  "topic": "order-events",
231
206
  "consumer": "analytics",
232
207
  "retryAfterSeconds": 300
@@ -246,47 +221,48 @@ Configure which topics and consumers your API route handles.
246
221
  - **Automatic Triggering**: Vercel triggers your API routes when messages are available
247
222
  - **Message Processing**: Your API routes receive message metadata via headers
248
223
  - **Configuration**: The `vercel.json` file tells Vercel which routes handle which topics/consumers
224
+ - **Delivery Modes**: The server uses CloudEvents binary content mode to deliver messages. For small messages, the full payload and receipt handle are pushed directly in the HTTP body and headers, avoiding an extra API fetch. For large messages, only the message ID is sent and the SDK fetches the payload.
249
225
 
250
226
  ## Advanced Features
251
227
 
252
- ### Client Class
228
+ ### Custom Client Configuration
253
229
 
254
- For custom configuration (tokens, headers, etc.), use the `Client` class:
230
+ For custom configuration (tokens, headers, transport), create a `QueueClient` and pass it via options:
255
231
 
256
232
  ```typescript
257
- import { Client } from "@vercel/queue";
233
+ import { QueueClient, send } from "@vercel/queue";
234
+ import { handleCallback } from "@vercel/queue/web";
258
235
 
259
- const client = new Client({
260
- token: "my-token", // Optional: custom auth token
261
- headers: { "X-Custom": "header" }, // Optional: custom headers
262
- pinToDeployment: false, // Optional: disable deployment pinning (default: true)
236
+ const client = new QueueClient({
237
+ token: "my-token",
238
+ headers: { "X-Custom": "header" },
263
239
  });
264
240
 
265
- // Send a message
266
- await client.send("my-topic", { hello: "world" });
241
+ // Send with custom client
242
+ await send("my-topic", { hello: "world" }, { client });
267
243
 
268
- // Handle callbacks using the same client
269
- export const POST = client.handleCallback({
270
- "my-topic": {
271
- "my-group": async (msg, meta) => console.log(msg),
272
- },
244
+ // Handle callbacks with custom client
245
+ export const POST = handleCallback(async (msg, meta) => console.log(msg), {
246
+ client,
273
247
  });
274
248
  ```
275
249
 
276
- ### Parsing Callback Requests
250
+ ### Core Handler (Framework Agnostic)
277
251
 
278
- For custom webhook handling, use `parseCallback` to extract queue information from CloudEvent requests:
252
+ For custom framework integration, use the core `handleCallback` from `@vercel/queue`. It takes parsed request data and throws on errors:
279
253
 
280
254
  ```typescript
281
- import { parseCallback } from "@vercel/queue";
282
-
283
- export async function POST(request: Request) {
284
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
285
-
286
- // Use the parsed information for custom processing...
287
- await myWorkflow.handleWebhook(queueName, consumerGroup, messageId);
255
+ import { handleCallback, parseRawCallback } from "@vercel/queue";
288
256
 
289
- return Response.json({ status: "success" });
257
+ // In your framework handler:
258
+ const parsed = parseRawCallback(body, headers);
259
+ try {
260
+ await handleCallback(async (msg, meta) => {
261
+ console.log("Processing:", msg);
262
+ }, parsed);
263
+ // success
264
+ } catch (error) {
265
+ // handle error → 500
290
266
  }
291
267
  ```
292
268
 
@@ -332,11 +308,41 @@ await send("json-topic", { data: "example" }, { transport });
332
308
  | Large payloads | StreamTransport | Very Low | Medium |
333
309
  | Real-time streams | StreamTransport | Very Low | High |
334
310
 
311
+ ## Handling Empty Queues
312
+
313
+ When no messages are available in the queue, the handler receives `null` for both the message and metadata parameters. This allows graceful handling without exceptions:
314
+
315
+ ```typescript
316
+ await receive("my-topic", "my-consumer", async (message, metadata) => {
317
+ if (!message) {
318
+ console.log("No message received - queue is empty");
319
+ return;
320
+ }
321
+
322
+ // Process the message
323
+ console.log("Processing:", message);
324
+ console.log("Message ID:", metadata.messageId);
325
+ });
326
+ ```
327
+
328
+ The same pattern works with `handleCallback`:
329
+
330
+ ```typescript
331
+ import { handleCallback } from "@vercel/queue/web";
332
+
333
+ export const POST = handleCallback(async (message, metadata) => {
334
+ if (!message) {
335
+ // No message available - handle gracefully
336
+ return;
337
+ }
338
+ await processMessage(message);
339
+ });
340
+ ```
341
+
335
342
  ## Error Handling
336
343
 
337
344
  The queue client provides specific error types:
338
345
 
339
- - **`QueueEmptyError`**: No messages available in the queue
340
346
  - **`MessageLockedError`**: Message is being processed by another consumer
341
347
  - **`MessageNotFoundError`**: Message doesn't exist or has expired
342
348
  - **`MessageNotAvailableError`**: Message exists but cannot be claimed
@@ -346,7 +352,6 @@ The queue client provides specific error types:
346
352
  - **`UnauthorizedError`**: Authentication failed (invalid or missing token)
347
353
  - **`ForbiddenError`**: Access denied (wrong environment or project)
348
354
  - **`DuplicateMessageError`**: Idempotency key was already used
349
- - **`ConcurrencyLimitError`**: Too many in-flight messages
350
355
  - **`ConsumerDiscoveryError`**: Could not reach the consumer deployment
351
356
  - **`ConsumerRegistryNotConfiguredError`**: Project not configured for queues
352
357
  - **`InternalServerError`**: Unexpected server error
@@ -357,7 +362,6 @@ Example error handling:
357
362
  ```typescript
358
363
  import {
359
364
  BadRequestError,
360
- ConcurrencyLimitError,
361
365
  DuplicateMessageError,
362
366
  ForbiddenError,
363
367
  InternalServerError,
@@ -375,13 +379,6 @@ try {
375
379
  console.log("Invalid parameters:", error.message);
376
380
  } else if (error instanceof DuplicateMessageError) {
377
381
  console.log("Duplicate message:", error.idempotencyKey);
378
- } else if (error instanceof ConcurrencyLimitError) {
379
- console.log(
380
- "Rate limited:",
381
- error.currentInflight,
382
- "/",
383
- error.maxConcurrency,
384
- );
385
382
  } else if (error instanceof InternalServerError) {
386
383
  console.log("Server error - retry with backoff");
387
384
  }
@@ -408,26 +405,32 @@ The following environment variables can be used to configure the queue client:
408
405
  ```typescript
409
406
  import { receive } from "@vercel/queue";
410
407
 
411
- // Process next available message
412
- await receive<T>(topicName, consumerGroup, handler);
408
+ // Process next available message (or null if queue is empty)
409
+ await receive<T>(topicName, consumerGroup, async (message, metadata) => {
410
+ if (!message) {
411
+ console.log("Queue is empty");
412
+ return;
413
+ }
414
+ // Process message
415
+ });
413
416
 
414
- // Process specific message by ID
417
+ // Batch processing: fetch up to 10 messages in one request
415
418
  await receive<T>(topicName, consumerGroup, handler, {
416
- messageId: "message-id",
419
+ limit: 10, // Default: 1, Min: 1, Max: 10
417
420
  });
418
421
 
419
- // Process message with options
422
+ // Process specific message by ID
420
423
  await receive<T>(topicName, consumerGroup, handler, {
421
- messageId: "message-id", // Optional: process specific message by ID
422
- transport: new JsonTransport(), // Optional: custom transport (defaults to JsonTransport)
423
- visibilityTimeoutSeconds: 30, // Optional: message visibility timeout
424
- visibilityRefreshInterval: 10, // Optional: how often to refresh the lock
424
+ messageId: "message-id",
425
425
  });
426
426
 
427
+ // Note: limit and messageId are mutually exclusive options
428
+
427
429
  // Handler function signature
430
+ // When queue is empty, both message and metadata are null
428
431
  type MessageHandler<T = unknown> = (
429
- message: T,
430
- metadata: MessageMetadata,
432
+ message: T | null,
433
+ metadata: MessageMetadata | null,
431
434
  ) => Promise<void> | void;
432
435
 
433
436
  // MessageMetadata type
@@ -446,7 +449,7 @@ interface MessageMetadata {
446
449
 
447
450
  | Limit | Value | Notes |
448
451
  | --------------------------- | --------------------- | ----------------------------------- |
449
- | Message throughput | 10,000s msg/sec/topic | Scales horizontally |
452
+ | Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
450
453
  | Payload size | 1 GB | Smaller messages have lower latency |
451
454
  | Number of topics | Unlimited | No hard limit |
452
455
  | Consumer groups per message | ~4,000 | Per-message limit |
@@ -464,11 +467,10 @@ interface MessageMetadata {
464
467
 
465
468
  #### Receiving Messages
466
469
 
467
- | Parameter | Default | Min | Max | Notes |
468
- | -------------------------- | --------- | --- | ------ | --------------------------- |
469
- | `visibilityTimeoutSeconds` | 30 | 0 | 3,600 | 0 = immediate re-visibility |
470
- | `limit` | 1 | 1 | 10 | Messages per request |
471
- | `maxConcurrency` | unlimited | 1 | 10,000 | In-flight message limit |
470
+ | Parameter | Default | Min | Max | Notes |
471
+ | -------------------------- | ------- | --- | ----- | --------------------------- |
472
+ | `visibilityTimeoutSeconds` | 30 | 0 | 3,600 | 0 = immediate re-visibility |
473
+ | `limit` | 1 | 1 | 10 | Messages per request |
472
474
 
473
475
  #### Visibility Extension
474
476
 
@@ -505,7 +507,7 @@ Topic patterns support wildcards for flexible routing:
505
507
  "app/api/queue/route.ts": {
506
508
  "experimentalTriggers": [
507
509
  {
508
- "type": "queue/v1beta",
510
+ "type": "queue/v2beta",
509
511
  "topic": "user-*",
510
512
  "consumer": "processor"
511
513
  }
@@ -524,12 +526,28 @@ Topic patterns support wildcards for flexible routing:
524
526
 
525
527
  ## API Reference
526
528
 
527
- ### Client Configuration
529
+ ### Export Structure
530
+
531
+ | Import Path | `handleCallback` |
532
+ | ---------------------------- | ---------------------------------------------------------------- |
533
+ | `@vercel/queue` | Core async function: `(handler, parsed, opts?) => Promise<void>` |
534
+ | `@vercel/queue/web` | Returns `(request: Request) => Promise<Response>` |
535
+ | `@vercel/queue/nextjs/pages` | Returns `(req, res) => Promise<void>` |
536
+
537
+ Additional exports from `@vercel/queue`:
538
+
539
+ | Export | Description |
540
+ | ------------------------- | ------------------------------------------------------------- |
541
+ | `parseCallback` | Parse a Web API `Request` into a `ParsedCallbackRequest` |
542
+ | `parseRawCallback` | Parse a pre-parsed body + headers (e.g. Pages Router) |
543
+ | `CLOUD_EVENT_TYPE_V2BETA` | `"com.vercel.queue.v2beta"` — binary CloudEvent type constant |
544
+
545
+ ### QueueClient Configuration
528
546
 
529
547
  ```typescript
530
- import { Client } from "@vercel/queue";
548
+ import { QueueClient } from "@vercel/queue";
531
549
 
532
- const client = new Client({
550
+ const client = new QueueClient({
533
551
  // Base URL for the queue service
534
552
  // Default: "https://vercel-queue.com"
535
553
  // Env: VERCEL_QUEUE_BASE_URL
@@ -554,6 +572,10 @@ const client = new Client({
554
572
  // Default: true
555
573
  pinToDeployment: true,
556
574
  });
575
+
576
+ // Pass to any function via options
577
+ await send("my-topic", payload, { client });
578
+ export const POST = handleCallback(handler, { client });
557
579
  ```
558
580
 
559
581
  ### Send Options
@@ -579,62 +601,70 @@ await send("my-topic", payload, {
579
601
 
580
602
  ### Receive Options
581
603
 
604
+ The `receive` function supports two mutually exclusive modes:
605
+
582
606
  ```typescript
607
+ // Batch mode: receive multiple messages
583
608
  await receive("my-topic", "my-consumer", handler, {
584
- // Specific message ID to consume (optional)
585
- messageId: "0-1",
609
+ // Maximum messages to retrieve in a single request
610
+ // Default: 1, Min: 1, Max: 10
611
+ limit: 10,
586
612
 
587
613
  // Message lock duration in seconds
588
- // Default: 30, Min: 0, Max: 3600
614
+ // Default: 300, Min: 30, Max: 3600
589
615
  visibilityTimeoutSeconds: 60,
590
-
591
- // How often to refresh the lock during processing
592
- // Default: visibilityTimeoutSeconds / 3
593
- visibilityRefreshInterval: 15,
594
-
595
- // Custom deserializer (default: JsonTransport)
596
- transport: new JsonTransport(),
597
616
  });
598
- ```
599
-
600
- ### Receive Options (Advanced)
601
617
 
602
- ```typescript
618
+ // By-ID mode: receive a specific message
603
619
  await receive("my-topic", "my-consumer", handler, {
604
- // Payload deserializer
605
- // Default: JsonTransport
606
- transport: new JsonTransport(),
620
+ // Specific message ID to consume
621
+ messageId: "0-1",
607
622
 
608
- // Message lock duration
609
- // Default: 30, Min: 0, Max: 3600
623
+ // Message lock duration in seconds
624
+ // Default: 300, Min: 30, Max: 3600
610
625
  visibilityTimeoutSeconds: 60,
611
-
612
- // How often to refresh the lock during processing
613
- // Default: visibilityTimeoutSeconds / 3
614
- visibilityRefreshInterval: 20,
615
626
  });
616
627
  ```
617
628
 
629
+ > **Note**: `limit` and `messageId` cannot be used together - they are mutually exclusive options.
630
+
618
631
  ### handleCallback Options
619
632
 
620
633
  ```typescript
634
+ import { handleCallback } from "@vercel/queue/web";
635
+
621
636
  export const POST = handleCallback(
622
- {
623
- "my-topic": {
624
- "my-consumer": async (message, metadata) => {
625
- await processMessage(message);
626
- },
627
- },
637
+ async (message, metadata) => {
638
+ await processMessage(message);
628
639
  },
629
640
  {
630
641
  // Message lock duration for long-running handlers
631
- // Default: 30, Min: 0, Max: 3600
632
- // visibilityRefreshInterval defaults to visibilityTimeoutSeconds / 3
642
+ // Default: 300, Min: 30, Max: 3600
633
643
  visibilityTimeoutSeconds: 300, // 5 minutes
634
644
  },
635
645
  );
636
646
  ```
637
647
 
648
+ ### Core handleCallback
649
+
650
+ The core `handleCallback` is an async function that takes already-parsed request data. Use it to build custom framework integrations:
651
+
652
+ ```typescript
653
+ import { handleCallback, parseCallback, parseRawCallback } from "@vercel/queue";
654
+
655
+ // Web API Request
656
+ const parsed = await parseCallback(request);
657
+
658
+ // Or, for frameworks that pre-parse the body (e.g. Pages Router)
659
+ const parsed = parseRawCallback(req.body, req.headers);
660
+
661
+ try {
662
+ await handleCallback(handler, parsed);
663
+ } catch (error) {
664
+ // handle error → 500
665
+ }
666
+ ```
667
+
638
668
  ## License
639
669
 
640
670
  MIT