@vercel/queue 0.0.0-alpha.37 → 0.0.0-alpha.39

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,126 +100,107 @@ 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`)
104
+
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.
113
106
 
114
- The recommended approach is to handle multiple topics and consumers in a single API route to keep your `vercel.json` configuration simple:
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";
127
+
128
+ const app = new Hono();
129
+
130
+ app.post(
131
+ "/api/queue",
132
+ handleCallback(async (message, metadata) => {
133
+ console.log("Processing:", message);
134
+ }),
135
+ );
147
136
 
148
- For Next.js Pages Router, import from `@vercel/queue/nextjs/pages` to get a handler compatible with the Pages Router API (`NextApiRequest`/`NextApiResponse`):
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
- "consumer": "my-consumer",
220
186
  "retryAfterSeconds": 60,
221
187
  "initialDelaySeconds": 0
222
- },
188
+ }
189
+ ]
190
+ },
191
+ "app/api/queue/orders/fulfillment/route.ts": {
192
+ "experimentalTriggers": [
223
193
  {
224
- "type": "queue/v1beta",
225
- "topic": "order-events",
226
- "consumer": "fulfillment"
227
- },
194
+ "type": "queue/v2beta",
195
+ "topic": "order-events"
196
+ }
197
+ ]
198
+ },
199
+ "app/api/queue/orders/analytics/route.ts": {
200
+ "experimentalTriggers": [
228
201
  {
229
- "type": "queue/v1beta",
202
+ "type": "queue/v2beta",
230
203
  "topic": "order-events",
231
- "consumer": "analytics",
232
204
  "retryAfterSeconds": 300
233
205
  }
234
206
  ]
@@ -246,47 +218,48 @@ Configure which topics and consumers your API route handles.
246
218
  - **Automatic Triggering**: Vercel triggers your API routes when messages are available
247
219
  - **Message Processing**: Your API routes receive message metadata via headers
248
220
  - **Configuration**: The `vercel.json` file tells Vercel which routes handle which topics/consumers
221
+ - **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
222
 
250
223
  ## Advanced Features
251
224
 
252
- ### Client Class
225
+ ### Custom Client Configuration
253
226
 
254
- For custom configuration (tokens, headers, etc.), use the `Client` class:
227
+ For custom configuration (tokens, headers, transport), create a `QueueClient` and pass it via options:
255
228
 
256
229
  ```typescript
257
- import { Client } from "@vercel/queue";
230
+ import { QueueClient, send } from "@vercel/queue";
231
+ import { handleCallback } from "@vercel/queue/web";
258
232
 
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)
233
+ const client = new QueueClient({
234
+ token: "my-token",
235
+ headers: { "X-Custom": "header" },
263
236
  });
264
237
 
265
- // Send a message
266
- await client.send("my-topic", { hello: "world" });
238
+ // Send with custom client
239
+ await send("my-topic", { hello: "world" }, { client });
267
240
 
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
- },
241
+ // Handle callbacks with custom client
242
+ export const POST = handleCallback(async (msg, meta) => console.log(msg), {
243
+ client,
273
244
  });
274
245
  ```
275
246
 
276
- ### Parsing Callback Requests
247
+ ### Core Handler (Framework Agnostic)
277
248
 
278
- For custom webhook handling, use `parseCallback` to extract queue information from CloudEvent requests:
249
+ For custom framework integration, use the core `handleCallback` from `@vercel/queue`. It takes parsed request data and throws on errors:
279
250
 
280
251
  ```typescript
281
- import { parseCallback } from "@vercel/queue";
252
+ import { handleCallback, parseRawCallback } from "@vercel/queue";
282
253
 
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);
288
-
289
- return Response.json({ status: "success" });
254
+ // In your framework handler:
255
+ const parsed = parseRawCallback(body, headers);
256
+ try {
257
+ await handleCallback(async (msg, meta) => {
258
+ console.log("Processing:", msg);
259
+ }, parsed);
260
+ // success
261
+ } catch (error) {
262
+ // handle error → 500
290
263
  }
