@vercel/queue 0.0.0-alpha.5 → 0.0.0-alpha.7

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,55 +54,19 @@ 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
- ```
91
-
92
- Run the script
93
-
94
- ```bash
95
- # Install dotenv-cli and ts-node if you need it
96
- npm i -g dotenv-cli ts-node typescript
97
-
98
- # Run the script with the OIDC token
99
- dotenv -e .env.local ts-node index.ts
100
63
  ```
101
64
 
102
65
  ## Usage with Vercel
103
66
 
104
67
  When deploying on Vercel, rather than having a persistent server subscribed to a
105
- queue, Vercel can trigger a callback route when a message is ready for
106
- consumption.
68
+ queue, Vercel can automatically trigger your API routes when messages are ready for
69
+ consumption based on your vercel.json configuration.
107
70
 
108
71
  To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
109
72
  existing app or create one using
@@ -134,69 +97,13 @@ Create a new server function to publish messages
134
97
  import { send } from "@vercel/queue";
135
98
 
136
99
  export async function publishTestMessage(message: string) {
137
- // Option 1: Using simple send shorthand
138
- const { messageId } = await send(
139
- "my-topic",
140
- { message, timestamp: Date.now() },
141
- {
142
- // Provide a callback URL to invoke a consumer when the message is ready to be processed
143
- callback: {
144
- url: getCallbackUrl() // implementation below
145
- },
146
- },
147
- );
148
-
149
- console.log(`Published message ${messageId}`);
150
- }
151
-
152
- // Option 2: Customize the topic, transport, consumer groups, etc.
153
- import { createTopic } from "@vercel/queue";
154
-
155
- export async function publishTestMessage(message: string) {
156
- // Create a topic with JSON serialization (default)
157
- const topic = createTopic<{ message: string; timestamp: number }>("my-topic");
158
-
159
- // Publish the message
160
- const { messageId } = await topic.publish(
161
- { message, timestamp: Date.now() },
162
- {
163
- // Provide multiple callback URLs to invoke multiple consumer groups
164
- callback: {
165
- {
166
- "consumer-group-1": {
167
- url: getCallbackUrl()
168
- },
169
- "consumer-group-2": {
170
- url: getCallbackUrl()
171
- delay: 5 // Delay callback by 5 seconds
172
- },
173
- }
174
- },
175
- },
176
- );
100
+ const { messageId } = await send("my-topic", {
101
+ message,
102
+ timestamp: Date.now(),
103
+ });
177
104
 
178
105
  console.log(`Published message ${messageId}`);
179
106
  }
180
-
181
- // Helper function to generate a local callback URL
182
- function getCallbackUrl() {
183
- const callbackUrl = new URL(
184
- process.env.VERCEL_URL
185
- ? `https://${process.env.VERCEL_URL}/api/queue/handle`
186
- : "http://localhost:3000/api/queue/handle"
187
- );
188
-
189
- // Add Vercel automation bypass secret if available (for preview deployments)
190
- if (process.env.VERCEL_AUTOMATION_BYPASS_SECRET) {
191
- callbackUrl.searchParams.set(
192
- "x-vercel-protection-bypass",
193
- process.env.VERCEL_AUTOMATION_BYPASS_SECRET
194
- );
195
- }
196
-
197
- return callbackUrl.toString();
198
- }
199
-
200
107
  ```
201
108
 
202
109
  Now wire up the server function to your app
@@ -218,86 +125,175 @@ export default function Page() {
218
125
 
219
126
  ### Consuming the queue
220
127
 
221
- Instead of running a persistent server that subscribes to the queue, we use the
222
- callback functionality of Vercel queues to consume messages on the fly, when a
223
- message is ready to be processed.
128
+ Messages are consumed using consumer groups, which provide load balancing and parallel processing capabilities.
129
+
130
+ ## Usage with Vercel
224
131
 
225
- The `handleCallback` helper function simplifies queue callback handling in NextJS:
132
+ To consume queue messages in a Vercel deployment, you need to create (Next.js) API routes and configure them in your `vercel.json` file.
133
+
134
+ ### 1. Create API Routes
135
+
136
+ Create API routes to handle incoming queue messages using the `handleCallback` helper:
226
137
 
227
138
  ```typescript
