@vercel/queue 0.0.0-alpha.33 → 0.0.0-alpha.35

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
@@ -18,11 +18,6 @@ A TypeScript client library for interacting with the Vercel Queue Service API, d
18
18
  npm install @vercel/queue
19
19
  ```
20
20
 
21
- The package includes:
22
-
23
- - **Main Library**: Queue client and utilities for production and development
24
- - **CLI Tool**: `npx vercel-queue-local-init` for local development handler initialization
25
-
26
21
  ## Quick Start
27
22
 
28
23
  For local development, you'll need to set up your Vercel project:
@@ -42,15 +37,7 @@ vc env pull
42
37
 
43
38
  **Queues just work locally.** After you have setup your Vercel project, when you `send()` messages in development mode, they automatically trigger your handlers locally - no external queue infrastructure needed.
44
39
 
45
- ### Next.js Lazy Loading
46
-
47
- For Next.js API routes (or others that are lazy-loaded), run this simple command to initialize handlers:
48
-
49
- ```bash
50
- npx vercel-queue-local-init
51
- ```
52
-
53
- That's it! The script reads your `vercel.json`, finds your queue handlers, and triggers Next.js to load them.
40
+ The library reads your `vercel.json` configuration, discovers your queue handlers, and triggers them automatically when messages are sent.
54
41
 
55
42
  ### Example Workflow
56
43
 
@@ -58,22 +45,9 @@ That's it! The script reads your `vercel.json`, finds your queue handlers, and t
58
45
  # Start your dev server
59
46
  npm run dev
60
47
 
61
- # Initialize handlers (only needed for frameworks that lazy load routes in dev)
62
- npx vercel-queue-local-init
63
-
64
48
  # Send messages - they process locally automatically!
65
49
  ```
66
50
 
67
- ### CLI Options
68
-
69
- ```bash
70
- # Custom port
71
- npx vercel-queue-local-init --port 3001
72
-
73
- # Different config file
74
- npx vercel-queue-local-init --config ./my-vercel.json
75
- ```
76
-
77
51
  ### TypeScript Configuration
78
52
 
79
53
  Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
@@ -107,6 +81,7 @@ await send(
107
81
  {
108
82
  idempotencyKey: "unique-key", // Optional: prevent duplicate messages
109
83
  retentionSeconds: 3600, // Optional: override retention time (defaults to 24 hours)
84
+ delaySeconds: 60, // Optional: delay message delivery by N seconds
110
85
  },
111
86
  );
112
87
  ```
@@ -146,7 +121,7 @@ export const POST = handleCallback({
146
121
  // Single topic with one consumer
147
122
  "my-topic": {
148
123
  "my-consumer": async (message, metadata) => {
149
- // metadata includes: { messageId, deliveryCount, createdAt }
124
+ // metadata includes: { messageId, deliveryCount, createdAt, topicName, consumerGroup }
150
125
  console.log("Processing message:", message);
151
126
 
152
127
  // If this throws an error, the message will be automatically retried
@@ -157,23 +132,10 @@ export const POST = handleCallback({
157
132
  // Multiple consumers for different purposes
158
133
  "order-events": {
159
134
  fulfillment: async (order, metadata) => {
160
- // By default, errors will trigger automatic retries
161
- // But you can control retry timing if needed:
162
- if (!isSystemReady()) {
163
- // Override default retry with a 5 minute delay
164
- return { timeoutSeconds: 300 };
165
- }
166
-
167
135
  await processOrder(order);
168
136
  },
169
137
  analytics: async (order, metadata) => {
170
- try {
171
- await trackOrder(order);
172
- } catch (error) {
173
- // Optional: Custom exponential backoff instead of default retry timing
174
- const timeoutSeconds = Math.pow(2, metadata.deliveryCount) * 60;
175
- return { timeoutSeconds };
176
- }
138
+ await trackOrder(order);
177
139
  },
178
140
  },
179
141
  });
@@ -198,9 +160,6 @@ export default handleCallback({
198
160
  },
199
161
  "order-events": {
200
162
  fulfillment: async (order, metadata) => {
201
- if (!isSystemReady()) {
202
- return { timeoutSeconds: 300 };
203
- }
204
163
  await processOrder(order);
205
164
  },
206
165
  analytics: async (order, metadata) => {
@@ -290,6 +249,47 @@ Configure which topics and consumers your API route handles.
290
249
 
291
250
  ## Advanced Features
292
251
 
252
+ ### Client Class
253
+
254
+ For custom configuration (tokens, headers, etc.), use the `Client` class:
255
+
256
+ ```typescript
257
+ import { Client } from "@vercel/queue";
258
+
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)
263
+ });
264
+
265
+ // Send a message
266
+ await client.send("my-topic", { hello: "world" });
267
+
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
+ },
273
+ });
274
+ ```
275
+
276
+ ### Parsing Callback Requests
277
+
278
+ For custom webhook handling, use `parseCallback` to extract queue information from CloudEvent requests:
279
+
280
+ ```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);
288
+
289
+ return Response.json({ status: "success" });
290
+ }
291
+ ```
292
+
293
293
  ### Serialization (Transport) System
294
294
 
295
295
  The queue client supports customizable serialization through the `Transport` interface:
@@ -314,36 +314,51 @@ await send(
314
314
  { data: "example" },
315
315
  { transport: new JsonTransport() },
316
316
  );
317
+
318
+ // JsonTransport with custom serialization
319
+ const transport = new JsonTransport({
320
+ replacer: (key, value) => (key === "password" ? undefined : value),
321
+ reviver: (key, value) => (key === "date" ? new Date(value) : value),
322
+ });
323
+ await send("json-topic", { data: "example" }, { transport });
317
324
  ```
