@vercel/queue 0.0.0-alpha.2 → 0.0.0-alpha.4

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
@@ -1,12 +1,17 @@
1
1
  # Vercel Queues
2
2
 
3
- A TypeScript client library for interacting with the Vercel Queue Service API with customizable serialization/deserialization (transport) support, including **streaming support** for memory-efficient processing of large payloads.
3
+ A TypeScript client library for interacting with the Vercel Queue Service API
4
+ with customizable serialization/deserialization (transport) support, including
5
+ **streaming support** for memory-efficient processing of large payloads.
4
6
 
5
7
  ## Features
6
8
 
7
- - **Generic Payload Support**: Send and receive any type of data with type safety
8
- - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
9
- - **Streaming Support**: Handle large payloads without loading them entirely into memory
9
+ - **Generic Payload Support**: Send and receive any type of data with type
10
+ safety
11
+ - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream)
12
+ or create your own
13
+ - **Streaming Support**: Handle large payloads without loading them entirely
14
+ into memory
10
15
  - **Pub/Sub Pattern**: Topic-based messaging with consumer groups
11
16
  - **Type Safety**: Full TypeScript support with generic types
12
17
  - **Automatic Retries**: Built-in visibility timeout management
@@ -19,7 +24,8 @@ npm install @vercel/queue
19
24
 
20
25
  ## Quick Start
21
26
 
22
- For local development, you'll need to pull your Vercel environment variables (including the OIDC token):
27
+ For local development, you'll need to pull your Vercel environment variables
28
+ (including the OIDC token):
23
29
 
24
30
  ```bash
25
31
  # Install Vercel CLI if you haven't already
@@ -33,16 +39,33 @@ Publishing and consuming messages on a queue
33
39
 
34
40
  ```typescript
35
41
  // index.ts
36
- import { QueueClient, createTopic, JsonTransport } from "@vercel/queue";
42
+ import { send, receive } from "@vercel/queue";
37
43
 
38
- // Create a client - automatically authenticated using the OIDC token
39
- const client = QueueClient.fromVercelFunction();
44
+ type Message = {
45
+ message: string;
46
+ timestamp: number;
47
+ };
48
+
49
+ // Option 1: Using the send and receive helpers (simplest)
50
+ // Automatically uses default client and JSON transport
51
+ await send<Message>("my-topic", {
52
+ message: "Hello, World!",
53
+ timestamp: Date.now(),
54
+ });
55
+
56
+ // Consume a single message off the queue
57
+ // (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);
60
+ });
61
+
62
+ // Option 2: Using createTopic for more control
63
+
64
+ import { createTopic } from "@vercel/queue";
40
65
 
41
66
  // Create a topic with JSON serialization (default)
42
- const topic = createTopic<{ message: string; timestamp: number }>(
43
- client,
44
- "my-topic",
45
- );
67
+ // Uses default QueueClient automatically authenticated from Vercel environment
68
+ const topic = createTopic<Message>("my-topic");
46
69
 
47
70
  // Publish a message
