@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 +151 -313
- package/dist/index.d.mts +52 -104
- package/dist/index.d.ts +52 -104
- package/dist/index.js +47 -224
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +47 -221
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
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:
|
|
187
|
+
// Option 1: Single topic with multiple consumer groups
|
|
232
188
|
export const POST = handleCallback({
|
|
233
|
-
"my-topic":
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
242
|
-
//
|
|
203
|
+
async function processGroup1(message: any) {
|
|
204
|
+
// Consumer group 1 specific logic
|
|
205
|
+
}
|
|
243
206
|
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
234
|
+
### 2. Configure vercel.json
|
|
261
235
|
|
|
262
|
-
|
|
236
|
+
Create a `vercel.json` file in your project root to declare which topics and consumer groups each API route handles:
|
|
263
237
|
|
|
264
|
-
|
|
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
|
-
|
|
264
|
+
// app/api/queue/users/route.ts - Handle user events
|
|
265
|
+
import { handleCallback } from "@vercel/queue";
|
|
268
266
|
|
|
269
|
-
const
|
|
270
|
-
"
|
|
271
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
529
|
+
### Callback Handler
|
|
551
530
|
|
|
552
531
|
```typescript
|
|
553
|
-
//
|
|
554
|
-
function
|
|
532
|
+
// Create a callback handler for Next.js route handlers
|
|
533
|
+
function handleCallback(
|
|
534
|
+
handlers: CallbackHandlers,
|
|
535
|
+
): (request: Request) => Promise<Response>;
|
|
555
536
|
|
|
556
|
-
//
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
544
|
+
"user-events": {
|
|
545
|
+
welcome: (message, metadata) => {
|
|
546
|
+
console.log(`New user event:`, message, metadata);
|
|
547
|
+
},
|
|
577
548
|
},
|
|
578
549
|
|
|
579
|
-
//
|
|
550
|
+
// Multiple consumer groups per topic
|
|
580
551
|
"image-processing": {
|
|
581
|
-
|
|
582
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|