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