48
71
  await topic.publish({
@@ -51,41 +74,45 @@ await topic.publish({
51
74
  });
52
75
 
53
76
  // Create a consumer group
54
- const consumer = topic.consumerGroup("my-processors");
55
-
56
- // Process messages continuously with cancellation support
57
- const controller = new AbortController();
77
+ const consumer = topic.consumerGroup("my-consumer-group");
58
78
 
59
- // Start processing (blocks until aborted or error)
79
+ // Process next available message (one-shot processing)
60
80
  try {
61
- await consumer.subscribe(controller.signal, async (message) => {
62
- console.log("Received:", message.payload.message);
63
- console.log("Timestamp:", new Date(message.payload.timestamp));
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 }
64
86
  });
65
87
  } catch (error) {
66
- console.error("Processing stopped due to error:", error);
88
+ console.error("Processing error:", error);
67
89
  }
68
-
69
- // Stop processing from elsewhere in your code
70
- // controller.abort();
71
90
  ```
72
91
 
73
92
  Run the script
74
93
 
75
94
  ```bash
76
- # Using dotenv to load the OIDC token
77
- dotenv -e .env.local node index.ts
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
78
100
  ```
79
101
 
80
102
  ## Usage with Vercel
81
103
 
82
- When deploying on Vercel, rather than having a persistent server subscribed to a queue, Vercel can trigger a callback route when a message is ready for consumption.
104
+ 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.
83
107
 
84
- To demonstrate using queues on Vercel, let's use a Next.js app. You can use an existing app or create one using [create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
108
+ To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
109
+ existing app or create one using
110
+ [create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
85
111
 
86
112
  ### TypeScript Configuration
87
113
 
88
- Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
114
+ Update your `tsconfig.json` to use `"bundler"` module resolution for proper
115
+ package export resolution:
89
116
 
90
117
  ```json
91
118
  {
@@ -101,38 +128,75 @@ Update your `tsconfig.json` to use `"bundler"` module resolution for proper pack
101
128
  Create a new server function to publish messages
102
129
 
103
130
  ```typescript
104
- // app/action.ts
131
+ // app/actions.ts
105
132
  "use server";
106
133
 
107
- import { QueueClient, createTopic } from "@vercel/queue";
134
+ import { send } from "@vercel/queue";
108
135
 
109
136
  export async function publishTestMessage(message: string) {
110
- // Initialize a queue client
111
- const client = await QueueClient.fromVercelFunction();
112
-
113
- // Create a topic with JSON serialization (default)
114
- const topic = createTopic<{ message: string; timestamp: number }>(
115
- client,
137
+ // Option 1: Using simple send shorthand
138
+ const { messageId } = await send(
116
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
+ },
117
147
  );
118
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
+
119
159
  // Publish the message
120
160
  const { messageId } = await topic.publish(
121
161
  { message, timestamp: Date.now() },
122
162
  {
123
- // Provide a callback URL to invoke a consumer when the message is ready to be processed
124
- callbacks: {
125
- webhook: {
126
- url: process.env.VERCEL_PROJECT_PRODUCTION_URL
127
- ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}/api/queue/handle`
128
- : "http://localhost:3000/api/queue/handle",
129
- },
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
+ }
130
174
  },
131
175
  },
132
176
  );
133
177
 
134
178
  console.log(`Published message ${messageId}`);
135
179
  }
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
+
136
200
  ```
137
201
 
138
202
  Now wire up the server function to your app
@@ -142,76 +206,56 @@ Now wire up the server function to your app
142
206
  "use client";
143
207
  import { publishTestMessage } from "./actions";
144
208
 
145
- export default function Button() {
209
+ export default function Page() {
146
210
  return (
147
211
  // ...
148
- <Button onClick={() => publishTestMessage("Hello world")} >
212
+ <button onClick={() => publishTestMessage("Hello world")}>
149
213
  Publish Test Message
150
- </a>
214
+ </button>
151
215
  );
152
216
  }
153
217
  ```
154
218
 
155
219
  ### Consuming the queue
156
220
 
157
- Instead of running a persistent server that subscribes to the queue, we use the callback functionality of Vercel queues to consume messages on the fly, when a message is ready to be processed.
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.
224
+
225
+ The `handleCallback` helper function simplifies queue callback handling in NextJS:
158
226
 
159
227
  ```typescript
160
228
  // app/api/queue/handle/route.ts
161
- import { QueueClient, Topic, parseCallbackRequest } from "@vercel/queue";
162
- import { NextRequest } from "next/server";
163
-
164
- // Handle Vercel Queue callback requests
165
- export async function POST(request: NextRequest) {
166
- try {
167
- // Parse the queue callback information
168
- const { queueName, consumerGroup, messageId } =
169
- parseCallbackRequest(request);
170
-
171
- // Create client
172
- const client = await QueueClient.fromVercelFunction();
173
-
174
- // Create topic and consumer group from the callback info
175
- const topic = new Topic(client, queueName);
176
- const cg = topic.consumerGroup(consumerGroup);
177
-
178
- // Process the message
179
- await cg.receiveMessage(messageId, async (message) => {
180
- const payload = message.payload as { message: string; timestamp: number };
181
- console.log(
182
- `Received message "${payload.message}" (Sent at: ${payload.timestamp})`,
183
- );
184
- });
229
+ import { handleCallback } from "@vercel/queue";
185
230
 
186
- return Response.json({ status: "success" });
187
- } catch (error) {
188
- console.error("Webhook error:", error);
189
- return Response.json(
190
- { error: "Failed to process webhook" },
191
- { status: 500 },
192
- );
193
- }
194
- }
195
- ```
231
+ // Option 1: Specify a single handler for the topic
232
+ export const POST = handleCallback({
233
+ "my-topic": (message, metadata) => {
234
+ console.log(`Received message:`, message, metadata);
235
+ // metadata: { messageId, deliveryCount, timestamp }
236
+ },
237
+
238
+ // .. more topic handlers can be provided here
239
+ });
240
+
241
+ // This consumes messages on the "default" consumer group, which is used when no consumer groups
242
+ // were specified in the publish `callback` earlierA
196
243
 
