@vercel/queue 0.0.0-alpha.1
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 +670 -0
- package/dist/index.d.mts +725 -0
- package/dist/index.d.ts +725 -0
- package/dist/index.js +1326 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1278 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
# VQS - Vercel Queue Service Client
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
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
|
|
10
|
+
- **Pub/Sub Pattern**: Topic-based messaging with consumer groups
|
|
11
|
+
- **Type Safety**: Full TypeScript support with generic types
|
|
12
|
+
- **Automatic Retries**: Built-in visibility timeout management
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @vercel/queue
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { VQSClient, createTopic, JsonTransport } from "@vercel/queue";
|
|
24
|
+
|
|
25
|
+
const client = new VQSClient({
|
|
26
|
+
token: "your-vercel-oidc-token",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Create a topic with JSON serialization (default)
|
|
30
|
+
const topic = createTopic<{ message: string; timestamp: number }>(
|
|
31
|
+
client,
|
|
32
|
+
"my-topic",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Publish a message
|
|
36
|
+
await topic.publish({
|
|
37
|
+
message: "Hello, World!",
|
|
38
|
+
timestamp: Date.now(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Create a consumer group
|
|
42
|
+
const consumer = topic.consumerGroup("my-processors");
|
|
43
|
+
|
|
44
|
+
// Process messages continuously with cancellation support
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
|
|
47
|
+
// Start processing (blocks until aborted or error)
|
|
48
|
+
try {
|
|
49
|
+
await consumer.subscribe(controller.signal, async (message) => {
|
|
50
|
+
console.log("Received:", message.payload.message);
|
|
51
|
+
console.log("Timestamp:", new Date(message.payload.timestamp));
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Processing stopped due to error:", error);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Stop processing from elsewhere in your code
|
|
58
|
+
// controller.abort();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Serialization (Transport) System
|
|
62
|
+
|
|
63
|
+
The VQS 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.
|
|
64
|
+
|
|
65
|
+
### Built-in Transports
|
|
66
|
+
|
|
67
|
+
#### JsonTransport (Default)
|
|
68
|
+
|
|
69
|
+
Buffers data for JSON parsing - suitable for structured data that fits in memory.
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { JsonTransport, createTopic } from "@vercel/queue";
|
|
73
|
+
|
|
74
|
+
const topic = createTopic<{ data: any }>(
|
|
75
|
+
client,
|
|
76
|
+
"json-topic",
|
|
77
|
+
new JsonTransport(),
|
|
78
|
+
);
|
|
79
|
+
// or simply (JsonTransport is the default):
|
|
80
|
+
const topic = createTopic<{ data: any }>(client, "json-topic");
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### BufferTransport
|
|
84
|
+
|
|
85
|
+
Buffers the entire payload into memory as a Buffer - suitable for binary data that fits in memory.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { BufferTransport, createTopic } from "@vercel/queue";
|
|
89
|
+
|
|
90
|
+
const topic = createTopic<Buffer>(
|
|
91
|
+
client,
|
|
92
|
+
"binary-topic",
|
|
93
|
+
new BufferTransport(),
|
|
94
|
+
);
|
|
95
|
+
const binaryData = Buffer.from("Binary data", "utf8");
|
|
96
|
+
await topic.publish(binaryData);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### StreamTransport
|
|
100
|
+
|
|
101
|
+
**True streaming support** - passes ReadableStream directly without buffering. Ideal for large files and memory-efficient processing.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { StreamTransport, createTopic } from "@vercel/queue";
|
|
105
|
+
|
|
106
|
+
const topic = createTopic<ReadableStream<Uint8Array>>(
|
|
107
|
+
client,
|
|
108
|
+
"streaming-topic",
|
|
109
|
+
new StreamTransport(),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Send large file as stream without loading into memory
|
|
113
|
+
const fileStream = new ReadableStream<Uint8Array>({
|
|
114
|
+
start(controller) {
|
|
115
|
+
// Read file in chunks
|
|
116
|
+
for (const chunk of readFileInChunks("large-file.bin")) {
|
|
117
|
+
controller.enqueue(chunk);
|
|
118
|
+
}
|
|
119
|
+
controller.close();
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await topic.publish(fileStream);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Custom Transport
|
|
127
|
+
|
|
128
|
+
You can create your own serialization format by implementing the `Transport` interface:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Transport } from "@vercel/queue";
|
|
132
|
+
|
|
133
|
+
interface Transport<T = unknown> {
|
|
134
|
+
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
135
|
+
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
136
|
+
contentType: string;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Choosing the Right Transport
|
|
141
|
+
|
|
142
|
+
| Use Case | Recommended Transport | Memory Usage | Performance |
|
|
143
|
+
| ---------------------- | --------------------- | ------------ | ----------- |
|
|
144
|
+
| Small JSON objects | `JsonTransport` | Low | High |
|
|
145
|
+
| Binary files < 100MB | `BufferTransport` | Medium | High |
|
|
146
|
+
| Large files > 100MB | `StreamTransport` | Very Low | Medium |
|
|
147
|
+
| Real-time data streams | `StreamTransport` | Very Low | High |
|
|
148
|
+
| Custom protocols | Custom implementation | Varies | Varies |
|
|
149
|
+
|
|
150
|
+
## Complete Example: Video Processing Pipeline
|
|
151
|
+
|
|
152
|
+
Here's a comprehensive example showing a video processing pipeline that processes videos with FFmpeg and stores the results in Vercel Blob:
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { VQSClient, createTopic, StreamTransport } from "@vercel/queue";
|
|
156
|
+
import { spawn } from "child_process";
|
|
157
|
+
import ffmpeg from "ffmpeg-static";
|
|
158
|
+
import { put } from "@vercel/blob";
|
|
159
|
+
|
|
160
|
+
const client = new VQSClient({
|
|
161
|
+
token: process.env.VQS_TOKEN!,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Input topic with unoptimized videos
|
|
165
|
+
const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
166
|
+
client,
|
|
167
|
+
"unoptimized-videos",
|
|
168
|
+
new StreamTransport(),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Output topic for optimized videos
|
|
172
|
+
const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
173
|
+
client,
|
|
174
|
+
"optimized-videos",
|
|
175
|
+
new StreamTransport(),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Step 1: Process videos with FFmpeg
|
|
179
|
+
const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
|
|
180
|
+
const processingController = new AbortController();
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await videoProcessor.subscribe(
|
|
184
|
+
processingController.signal,
|
|
185
|
+
async (message) => {
|
|
186
|
+
const inputVideoStream = message.payload;
|
|
187
|
+
console.log("Processing video...");
|
|
188
|
+
|
|
189
|
+
if (!ffmpeg) {
|
|
190
|
+
throw new Error("FFmpeg not available");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create optimized video stream using FFmpeg
|
|
194
|
+
const optimizedStream = new ReadableStream<Uint8Array>({
|
|
195
|
+
start(controller) {
|
|
196
|
+
const ffmpegProcess = spawn(
|
|
197
|
+
ffmpeg,
|
|
198
|
+
[
|
|
199
|
+
"-i",
|
|
200
|
+
"pipe:0", // Input from stdin
|
|
201
|
+
"-c:v",
|
|
202
|
+
"libvpx-vp9", // Video codec
|
|
203
|
+
"-c:a",
|
|
204
|
+
"libopus", // Audio codec
|
|
205
|
+
"-crf",
|
|
206
|
+
"23", // Quality
|
|
207
|
+
"-f",
|
|
208
|
+
"webm", // Output format
|
|
209
|
+
"pipe:1", // Output to stdout
|
|
210
|
+
],
|
|
211
|
+
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Pipe input stream to FFmpeg
|
|
215
|
+
const reader = inputVideoStream.getReader();
|
|
216
|
+
const pipeInput = async () => {
|
|
217
|
+
while (true) {
|
|
218
|
+
const { done, value } = await reader.read();
|
|
219
|
+
if (done) {
|
|
220
|
+
ffmpegProcess.stdin?.end();
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
ffmpegProcess.stdin?.write(value);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
pipeInput();
|
|
227
|
+
|
|
228
|
+
// Stream FFmpeg output
|
|
229
|
+
ffmpegProcess.stdout?.on("data", (chunk) => {
|
|
230
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
ffmpegProcess.on("close", (code) => {
|
|
234
|
+
if (code === 0) {
|
|
235
|
+
controller.close();
|
|
236
|
+
} else {
|
|
237
|
+
controller.error(new Error(`FFmpeg failed with code ${code}`));
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Publish optimized video to next topic
|
|
244
|
+
await optimizedVideosTopic.publish(optimizedStream);
|
|
245
|
+
console.log("Video optimized and published");
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error("Video processing error:", error);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Step 2: Store optimized videos in Vercel Blob
|
|
253
|
+
const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
|
|
254
|
+
const uploadController = new AbortController();
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
await blobUploader.subscribe(uploadController.signal, async (message) => {
|
|
258
|
+
const optimizedVideo = message.payload;
|
|
259
|
+
|
|
260
|
+
// Upload to Vercel Blob storage
|
|
261
|
+
const filename = `optimized-${Date.now()}.webm`;
|
|
262
|
+
const blob = await put(filename, optimizedVideo, {
|
|
263
|
+
access: "public",
|
|
264
|
+
contentType: "video/webm",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.error("Blob upload error:", error);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Graceful shutdown
|
|
274
|
+
process.on("SIGINT", () => {
|
|
275
|
+
processingController.abort();
|
|
276
|
+
uploadController.abort();
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## API Reference
|
|
281
|
+
|
|
282
|
+
### VQSClient
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const client = new VQSClient({
|
|
286
|
+
token: string;
|
|
287
|
+
baseUrl?: string; // defaults to 'https://@vercel/queue.vercel.sh'
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Topic
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const topic = createTopic<T>(client, topicName, transport?);
|
|
295
|
+
|
|
296
|
+
// Publish a message (uses topic's transport)
|
|
297
|
+
await topic.publish(payload, options?);
|
|
298
|
+
|
|
299
|
+
// Create a consumer group (can override transport)
|
|
300
|
+
const consumer = topic.consumerGroup<U>(groupName, options?);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### ConsumerGroup
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// Start continuous processing (blocks until signal is aborted or error occurs)
|
|
307
|
+
await consumer.subscribe(signal, handler, options?);
|
|
308
|
+
|
|
309
|
+
// Process a specific message by ID
|
|
310
|
+
await consumer.receiveMessage(messageId, handler);
|
|
311
|
+
|
|
312
|
+
// Process the next available message
|
|
313
|
+
await consumer.receiveNextMessage(handler);
|
|
314
|
+
|
|
315
|
+
// Handle a specific message by ID without payload
|
|
316
|
+
await consumer.handleMessage(messageId, handler);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Message Handler
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// Handler function signature
|
|
323
|
+
type MessageHandler<T> = (
|
|
324
|
+
message: Message<T>,
|
|
325
|
+
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
326
|
+
|
|
327
|
+
// Handler result types
|
|
328
|
+
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
329
|
+
|
|
330
|
+
interface MessageTimeoutResult {
|
|
331
|
+
timeoutSeconds: number; // seconds before message becomes available again
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Transport Interface
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
interface Transport<T = unknown> {
|
|
339
|
+
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
340
|
+
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
341
|
+
contentType: string;
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Callback Utilities
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Parse VQS callback request headers
|
|
349
|
+
function parseCallbackRequest(request: Request): CallbackMessageOptions;
|
|
350
|
+
|
|
351
|
+
// Callback options type
|
|
352
|
+
interface CallbackMessageOptions {
|
|
353
|
+
queueName: string;
|
|
354
|
+
consumerGroup: string;
|
|
355
|
+
messageId: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Error thrown for invalid callback requests
|
|
359
|
+
class InvalidCallbackError extends Error;
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Examples
|
|
363
|
+
|
|
364
|
+
### Basic JSON Processing
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
interface UserEvent {
|
|
368
|
+
userId: string;
|
|
369
|
+
action: string;
|
|
370
|
+
timestamp: number;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const userTopic = createTopic<UserEvent>(client, "user-events");
|
|
374
|
+
|
|
375
|
+
await userTopic.publish({
|
|
376
|
+
userId: "123",
|
|
377
|
+
action: "login",
|
|
378
|
+
timestamp: Date.now(),
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
const consumer = userTopic.consumerGroup("processors");
|
|
382
|
+
const controller = new AbortController();
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await consumer.subscribe(controller.signal, async (message) => {
|
|
386
|
+
console.log(
|
|
387
|
+
`User ${message.payload.userId} performed ${message.payload.action}`,
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error("Processing error:", error);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Stop processing when needed
|
|
395
|
+
// controller.abort();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Processing Specific Messages by ID
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const userTopic = createTopic<{ userId: string; action: string }>(
|
|
402
|
+
client,
|
|
403
|
+
"user-events",
|
|
404
|
+
);
|
|
405
|
+
const consumer = userTopic.consumerGroup("processors");
|
|
406
|
+
|
|
407
|
+
// Process a specific message if you know its ID
|
|
408
|
+
const messageId = "01234567-89ab-cdef-0123-456789abcdef";
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
await consumer.receiveMessage(messageId, async (message) => {
|
|
412
|
+
console.log(`Processing specific message: ${message.messageId}`);
|
|
413
|
+
console.log(
|
|
414
|
+
`User ${message.payload.userId} performed ${message.payload.action}`,
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
console.log("Message processed successfully");
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error.message.includes("not found or not available")) {
|
|
420
|
+
console.log("Message was already processed or does not exist");
|
|
421
|
+
} else if (error.message.includes("FIFO ordering violation")) {
|
|
422
|
+
console.log("FIFO queue requires processing messages in order");
|
|
423
|
+
} else {
|
|
424
|
+
console.error("Error processing message:", error);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Processing Next Available Message
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
433
|
+
client,
|
|
434
|
+
"work-queue",
|
|
435
|
+
);
|
|
436
|
+
const worker = workTopic.consumerGroup("workers");
|
|
437
|
+
|
|
438
|
+
// Process the next available message (one-shot processing)
|
|
439
|
+
try {
|
|
440
|
+
await worker.receiveNextMessage(async (message) => {
|
|
441
|
+
console.log(`Processing task: ${message.payload.taskType}`);
|
|
442
|
+
await processTask(message.payload.taskType, message.payload.data);
|
|
443
|
+
});
|
|
444
|
+
console.log("Message processed successfully");
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (error instanceof QueueEmptyError) {
|
|
447
|
+
console.log("No messages available");
|
|
448
|
+
} else if (error instanceof MessageLockedError) {
|
|
449
|
+
console.log("Next message is locked (FIFO queue)");
|
|
450
|
+
if (error.retryAfter) {
|
|
451
|
+
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
console.error("Error processing message:", error);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// You can also use it with timeout results
|
|
459
|
+
await worker.receiveNextMessage(async (message) => {
|
|
460
|
+
if (!canProcessTaskType(message.payload.taskType)) {
|
|
461
|
+
// Return timeout to retry later
|
|
462
|
+
return { timeoutSeconds: 60 };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
await processTask(message.payload.taskType, message.payload.data);
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Timing Out Messages
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
473
|
+
client,
|
|
474
|
+
"work-queue",
|
|
475
|
+
);
|
|
476
|
+
const worker = workTopic.consumerGroup("workers");
|
|
477
|
+
const controller = new AbortController();
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
await worker.subscribe(controller.signal, async (message) => {
|
|
481
|
+
const { taskType, data } = message.payload;
|
|
482
|
+
|
|
483
|
+
// Check if we can process this task type right now
|
|
484
|
+
if (taskType === "heavy-computation" && isSystemOverloaded()) {
|
|
485
|
+
// Return timeout to retry later (5 minutes)
|
|
486
|
+
return { timeoutSeconds: 300 };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Check if we have required resources
|
|
490
|
+
if (taskType === "external-api" && !isExternalServiceAvailable()) {
|
|
491
|
+
// Return timeout to retry in 1 minute
|
|
492
|
+
return { timeoutSeconds: 60 };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Process the message normally
|
|
496
|
+
console.log(`Processing ${taskType} task`);
|
|
497
|
+
await processTask(taskType, data);
|
|
498
|
+
// Message will be automatically deleted on successful completion
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error("Worker processing error:", error);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Example with exponential backoff
|
|
505
|
+
const backoffController = new AbortController();
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
await worker.subscribe(backoffController.signal, async (message) => {
|
|
509
|
+
const maxRetries = 3;
|
|
510
|
+
const deliveryCount = message.deliveryCount;
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
await processMessage(message.payload);
|
|
514
|
+
// Successful processing - message will be deleted
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (deliveryCount < maxRetries) {
|
|
517
|
+
// Exponential backoff: 2^deliveryCount minutes
|
|
518
|
+
const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
|
|
519
|
+
console.log(
|
|
520
|
+
`Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
|
|
521
|
+
);
|
|
522
|
+
return { timeoutSeconds: timeoutSeconds };
|
|
523
|
+
} else {
|
|
524
|
+
// Max retries reached, let the message fail and be deleted
|
|
525
|
+
console.error("Max retries reached, message will be discarded:", error);
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error("Backoff processing error:", error);
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
MIT
|
|
538
|
+
|
|
539
|
+
## Error Handling
|
|
540
|
+
|
|
541
|
+
The VQS client provides specific error types for different failure scenarios:
|
|
542
|
+
|
|
543
|
+
### Error Types
|
|
544
|
+
|
|
545
|
+
- **`QueueEmptyError`**: Thrown when attempting to receive messages from an empty queue (204 status)
|
|
546
|
+
|
|
547
|
+
- Only thrown when directly using `client.receiveMessages()`
|
|
548
|
+
- `ConsumerGroup.subscribe()` handles this error internally and continues polling
|
|
549
|
+
|
|
550
|
+
- **`MessageLockedError`**: Thrown when a message is temporarily locked (423 status)
|
|
551
|
+
|
|
552
|
+
- Contains optional `retryAfter` property with seconds to wait before retry
|
|
553
|
+
- For `receiveMessages()` on FIFO queues: the next message in sequence is locked
|
|
554
|
+
- For `receiveMessageById()`: the requested message is locked
|
|
555
|
+
- `ConsumerGroup.subscribe()` handles this error internally when polling
|
|
556
|
+
|
|
557
|
+
- **`MessageNotFoundError`**: Message doesn't exist (404 status)
|
|
558
|
+
|
|
559
|
+
- **`MessageNotAvailableError`**: Message exists but isn't available for processing (409 status)
|
|
560
|
+
|
|
561
|
+
- **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status with nextMessageId)
|
|
562
|
+
|
|
563
|
+
- Contains `nextMessageId` property indicating which message to process first
|
|
564
|
+
|
|
565
|
+
- **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424 status)
|
|
566
|
+
|
|
567
|
+
- Contains `nextMessageId` property indicating which message must be processed first
|
|
568
|
+
- Similar to `FifoOrderingViolationError` but specifically for receive-by-ID operations
|
|
569
|
+
|
|
570
|
+
- **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
|
|
571
|
+
|
|
572
|
+
- **`BadRequestError`**: Invalid request parameters (400 status)
|
|
573
|
+
|
|
574
|
+
- Invalid queue names, FIFO limit violations, missing required parameters
|
|
575
|
+
|
|
576
|
+
- **`UnauthorizedError`**: Authentication failure (401 status)
|
|
577
|
+
|
|
578
|
+
- Missing or invalid authentication token
|
|
579
|
+
|
|
580
|
+
- **`ForbiddenError`**: Access denied (403 status)
|
|
581
|
+
|
|
582
|
+
- Queue environment doesn't match token environment
|
|
583
|
+
|
|
584
|
+
- **`InternalServerError`**: Server-side errors (500+ status codes)
|
|
585
|
+
- Unexpected server errors, service unavailable, etc.
|
|
586
|
+
|
|
587
|
+
### Error Handling Examples
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
import {
|
|
591
|
+
QueueEmptyError,
|
|
592
|
+
MessageLockedError,
|
|
593
|
+
FifoOrderingViolationError,
|
|
594
|
+
FailedDependencyError,
|
|
595
|
+
BadRequestError,
|
|
596
|
+
UnauthorizedError,
|
|
597
|
+
ForbiddenError,
|
|
598
|
+
InternalServerError,
|
|
599
|
+
} from "@vercel/queue";
|
|
600
|
+
|
|
601
|
+
// Handle empty queue or locked messages
|
|
602
|
+
try {
|
|
603
|
+
for await (const message of client.receiveMessages(options, transport)) {
|
|
604
|
+
// Process messages
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error instanceof QueueEmptyError) {
|
|
608
|
+
console.log("Queue is empty, retry later");
|
|
609
|
+
} else if (error instanceof MessageLockedError) {
|
|
610
|
+
console.log("Next message in FIFO queue is locked");
|
|
611
|
+
if (error.retryAfter) {
|
|
612
|
+
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Handle locked message with retry
|
|
618
|
+
try {
|
|
619
|
+
await consumer.receiveMessage(messageId, handler);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error instanceof MessageLockedError) {
|
|
622
|
+
console.log("Message is locked by another consumer");
|
|
623
|
+
if (error.retryAfter) {
|
|
624
|
+
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
625
|
+
setTimeout(() => retry(), error.retryAfter * 1000);
|
|
626
|
+
}
|
|
627
|
+
} else if (error instanceof FailedDependencyError) {
|
|
628
|
+
// FIFO ordering violation for receive by ID
|
|
629
|
+
console.log(`Must process ${error.nextMessageId} first`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Handle authentication and authorization errors
|
|
634
|
+
try {
|
|
635
|
+
await topic.publish(payload);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
if (error instanceof UnauthorizedError) {
|
|
638
|
+
console.log("Invalid token - refresh authentication");
|
|
639
|
+
} else if (error instanceof ForbiddenError) {
|
|
640
|
+
console.log("Environment mismatch - check token/queue configuration");
|
|
641
|
+
} else if (error instanceof BadRequestError) {
|
|
642
|
+
console.log("Invalid parameters:", error.message);
|
|
643
|
+
} else if (error instanceof InternalServerError) {
|
|
644
|
+
console.log("Server error - retry with backoff");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Complete error handling pattern
|
|
649
|
+
function handleVQSError(error: unknown): void {
|
|
650
|
+
if (error instanceof QueueEmptyError || error instanceof MessageLockedError) {
|
|
651
|
+
// Transient errors - safe to retry
|
|
652
|
+
console.log("Temporary condition, will retry");
|
|
653
|
+
} else if (
|
|
654
|
+
error instanceof UnauthorizedError ||
|
|
655
|
+
error instanceof ForbiddenError
|
|
656
|
+
) {
|
|
657
|
+
// Authentication/authorization errors - need to fix configuration
|
|
658
|
+
console.log("Auth error - check credentials");
|
|
659
|
+
} else if (error instanceof BadRequestError) {
|
|
660
|
+
// Client error - fix the request
|
|
661
|
+
console.log("Invalid request:", error.message);
|
|
662
|
+
} else if (error instanceof InternalServerError) {
|
|
663
|
+
// Server error - implement exponential backoff
|
|
664
|
+
console.log("Server error - retry with backoff");
|
|
665
|
+
} else {
|
|
666
|
+
// Unknown error
|
|
667
|
+
console.error("Unexpected error:", error);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|