318
325
 
319
326
  ### Transport Selection Guide
320
327
 
321
- | Use Case | Recommended Transport | Memory Usage | Performance |
322
- | -------------------- | --------------------- | ------------ | ----------- |
323
- | Small JSON objects | JsonTransport | Low | High |
324
- | Binary files < 100MB | BufferTransport | Medium | High |
325
- | Large files > 100MB | StreamTransport | Very Low | Medium |
326
- | Real-time streams | StreamTransport | Very Low | High |
328
+ | Use Case | Recommended Transport | Memory Usage | Performance |
329
+ | ------------------ | --------------------- | ------------ | ----------- |
330
+ | Small JSON objects | JsonTransport | Low | High |
331
+ | Binary data | BufferTransport | Medium | High |
332
+ | Large payloads | StreamTransport | Very Low | Medium |
333
+ | Real-time streams | StreamTransport | Very Low | High |
327
334
 
328
335
  ## Error Handling
329
336
 
330
337
  The queue client provides specific error types:
331
338
 
332
- - **`QueueEmptyError`**: No messages available (204)
333
- - **`MessageLockedError`**: Message temporarily locked (423)
334
- - **`MessageNotFoundError`**: Message doesn't exist (404)
335
- - **`MessageNotAvailableError`**: Message exists but unavailable (409)
336
- - **`MessageCorruptedError`**: Message data corrupted
337
- - **`BadRequestError`**: Invalid parameters (400)
338
- - **`UnauthorizedError`**: Authentication failure (401)
339
- - **`ForbiddenError`**: Access denied (403)
340
- - **`InternalServerError`**: Server errors (500+)
339
+ - **`QueueEmptyError`**: No messages available in the queue
340
+ - **`MessageLockedError`**: Message is being processed by another consumer
341
+ - **`MessageNotFoundError`**: Message doesn't exist or has expired
342
+ - **`MessageNotAvailableError`**: Message exists but cannot be claimed
343
+ - **`MessageAlreadyProcessedError`**: Message was already successfully processed
344
+ - **`MessageCorruptedError`**: Message data could not be parsed
345
+ - **`BadRequestError`**: Invalid request parameters
346
+ - **`UnauthorizedError`**: Authentication failed (invalid or missing token)
347
+ - **`ForbiddenError`**: Access denied (wrong environment or project)
348
+ - **`DuplicateMessageError`**: Idempotency key was already used
349
+ - **`ConcurrencyLimitError`**: Too many in-flight messages
350
+ - **`ConsumerDiscoveryError`**: Could not reach the consumer deployment
351
+ - **`ConsumerRegistryNotConfiguredError`**: Project not configured for queues
352
+ - **`InternalServerError`**: Unexpected server error
353
+ - **`InvalidLimitError`**: Batch limit outside valid range (1-10)
341
354
 
342
355
  Example error handling:
343
356
 
344
357
  ```typescript
345
358
  import {
346
359
  BadRequestError,
360
+ ConcurrencyLimitError,
361
+ DuplicateMessageError,
347
362
  ForbiddenError,
348
363
  InternalServerError,
349
364
  UnauthorizedError,
@@ -358,59 +373,131 @@ try {
358
373
  console.log("Environment mismatch - check configuration");
359
374
  } else if (error instanceof BadRequestError) {
360
375
  console.log("Invalid parameters:", error.message);
376
+ } else if (error instanceof DuplicateMessageError) {
377
+ 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
+ );
361
385
  } else if (error instanceof InternalServerError) {
362
386
  console.log("Server error - retry with backoff");
363
387
  }
364
388
  }
365
389
  ```
366
390
 
