@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 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
- - **Generic Payload Support**: Send and receive any type of data with type
10
- safety
11
- - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream)
12
- or create your own
13
- - **Streaming Support**: Handle large payloads without loading them entirely
14
- into memory
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
- For local development, you'll need to pull your Vercel environment variables
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
- # Install Vercel CLI if you haven't already
32
- npm i -g vercel
25
+ # .env.production (on Vercel, inherits the platform's region)
26
+ QUEUE_REGION=${VERCEL_REGION}
33
27
 
34
- # Pull environment variables from your Vercel project
35
- vc env pull
28
+ # .env.development (fixed region for local dev — iad1 is recommended)
29
+ QUEUE_REGION=iad1
36
30
  ```
37
31
 
38
- Publishing and consuming messages on a queue
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
- // index.ts
42
- import { send, receive } from "@vercel/queue";
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
- // Consume a single message off the queue
56
- // (Often wrapped in a loop to keep polling messages off the queue)
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
- ## Usage with Vercel
44
+ Send a message anywhere in your app:
66
45
 
67
- When deploying on Vercel, rather than having a persistent server subscribed to a
68
- queue, Vercel can automatically trigger your API routes when messages are ready for
69
- consumption based on your vercel.json configuration.
46
+ ```typescript
47
+ import { send } from "@/lib/queue";
70
48
 
71
- To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
72
- existing app or create one using
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
- ### TypeScript Configuration
52
+ Handle incoming messages with a route handler:
76
53
 