228
139
  // app/api/queue/handle/route.ts
229
140
  import { handleCallback } from "@vercel/queue";
230
141
 
231
- // Option 1: Specify a single handler for the topic
142
+ // Option 1: Single topic with multiple consumer groups
232
143
  export const POST = handleCallback({
233
- "my-topic": (message, metadata) => {
234
- console.log(`Received message:`, message, metadata);
235
- // metadata: { messageId, deliveryCount, timestamp }
144
+ "my-topic": {
145
+ "consumer-group-1": async (message, metadata) => {
146
+ console.log(`Consumer group 1 processing:`, message, metadata);
147
+ // Handle consumer group 1 logic
148
+ await processGroup1(message);
149
+ },
150
+ "consumer-group-2": async (message, metadata) => {
151
+ console.log(`Consumer group 2 processing:`, message, metadata);
152
+ // Handle consumer group 2 logic
153
+ await processGroup2(message);
154
+ },
236
155
  },
237
-
238
- // .. more topic handlers can be provided here
239
156
  });
240
157
 
241
- // This consumes messages on the "default" consumer group, which is used when no consumer groups
242
- // were specified in the publish `callback` earlierA
158
+ async function processGroup1(message: any) {
159
+ // Consumer group 1 specific logic
160
+ }
243
161
 
244
- // Option 2: Multiple consumer groups
162
+ async function processGroup2(message: any) {
163
+ // Consumer group 2 specific logic
164
+ }
165
+ ```
166
+
167
+ ```typescript
168
+ // Alternative: Multiple topics in one handler
245
169
  export const POST = handleCallback({
246
- // topic: "my-topic"
247
- "my-topic": {
248
- // consumer group: "compress"
249
- "consumer-group-1": (message, metadata) => {
250
- console.log("Message:", message);
170
+ "user-events": {
171
+ welcome: async (message, metadata) => {
172
+ console.log(`New user event:`, message, metadata);
173
+ await sendWelcomeEmail(message.email);
174
+ },
175
+ },
176
+ "order-events": {
177
+ fulfillment: async (order, metadata) => {
178
+ console.log(`Processing order:`, order, metadata);
179
+ await fulfillOrder(order);
251
180
  },
252
- // consumer group: "resize"
253
- "consume-group-2": (message, metadata) => {
254
- console.log("Message", message);
181
+ analytics: async (order, metadata) => {
182
+ console.log(`Tracking order:`, order, metadata);
183
+ await trackOrder(order);
255
184
  },
256
185
  },
257
186
  });
258
187
  ```
259
188
 
260
- ## Key Features
189
+ ### 2. Configure vercel.json
190
+
191
+ Create a `vercel.json` file in your project root to declare which topics and consumer groups each API route handles:
192
+
193
+ ```json
194
+ {
195
+ "functions": {
196
+ "app/api/queue/handle/route.ts": {
197
+ "experimentalTriggers": [
198
+ {
199
+ "type": "queue/v1beta",
200
+ "topic": "my-topic",
201
+ "consumer": "consumer-group-1"
202
+ },
203
+ {
204
+ "type": "queue/v1beta",
205
+ "topic": "my-topic",
206
+ "consumer": "consumer-group-2"
207
+ }
208
+ ]
209
+ }
210
+ }
211
+ }
212
+ ```
261
213
 
262
- ### Streaming Support
214
+ ### 3. Multiple API Routes
263
215
 
264
- Handle large files and data streams without loading them into memory:
216
+ You can also create separate API routes for different topics:
265
217
 
266
218
  ```typescript
267
- import { createTopic, StreamTransport } from "@vercel/queue";
219
+ // app/api/queue/users/route.ts - Handle user events
220
+ import { handleCallback } from "@vercel/queue";
268
221
 
269
- const videoTopic = createTopic<ReadableStream<Uint8Array>>(
270
- "video-processing",
271
- new StreamTransport(),
272
- );
222
+ export const POST = handleCallback({
223
+ "user-events": {
224
+ processors: async (user, metadata) => {
225
+ console.log(`Processing user event:`, user, metadata);
226
+ await sendWelcomeEmail(user.email);
227
+ },
228
+ },
229
+ });
230
+ ```
273
231
 
274
- // Process large video files efficiently
275
- const processor = videoTopic.consumerGroup("processors");
276
- await processor.consume(async (videoStream) => {
277
- // Process stream chunk by chunk
278
- const reader = videoStream.getReader();
279
- while (true) {
280
- const { done, value } = await reader.read();
281
- if (done) break;
282
- await processChunk(value);
283
- }
232
+ ```typescript
233
+ // app/api/queue/orders/route.ts - Handle order events
234
+ import { handleCallback } from "@vercel/queue";
235
+
236
+ export const POST = handleCallback({
237
+ "order-events": {
238
+ fulfillment: async (order, metadata) => {
239
+ console.log(`Processing order:`, order, metadata);
240
+ await fulfillOrder(order);
241
+ },
242
+ },
284
243
  });
285
244
  ```
286
245
 
246
+ With corresponding `vercel.json`:
247
+
248
+ ```json
249
+ {
250
+ "functions": {
251
+ "app/api/queue/users/route.ts": {
252
+ "experimentalTriggers": [
253
+ {
254
+ "type": "queue/v1beta",
255
+ "topic": "user-events",
256
+ "consumer": "processors"
257
+ }
258
+ ]
259
+ },
260
+ "app/api/queue/orders/route.ts": {
261
+ "experimentalTriggers": [
262
+ {
263
+ "type": "queue/v1beta",
264
+ "topic": "order-events",
265
+ "consumer": "fulfillment"
266
+ }
267
+ ]
268
+ }
269
+ }
270
+ }
271
+ ```
272
+
273
+ ### Key Points
274
+
275
+ - **Automatic Triggering**: Vercel automatically triggers your API routes when messages are available for the configured topic/consumer combinations
276
+ - **Message Processing**: Your API routes receive the message ID and other metadata via headers, then use the queue client to process the specific message
277
+ - **Configuration Required**: The `vercel.json` file is essential - it tells Vercel which topics and consumers each route should handle
278
+ - **No Polling**: Unlike traditional queue consumers, you don't need to poll for messages - Vercel handles the triggering automatically
279
+
280
+ ## Key Features
281
+
287
282
  ### Consumer Groups
288
283
 
289
284
  Multiple consumers can process messages from the same topic in parallel:
290
285
 
291
286
  ```typescript
292
287
  // Multiple workers in the same group - they share/split messages
293
- const worker1 = topic.consumerGroup("workers");
294
- const worker2 = topic.consumerGroup("workers"); // Same group name
295
- // 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)
296
292
 
297
293
  // Different consumer groups - each gets copies of ALL messages
298
- const analytics = topic.consumerGroup("analytics");
299
- const webhooks = topic.consumerGroup("webhooks");
300
- // 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
301
297
  ```
302
298
 
303
299
  ## Architecture
@@ -305,8 +301,8 @@ const webhooks = topic.consumerGroup("webhooks");
305
301
  - **Topics**: Named message channels with configurable serialization
306
302
  - **Consumer Groups**: Named groups of consumers that process messages in
307
303
  parallel
308
- - `consume()`: Process messages with flexible consumption patterns
309
- - No options: Process next available message
304
+ - `receive()`: Process messages with flexible consumption patterns
305
+ - Basic usage: Process next available message
310
306
  - With `messageId`: Process specific message by ID
311
307
  - With `skipPayload: true`: Process message metadata only (without payload)
312
308
  - **Transports**: Pluggable serialization/deserialization for different data
@@ -328,8 +324,7 @@ The multipart parser is optimized for high-throughput scenarios:
328
324
 
329
325
  The queue client supports customizable serialization through the `Transport`
330
326
  interface with **streaming support** for memory-efficient processing. Transport
331
- can be configured at the **topic level** when creating a topic, or at the
332
- **consumer group level** when creating a consumer group.
327
+ can be configured using the `transport` option when calling `send()` or `receive()`.
333
328
 
334
329
  ### Built-in Transports
335
330
 
@@ -339,11 +334,15 @@ Buffers data for JSON parsing - suitable for structured data that fits in
339
334
  memory.
340
335
 
341
336
  ```typescript
342
- import { createTopic, JsonTransport } from "@vercel/queue";
343
-
344
- const topic = createTopic<{ data: any }>("json-topic", new JsonTransport());
345
- // or simply (JsonTransport is the default):
346
- 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
+ );
347
346
  ```
348
347
 
349
348
  #### BufferTransport
@@ -351,55 +350,15 @@ const topic = createTopic<{ data: any }>("json-topic");
351
350
  Buffers the entire payload into memory as a Buffer - suitable for binary data
352
351
  that fits in memory.
353
352
 
354
- ```typescript
355
- import { BufferTransport, createTopic } from "@vercel/queue";
356
-
357
- const topic = createTopic<Buffer>("binary-topic", new BufferTransport());
358
- const binaryData = Buffer.from("Binary data", "utf8");
359
- await topic.publish(binaryData);
360
- ```
361
-
362
353
  #### StreamTransport
363
354
 
364
355
  **True streaming support** - passes ReadableStream directly without buffering.
365
356
  Ideal for large files and memory-efficient processing.
366
357
 
367
- ```typescript
368
- import { createTopic, StreamTransport } from "@vercel/queue";
369
-
370
- const topic = createTopic<ReadableStream<Uint8Array>>(
371
- "streaming-topic",
372
- new StreamTransport(),
373
- );
374
-
375
- // Send large file as stream without loading into memory
376
- const fileStream = new ReadableStream<Uint8Array>({
377
- start(controller) {
378
- // Read file in chunks
379
- for (const chunk of readFileInChunks("large-file.bin")) {
380
- controller.enqueue(chunk);
381
- }
382
- controller.close();
383
- },
384
- });
385
-
386
- await topic.publish(fileStream);
387
- ```
388
-
389
358
  ### Custom Transport
390
359
 
391
360
  You can create your own serialization format by implementing the `Transport`
392
- interface:
393
-
394
- ```typescript
395
- import { Transport } from "@vercel/queue";
396
-
397
- interface Transport<T = unknown> {
398
- serialize(value: T): Buffer | ReadableStream<Uint8Array>;
399
- deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
400
- contentType: string;
401
- }
402
- ```
361
+ interface.
403
362
 
404
363
  ### Choosing the Right Transport
405
364
 
@@ -413,52 +372,7 @@ interface Transport<T = unknown> {
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
- // Trigger a callback URL when the message is
443
- // ready for consumption
444
- await topic.publish(payload, {
445
- callback: { url: "https://example.com/webhook" }
446
- });
447
-
448
- // Or provide multiple callbacks (each URL is called
449
- // with a separate consumer group)
450
- await topic.publish(payload, {
451
- callback: {
452
- group1: { url: "https://example.com/webhook1" },
453
- group2: { url: "https://example.com/webhook2", delay: 30 }
454
- }
455
- });
456
-
457
- // Create a consumer group (can override transport)
458
- const consumer = topic.consumerGroup<U>(groupName, options?);
459
- ```
460
-
461
- ### Send (Shorthand)
375
+ ### Send Function
462
376
 
463
377
  ```typescript
464
378
  // Simple send - automatically uses default client and JSON transport
@@ -469,7 +383,6 @@ await send<T>(topicName, payload, {
469
383
  transport?: Transport<T>;
470
384
  idempotencyKey?: string;
471
385
  retentionSeconds?: number;
472
- callback?: Record<string, CallbackConfig> | CallbackConfig;
473
386
  });
474
387
 
475
388
  // Examples:
@@ -477,31 +390,31 @@ await send("notifications", { userId: "123", message: "Welcome!" });
477
390
 
478
391
  await send("images", imageBuffer, {
479
392
  transport: new BufferTransport(),
480
- callback: { url: "https://example.com/process-image" }
481
393
  });
482
394
 
483
395
  await send("events", eventData, {
484
396
  idempotencyKey: "unique-key-123",
485
397
  retentionSeconds: 3600,
486
- callback: {
487
- analytics: { url: "https://analytics.example.com/webhook" },
488
- notifications: { url: "https://notifications.example.com/webhook", delay: 30 }
489
- }
490
398
  });
491
399
  ```
492
400
 
493
- ### ConsumerGroup
401
+ ### Receive Function
494
402
 
495
403
  ```typescript
496
404
  // Process next available message (simplest form)
497
- await consumer.consume(handler);
405
+ await receive("topic-name", "consumer-group", handler);
498
406
 
499
407
  // Process specific message by ID with payload
500
- await consumer.consume(handler, { messageId: "message-id" });
408
+ await receive("topic-name", "consumer-group", handler, {
409
+ messageId: "message-id",
410
+ });
501
411
 
502
412
  // Process specific message by ID without payload (metadata only)
503
413
  // handler will be called with `undefined` as the payload
504
- 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
+ });
505
418
  ```
506
419
 
507
420
  ### Message Handler
@@ -528,12 +441,16 @@ interface MessageMetadata {
528
441
  }
529
442
  ```
530
443
 
531
- ### ConsumeOptions Interface
444
+ ### Receive Options
532
445
 
533
446
  ```typescript
534
- interface ConsumeOptions {
447
+ // Options for the receive function
448
+ interface ReceiveOptions<T = unknown> {
535
449
  messageId?: string; // Process specific message by ID
536
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
537
454
  }
538
455
  ```
539
456
 
@@ -547,44 +464,33 @@ interface Transport<T = unknown> {
547
464
  }
548
465
  ```
549
466
 
550
- ### Callback Utilities
467
+ ### Callback Handler
551
468
 
552
469
  ```typescript
553
- // Parse queue callback request headers
554
- function parseCallbackRequest(request: Request): CallbackMessageOptions;
470
+ // Create a callback handler for Next.js route handlers
471
+ function handleCallback(
472
+ handlers: CallbackHandlers,
473
+ ): (request: Request) => Promise<Response>;
555
474
 
556
- // Callback options type
557
- interface CallbackMessageOptions {
558
- queueName: string;
559
- consumerGroup: string;
560
- messageId: string;
561
- }
562
- // Create a callback handler for NextJS route handlers
563
- function handleCallback(handlers: CallbackHandlers): (request: Request) => Promise<Response>;
564
-
565
- // Configuration object with handlers for different topics
475
+ // Configuration object with handlers for different topics and consumer groups
566
476
  type CallbackHandlers = {
567
- [topicName: string]:
568
- | MessageHandler // Single handler (uses 'default' consumer group)
569
- | { [consumerGroup: string]: MessageHandler }; // Multiple consumer group handlers
477
+ [topicName: string]: { [consumerGroup: string]: MessageHandler };
570
478
  };
571
479
 
572
480
  // Example usage:
573
481
  export const POST = handleCallback({
574
- // Topic handler (uses 'default' consumer group)
575
- "new-users": (message, metadata) => {
576
- console.log(`New user event:`, message, metadata);
482
+ "user-events": {
483
+ welcome: (message, metadata) => {
484
+ console.log(`New user event:`, message, metadata);
485
+ },
577
486
  },
578
487
 
579
- // Consumer group specific handlers
488
+ // Multiple consumer groups per topic
580
489
  "image-processing": {
581
- "compress": (message, metadata) => console.log("Compressing image", message),
582
- "resize": (message, metadata) => console.log("Resizing image", message),
583
- }
490
+ compress: (message, metadata) => console.log("Compressing image", message),
491
+ resize: (message, metadata) => console.log("Resizing image", message),
492
+ },
584
493
  });
585
-
586
- // Error thrown for invalid callback requests
587
- class InvalidCallbackError extends Error;
588
494
  ```
589
495
 
590
496
  ## Examples
@@ -598,27 +504,16 @@ interface UserEvent {
598
504
  timestamp: number;
599
505
  }
600
506
 
601
- // Option 1: Using send shorthand
507
+ // Send a message
602
508
  await send<UserEvent>("user-events", {
603
509
  userId: "123",
604
510
  action: "login",
605
511
  timestamp: Date.now(),
606
512
  });
607
513
 
608
- // Option 2: Using createTopic for consumers
609
- const userTopic = createTopic<UserEvent>("user-events");
610
-
611
- await userTopic.publish({
612
- userId: "123",
613
- action: "login",
614
- timestamp: Date.now(),
615
- });
616
-
617
- const consumer = userTopic.consumerGroup("processors");
618
-
619
- // Process next available message
514
+ // Receive and process a message
620
515
  try {
621
- await consumer.consume(async (message) => {
516
+ await receive<UserEvent>("user-events", "processors", async (message) => {
622
517
  console.log(`User ${message.userId} performed ${message.action}`);
623
518
  });
624
519
  } catch (error) {
@@ -629,16 +524,13 @@ try {
629
524
  ### Processing Specific Messages by ID
630
525
 
631
526
  ```typescript
632
- const userTopic = createTopic<{ userId: string; action: string }>(
633
- "user-events",
634
- );
635
- const consumer = userTopic.consumerGroup("processors");
636
-
637
527
  // Process a specific message if you know its ID
638
528
  const messageId = "01234567-89ab-cdef-0123-456789abcdef";
639
529
 
640
530
  try {
641
- await consumer.consume(
531
+ await receive<{ userId: string; action: string }>(
532
+ "user-events",
533
+ "processors",
642
534
  async (message, { messageId }) => {
643
535
  console.log(`Processing specific message: ${messageId}`);
644
536
  console.log(`User ${message.userId} performed ${message.action}`);
@@ -649,8 +541,6 @@ try {
649
541
  } catch (error) {
650
542
  if (error.message.includes("not found or not available")) {
651
543
  console.log("Message was already processed or does not exist");
652
- } else if (error.message.includes("FIFO ordering violation")) {
653
- console.log("FIFO queue requires processing messages in order");
654
544
  } else {
655
545
  console.error("Error processing message:", error);
656
546
  }
@@ -660,21 +550,22 @@ try {
660
550
  ### Processing Next Available Message
661
551
 
662
552
  ```typescript
663
- const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
664
- const worker = workTopic.consumerGroup("workers");
665
-
666
553
  // Process the next available message (one-shot processing)
667
554
  try {
668
- await worker.consume(async (message) => {
669
- console.log(`Processing task: ${message.taskType}`);
670
- await processTask(message.taskType, message.data);
671
- });
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
+ );
672
563
  console.log("Message processed successfully");
673
564
  } catch (error) {
674
565
  if (error instanceof QueueEmptyError) {
675
566
  console.log("No messages available");
676
567
  } else if (error instanceof MessageLockedError) {
677
- console.log("Next message is locked (FIFO queue)");
568
+ console.log("Next message is locked");
678
569
  if (error.retryAfter) {
679
570
  console.log(`Retry after ${error.retryAfter} seconds`);
680
571
  }
@@ -684,17 +575,23 @@ try {
684
575
  }
685
576
 
686
577
  // Handle conditional timeouts
687
- await worker.consume(async (message) => {
688
- if (!canProcessTaskType(message.taskType)) {
689
- // Return timeout to retry later
690
- return { timeoutSeconds: 60 };
691
- }
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
+ }
692
586
 
693
- await processTask(message.taskType, message.data);
694
- });
587
+ await processTask(message.taskType, message.data);
588
+ },
589
+ );
695
590
 
696
591
  // Process specific message metadata only (no payload download)
697
- await worker.consume(
592
+ await receive<{ taskType: string; data: any }>(
593
+ "work-queue",
594
+ "workers",
698
595
  async (_, metadata) => {
699
596
  console.log(`Message ID: ${metadata.messageId}`);
700
597
  console.log(`Delivery count: ${metadata.deliveryCount}`);
@@ -708,172 +605,69 @@ await worker.consume(
708
605
  ### Timing Out Messages
709
606
 
710
607
  ```typescript
711
- const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
712
- const worker = workTopic.consumerGroup("workers");
713
-
714
608
  // Process a message with conditional timeout
715
609
  try {
716
- await worker.consume(async ({ taskType, data }) => {
717
- // Check if we can process this task type right now
718
- if (taskType === "heavy-computation" && isSystemOverloaded()) {
719
- // Return timeout to retry later (5 minutes)
720
- return { timeoutSeconds: 300 };
721
- }
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
+ }
722
619
 
723
- // Check if we have required resources
724
- if (taskType === "external-api" && !isExternalServiceAvailable()) {
725
- // Return timeout to retry in 1 minute
726
- return { timeoutSeconds: 60 };
727
- }
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
+ }
728
625
 
729
- // Process the message normally
730
- console.log(`Processing ${taskType} task`);
731
- await processTask(taskType, data);
732
- // Message will be automatically deleted on successful completion
733
- });
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
+ );
734
632
  } catch (error) {
735
633
  console.error("Worker processing error:", error);
736
634
  }
737
635
 
738
636
  // Example with exponential backoff
739
637
  try {
740
- await worker.consume(async (message, { deliveryCount }) => {
741
- const maxRetries = 3;
742
-
743
- try {
744
- await processMessage(message);
745
- // Successful processing - message will be deleted
746
- } catch (error) {
747
- if (deliveryCount < maxRetries) {
748
- // Exponential backoff: 2^deliveryCount minutes
749
- const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
750
- console.log(
751
- `Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
752
- );
753
- return { timeoutSeconds: timeoutSeconds };
754
- } else {
755
- // Max retries reached, let the message fail and be deleted
756
- console.error("Max retries reached, message will be discarded:", error);
757
- 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
+ }
758
663
  }
759
- }
760
- });
664
+ },
665
+ );
761
666
  } catch (error) {
762
667
  console.error("Backoff processing error:", error);
763
668
  }
764
669
  ```
765
670
 
766
- ### Complete Example: Video Processing Pipeline
767
-
768
- Here's a comprehensive example showing a video processing pipeline that
769
- processes videos with FFmpeg and stores the results in Vercel Blob:
770
-
771
- ```typescript
772
- import { createTopic, StreamTransport } from "@vercel/queue";
773
- import { spawn } from "child_process";
774
- import ffmpeg from "ffmpeg-static";
775
- import { put } from "@vercel/blob";
776
-
777
- // Input topic with unoptimized videos
778
- const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
779
- "unoptimized-videos",
780
- new StreamTransport(),
781
- );
782
-
783
- // Output topic for optimized videos
784
- const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
785
- "optimized-videos",
786
- new StreamTransport(),
787
- );
788
-
789
- // Step 1: Process videos with FFmpeg
790
- const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
791
-
792
- try {
793
- await videoProcessor.consume(async (inputVideoStream) => {
794
- console.log("Processing video...");
795
-
796
- if (!ffmpeg) {
797
- throw new Error("FFmpeg not available");
798
- }
799
-
800
- // Create optimized video stream using FFmpeg
801
- const optimizedStream = new ReadableStream<Uint8Array>({
802
- start(controller) {
803
- const ffmpegProcess = spawn(
804
- ffmpeg,
805
- [
806
- "-i",
807
- "pipe:0", // Input from stdin
808
- "-c:v",
809
- "libvpx-vp9", // Video codec
810
- "-c:a",
811
- "libopus", // Audio codec
812
- "-crf",
813
- "23", // Quality
814
- "-f",
815
- "webm", // Output format
816
- "pipe:1", // Output to stdout
817
- ],
818
- { stdio: ["pipe", "pipe", "pipe"] },
819
- );
820
-
821
- // Pipe input stream to FFmpeg
822
- const reader = inputVideoStream.getReader();
823
- const pipeInput = async () => {
824
- while (true) {
825
- const { done, value } = await reader.read();
826
- if (done) {
827
- ffmpegProcess.stdin?.end();
828
- break;
829
- }
830
- ffmpegProcess.stdin?.write(value);
831
- }
832
- };
833
- pipeInput();
834
-
835
- // Stream FFmpeg output
836
- ffmpegProcess.stdout?.on("data", (chunk) => {
837
- controller.enqueue(new Uint8Array(chunk));
838
- });
839
-
840
- ffmpegProcess.on("close", (code) => {
841
- if (code === 0) {
842
- controller.close();
843
- } else {
844
- controller.error(new Error(`FFmpeg failed with code ${code}`));
845
- }
846
- });
847
- },
848
- });
849
-
850
- // Publish optimized video to next topic
851
- await optimizedVideosTopic.publish(optimizedStream);
852
- console.log("Video optimized and published");
853
- });
854
- } catch (error) {
855
- console.error("Video processing error:", error);
856
- }
857
-
858
- // Step 2: Store optimized videos in Vercel Blob
859
- const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
860
-
861
- try {
862
- await blobUploader.consume(async (optimizedVideo) => {
863
- // Upload to Vercel Blob storage
864
- const filename = `optimized-${Date.now()}.webm`;
865
- const blob = await put(filename, optimizedVideo, {
866
- access: "public",
867
- contentType: "video/webm",
868
- });
869
-
870
- console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
871
- });
872
- } catch (error) {
873
- console.error("Blob upload error:", error);
874
- }
875
- ```
876
-
877
671
  ## Error Handling
878
672
 
879
673
  The queue client provides specific error types for different failure scenarios:
@@ -883,39 +677,25 @@ The queue client provides specific error types for different failure scenarios:
883
677
  - **`QueueEmptyError`**: Thrown when attempting to receive messages from an
884
678
  empty queue (204 status)
885
679
 
886
- - Thrown by `consume()` when no messages are available
887
- - Also thrown when directly using `client.receiveMessages()`
680
+ - Thrown by `receive()` when no messages are available
888
681
 
889
682
  - **`MessageLockedError`**: Thrown when a message is temporarily locked (423
890
683
  status)
891
684
 
892
685
  - Contains optional `retryAfter` property with seconds to wait before retry
893
- - For `consume()` without options: the next message in FIFO sequence is locked
894
- - 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
895
688
 
896
689
  - **`MessageNotFoundError`**: Message doesn't exist (404 status)
897
690
 
898
691
  - **`MessageNotAvailableError`**: Message exists but isn't available for
899
692
  processing (409 status)
900
693
 
901
- - **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status
902
- with nextMessageId)
903
-
904
- - Contains `nextMessageId` property indicating which message to process first
905
-
906
- - **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424
907
- status)
908
-
909
- - Contains `nextMessageId` property indicating which message must be processed
910
- first
911
- - Similar to `FifoOrderingViolationError` but specifically for receive-by-ID
912
- operations
913
-
914
694
  - **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
915
695
 
916
696
  - **`BadRequestError`**: Invalid request parameters (400 status)
917
697
 
918
- - Invalid queue names, FIFO limit violations, missing required parameters
698
+ - Invalid queue names, missing required parameters
919
699
 
920
700
  - **`UnauthorizedError`**: Authentication failure (401 status)
921
701
 
@@ -933,8 +713,6 @@ The queue client provides specific error types for different failure scenarios:
933
713
  ```typescript