391
+ ## Environment Variables
392
+
393
+ The following environment variables can be used to configure the queue client:
394
+
395
+ | Variable | Description | Default |
396
+ | ------------------------ | ------------------------------------ | -------------------------- |
397
+ | `VERCEL_QUEUE_BASE_URL` | Override the queue service URL | `https://vercel-queue.com` |
398
+ | `VERCEL_QUEUE_BASE_PATH` | Override the API base path | `/api/v3/topic` |
399
+ | `VERCEL_QUEUE_DEBUG` | Enable debug logging (`1` or `true`) | - |
400
+ | `VERCEL_DEPLOYMENT_ID` | Deployment ID (auto-set by Vercel) | - |
401
+
367
402
  ## Advanced Usage
368
403
 
369
404
  ### Direct Message Processing
370
405
 
371
- > **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.
406
+ > **Note**: The `receive` function is for advanced use cases where you need direct message processing control outside of Vercel's automatic triggering.
372
407
 
373
408
  ```typescript
409
+ import { receive } from "@vercel/queue";
410
+
374
411
  // Process next available message
375
412
  await receive<T>(topicName, consumerGroup, handler);
376
413
 
377
414
  // Process specific message by ID
378
415
  await receive<T>(topicName, consumerGroup, handler, {
379
- messageId: "message-id"
416
+ messageId: "message-id",
380
417
  });
381
418
 
382
419
  // Process message with options
383
420
  await receive<T>(topicName, consumerGroup, handler, {
384
- messageId?: string; // Process specific message by ID
385
- skipPayload?: boolean; // Skip payload download (requires messageId)
386
- transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
387
- visibilityTimeoutSeconds?: number; // Message visibility timeout
388
- refreshInterval?: number; // Refresh interval for long-running operations
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
389
425
  });
390
426
 
391
427
  // Handler function signature
392
428
  type MessageHandler<T = unknown> = (
393
429
  message: T,
394
- metadata: MessageMetadata
395
- ) => Promise<MessageHandlerResult> | MessageHandlerResult;
396
-
397
- // Handler result types
398
- type MessageHandlerResult = void | MessageTimeoutResult;
399
-
400
- interface MessageTimeoutResult {
401
- timeoutSeconds: number; // seconds before message becomes available again
430
+ metadata: MessageMetadata,
431
+ ) => Promise<void> | void;
432
+
433
+ // MessageMetadata type
434
+ interface MessageMetadata {
435
+ messageId: string;
436
+ deliveryCount: number;
437
+ createdAt: Date;
438
+ topicName: string;
439
+ consumerGroup: string;
402
440
  }
403
441
  ```
404
442
 
405
- ## Limits
443
+ ## Service Limits & Constraints
444
+
445
+ ### Throughput & Storage
446
+
447
+ | Limit | Value | Notes |
448
+ | --------------------------- | --------------------- | ----------------------------------- |
449
+ | Message throughput | 10,000s msg/sec/topic | Scales horizontally |
450
+ | Payload size | 1 GB | Smaller messages have lower latency |
451
+ | Number of topics | Unlimited | No hard limit |
452
+ | Consumer groups per message | ~4,000 | Per-message limit |
453
+ | Messages per queue | Unlimited | No hard limit |
454
+
455
+ ### Parameter Constraints
456
+
457
+ #### Publishing Messages
458
+
459
+ | Parameter | Default | Min | Max | Notes |
460
+ | ------------------ | ------------ | --- | ----------- | ----------------------------------- |
461
+ | `retentionSeconds` | 86,400 (24h) | 60 | 86,400 | Message TTL |
462
+ | `delaySeconds` | 0 | 0 | ≤ retention | Cannot exceed retention |
463
+ | `idempotencyKey` | — | — | — | Dedup window: `min(retention, 24h)` |
406
464
 
407
- - **Message Throughput**: Each topic can handle up to 1,000 messages per second
408
- - **Payload Size**: Maximum payload size is 4.5MB (this limit will be increased soon)
409
- - **Number of Topics**: No limit on the number of topics you can create
465
+ #### Receiving Messages
410
466
 
411
- ### Scaling Beyond Limits
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 |
412
472
 
413
- 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`:
473
+ #### Visibility Extension
474
+
475
+ | Constraint | Value |
476
+ | -------------------------- | ---------------------------------- |
477
+ | `visibilityTimeoutSeconds` | 0 - 3,600 seconds |
478
+ | Cannot extend beyond | Message's original expiration time |
479
+ | Receipt handle | Must match the receive operation |
480
+
481
+ ### Identifier Formats
482
+
483
+ | Identifier | Pattern | Example |
484
+ | ---------------- | ---------------- | -------------------------------- |
485
+ | Topic/Queue name | `[A-Za-z0-9_-]+` | `my-queue`, `task_queue_v2` |
486
+ | Consumer group | `[A-Za-z0-9_-]+` | `worker-1`, `analytics_consumer` |
487
+ | Message ID | Opaque string | `0-1`, `3-7K9mNpQrS` |
488
+ | Receipt handle | Opaque string | Used for delete/visibility ops |
489
+
490
+ ### Content-Type Handling
491
+
492
+ | Scenario | Result |
493
+ | ------------------------------- | -------------------------- |
494
+ | Client provides `Content-Type` | Used as-is |
495
+ | No header, magic bytes detected | Auto-detected MIME type |
496
+ | No header, detection fails | `application/octet-stream` |
497
+
498
+ ### Wildcard Topics
499
+
500
+ Topic patterns support wildcards for flexible routing:
414
501
 
415
502
  ```json
