@vercel/queue 0.0.0-alpha.11 → 0.0.0-alpha.2
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 +767 -208
- package/dist/index.d.mts +477 -114
- package/dist/index.d.ts +477 -114
- package/dist/index.js +545 -267
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +536 -264
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# Vercel Queues
|
|
2
2
|
|
|
3
|
-
A TypeScript client library for interacting with the Vercel Queue Service API,
|
|
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
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Automatic Queue Triggering**: Vercel automatically triggers your API routes when messages are ready
|
|
8
|
-
- **Next.js Integration**: Built-in support for Next.js API routes and Server Actions
|
|
9
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
10
|
- **Pub/Sub Pattern**: Topic-based messaging with consumer groups
|
|
11
11
|
- **Type Safety**: Full TypeScript support with generic types
|
|
12
|
-
- **
|
|
13
|
-
- **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
|
|
12
|
+
- **Automatic Retries**: Built-in visibility timeout management
|
|
14
13
|
|
|
15
14
|
## Installation
|
|
16
15
|
|
|
@@ -20,7 +19,7 @@ npm install @vercel/queue
|
|
|
20
19
|
|
|
21
20
|
## Quick Start
|
|
22
21
|
|
|
23
|
-
For local development, you'll need to pull your Vercel environment variables:
|
|
22
|
+
For local development, you'll need to pull your Vercel environment variables (including the OIDC token):
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
25
|
# Install Vercel CLI if you haven't already
|
|
@@ -30,6 +29,60 @@ npm i -g vercel
|
|
|
30
29
|
vc env pull
|
|
31
30
|
```
|
|
32
31
|
|
|
32
|
+
Publishing and consuming messages on a queue
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// index.ts
|
|
36
|
+
import { QueueClient, createTopic, JsonTransport } from "@vercel/queue";
|
|
37
|
+
|
|
38
|
+
// Create a client - automatically authenticated using the OIDC token
|
|
39
|
+
const client = QueueClient.fromVercelFunction();
|
|
40
|
+
|
|
41
|
+
// Create a topic with JSON serialization (default)
|
|
42
|
+
const topic = createTopic<{ message: string; timestamp: number }>(
|
|
43
|
+
client,
|
|
44
|
+
"my-topic",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Publish a message
|
|
48
|
+
await topic.publish({
|
|
49
|
+
message: "Hello, World!",
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create a consumer group
|
|
54
|
+
const consumer = topic.consumerGroup("my-processors");
|
|
55
|
+
|
|
56
|
+
// Process messages continuously with cancellation support
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
|
|
59
|
+
// Start processing (blocks until aborted or error)
|
|
60
|
+
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));
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error("Processing stopped due to error:", error);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Stop processing from elsewhere in your code
|
|
70
|
+
// controller.abort();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Run the script
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Using dotenv to load the OIDC token
|
|
77
|
+
dotenv -e .env.local node index.ts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage with Vercel
|
|
81
|
+
|
|
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.
|
|
83
|
+
|
|
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).
|
|
85
|
+
|
|
33
86
|
### TypeScript Configuration
|
|
34
87
|
|
|
35
88
|
Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
|
|
@@ -43,289 +96,795 @@ Update your `tsconfig.json` to use `"bundler"` module resolution for proper pack
|
|
|
43
96
|
}
|
|
44
97
|
```
|
|
45
98
|
|
|
46
|
-
### Publishing
|
|
99
|
+
### Publishing messages to a queue
|
|
47
100
|
|
|
48
|
-
|
|
101
|
+
Create a new server function to publish messages
|
|
49
102
|
|
|
50
103
|
```typescript
|
|
51
|
-
|
|
104
|
+
// app/action.ts
|
|
105
|
+
"use server";
|
|
106
|
+
|
|
107
|
+
import { QueueClient, createTopic } from "@vercel/queue";
|
|
108
|
+
|
|
109
|
+
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,
|
|
116
|
+
"my-topic",
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Publish the message
|
|
120
|
+
const { messageId } = await topic.publish(
|
|
121
|
+
{ message, timestamp: Date.now() },
|
|
122
|
+
{
|
|
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
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
);
|
|
52
133
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
134
|
+
console.log(`Published message ${messageId}`);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
57
137
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
138
|
+
Now wire up the server function to your app
|
|
139
|
+
|
|
140
|
+
```jsx
|
|
141
|
+
// app/some/page.tsx
|
|
142
|
+
"use client";
|
|
143
|
+
import { publishTestMessage } from "./actions";
|
|
144
|
+
|
|
145
|
+
export default function Button() {
|
|
146
|
+
return (
|
|
147
|
+
// ...
|
|
148
|
+
<Button onClick={() => publishTestMessage("Hello world")} >
|
|
149
|
+
Publish Test Message
|
|
150
|
+
</a>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Consuming the queue
|
|
156
|
+
|
|
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.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// 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
|
+
});
|
|
185
|
+
|
|
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
|
+
```
|
|
196
|
+
|
|
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.
|
|
215
|
+
|
|
216
|
+
## Key Features
|
|
217
|
+
|
|
218
|
+
### Streaming Support
|
|
219
|
+
|
|
220
|
+
Handle large files and data streams without loading them into memory:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { StreamTransport } from "@vercel/queue";
|
|
224
|
+
|
|
225
|
+
const videoTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
226
|
+
client,
|
|
227
|
+
"video-processing",
|
|
228
|
+
new StreamTransport(),
|
|
68
229
|
);
|
|
230
|
+
|
|
231
|
+
// Process large video files efficiently
|
|
232
|
+
const processor = videoTopic.consumerGroup("processors");
|
|
233
|
+
await processor.subscribe(signal, async (message) => {
|
|
234
|
+
const videoStream = message.payload;
|
|
235
|
+
// Process stream chunk by chunk
|
|
236
|
+
const reader = videoStream.getReader();
|
|
237
|
+
while (true) {
|
|
238
|
+
const { done, value } = await reader.read();
|
|
239
|
+
if (done) break;
|
|
240
|
+
await processChunk(value);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
69
243
|
```
|
|
70
244
|
|
|
71
|
-
|
|
245
|
+
### Consumer Groups
|
|
246
|
+
|
|
247
|
+
Multiple consumers can process messages from the same topic in parallel:
|
|
72
248
|
|
|
73
249
|
```typescript
|
|
74
|
-
//
|
|
75
|
-
|
|
250
|
+
// Multiple workers in the same group - they share/split messages
|
|
251
|
+
const worker1 = topic.consumerGroup("workers");
|
|
252
|
+
const worker2 = topic.consumerGroup("workers"); // Same group name
|
|
253
|
+
// worker1 and worker2 will receive different messages (load balancing)
|
|
254
|
+
|
|
255
|
+
// Different consumer groups - each gets copies of ALL messages
|
|
256
|
+
const analytics = topic.consumerGroup("analytics");
|
|
257
|
+
const webhooks = topic.consumerGroup("webhooks");
|
|
258
|
+
// analytics and webhooks will both receive every message
|
|
259
|
+
```
|
|
76
260
|
|
|
77
|
-
|
|
78
|
-
const body = await request.json();
|
|
261
|
+
## Architecture
|
|
79
262
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
263
|
+
- **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
|
|
270
|
+
- **Streaming**: Memory-efficient processing of large payloads
|
|
271
|
+
- **Visibility Timeouts**: Automatic message lifecycle management
|
|
83
272
|
|
|
84
|
-
|
|
85
|
-
|
|
273
|
+
## Performance
|
|
274
|
+
|
|
275
|
+
The multipart parser is optimized for high-throughput scenarios:
|
|
276
|
+
|
|
277
|
+
- **Streaming**: Messages are yielded immediately as headers are parsed
|
|
278
|
+
- **Memory Efficient**: No buffering of complete payloads
|
|
279
|
+
- **Fast Parsing**: Native Buffer operations for ~50% performance improvement
|
|
280
|
+
- **Scalable**: Can handle arbitrarily large responses without memory constraints
|
|
281
|
+
|
|
282
|
+
## Serialization (Transport) System
|
|
283
|
+
|
|
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.
|
|
285
|
+
|
|
286
|
+
### Built-in Transports
|
|
287
|
+
|
|
288
|
+
#### JsonTransport (Default)
|
|
289
|
+
|
|
290
|
+
Buffers data for JSON parsing - suitable for structured data that fits in memory.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { JsonTransport, createTopic } from "@vercel/queue";
|
|
294
|
+
|
|
295
|
+
const topic = createTopic<{ data: any }>(
|
|
296
|
+
client,
|
|
297
|
+
"json-topic",
|
|
298
|
+
new JsonTransport(),
|
|
299
|
+
);
|
|
300
|
+
// or simply (JsonTransport is the default):
|
|
301
|
+
const topic = createTopic<{ data: any }>(client, "json-topic");
|
|
86
302
|
```
|
|
87
303
|
|
|
88
|
-
|
|
304
|
+
#### BufferTransport
|
|
305
|
+
|
|
306
|
+
Buffers the entire payload into memory as a Buffer - suitable for binary data that fits in memory.
|
|
89
307
|
|
|
90
|
-
|
|
308
|
+
```typescript
|
|
309
|
+
import { BufferTransport, createTopic } from "@vercel/queue";
|
|
91
310
|
|
|
92
|
-
|
|
311
|
+
const topic = createTopic<Buffer>(
|
|
312
|
+
client,
|
|
313
|
+
"binary-topic",
|
|
314
|
+
new BufferTransport(),
|
|
315
|
+
);
|
|
316
|
+
const binaryData = Buffer.from("Binary data", "utf8");
|
|
317
|
+
await topic.publish(binaryData);
|
|
318
|
+
```
|
|
93
319
|
|
|
94
|
-
|
|
320
|
+
#### StreamTransport
|
|
321
|
+
|
|
322
|
+
**True streaming support** - passes ReadableStream directly without buffering. Ideal for large files and memory-efficient processing.
|
|
95
323
|
|
|
96
324
|
```typescript
|
|
97
|
-
|
|
98
|
-
import { handleCallback } from "@vercel/queue";
|
|
99
|
-
|
|
100
|
-
export const POST = handleCallback({
|
|
101
|
-
// Single topic with one consumer
|
|
102
|
-
"my-topic": {
|
|
103
|
-
"my-consumer": async (message, metadata) => {
|
|
104
|
-
// metadata includes: { messageId, deliveryCount, createdAt }
|
|
105
|
-
console.log("Processing message:", message);
|
|
106
|
-
|
|
107
|
-
// If this throws an error, the message will be automatically retried
|
|
108
|
-
await processMessage(message);
|
|
109
|
-
},
|
|
110
|
-
},
|
|
325
|
+
import { StreamTransport, createTopic } from "@vercel/queue";
|
|
111
326
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (!isSystemReady()) {
|
|
118
|
-
// Override default retry with a 5 minute delay
|
|
119
|
-
return { timeoutSeconds: 300 };
|
|
120
|
-
}
|
|
327
|
+
const topic = createTopic<ReadableStream<Uint8Array>>(
|
|
328
|
+
client,
|
|
329
|
+
"streaming-topic",
|
|
330
|
+
new StreamTransport(),
|
|
331
|
+
);
|
|
121
332
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return { timeoutSeconds };
|
|
131
|
-
}
|
|
132
|
-
},
|
|
333
|
+
// Send large file as stream without loading into memory
|
|
334
|
+
const fileStream = new ReadableStream<Uint8Array>({
|
|
335
|
+
start(controller) {
|
|
336
|
+
// Read file in chunks
|
|
337
|
+
for (const chunk of readFileInChunks("large-file.bin")) {
|
|
338
|
+
controller.enqueue(chunk);
|
|
339
|
+
}
|
|
340
|
+
controller.close();
|
|
133
341
|
},
|
|
134
342
|
});
|
|
343
|
+
|
|
344
|
+
await topic.publish(fileStream);
|
|
135
345
|
```
|
|
136
346
|
|
|
137
|
-
|
|
347
|
+
### Custom Transport
|
|
138
348
|
|
|
139
|
-
|
|
349
|
+
You can create your own serialization format by implementing the `Transport` interface:
|
|
140
350
|
|
|
141
|
-
|
|
351
|
+
```typescript
|
|
352
|
+
import { Transport } from "@vercel/queue";
|
|
142
353
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"experimentalTriggers": [
|
|
148
|
-
{
|
|
149
|
-
"type": "queue/v1beta",
|
|
150
|
-
"topic": "my-topic",
|
|
151
|
-
"consumer": "my-consumer",
|
|
152
|
-
"maxAttempts": 3, // Optional: Maximum number of delivery attempts (default: 3)
|
|
153
|
-
"retryAfterSeconds": 60, // Optional: Delay between retries (default: 60)
|
|
154
|
-
"initialDelaySeconds": 0 // Optional: Initial delay before first delivery (default: 0)
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
"type": "queue/v1beta",
|
|
158
|
-
"topic": "order-events",
|
|
159
|
-
"consumer": "fulfillment"
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
"type": "queue/v1beta",
|
|
163
|
-
"topic": "order-events",
|
|
164
|
-
"consumer": "analytics",
|
|
165
|
-
"maxAttempts": 5, // Retry up to 5 times
|
|
166
|
-
"retryAfterSeconds": 300 // Wait 5 minutes between retries
|
|
167
|
-
}
|
|
168
|
-
]
|
|
169
|
-
}
|
|
170
|
-
}
|
|
354
|
+
interface Transport<T = unknown> {
|
|
355
|
+
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
356
|
+
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
357
|
+
contentType: string;
|
|
171
358
|
}
|
|
172
359
|
```
|
|
173
360
|
|
|
174
|
-
###
|
|
361
|
+
### Choosing the Right Transport
|
|
175
362
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
363
|
+
| Use Case | Recommended Transport | Memory Usage | Performance |
|
|
364
|
+
| ---------------------- | --------------------- | ------------ | ----------- |
|
|
365
|
+
| Small JSON objects | `JsonTransport` | Low | High |
|
|
366
|
+
| Binary files < 100MB | `BufferTransport` | Medium | High |
|
|
367
|
+
| Large files > 100MB | `StreamTransport` | Very Low | Medium |
|
|
368
|
+
| Real-time data streams | `StreamTransport` | Very Low | High |
|
|
369
|
+
| Custom protocols | Custom implementation | Varies | Varies |
|
|
183
370
|
|
|
184
|
-
##
|
|
371
|
+
## API Reference
|
|
185
372
|
|
|
186
|
-
###
|
|
373
|
+
### QueueClient
|
|
187
374
|
|
|
188
|
-
|
|
375
|
+
```typescript
|
|
376
|
+
const client = new QueueClient({
|
|
377
|
+
token: string;
|
|
378
|
+
baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
|
|
379
|
+
});
|
|
380
|
+
```
|
|
189
381
|
|
|
190
|
-
|
|
382
|
+
### Topic
|
|
191
383
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
384
|
+
```typescript
|
|
385
|
+
const topic = createTopic<T>(client, topicName, transport?);
|
|
386
|
+
|
|
387
|
+
// Publish a message (uses topic's transport)
|
|
388
|
+
await topic.publish(payload, options?);
|
|
389
|
+
|
|
390
|
+
// Create a consumer group (can override transport)
|
|
391
|
+
const consumer = topic.consumerGroup<U>(groupName, options?);
|
|
392
|
+
```
|
|
195
393
|
|
|
196
|
-
|
|
394
|
+
### ConsumerGroup
|
|
197
395
|
|
|
198
396
|
```typescript
|
|
199
|
-
|
|
397
|
+
// Start continuous processing (blocks until signal is aborted or error occurs)
|
|
398
|
+
await consumer.subscribe(signal, handler, options?);
|
|
200
399
|
|
|
201
|
-
//
|
|
202
|
-
await
|
|
400
|
+
// Process a specific message by ID
|
|
401
|
+
await consumer.receiveMessage(messageId, handler);
|
|
203
402
|
|
|
204
|
-
//
|
|
205
|
-
await
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
403
|
+
// Process the next available message
|
|
404
|
+
await consumer.receiveNextMessage(handler);
|
|
405
|
+
|
|
406
|
+
// Handle a specific message by ID without payload
|
|
407
|
+
await consumer.handleMessage(messageId, handler);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Message Handler
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// Handler function signature
|
|
414
|
+
type MessageHandler<T> = (
|
|
415
|
+
message: Message<T>,
|
|
416
|
+
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
417
|
+
|
|
418
|
+
// Handler result types
|
|
419
|
+
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
420
|
+
|
|
421
|
+
interface MessageTimeoutResult {
|
|
422
|
+
timeoutSeconds: number; // seconds before message becomes available again
|
|
423
|
+
}
|
|
210
424
|
```
|
|
211
425
|
|
|
212
|
-
### Transport
|
|
426
|
+
### Transport Interface
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
interface Transport<T = unknown> {
|
|
430
|
+
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
431
|
+
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
432
|
+
contentType: string;
|
|
433
|
+
}
|
|
434
|
+
```
|
|
213
435
|
|
|
214
|
-
|
|
215
|
-
| -------------------- | --------------------- | ------------ | ----------- |
|
|
216
|
-
| Small JSON objects | JsonTransport | Low | High |
|
|
217
|
-
| Binary files < 100MB | BufferTransport | Medium | High |
|
|
218
|
-
| Large files > 100MB | StreamTransport | Very Low | Medium |
|
|
219
|
-
| Real-time streams | StreamTransport | Very Low | High |
|
|
436
|
+
### Callback Utilities
|
|
220
437
|
|
|
221
|
-
|
|
438
|
+
```typescript
|
|
439
|
+
// Parse queue callback request headers
|
|
440
|
+
function parseCallbackRequest(request: Request): CallbackMessageOptions;
|
|
441
|
+
|
|
442
|
+
// Callback options type
|
|
443
|
+
interface CallbackMessageOptions {
|
|
444
|
+
queueName: string;
|
|
445
|
+
consumerGroup: string;
|
|
446
|
+
messageId: string;
|
|
447
|
+
}
|
|
222
448
|
|
|
223
|
-
|
|
449
|
+
// Error thrown for invalid callback requests
|
|
450
|
+
class InvalidCallbackError extends Error;
|
|
451
|
+
```
|
|
224
452
|
|
|
225
|
-
|
|
226
|
-
- **`MessageLockedError`**: Message temporarily locked (423)
|
|
227
|
-
- **`MessageNotFoundError`**: Message doesn't exist (404)
|
|
228
|
-
- **`MessageNotAvailableError`**: Message exists but unavailable (409)
|
|
229
|
-
- **`MessageCorruptedError`**: Message data corrupted
|
|
230
|
-
- **`BadRequestError`**: Invalid parameters (400)
|
|
231
|
-
- **`UnauthorizedError`**: Authentication failure (401)
|
|
232
|
-
- **`ForbiddenError`**: Access denied (403)
|
|
233
|
-
- **`InternalServerError`**: Server errors (500+)
|
|
453
|
+
## Examples
|
|
234
454
|
|
|
235
|
-
|
|
455
|
+
### Basic JSON Processing
|
|
236
456
|
|
|
237
457
|
```typescript
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
458
|
+
interface UserEvent {
|
|
459
|
+
userId: string;
|
|
460
|
+
action: string;
|
|
461
|
+
timestamp: number;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const userTopic = createTopic<UserEvent>(client, "user-events");
|
|
465
|
+
|
|
466
|
+
await userTopic.publish({
|
|
467
|
+
userId: "123",
|
|
468
|
+
action: "login",
|
|
469
|
+
timestamp: Date.now(),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const consumer = userTopic.consumerGroup("processors");
|
|
473
|
+
const controller = new AbortController();
|
|
244
474
|
|
|
245
475
|
try {
|
|
246
|
-
await
|
|
476
|
+
await consumer.subscribe(controller.signal, async (message) => {
|
|
477
|
+
console.log(
|
|
478
|
+
`User ${message.payload.userId} performed ${message.payload.action}`,
|
|
479
|
+
);
|
|
480
|
+
});
|
|
247
481
|
} catch (error) {
|
|
248
|
-
|
|
249
|
-
console.log("Invalid token - refresh authentication");
|
|
250
|
-
} else if (error instanceof ForbiddenError) {
|
|
251
|
-
console.log("Environment mismatch - check configuration");
|
|
252
|
-
} else if (error instanceof BadRequestError) {
|
|
253
|
-
console.log("Invalid parameters:", error.message);
|
|
254
|
-
} else if (error instanceof InternalServerError) {
|
|
255
|
-
console.log("Server error - retry with backoff");
|
|
256
|
-
}
|
|
482
|
+
console.error("Processing error:", error);
|
|
257
483
|
}
|
|
484
|
+
|
|
485
|
+
// Stop processing when needed
|
|
486
|
+
// controller.abort();
|
|
258
487
|
```
|
|
259
488
|
|
|
260
|
-
|
|
489
|
+
### Processing Specific Messages by ID
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const userTopic = createTopic<{ userId: string; action: string }>(
|
|
493
|
+
client,
|
|
494
|
+
"user-events",
|
|
495
|
+
);
|
|
496
|
+
const consumer = userTopic.consumerGroup("processors");
|
|
497
|
+
|
|
498
|
+
// Process a specific message if you know its ID
|
|
499
|
+
const messageId = "01234567-89ab-cdef-0123-456789abcdef";
|
|
261
500
|
|
|
262
|
-
|
|
501
|
+
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
|
+
});
|
|
508
|
+
console.log("Message processed successfully");
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (error.message.includes("not found or not available")) {
|
|
511
|
+
console.log("Message was already processed or does not exist");
|
|
512
|
+
} else if (error.message.includes("FIFO ordering violation")) {
|
|
513
|
+
console.log("FIFO queue requires processing messages in order");
|
|
514
|
+
} else {
|
|
515
|
+
console.error("Error processing message:", error);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
263
519
|
|
|
264
|
-
|
|
520
|
+
### Processing Next Available Message
|
|
265
521
|
|
|
266
522
|
```typescript
|
|
267
|
-
|
|
268
|
-
|
|
523
|
+
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
524
|
+
client,
|
|
525
|
+
"work-queue",
|
|
526
|
+
);
|
|
527
|
+
const worker = workTopic.consumerGroup("workers");
|
|
269
528
|
|
|
270
|
-
// Process
|
|
271
|
-
|
|
272
|
-
|
|
529
|
+
// Process the next available message (one-shot processing)
|
|
530
|
+
try {
|
|
531
|
+
await worker.receiveNextMessage(async (message) => {
|
|
532
|
+
console.log(`Processing task: ${message.payload.taskType}`);
|
|
533
|
+
await processTask(message.payload.taskType, message.payload.data);
|
|
534
|
+
});
|
|
535
|
+
console.log("Message processed successfully");
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error instanceof QueueEmptyError) {
|
|
538
|
+
console.log("No messages available");
|
|
539
|
+
} else if (error instanceof MessageLockedError) {
|
|
540
|
+
console.log("Next message is locked (FIFO queue)");
|
|
541
|
+
if (error.retryAfter) {
|
|
542
|
+
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
console.error("Error processing message:", error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// You can also use it with timeout results
|
|
550
|
+
await worker.receiveNextMessage(async (message) => {
|
|
551
|
+
if (!canProcessTaskType(message.payload.taskType)) {
|
|
552
|
+
// Return timeout to retry later
|
|
553
|
+
return { timeoutSeconds: 60 };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
await processTask(message.payload.taskType, message.payload.data);
|
|
273
557
|
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Timing Out Messages
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
const workTopic = createTopic<{ taskType: string; data: any }>(
|
|
564
|
+
client,
|
|
565
|
+
"work-queue",
|
|
566
|
+
);
|
|
567
|
+
const worker = workTopic.consumerGroup("workers");
|
|
568
|
+
const controller = new AbortController();
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
await worker.subscribe(controller.signal, async (message) => {
|
|
572
|
+
const { taskType, data } = message.payload;
|
|
274
573
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
574
|
+
// Check if we can process this task type right now
|
|
575
|
+
if (taskType === "heavy-computation" && isSystemOverloaded()) {
|
|
576
|
+
// Return timeout to retry later (5 minutes)
|
|
577
|
+
return { timeoutSeconds: 300 };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Check if we have required resources
|
|
581
|
+
if (taskType === "external-api" && !isExternalServiceAvailable()) {
|
|
582
|
+
// Return timeout to retry in 1 minute
|
|
583
|
+
return { timeoutSeconds: 60 };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Process the message normally
|
|
587
|
+
console.log(`Processing ${taskType} task`);
|
|
588
|
+
await processTask(taskType, data);
|
|
589
|
+
// Message will be automatically deleted on successful completion
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
console.error("Worker processing error:", error);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Example with exponential backoff
|
|
596
|
+
const backoffController = new AbortController();
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
await worker.subscribe(backoffController.signal, async (message) => {
|
|
600
|
+
const maxRetries = 3;
|
|
601
|
+
const deliveryCount = message.deliveryCount;
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
await processMessage(message.payload);
|
|
605
|
+
// Successful processing - message will be deleted
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (deliveryCount < maxRetries) {
|
|
608
|
+
// Exponential backoff: 2^deliveryCount minutes
|
|
609
|
+
const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
|
|
610
|
+
console.log(
|
|
611
|
+
`Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
|
|
612
|
+
);
|
|
613
|
+
return { timeoutSeconds: timeoutSeconds };
|
|
614
|
+
} else {
|
|
615
|
+
// Max retries reached, let the message fail and be deleted
|
|
616
|
+
console.error("Max retries reached, message will be discarded:", error);
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
} catch (error) {
|
|
622
|
+
console.error("Backoff processing error:", error);
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Complete Example: Video Processing Pipeline
|
|
627
|
+
|
|
628
|
+
Here's a comprehensive example showing a video processing pipeline that processes videos with FFmpeg and stores the results in Vercel Blob:
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
import { QueueClient, createTopic, StreamTransport } from "@vercel/queue";
|
|
632
|
+
import { spawn } from "child_process";
|
|
633
|
+
import ffmpeg from "ffmpeg-static";
|
|
634
|
+
import { put } from "@vercel/blob";
|
|
635
|
+
|
|
636
|
+
const client = new QueueClient({
|
|
637
|
+
token: "your-vercel-oidc-token",
|
|
282
638
|
});
|
|
283
639
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
)
|
|
640
|
+
// Input topic with unoptimized videos
|
|
641
|
+
const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
642
|
+
client,
|
|
643
|
+
"unoptimized-videos",
|
|
644
|
+
new StreamTransport(),
|
|
645
|
+
);
|
|
289
646
|
|
|
290
|
-
//
|
|
291
|
-
|
|
647
|
+
// Output topic for optimized videos
|
|
648
|
+
const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
|
|
649
|
+
client,
|
|
650
|
+
"optimized-videos",
|
|
651
|
+
new StreamTransport(),
|
|
652
|
+
);
|
|
292
653
|
|
|
293
|
-
|
|
294
|
-
|
|
654
|
+
// Step 1: Process videos with FFmpeg
|
|
655
|
+
const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
|
|
656
|
+
const processingController = new AbortController();
|
|
657
|
+
|
|
658
|
+
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
|
+
}
|
|
668
|
+
|
|
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}`));
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Publish optimized video to next topic
|
|
720
|
+
await optimizedVideosTopic.publish(optimizedStream);
|
|
721
|
+
console.log("Video optimized and published");
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
} catch (error) {
|
|
725
|
+
console.error("Video processing error:", error);
|
|
295
726
|
}
|
|
727
|
+
|
|
728
|
+
// Step 2: Store optimized videos in Vercel Blob
|
|
729
|
+
const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
|
|
730
|
+
const uploadController = new AbortController();
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
await blobUploader.subscribe(uploadController.signal, async (message) => {
|
|
734
|
+
const optimizedVideo = message.payload;
|
|
735
|
+
|
|
736
|
+
// Upload to Vercel Blob storage
|
|
737
|
+
const filename = `optimized-${Date.now()}.webm`;
|
|
738
|
+
const blob = await put(filename, optimizedVideo, {
|
|
739
|
+
access: "public",
|
|
740
|
+
contentType: "video/webm",
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
|
|
744
|
+
});
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.error("Blob upload error:", error);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Graceful shutdown
|
|
750
|
+
process.on("SIGINT", () => {
|
|
751
|
+
processingController.abort();
|
|
752
|
+
uploadController.abort();
|
|
753
|
+
});
|
|
296
754
|
```
|
|
297
755
|
|
|
298
|
-
##
|
|
756
|
+
## Error Handling
|
|
757
|
+
|
|
758
|
+
The queue client provides specific error types for different failure scenarios:
|
|
299
759
|
|
|
300
|
-
|
|
301
|
-
- **Payload Size**: Maximum payload size is 4.5MB (this limit will be increased soon)
|
|
302
|
-
- **Number of Topics**: No limit on the number of topics you can create
|
|
760
|
+
### Error Types
|
|
303
761
|
|
|
304
|
-
|
|
762
|
+
- **`QueueEmptyError`**: Thrown when attempting to receive messages from an empty queue (204 status)
|
|
305
763
|
|
|
306
|
-
|
|
764
|
+
- Only thrown when directly using `client.receiveMessages()`
|
|
765
|
+
- `ConsumerGroup.subscribe()` handles this error internally and continues polling
|
|
307
766
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
767
|
+
- **`MessageLockedError`**: Thrown when a message is temporarily locked (423 status)
|
|
768
|
+
|
|
769
|
+
- 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
|
|
773
|
+
|
|
774
|
+
- **`MessageNotFoundError`**: Message doesn't exist (404 status)
|
|
775
|
+
|
|
776
|
+
- **`MessageNotAvailableError`**: Message exists but isn't available for processing (409 status)
|
|
777
|
+
|
|
778
|
+
- **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status with nextMessageId)
|
|
779
|
+
|
|
780
|
+
- Contains `nextMessageId` property indicating which message to process first
|
|
781
|
+
|
|
782
|
+
- **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424 status)
|
|
783
|
+
|
|
784
|
+
- Contains `nextMessageId` property indicating which message must be processed first
|
|
785
|
+
- Similar to `FifoOrderingViolationError` but specifically for receive-by-ID operations
|
|
786
|
+
|
|
787
|
+
- **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
|
|
788
|
+
|
|
789
|
+
- **`BadRequestError`**: Invalid request parameters (400 status)
|
|
790
|
+
|
|
791
|
+
- Invalid queue names, FIFO limit violations, missing required parameters
|
|
792
|
+
|
|
793
|
+
- **`UnauthorizedError`**: Authentication failure (401 status)
|
|
794
|
+
|
|
795
|
+
- Missing or invalid authentication token
|
|
796
|
+
|
|
797
|
+
- **`ForbiddenError`**: Access denied (403 status)
|
|
798
|
+
|
|
799
|
+
- Queue environment doesn't match token environment
|
|
800
|
+
|
|
801
|
+
- **`InternalServerError`**: Server-side errors (500+ status codes)
|
|
802
|
+
- Unexpected server errors, service unavailable, etc.
|
|
803
|
+
|
|
804
|
+
### Error Handling Examples
|
|
805
|
+
|
|
806
|
+
```typescript
|
|
807
|
+
import {
|
|
808
|
+
QueueEmptyError,
|
|
809
|
+
MessageLockedError,
|
|
810
|
+
FifoOrderingViolationError,
|
|
811
|
+
FailedDependencyError,
|
|
812
|
+
BadRequestError,
|
|
813
|
+
UnauthorizedError,
|
|
814
|
+
ForbiddenError,
|
|
815
|
+
InternalServerError,
|
|
816
|
+
} from "@vercel/queue";
|
|
817
|
+
|
|
818
|
+
// Handle empty queue or locked messages
|
|
819
|
+
try {
|
|
820
|
+
for await (const message of client.receiveMessages(options, transport)) {
|
|
821
|
+
// Process messages
|
|
822
|
+
}
|
|
823
|
+
} catch (error) {
|
|
824
|
+
if (error instanceof QueueEmptyError) {
|
|
825
|
+
console.log("Queue is empty, retry later");
|
|
826
|
+
} else if (error instanceof MessageLockedError) {
|
|
827
|
+
console.log("Next message in FIFO queue is locked");
|
|
828
|
+
if (error.retryAfter) {
|
|
829
|
+
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
319
830
|
}
|
|
320
831
|
}
|
|
321
832
|
}
|
|
322
|
-
```
|
|
323
833
|
|
|
324
|
-
|
|
834
|
+
// Handle locked message with retry
|
|
835
|
+
try {
|
|
836
|
+
await consumer.receiveMessage(messageId, handler);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (error instanceof MessageLockedError) {
|
|
839
|
+
console.log("Message is locked by another consumer");
|
|
840
|
+
if (error.retryAfter) {
|
|
841
|
+
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
842
|
+
setTimeout(() => retry(), error.retryAfter * 1000);
|
|
843
|
+
}
|
|
844
|
+
} else if (error instanceof FailedDependencyError) {
|
|
845
|
+
// FIFO ordering violation for receive by ID
|
|
846
|
+
console.log(`Must process ${error.nextMessageId} first`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Handle authentication and authorization errors
|
|
851
|
+
try {
|
|
852
|
+
await topic.publish(payload);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
if (error instanceof UnauthorizedError) {
|
|
855
|
+
console.log("Invalid token - refresh authentication");
|
|
856
|
+
} else if (error instanceof ForbiddenError) {
|
|
857
|
+
console.log("Environment mismatch - check token/queue configuration");
|
|
858
|
+
} else if (error instanceof BadRequestError) {
|
|
859
|
+
console.log("Invalid parameters:", error.message);
|
|
860
|
+
} else if (error instanceof InternalServerError) {
|
|
861
|
+
console.log("Server error - retry with backoff");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
325
864
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
865
|
+
// Complete error handling pattern
|
|
866
|
+
function handleQueueError(error: unknown): void {
|
|
867
|
+
if (error instanceof QueueEmptyError || error instanceof MessageLockedError) {
|
|
868
|
+
// Transient errors - safe to retry
|
|
869
|
+
console.log("Temporary condition, will retry");
|
|
870
|
+
} else if (
|
|
871
|
+
error instanceof UnauthorizedError ||
|
|
872
|
+
error instanceof ForbiddenError
|
|
873
|
+
) {
|
|
874
|
+
// Authentication/authorization errors - need to fix configuration
|
|
875
|
+
console.log("Auth error - check credentials");
|
|
876
|
+
} else if (error instanceof BadRequestError) {
|
|
877
|
+
// Client error - fix the request
|
|
878
|
+
console.log("Invalid request:", error.message);
|
|
879
|
+
} else if (error instanceof InternalServerError) {
|
|
880
|
+
// Server error - implement exponential backoff
|
|
881
|
+
console.log("Server error - retry with backoff");
|
|
882
|
+
} else {
|
|
883
|
+
// Unknown error
|
|
884
|
+
console.error("Unexpected error:", error);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
```
|
|
329
888
|
|
|
330
889
|
## License
|
|
331
890
|
|