@vercel/queue 0.0.0-alpha.34 → 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
@@ -81,6 +81,7 @@ await send(
81
81
  {
82
82
  idempotencyKey: "unique-key", // Optional: prevent duplicate messages
83
83
  retentionSeconds: 3600, // Optional: override retention time (defaults to 24 hours)
84
+ delaySeconds: 60, // Optional: delay message delivery by N seconds
84
85
  },
85
86
  );
86
87
  ```
@@ -120,7 +121,7 @@ export const POST = handleCallback({
120
121
  // Single topic with one consumer
121
122
  "my-topic": {
122
123
  "my-consumer": async (message, metadata) => {
123
- // metadata includes: { messageId, deliveryCount, createdAt }
124
+ // metadata includes: { messageId, deliveryCount, createdAt, topicName, consumerGroup }
124
125
  console.log("Processing message:", message);
125
126
 
126
127
  // If this throws an error, the message will be automatically retried
@@ -248,6 +249,47 @@ Configure which topics and consumers your API route handles.
248
249
 
249
250
  ## Advanced Features
250
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
+
251
293
  ### Serialization (Transport) System
252
294
 
253
295
  The queue client supports customizable serialization through the `Transport` interface:
@@ -272,36 +314,51 @@ await send(
272
314
  { data: "example" },
273
315
  { transport: new JsonTransport() },
274
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 });
275
324
  ```
276
325
 
277
326
  ### Transport Selection Guide
278
327
 
279
- | Use Case | Recommended Transport | Memory Usage | Performance |
280
- | -------------------- | --------------------- | ------------ | ----------- |
281
- | Small JSON objects | JsonTransport | Low | High |
282
- | Binary files < 100MB | BufferTransport | Medium | High |
283
- | Large files > 100MB | StreamTransport | Very Low | Medium |
284
- | 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 |
285
334
 
286
335
  ## Error Handling
287
336
 
288
337
  The queue client provides specific error types:
289
338
 
290
- - **`QueueEmptyError`**: No messages available (204)
291
- - **`MessageLockedError`**: Message temporarily locked (423)
292
- - **`MessageNotFoundError`**: Message doesn't exist (404)
293
- - **`MessageNotAvailableError`**: Message exists but unavailable (409)
294
- - **`MessageCorruptedError`**: Message data corrupted
295
- - **`BadRequestError`**: Invalid parameters (400)
296
- - **`UnauthorizedError`**: Authentication failure (401)
297
- - **`ForbiddenError`**: Access denied (403)
298
- - **`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)
299
354
 
300
355
  Example error handling:
301
356
 
302
357
  ```typescript
303
358
  import {
304
359
  BadRequestError,
360
+ ConcurrencyLimitError,
361
+ DuplicateMessageError,
305
362
  ForbiddenError,
306
363
  InternalServerError,
307
364
  UnauthorizedError,
@@ -316,52 +373,131 @@ try {
316
373
  console.log("Environment mismatch - check configuration");
317
374
  } else if (error instanceof BadRequestError) {
318
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
+ );
319
385
  } else if (error instanceof InternalServerError) {
320
386
  console.log("Server error - retry with backoff");
321
387
  }
322
388
  }
323
389
  ```
324
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
+
325
402
  ## Advanced Usage
326
403
 
327
404
  ### Direct Message Processing
328
405
 
329
- > **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.
330
407
 
331
408
  ```typescript
409
+ import { receive } from "@vercel/queue";
410
+
332
411
  // Process next available message
333
412
  await receive<T>(topicName, consumerGroup, handler);
334
413
 
335
414
  // Process specific message by ID
336
415
  await receive<T>(topicName, consumerGroup, handler, {
337
- messageId: "message-id"
416
+ messageId: "message-id",
338
417
  });
339
418
 
340
419
  // Process message with options
341
420
  await receive<T>(topicName, consumerGroup, handler, {
342
- messageId?: string; // Process specific message by ID
343
- skipPayload?: boolean; // Skip payload download (requires messageId)
344
- transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
345
- visibilityTimeoutSeconds?: number; // Message visibility timeout
346
- 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
347
425
  });
348
426
 
349
427
  // Handler function signature
350
428
  type MessageHandler<T = unknown> = (
351
429
  message: T,
352
- metadata: MessageMetadata
430
+ metadata: MessageMetadata,
353
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;
440
+ }
354
441
  ```
355
442
 
356
- ## 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
357
456
 
358
- - **Message Throughput**: Each topic can handle up to 1,000 messages per second
359
- - **Payload Size**: Maximum payload size is 4.5MB (this limit will be increased soon)
360
- - **Number of Topics**: No limit on the number of topics you can create
457
+ #### Publishing Messages
361
458
 
362
- ### Scaling Beyond Limits
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)` |
363
464
 
364
- 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`:
465
+ #### Receiving Messages
466
+
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 |
472
+
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:
365
501
 
366
502
  ```json
367
503
  {
@@ -379,11 +515,125 @@ If you need more than 1,000 messages per second, you can create multiple topics
379
515
  }
380
516
  ```
381
517
 
382
- 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`
383
524
 
384
- - Create topics like `user-1`, `user-2`, etc.
385
- - Process messages from all user topics with a single handler
386
- - Each topic gets its own 1,000 messages per second quota
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,
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
+ });
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
+ ```
387
637
 
388
638
  ## License
389
639