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

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
@@ -89,21 +89,11 @@ try {
89
89
  }
90
90
  ```
91
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
- ```
101
-
102
92
  ## Usage with Vercel
103
93
 
104
94
  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.
95
+ queue, Vercel can automatically trigger your API routes when messages are ready for
96
+ consumption based on your vercel.json configuration.
107
97
 
108
98
  To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
109
99
  existing app or create one using
@@ -138,11 +128,6 @@ export async function publishTestMessage(message: string) {
138
128
  const { messageId } = await send(
139
129
  "my-topic",
140
130
  { 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
131
  },
147
132
  );
148
133
 
@@ -159,44 +144,11 @@ export async function publishTestMessage(message: string) {
159
144
  // Publish the message
160
145
  const { messageId } = await topic.publish(
161
146
  { 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
147
  );
177
148
 
178
149
  console.log(`Published message ${messageId}`);
179
150
  }
180
151
 
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
152
  ```
201
153
 
202
154
  Now wire up the server function to your app
@@ -218,72 +170,160 @@ export default function Page() {
218
170
 
219
171
  ### Consuming the queue
220
172
 
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.
173
+ Messages are consumed using consumer groups, which provide load balancing and parallel processing capabilities.
174
+
175
+ ## Usage with Vercel
176
+
177
+ To consume queue messages in a Vercel deployment, you need to create (Next.js) API routes and configure them in your `vercel.json` file.
178
+
179
+ ### 1. Create API Routes
224
180
 
225
- The `handleCallback` helper function simplifies queue callback handling in NextJS:
181
+ Create API routes to handle incoming queue messages using the `handleCallback` helper:
226
182
 
227
183
  ```typescript
228
184
  // app/api/queue/handle/route.ts
229
185
  import { handleCallback } from "@vercel/queue";
230
186
 
231
- // Option 1: Specify a single handler for the topic
187
+ // Option 1: Single topic with multiple consumer groups
232
188
  export const POST = handleCallback({
233
- "my-topic": (message, metadata) => {
234
- console.log(`Received message:`, message, metadata);
235
- // metadata: { messageId, deliveryCount, timestamp }
189
+ "my-topic": {
190
+ "consumer-group-1": async (message, metadata) => {
191
+ console.log(`Consumer group 1 processing:`, message, metadata);
192
+ // Handle consumer group 1 logic
193
+ await processGroup1(message);
194
+ },
195
+ "consumer-group-2": async (message, metadata) => {
196
+ console.log(`Consumer group 2 processing:`, message, metadata);
197
+ // Handle consumer group 2 logic
198
+ await processGroup2(message);
199
+ },
236
200
  },
237
-
238
- // .. more topic handlers can be provided here
239
201
  });
240
202
 
241
- // This consumes messages on the "default" consumer group, which is used when no consumer groups
242
- // were specified in the publish `callback` earlierA
203
+ async function processGroup1(message: any) {
204
+ // Consumer group 1 specific logic
205
+ }
243
206
 
244
- // Option 2: Multiple consumer groups
207
+ async function processGroup2(message: any) {
208
+ // Consumer group 2 specific logic
209
+ }
210
+ ```
211
+
212
+ ```typescript
213
+ // Alternative: Multiple topics in one handler
245
214
  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);
215
+ "user-events": {
216
+ welcome: async (message, metadata) => {
217
+ console.log(`New user event:`, message, metadata);
218
+ await sendWelcomeEmail(message.email);
219
+ },
220
+ },
221
+ "order-events": {
222
+ fulfillment: async (order, metadata) => {
223
+ console.log(`Processing order:`, order, metadata);
224
+ await fulfillOrder(order);
251
225
  },
252
- // consumer group: "resize"
253
- "consume-group-2": (message, metadata) => {
254
- console.log("Message", message);
226
+ analytics: async (order, metadata) => {
227
+ console.log(`Tracking order:`, order, metadata);
228
+ await trackOrder(order);
255
229
  },
256
230
  },
257
231
  });
258
232
  ```
259
233
 
260
- ## Key Features
234
+ ### 2. Configure vercel.json
261
235
 
262
- ### Streaming Support
236
+ Create a `vercel.json` file in your project root to declare which topics and consumer groups each API route handles:
263
237
 
264
- Handle large files and data streams without loading them into memory:
238
+ ```json
239
+ {
240
+ "functions": {
241
+ "app/api/queue/handle/route.ts": {
242
+ "experimentalTriggers": [
243
+ {
244
+ "type": "queue/v1beta",
245
+ "topic": "my-topic",
246
+ "consumer": "consumer-group-1"
247
+ },
248
+ {
249
+ "type": "queue/v1beta",
250
+ "topic": "my-topic",
251
+ "consumer": "consumer-group-2"
252
+ }
253
+ ]
254
+ }
255
+ }
256
+ }
257
+ ```
258
+
259
+ ### 3. Multiple API Routes
260
+
261
+ You can also create separate API routes for different topics:
265
262
 
266
263
  ```typescript
267
- import { createTopic, StreamTransport } from "@vercel/queue";
264
+ // app/api/queue/users/route.ts - Handle user events
265
+ import { handleCallback } from "@vercel/queue";
268
266
 
269
- const videoTopic = createTopic<ReadableStream<Uint8Array>>(
270
- "video-processing",
271
- new StreamTransport(),
272
- );
267
+ export const POST = handleCallback({
268
+ "user-events": {
269
+ processors: async (user, metadata) => {
270
+ console.log(`Processing user event:`, user, metadata);
271
+ await sendWelcomeEmail(user.email);
272
+ },
273
+ },
274
+ });
275
+ ```
273
276
 
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
- }
277
+ ```typescript
278
+ // app/api/queue/orders/route.ts - Handle order events
279
+ import { handleCallback } from "@vercel/queue";
280
+
281
+ export const POST = handleCallback({
282
+ "order-events": {
283
+ fulfillment: async (order, metadata) => {
284
+ console.log(`Processing order:`, order, metadata);
285
+ await fulfillOrder(order);
286
+ },
287
+ },
284
288
  });
285
289
  ```
286
290
 
291
+ With corresponding `vercel.json`:
292
+
293
+ ```json
294
+ {
295
+ "functions": {
296
+ "app/api/queue/users/route.ts": {
297
+ "experimentalTriggers": [
298
+ {
299
+ "type": "queue/v1beta",
300
+ "topic": "user-events",
301
+ "consumer": "processors"
302
+ }
303
+ ]
304
+ },
305
+ "app/api/queue/orders/route.ts": {
306
+ "experimentalTriggers": [
307
+ {
308
+ "type": "queue/v1beta",
309
+ "topic": "order-events",
310
+ "consumer": "fulfillment"
311
+ }
312
+ ]
313
+ }
314
+ }
315
+ }
316
+ ```
317
+
318
+ ### Key Points
319
+
320
+ - **Automatic Triggering**: Vercel automatically triggers your API routes when messages are available for the configured topic/consumer combinations
321
+ - **Message Processing**: Your API routes receive the message ID and other metadata via headers, then use the queue client to process the specific message
322
+ - **Configuration Required**: The `vercel.json` file is essential - it tells Vercel which topics and consumers each route should handle
323
+ - **No Polling**: Unlike traditional queue consumers, you don't need to poll for messages - Vercel handles the triggering automatically
324
+
325
+ ## Key Features
326
+
287
327
  ### Consumer Groups
288
328
 
289
329
  Multiple consumers can process messages from the same topic in parallel:
@@ -351,55 +391,15 @@ const topic = createTopic<{ data: any }>("json-topic");
351
391
  Buffers the entire payload into memory as a Buffer - suitable for binary data
352
392
  that fits in memory.
353
393
 
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
394
  #### StreamTransport
363
395
 
364
396
  **True streaming support** - passes ReadableStream directly without buffering.
365
397
  Ideal for large files and memory-efficient processing.
366
398
 
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
399
  ### Custom Transport
390
400
 
391
401
  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
- ```
402
+ interface.
403
403
 
404
404
  ### Choosing the Right Transport
405
405
 
@@ -439,21 +439,6 @@ const topic = new Topic<T>(customClient, topicName, transport?);
439
439
  // Publish a message (uses topic's transport)
440
440
  await topic.publish(payload, options?);
441
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
442
  // Create a consumer group (can override transport)
458
443
  const consumer = topic.consumerGroup<U>(groupName, options?);
459
444
  ```
@@ -469,7 +454,6 @@ await send<T>(topicName, payload, {
469
454
  transport?: Transport<T>;
470
455
  idempotencyKey?: string;
471
456
  retentionSeconds?: number;
472
- callback?: Record<string, CallbackConfig> | CallbackConfig;
473
457
  });
474
458
 
475
459
  // Examples:
@@ -477,16 +461,11 @@ await send("notifications", { userId: "123", message: "Welcome!" });
477
461
 
478
462
  await send("images", imageBuffer, {
479
463
  transport: new BufferTransport(),
480
- callback: { url: "https://example.com/process-image" }
481
464
  });
482
465
 
483
466
  await send("events", eventData, {
484
467
  idempotencyKey: "unique-key-123",
485
468
  retentionSeconds: 3600,
486
- callback: {
487
- analytics: { url: "https://analytics.example.com/webhook" },
488
- notifications: { url: "https://notifications.example.com/webhook", delay: 30 }
489
- }
490
469
  });
491
470
  ```
492
471
 
@@ -547,44 +526,33 @@ interface Transport<T = unknown> {
547
526
  }
548
527
  ```
549
528
 
550
- ### Callback Utilities
529
+ ### Callback Handler
551
530
 
552
531
  ```typescript
553
- // Parse queue callback request headers
554
- function parseCallbackRequest(request: Request): CallbackMessageOptions;
532
+ // Create a callback handler for Next.js route handlers
533
+ function handleCallback(
534
+ handlers: CallbackHandlers,
535
+ ): (request: Request) => Promise<Response>;
555
536
 
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
537
+ // Configuration object with handlers for different topics and consumer groups
566
538
  type CallbackHandlers = {
567
- [topicName: string]:
568
- | MessageHandler // Single handler (uses 'default' consumer group)
569
- | { [consumerGroup: string]: MessageHandler }; // Multiple consumer group handlers
539
+ [topicName: string]: { [consumerGroup: string]: MessageHandler };
570
540
  };
571
541
 
572
542
  // Example usage:
573
543
  export const POST = handleCallback({
574
- // Topic handler (uses 'default' consumer group)
575
- "new-users": (message, metadata) => {
576
- console.log(`New user event:`, message, metadata);
544
+ "user-events": {
545
+ welcome: (message, metadata) => {
546
+ console.log(`New user event:`, message, metadata);
547
+ },
577
548
  },
578
549
 
579
- // Consumer group specific handlers
550
+ // Multiple consumer groups per topic
580
551
  "image-processing": {
581
- "compress": (message, metadata) => console.log("Compressing image", message),
582
- "resize": (message, metadata) => console.log("Resizing image", message),
583
- }
552
+ compress: (message, metadata) => console.log("Compressing image", message),
553
+ resize: (message, metadata) => console.log("Resizing image", message),
554
+ },
584
555
  });
585
-
586
- // Error thrown for invalid callback requests
587
- class InvalidCallbackError extends Error;
588
556
  ```
589
557
 
590
558
  ## Examples
@@ -649,8 +617,6 @@ try {
649
617
  } catch (error) {
650
618
  if (error.message.includes("not found or not available")) {
651
619
  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
620
  } else {
655
621
  console.error("Error processing message:", error);
656
622
  }
@@ -674,7 +640,7 @@ try {
674
640
  if (error instanceof QueueEmptyError) {
675
641
  console.log("No messages available");
676
642
  } else if (error instanceof MessageLockedError) {
677
- console.log("Next message is locked (FIFO queue)");
643
+ console.log("Next message is locked");
678
644
  if (error.retryAfter) {
679
645
  console.log(`Retry after ${error.retryAfter} seconds`);
680
646
  }
@@ -763,117 +729,6 @@ try {
763
729
  }
764
730
  ```
765
731
 
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
732
  ## Error Handling
878
733
 
879
734
  The queue client provides specific error types for different failure scenarios:
@@ -890,7 +745,7 @@ The queue client provides specific error types for different failure scenarios:
890
745
  status)
