@vercel/queue 0.0.0-alpha.9 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +468 -604
- package/dist/index.d.mts +476 -188
- package/dist/index.d.ts +476 -188
- package/dist/index.js +1418 -765
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1394 -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,425 @@ With corresponding `vercel.json`:
|
|
|
270
231
|
}
|
|
271
232
|
```
|
|
272
233
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- **Automatic Triggering**: Vercel automatically triggers your API routes when messages are available for the configured topic/consumer combinations
|
|
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
|
|
234
|
+
Multiple route files for the same topic create separate consumer groups — each receives a copy of every message.
|
|
279
235
|
|
|
280
|
-
|
|
236
|
+
### 3. Retry and Backoff
|
|
281
237
|
|
|
282
|
-
|
|
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).
|
|
283
239
|
|
|
284
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
- **Topics**: Named message channels with configurable serialization
|
|
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
|
|
312
|
-
|
|
313
|
-
## Performance
|
|
314
|
-
|
|
315
|
-
The multipart parser is optimized for high-throughput scenarios:
|
|
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.
|
|
316
257
|
|
|
317
|
-
|
|
318
|
-
- **Memory Efficient**: No buffering of complete payloads
|
|
319
|
-
- **Fast Parsing**: Native Buffer operations for ~50% performance improvement
|
|
320
|
-
- **Scalable**: Can handle arbitrarily large responses without memory
|
|
321
|
-
constraints
|
|
258
|
+
**Exponential backoff** uses `metadata.deliveryCount` (starts at 1, increments each delivery):
|
|
322
259
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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`:
|
|
376
336
|
|
|
377
337
|
```typescript
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// Send with options including custom transport
|
|
382
|
-
await send<T>(topicName, payload, {
|
|
383
|
-
transport?: Transport<T>;
|
|
384
|
-
idempotencyKey?: string;
|
|
385
|
-
retentionSeconds?: number;
|
|
338
|
+
const queue = new QueueClient({
|
|
339
|
+
region: process.env.QUEUE_REGION!,
|
|
340
|
+
resolveBaseUrl: (region) => `https://${region}.my-proxy.example`,
|
|
386
341
|
});
|
|
342
|
+
```
|
|
387
343
|
|
|
388
|
-
|
|
389
|
-
await send("notifications", { userId: "123", message: "Welcome!" });
|
|
390
|
-
|
|
391
|
-
await send("images", imageBuffer, {
|
|
392
|
-
transport: new BufferTransport(),
|
|
393
|
-
});
|
|
344
|
+
## Transports
|
|
394
345
|
|
|
395
|
-
|
|
396
|
-
idempotencyKey: "unique-key-123",
|
|
397
|
-
retentionSeconds: 3600,
|
|
398
|
-
});
|
|
399
|
-
```
|
|
346
|
+
The transport controls how message payloads are serialized and deserialized.
|
|
400
347
|
|
|
401
|
-
|
|
348
|
+
| Use Case | Transport | Memory Usage | Notes |
|
|
349
|
+
| --------------- | ----------------- | ------------ | ----------------------- |
|
|
350
|
+
| Structured data | `JsonTransport` | Low | Default, JSON encoding |
|
|
351
|
+
| Binary data | `BufferTransport` | Medium | Raw bytes |
|
|
352
|
+
| Large payloads | `StreamTransport` | Very Low | No buffering, streaming |
|
|
402
353
|
|
|
403
354
|
```typescript
|
|
404
|
-
|
|
405
|
-
|
|
355
|
+
import {
|
|
356
|
+
QueueClient,
|
|
357
|
+
JsonTransport,
|
|
358
|
+
BufferTransport,
|
|
359
|
+
StreamTransport,
|
|
360
|
+
} from "@vercel/queue";
|
|
406
361
|
|
|
407
|
-
//
|
|
408
|
-
|
|
409
|
-
|
|
362
|
+
// JSON with custom serialization
|
|
363
|
+
const queue = new QueueClient({
|
|
364
|
+
region: process.env.QUEUE_REGION!,
|
|
365
|
+
transport: new JsonTransport({
|
|
366
|
+
replacer: (key, value) => (key === "password" ? undefined : value),
|
|
367
|
+
reviver: (key, value) => (key === "date" ? new Date(value) : value),
|
|
368
|
+
}),
|
|
410
369
|
});
|
|
411
370
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
skipPayload: true,
|
|
371
|
+
// Binary data
|
|
372
|
+
const binQueue = new QueueClient({
|
|
373
|
+
region: process.env.QUEUE_REGION!,
|
|
374
|
+
transport: new BufferTransport(),
|
|
417
375
|
});
|
|
418
|
-
|
|
376
|
+
await binQueue.send("binary-topic", myBuffer);
|
|
419
377
|
|
|
420
|
-
|
|
378
|
+
// Streaming for large payloads
|
|
379
|
+
const streamQueue = new QueueClient({
|
|
380
|
+
region: process.env.QUEUE_REGION!,
|
|
381
|
+
transport: new StreamTransport(),
|
|
382
|
+
});
|
|
383
|
+
await streamQueue.send("large-file", myReadableStream);
|
|
384
|
+
```
|
|
421
385
|
|
|
422
|
-
|
|
423
|
-
// Handler function signature
|
|
424
|
-
type MessageHandler<T = unknown> = (
|
|
425
|
-
message: T,
|
|
426
|
-
metadata: MessageMetadata,
|
|
427
|
-
) => Promise<MessageHandlerResult> | MessageHandlerResult;
|
|
386
|
+
## Manual Receive
|
|
428
387
|
|
|
429
|
-
|
|
430
|
-
type MessageHandlerResult = void | MessageTimeoutResult;
|
|
388
|
+
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.
|
|
431
389
|
|
|
432
|
-
|
|
433
|
-
timeoutSeconds: number; // seconds before message becomes available again
|
|
434
|
-
}
|
|
390
|
+
### Region considerations
|
|
435
391
|
|
|
436
|
-
|
|
437
|
-
interface MessageMetadata {
|
|
438
|
-
messageId: string;
|
|
439
|
-
deliveryCount: number;
|
|
440
|
-
timestamp: string;
|
|
441
|
-
}
|
|
442
|
-
```
|
|
392
|
+
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.
|
|
443
393
|
|
|
444
|
-
|
|
394
|
+
```bash
|
|
395
|
+
# .env.production — fixed region for manual receive workflows
|
|
396
|
+
QUEUE_REGION=iad1
|
|
445
397
|
|
|
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
|
-
}
|
|
398
|
+
# .env.development
|
|
399
|
+
QUEUE_REGION=iad1
|
|
455
400
|
```
|
|
456
401
|
|
|
457
|
-
|
|
402
|
+
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
403
|
|
|
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
|
-
```
|
|
404
|
+
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
405
|
|
|
467
|
-
###
|
|
406
|
+
### Usage
|
|
468
407
|
|
|
469
408
|
```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
|
-
},
|
|
409
|
+
import { QueueClient } from "@vercel/queue";
|
|
487
410
|
|
|
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
|
|
411
|
+
const { receive } = new QueueClient({ region: "iad1" });
|
|
497
412
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
413
|
+
// Process next available message
|
|
414
|
+
const result = await receive(
|
|
415
|
+
"my-topic",
|
|
416
|
+
"my-group",
|
|
417
|
+
async (message, metadata) => {
|
|
418
|
+
console.log("Processing:", message);
|
|
419
|
+
},
|
|
420
|
+
);
|
|
421
|
+
if (!result.ok) {
|
|
422
|
+
console.log("Queue was empty:", result.reason);
|
|
505
423
|
}
|
|
506
424
|
|
|
507
|
-
//
|
|
508
|
-
await
|
|
509
|
-
userId: "123",
|
|
510
|
-
action: "login",
|
|
511
|
-
timestamp: Date.now(),
|
|
512
|
-
});
|
|
425
|
+
// Batch processing: up to 10 messages in one request
|
|
426
|
+
await receive("my-topic", "my-group", handler, { limit: 10 });
|
|
513
427
|
|
|
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
|
-
}
|
|
428
|
+
// Process a specific message by ID
|
|
429
|
+
await receive("my-topic", "my-group", handler, { messageId: "msg-123" });
|
|
522
430
|
```
|
|
523
431
|
|
|
524
|
-
|
|
432
|
+
> **Note:** `limit` and `messageId` are mutually exclusive options. The handler is never called when the queue is empty — check `result.ok` instead.
|
|
433
|
+
|
|
434
|
+
## Error Handling
|
|
525
435
|
|
|
526
436
|
```typescript
|
|
527
|
-
|
|
528
|
-
|
|
437
|
+
import {
|
|
438
|
+
BadRequestError,
|
|
439
|
+
DuplicateMessageError,
|
|
440
|
+
ForbiddenError,
|
|
441
|
+
InternalServerError,
|
|
442
|
+
UnauthorizedError,
|
|
443
|
+
} from "@vercel/queue";
|
|
444
|
+
import { send } from "@/lib/queue";
|
|
529
445
|
|
|
530
446
|
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");
|
|
447
|
+
await send("my-topic", payload);
|
|
541
448
|
} catch (error) {
|
|
542
|
-
if (error
|
|
543
|
-
console.log("
|
|
544
|
-
} else {
|
|
545
|
-
console.
|
|
449
|
+
if (error instanceof UnauthorizedError) {
|
|
450
|
+
console.log("Invalid token - refresh authentication");
|
|
451
|
+
} else if (error instanceof ForbiddenError) {
|
|
452
|
+
console.log("Environment mismatch - check configuration");
|
|
453
|
+
} else if (error instanceof BadRequestError) {
|
|
454
|
+
console.log("Invalid parameters:", error.message);
|
|
455
|
+
} else if (error instanceof DuplicateMessageError) {
|
|
456
|
+
console.log("Duplicate message:", error.idempotencyKey);
|
|
457
|
+
} else if (error instanceof InternalServerError) {
|
|
458
|
+
console.log("Server error - retry with backoff");
|
|
546
459
|
}
|
|
547
460
|
}
|
|
548
461
|
```
|
|
549
462
|
|
|
550
|
-
|
|
463
|
+
All error types:
|
|
464
|
+
|
|
465
|
+
| Error | Description |
|
|
466
|
+
| ------------------------------------ | --------------------------------------------- |
|
|
467
|
+
| `BadRequestError` | Invalid request parameters |
|
|
468
|
+
| `UnauthorizedError` | Authentication failed (invalid/missing token) |
|
|
469
|
+
| `ForbiddenError` | Access denied (wrong environment/project) |
|
|
470
|
+
| `DuplicateMessageError` | Idempotency key already used |
|
|
471
|
+
| `ConsumerDiscoveryError` | Could not reach consumer deployment |
|
|
472
|
+
| `ConsumerRegistryNotConfiguredError` | Project not configured for queues |
|
|
473
|
+
| `InternalServerError` | Unexpected server error |
|
|
474
|
+
| `InvalidLimitError` | Batch limit outside valid range (1-10) |
|
|
475
|
+
| `MessageNotFoundError` | Message doesn't exist or expired |
|
|
476
|
+
| `MessageNotAvailableError` | Message exists but cannot be claimed |
|
|
477
|
+
| `MessageAlreadyProcessedError` | Message already successfully processed |
|
|
478
|
+
| `MessageLockedError` | Message being processed by another consumer |
|
|
479
|
+
| `MessageCorruptedError` | Message data could not be parsed |
|
|
480
|
+
| `QueueEmptyError` | No messages available in queue |
|
|
481
|
+
|
|
482
|
+
## Environment Variables
|
|
483
|
+
|
|
484
|
+
| Variable | Description | Default |
|
|
485
|
+
| ---------------------- | ----------------------------------------------- | ------- |
|
|
486
|
+
| `QUEUE_REGION` | Region code for the queue client (user-defined) | - |
|
|
487
|
+
| `VERCEL_REGION` | Current region (auto-set by Vercel) | - |
|
|
488
|
+
| `VERCEL_QUEUE_DEBUG` | Enable debug logging (`1` or `true`) | - |
|
|
489
|
+
| `VERCEL_DEPLOYMENT_ID` | Deployment ID (auto-set by Vercel) | - |
|
|
490
|
+
|
|
491
|
+
## Service Limits & Constraints
|
|
492
|
+
|
|
493
|
+
### Throughput & Storage
|
|
494
|
+
|
|
495
|
+
| Limit | Value | Notes |
|
|
496
|
+
| --------------------------- | --------------------- | ----------------------------------- |
|
|
497
|
+
| Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
|
|
498
|
+
| Payload size | 1 GB | Smaller messages have lower latency |
|
|
499
|
+
| Number of topics | Unlimited | No hard limit |
|
|
500
|
+
| Consumer groups per message | ~4,000 | Per-message limit |
|
|
501
|
+
| Messages per queue | Unlimited | No hard limit |
|
|
502
|
+
|
|
503
|
+
### Parameter Constraints
|
|
504
|
+
|
|
505
|
+
#### Publishing Messages
|
|
506
|
+
|
|
507
|
+
| Parameter | Default | Min | Max | Notes |
|
|
508
|
+
| ------------------ | ------------ | --- | ----------- | ----------------------------------- |
|
|
509
|
+
| `retentionSeconds` | 86,400 (24h) | 60 | 86,400 | Message TTL |
|
|
510
|
+
| `delaySeconds` | 0 | 0 | ≤ retention | Cannot exceed retention |
|
|
511
|
+
| `idempotencyKey` | — | — | — | Dedup window: `min(retention, 24h)` |
|
|
512
|
+
|
|
513
|
+
#### Receiving Messages
|
|
514
|
+
|
|
515
|
+
| Parameter | Default | Min | Max | Notes |
|
|
516
|
+
| -------------------------- | ------- | --- | ----- | ------------------------------- |
|
|
517
|
+
| `visibilityTimeoutSeconds` | 300 | 30 | 3,600 | Lock duration during processing |
|
|
518
|
+
| `limit` | 1 | 1 | 10 | Messages per request |
|
|
519
|
+
|
|
520
|
+
### Identifier Formats
|
|
521
|
+
|
|
522
|
+
| Identifier | Pattern | Example |
|
|
523
|
+
| -------------- | ---------------- | ----------------------------------- |
|
|
524
|
+
| Topic name | `[A-Za-z0-9_-]+` | `my-queue`, `task_queue_v2` |
|
|
525
|
+
| Consumer group | `[A-Za-z0-9_-]+` | `worker-1`, `analytics_consumer` |
|
|
526
|
+
| Message ID | Opaque string | `0-1`, `3-7K9mNpQrS` |
|
|
527
|
+
| Receipt handle | Opaque string | Used for acknowledge/visibility ops |
|
|
528
|
+
|
|
529
|
+
### Wildcard Topics
|
|
551
530
|
|
|
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`);
|
|
531
|
+
```json
|
|
532
|
+
{
|
|
533
|
+
"functions": {
|
|
534
|
+
"app/api/queue/route.ts": {
|
|
535
|
+
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "user-*" }]
|
|
571
536
|
}
|
|
572
|
-
} else {
|
|
573
|
-
console.error("Error processing message:", error);
|
|
574
537
|
}
|
|
575
538
|
}
|
|
539
|
+
```
|
|
576
540
|
|
|
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
|
-
);
|
|
541
|
+
- `*` may only appear **once** in the pattern
|
|
542
|
+
- `*` must be at the **end** of the topic name
|
|
543
|
+
- Valid: `user-*`, `orders-*`
|
|
544
|
+
- Invalid: `*-events`, `user-*-data`
|
|
590
545
|
|
|
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
|
-
```
|
|
546
|
+
## API Reference
|
|
604
547
|
|
|
605
|
-
###
|
|
548
|
+
### `QueueClient`
|
|
606
549
|
|
|
607
550
|
```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
|
-
}
|
|
551
|
+
import { QueueClient } from "@vercel/queue";
|
|
552
|
+
|
|
553
|
+
const queue = new QueueClient({
|
|
554
|
+
region: process.env.QUEUE_REGION!, // Required — see Quick Start for env setup
|
|
555
|
+
resolveBaseUrl: (r) => `https://${r}.vercel-queue.com`, // Default resolver
|
|
556
|
+
token: "my-token", // Auto-fetched via OIDC if omitted
|
|
557
|
+
headers: { "X-Custom": "value" },
|
|
558
|
+
transport: new JsonTransport(), // Default: JsonTransport
|
|
559
|
+
deploymentId: undefined, // omit = auto from env (pinned), null = unpinned, or explicit string
|
|
560
|
+
});
|
|
635
561
|
|
|
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
|
-
}
|
|
562
|
+
// Methods (arrow functions — safe to destructure)
|
|
563
|
+
const { send, receive, handleCallback, handleNodeCallback } = queue;
|
|
669
564
|
```
|
|
670
565
|
|
|
671
|
-
|
|
566
|
+
### `send(topicName, payload, options?)`
|
|
672
567
|
|
|
673
|
-
|
|
568
|
+
Returns `{ messageId: string | null }`. `messageId` is `null` when the server accepted the message for deferred processing (e.g. during a server-side outage).
|
|
674
569
|
|
|
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)
|
|
570
|
+
```typescript
|
|
571
|
+
const { messageId } = await send("my-topic", payload, {
|
|
572
|
+
idempotencyKey: "unique-key", // Dedup window: min(retention, 24h)
|
|
573
|
+
retentionSeconds: 3600, // Message TTL (default: 86400)
|
|
574
|
+
delaySeconds: 60, // Delay before visible (default: 0)
|
|
575
|
+
headers: { "X-Custom": "val" }, // Custom headers
|
|
576
|
+
});
|
|
577
|
+
```
|
|
690
578
|
|
|
691
|
-
|
|
692
|
-
processing (409 status)
|
|
579
|
+
### `receive(topicName, consumerGroup, handler, options?)`
|
|
693
580
|
|
|
694
|
-
|
|
581
|
+
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
582
|
|
|
696
|
-
-
|
|
583
|
+
For receive-by-id, operational errors are returned instead of thrown:
|
|
697
584
|
|
|
698
|
-
|
|
585
|
+
```typescript
|
|
586
|
+
const result = await receive("my-topic", "my-group", handler, {
|
|
587
|
+
messageId: "msg-123",
|
|
588
|
+
});
|
|
589
|
+
if (!result.ok) {
|
|
590
|
+
// result.reason is "not_found" | "not_available" | "already_processed"
|
|
591
|
+
console.log(result.reason, result.messageId);
|
|
592
|
+
}
|
|
593
|
+
```
|
|
699
594
|
|
|
700
|
-
|
|
595
|
+
```typescript
|
|
596
|
+
// Batch mode
|
|
597
|
+
const result = await receive("my-topic", "my-group", handler, {
|
|
598
|
+
limit: 10, // Max messages (default: 1, max: 10)
|
|
599
|
+
visibilityTimeoutSeconds: 60, // Lock duration (default: 300)
|
|
600
|
+
});
|
|
601
|
+
```
|
|
701
602
|
|
|
702
|
-
|
|
603
|
+
### `handleCallback(handler, options?)`
|
|
703
604
|
|
|
704
|
-
|
|
605
|
+
Vercel only. Returns `(request: Request) => Promise<Response>` — for frameworks that export Web API route handlers.
|
|
705
606
|
|
|
706
|
-
|
|
607
|
+
```typescript
|
|
608
|
+
export const POST = handleCallback(
|
|
609
|
+
async (message, metadata) => {
|
|
610
|
+
await processMessage(message);
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
visibilityTimeoutSeconds: 300, // Lock duration (default: 300)
|
|
614
|
+
retry: (error, metadata) => {
|
|
615
|
+
// Optional: return { afterSeconds: N } to reschedule, { acknowledge: true } to ack, or undefined to propagate
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
```
|
|
707
620
|
|
|
708
|
-
|
|
709
|
-
- Unexpected server errors, service unavailable, etc.
|
|
621
|
+
### `handleNodeCallback(handler, options?)`
|
|
710
622
|
|
|
711
|
-
|
|
623
|
+
Vercel only. Returns `(req, res) => Promise<void>` — for frameworks that export Connect-style handlers.
|
|
712
624
|
|
|
713
625
|
```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
|
-
}
|
|
626
|
+
// pages/api/queue/my-topic.ts
|
|
627
|
+
export default handleNodeCallback(
|
|
628
|
+
async (message, metadata) => {
|
|
629
|
+
await processMessage(message);
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
retry: (error, metadata) => ({ afterSeconds: 60 }),
|
|
633
|
+
},
|
|
634
|
+
);
|
|
635
|
+
```
|
|
739
636
|
|
|
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
|
-
}
|
|
637
|
+
### Handler Signature
|
|
752
638
|
|
|
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
|
-
}
|
|
639
|
+
```typescript
|
|
640
|
+
type MessageHandler<T> = (
|
|
641
|
+
message: T,
|
|
642
|
+
metadata: MessageMetadata,
|
|
643
|
+
) => Promise<void> | void;
|
|
767
644
|
|
|
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
|
-
}
|
|
645
|
+
interface MessageMetadata {
|
|
646
|
+
messageId: string;
|
|
647
|
+
deliveryCount: number;
|
|
648
|
+
createdAt: Date;
|
|
649
|
+
expiresAt?: Date;
|
|
650
|
+
topicName: string;
|
|
651
|
+
consumerGroup: string;
|
|
652
|
+
region: string;
|
|
789
653
|
}
|
|
790
654
|
```
|
|
791
655
|
|