416
503
  {
@@ -428,11 +515,125 @@ If you need more than 1,000 messages per second, you can create multiple topics
428
515
  }
429
516
  ```
430
517
 
431
- This allows you to:
518
+ **Wildcard Rules:**
519
+
520
+ - `*` may only appear **once** in the pattern
521
+ - `*` must be at the **end** of the topic name
522
+ - Valid: `user-*`, `orders-*`
523
+ - Invalid: `*-events`, `user-*-data`
524
+
525
+ ## API Reference
526
+
527
+ ### Client Configuration
528
+
529
+ ```typescript
530
+ import { Client } from "@vercel/queue";
531
+
532
+ const client = new Client({
533
+ // Base URL for the queue service
534
+ // Default: "https://vercel-queue.com"
535
+ // Env: VERCEL_QUEUE_BASE_URL
536
+ baseUrl: "https://vercel-queue.com",
537
+
538
+ // API path prefix
539
+ // Default: "/api/v3/topic"
540
+ // Env: VERCEL_QUEUE_BASE_PATH
541
+ basePath: "/api/v3/topic",
542
+
543
+ // Auth token (auto-fetched via OIDC if not provided)
544
+ token: "my-token",
545
+
546
+ // Custom headers for all requests
547
+ headers: { "X-Custom": "value" },
548
+
549
+ // Deployment ID for message routing
550
+ // Default: process.env.VERCEL_DEPLOYMENT_ID
551
+ deploymentId: "dpl_xxx",
552
+
553
+ // Pin messages to current deployment when publishing
554
+ // Default: true
555
+ pinToDeployment: true,
556
+ });
557
+ ```
558
+
559
+ ### Send Options
560
+
561
+ ```typescript
562
+ await send("my-topic", payload, {
563
+ // Deduplication key
564
+ // Dedup window: min(retentionSeconds, 24 hours)
565
+ idempotencyKey: "unique-key",
566
+
567
+ // Message TTL in seconds
568
+ // Default: 86400, Min: 60, Max: 86400
569
+ retentionSeconds: 3600,
570
+
571
+ // Delay before message becomes visible
572
+ // Default: 0, Min: 0, Max: retentionSeconds
573
+ delaySeconds: 60,
574
+
575
+ // Custom serializer (default: JsonTransport)
576
+ transport: new JsonTransport(),
577
+ });
578
+ ```
579
+
580
+ ### Receive Options
581
+
582
+ ```typescript
583
+ await receive("my-topic", "my-consumer", handler, {
584
+ // Specific message ID to consume (optional)
585
+ messageId: "0-1",
586
+
587
+ // Message lock duration in seconds
588
+ // Default: 30, Min: 0, Max: 3600
589
+ visibilityTimeoutSeconds: 60,
432
590
 
433
- - Create topics like `user-1`, `user-2`, etc.
434
- - Process messages from all user topics with a single handler
435
- - Each topic gets its own 1,000 messages per second quota
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
+ });
598
+ ```
599
+
600
+ ### Receive Options (Advanced)
601
+
602
+ ```typescript
603
+ await receive("my-topic", "my-consumer", handler, {
604
+ // Payload deserializer
605
+ // Default: JsonTransport
606
+ transport: new JsonTransport(),
607
+
608
+ // Message lock duration
609
+ // Default: 30, Min: 0, Max: 3600
610
+ visibilityTimeoutSeconds: 60,
611
+
612
+ // How often to refresh the lock during processing
613
+ // Default: visibilityTimeoutSeconds / 3
614
+ visibilityRefreshInterval: 20,
615
+ });
616
+ ```
617
+
618
+ ### handleCallback Options
619
+
620
+ ```typescript
621
+ export const POST = handleCallback(
622
+ {
623
+ "my-topic": {
624
+ "my-consumer": async (message, metadata) => {
625
+ await processMessage(message);
626
+ },
627
+ },
628
+ },
629
+ {
630
+ // Message lock duration for long-running handlers
631
+ // Default: 30, Min: 0, Max: 3600
632
+ // visibilityRefreshInterval defaults to visibilityTimeoutSeconds / 3
633
+ visibilityTimeoutSeconds: 300, // 5 minutes
634
+ },
635
+ );
636
+ ```
436
637
 
437
638
  ## License
438
639