934
714
  import {
935
715
  BadRequestError,
936
- FailedDependencyError,
937
- FifoOrderingViolationError,
938
716
  ForbiddenError,
939
717
  InternalServerError,
940
718
  MessageLockedError,
@@ -944,14 +722,15 @@ import {
944
722
 
945
723
  // Handle empty queue or locked messages
946
724
  try {
947
- for await (const message of client.receiveMessages(options, transport)) {
948
- // Process messages
949
- }
725
+ await receive("my-topic", "my-consumer", async (message) => {
726
+ // Process message
727
+ console.log("Processing message:", message);
728
+ });
950
729
  } catch (error) {
951
730
  if (error instanceof QueueEmptyError) {
952
731
  console.log("Queue is empty, retry later");
953
732
  } else if (error instanceof MessageLockedError) {
954
- console.log("Next message in FIFO queue is locked");
733
+ console.log("Next message is locked");
955
734
  if (error.retryAfter) {
956
735
  console.log(`Retry after ${error.retryAfter} seconds`);
957
736
  }
@@ -960,7 +739,7 @@ try {
960
739
 
961
740
  // Handle locked message with retry
962
741
  try {
963
- await consumer.consume(handler, { messageId });
742
+ await receive("my-topic", "my-consumer", handler, { messageId });
964
743
  } catch (error) {
965
744
  if (error instanceof MessageLockedError) {
966
745
  console.log("Message is locked by another consumer");
@@ -968,15 +747,12 @@ try {
968
747
  console.log(`Retry after ${error.retryAfter} seconds`);
969
748
  setTimeout(() => retry(), error.retryAfter * 1000);
970
749
  }
971
- } else if (error instanceof FailedDependencyError) {
972
- // FIFO ordering violation for receive by ID
973
- console.log(`Must process ${error.nextMessageId} first`);
974
750
  }
975
751
  }
976
752
 
977
753
  // Handle authentication and authorization errors
978
754
  try {
979
- await topic.publish(payload);
755
+ await send("my-topic", payload);
980
756
  } catch (error) {
981
757
  if (error instanceof UnauthorizedError) {
982
758
  console.log("Invalid token - refresh authentication");