@vercel/queue 0.0.0-alpha.6 → 0.0.0-alpha.8

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
@@ -46,8 +46,7 @@ type Message = {
46
46
  timestamp: number;
47
47
  };
48
48
 
49
- // Option 1: Using the send and receive helpers (simplest)
50
- // Automatically uses default client and JSON transport
49
+ // Send a message to a topic
51
50
  await send<Message>("my-topic", {
52
51
  message: "Hello, World!",
53
52
  timestamp: Date.now(),
@@ -55,38 +54,12 @@ await send<Message>("my-topic", {
55
54
 
56
55
  // Consume a single message off the queue
57
56
  // (Often wrapped in a loop to keep polling messages off the queue)
58
- await receive<Message>("my-topic", "my-consumer-group", (m) => {
59
- console.log(m.message);
57
+ await receive<Message>("my-topic", "my-consumer-group", (message, metadata) => {
58
+ console.log("Received:", message.message);
59
+ console.log("Timestamp:", new Date(message.timestamp));
60
+ console.log("Message Metadata", metadata);
61
+ // => { messageId, deliveryCount, timestamp }
60
62
  });
61
-
62
- // Option 2: Using createTopic for more control
63
-
64
- import { createTopic } from "@vercel/queue";
65
-
66
- // Create a topic with JSON serialization (default)
67
- // Uses default QueueClient automatically authenticated from Vercel environment
68
- const topic = createTopic<Message>("my-topic");
69
-
70
- // Publish a message
71
- await topic.publish({
72
- message: "Hello, World!",
73
- timestamp: Date.now(),
74
- });
75
-
76
- // Create a consumer group
77
- const consumer = topic.consumerGroup("my-consumer-group");
78
-
79
- // Process next available message (one-shot processing)
80
- try {
81
- await consumer.consume(async (message, metadata) => {
82
- console.log("Received:", message.message);
83
- console.log("Timestamp:", new Date(message.timestamp));
84
- console.log("Message Metadata", metadata);
85
- // => { messageId, deliveryCount, timestamp }
86
- });
87
- } catch (error) {
88
- console.error("Processing error:", error);
89
- }
90
63
  ```
91
64
 
92
65
  ## Usage with Vercel
@@ -124,31 +97,13 @@ Create a new server function to publish messages
124
97
  import { send } from "@vercel/queue";
125
98
 
126
99
  export async function publishTestMessage(message: string) {
127
- // Option 1: Using simple send shorthand
128
- const { messageId } = await send(
129
- "my-topic",
130
- { message, timestamp: Date.now() },
131
- },
132
- );
133
-
134
- console.log(`Published message ${messageId}`);
135
- }
136
-
137
- // Option 2: Customize the topic, transport, consumer groups, etc.
138
- import { createTopic } from "@vercel/queue";
139
-
140
- export async function publishTestMessage(message: string) {
141
- // Create a topic with JSON serialization (default)
142
- const topic = createTopic<{ message: string; timestamp: number }>("my-topic");
143
-
144
- // Publish the message
145
- const { messageId } = await topic.publish(
146
- { message, timestamp: Date.now() },
147
- );
100
+ const { messageId } = await send("my-topic", {
101
+ message,
102
+ timestamp: Date.now(),
103
+ });
148
104
 
149
105
  console.log(`Published message ${messageId}`);
150
106
  }
151
-
152
107
  ```
153
108
 
154
109
  Now wire up the server function to your app
@@ -330,14 +285,15 @@ Multiple consumers can process messages from the same topic in parallel:
330
285
 
331
286
  ```typescript
332
287
  // Multiple workers in the same group - they share/split messages
333
- const worker1 = topic.consumerGroup("workers");
334
- const worker2 = topic.consumerGroup("workers"); // Same group name
335
- // worker1 and worker2 will receive different messages (load balancing)
288
+ // Using the same consumer group name means they will load balance messages
289
+ await receive("my-topic", "workers", handler1);
290
+ await receive("my-topic", "workers", handler2);
291
+ // handler1 and handler2 will receive different messages (load balancing)
336
292
 
337
293
  // Different consumer groups - each gets copies of ALL messages
338
- const analytics = topic.consumerGroup("analytics");
339
- const webhooks = topic.consumerGroup("webhooks");
340
- // analytics and webhooks will both receive every message
294
+ await receive("my-topic", "analytics", analyticsHandler);
295
+ await receive("my-topic", "webhooks", webhooksHandler);
296
+ // analyticsHandler and webhooksHandler will both receive every message
341
297
  ```
342
298
 
343
299
  ## Architecture
@@ -345,8 +301,8 @@ const webhooks = topic.consumerGroup("webhooks");
345
301
  - **Topics**: Named message channels with configurable serialization
346
302
  - **Consumer Groups**: Named groups of consumers that process messages in
347
303
  parallel
348
- - `consume()`: Process messages with flexible consumption patterns
349
- - No options: Process next available message
304
+ - `receive()`: Process messages with flexible consumption patterns
305
+ - Basic usage: Process next available message
350
306
  - With `messageId`: Process specific message by ID
351
307
  - With `skipPayload: true`: Process message metadata only (without payload)
352
308
  - **Transports**: Pluggable serialization/deserialization for different data
@@ -368,8 +324,7 @@ The multipart parser is optimized for high-throughput scenarios:
368
324
 
369
325
  The queue client supports customizable serialization through the `Transport`
370
326
  interface with **streaming support** for memory-efficient processing. Transport
371
- can be configured at the **topic level** when creating a topic, or at the
372
- **consumer group level** when creating a consumer group.
327
+ can be configured using the `transport` option when calling `send()` or `receive()`.
373
328
 
374
329
  ### Built-in Transports
375
330
 
@@ -379,11 +334,15 @@ Buffers data for JSON parsing - suitable for structured data that fits in
379
334
  memory.
380
335
 
381
336
  ```typescript
382
- import { createTopic, JsonTransport } from "@vercel/queue";
383
-
384
- const topic = createTopic<{ data: any }>("json-topic", new JsonTransport());
385
- // or simply (JsonTransport is the default):
386
- const topic = createTopic<{ data: any }>("json-topic");
337
+ import { send, JsonTransport } from "@vercel/queue";
338
+
339
+ // JsonTransport is the default, so these are equivalent:
340
+ await send("json-topic", { data: "example" });
341
+ await send(
342
+ "json-topic",
343
+ { data: "example" },
344
+ { transport: new JsonTransport() },
345
+ );
387
346
  ```
388
347
 
389
348
  #### BufferTransport
@@ -413,37 +372,7 @@ interface.
413
372
 
414
373
  ## API Reference
415
374
 
416
- ### QueueClient
417
-
418
- ```typescript
419
- // Simple usage - automatically gets OIDC token from Vercel environment
420
- const client = new QueueClient();
421
-
422
- // Or with options
423
- const client = new QueueClient({
424
- token?: string; // Optional - will auto-detect if not provided
425
- baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
426
- });
427
- ```
428
-
429
- ### Topic
430
-
431
- ```typescript
432
- // Simple usage with default client
433
- const topic = createTopic<T>(topicName, transport?);
434
-
435
- // For custom client configuration, use Topic constructor directly
436
- const customClient = new QueueClient({ baseUrl: "https://custom.vqs.vercel.sh" });
437
- const topic = new Topic<T>(customClient, topicName, transport?);
438
-
439
- // Publish a message (uses topic's transport)
440
- await topic.publish(payload, options?);
441
-
442
- // Create a consumer group (can override transport)
443
- const consumer = topic.consumerGroup<U>(groupName, options?);
444
- ```
445
-
446
- ### Send (Shorthand)
375
+ ### Send Function
447
376
 
448
377
  ```typescript
449
378
  // Simple send - automatically uses default client and JSON transport
@@ -469,18 +398,23 @@ await send("events", eventData, {
469
398
  });
470
399
  ```
471
400
 
472
- ### ConsumerGroup
401
+ ### Receive Function
473
402
 
474
403
  ```typescript
475
404
  // Process next available message (simplest form)
476
- await consumer.consume(handler);
405
+ await receive("topic-name", "consumer-group", handler);
477
406
 
478
407
  // Process specific message by ID with payload
479
- await consumer.consume(handler, { messageId: "message-id" });
408
+ await receive("topic-name", "consumer-group", handler, {
409
+ messageId: "message-id",
410
+ });
480
411
 
481
412
  // Process specific message by ID without payload (metadata only)
482
413
  // handler will be called with `undefined` as the payload
483
- await consumer.consume(handler, { messageId: "message-id", skipPayload: true });
414
+ await receive("topic-name", "consumer-group", handler, {
415
+ messageId: "message-id",
416
+ skipPayload: true,
417
+ });
484
418
  ```
485
419
 
486
420
  ### Message Handler
@@ -507,12 +441,16 @@ interface MessageMetadata {
507
441
  }
508
442
  ```
509
443
 
510
- ### ConsumeOptions Interface
444
+ ### Receive Options
511
445
 
512
446
  ```typescript
513
- interface ConsumeOptions {
447
+ // Options for the receive function
448
+ interface ReceiveOptions<T = unknown> {
514
449
  messageId?: string; // Process specific message by ID
515
450
  skipPayload?: boolean; // Skip payload download (requires messageId)
451
+ transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
452
+ visibilityTimeoutSeconds?: number; // Message visibility timeout
453
+ refreshInterval?: number; // Refresh interval for long-running operations
516
454
  }
517
455
  ```
518
456
 
@@ -566,27 +504,16 @@ interface UserEvent {
566
504
  timestamp: number;
567
505
  }
568
506
 
569
- // Option 1: Using send shorthand
507
+ // Send a message
570
508
  await send<UserEvent>("user-events", {
571
509
  userId: "123",
572
510
  action: "login",
573
511
  timestamp: Date.now(),
574
512
  });
575
513
 
576
- // Option 2: Using createTopic for consumers
577
- const userTopic = createTopic<UserEvent>("user-events");
578
-
579
- await userTopic.publish({
580
- userId: "123",
581
- action: "login",
582
- timestamp: Date.now(),
583
- });
584
-
585
- const consumer = userTopic.consumerGroup("processors");
586
-
587
- // Process next available message
514
+ // Receive and process a message
588
515
  try {
589
- await consumer.consume(async (message) => {
516
+ await receive<UserEvent>("user-events", "processors", async (message) => {
590
517
  console.log(`User ${message.userId} performed ${message.action}`);
591
518
  });
592
519
  } catch (error) {
@@ -597,16 +524,13 @@ try {
597
524
  ### Processing Specific Messages by ID
598
525
 
599
526
  ```typescript
600
- const userTopic = createTopic<{ userId: string; action: string }>(
601
- "user-events",
602
- );
603
- const consumer = userTopic.consumerGroup("processors");
604
-
605
527
  // Process a specific message if you know its ID
606
528
  const messageId = "01234567-89ab-cdef-0123-456789abcdef";
607
529
 
608
530
  try {
609
- await consumer.consume(
531
+ await receive<{ userId: string; action: string }>(
532
+ "user-events",
533
+ "processors",
610
534
  async (message, { messageId }) => {
611
535
  console.log(`Processing specific message: ${messageId}`);
612
536
  console.log(`User ${message.userId} performed ${message.action}`);
@@ -626,15 +550,16 @@ try {
626
550
  ### Processing Next Available Message
627
551
 
628
552
  ```typescript
629
- const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
630
- const worker = workTopic.consumerGroup("workers");
631
-
632
553
  // Process the next available message (one-shot processing)
633
554
  try {
634
- await worker.consume(async (message) => {
635
- console.log(`Processing task: ${message.taskType}`);
636
- await processTask(message.taskType, message.data);
637
- });
555
+ await receive<{ taskType: string; data: any }>(
556
+ "work-queue",
557
+ "workers",
558
+ async (message) => {
559
+ console.log(`Processing task: ${message.taskType}`);
560
+ await processTask(message.taskType, message.data);
561
+ },
562
+ );
638
563
  console.log("Message processed successfully");
639
564
  } catch (error) {
640
565
  if (error instanceof QueueEmptyError) {
@@ -650,17 +575,23 @@ try {
650
575
  }
651
576
 
652
577
  // Handle conditional timeouts
653
- await worker.consume(async (message) => {
654
- if (!canProcessTaskType(message.taskType)) {
655
- // Return timeout to retry later
656
- return { timeoutSeconds: 60 };
657
- }
578
+ await receive<{ taskType: string; data: any }>(
579
+ "work-queue",
580
+ "workers",
581
+ async (message) => {
582
+ if (!canProcessTaskType(message.taskType)) {
583
+ // Return timeout to retry later
584
+ return { timeoutSeconds: 60 };
585
+ }
658
586
 
659
- await processTask(message.taskType, message.data);
660
- });
587
+ await processTask(message.taskType, message.data);
588
+ },
589
+ );
661
590
 
662
591
  // Process specific message metadata only (no payload download)
663
- await worker.consume(
592
+ await receive<{ taskType: string; data: any }>(
593
+ "work-queue",
594
+ "workers",
664
595
  async (_, metadata) => {
665
596
  console.log(`Message ID: ${metadata.messageId}`);
666
597
  console.log(`Delivery count: ${metadata.deliveryCount}`);
@@ -674,56 +605,64 @@ await worker.consume(
674
605
  ### Timing Out Messages
675
606
 
676
607
  ```typescript
677
- const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
678
- const worker = workTopic.consumerGroup("workers");
679
-
680
608
  // Process a message with conditional timeout
681
609
  try {
682
- await worker.consume(async ({ taskType, data }) => {
683
- // Check if we can process this task type right now
684
- if (taskType === "heavy-computation" && isSystemOverloaded()) {
685
- // Return timeout to retry later (5 minutes)
686
- return { timeoutSeconds: 300 };
687
- }
610
+ await receive<{ taskType: string; data: any }>(
611
+ "work-queue",
612
+ "workers",
613
+ async ({ taskType, data }) => {
614
+ // Check if we can process this task type right now
615
+ if (taskType === "heavy-computation" && isSystemOverloaded()) {
616
+ // Return timeout to retry later (5 minutes)
617
+ return { timeoutSeconds: 300 };
618
+ }
688
619
 
689
- // Check if we have required resources
690
- if (taskType === "external-api" && !isExternalServiceAvailable()) {
691
- // Return timeout to retry in 1 minute
692
- return { timeoutSeconds: 60 };
693
- }
620
+ // Check if we have required resources
621
+ if (taskType === "external-api" && !isExternalServiceAvailable()) {
622
+ // Return timeout to retry in 1 minute
623
+ return { timeoutSeconds: 60 };
624
+ }
694
625
 
695
- // Process the message normally
696
- console.log(`Processing ${taskType} task`);
697
- await processTask(taskType, data);
698
- // Message will be automatically deleted on successful completion
699
- });
626
+ // Process the message normally
627
+ console.log(`Processing ${taskType} task`);
628
+ await processTask(taskType, data);
629
+ // Message will be automatically deleted on successful completion
630
+ },
631
+ );
700
632
  } catch (error) {
701
633
  console.error("Worker processing error:", error);
702
634
  }
703
635
 
704
636
  // Example with exponential backoff
705
637
  try {
706
- await worker.consume(async (message, { deliveryCount }) => {
707
- const maxRetries = 3;
708
-
709
- try {
710
- await processMessage(message);
711
- // Successful processing - message will be deleted
712
- } catch (error) {
713
- if (deliveryCount < maxRetries) {
714
- // Exponential backoff: 2^deliveryCount minutes
715
- const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
716
- console.log(
717
- `Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
718
- );
719
- return { timeoutSeconds: timeoutSeconds };
720
- } else {
721
- // Max retries reached, let the message fail and be deleted
722
- console.error("Max retries reached, message will be discarded:", error);
723
- throw error;
638
+ await receive<{ taskType: string; data: any }>(
639
+ "work-queue",
640
+ "workers",
641
+ async (message, { deliveryCount }) => {
642
+ const maxRetries = 3;
643
+
644
+ try {
645
+ await processMessage(message);
646
+ // Successful processing - message will be deleted
647
+ } catch (error) {
648
+ if (deliveryCount < maxRetries) {
649
+ // Exponential backoff: 2^deliveryCount minutes
650
+ const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
651
+ console.log(
652
+ `Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
653
+ );
654
+ return { timeoutSeconds: timeoutSeconds };
655
+ } else {
656
+ // Max retries reached, let the message fail and be deleted
657
+ console.error(
658
+ "Max retries reached, message will be discarded:",
659
+ error,
660
+ );
661
+ throw error;
662
+ }
724
663
  }
725
- }
726
- });
664
+ },
665
+ );
727
666
  } catch (error) {
728
667
  console.error("Backoff processing error:", error);
729
668
  }
@@ -738,15 +677,14 @@ The queue client provides specific error types for different failure scenarios:
738
677
  - **`QueueEmptyError`**: Thrown when attempting to receive messages from an
739
678
  empty queue (204 status)
740
679
 
741
- - Thrown by `consume()` when no messages are available
742
- - Also thrown when directly using `client.receiveMessages()`
680
+ - Thrown by `receive()` when no messages are available
743
681
 
744
682
  - **`MessageLockedError`**: Thrown when a message is temporarily locked (423
745
683
  status)
746
684
 
747
685
  - Contains optional `retryAfter` property with seconds to wait before retry
748
- - For `consume()` without options: the next message is locked
749
- - For `consume()` with messageId: the requested message is locked
686
+ - For `receive()` without options: the next message is locked
687
+ - For `receive()` with messageId: the requested message is locked
750
688
 
751
689
  - **`MessageNotFoundError`**: Message doesn't exist (404 status)
752
690
 
@@ -775,7 +713,6 @@ The queue client provides specific error types for different failure scenarios:
775
713
  ```typescript
776
714
  import {
777
715
  BadRequestError,
778
-
779
716
  ForbiddenError,
780
717
  InternalServerError,
781
718
  MessageLockedError,
@@ -785,9 +722,10 @@ import {
785
722
 
786
723
  // Handle empty queue or locked messages
787
724
  try {
788
- for await (const message of client.receiveMessages(options, transport)) {
789
- // Process messages
790
- }
725
+ await receive("my-topic", "my-consumer", async (message) => {
726
+ // Process message
727
+ console.log("Processing message:", message);
728
+ });
791
729
  } catch (error) {
792
730
  if (error instanceof QueueEmptyError) {
793
731
  console.log("Queue is empty, retry later");
@@ -801,7 +739,7 @@ try {
801
739
 
802
740
  // Handle locked message with retry
803
741
  try {
804
- await consumer.consume(handler, { messageId });
742
+ await receive("my-topic", "my-consumer", handler, { messageId });
805
743
  } catch (error) {
806
744
  if (error instanceof MessageLockedError) {
807
745
  console.log("Message is locked by another consumer");
@@ -809,12 +747,12 @@ try {
809
747
  console.log(`Retry after ${error.retryAfter} seconds`);
810
748
  setTimeout(() => retry(), error.retryAfter * 1000);
811
749
  }
812
-
750
+ }
813
751
  }
814
752
 
815
753
  // Handle authentication and authorization errors
816
754
  try {
817
- await topic.publish(payload);
755
+ await send("my-topic", payload);
818
756
  } catch (error) {
819
757
  if (error instanceof UnauthorizedError) {
820
758
  console.log("Invalid token - refresh authentication");