@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 +130 -192
- package/dist/index.d.mts +2 -386
- package/dist/index.d.ts +2 -386
- package/dist/index.js +106 -100
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +106 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,8 +46,7 @@ type Message = {
|
|
|
46
46
|
timestamp: number;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
//
|
|
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", (
|
|
59
|
-
console.log(
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
//
|
|
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
|
-
- `
|
|
349
|
-
-
|
|
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
|
|
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 {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
401
|
+
### Receive Function
|
|
473
402
|
|
|
474
403
|
```typescript
|
|
475
404
|
// Process next available message (simplest form)
|
|
476
|
-
await consumer
|
|
405
|
+
await receive("topic-name", "consumer-group", handler);
|
|
477
406
|
|
|
478
407
|
// Process specific message by ID with payload
|
|
479
|
-
await
|
|
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
|
|
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
|
-
###
|
|
444
|
+
### Receive Options
|
|
511
445
|
|
|
512
446
|
```typescript
|
|
513
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
635
|
-
|
|
636
|
-
|
|
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
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
660
|
-
}
|
|
587
|
+
await processTask(message.taskType, message.data);
|
|
588
|
+
},
|
|
589
|
+
);
|
|
661
590
|
|
|
662
591
|
// Process specific message metadata only (no payload download)
|
|
663
|
-
await
|
|
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
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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 `
|
|
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 `
|
|
749
|
-
- For `
|
|
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
|
-
|
|
789
|
-
// Process
|
|
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
|
|
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
|
|
755
|
+
await send("my-topic", payload);
|
|
818
756
|
} catch (error) {
|
|
819
757
|
if (error instanceof UnauthorizedError) {
|
|
820
758
|
console.log("Invalid token - refresh authentication");
|