291
264
  ```
292
265
 
@@ -332,11 +305,41 @@ await send("json-topic", { data: "example" }, { transport });
332
305
  | Large payloads | StreamTransport | Very Low | Medium |
333
306
  | Real-time streams | StreamTransport | Very Low | High |
334
307
 
308
+ ## Handling Empty Queues
309
+
310
+ 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:
311
+
312
+ ```typescript
313
+ await receive("my-topic", "my-consumer", async (message, metadata) => {
314
+ if (!message) {
315
+ console.log("No message received - queue is empty");
316
+ return;
317
+ }
318
+
319
+ // Process the message
320
+ console.log("Processing:", message);
321
+ console.log("Message ID:", metadata.messageId);
322
+ });
323
+ ```
324
+
325
+ The same pattern works with `handleCallback`:
326
+
327
+ ```typescript
328
+ import { handleCallback } from "@vercel/queue/web";
329
+
330
+ export const POST = handleCallback(async (message, metadata) => {
331
+ if (!message) {
332
+ // No message available - handle gracefully
333
+ return;
334
+ }
335
+ await processMessage(message);
336
+ });
337
+ ```
338
+
335
339
  ## Error Handling
336
340
 
337
341
  The queue client provides specific error types:
338
342
 
339
- - **`QueueEmptyError`**: No messages available in the queue
340
343
  - **`MessageLockedError`**: Message is being processed by another consumer
341
344
  - **`MessageNotFoundError`**: Message doesn't exist or has expired
342
345
  - **`MessageNotAvailableError`**: Message exists but cannot be claimed
@@ -399,26 +402,32 @@ The following environment variables can be used to configure the queue client:
399
402
  ```typescript
400
403
  import { receive } from "@vercel/queue";
401
404
 
402
- // Process next available message
403
- await receive<T>(topicName, consumerGroup, handler);
405
+ // Process next available message (or null if queue is empty)
406
+ await receive<T>(topicName, consumerGroup, async (message, metadata) => {
407
+ if (!message) {
408
+ console.log("Queue is empty");
409
+ return;
410
+ }
411
+ // Process message
412
+ });
404
413
 
405
- // Process specific message by ID
414
+ // Batch processing: fetch up to 10 messages in one request
406
415
  await receive<T>(topicName, consumerGroup, handler, {
407
- messageId: "message-id",
416
+ limit: 10, // Default: 1, Min: 1, Max: 10
408
417
  });
409
418
 
410
- // Process message with options
419
+ // Process specific message by ID
411
420
  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
421
+ messageId: "message-id",
416
422
  });
417
423
 
424
+ // Note: limit and messageId are mutually exclusive options
425
+
418
426
  // Handler function signature
427
+ // When queue is empty, both message and metadata are null
419
428
  type MessageHandler<T = unknown> = (
420
- message: T,
421
- metadata: MessageMetadata,
429
+ message: T | null,
430
+ metadata: MessageMetadata | null,
422
431
  ) => Promise<void> | void;
423
432
 
424
433
  // MessageMetadata type