891
746
 
892
747
  - Contains optional `retryAfter` property with seconds to wait before retry
893
- - For `consume()` without options: the next message in FIFO sequence is locked
748
+ - For `consume()` without options: the next message is locked
894
749
  - For `consume()` with messageId: the requested message is locked
895
750
 
896
751
  - **`MessageNotFoundError`**: Message doesn't exist (404 status)
@@ -898,24 +753,11 @@ The queue client provides specific error types for different failure scenarios:
898
753
  - **`MessageNotAvailableError`**: Message exists but isn't available for
899
754
  processing (409 status)
900
755
 
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
756
  - **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
915
757
 
916
758
  - **`BadRequestError`**: Invalid request parameters (400 status)
917
759
 
918
- - Invalid queue names, FIFO limit violations, missing required parameters
760
+ - Invalid queue names, missing required parameters
919
761
 
920
762
  - **`UnauthorizedError`**: Authentication failure (401 status)
921
763
 
@@ -933,8 +775,7 @@ The queue client provides specific error types for different failure scenarios:
933
775
  ```typescript
934
776
  import {
935
777
  BadRequestError,
936
- FailedDependencyError,
937
- FifoOrderingViolationError,
778
+
938
779
  ForbiddenError,
939
780
  InternalServerError,
940
781
  MessageLockedError,
@@ -951,7 +792,7 @@ try {
951
792
  if (error instanceof QueueEmptyError) {
952
793
  console.log("Queue is empty, retry later");
953
794
  } else if (error instanceof MessageLockedError) {
954
- console.log("Next message in FIFO queue is locked");
795
+ console.log("Next message is locked");
955
796
  if (error.retryAfter) {
956
797
  console.log(`Retry after ${error.retryAfter} seconds`);
957
798
  }
@@ -968,10 +809,7 @@ try {
968
809
  console.log(`Retry after ${error.retryAfter} seconds`);
969
810
  setTimeout(() => retry(), error.retryAfter * 1000);
970
811
  }
971
- } else if (error instanceof FailedDependencyError) {
972
- // FIFO ordering violation for receive by ID
973
- console.log(`Must process ${error.nextMessageId} first`);
974
- }
812
+
975
813
  }
976
814
 
977
815
  // Handle authentication and authorization errors