77
- Update your `tsconfig.json` to use `"bundler"` module resolution for proper
78
- package export resolution:
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
- "compilerOptions": {
83
- "moduleResolution": "bundler"
84
- // ... other options
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
- ### Publishing messages to a queue
75
+ ### Project Setup
90
76
 
91
- Create a new server function to publish messages
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
- // app/actions.ts
95
- "use server";
94
+ import { QueueClient } from "@vercel/queue";
96
95
 
97
- import { send } from "@vercel/queue";
96
+ const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
98
97
 
99
- export async function publishTestMessage(message: string) {
100
- const { messageId } = await send("my-topic", {
101
- message,
102
- timestamp: Date.now(),
103
- });
98
+ // Simple send
99
+ await send("my-topic", { message: "Hello world" });
104
100
 
105
- console.log(`Published message ${messageId}`);
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
- Now wire up the server function to your app
113
+ Example usage in an API route:
110
114
 
111
- ```jsx
112
- // app/some/page.tsx
113
- "use client";
114
- import { publishTestMessage } from "./actions";
115
+ ```typescript
116
+ // app/api/send-message/route.ts
117
+ import { send } from "@/lib/queue";
115
118
 
116
- export default function Page() {
117
- return (
118
- // ...
119
- <button onClick={() => publishTestMessage("Hello world")}>
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
- ### Consuming the queue
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
- Messages are consumed using consumer groups, which provide load balancing and parallel processing capabilities.
128
+ ## Consuming Messages
129
129
 
130
- ## Usage with Vercel
130
+ ### On Vercel
131
131
 
132
- To consume queue messages in a Vercel deployment, you need to create (Next.js) API routes and configure them in your `vercel.json` file.
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
- ### 1. Create API Routes
134
+ #### Web API — `handleCallback`
135
135
 
136
- Create API routes to handle incoming queue messages using the `handleCallback` helper:
136
+ Returns `(Request) => Promise<Response>`. For frameworks that export Web API route handlers (Next.js App Router, Hono, etc.).
137
137
 
138
- ```typescript
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
- // Alternative: Multiple topics in one handler
169
- export const POST = handleCallback({
170
- "user-events": {
171
- welcome: async (message, metadata) => {
172
- console.log(`New user event:`, message, metadata);
173
- await sendWelcomeEmail(message.email);
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
- ### 2. Configure vercel.json
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
- ```json
194
- {
195
- "functions": {
196
- "app/api/queue/handle/route.ts": {
197
- "experimentalTriggers": [
198
- {
199
- "type": "queue/v1beta",
200
- "topic": "my-topic",
201
- "consumer": "consumer-group-1"
202
- },
203
- {
204
- "type": "queue/v1beta",
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
- ### 3. Multiple API Routes
167
+ #### Connect-style `handleNodeCallback`
215
168
 
216
- You can also create separate API routes for different topics:
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
- // app/api/queue/users/route.ts - Handle user events
220
- import { handleCallback } from "@vercel/queue";
221
-
222
- export const POST = handleCallback({
223
- "user-events": {
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
- // app/api/queue/orders/route.ts - Handle order events
234
- import { handleCallback } from "@vercel/queue";
235
-
236
- export const POST = handleCallback({
237
- "order-events": {
238
- fulfillment: async (order, metadata) => {
239
- console.log(`Processing order:`, order, metadata);
240
- await fulfillOrder(order);
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
- With corresponding `vercel.json`:
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/users/route.ts": {
206
+ "app/api/queue/my-topic/route.ts": {
252
207
  "experimentalTriggers": [
253
208
  {
254
- "type": "queue/v1beta",
255
- "topic": "user-events",
256
- "consumer": "processors"
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/v1beta",
224
+ "type": "queue/v2beta",
264
225
  "topic": "order-events",
265
- "consumer": "fulfillment"
226
+ "retryAfterSeconds": 300
266
227
  }
267
228
  ]
268
229
  }
@@ -270,522 +231,436 @@ With corresponding `vercel.json`:
270
231
  }
271
232
  ```
272
233
 
273
- ### Key Points
234
+ Multiple route files for the same topic create separate consumer groups — each receives a copy of every message.
274
235
 
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
236
+ ### 3. Retry and Backoff
279
237
 
280
- ## Key Features
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
- ### Consumer Groups
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
- // Multiple workers in the same group - they share/split messages
288
- // Using the same consumer group name means they will load balance messages
289
- await receive("my-topic", "workers", handler1);
290
- await receive("my-topic", "workers", handler2);
291
- // handler1 and handler2 will receive different messages (load balancing)
292
-
293
- // Different consumer groups - each gets copies of ALL messages
294
- await receive("my-topic", "analytics", analyticsHandler);
295
- await receive("my-topic", "webhooks", webhooksHandler);
296
- // analyticsHandler and webhooksHandler will both receive every message
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
- ## Architecture
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
- - **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
258
+ **Exponential backoff** uses `metadata.deliveryCount` (starts at 1, increments each delivery):
312
259
 
313
- ## Performance
314
-
315
- The multipart parser is optimized for high-throughput scenarios:
316
-
317
- - **Streaming**: Messages are yielded immediately as headers are parsed
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
322
-
323
- ## Serialization (Transport) System
324
-
325
- The queue client supports customizable serialization through the `Transport`
326
- interface with **streaming support** for memory-efficient processing. Transport
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
- Buffers data for JSON parsing - suitable for structured data that fits in
334
- memory.
275
+ **Conditional retry** only retry transient errors:
335
276
 
336
277
  ```typescript
337
- import { send, JsonTransport } from "@vercel/queue";
338
-
339
- // JsonTransport is the default, so these are equivalent:
340
- await send("json-topic", { data: "example" });
341
- await send(
342
- "json-topic",
343
- { data: "example" },
344
- { transport: new JsonTransport() },
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
- #### BufferTransport
292
+ **Acknowledging poison messages** — stop retrying messages that can never succeed:
349
293
 
350
- Buffers the entire payload into memory as a Buffer - suitable for binary data
351
- that fits in memory.
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
- #### StreamTransport
309
+ The `retry` option is available on `handleCallback`, `handleNodeCallback`, and `receive`.
354
310
 
355
- **True streaming support** - passes ReadableStream directly without buffering.
356
- Ideal for large files and memory-efficient processing.
311
+ ## Custom Client Configuration
357
312
 
358
- ### Custom Transport
313
+ All configuration lives on the `QueueClient`:
359
314
 
360
- You can create your own serialization format by implementing the `Transport`
361
- interface.
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
- ### Choosing the Right Transport
326
+ // Use directly
327
+ await queue.send("my-topic", myBuffer);
364
328
 
365
- | Use Case | Recommended Transport | Memory Usage | Performance |
366
- | ---------------------- | --------------------- | ------------ | ----------- |
367
- | Small JSON objects | `JsonTransport` | Low | High |
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
- ## API Reference
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
- ### Send Function
335
+ To customize the URL scheme, provide a `resolveBaseUrl` that returns a `URL`:
376
336
 
377
337
  ```typescript
378
- // Simple send - automatically uses default client and JSON transport
379
- await send<T>(topicName, payload);
380
-
381
- // Send with options including custom transport
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
- await send("events", eventData, {
396
- idempotencyKey: "unique-key-123",
397
- retentionSeconds: 3600,
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
- ### Receive Function
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
- // Process next available message (simplest form)
405
- await receive("topic-name", "consumer-group", handler);
366
+ import {
367
+ QueueClient,
368
+ JsonTransport,
369
+ BufferTransport,
370
+ StreamTransport,
371
+ } from "@vercel/queue";
406
372
 
407
- // Process specific message by ID with payload
408
- await receive("topic-name", "consumer-group", handler, {
409
- messageId: "message-id",
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
- // Process specific message by ID without payload (metadata only)
413
- // handler will be called with `undefined` as the payload
414
- await receive("topic-name", "consumer-group", handler, {
415
- messageId: "message-id",
416
- skipPayload: true,
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
- ### Message Handler
397
+ ## Manual Receive
421
398
 
422
- ```typescript
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
- // Handler result types
430
- type MessageHandlerResult = void | MessageTimeoutResult;
401
+ ### Region considerations
431
402
 
432
- interface MessageTimeoutResult {
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
- // Message Metadata
437
- interface MessageMetadata {
438
- messageId: string;
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
- ```typescript
447
- // Options for the receive function
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
- ### Transport Interface
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
- ```typescript
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
- ### Callback Handler
417
+ ### Usage
468
418
 
469
419
  ```typescript
470
- // Create a callback handler for Next.js route handlers
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
- // Multiple consumer groups per topic
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
- ### Basic JSON Processing
499
-
500
- ```typescript
501
- interface UserEvent {
502
- userId: string;
503
- action: string;
504
- timestamp: number;
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
- // Send a message
508
- await send<UserEvent>("user-events", {
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
- // Receive and process a message
515
- try {
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
- ### Processing Specific Messages by ID
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
- // Process a specific message if you know its ID
528
- const messageId = "01234567-89ab-cdef-0123-456789abcdef";
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 receive<{ userId: string; action: string }>(
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.message.includes("not found or not available")) {
543
- console.log("Message was already processed or does not exist");
544
- } else {
545
- console.error("Error processing message:", error);
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
- ### Processing Next Available Message
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
- ```typescript
553
- // Process the next available message (one-shot processing)
554
- try {
555
- await receive<{ taskType: string; data: any }>(
556
- "work-queue",
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
- // Handle conditional timeouts
578
- await receive<{ taskType: string; data: any }>(
579
- "work-queue",
580
- "workers",
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
- // Process specific message metadata only (no payload download)
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
- ### Timing Out Messages
559
+ ### `QueueClient`
606
560
 
607
561
  ```typescript
608
- // Process a message with conditional timeout
609
- try {
610
- await receive<{ taskType: string; data: any }>(
611
- "work-queue",
612
- "workers",
613
- async ({ taskType, data }) => {
614
- // Check if we can process this task type right now
615
- if (taskType === "heavy-computation" && isSystemOverloaded()) {
616
- // Return timeout to retry later (5 minutes)
617
- return { timeoutSeconds: 300 };
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
- // Example with exponential backoff
637
- try {
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
- ## Error Handling
577
+ ### `send(topicName, payload, options?)`
672
578
 
673
- The queue client provides specific error types for different failure scenarios:
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
- ### Error Types
676
-
677
- - **`QueueEmptyError`**: Thrown when attempting to receive messages from an
678
- empty queue (204 status)
679
-
680
- - Thrown by `receive()` when no messages are available
681
-
682
- - **`MessageLockedError`**: Thrown when a message is temporarily locked (423
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
- - **`MessageNotAvailableError`**: Message exists but isn't available for
692
- processing (409 status)
590
+ ### `receive(topicName, consumerGroup, handler, options?)`
693
591
 
694
- - **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
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
- - **`BadRequestError`**: Invalid request parameters (400 status)
594
+ For receive-by-id, operational errors are returned instead of thrown:
697
595
 
698
- - Invalid queue names, missing required parameters
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
- - **`UnauthorizedError`**: Authentication failure (401 status)
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
- - Missing or invalid authentication token
614
+ ### `handleCallback(handler, options?)`
703
615
 
704
- - **`ForbiddenError`**: Access denied (403 status)
616
+ Vercel only. Returns `(request: Request) => Promise<Response>` — for frameworks that export Web API route handlers.
705
617
 
706
- - Queue environment doesn't match token environment
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
- - **`InternalServerError`**: Server-side errors (500+ status codes)
709
- - Unexpected server errors, service unavailable, etc.
632
+ ### `handleNodeCallback(handler, options?)`
710
633
 
711
- ### Error Handling Examples
634
+ Vercel only. Returns `(req, res) => Promise<void>` — for frameworks that export Connect-style handlers.
712
635
 
713
636
  ```typescript
714
- import {
715
- BadRequestError,
716
- ForbiddenError,
717
- InternalServerError,
718
- MessageLockedError,
719
- QueueEmptyError,
720
- UnauthorizedError,
721
- } from "@vercel/queue";
722
-
723
- // Handle empty queue or locked messages
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
- // Handle locked message with retry
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
- // Handle authentication and authorization errors
754
- try {
755
- await send("my-topic", payload);
756
- } catch (error) {
757
- if (error instanceof UnauthorizedError) {
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
- // Complete error handling pattern
769
- function handleQueueError(error: unknown): void {
770
- if (error instanceof QueueEmptyError || error instanceof MessageLockedError) {
771
- // Transient errors - safe to retry
772
- console.log("Temporary condition, will retry");
773
- } else if (
774
- error instanceof UnauthorizedError ||
775
- error instanceof ForbiddenError
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