197
- > ![NOTE]
198
- > A single webhook handle can be used to process messages across various queues and consumer groups. Use the values of `queueName` and `consumerGroup` from `parseCallbackRequest()` to dynamically handle different code paths:
199
- >
200
- > ```typescript
201
- > if (queueName === "upload-queue") {
202
- > processImageQueue(consumerGroup, message);
203
- > }
204
- > // ...
205
- > function processImageQueue(consumerGroup, message) {
206
- > if (consumerGroup === "compress") {
207
- > // handle image compression
208
- > }
209
- > // ...
210
- > }
211
- > // ...
212
- > ```
213
- >
214
- > We are building an SDK to make queues and workflow easier to use. Reach out if you're interested.
244
+ // Option 2: Multiple consumer groups
245
+ 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);
251
+ },
252
+ // consumer group: "resize"
253
+ "consume-group-2": (message, metadata) => {
254
+ console.log("Message", message);
255
+ },
256
+ },
257
+ });
258
+ ```
215
259
 
216
260
  ## Key Features
217
261
 
@@ -220,18 +264,16 @@ export async function POST(request: NextRequest) {
220
264
  Handle large files and data streams without loading them into memory:
221
265
 
222
266
  ```typescript
223
- import { StreamTransport } from "@vercel/queue";
267
+ import { createTopic, StreamTransport } from "@vercel/queue";
224
268
 
225
269
  const videoTopic = createTopic<ReadableStream<Uint8Array>>(
226
- client,
227
270
  "video-processing",
228
271
  new StreamTransport(),
229
272
  );
230
273
 
231
274
  // Process large video files efficiently
232
275
  const processor = videoTopic.consumerGroup("processors");
