@vercel/queue 0.0.0-alpha.37 → 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
@@ -399,26 +405,32 @@ The following environment variables can be used to configure the queue client:
399
405
  ```typescript
400
406
  import { receive } from "@vercel/queue";
401
407
 
402
- // Process next available message
403
- 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
+ });
404
416
 
405
- // Process specific message by ID
417
+ // Batch processing: fetch up to 10 messages in one request
406
418
  await receive<T>(topicName, consumerGroup, handler, {
407
- messageId: "message-id",
419
+ limit: 10, // Default: 1, Min: 1, Max: 10
408
420
  });
409
421
 
410
- // Process message with options
422
+ // Process specific message by ID
411
423
  await receive<T>(topicName, consumerGroup, handler, {
412
- messageId: "message-id", // Optional: process specific message by ID
413
- transport: new JsonTransport(), // Optional: custom transport (defaults to JsonTransport)
414
- visibilityTimeoutSeconds: 30, // Optional: message visibility timeout
415
- visibilityRefreshInterval: 10, // Optional: how often to refresh the lock
424
+ messageId: "message-id",
416
425
  });
417
426
 
427
+ // Note: limit and messageId are mutually exclusive options
428
+
418
429
  // Handler function signature
430
+ // When queue is empty, both message and metadata are null
419
431
  type MessageHandler<T = unknown> = (
420
- message: T,
421
- metadata: MessageMetadata,
432
+ message: T | null,
433
+ metadata: MessageMetadata | null,
422
434
  ) => Promise<void> | void;
423
435
 
424
436
  // MessageMetadata type
@@ -437,7 +449,7 @@ interface MessageMetadata {
437
449
 
438
450
  | Limit | Value | Notes |
439
451
  | --------------------------- | --------------------- | ----------------------------------- |
440
- | Message throughput | 10,000s msg/sec/topic | Scales horizontally |
452
+ | Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
441
453
  | Payload size | 1 GB | Smaller messages have lower latency |
442
454
  | Number of topics | Unlimited | No hard limit |
443
455
  | Consumer groups per message | ~4,000 | Per-message limit |
@@ -495,7 +507,7 @@ Topic patterns support wildcards for flexible routing:
495
507
  "app/api/queue/route.ts": {
496
508
  "experimentalTriggers": [
497
509
  {
498
- "type": "queue/v1beta",
510
+ "type": "queue/v2beta",
499
511
  "topic": "user-*",
500
512
  "consumer": "processor"
501
513
  }
@@ -514,12 +526,28 @@ Topic patterns support wildcards for flexible routing:
514
526
 
515
527
  ## API Reference
516
528
 
517
- ### 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
518
546
 
519
547
  ```typescript
520
- import { Client } from "@vercel/queue";
548
+ import { QueueClient } from "@vercel/queue";
521
549
 
522
- const client = new Client({
550
+ const client = new QueueClient({
523
551
  // Base URL for the queue service
524
552
  // Default: "https://vercel-queue.com"
525
553
  // Env: VERCEL_QUEUE_BASE_URL
@@ -544,6 +572,10 @@ const client = new Client({
544
572
  // Default: true
545
573
  pinToDeployment: true,
546
574
  });
575
+
576
+ // Pass to any function via options
577
+ await send("my-topic", payload, { client });
578
+ export const POST = handleCallback(handler, { client });
547
579
  ```
548
580
 
549
581
  ### Send Options
@@ -569,62 +601,70 @@ await send("my-topic", payload, {
569
601
 
570
602
  ### Receive Options
571
603
 
604
+ The `receive` function supports two mutually exclusive modes:
605
+
572
606
  ```typescript
607
+ // Batch mode: receive multiple messages
573
608
  await receive("my-topic", "my-consumer", handler, {
574
- // Specific message ID to consume (optional)
575
- messageId: "0-1",
609
+ // Maximum messages to retrieve in a single request
610
+ // Default: 1, Min: 1, Max: 10
611
+ limit: 10,
576
612
 
577
613
  // Message lock duration in seconds
578
- // Default: 30, Min: 0, Max: 3600
614
+ // Default: 300, Min: 30, Max: 3600
579
615
  visibilityTimeoutSeconds: 60,
580
-
581
- // How often to refresh the lock during processing
582
- // Default: visibilityTimeoutSeconds / 3
583
- visibilityRefreshInterval: 15,
584
-
585
- // Custom deserializer (default: JsonTransport)
586
- transport: new JsonTransport(),
587
616
  });
588
- ```
589
-
590
- ### Receive Options (Advanced)
591
617
 
592
- ```typescript
618
+ // By-ID mode: receive a specific message
593
619
  await receive("my-topic", "my-consumer", handler, {
594
- // Payload deserializer
595
- // Default: JsonTransport
596
- transport: new JsonTransport(),
620
+ // Specific message ID to consume
621
+ messageId: "0-1",
597
622
 
598
- // Message lock duration
599
- // Default: 30, Min: 0, Max: 3600
623
+ // Message lock duration in seconds
624
+ // Default: 300, Min: 30, Max: 3600
600
625
  visibilityTimeoutSeconds: 60,
601
-
602
- // How often to refresh the lock during processing
603
- // Default: visibilityTimeoutSeconds / 3
604
- visibilityRefreshInterval: 20,
605
626
  });
606
627
  ```
607
628
 
629
+ > **Note**: `limit` and `messageId` cannot be used together - they are mutually exclusive options.
630
+
608
631
  ### handleCallback Options
609
632
 
610
633
  ```typescript
634
+ import { handleCallback } from "@vercel/queue/web";
635
+
611
636
  export const POST = handleCallback(
612
- {
613
- "my-topic": {
614
- "my-consumer": async (message, metadata) => {
615
- await processMessage(message);
616
- },
617
- },
637
+ async (message, metadata) => {
638
+ await processMessage(message);
618
639
  },
619
640
  {
620
641
  // Message lock duration for long-running handlers
621
- // Default: 30, Min: 0, Max: 3600
622
- // visibilityRefreshInterval defaults to visibilityTimeoutSeconds / 3
642
+ // Default: 300, Min: 30, Max: 3600
623
643
  visibilityTimeoutSeconds: 300, // 5 minutes
624
644
  },
625
645
  );
626
646
  ```
627
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
+
628
668
  ## License
629
669
 
630
670
  MIT