@vercel/queue 0.0.0-alpha.39 → 0.0.0-alpha.40
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 +367 -375
- package/dist/index.d.mts +603 -111
- package/dist/index.d.ts +603 -111
- package/dist/index.js +370 -238
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +369 -234
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -21
- package/dist/callback-lq_sorrn.d.mts +0 -718
- package/dist/callback-lq_sorrn.d.ts +0 -718
- package/dist/nextjs-pages.d.mts +0 -68
- package/dist/nextjs-pages.d.ts +0 -68
- package/dist/nextjs-pages.js +0 -1438
- package/dist/nextjs-pages.js.map +0 -1
- package/dist/nextjs-pages.mjs +0 -1401
- package/dist/nextjs-pages.mjs.map +0 -1
- package/dist/web.d.mts +0 -60
- package/dist/web.d.ts +0 -60
- package/dist/web.js +0 -1457
- package/dist/web.js.map +0 -1
- package/dist/web.mjs +0 -1420
- package/dist/web.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -4,14 +4,12 @@ A TypeScript client library for interacting with the Vercel Queue Service API, d
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
|
|
14
|
-
- **Framework Adapters**: Web API, Next.js App Router, and Pages Router support
|
|
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
|
|
15
13
|
|
|
16
14
|
## Installation
|
|
17
15
|
|
|
@@ -21,58 +19,93 @@ npm install @vercel/queue
|
|
|
21
19
|
|
|
22
20
|
## Quick Start
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
Set up your region via environment variables. If your framework supports `.env` files (Next.js, Vite, Nuxt, etc.):
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
|
-
#
|
|
28
|
-
|
|
25
|
+
# .env.production (on Vercel, inherits the platform's region)
|
|
26
|
+
QUEUE_REGION=${VERCEL_REGION}
|
|
29
27
|
|
|
30
|
-
#
|
|
31
|
-
|
|
28
|
+
# .env.development (fixed region for local dev — iad1 is recommended)
|
|
29
|
+
QUEUE_REGION=iad1
|
|
30
|
+
```
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
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:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// lib/queue.ts
|
|
38
|
+
import { QueueClient } from "@vercel/queue";
|
|
39
|
+
|
|
40
|
+
const queue = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
41
|
+
export const { send, receive, handleCallback, handleNodeCallback } = queue;
|
|
35
42
|
```
|
|
36
43
|
|
|
37
|
-
|
|
44
|
+
Send a message anywhere in your app:
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
```typescript
|
|
47
|
+
import { send } from "@/lib/queue";
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
await send("my-topic", { message: "Hello world" });
|
|
50
|
+
```
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
Handle incoming messages with a route handler:
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
```typescript
|
|
55
|
+
// app/api/queue/my-topic/route.ts
|
|
56
|
+
import { handleCallback } from "@/lib/queue";
|
|
46
57
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
58
|
+
export const POST = handleCallback(async (message, metadata) => {
|
|
59
|
+
console.log("Processing:", message);
|
|
60
|
+
});
|
|
61
|
+
```
|
|
50
62
|
|
|
51
|
-
|
|
63
|
+
Configure your `vercel.json`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"functions": {
|
|
68
|
+
"app/api/queue/my-topic/route.ts": {
|
|
69
|
+
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
52
73
|
```
|
|
53
74
|
|
|
54
|
-
###
|
|
75
|
+
### Project Setup
|
|
55
76
|
|
|
56
|
-
|
|
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
|
|
57
92
|
|
|
58
93
|
```typescript
|
|
59
|
-
import {
|
|
94
|
+
import { QueueClient } from "@vercel/queue";
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
await send("my-topic", {
|
|
63
|
-
message: "Hello world",
|
|
64
|
-
});
|
|
96
|
+
const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
65
97
|
|
|
66
|
-
//
|
|
98
|
+
// Simple send
|
|
99
|
+
await send("my-topic", { message: "Hello world" });
|
|
100
|
+
|
|
101
|
+
// With options
|
|
67
102
|
await send(
|
|
68
103
|
"my-topic",
|
|
104
|
+
{ message: "Hello world" },
|
|
69
105
|
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
idempotencyKey: "unique-key", // Optional: prevent duplicate messages
|
|
74
|
-
retentionSeconds: 3600, // Optional: override retention time (defaults to 24 hours)
|
|
75
|
-
delaySeconds: 60, // Optional: delay message delivery by N seconds
|
|
106
|
+
idempotencyKey: "unique-key", // Prevent duplicate messages
|
|
107
|
+
retentionSeconds: 3600, // 1 hour TTL (default: 24h)
|
|
108
|
+
delaySeconds: 60, // Delay delivery by 1 minute
|
|
76
109
|
},
|
|
77
110
|
);
|
|
78
111
|
```
|
|
@@ -81,41 +114,37 @@ Example usage in an API route:
|
|
|
81
114
|
|
|
82
115
|
```typescript
|
|
83
116
|
// app/api/send-message/route.ts
|
|
84
|
-
import { send } from "
|
|
117
|
+
import { send } from "@/lib/queue";
|
|
85
118
|
|
|
86
119
|
export async function POST(request: Request) {
|
|
87
120
|
const body = await request.json();
|
|
88
|
-
|
|
89
|
-
const { messageId } = await send("my-topic", {
|
|
90
|
-
message: body.message,
|
|
91
|
-
});
|
|
92
|
-
|
|
121
|
+
const { messageId } = await send("my-topic", { message: body.message });
|
|
93
122
|
return Response.json({ messageId });
|
|
94
123
|
}
|
|
95
124
|
```
|
|
96
125
|
|
|
97
|
-
|
|
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.
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
## Consuming Messages
|
|
100
129
|
|
|
101
|
-
|
|
130
|
+
### On Vercel
|
|
102
131
|
|
|
103
|
-
|
|
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.
|
|
104
133
|
|
|
105
|
-
|
|
134
|
+
#### Web API — `handleCallback`
|
|
135
|
+
|
|
136
|
+
Returns `(Request) => Promise<Response>`. For frameworks that export Web API route handlers (Next.js App Router, Hono, etc.).
|
|
106
137
|
|
|
107
138
|
**Next.js App Router:**
|
|
108
139
|
|
|
109
140
|
```typescript
|
|
110
141
|
// app/api/queue/my-topic/route.ts
|
|
111
|
-
import { handleCallback } from "
|
|
142
|
+
import { handleCallback } from "@/lib/queue";
|
|
112
143
|
|
|
113
144
|
export const POST = handleCallback(async (message, metadata) => {
|
|
114
|
-
// metadata
|
|
115
|
-
console.log("Processing message:", message);
|
|
116
|
-
|
|
117
|
-
// If this throws an error, the message will be automatically retried
|
|
145
|
+
// metadata: { messageId, deliveryCount, createdAt, expiresAt?, topicName, consumerGroup, region }
|
|
118
146
|
await processMessage(message);
|
|
147
|
+
// Throwing an error will automatically retry the message
|
|
119
148
|
});
|
|
120
149
|
```
|
|
121
150
|
|
|
@@ -123,57 +152,53 @@ export const POST = handleCallback(async (message, metadata) => {
|
|
|
123
152
|
|
|
124
153
|
```typescript
|
|
125
154
|
import { Hono } from "hono";
|
|
126
|
-
import { handleCallback } from "
|
|
155
|
+
import { handleCallback } from "@/lib/queue";
|
|
127
156
|
|
|
128
157
|
const app = new Hono();
|
|
129
|
-
|
|
130
158
|
app.post(
|
|
131
159
|
"/api/queue",
|
|
132
160
|
handleCallback(async (message, metadata) => {
|
|
133
|
-
|
|
161
|
+
await processMessage(message);
|
|
134
162
|
}),
|
|
135
163
|
);
|
|
136
|
-
|
|
137
164
|
export default app;
|
|
138
165
|
```
|
|
139
166
|
|
|
140
|
-
|
|
167
|
+
#### Connect-style — `handleNodeCallback`
|
|
141
168
|
|
|
142
|
-
|
|
143
|
-
// app/api/queue/orders/fulfillment/route.ts
|
|
144
|
-
import { handleCallback } from "@vercel/queue/web";
|
|
169
|
+
Returns `(req, res) => Promise<void>`. For frameworks that export Connect-style handlers (Express, Next.js Pages Router, etc.).
|
|
145
170
|
|
|
146
|
-
|
|
147
|
-
await processOrder(order);
|
|
148
|
-
});
|
|
149
|
-
```
|
|
171
|
+
**Next.js Pages Router:**
|
|
150
172
|
|
|
151
173
|
```typescript
|
|
152
|
-
//
|
|
153
|
-
import {
|
|
174
|
+
// pages/api/queue/my-topic.ts
|
|
175
|
+
import { handleNodeCallback } from "@/lib/queue";
|
|
154
176
|
|
|
155
|
-
export
|
|
156
|
-
await
|
|
177
|
+
export default handleNodeCallback(async (message, metadata) => {
|
|
178
|
+
await processMessage(message);
|
|
157
179
|
});
|
|
158
180
|
```
|
|
159
181
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
For Next.js Pages Router, import from `@vercel/queue/nextjs/pages`. This returns a `(req, res) => Promise<void>` handler:
|
|
182
|
+
**Express:**
|
|
163
183
|
|
|
164
184
|
```typescript
|
|
165
|
-
|
|
166
|
-
import {
|
|
185
|
+
import express from "express";
|
|
186
|
+
import { handleNodeCallback } from "@/lib/queue";
|
|
167
187
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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;
|
|
172
197
|
```
|
|
173
198
|
|
|
174
|
-
|
|
199
|
+
### 2. Configure vercel.json
|
|
175
200
|
|
|
176
|
-
|
|
201
|
+
Tell Vercel which routes handle which topics:
|
|
177
202
|
|
|
178
203
|
```json
|
|
179
204
|
{
|
|
@@ -190,10 +215,7 @@ Configure which topics and consumers your API routes handle.
|
|
|
190
215
|
},
|
|
191
216
|
"app/api/queue/orders/fulfillment/route.ts": {
|
|
192
217
|
"experimentalTriggers": [
|
|
193
|
-
{
|
|
194
|
-
"type": "queue/v2beta",
|
|
195
|
-
"topic": "order-events"
|
|
196
|
-
}
|
|
218
|
+
{ "type": "queue/v2beta", "topic": "order-events" }
|
|
197
219
|
]
|
|
198
220
|
},
|
|
199
221
|
"app/api/queue/orders/analytics/route.ts": {
|
|
@@ -209,152 +231,207 @@ Configure which topics and consumers your API routes handle.
|
|
|
209
231
|
}
|
|
210
232
|
```
|
|
211
233
|
|
|
212
|
-
|
|
234
|
+
Multiple route files for the same topic create separate consumer groups — each receives a copy of every message.
|
|
213
235
|
|
|
214
|
-
|
|
215
|
-
- **Consumer Groups**: Named groups of consumers that process messages in parallel
|
|
216
|
-
- Different consumer groups for the same topic each get a copy of every message
|
|
217
|
-
- Multiple consumers in the same group share/split messages for load balancing
|
|
218
|
-
- **Automatic Triggering**: Vercel triggers your API routes when messages are available
|
|
219
|
-
- **Message Processing**: Your API routes receive message metadata via headers
|
|
220
|
-
- **Configuration**: The `vercel.json` file tells Vercel which routes handle which topics/consumers
|
|
221
|
-
- **Delivery Modes**: The server uses CloudEvents binary content mode to deliver messages. For small messages, the full payload and receipt handle are pushed directly in the HTTP body and headers, avoiding an extra API fetch. For large messages, only the message ID is sent and the SDK fetches the payload.
|
|
236
|
+
### 3. Retry and Backoff
|
|
222
237
|
|
|
223
|
-
|
|
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).
|
|
239
|
+
|
|
240
|
+
For finer control over retry timing, pass a `retry` option:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
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
|
+
);
|
|
254
|
+
```
|
|
224
255
|
|
|
225
|
-
|
|
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.
|
|
226
257
|
|
|
227
|
-
|
|
258
|
+
**Exponential backoff** uses `metadata.deliveryCount` (starts at 1, increments each delivery):
|
|
228
259
|
|
|
229
260
|
```typescript
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
```
|
|
232
274
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
275
|
+
**Conditional retry** — only retry transient errors:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
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
|
+
},
|
|
289
|
+
);
|
|
290
|
+
```
|
|
237
291
|
|
|
238
|
-
|
|
239
|
-
await send("my-topic", { hello: "world" }, { client });
|
|
292
|
+
**Acknowledging poison messages** — stop retrying messages that can never succeed:
|
|
240
293
|
|
|
241
|
-
|
|
242
|
-
export const POST = handleCallback(
|
|
243
|
-
|
|
244
|
-
|
|
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
|
+
);
|
|
245
307
|
```
|
|
246
308
|
|
|
247
|
-
|
|
309
|
+
The `retry` option is available on `handleCallback`, `handleNodeCallback`, and `receive`.
|
|
310
|
+
|
|
311
|
+
## Custom Client Configuration
|
|
248
312
|
|
|
249
|
-
|
|
313
|
+
All configuration lives on the `QueueClient`:
|
|
250
314
|
|
|
251
315
|
```typescript
|
|
252
|
-
import {
|
|
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
|
+
});
|
|
253
325
|
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}, parsed);
|
|
260
|
-
// success
|
|
261
|
-
} catch (error) {
|
|
262
|
-
// handle error → 500
|
|
263
|
-
}
|
|
326
|
+
// Use directly
|
|
327
|
+
await queue.send("my-topic", myBuffer);
|
|
328
|
+
|
|
329
|
+
// Or destructure
|
|
330
|
+
export const { send, receive, handleCallback, handleNodeCallback } = queue;
|
|
264
331
|
```
|
|
265
332
|
|
|
266
|
-
|
|
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.
|
|
267
334
|
|
|
268
|
-
|
|
335
|
+
To customize the URL scheme, provide a `resolveBaseUrl`:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
const queue = new QueueClient({
|
|
339
|
+
region: process.env.QUEUE_REGION!,
|
|
340
|
+
resolveBaseUrl: (region) => `https://${region}.my-proxy.example`,
|
|
341
|
+
});
|
|
342
|
+
```
|
|
269
343
|
|
|
270
|
-
|
|
344
|
+
## Transports
|
|
271
345
|
|
|
272
|
-
|
|
273
|
-
2. **BufferTransport**: For binary data that fits in memory
|
|
274
|
-
3. **StreamTransport**: For large files and memory-efficient processing
|
|
346
|
+
The transport controls how message payloads are serialized and deserialized.
|
|
275
347
|
|
|
276
|
-
|
|
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 |
|
|
277
353
|
|
|
278
354
|
```typescript
|
|
279
|
-
import {
|
|
355
|
+
import {
|
|
356
|
+
QueueClient,
|
|
357
|
+
JsonTransport,
|
|
358
|
+
BufferTransport,
|
|
359
|
+
StreamTransport,
|
|
360
|
+
} from "@vercel/queue";
|
|
280
361
|
|
|
281
|
-
//
|
|
282
|
-
|
|
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
|
+
}),
|
|
369
|
+
});
|
|
283
370
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
);
|
|
371
|
+
// Binary data
|
|
372
|
+
const binQueue = new QueueClient({
|
|
373
|
+
region: process.env.QUEUE_REGION!,
|
|
374
|
+
transport: new BufferTransport(),
|
|
375
|
+
});
|
|
376
|
+
await binQueue.send("binary-topic", myBuffer);
|
|
290
377
|
|
|
291
|
-
//
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
378
|
+
// Streaming for large payloads
|
|
379
|
+
const streamQueue = new QueueClient({
|
|
380
|
+
region: process.env.QUEUE_REGION!,
|
|
381
|
+
transport: new StreamTransport(),
|
|
295
382
|
});
|
|
296
|
-
await send("
|
|
383
|
+
await streamQueue.send("large-file", myReadableStream);
|
|
297
384
|
```
|
|
298
385
|
|
|
299
|
-
|
|
386
|
+
## Manual Receive
|
|
300
387
|
|
|
301
|
-
|
|
302
|
-
| ------------------ | --------------------- | ------------ | ----------- |
|
|
303
|
-
| Small JSON objects | JsonTransport | Low | High |
|
|
304
|
-
| Binary data | BufferTransport | Medium | High |
|
|
305
|
-
| Large payloads | StreamTransport | Very Low | Medium |
|
|
306
|
-
| Real-time streams | StreamTransport | Very Low | High |
|
|
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.
|
|
307
389
|
|
|
308
|
-
|
|
390
|
+
### Region considerations
|
|
309
391
|
|
|
310
|
-
|
|
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.
|
|
311
393
|
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.log("No message received - queue is empty");
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
394
|
+
```bash
|
|
395
|
+
# .env.production — fixed region for manual receive workflows
|
|
396
|
+
QUEUE_REGION=iad1
|
|
318
397
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
console.log("Message ID:", metadata.messageId);
|
|
322
|
-
});
|
|
398
|
+
# .env.development
|
|
399
|
+
QUEUE_REGION=iad1
|
|
323
400
|
```
|
|
324
401
|
|
|
325
|
-
|
|
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).
|
|
403
|
+
|
|
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.
|
|
405
|
+
|
|
406
|
+
### Usage
|
|
326
407
|
|
|
327
408
|
```typescript
|
|
328
|
-
import {
|
|
409
|
+
import { QueueClient } from "@vercel/queue";
|
|
329
410
|
|
|
330
|
-
|
|
331
|
-
if (!message) {
|
|
332
|
-
// No message available - handle gracefully
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
await processMessage(message);
|
|
336
|
-
});
|
|
337
|
-
```
|
|
411
|
+
const { receive } = new QueueClient({ region: "iad1" });
|
|
338
412
|
|
|
339
|
-
|
|
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);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Batch processing: up to 10 messages in one request
|
|
426
|
+
await receive("my-topic", "my-group", handler, { limit: 10 });
|
|
340
427
|
|
|
341
|
-
|
|
428
|
+
// Process a specific message by ID
|
|
429
|
+
await receive("my-topic", "my-group", handler, { messageId: "msg-123" });
|
|
430
|
+
```
|
|
342
431
|
|
|
343
|
-
|
|
344
|
-
- **`MessageNotFoundError`**: Message doesn't exist or has expired
|
|
345
|
-
- **`MessageNotAvailableError`**: Message exists but cannot be claimed
|
|
346
|
-
- **`MessageAlreadyProcessedError`**: Message was already successfully processed
|
|
347
|
-
- **`MessageCorruptedError`**: Message data could not be parsed
|
|
348
|
-
- **`BadRequestError`**: Invalid request parameters
|
|
349
|
-
- **`UnauthorizedError`**: Authentication failed (invalid or missing token)
|
|
350
|
-
- **`ForbiddenError`**: Access denied (wrong environment or project)
|
|
351
|
-
- **`DuplicateMessageError`**: Idempotency key was already used
|
|
352
|
-
- **`ConsumerDiscoveryError`**: Could not reach the consumer deployment
|
|
353
|
-
- **`ConsumerRegistryNotConfiguredError`**: Project not configured for queues
|
|
354
|
-
- **`InternalServerError`**: Unexpected server error
|
|
355
|
-
- **`InvalidLimitError`**: Batch limit outside valid range (1-10)
|
|
432
|
+
> **Note:** `limit` and `messageId` are mutually exclusive options. The handler is never called when the queue is empty — check `result.ok` instead.
|
|
356
433
|
|
|
357
|
-
|
|
434
|
+
## Error Handling
|
|
358
435
|
|
|
359
436
|
```typescript
|
|
360
437
|
import {
|
|
@@ -364,6 +441,7 @@ import {
|
|
|
364
441
|
InternalServerError,
|
|
365
442
|
UnauthorizedError,
|
|
366
443
|
} from "@vercel/queue";
|
|
444
|
+
import { send } from "@/lib/queue";
|
|
367
445
|
|
|
368
446
|
try {
|
|
369
447
|
await send("my-topic", payload);
|
|
@@ -382,63 +460,33 @@ try {
|
|
|
382
460
|
}
|
|
383
461
|
```
|
|
384
462
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
|
390
|
-
|
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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 |
|
|
399
481
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
```typescript
|
|
403
|
-
import { receive } from "@vercel/queue";
|
|
404
|
-
|
|
405
|
-
// Process next available message (or null if queue is empty)
|
|
406
|
-
await receive<T>(topicName, consumerGroup, async (message, metadata) => {
|
|
407
|
-
if (!message) {
|
|
408
|
-
console.log("Queue is empty");
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
// Process message
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
// Batch processing: fetch up to 10 messages in one request
|
|
415
|
-
await receive<T>(topicName, consumerGroup, handler, {
|
|
416
|
-
limit: 10, // Default: 1, Min: 1, Max: 10
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
// Process specific message by ID
|
|
420
|
-
await receive<T>(topicName, consumerGroup, handler, {
|
|
421
|
-
messageId: "message-id",
|
|
422
|
-
});
|
|
482
|
+
## Environment Variables
|
|
423
483
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
metadata: MessageMetadata | null,
|
|
431
|
-
) => Promise<void> | void;
|
|
432
|
-
|
|
433
|
-
// MessageMetadata type
|
|
434
|
-
interface MessageMetadata {
|
|
435
|
-
messageId: string;
|
|
436
|
-
deliveryCount: number;
|
|
437
|
-
createdAt: Date;
|
|
438
|
-
topicName: string;
|
|
439
|
-
consumerGroup: string;
|
|
440
|
-
}
|
|
441
|
-
```
|
|
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) | - |
|
|
442
490
|
|
|
443
491
|
## Service Limits & Constraints
|
|
444
492
|
|
|
@@ -464,57 +512,32 @@ interface MessageMetadata {
|
|
|
464
512
|
|
|
465
513
|
#### Receiving Messages
|
|
466
514
|
|
|
467
|
-
| Parameter | Default | Min | Max | Notes
|
|
468
|
-
| -------------------------- | ------- | --- | ----- |
|
|
469
|
-
| `visibilityTimeoutSeconds` |
|
|
470
|
-
| `limit` | 1 | 1 | 10 | Messages per request
|
|
471
|
-
|
|
472
|
-
#### Visibility Extension
|
|
473
|
-
|
|
474
|
-
| Constraint | Value |
|
|
475
|
-
| -------------------------- | ---------------------------------- |
|
|
476
|
-
| `visibilityTimeoutSeconds` | 0 - 3,600 seconds |
|
|
477
|
-
| Cannot extend beyond | Message's original expiration time |
|
|
478
|
-
| Receipt handle | Must match the receive operation |
|
|
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 |
|
|
479
519
|
|
|
480
520
|
### Identifier Formats
|
|
481
521
|
|
|
482
|
-
| Identifier
|
|
483
|
-
|
|
|
484
|
-
| Topic
|
|
485
|
-
| Consumer group
|
|
486
|
-
| Message ID
|
|
487
|
-
| Receipt handle
|
|
488
|
-
|
|
489
|
-
### Content-Type Handling
|
|
490
|
-
|
|
491
|
-
| Scenario | Result |
|
|
492
|
-
| ------------------------------- | -------------------------- |
|
|
493
|
-
| Client provides `Content-Type` | Used as-is |
|
|
494
|
-
| No header, magic bytes detected | Auto-detected MIME type |
|
|
495
|
-
| No header, detection fails | `application/octet-stream` |
|
|
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 |
|
|
496
528
|
|
|
497
529
|
### Wildcard Topics
|
|
498
530
|
|
|
499
|
-
Topic patterns support wildcards for flexible routing:
|
|
500
|
-
|
|
501
531
|
```json
|
|
502
532
|
{
|
|
503
533
|
"functions": {
|
|
504
534
|
"app/api/queue/route.ts": {
|
|
505
|
-
"experimentalTriggers": [
|
|
506
|
-
{
|
|
507
|
-
"type": "queue/v2beta",
|
|
508
|
-
"topic": "user-*"
|
|
509
|
-
}
|
|
510
|
-
]
|
|
535
|
+
"experimentalTriggers": [{ "type": "queue/v2beta", "topic": "user-*" }]
|
|
511
536
|
}
|
|
512
537
|
}
|
|
513
538
|
}
|
|
514
539
|
```
|
|
515
540
|
|
|
516
|
-
**Wildcard Rules:**
|
|
517
|
-
|
|
518
541
|
- `*` may only appear **once** in the pattern
|
|
519
542
|
- `*` must be at the **end** of the topic name
|
|
520
543
|
- Valid: `user-*`, `orders-*`
|
|
@@ -522,142 +545,111 @@ Topic patterns support wildcards for flexible routing:
|
|
|
522
545
|
|
|
523
546
|
## API Reference
|
|
524
547
|
|
|
525
|
-
###
|
|
526
|
-
|
|
527
|
-
| Import Path | `handleCallback` |
|
|
528
|
-
| ---------------------------- | ---------------------------------------------------------------- |
|
|
529
|
-
| `@vercel/queue` | Core async function: `(handler, parsed, opts?) => Promise<void>` |
|
|
530
|
-
| `@vercel/queue/web` | Returns `(request: Request) => Promise<Response>` |
|
|
531
|
-
| `@vercel/queue/nextjs/pages` | Returns `(req, res) => Promise<void>` |
|
|
532
|
-
|
|
533
|
-
Additional exports from `@vercel/queue`:
|
|
534
|
-
|
|
535
|
-
| Export | Description |
|
|
536
|
-
| ------------------------- | ------------------------------------------------------------- |
|
|
537
|
-
| `parseCallback` | Parse a Web API `Request` into a `ParsedCallbackRequest` |
|
|
538
|
-
| `parseRawCallback` | Parse a pre-parsed body + headers (e.g. Pages Router) |
|
|
539
|
-
| `CLOUD_EVENT_TYPE_V2BETA` | `"com.vercel.queue.v2beta"` — binary CloudEvent type constant |
|
|
540
|
-
|
|
541
|
-
### QueueClient Configuration
|
|
548
|
+
### `QueueClient`
|
|
542
549
|
|
|
543
550
|
```typescript
|
|
544
551
|
import { QueueClient } from "@vercel/queue";
|
|
545
552
|
|
|
546
|
-
const
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
//
|
|
550
|
-
baseUrl: "https://vercel-queue.com",
|
|
551
|
-
|
|
552
|
-
// API path prefix
|
|
553
|
-
// Default: "/api/v3/topic"
|
|
554
|
-
// Env: VERCEL_QUEUE_BASE_PATH
|
|
555
|
-
basePath: "/api/v3/topic",
|
|
556
|
-
|
|
557
|
-
// Auth token (auto-fetched via OIDC if not provided)
|
|
558
|
-
token: "my-token",
|
|
559
|
-
|
|
560
|
-
// Custom headers for all requests
|
|
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
|
|
561
557
|
headers: { "X-Custom": "value" },
|
|
562
|
-
|
|
563
|
-
//
|
|
564
|
-
// Default: process.env.VERCEL_DEPLOYMENT_ID
|
|
565
|
-
deploymentId: "dpl_xxx",
|
|
566
|
-
|
|
567
|
-
// Pin messages to current deployment when publishing
|
|
568
|
-
// Default: true
|
|
569
|
-
pinToDeployment: true,
|
|
558
|
+
transport: new JsonTransport(), // Default: JsonTransport
|
|
559
|
+
deploymentId: undefined, // omit = auto from env (pinned), null = unpinned, or explicit string
|
|
570
560
|
});
|
|
571
561
|
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
export const POST = handleCallback(handler, { client });
|
|
562
|
+
// Methods (arrow functions — safe to destructure)
|
|
563
|
+
const { send, receive, handleCallback, handleNodeCallback } = queue;
|
|
575
564
|
```
|
|
576
565
|
|
|
577
|
-
###
|
|
578
|
-
|
|
579
|
-
```typescript
|
|
580
|
-
await send("my-topic", payload, {
|
|
581
|
-
// Deduplication key
|
|
582
|
-
// Dedup window: min(retentionSeconds, 24 hours)
|
|
583
|
-
idempotencyKey: "unique-key",
|
|
584
|
-
|
|
585
|
-
// Message TTL in seconds
|
|
586
|
-
// Default: 86400, Min: 60, Max: 86400
|
|
587
|
-
retentionSeconds: 3600,
|
|
566
|
+
### `send(topicName, payload, options?)`
|
|
588
567
|
|
|
589
|
-
|
|
590
|
-
// Default: 0, Min: 0, Max: retentionSeconds
|
|
591
|
-
delaySeconds: 60,
|
|
568
|
+
Returns `{ messageId: string | null }`. `messageId` is `null` when the server accepted the message for deferred processing (e.g. during a server-side outage).
|
|
592
569
|
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
595
576
|
});
|
|
596
577
|
```
|
|
597
578
|
|
|
598
|
-
###
|
|
579
|
+
### `receive(topicName, consumerGroup, handler, options?)`
|
|
580
|
+
|
|
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.
|
|
599
582
|
|
|
600
|
-
|
|
583
|
+
For receive-by-id, operational errors are returned instead of thrown:
|
|
601
584
|
|
|
602
585
|
```typescript
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// Maximum messages to retrieve in a single request
|
|
606
|
-
// Default: 1, Min: 1, Max: 10
|
|
607
|
-
limit: 10,
|
|
608
|
-
|
|
609
|
-
// Message lock duration in seconds
|
|
610
|
-
// Default: 300, Min: 30, Max: 3600
|
|
611
|
-
visibilityTimeoutSeconds: 60,
|
|
586
|
+
const result = await receive("my-topic", "my-group", handler, {
|
|
587
|
+
messageId: "msg-123",
|
|
612
588
|
});
|
|
589
|
+
if (!result.ok) {
|
|
590
|
+
// result.reason is "not_found" | "not_available" | "already_processed"
|
|
591
|
+
console.log(result.reason, result.messageId);
|
|
592
|
+
}
|
|
593
|
+
```
|
|
613
594
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
// Message lock duration in seconds
|
|
620
|
-
// Default: 300, Min: 30, Max: 3600
|
|
621
|
-
visibilityTimeoutSeconds: 60,
|
|
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)
|
|
622
600
|
});
|
|
623
601
|
```
|
|
624
602
|
|
|
625
|
-
|
|
603
|
+
### `handleCallback(handler, options?)`
|
|
626
604
|
|
|
627
|
-
|
|
605
|
+
Vercel only. Returns `(request: Request) => Promise<Response>` — for frameworks that export Web API route handlers.
|
|
628
606
|
|
|
629
607
|
```typescript
|
|
630
|
-
import { handleCallback } from "@vercel/queue/web";
|
|
631
|
-
|
|
632
608
|
export const POST = handleCallback(
|
|
633
609
|
async (message, metadata) => {
|
|
634
610
|
await processMessage(message);
|
|
635
611
|
},
|
|
636
612
|
{
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
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
|
+
},
|
|
640
617
|
},
|
|
641
618
|
);
|
|
642
619
|
```
|
|
643
620
|
|
|
644
|
-
###
|
|
621
|
+
### `handleNodeCallback(handler, options?)`
|
|
645
622
|
|
|
646
|
-
|
|
623
|
+
Vercel only. Returns `(req, res) => Promise<void>` — for frameworks that export Connect-style handlers.
|
|
647
624
|
|
|
648
625
|
```typescript
|
|
649
|
-
|
|
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
|
+
```
|
|
650
636
|
|
|
651
|
-
|
|
652
|
-
const parsed = await parseCallback(request);
|
|
637
|
+
### Handler Signature
|
|
653
638
|
|
|
654
|
-
|
|
655
|
-
|
|
639
|
+
```typescript
|
|
640
|
+
type MessageHandler<T> = (
|
|
641
|
+
message: T,
|
|
642
|
+
metadata: MessageMetadata,
|
|
643
|
+
) => Promise<void> | void;
|
|
656
644
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
645
|
+
interface MessageMetadata {
|
|
646
|
+
messageId: string;
|
|
647
|
+
deliveryCount: number;
|
|
648
|
+
createdAt: Date;
|
|
649
|
+
expiresAt?: Date;
|
|
650
|
+
topicName: string;
|
|
651
|
+
consumerGroup: string;
|
|
652
|
+
region: string;
|
|
661
653
|
}
|
|
662
654
|
```
|
|
663
655
|
|