@@ -437,7 +446,7 @@ interface MessageMetadata {
437
446
 
438
447
  | Limit | Value | Notes |
439
448
  | --------------------------- | --------------------- | ----------------------------------- |
440
- | Message throughput | 10,000s msg/sec/topic | Scales horizontally |
449
+ | Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
441
450
  | Payload size | 1 GB | Smaller messages have lower latency |
442
451
  | Number of topics | Unlimited | No hard limit |
443
452
  | Consumer groups per message | ~4,000 | Per-message limit |
@@ -495,9 +504,8 @@ Topic patterns support wildcards for flexible routing:
495
504
  "app/api/queue/route.ts": {
496
505
  "experimentalTriggers": [
497
506
  {
498
- "type": "queue/v1beta",
499
- "topic": "user-*",
500
- "consumer": "processor"
507
+ "type": "queue/v2beta",
508
+ "topic": "user-*"
501
509
  }
502
510
  ]
503
511
  }
@@ -514,12 +522,28 @@ Topic patterns support wildcards for flexible routing:
514
522
 
515
523
  ## API Reference
516
524
 
517
- ### Client Configuration
525
+ ### Export Structure
526
+
527
+ | Import Path | `handleCallback` |
528
+ | ---------------------------- | ---------------------------------------------------------------- |
529
+ | `@vercel/queue` | Core async function: `(handler, parsed, opts?) => Promise<void>` |
530
+ | `@vercel/queue/web` | Returns `(request: Request) => Promise<Response>` |
531
+ | `@vercel/queue/nextjs/pages` | Returns `(req, res) => Promise<void>` |
532
+
533
+ Additional exports from `@vercel/queue`:
534
+
535
+ | Export | Description |
536
+ | ------------------------- | ------------------------------------------------------------- |
537
+ | `parseCallback` | Parse a Web API `Request` into a `ParsedCallbackRequest` |
538
+ | `parseRawCallback` | Parse a pre-parsed body + headers (e.g. Pages Router) |
539
+ | `CLOUD_EVENT_TYPE_V2BETA` | `"com.vercel.queue.v2beta"` — binary CloudEvent type constant |
540
+
541
+ ### QueueClient Configuration
518
542
 
519
543
  ```typescript
520
- import { Client } from "@vercel/queue";
544
+ import { QueueClient } from "@vercel/queue";
521
545
 
522
- const client = new Client({
546
+ const client = new QueueClient({
523
547
  // Base URL for the queue service
524
548
  // Default: "https://vercel-queue.com"
525
549
  // Env: VERCEL_QUEUE_BASE_URL
@@ -544,6 +568,10 @@ const client = new Client({
544
568
  // Default: true
545
569
  pinToDeployment: true,
546
570
  });
571
+
572
+ // Pass to any function via options
573
+ await send("my-topic", payload, { client });
574
+ export const POST = handleCallback(handler, { client });
547
575
  ```
548
576
 
549
577
  ### Send Options
@@ -569,62 +597,70 @@ await send("my-topic", payload, {
569
597
 
570
598
  ### Receive Options
571
599
 
600
+ The `receive` function supports two mutually exclusive modes:
601
+
572
602
  ```typescript
603
+ // Batch mode: receive multiple messages
573
604
  await receive("my-topic", "my-consumer", handler, {
574
- // Specific message ID to consume (optional)
575
- messageId: "0-1",
605
+ // Maximum messages to retrieve in a single request
606
+ // Default: 1, Min: 1, Max: 10
607
+ limit: 10,
576
608
 
577
609
  // Message lock duration in seconds
578
- // Default: 30, Min: 0, Max: 3600
610
+ // Default: 300, Min: 30, Max: 3600
579
611
  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
612
  });
588
- ```
589
613
 
590
- ### Receive Options (Advanced)
591
-
592
- ```typescript
614
+ // By-ID mode: receive a specific message
593
615
  await receive("my-topic", "my-consumer", handler, {
594
- // Payload deserializer
595
- // Default: JsonTransport
596
- transport: new JsonTransport(),
616
+ // Specific message ID to consume
617
+ messageId: "0-1",
597
618
 
598
- // Message lock duration
599
- // Default: 30, Min: 0, Max: 3600
619
+ // Message lock duration in seconds
620
+ // Default: 300, Min: 30, Max: 3600
600
621
  visibilityTimeoutSeconds: 60,
601
-
602
- // How often to refresh the lock during processing
603
- // Default: visibilityTimeoutSeconds / 3
604
- visibilityRefreshInterval: 20,
605
622
  });
606
623
  ```
607
624
 
625
+ > **Note**: `limit` and `messageId` cannot be used together - they are mutually exclusive options.
626
+
608
627
  ### handleCallback Options
609
628
 
610
629
  ```typescript
630
+ import { handleCallback } from "@vercel/queue/web";
631
+
611
632
  export const POST = handleCallback(
612
- {
613
- "my-topic": {
614
- "my-consumer": async (message, metadata) => {
615
- await processMessage(message);
616
- },
617
- },
633
+ async (message, metadata) => {
634
+ await processMessage(message);
618
635
  },
619
636
  {
620
637
  // Message lock duration for long-running handlers
621
- // Default: 30, Min: 0, Max: 3600
622
- // visibilityRefreshInterval defaults to visibilityTimeoutSeconds / 3
638
+ // Default: 300, Min: 30, Max: 3600
623
639
  visibilityTimeoutSeconds: 300, // 5 minutes
624
640
  },
625
641
  );
626
642
  ```
627
643
 
644
+ ### Core handleCallback
645
+
646
+ The core `handleCallback` is an async function that takes already-parsed request data. Use it to build custom framework integrations:
647
+
648
+ ```typescript
649
+ import { handleCallback, parseCallback, parseRawCallback } from "@vercel/queue";
650
+
651
+ // Web API Request
652
+ const parsed = await parseCallback(request);
653
+
654
+ // Or, for frameworks that pre-parse the body (e.g. Pages Router)
655
+ const parsed = parseRawCallback(req.body, req.headers);
656
+
657
+ try {
658
+ await handleCallback(handler, parsed);
659
+ } catch (error) {
660
+ // handle error → 500
661
+ }
662
+ ```
663
+
628
664
  ## License
629
665
 
630
666
  MIT