@vercel/queue 0.0.0-alpha.9 → 0.0.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 +478 -603
- package/dist/index.d.mts +497 -188
- package/dist/index.d.ts +497 -188
- package/dist/index.js +1419 -765
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1395 -757
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
# Vercel Queues
|
|
2
2
|
|
|
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.
|
|
3
|
+
A TypeScript client library for interacting with the Vercel Queue Service API, designed for seamless integration with Vercel deployments.
|
|
6
4
|
|
|
7
5
|
## Features
|
|
8
6
|
|
|
9
|
-
- **
|
|
10
|
-
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
- **Pub/Sub Pattern**: Topic-based messaging with consumer groups
|
|
16
|
-
- **Type Safety**: Full TypeScript support with generic types
|
|
17
|
-
- **Automatic Retries**: Built-in visibility timeout management
|
|
7
|
+
- **Simple API**: `send` and `receive` are all you need
|
|
8
|
+
- **Automatic Triggering on Vercel**: Vercel invokes your route handlers when messages are ready
|
|
9
|
+
- **Works Anywhere**: `send` and `receive` work in any Node.js environment
|
|
10
|
+
- **Type Safety**: Full TypeScript generics support
|
|
11
|
+
- **Customizable Serialization**: Built-in JSON, Buffer, and Stream transports
|
|
12
|
+
- **Local Dev Mode**: Messages sent locally trigger your handlers automatically
|
|
18
13
|
|
|
19
14
|
## Installation
|
|
20
15
|
|
|
@@ -24,245 +19,211 @@ npm install @vercel/queue
|
|
|
24
19
|
|
|
25
20
|
## Quick Start
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
(including the OIDC token):
|
|
22
|
+
Set up your region via environment variables. If your framework supports `.env` files (Next.js, Vite, Nuxt, etc.):
|
|
29
23
|
|
|
30
24
|
```bash
|
|
31
|
-
#
|
|
32
|
-
|
|
25
|
+
# .env.production (on Vercel, inherits the platform's region)
|
|
26
|
+
QUEUE_REGION=${VERCEL_REGION}
|
|
33
27
|
|
|
34
|
-
#
|
|
35
|
-
|
|
28
|
+
# .env.development (fixed region for local dev — iad1 is recommended)
|
|
29
|
+
QUEUE_REGION=iad1
|
|
36
30
|
```
|
|
37
31
|
|
|
38
|
-
|
|
32
|
+
Otherwise, set `QUEUE_REGION` in your environment directly (e.g. via your hosting provider's dashboard or a `dotenv` setup).
|
|
33
|
+
|
|
34
|
+
Create a shared queue client:
|
|
39
35
|
|
|
40
36
|
```typescript
|
|
41
|
-
//
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
type Message = {
|
|
45
|
-
message: string;
|
|
46
|
-
timestamp: number;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// Send a message to a topic
|
|
50
|
-
await send<Message>("my-topic", {
|
|
51
|
-
message: "Hello, World!",
|
|
52
|
-
timestamp: Date.now(),
|
|
53
|
-
});
|
|
37
|
+
// lib/queue.ts
|
|
38
|
+
import { QueueClient } from "@vercel/queue";
|
|
54
39
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
await receive<Message>("my-topic", "my-consumer-group", (message, metadata) => {
|
|
58
|
-
console.log("Received:", message.message);
|
|
59
|
-
console.log("Timestamp:", new Date(message.timestamp));
|
|
60
|
-
console.log("Message Metadata", metadata);
|
|
61
|
-
// => { messageId, deliveryCount, timestamp }
|
|
62
|
-
});
|
|
40
|
+
const queue = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
41
|
+
export const { send, receive, handleCallback, handleNodeCallback } = queue;
|
|
63
42
|
```
|
|
64
43
|
|
|
65
|
-
|
|
44
|
+
Send a message anywhere in your app:
|
|
66
45
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
consumption based on your vercel.json configuration.
|
|
46
|
+
```typescript
|
|
47
|
+
import { send } from "@/lib/queue";
|
|
70
48
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
[create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
49
|
+
await send("my-topic", { message: "Hello world" });
|
|
50
|
+
```
|
|
74
51
|
|
|
75
|
-
|
|
52
|
+
Handle incoming messages with a route handler:
|
|
76
53
|
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
```typescript
|
|
55
|
+
// app/api/queue/my-topic/route.ts
|
|
56
|
+
import { handleCallback } from "@/lib/queue";
|
|
57
|
+
|
|
58
|
+
export const POST = handleCallback(async (message, metadata) => {
|
|
59
|
+
console.log("Processing:", message);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Configure your `vercel.json`:
|
|
79
64
|
|
|
80
65
|
```json
|
|
81
66
|
{
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
|
|
67
|
+
"functions": {
|
|
68
|
+
"app/api/queue/my-topic/route.ts": {
|
|
69
|
+
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
|
|
70
|
+
}
|
|
85
71
|
}
|
|
86
72
|
}
|
|
87
73
|
```
|
|
88
74
|
|
|
89
|
-
###
|
|
75
|
+
### Project Setup
|
|
90
76
|
|
|
91
|
-
|
|
77
|
+
For local development, link your Vercel project:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm i -g vercel
|
|
81
|
+
vc link
|
|
82
|
+
vc env pull
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Local Development
|
|
86
|
+
|
|
87
|
+
**Queues just work locally.** When you `send()` messages in development mode, the library sends them to the real Vercel Queue Service, reads your `vercel.json` configuration, discovers your queue handlers, and triggers them automatically via local HTTP requests. This means your local dev environment behaves identically to production — no surprising behavior differences.
|
|
88
|
+
|
|
89
|
+
> **Note:** Local dev mode is enabled when `NODE_ENV=development`. Most frameworks (Next.js, etc.) set this automatically during `npm run dev`.
|
|
90
|
+
|
|
91
|
+
## Publishing Messages
|
|
92
92
|
|
|
93
93
|
```typescript
|
|
94
|
-
|
|
95
|
-
"use server";
|
|
94
|
+
import { QueueClient } from "@vercel/queue";
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
message,
|
|
102
|
-
timestamp: Date.now(),
|
|
103
|
-
});
|
|
98
|
+
// Simple send
|
|
99
|
+
await send("my-topic", { message: "Hello world" });
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
// With options
|
|
102
|
+
await send(
|
|
103
|
+
"my-topic",
|
|
104
|
+
{ message: "Hello world" },
|
|
105
|
+
{
|
|
106
|
+
idempotencyKey: "unique-key", // Prevent duplicate messages
|
|
107
|
+
retentionSeconds: 3600, // 1 hour TTL (default: 24h)
|
|
108
|
+
delaySeconds: 60, // Delay delivery by 1 minute
|
|
109
|
+
},
|
|
110
|
+
);
|
|
107
111
|
```
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
Example usage in an API route:
|
|
110
114
|
|
|
111
|
-
```
|
|
112
|
-
// app/
|
|
113
|
-
|
|
114
|
-
import { publishTestMessage } from "./actions";
|
|
115
|
+
```typescript
|
|
116
|
+
// app/api/send-message/route.ts
|
|
117
|
+
import { send } from "@/lib/queue";
|
|
115
118
|
|
|
116
|
-
export
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Publish Test Message
|
|
121
|
-
</button>
|
|
122
|
-
);
|
|
119
|
+
export async function POST(request: Request) {
|
|
120
|
+
const body = await request.json();
|
|
121
|
+
const { messageId } = await send("my-topic", { message: body.message });
|
|
122
|
+
return Response.json({ messageId });
|
|
123
123
|
}
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
> **Note:** `messageId` is `null` when the server accepts the message for deferred processing (e.g. during a server-side outage). The message will still be delivered.
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
## Consuming Messages
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
### On Vercel
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
On Vercel, messages are consumed using API route handlers that Vercel automatically invokes when messages are available. Use `handleCallback` or `handleNodeCallback` to create these route handlers.
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
#### Web API — `handleCallback`
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
Returns `(Request) => Promise<Response>`. For frameworks that export Web API route handlers (Next.js App Router, Hono, etc.).
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
// app/api/queue/handle/route.ts
|
|
140
|
-
import { handleCallback } from "@vercel/queue";
|
|
141
|
-
|
|
142
|
-
// Option 1: Single topic with multiple consumer groups
|
|
143
|
-
export const POST = handleCallback({
|
|
144
|
-
"my-topic": {
|
|
145
|
-
"consumer-group-1": async (message, metadata) => {
|
|
146
|
-
console.log(`Consumer group 1 processing:`, message, metadata);
|
|
147
|
-
// Handle consumer group 1 logic
|
|
148
|
-
await processGroup1(message);
|
|
149
|
-
},
|
|
150
|
-
"consumer-group-2": async (message, metadata) => {
|
|
151
|
-
console.log(`Consumer group 2 processing:`, message, metadata);
|
|
152
|
-
// Handle consumer group 2 logic
|
|
153
|
-
await processGroup2(message);
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
async function processGroup1(message: any) {
|
|
159
|
-
// Consumer group 1 specific logic
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function processGroup2(message: any) {
|
|
163
|
-
// Consumer group 2 specific logic
|
|
164
|
-
}
|
|
165
|
-
```
|
|
138
|
+
**Next.js App Router:**
|
|
166
139
|
|
|
167
140
|
```typescript
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
},
|
|
176
|
-
"order-events": {
|
|
177
|
-
fulfillment: async (order, metadata) => {
|
|
178
|
-
console.log(`Processing order:`, order, metadata);
|
|
179
|
-
await fulfillOrder(order);
|
|
180
|
-
},
|
|
181
|
-
analytics: async (order, metadata) => {
|
|
182
|
-
console.log(`Tracking order:`, order, metadata);
|
|
183
|
-
await trackOrder(order);
|
|
184
|
-
},
|
|
185
|
-
},
|
|
141
|
+
// app/api/queue/my-topic/route.ts
|
|
142
|
+
import { handleCallback } from "@/lib/queue";
|
|
143
|
+
|
|
144
|
+
export const POST = handleCallback(async (message, metadata) => {
|
|
145
|
+
// metadata: { messageId, deliveryCount, createdAt, expiresAt?, topicName, consumerGroup, region }
|
|
146
|
+
await processMessage(message);
|
|
147
|
+
// Throwing an error will automatically retry the message
|
|
186
148
|
});
|
|
187
149
|
```
|
|
188
150
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
Create a `vercel.json` file in your project root to declare which topics and consumer groups each API route handles:
|
|
151
|
+
**Hono:**
|
|
192
152
|
|
|
193
|
-
```
|
|
194
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"topic": "my-topic",
|
|
206
|
-
"consumer": "consumer-group-2"
|
|
207
|
-
}
|
|
208
|
-
]
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
153
|
+
```typescript
|
|
154
|
+
import { Hono } from "hono";
|
|
155
|
+
import { handleCallback } from "@/lib/queue";
|
|
156
|
+
|
|
157
|
+
const app = new Hono();
|
|
158
|
+
app.post(
|
|
159
|
+
"/api/queue",
|
|
160
|
+
handleCallback(async (message, metadata) => {
|
|
161
|
+
await processMessage(message);
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
export default app;
|
|
212
165
|
```
|
|
213
166
|
|
|
214
|
-
|
|
167
|
+
#### Connect-style — `handleNodeCallback`
|
|
215
168
|
|
|
216
|
-
|
|
169
|
+
Returns `(req, res) => Promise<void>`. For frameworks that export Connect-style handlers (Express, Next.js Pages Router, etc.).
|
|
170
|
+
|
|
171
|
+
**Next.js Pages Router:**
|
|
217
172
|
|
|
218
173
|
```typescript
|
|
219
|
-
//
|
|
220
|
-
import {
|
|
221
|
-
|
|
222
|
-
export
|
|
223
|
-
|
|
224
|
-
processors: async (user, metadata) => {
|
|
225
|
-
console.log(`Processing user event:`, user, metadata);
|
|
226
|
-
await sendWelcomeEmail(user.email);
|
|
227
|
-
},
|
|
228
|
-
},
|
|
174
|
+
// pages/api/queue/my-topic.ts
|
|
175
|
+
import { handleNodeCallback } from "@/lib/queue";
|
|
176
|
+
|
|
177
|
+
export default handleNodeCallback(async (message, metadata) => {
|
|
178
|
+
await processMessage(message);
|
|
229
179
|
});
|
|
230
180
|
```
|
|
231
181
|
|
|
182
|
+
**Express:**
|
|
183
|
+
|
|
232
184
|
```typescript
|
|
233
|
-
|
|
234
|
-
import {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
},
|
|
243
|
-
|
|
185
|
+
import express from "express";
|
|
186
|
+
import { handleNodeCallback } from "@/lib/queue";
|
|
187
|
+
|
|
188
|
+
const app = express();
|
|
189
|
+
app.use(express.json());
|
|
190
|
+
app.post(
|
|
191
|
+
"/api/queue/my-topic",
|
|
192
|
+
handleNodeCallback(async (message, metadata) => {
|
|
193
|
+
await processMessage(message);
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
export default app;
|
|
244
197
|
```
|
|
245
198
|
|
|
246
|
-
|
|
199
|
+
### 2. Configure vercel.json
|
|
200
|
+
|
|
201
|
+
Tell Vercel which routes handle which topics:
|
|
247
202
|
|
|
248
203
|
```json
|
|
249
204
|
{
|
|
250
205
|
"functions": {
|
|
251
|
-
"app/api/queue/
|
|
206
|
+
"app/api/queue/my-topic/route.ts": {
|
|
252
207
|
"experimentalTriggers": [
|
|
253
208
|
{
|
|
254
|
-
"type": "queue/
|
|
255
|
-
"topic": "
|
|
256
|
-
"
|
|
209
|
+
"type": "queue/v2beta",
|
|
210
|
+
"topic": "my-topic",
|
|
211
|
+
"retryAfterSeconds": 60,
|
|
212
|
+
"initialDelaySeconds": 0
|
|
257
213
|
}
|
|
258
214
|
]
|
|
259
215
|
},
|
|
260
|
-
"app/api/queue/orders/route.ts": {
|
|
216
|
+
"app/api/queue/orders/fulfillment/route.ts": {
|
|
217
|
+
"experimentalTriggers": [
|
|
218
|
+
{ "type": "queue/v2beta", "topic": "order-events" }
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
"app/api/queue/orders/analytics/route.ts": {
|
|
261
222
|
"experimentalTriggers": [
|
|
262
223
|
{
|
|
263
|
-
"type": "queue/
|
|
224
|
+
"type": "queue/v2beta",
|
|
264
225
|
"topic": "order-events",
|
|
265
|
-
"
|
|
226
|
+
"retryAfterSeconds": 300
|
|
266
227
|
}
|
|
267
228
|
]
|
|
268
229
|
}
|
|
@@ -270,522 +231,436 @@ With corresponding `vercel.json`:
|
|
|
270
231
|
}
|
|
271
232
|
```
|
|
272
233
|
|
|
273
|
-
|
|
234
|
+
Multiple route files for the same topic create separate consumer groups — each receives a copy of every message.
|
|
274
235
|
|
|
275
|
-
|
|
276
|
-
- **Message Processing**: Your API routes receive the message ID and other metadata via headers, then use the queue client to process the specific message
|
|
277
|
-
- **Configuration Required**: The `vercel.json` file is essential - it tells Vercel which topics and consumers each route should handle
|
|
278
|
-
- **No Polling**: Unlike traditional queue consumers, you don't need to poll for messages - Vercel handles the triggering automatically
|
|
236
|
+
### 3. Retry and Backoff
|
|
279
237
|
|
|
280
|
-
|
|
238
|
+
When a handler throws, the message is not acknowledged and becomes available for redelivery after the `retryAfterSeconds` interval configured in `vercel.json`. Retries continue until the handler succeeds or the message expires (default: 24 hours).
|
|
281
239
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
Multiple consumers can process messages from the same topic in parallel:
|
|
240
|
+
For finer control over retry timing, pass a `retry` option:
|
|
285
241
|
|
|
286
242
|
```typescript
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
await
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
243
|
+
export const POST = handleCallback(
|
|
244
|
+
async (message, metadata) => {
|
|
245
|
+
await processMessage(message);
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
retry: (error, metadata) => {
|
|
249
|
+
if (error instanceof RateLimitError) return { afterSeconds: 60 };
|
|
250
|
+
// Return undefined to let the error propagate normally
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
);
|
|
297
254
|
```
|
|
298
255
|
|
|
299
|
-
|
|
256
|
+
When `retry` returns `{ afterSeconds: N }`, the message is rescheduled for redelivery after N seconds. Return `{ acknowledge: true }` to acknowledge the message so it is never retried. When it returns `undefined`, the error propagates normally and the message is retried at the default interval.
|
|
300
257
|
|
|
301
|
-
|
|
302
|
-
- **Consumer Groups**: Named groups of consumers that process messages in
|
|
303
|
-
parallel
|
|
304
|
-
- `receive()`: Process messages with flexible consumption patterns
|
|
305
|
-
- Basic usage: Process next available message
|
|
306
|
-
- With `messageId`: Process specific message by ID
|
|
307
|
-
- With `skipPayload: true`: Process message metadata only (without payload)
|
|
308
|
-
- **Transports**: Pluggable serialization/deserialization for different data
|
|
309
|
-
types
|
|
310
|
-
- **Streaming**: Memory-efficient processing of large payloads
|
|
311
|
-
- **Visibility Timeouts**: Automatic message lifecycle management
|
|
258
|
+
**Exponential backoff** uses `metadata.deliveryCount` (starts at 1, increments each delivery):
|
|
312
259
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
can be configured using the `transport` option when calling `send()` or `receive()`.
|
|
328
|
-
|
|
329
|
-
### Built-in Transports
|
|
330
|
-
|
|
331
|
-
#### JsonTransport (Default)
|
|
260
|
+
```typescript
|
|
261
|
+
export const POST = handleCallback(
|
|
262
|
+
async (message, metadata) => {
|
|
263
|
+
await processMessage(message);
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
retry: (error, metadata) => {
|
|
267
|
+
// 5s → 10s → 20s → 40s → ... capped at 5 min
|
|
268
|
+
const delay = Math.min(300, 2 ** metadata.deliveryCount * 5);
|
|
269
|
+
return { afterSeconds: delay };
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
```
|
|
332
274
|
|
|
333
|
-
|
|
334
|
-
memory.
|
|
275
|
+
**Conditional retry** — only retry transient errors:
|
|
335
276
|
|
|
336
277
|
```typescript
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
278
|
+
export const POST = handleCallback(
|
|
279
|
+
async (message, metadata) => {
|
|
280
|
+
await processMessage(message);
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
retry: (error, metadata) => {
|
|
284
|
+
if (error instanceof RateLimitError) return { afterSeconds: 60 };
|
|
285
|
+
if (error instanceof TemporaryError) return { afterSeconds: 30 };
|
|
286
|
+
// Permanent errors: return undefined → retried at the default interval
|
|
287
|
+
},
|
|
288
|
+
},
|
|
345
289
|
);
|
|
346
290
|
```
|
|
347
291
|
|
|
348
|
-
|
|
292
|
+
**Acknowledging poison messages** — stop retrying messages that can never succeed:
|
|
349
293
|
|
|
350
|
-
|
|
351
|
-
|
|
294
|
+
```typescript
|
|
295
|
+
export const POST = handleCallback(
|
|
296
|
+
async (message, metadata) => {
|
|
297
|
+
await processMessage(message);
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
retry: (error, metadata) => {
|
|
301
|
+
if (error instanceof ValidationError) return { acknowledge: true };
|
|
302
|
+
if (metadata.deliveryCount > 5) return { acknowledge: true };
|
|
303
|
+
return { afterSeconds: Math.min(300, 2 ** metadata.deliveryCount * 5) };
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
```
|
|
352
308
|
|
|
353
|
-
|
|
309
|
+
The `retry` option is available on `handleCallback`, `handleNodeCallback`, and `receive`.
|
|
354
310
|
|
|
355
|
-
|
|
356
|
-
Ideal for large files and memory-efficient processing.
|
|
311
|
+
## Custom Client Configuration
|
|
357
312
|
|
|
358
|
-
|
|
313
|
+
All configuration lives on the `QueueClient`:
|
|
359
314
|
|
|
360
|
-
|
|
361
|
-
|
|
315
|
+
```typescript
|
|
316
|
+
import { QueueClient, BufferTransport } from "@vercel/queue";
|
|
317
|
+
|
|
318
|
+
const queue = new QueueClient({
|
|
319
|
+
region: process.env.QUEUE_REGION!, // Required — see Quick Start for env setup
|
|
320
|
+
token: "my-token", // Auth token (default: OIDC auto-detection)
|
|
321
|
+
transport: new BufferTransport(), // Serialization (default: JsonTransport)
|
|
322
|
+
headers: { "X-Custom": "header" }, // Custom headers on all requests
|
|
323
|
+
deploymentId: null, // null = unpinned, omit = auto from env, or explicit string
|
|
324
|
+
});
|
|
362
325
|
|
|
363
|
-
|
|
326
|
+
// Use directly
|
|
327
|
+
await queue.send("my-topic", myBuffer);
|
|
364
328
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
| Binary files < 100MB | `BufferTransport` | Medium | High |
|
|
369
|
-
| Large files > 100MB | `StreamTransport` | Very Low | Medium |
|
|
370
|
-
| Real-time data streams | `StreamTransport` | Very Low | High |
|
|
371
|
-
| Custom protocols | Custom implementation | Varies | Varies |
|
|
329
|
+
// Or destructure
|
|
330
|
+
export const { send, receive, handleCallback, handleNodeCallback } = queue;
|
|
331
|
+
```
|
|
372
332
|
|
|
373
|
-
|
|
333
|
+
The client sends requests to `https://${region}.vercel-queue.com`. When `handleCallback` receives a message, it reads the `ce-vqsregion` header and routes follow-up API calls to the correct regional endpoint.
|
|
374
334
|
|
|
375
|
-
|
|
335
|
+
To customize the URL scheme, provide a `resolveBaseUrl` that returns a `URL`:
|
|
376
336
|
|
|
377
337
|
```typescript
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
await send<T>(topicName, payload, {
|
|
383
|
-
transport?: Transport<T>;
|
|
384
|
-
idempotencyKey?: string;
|
|
385
|
-
retentionSeconds?: number;
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
// Examples:
|
|
389
|
-
await send("notifications", { userId: "123", message: "Welcome!" });
|
|
390
|
-
|
|
391
|
-
await send("images", imageBuffer, {
|
|
392
|
-
transport: new BufferTransport(),
|
|
338
|
+
// Custom domain
|
|
339
|
+
const queue = new QueueClient({
|
|
340
|
+
region: process.env.QUEUE_REGION!,
|
|
341
|
+
resolveBaseUrl: (region) => new URL(`https://${region}.my-proxy.example`),
|
|
393
342
|
});
|
|
394
343
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
344
|
+
// Custom domain with a base path (e.g. reverse proxy prefix)
|
|
345
|
+
const queue = new QueueClient({
|
|
346
|
+
region: process.env.QUEUE_REGION!,
|
|
347
|
+
resolveBaseUrl: (region) =>
|
|
348
|
+
new URL(`https://my-proxy.example/queues/${region}`),
|
|
349
|
+
// → requests go to https://my-proxy.example/queues/<region>/api/v3/…
|
|
398
350
|
});
|
|
399
351
|
```
|
|
400
352
|
|
|
401
|
-
|
|
353
|
+
The SDK always appends its own API path (`/api/v3/…`) to the returned URL.
|
|
354
|
+
|
|
355
|
+
## Transports
|
|
356
|
+
|
|
357
|
+
The transport controls how message payloads are serialized and deserialized.
|
|
358
|
+
|
|
359
|
+
| Use Case | Transport | Memory Usage | Notes |
|
|
360
|
+
| --------------- | ----------------- | ------------ | ----------------------- |
|
|
361
|
+
| Structured data | `JsonTransport` | Low | Default, JSON encoding |
|
|
362
|
+
| Binary data | `BufferTransport` | Medium | Raw bytes |
|
|
363
|
+
| Large payloads | `StreamTransport` | Very Low | No buffering, streaming |
|
|
402
364
|
|
|
403
365
|
```typescript
|
|
404
|
-
|
|
405
|
-
|
|
366
|
+
import {
|
|
367
|
+
QueueClient,
|
|
368
|
+
JsonTransport,
|
|
369
|
+
BufferTransport,
|
|
370
|
+
StreamTransport,
|
|
371
|
+
} from "@vercel/queue";
|
|
406
372
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
373
|
+
// JSON with custom serialization
|
|
374
|
+
const queue = new QueueClient({
|
|
375
|
+
region: process.env.QUEUE_REGION!,
|
|
376
|
+
transport: new JsonTransport({
|
|
377
|
+
replacer: (key, value) => (key === "password" ? undefined : value),
|
|
378
|
+
reviver: (key, value) => (key === "date" ? new Date(value) : value),
|
|
379
|
+
}),
|
|
410
380
|
});
|
|
411
381
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
382
|
+
// Binary data
|
|
383
|
+
const binQueue = new QueueClient({
|
|
384
|
+
region: process.env.QUEUE_REGION!,
|
|
385
|
+
transport: new BufferTransport(),
|
|
386
|
+
});
|
|
387
|
+
await binQueue.send("binary-topic", myBuffer);
|
|
388
|
+
|
|
389
|
+
// Streaming for large payloads
|
|
390
|
+
const streamQueue = new QueueClient({
|
|
391
|
+
region: process.env.QUEUE_REGION!,
|
|
392
|
+
transport: new StreamTransport(),
|
|
417
393
|
});
|
|
394
|
+
await streamQueue.send("large-file", myReadableStream);
|
|
418
395
|
```
|
|
419
396
|
|
|
420
|
-
|
|
397
|
+
## Manual Receive
|
|
421
398
|
|
|
422
|
-
|
|
423
|
-
// Handler function signature
|
|
424
|
-
type MessageHandler<T = unknown> = (
|
|
425
|
-
message: T,
|
|
426
|
-
metadata: MessageMetadata,
|
|
427
|
-
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
399
|
+
Use `receive` to pull and process messages directly. This is an advanced alternative to `handleCallback` that works in any Node.js environment, both on and off Vercel.
|
|
428
400
|
|
|
429
|
-
|
|
430
|
-
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
401
|
+
### Region considerations
|
|
431
402
|
|
|
432
|
-
|
|
433
|
-
timeoutSeconds: number; // seconds before message becomes available again
|
|
434
|
-
}
|
|
403
|
+
Messages can only be received from the region they were sent to. When using `receive`, use a **fixed region** (e.g. `"iad1"`) for both sending and receiving — do not use `VERCEL_REGION` (or `QUEUE_REGION=${VERCEL_REGION}`), because Vercel may route requests to different regions due to failover or load balancing, distributing your messages across regions unpredictably.
|
|
435
404
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
deliveryCount: number;
|
|
440
|
-
timestamp: string;
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### Receive Options
|
|
405
|
+
```bash
|
|
406
|
+
# .env.production — fixed region for manual receive workflows
|
|
407
|
+
QUEUE_REGION=iad1
|
|
445
408
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
interface ReceiveOptions<T = unknown> {
|
|
449
|
-
messageId?: string; // Process specific message by ID
|
|
450
|
-
skipPayload?: boolean; // Skip payload download (requires messageId)
|
|
451
|
-
transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
|
|
452
|
-
visibilityTimeoutSeconds?: number; // Message visibility timeout
|
|
453
|
-
refreshInterval?: number; // Refresh interval for long-running operations
|
|
454
|
-
}
|
|
409
|
+
# .env.development
|
|
410
|
+
QUEUE_REGION=iad1
|
|
455
411
|
```
|
|
456
412
|
|
|
457
|
-
|
|
413
|
+
A single region is still highly available — Vercel deploys across 3+ availability zones within each region. If you need multi-region availability, you are responsible for designing your own HA strategy (e.g. sending to multiple regions and receiving from each).
|
|
458
414
|
|
|
459
|
-
|
|
460
|
-
interface Transport<T = unknown> {
|
|
461
|
-
serialize(value: T): Buffer | ReadableStream<Uint8Array>;
|
|
462
|
-
deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
|
|
463
|
-
contentType: string;
|
|
464
|
-
}
|
|
465
|
-
```
|
|
415
|
+
For most use cases on Vercel, `handleCallback` is the recommended approach — the platform handles region routing automatically and the SDK routes follow-up calls to the correct region via the `ce-vqsregion` header.
|
|
466
416
|
|
|
467
|
-
###
|
|
417
|
+
### Usage
|
|
468
418
|
|
|
469
419
|
```typescript
|
|
470
|
-
|
|
471
|
-
function handleCallback(
|
|
472
|
-
handlers: CallbackHandlers,
|
|
473
|
-
): (request: Request) => Promise<Response>;
|
|
474
|
-
|
|
475
|
-
// Configuration object with handlers for different topics and consumer groups
|
|
476
|
-
type CallbackHandlers = {
|
|
477
|
-
[topicName: string]: { [consumerGroup: string]: MessageHandler };
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
// Example usage:
|
|
481
|
-
export const POST = handleCallback({
|
|
482
|
-
"user-events": {
|
|
483
|
-
welcome: (message, metadata) => {
|
|
484
|
-
console.log(`New user event:`, message, metadata);
|
|
485
|
-
},
|
|
486
|
-
},
|
|
420
|
+
import { QueueClient } from "@vercel/queue";
|
|
487
421
|
|
|
488
|
-
|
|
489
|
-
"image-processing": {
|
|
490
|
-
compress: (message, metadata) => console.log("Compressing image", message),
|
|
491
|
-
resize: (message, metadata) => console.log("Resizing image", message),
|
|
492
|
-
},
|
|
493
|
-
});
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
## Examples
|
|
422
|
+
const { receive } = new QueueClient({ region: "iad1" });
|
|
497
423
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
424
|
+
// Process next available message
|
|
425
|
+
const result = await receive(
|
|
426
|
+
"my-topic",
|
|
427
|
+
"my-group",
|
|
428
|
+
async (message, metadata) => {
|
|
429
|
+
console.log("Processing:", message);
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
if (!result.ok) {
|
|
433
|
+
console.log("Queue was empty:", result.reason);
|
|
505
434
|
}
|
|
506
435
|
|
|
507
|
-
//
|
|
508
|
-
await
|
|
509
|
-
userId: "123",
|
|
510
|
-
action: "login",
|
|
511
|
-
timestamp: Date.now(),
|
|
512
|
-
});
|
|
436
|
+
// Batch processing: up to 10 messages in one request
|
|
437
|
+
await receive("my-topic", "my-group", handler, { limit: 10 });
|
|
513
438
|
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
await receive<UserEvent>("user-events", "processors", async (message) => {
|
|
517
|
-
console.log(`User ${message.userId} performed ${message.action}`);
|
|
518
|
-
});
|
|
519
|
-
} catch (error) {
|
|
520
|
-
console.error("Processing error:", error);
|
|
521
|
-
}
|
|
439
|
+
// Process a specific message by ID
|
|
440
|
+
await receive("my-topic", "my-group", handler, { messageId: "msg-123" });
|
|
522
441
|
```
|
|
523
442
|
|
|
524
|
-
|
|
443
|
+
> **Note:** `limit` and `messageId` are mutually exclusive options. The handler is never called when the queue is empty — check `result.ok` instead.
|
|
444
|
+
|
|
445
|
+
## Error Handling
|
|
525
446
|
|
|
526
447
|
```typescript
|
|
527
|
-
|
|
528
|
-
|
|
448
|
+
import {
|
|
449
|
+
BadRequestError,
|
|
450
|
+
DuplicateMessageError,
|
|
451
|
+
ForbiddenError,
|
|
452
|
+
InternalServerError,
|
|
453
|
+
UnauthorizedError,
|
|
454
|
+
} from "@vercel/queue";
|
|
455
|
+
import { send } from "@/lib/queue";
|
|
529
456
|
|
|
530
457
|
try {
|
|
531
|
-
await
|
|
532
|
-
"user-events",
|
|
533
|
-
"processors",
|
|
534
|
-
async (message, { messageId }) => {
|
|
535
|
-
console.log(`Processing specific message: ${messageId}`);
|
|
536
|
-
console.log(`User ${message.userId} performed ${message.action}`);
|
|
537
|
-
},
|
|
538
|
-
{ messageId },
|
|
539
|
-
);
|
|
540
|
-
console.log("Message processed successfully");
|
|
458
|
+
await send("my-topic", payload);
|
|
541
459
|
} catch (error) {
|
|
542
|
-
if (error
|
|
543
|
-
console.log("
|
|
544
|
-
} else {
|
|
545
|
-
console.
|
|
460
|
+
if (error instanceof UnauthorizedError) {
|
|
461
|
+
console.log("Invalid token - refresh authentication");
|
|
462
|
+
} else if (error instanceof ForbiddenError) {
|
|
463
|
+
console.log("Environment mismatch - check configuration");
|
|
464
|
+
} else if (error instanceof BadRequestError) {
|
|
465
|
+
console.log("Invalid parameters:", error.message);
|
|
466
|
+
} else if (error instanceof DuplicateMessageError) {
|
|
467
|
+
console.log("Duplicate message:", error.idempotencyKey);
|
|
468
|
+
} else if (error instanceof InternalServerError) {
|
|
469
|
+
console.log("Server error - retry with backoff");
|
|
546
470
|
}
|
|
547
471
|
}
|
|
548
472
|
```
|
|
549
473
|
|
|
550
|
-
|
|
474
|
+
All error types:
|
|
475
|
+
|
|
476
|
+
| Error | Description |
|
|
477
|
+
| ------------------------------------ | --------------------------------------------- |
|
|
478
|
+
| `BadRequestError` | Invalid request parameters |
|
|
479
|
+
| `UnauthorizedError` | Authentication failed (invalid/missing token) |
|
|
480
|
+
| `ForbiddenError` | Access denied (wrong environment/project) |
|
|
481
|
+
| `DuplicateMessageError` | Idempotency key already used |
|
|
482
|
+
| `ConsumerDiscoveryError` | Could not reach consumer deployment |
|
|
483
|
+
| `ConsumerRegistryNotConfiguredError` | Project not configured for queues |
|
|
484
|
+
| `InternalServerError` | Unexpected server error |
|
|
485
|
+
| `InvalidLimitError` | Batch limit outside valid range (1-10) |
|
|
486
|
+
| `MessageNotFoundError` | Message doesn't exist or expired |
|
|
487
|
+
| `MessageNotAvailableError` | Message exists but cannot be claimed |
|
|
488
|
+
| `MessageAlreadyProcessedError` | Message already successfully processed |
|
|
489
|
+
| `MessageLockedError` | Message being processed by another consumer |
|
|
490
|
+
| `MessageCorruptedError` | Message data could not be parsed |
|
|
491
|
+
| `QueueEmptyError` | No messages available in queue |
|
|
492
|
+
|
|
493
|
+
## Environment Variables
|
|
494
|
+
|
|
495
|
+
| Variable | Description | Default |
|
|
496
|
+
| ---------------------- | ----------------------------------------------- | ------- |
|
|
497
|
+
| `QUEUE_REGION` | Region code for the queue client (user-defined) | - |
|
|
498
|
+
| `VERCEL_REGION` | Current region (auto-set by Vercel) | - |
|
|
499
|
+
| `VERCEL_QUEUE_DEBUG` | Enable debug logging (`1` or `true`) | - |
|
|
500
|
+
| `VERCEL_DEPLOYMENT_ID` | Deployment ID (auto-set by Vercel) | - |
|
|
501
|
+
|
|
502
|
+
## Service Limits & Constraints
|
|
503
|
+
|
|
504
|
+
### Throughput & Storage
|
|
505
|
+
|
|
506
|
+
| Limit | Value | Notes |
|
|
507
|
+
| --------------------------- | --------------------- | ----------------------------------- |
|
|
508
|
+
| Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
|
|
509
|
+
| Payload size | 1 GB | Smaller messages have lower latency |
|
|
510
|
+
| Number of topics | Unlimited | No hard limit |
|
|
511
|
+
| Consumer groups per message | ~4,000 | Per-message limit |
|
|
512
|
+
| Messages per queue | Unlimited | No hard limit |
|
|
513
|
+
|
|
514
|
+
### Parameter Constraints
|
|
515
|
+
|
|
516
|
+
#### Publishing Messages
|
|
517
|
+
|
|
518
|
+
| Parameter | Default | Min | Max | Notes |
|
|
519
|
+
| ------------------ | ------------ | --- | ----------- | ----------------------------------- |
|
|
520
|
+
| `retentionSeconds` | 86,400 (24h) | 60 | 86,400 | Message TTL |
|
|
521
|
+
| `delaySeconds` | 0 | 0 | ≤ retention | Cannot exceed retention |
|
|
522
|
+
| `idempotencyKey` | — | — | — | Dedup window: `min(retention, 24h)` |
|
|
523
|
+
|
|
524
|
+
#### Receiving Messages
|
|
525
|
+
|
|
526
|
+
| Parameter | Default | Min | Max | Notes |
|
|
527
|
+
| -------------------------- | ------- | --- | ----- | ------------------------------- |
|
|
528
|
+
| `visibilityTimeoutSeconds` | 300 | 30 | 3,600 | Lock duration during processing |
|
|
529
|
+
| `limit` | 1 | 1 | 10 | Messages per request |
|
|
530
|
+
|
|
531
|
+
### Identifier Formats
|
|
532
|
+
|
|
533
|
+
| Identifier | Pattern | Example |
|
|
534
|
+
| -------------- | ---------------- | ----------------------------------- |
|
|
535
|
+
| Topic name | `[A-Za-z0-9_-]+` | `my-queue`, `task_queue_v2` |
|
|
536
|
+
| Consumer group | `[A-Za-z0-9_-]+` | `worker-1`, `analytics_consumer` |
|
|
537
|
+
| Message ID | Opaque string | `0-1`, `3-7K9mNpQrS` |
|
|
538
|
+
| Receipt handle | Opaque string | Used for acknowledge/visibility ops |
|
|
539
|
+
|
|
540
|
+
### Wildcard Topics
|
|
551
541
|
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
"workers",
|
|
558
|
-
async (message) => {
|
|
559
|
-
console.log(`Processing task: ${message.taskType}`);
|
|
560
|
-
await processTask(message.taskType, message.data);
|
|
561
|
-
},
|
|
562
|
-
);
|
|
563
|
-
console.log("Message processed successfully");
|
|
564
|
-
} catch (error) {
|
|
565
|
-
if (error instanceof QueueEmptyError) {
|
|
566
|
-
console.log("No messages available");
|
|
567
|
-
} else if (error instanceof MessageLockedError) {
|
|
568
|
-
console.log("Next message is locked");
|
|
569
|
-
if (error.retryAfter) {
|
|
570
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"functions": {
|
|
545
|
+
"app/api/queue/route.ts": {
|
|
546
|
+
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "user-*" }]
|
|
571
547
|
}
|
|
572
|
-
} else {
|
|
573
|
-
console.error("Error processing message:", error);
|
|
574
548
|
}
|
|
575
549
|
}
|
|
550
|
+
```
|
|
576
551
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
async (message) => {
|
|
582
|
-
if (!canProcessTaskType(message.taskType)) {
|
|
583
|
-
// Return timeout to retry later
|
|
584
|
-
return { timeoutSeconds: 60 };
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
await processTask(message.taskType, message.data);
|
|
588
|
-
},
|
|
589
|
-
);
|
|
552
|
+
- `*` may only appear **once** in the pattern
|
|
553
|
+
- `*` must be at the **end** of the topic name
|
|
554
|
+
- Valid: `user-*`, `orders-*`
|
|
555
|
+
- Invalid: `*-events`, `user-*-data`
|
|
590
556
|
|
|
591
|
-
|
|
592
|
-
await receive<{ taskType: string; data: any }>(
|
|
593
|
-
"work-queue",
|
|
594
|
-
"workers",
|
|
595
|
-
async (_, metadata) => {
|
|
596
|
-
console.log(`Message ID: ${metadata.messageId}`);
|
|
597
|
-
console.log(`Delivery count: ${metadata.deliveryCount}`);
|
|
598
|
-
console.log(`Timestamp: ${metadata.timestamp}`);
|
|
599
|
-
// _ is undefined - no payload was downloaded
|
|
600
|
-
},
|
|
601
|
-
{ messageId: "specific-message-id", skipPayload: true },
|
|
602
|
-
);
|
|
603
|
-
```
|
|
557
|
+
## API Reference
|
|
604
558
|
|
|
605
|
-
###
|
|
559
|
+
### `QueueClient`
|
|
606
560
|
|
|
607
561
|
```typescript
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Check if we have required resources
|
|
621
|
-
if (taskType === "external-api" && !isExternalServiceAvailable()) {
|
|
622
|
-
// Return timeout to retry in 1 minute
|
|
623
|
-
return { timeoutSeconds: 60 };
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Process the message normally
|
|
627
|
-
console.log(`Processing ${taskType} task`);
|
|
628
|
-
await processTask(taskType, data);
|
|
629
|
-
// Message will be automatically deleted on successful completion
|
|
630
|
-
},
|
|
631
|
-
);
|
|
632
|
-
} catch (error) {
|
|
633
|
-
console.error("Worker processing error:", error);
|
|
634
|
-
}
|
|
562
|
+
import { QueueClient } from "@vercel/queue";
|
|
563
|
+
|
|
564
|
+
const queue = new QueueClient({
|
|
565
|
+
region: process.env.QUEUE_REGION!, // Required — see Quick Start for env setup
|
|
566
|
+
resolveBaseUrl: (r) => new URL(`https://${r}.vercel-queue.com`), // Default resolver
|
|
567
|
+
token: "my-token", // Auto-fetched via OIDC if omitted
|
|
568
|
+
headers: { "X-Custom": "value" },
|
|
569
|
+
transport: new JsonTransport(), // Default: JsonTransport
|
|
570
|
+
deploymentId: undefined, // omit = auto from env (pinned), null = unpinned, or explicit string
|
|
571
|
+
});
|
|
635
572
|
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
await receive<{ taskType: string; data: any }>(
|
|
639
|
-
"work-queue",
|
|
640
|
-
"workers",
|
|
641
|
-
async (message, { deliveryCount }) => {
|
|
642
|
-
const maxRetries = 3;
|
|
643
|
-
|
|
644
|
-
try {
|
|
645
|
-
await processMessage(message);
|
|
646
|
-
// Successful processing - message will be deleted
|
|
647
|
-
} catch (error) {
|
|
648
|
-
if (deliveryCount < maxRetries) {
|
|
649
|
-
// Exponential backoff: 2^deliveryCount minutes
|
|
650
|
-
const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
|
|
651
|
-
console.log(
|
|
652
|
-
`Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
|
|
653
|
-
);
|
|
654
|
-
return { timeoutSeconds: timeoutSeconds };
|
|
655
|
-
} else {
|
|
656
|
-
// Max retries reached, let the message fail and be deleted
|
|
657
|
-
console.error(
|
|
658
|
-
"Max retries reached, message will be discarded:",
|
|
659
|
-
error,
|
|
660
|
-
);
|
|
661
|
-
throw error;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
},
|
|
665
|
-
);
|
|
666
|
-
} catch (error) {
|
|
667
|
-
console.error("Backoff processing error:", error);
|
|
668
|
-
}
|
|
573
|
+
// Methods (arrow functions — safe to destructure)
|
|
574
|
+
const { send, receive, handleCallback, handleNodeCallback } = queue;
|
|
669
575
|
```
|
|
670
576
|
|
|
671
|
-
|
|
577
|
+
### `send(topicName, payload, options?)`
|
|
672
578
|
|
|
673
|
-
|
|
579
|
+
Returns `{ messageId: string | null }`. `messageId` is `null` when the server accepted the message for deferred processing (e.g. during a server-side outage).
|
|
674
580
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
status)
|
|
684
|
-
|
|
685
|
-
- Contains optional `retryAfter` property with seconds to wait before retry
|
|
686
|
-
- For `receive()` without options: the next message is locked
|
|
687
|
-
- For `receive()` with messageId: the requested message is locked
|
|
688
|
-
|
|
689
|
-
- **`MessageNotFoundError`**: Message doesn't exist (404 status)
|
|
581
|
+
```typescript
|
|
582
|
+
const { messageId } = await send("my-topic", payload, {
|
|
583
|
+
idempotencyKey: "unique-key", // Dedup window: min(retention, 24h)
|
|
584
|
+
retentionSeconds: 3600, // Message TTL (default: 86400)
|
|
585
|
+
delaySeconds: 60, // Delay before visible (default: 0)
|
|
586
|
+
headers: { "X-Custom": "val" }, // Custom headers
|
|
587
|
+
});
|
|
588
|
+
```
|
|
690
589
|
|
|
691
|
-
|
|
692
|
-
processing (409 status)
|
|
590
|
+
### `receive(topicName, consumerGroup, handler, options?)`
|
|
693
591
|
|
|
694
|
-
|
|
592
|
+
Returns a discriminated result: `{ ok: true }` on success, or `{ ok: false, reason }` when no message was processed. The handler is never called when the queue is empty.
|
|
695
593
|
|
|
696
|
-
-
|
|
594
|
+
For receive-by-id, operational errors are returned instead of thrown:
|
|
697
595
|
|
|
698
|
-
|
|
596
|
+
```typescript
|
|
597
|
+
const result = await receive("my-topic", "my-group", handler, {
|
|
598
|
+
messageId: "msg-123",
|
|
599
|
+
});
|
|
600
|
+
if (!result.ok) {
|
|
601
|
+
// result.reason is "not_found" | "not_available" | "already_processed"
|
|
602
|
+
console.log(result.reason, result.messageId);
|
|
603
|
+
}
|
|
604
|
+
```
|
|
699
605
|
|
|
700
|
-
|
|
606
|
+
```typescript
|
|
607
|
+
// Batch mode
|
|
608
|
+
const result = await receive("my-topic", "my-group", handler, {
|
|
609
|
+
limit: 10, // Max messages (default: 1, max: 10)
|
|
610
|
+
visibilityTimeoutSeconds: 60, // Lock duration (default: 300)
|
|
611
|
+
});
|
|
612
|
+
```
|
|
701
613
|
|
|
702
|
-
|
|
614
|
+
### `handleCallback(handler, options?)`
|
|
703
615
|
|
|
704
|
-
|
|
616
|
+
Vercel only. Returns `(request: Request) => Promise<Response>` — for frameworks that export Web API route handlers.
|
|
705
617
|
|
|
706
|
-
|
|
618
|
+
```typescript
|
|
619
|
+
export const POST = handleCallback(
|
|
620
|
+
async (message, metadata) => {
|
|
621
|
+
await processMessage(message);
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
visibilityTimeoutSeconds: 300, // Lock duration (default: 300)
|
|
625
|
+
retry: (error, metadata) => {
|
|
626
|
+
// Optional: return { afterSeconds: N } to reschedule, { acknowledge: true } to ack, or undefined to propagate
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
);
|
|
630
|
+
```
|
|
707
631
|
|
|
708
|
-
|
|
709
|
-
- Unexpected server errors, service unavailable, etc.
|
|
632
|
+
### `handleNodeCallback(handler, options?)`
|
|
710
633
|
|
|
711
|
-
|
|
634
|
+
Vercel only. Returns `(req, res) => Promise<void>` — for frameworks that export Connect-style handlers.
|
|
712
635
|
|
|
713
636
|
```typescript
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
try {
|
|
725
|
-
await receive("my-topic", "my-consumer", async (message) => {
|
|
726
|
-
// Process message
|
|
727
|
-
console.log("Processing message:", message);
|
|
728
|
-
});
|
|
729
|
-
} catch (error) {
|
|
730
|
-
if (error instanceof QueueEmptyError) {
|
|
731
|
-
console.log("Queue is empty, retry later");
|
|
732
|
-
} else if (error instanceof MessageLockedError) {
|
|
733
|
-
console.log("Next message is locked");
|
|
734
|
-
if (error.retryAfter) {
|
|
735
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
637
|
+
// pages/api/queue/my-topic.ts
|
|
638
|
+
export default handleNodeCallback(
|
|
639
|
+
async (message, metadata) => {
|
|
640
|
+
await processMessage(message);
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
retry: (error, metadata) => ({ afterSeconds: 60 }),
|
|
644
|
+
},
|
|
645
|
+
);
|
|
646
|
+
```
|
|
739
647
|
|
|
740
|
-
|
|
741
|
-
try {
|
|
742
|
-
await receive("my-topic", "my-consumer", handler, { messageId });
|
|
743
|
-
} catch (error) {
|
|
744
|
-
if (error instanceof MessageLockedError) {
|
|
745
|
-
console.log("Message is locked by another consumer");
|
|
746
|
-
if (error.retryAfter) {
|
|
747
|
-
console.log(`Retry after ${error.retryAfter} seconds`);
|
|
748
|
-
setTimeout(() => retry(), error.retryAfter * 1000);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
648
|
+
### Handler Signature
|
|
752
649
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
console.log("Invalid token - refresh authentication");
|
|
759
|
-
} else if (error instanceof ForbiddenError) {
|
|
760
|
-
console.log("Environment mismatch - check token/queue configuration");
|
|
761
|
-
} else if (error instanceof BadRequestError) {
|
|
762
|
-
console.log("Invalid parameters:", error.message);
|
|
763
|
-
} else if (error instanceof InternalServerError) {
|
|
764
|
-
console.log("Server error - retry with backoff");
|
|
765
|
-
}
|
|
766
|
-
}
|
|
650
|
+
```typescript
|
|
651
|
+
type MessageHandler<T> = (
|
|
652
|
+
message: T,
|
|
653
|
+
metadata: MessageMetadata,
|
|
654
|
+
) => Promise<void> | void;
|
|
767
655
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
) {
|
|
777
|
-
// Authentication/authorization errors - need to fix configuration
|
|
778
|
-
console.log("Auth error - check credentials");
|
|
779
|
-
} else if (error instanceof BadRequestError) {
|
|
780
|
-
// Client error - fix the request
|
|
781
|
-
console.log("Invalid request:", error.message);
|
|
782
|
-
} else if (error instanceof InternalServerError) {
|
|
783
|
-
// Server error - implement exponential backoff
|
|
784
|
-
console.log("Server error - retry with backoff");
|
|
785
|
-
} else {
|
|
786
|
-
// Unknown error
|
|
787
|
-
console.error("Unexpected error:", error);
|
|
788
|
-
}
|
|
656
|
+
interface MessageMetadata {
|
|
657
|
+
messageId: string;
|
|
658
|
+
deliveryCount: number;
|
|
659
|
+
createdAt: Date;
|
|
660
|
+
expiresAt?: Date;
|
|
661
|
+
topicName: string;
|
|
662
|
+
consumerGroup: string;
|
|
663
|
+
region: string;
|
|
789
664
|
}
|
|
790
665
|
```
|
|
791
666
|
|