233
- await processor.subscribe(signal, async (message) => {
234
- const videoStream = message.payload;
276
+ await processor.consume(async (videoStream) => {
235
277
  // Process stream chunk by chunk
236
278
  const reader = videoStream.getReader();
237
279
  while (true) {
@@ -261,12 +303,14 @@ const webhooks = topic.consumerGroup("webhooks");
261
303
  ## Architecture
262
304
 
263
305
  - **Topics**: Named message channels with configurable serialization
264
- - **Consumer Groups**: Named groups of consumers that process messages in parallel
265
- - `subscribe()`: Continuously process messages with automatic polling
266
- - `receiveMessage()`: Process a specific message by ID
267
- - `receiveNextMessage()`: Process the next available message (one-shot)
268
- - `handleMessage()`: Process message metadata only (without payload)
269
- - **Transports**: Pluggable serialization/deserialization for different data types
306
+ - **Consumer Groups**: Named groups of consumers that process messages in
307
+ parallel
308
+ - `consume()`: Process messages with flexible consumption patterns
309
+ - No options: Process next available message
310
+ - With `messageId`: Process specific message by ID
311
+ - With `skipPayload: true`: Process message metadata only (without payload)
312
+ - **Transports**: Pluggable serialization/deserialization for different data
313
+ types
270
314
  - **Streaming**: Memory-efficient processing of large payloads
271
315
  - **Visibility Timeouts**: Automatic message lifecycle management
272
316
 
@@ -277,55 +321,53 @@ The multipart parser is optimized for high-throughput scenarios:
277
321
  - **Streaming**: Messages are yielded immediately as headers are parsed
278
322
  - **Memory Efficient**: No buffering of complete payloads
279
323
  - **Fast Parsing**: Native Buffer operations for ~50% performance improvement
280
- - **Scalable**: Can handle arbitrarily large responses without memory constraints
324
+ - **Scalable**: Can handle arbitrarily large responses without memory
325
+ constraints
281
326
 
282
327
  ## Serialization (Transport) System
283
328
 
284
- The queue client supports customizable serialization through the `Transport` interface with **streaming support** for memory-efficient processing. Transport can be configured at the **topic level** when creating a topic, or at the **consumer group level** when creating a consumer group.
329
+ The queue client supports customizable serialization through the `Transport`
330
+ 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.
285
333
 
286
334
  ### Built-in Transports
287
335
 
288
336
  #### JsonTransport (Default)
289
337
 
290
- Buffers data for JSON parsing - suitable for structured data that fits in memory.
338
+ Buffers data for JSON parsing - suitable for structured data that fits in
339
+ memory.
291
340
 
292
341
  ```typescript
293
- import { JsonTransport, createTopic } from "@vercel/queue";
342
+ import { createTopic, JsonTransport } from "@vercel/queue";
294
343
 
295
- const topic = createTopic<{ data: any }>(
296
- client,
297
- "json-topic",
298
- new JsonTransport(),
299
- );
344
+ const topic = createTopic<{ data: any }>("json-topic", new JsonTransport());
300
345
  // or simply (JsonTransport is the default):
301
- const topic = createTopic<{ data: any }>(client, "json-topic");
346
+ const topic = createTopic<{ data: any }>("json-topic");
302
347
  ```
303
348
 
304
349
  #### BufferTransport
305
350
 
306
- Buffers the entire payload into memory as a Buffer - suitable for binary data that fits in memory.
351
+ Buffers the entire payload into memory as a Buffer - suitable for binary data
352
+ that fits in memory.
307
353
 
308
354
  ```typescript
309
355
  import { BufferTransport, createTopic } from "@vercel/queue";
310
356
 
311
- const topic = createTopic<Buffer>(
312
- client,
313
- "binary-topic",
314
- new BufferTransport(),
315
- );
357
+ const topic = createTopic<Buffer>("binary-topic", new BufferTransport());
316
358
  const binaryData = Buffer.from("Binary data", "utf8");
317
359
  await topic.publish(binaryData);
318
360
  ```
319
361
 
320
362
  #### StreamTransport
321
363
 
322
- **True streaming support** - passes ReadableStream directly without buffering. Ideal for large files and memory-efficient processing.
364
+ **True streaming support** - passes ReadableStream directly without buffering.
365
+ Ideal for large files and memory-efficient processing.
323
366
 
324
367
  ```typescript
325
- import { StreamTransport, createTopic } from "@vercel/queue";
368
+ import { createTopic, StreamTransport } from "@vercel/queue";
326
369
 
327
370
  const topic = createTopic<ReadableStream<Uint8Array>>(
328
- client,
329
371
  "streaming-topic",
330
372
  new StreamTransport(),
331
373
  );
@@ -346,7 +388,8 @@ await topic.publish(fileStream);
346
388
 
347
389
  ### Custom Transport
348
390
 
349
- You can create your own serialization format by implementing the `Transport` interface:
391
+ You can create your own serialization format by implementing the `Transport`
392
+ interface:
350
393
 
351
394
  ```typescript
352
395
  import { Transport } from "@vercel/queue";
@@ -373,8 +416,12 @@ interface Transport<T = unknown> {
373
416
  ### QueueClient
374
417
 
375
418
  ```typescript
419
+ // Simple usage - automatically gets OIDC token from Vercel environment
420
+ const client = new QueueClient();
421
+
422
+ // Or with options
376
423
  const client = new QueueClient({
377
- token: string;
424
+ token?: string; // Optional - will auto-detect if not provided
378
425
  baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
379
426
  });
380
427
  ```
@@ -382,37 +429,88 @@ const client = new QueueClient({
382
429
  ### Topic
383
430
 
384
431
  ```typescript
385
- const topic = createTopic<T>(client, topicName, transport?);
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?);
386
438
 
387
439
  // Publish a message (uses topic's transport)
388
440
  await topic.publish(payload, options?);
389
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
+
390
457
  // Create a consumer group (can override transport)
391
458
  const consumer = topic.consumerGroup<U>(groupName, options?);
392
459
  ```
393
460
 
394
- ### ConsumerGroup
461
+ ### Send (Shorthand)
395
462
 
396
463
  ```typescript
397
- // Start continuous processing (blocks until signal is aborted or error occurs)
398
- await consumer.subscribe(signal, handler, options?);
464
+ // Simple send - automatically uses default client and JSON transport
465
+ await send<T>(topicName, payload);
466
+
467
+ // Send with options including custom transport
468
+ await send<T>(topicName, payload, {
469
+ transport?: Transport<T>;
470
+ idempotencyKey?: string;
471
+ retentionSeconds?: number;
472
+ callback?: Record<string, CallbackConfig> | CallbackConfig;
473
+ });
474
+
475
+ // Examples:
476
+ await send("notifications", { userId: "123", message: "Welcome!" });
477
+
478
+ await send("images", imageBuffer, {
479
+ transport: new BufferTransport(),
480
+ callback: { url: "https://example.com/process-image" }
481
+ });
399
482
 
400
- // Process a specific message by ID
401
- await consumer.receiveMessage(messageId, handler);
483
+ await send("events", eventData, {
484
+ idempotencyKey: "unique-key-123",
485
+ retentionSeconds: 3600,
486
+ callback: {
487
+ analytics: { url: "https://analytics.example.com/webhook" },
488
+ notifications: { url: "https://notifications.example.com/webhook", delay: 30 }
489
+ }
490
+ });
491
+ ```
402
492
 
403
- // Process the next available message
404
- await consumer.receiveNextMessage(handler);
493
+ ### ConsumerGroup
405
494
 
406
- // Handle a specific message by ID without payload
407
- await consumer.handleMessage(messageId, handler);
495
+ ```typescript
496
+ // Process next available message (simplest form)
497
+ await consumer.consume(handler);
498
+
499
+ // Process specific message by ID with payload
500
+ await consumer.consume(handler, { messageId: "message-id" });
501
+
502
+ // Process specific message by ID without payload (metadata only)
503
+ // handler will be called with `undefined` as the payload
504
+ await consumer.consume(handler, { messageId: "message-id", skipPayload: true });
408
505
  ```
409
506
 
410
507
  ### Message Handler
411
508
 
412
509
  ```typescript
413
510
  // Handler function signature
414
- type MessageHandler<T> = (
415
- message: Message<T>,
511
+ type MessageHandler<T = unknown> = (
512
+ message: T,
513
+ metadata: MessageMetadata,
416
514
  ) => Promise<MessageHandlerResult> | MessageHandlerResult;
417
515
 
418
516
  // Handler result types
@@ -421,6 +519,22 @@ type MessageHandlerResult = void | MessageTimeoutResult;
421
519
  interface MessageTimeoutResult {
422
520
  timeoutSeconds: number; // seconds before message becomes available again
423
521
  }
522
+
523
+ // Message Metadata
524
+ interface MessageMetadata {
525
+ messageId: string;
526
+ deliveryCount: number;
527
+ timestamp: string;
528
+ }
529
+ ```
530
+
531
+ ### ConsumeOptions Interface
532
+
533
+ ```typescript
534
+ interface ConsumeOptions {
535
+ messageId?: string; // Process specific message by ID
536
+ skipPayload?: boolean; // Skip payload download (requires messageId)
537
+ }
424
538
  ```
425
539
 
426
540
  ### Transport Interface
@@ -445,6 +559,29 @@ interface CallbackMessageOptions {
445
559
  consumerGroup: string;
446
560
  messageId: string;
447
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
566
+ type CallbackHandlers = {
567
+ [topicName: string]:
568
+ | MessageHandler // Single handler (uses 'default' consumer group)
569
+ | { [consumerGroup: string]: MessageHandler }; // Multiple consumer group handlers
570
+ };
571
+
572
+ // Example usage:
573
+ export const POST = handleCallback({
574
+ // Topic handler (uses 'default' consumer group)
575
+ "new-users": (message, metadata) => {
576
+ console.log(`New user event:`, message, metadata);
577
+ },
578
+
579
+ // Consumer group specific handlers
580
+ "image-processing": {
581
+ "compress": (message, metadata) => console.log("Compressing image", message),
582
+ "resize": (message, metadata) => console.log("Resizing image", message),
583
+ }
584
+ });
448
585
 
449
586
  // Error thrown for invalid callback requests
450
587
  class InvalidCallbackError extends Error;
@@ -461,7 +598,15 @@ interface UserEvent {
461
598
  timestamp: number;
462
599
  }
463
600
 
464
- const userTopic = createTopic<UserEvent>(client, "user-events");
601
+ // Option 1: Using send shorthand
602
+ await send<UserEvent>("user-events", {
603
+ userId: "123",
604
+ action: "login",
605
+ timestamp: Date.now(),
606
+ });
607
+
608
+ // Option 2: Using createTopic for consumers
609
+ const userTopic = createTopic<UserEvent>("user-events");
465
610
 
466
611
  await userTopic.publish({
467
612
  userId: "123",
@@ -470,27 +615,21 @@ await userTopic.publish({
470
615
  });
471
616
 
472
617
  const consumer = userTopic.consumerGroup("processors");
473
- const controller = new AbortController();
474
618
 
619
+ // Process next available message
475
620
  try {
476
- await consumer.subscribe(controller.signal, async (message) => {
477
- console.log(
478
- `User ${message.payload.userId} performed ${message.payload.action}`,
479
- );
621
+ await consumer.consume(async (message) => {
622
+ console.log(`User ${message.userId} performed ${message.action}`);
480
623
  });
481
624
  } catch (error) {
482
625
  console.error("Processing error:", error);
483
626
  }
484
-
485
- // Stop processing when needed
486
- // controller.abort();
487
627
  ```
488
628
 
489
629
  ### Processing Specific Messages by ID
490
630
 
491
631
  ```typescript
492
632
  const userTopic = createTopic<{ userId: string; action: string }>(
493
- client,
494
633
  "user-events",
495
634
  );
496
635
  const consumer = userTopic.consumerGroup("processors");
@@ -499,12 +638,13 @@ const consumer = userTopic.consumerGroup("processors");
499
638
  const messageId = "01234567-89ab-cdef-0123-456789abcdef";
500
639
 
501
640
  try {
502
- await consumer.receiveMessage(messageId, async (message) => {
503
- console.log(`Processing specific message: ${message.messageId}`);
504
- console.log(
505
- `User ${message.payload.userId} performed ${message.payload.action}`,
506
- );
507
- });
641
+ await consumer.consume(
642
+ async (message, { messageId }) => {
643
+ console.log(`Processing specific message: ${messageId}`);
644
+ console.log(`User ${message.userId} performed ${message.action}`);
645
+ },
646
+ { messageId },
647
+ );
508
648
  console.log("Message processed successfully");
509
649
  } catch (error) {
510
650
  if (error.message.includes("not found or not available")) {
@@ -520,17 +660,14 @@ try {
520
660
  ### Processing Next Available Message
521
661
 
522
662
  ```typescript
523
- const workTopic = createTopic<{ taskType: string; data: any }>(
524
- client,
525
- "work-queue",
526
- );
663
+ const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
527
664
  const worker = workTopic.consumerGroup("workers");
528
665
 
529
666
  // Process the next available message (one-shot processing)
530
667
  try {
531
- await worker.receiveNextMessage(async (message) => {
532
- console.log(`Processing task: ${message.payload.taskType}`);
533
- await processTask(message.payload.taskType, message.payload.data);
668
+ await worker.consume(async (message) => {
669
+ console.log(`Processing task: ${message.taskType}`);
670
+ await processTask(message.taskType, message.data);
534
671
  });
535
672
  console.log("Message processed successfully");
536
673
  } catch (error) {
@@ -546,31 +683,37 @@ try {
546
683
  }
547
684
  }
548
685
 
549
- // You can also use it with timeout results
550
- await worker.receiveNextMessage(async (message) => {
551
- if (!canProcessTaskType(message.payload.taskType)) {
686
+ // Handle conditional timeouts
687
+ await worker.consume(async (message) => {
688
+ if (!canProcessTaskType(message.taskType)) {
552
689
  // Return timeout to retry later
553
690
  return { timeoutSeconds: 60 };
554
691
  }
555
692
 
556
- await processTask(message.payload.taskType, message.payload.data);
693
+ await processTask(message.taskType, message.data);
557
694
  });
695
+
696
+ // Process specific message metadata only (no payload download)
697
+ await worker.consume(
698
+ async (_, metadata) => {
699
+ console.log(`Message ID: ${metadata.messageId}`);
700
+ console.log(`Delivery count: ${metadata.deliveryCount}`);
701
+ console.log(`Timestamp: ${metadata.timestamp}`);
702
+ // _ is undefined - no payload was downloaded
703
+ },
704
+ { messageId: "specific-message-id", skipPayload: true },
705
+ );
558
706
  ```
559
707
 
560
708
  ### Timing Out Messages
561
709
 
562
710
  ```typescript
563
- const workTopic = createTopic<{ taskType: string; data: any }>(
564
- client,
565
- "work-queue",
566
- );
711
+ const workTopic = createTopic<{ taskType: string; data: any }>("work-queue");
567
712
  const worker = workTopic.consumerGroup("workers");
568
- const controller = new AbortController();
569
713
 
714
+ // Process a message with conditional timeout
570
715
  try {
571
- await worker.subscribe(controller.signal, async (message) => {
572
- const { taskType, data } = message.payload;
573
-
716
+ await worker.consume(async ({ taskType, data }) => {
574
717
  // Check if we can process this task type right now
575
718
  if (taskType === "heavy-computation" && isSystemOverloaded()) {
576
719
  // Return timeout to retry later (5 minutes)
@@ -593,15 +736,12 @@ try {
593
736
  }
594
737
 
595
738
  // Example with exponential backoff
596
- const backoffController = new AbortController();
597
-
598
739
  try {
599
- await worker.subscribe(backoffController.signal, async (message) => {
740
+ await worker.consume(async (message, { deliveryCount }) => {
600
741
  const maxRetries = 3;
601
- const deliveryCount = message.deliveryCount;
602
742
 
603
743
  try {
604
- await processMessage(message.payload);
744
+ await processMessage(message);
605
745
  // Successful processing - message will be deleted
606
746
  } catch (error) {
607
747
  if (deliveryCount < maxRetries) {
@@ -625,114 +765,101 @@ try {
625
765
 
626
766
  ### Complete Example: Video Processing Pipeline
627
767
 
628
- Here's a comprehensive example showing a video processing pipeline that processes videos with FFmpeg and stores the results in Vercel Blob:
768
+ Here's a comprehensive example showing a video processing pipeline that
769
+ processes videos with FFmpeg and stores the results in Vercel Blob:
629
770
 
630
771
  ```typescript
631
- import { QueueClient, createTopic, StreamTransport } from "@vercel/queue";
772
+ import { createTopic, StreamTransport } from "@vercel/queue";
632
773
  import { spawn } from "child_process";
633
774
  import ffmpeg from "ffmpeg-static";
634
775
  import { put } from "@vercel/blob";
635
776
 
636
- const client = new QueueClient({
637
- token: "your-vercel-oidc-token",
638
- });
639
-
640
777
  // Input topic with unoptimized videos
641
778
  const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
642
- client,
643
779
  "unoptimized-videos",
644
780
  new StreamTransport(),
645
781
  );
646
782
 
647
783
  // Output topic for optimized videos
648
784
  const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
649
- client,
650
785
  "optimized-videos",
651
786
  new StreamTransport(),
652
787
  );
653
788
 
654
789
  // Step 1: Process videos with FFmpeg
655
790
  const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
656
- const processingController = new AbortController();
657
791
 
658
792
  try {
659
- await videoProcessor.subscribe(
660
- processingController.signal,
661
- async (message) => {
662
- const inputVideoStream = message.payload;
663
- console.log("Processing video...");
664
-
665
- if (!ffmpeg) {
666
- throw new Error("FFmpeg not available");
667
- }
793
+ await videoProcessor.consume(async (inputVideoStream) => {
794
+ console.log("Processing video...");
668
795
 
669
- // Create optimized video stream using FFmpeg
670
- const optimizedStream = new ReadableStream<Uint8Array>({
671
- start(controller) {
672
- const ffmpegProcess = spawn(
673
- ffmpeg,
674
- [
675
- "-i",
676
- "pipe:0", // Input from stdin
677
- "-c:v",
678
- "libvpx-vp9", // Video codec
679
- "-c:a",
680
- "libopus", // Audio codec
681
- "-crf",
682
- "23", // Quality
683
- "-f",
684
- "webm", // Output format
685
- "pipe:1", // Output to stdout
686
- ],
687
- { stdio: ["pipe", "pipe", "pipe"] },
688
- );
689
-
690
- // Pipe input stream to FFmpeg
691
- const reader = inputVideoStream.getReader();
692
- const pipeInput = async () => {
693
- while (true) {
694
- const { done, value } = await reader.read();
695
- if (done) {
696
- ffmpegProcess.stdin?.end();
697
- break;
698
- }
699
- ffmpegProcess.stdin?.write(value);
700
- }
701
- };
702
- pipeInput();
703
-
704
- // Stream FFmpeg output
705
- ffmpegProcess.stdout?.on("data", (chunk) => {
706
- controller.enqueue(new Uint8Array(chunk));
707
- });
708
-
709
- ffmpegProcess.on("close", (code) => {
710
- if (code === 0) {
711
- controller.close();
712
- } else {
713
- controller.error(new Error(`FFmpeg failed with code ${code}`));
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;
714
829
  }
715
- });
716
- },
717
- });
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
+ });
718
849
 
719
- // Publish optimized video to next topic
720
- await optimizedVideosTopic.publish(optimizedStream);
721
- console.log("Video optimized and published");
722
- },
723
- );
850
+ // Publish optimized video to next topic
851
+ await optimizedVideosTopic.publish(optimizedStream);
852
+ console.log("Video optimized and published");
853
+ });
724
854
  } catch (error) {
725
855
  console.error("Video processing error:", error);
726
856
  }
727
857
 
728
858
  // Step 2: Store optimized videos in Vercel Blob
729
859
  const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
730
- const uploadController = new AbortController();
731
860
 
732
861
  try {
733
- await blobUploader.subscribe(uploadController.signal, async (message) => {
734
- const optimizedVideo = message.payload;
735
-
862
+ await blobUploader.consume(async (optimizedVideo) => {
736
863
  // Upload to Vercel Blob storage
737
864
  const filename = `optimized-${Date.now()}.webm`;
738
865
  const blob = await put(filename, optimizedVideo, {
@@ -745,12 +872,6 @@ try {
745
872
  } catch (error) {
746
873
  console.error("Blob upload error:", error);
747
874
  }
748
-
749
- // Graceful shutdown
750
- process.on("SIGINT", () => {
751
- processingController.abort();
752
- uploadController.abort();
753
- });
754
875
  ```
755
876
 
756
877
  ## Error Handling
@@ -759,30 +880,36 @@ The queue client provides specific error types for different failure scenarios:
759
880
 
760
881
  ### Error Types
761
882
 
762
- - **`QueueEmptyError`**: Thrown when attempting to receive messages from an empty queue (204 status)
883
+ - **`QueueEmptyError`**: Thrown when attempting to receive messages from an
884
+ empty queue (204 status)
763
885
 
764
- - Only thrown when directly using `client.receiveMessages()`
765
- - `ConsumerGroup.subscribe()` handles this error internally and continues polling
886
+ - Thrown by `consume()` when no messages are available
887
+ - Also thrown when directly using `client.receiveMessages()`
766
888
 
767
- - **`MessageLockedError`**: Thrown when a message is temporarily locked (423 status)
889
+ - **`MessageLockedError`**: Thrown when a message is temporarily locked (423
890
+ status)
768
891
 
769
892
  - Contains optional `retryAfter` property with seconds to wait before retry
770
- - For `receiveMessages()` on FIFO queues: the next message in sequence is locked
771
- - For `receiveMessageById()`: the requested message is locked
772
- - `ConsumerGroup.subscribe()` handles this error internally when polling
893
+ - For `consume()` without options: the next message in FIFO sequence is locked
894
+ - For `consume()` with messageId: the requested message is locked
773
895
 
774
896
  - **`MessageNotFoundError`**: Message doesn't exist (404 status)
775
897
 
776
- - **`MessageNotAvailableError`**: Message exists but isn't available for processing (409 status)
898
+ - **`MessageNotAvailableError`**: Message exists but isn't available for
899
+ processing (409 status)
777
900
 
778
- - **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status with nextMessageId)
901
+ - **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status
902
+ with nextMessageId)
779
903
 
780
904
  - Contains `nextMessageId` property indicating which message to process first
781
905
 
782
- - **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424 status)
906
+ - **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424
907
+ status)
783
908
 
784
- - Contains `nextMessageId` property indicating which message must be processed first
785
- - Similar to `FifoOrderingViolationError` but specifically for receive-by-ID operations
909
+ - Contains `nextMessageId` property indicating which message must be processed
910
+ first
911
+ - Similar to `FifoOrderingViolationError` but specifically for receive-by-ID
912
+ operations
786
913
 
787
914
  - **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
788
915
 
@@ -805,14 +932,14 @@ The queue client provides specific error types for different failure scenarios:
805
932
 
806
933
  ```typescript
807
934
  import {
808
- QueueEmptyError,
809
- MessageLockedError,
810
- FifoOrderingViolationError,
811
- FailedDependencyError,
812
935
  BadRequestError,
813
- UnauthorizedError,
936
+ FailedDependencyError,
937
+ FifoOrderingViolationError,
814
938
  ForbiddenError,
815
939
  InternalServerError,
940
+ MessageLockedError,
941
+ QueueEmptyError,
942
+ UnauthorizedError,
816
943
  } from "@vercel/queue";
817
944
 
818
945
  // Handle empty queue or locked messages
@@ -833,7 +960,7 @@ try {
833
960
 
834
961
  // Handle locked message with retry
835
962
  try {
836
- await consumer.receiveMessage(messageId, handler);
963
+ await consumer.consume(handler, { messageId });
837
964
  } catch (error) {
838
965
  if (error instanceof MessageLockedError) {
839
966
  console.log("Message is locked by another consumer");