@vercel/queue 0.1.1 → 0.1.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 +24 -14
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +210 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +210 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -62,15 +62,17 @@ export const POST = handleCallback(async (message, metadata) => {
|
|
|
62
62
|
|
|
63
63
|
That's it. The top-level `send` and `handleCallback` use an auto-configured default client. The region is auto-detected from `VERCEL_REGION` (set automatically on Vercel). If the region can't be detected (e.g. local dev), it falls back to `iad1`.
|
|
64
64
|
|
|
65
|
-
To target a specific region for sending
|
|
65
|
+
To target a specific region for sending with the top-level `send`, pass the `region` option:
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
68
|
await send("my-topic", payload, { region: "sfo1" });
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
+
> **Note:** The `region` option is only available on the top-level `send()` convenience export. When using a `QueueClient` instance, the region is set once in the constructor via `new QueueClient({ region: "sfo1" })` and applies to all operations on that client.
|
|
72
|
+
|
|
71
73
|
## Local Development
|
|
72
74
|
|
|
73
|
-
**Queues just work locally.** When you `send()` messages in development mode, the library sends them to the real Vercel Queue Service, then invokes your registered `handleCallback` handlers directly in-process using the same code path as production. Your handlers are called with the
|
|
75
|
+
**Queues just work locally.** When you `send()` messages in development mode, the library sends them to the real Vercel Queue Service, then invokes your registered `handleCallback` handlers directly in-process using the same code path as production. Your handlers are called with the same lifecycle (receive, visibility extension, ack) as in production. If a handler throws, the message is re-delivered after the configured retry delay (from `retryAfterSeconds` in `vercel.json` or the `retry` callback's `afterSeconds`), with an incrementing `deliveryCount`, matching production retry semantics.
|
|
74
76
|
|
|
75
77
|
Works with Next.js (Turbopack and webpack), Nuxt, SvelteKit, and any framework that runs server-side JavaScript. The SDK automatically discovers handlers from your `vercel.json` configuration and loads route modules on demand — no manual setup required beyond `vercel.json`.
|
|
76
78
|
|
|
@@ -92,7 +94,7 @@ await send(
|
|
|
92
94
|
idempotencyKey: "unique-key", // Prevent duplicate messages
|
|
93
95
|
retentionSeconds: 3600, // 1 hour TTL (default: 24h)
|
|
94
96
|
delaySeconds: 60, // Delay delivery by 1 minute
|
|
95
|
-
region: "sfo1", //
|
|
97
|
+
region: "sfo1", // Top-level send() only — not available on QueueClient.send()
|
|
96
98
|
},
|
|
97
99
|
);
|
|
98
100
|
```
|
|
@@ -128,11 +130,16 @@ Returns `(Request) => Promise<Response>`. For frameworks that export Web API rou
|
|
|
128
130
|
// app/api/queue/my-topic/route.ts
|
|
129
131
|
import { handleCallback } from "@vercel/queue";
|
|
130
132
|
|
|
131
|
-
export const POST = handleCallback(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
export const POST = handleCallback(
|
|
134
|
+
async (message, metadata) => {
|
|
135
|
+
// metadata: { messageId, deliveryCount, createdAt, expiresAt, topicName, consumerGroup, region }
|
|
136
|
+
await processMessage(message);
|
|
137
|
+
// Throwing an error will automatically retry the message
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
visibilityTimeoutSeconds: 600, // Lock duration while processing (default: 300)
|
|
141
|
+
},
|
|
142
|
+
);
|
|
136
143
|
```
|
|
137
144
|
|
|
138
145
|
**Nuxt:**
|
|
@@ -179,7 +186,7 @@ export default app;
|
|
|
179
186
|
|
|
180
187
|
#### Connect-style — `handleNodeCallback`
|
|
181
188
|
|
|
182
|
-
Returns `(req, res) => Promise<void>`. For frameworks that export Connect-style handlers (Vercel Node.js functions, Express, Next.js Pages Router, etc.).
|
|
189
|
+
Returns `(req, res) => Promise<void>`. For frameworks that export Connect-style handlers (Vercel Node.js functions, Express, Next.js Pages Router, etc.). **`handleNodeCallback` is not a top-level export** — it is only available via a `QueueClient` instance:
|
|
183
190
|
|
|
184
191
|
```typescript
|
|
185
192
|
// lib/queue.ts
|
|
@@ -269,7 +276,7 @@ Multiple route files for the same topic create separate consumer groups — each
|
|
|
269
276
|
|
|
270
277
|
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).
|
|
271
278
|
|
|
272
|
-
For finer control over retry timing, pass a `retry` option:
|
|
279
|
+
For finer control over retry timing, pass a `retry` option. You can also set `visibilityTimeoutSeconds` to control how long the message is locked during processing (default: 300):
|
|
273
280
|
|
|
274
281
|
```typescript
|
|
275
282
|
import { handleCallback } from "@vercel/queue";
|
|
@@ -279,6 +286,7 @@ export const POST = handleCallback(
|
|
|
279
286
|
await processMessage(message);
|
|
280
287
|
},
|
|
281
288
|
{
|
|
289
|
+
visibilityTimeoutSeconds: 600, // Lock duration while processing (default: 300)
|
|
282
290
|
retry: (error, metadata) => {
|
|
283
291
|
if (error instanceof RateLimitError) return { afterSeconds: 60 };
|
|
284
292
|
// Return undefined to let the error propagate normally
|
|
@@ -553,7 +561,7 @@ All error types:
|
|
|
553
561
|
| Limit | Value | Notes |
|
|
554
562
|
| --------------------------- | --------------------- | ----------------------------------- |
|
|
555
563
|
| Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
|
|
556
|
-
| Payload size |
|
|
564
|
+
| Payload size | 100 MB | Smaller messages have lower latency |
|
|
557
565
|
| Number of topics | Unlimited | No hard limit |
|
|
558
566
|
| Consumer groups per message | ~4,000 | Per-message limit |
|
|
559
567
|
| Messages per queue | Unlimited | No hard limit |
|
|
@@ -615,10 +623,12 @@ const { messageId } = await send("my-topic", payload, {
|
|
|
615
623
|
retentionSeconds: 3600, // Message TTL (default: 86400)
|
|
616
624
|
delaySeconds: 60, // Delay before visible (default: 0)
|
|
617
625
|
headers: { "X-Custom": "val" }, // Custom headers
|
|
618
|
-
region: "sfo1", //
|
|
626
|
+
region: "sfo1", // Override the auto-detected region for this send
|
|
619
627
|
});
|
|
620
628
|
```
|
|
621
629
|
|
|
630
|
+
The `region` option is exclusive to the top-level `send()`. It creates a one-off client targeting the given region. When using `QueueClient`, the region is set once in the constructor and applies to all operations.
|
|
631
|
+
|
|
622
632
|
Returns `{ messageId: string | null }`. `messageId` is `null` when the server accepted the message for deferred processing (e.g. during a server-side outage).
|
|
623
633
|
|
|
624
634
|
### Top-level `handleCallback(handler, options?)`
|
|
@@ -707,7 +717,7 @@ const result = await receive("my-topic", "my-group", handler, {
|
|
|
707
717
|
|
|
708
718
|
### `handleNodeCallback(handler, options?)`
|
|
709
719
|
|
|
710
|
-
Available on `QueueClient` only. Vercel only.
|
|
720
|
+
Available on `QueueClient` instances only (not a top-level export). Vercel only.
|
|
711
721
|
|
|
712
722
|
Returns `(req, res) => Promise<void>` — for frameworks that export Connect-style handlers.
|
|
713
723
|
|
|
@@ -740,7 +750,7 @@ interface MessageMetadata {
|
|
|
740
750
|
messageId: string;
|
|
741
751
|
deliveryCount: number;
|
|
742
752
|
createdAt: Date;
|
|
743
|
-
expiresAt
|
|
753
|
+
expiresAt: Date;
|
|
744
754
|
topicName: string;
|
|
745
755
|
consumerGroup: string;
|
|
746
756
|
region: string;
|
package/dist/index.d.mts
CHANGED
|
@@ -289,7 +289,8 @@ interface Message<T = unknown> {
|
|
|
289
289
|
createdAt: Date;
|
|
290
290
|
/**
|
|
291
291
|
* Timestamp when the message expires.
|
|
292
|
-
*
|
|
292
|
+
* Present in v2beta binary callback (`ce-vqsexpiresat` header) and
|
|
293
|
+
* V3 multipart/NDJSON responses (`Vqs-Expires-At` header / `expiresAt` field).
|
|
293
294
|
*/
|
|
294
295
|
expiresAt?: Date;
|
|
295
296
|
/**
|
|
@@ -311,7 +312,7 @@ interface MessageMetadata {
|
|
|
311
312
|
messageId: string;
|
|
312
313
|
deliveryCount: number;
|
|
313
314
|
createdAt: Date;
|
|
314
|
-
expiresAt
|
|
315
|
+
expiresAt: Date;
|
|
315
316
|
topicName: string;
|
|
316
317
|
consumerGroup: string;
|
|
317
318
|
/** Vercel region the client is targeting. */
|
package/dist/index.d.ts
CHANGED
|
@@ -289,7 +289,8 @@ interface Message<T = unknown> {
|
|
|
289
289
|
createdAt: Date;
|
|
290
290
|
/**
|
|
291
291
|
* Timestamp when the message expires.
|
|
292
|
-
*
|
|
292
|
+
* Present in v2beta binary callback (`ce-vqsexpiresat` header) and
|
|
293
|
+
* V3 multipart/NDJSON responses (`Vqs-Expires-At` header / `expiresAt` field).
|
|
293
294
|
*/
|
|
294
295
|
expiresAt?: Date;
|
|
295
296
|
/**
|
|
@@ -311,7 +312,7 @@ interface MessageMetadata {
|
|
|
311
312
|
messageId: string;
|
|
312
313
|
deliveryCount: number;
|
|
313
314
|
createdAt: Date;
|
|
314
|
-
expiresAt
|
|
315
|
+
expiresAt: Date;
|
|
315
316
|
topicName: string;
|
|
316
317
|
consumerGroup: string;
|
|
317
318
|
/** Vercel region the client is targeting. */
|
package/dist/index.js
CHANGED
|
@@ -138,6 +138,7 @@ var import_mixpart = require("mixpart");
|
|
|
138
138
|
var fs = __toESM(require("fs"));
|
|
139
139
|
var net = __toESM(require("net"));
|
|
140
140
|
var path = __toESM(require("path"));
|
|
141
|
+
var import_minimatch = require("minimatch");
|
|
141
142
|
|
|
142
143
|
// src/types.ts
|
|
143
144
|
var MessageNotFoundError = class extends Error {
|
|
@@ -400,11 +401,12 @@ var ConsumerGroup = class {
|
|
|
400
401
|
message.receiptHandle,
|
|
401
402
|
options
|
|
402
403
|
);
|
|
404
|
+
const DEFAULT_RETENTION_MS = 864e5;
|
|
403
405
|
const metadata = {
|
|
404
406
|
messageId: message.messageId,
|
|
405
407
|
deliveryCount: message.deliveryCount,
|
|
406
408
|
createdAt: message.createdAt,
|
|
407
|
-
expiresAt: message.expiresAt,
|
|
409
|
+
expiresAt: message.expiresAt ?? new Date(message.createdAt.getTime() + DEFAULT_RETENTION_MS),
|
|
408
410
|
topicName: this.topicName,
|
|
409
411
|
consumerGroup: this.consumerGroupName,
|
|
410
412
|
region: this.client.getRegion()
|
|
@@ -558,7 +560,9 @@ var Topic = class {
|
|
|
558
560
|
invokeDevHandlers(
|
|
559
561
|
this.topicName,
|
|
560
562
|
result.messageId,
|
|
561
|
-
this.client.getRegion()
|
|
563
|
+
this.client.getRegion(),
|
|
564
|
+
options?.delaySeconds,
|
|
565
|
+
options?.retentionSeconds
|
|
562
566
|
);
|
|
563
567
|
}
|
|
564
568
|
return { messageId: result.messageId };
|
|
@@ -770,7 +774,21 @@ function isDevMode() {
|
|
|
770
774
|
}
|
|
771
775
|
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
772
776
|
function filePathToConsumerGroup(filePath) {
|
|
773
|
-
|
|
777
|
+
let result = "";
|
|
778
|
+
for (const char of filePath) {
|
|
779
|
+
if (char === "_") {
|
|
780
|
+
result += "__";
|
|
781
|
+
} else if (char === "/") {
|
|
782
|
+
result += "_S";
|
|
783
|
+
} else if (char === ".") {
|
|
784
|
+
result += "_D";
|
|
785
|
+
} else if (/[A-Za-z0-9-]/.test(char)) {
|
|
786
|
+
result += char;
|
|
787
|
+
} else {
|
|
788
|
+
result += "_" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return result;
|
|
774
792
|
}
|
|
775
793
|
function getDevRouteMappings() {
|
|
776
794
|
const g = globalThis;
|
|
@@ -802,7 +820,8 @@ function getDevRouteMappings() {
|
|
|
802
820
|
mappings.push({
|
|
803
821
|
filePath,
|
|
804
822
|
topic: trigger.topic,
|
|
805
|
-
consumer: filePathToConsumerGroup(filePath)
|
|
823
|
+
consumer: filePathToConsumerGroup(filePath),
|
|
824
|
+
retryAfterSeconds: trigger.retryAfterSeconds
|
|
806
825
|
});
|
|
807
826
|
}
|
|
808
827
|
}
|
|
@@ -824,6 +843,20 @@ function findMatchingRoutes(topicName) {
|
|
|
824
843
|
return mapping.topic === topicName;
|
|
825
844
|
});
|
|
826
845
|
}
|
|
846
|
+
function findRetryAfterSeconds(topicName, consumerGroup) {
|
|
847
|
+
const routes = findMatchingRoutes(topicName);
|
|
848
|
+
const route = routes.find((r) => r.consumer === consumerGroup);
|
|
849
|
+
return route?.retryAfterSeconds;
|
|
850
|
+
}
|
|
851
|
+
function stripSrcPrefix(filePath) {
|
|
852
|
+
if (/^src\/(app|pages|server)\//.test(filePath)) {
|
|
853
|
+
return filePath.slice(4);
|
|
854
|
+
}
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
function matchesFunctionsPattern(sourceFile, pattern) {
|
|
858
|
+
return sourceFile === pattern || (0, import_minimatch.minimatch)(sourceFile, pattern);
|
|
859
|
+
}
|
|
827
860
|
function findMappingsForFile(absolutePath) {
|
|
828
861
|
const mappings = getDevRouteMappings();
|
|
829
862
|
if (!mappings) return [];
|
|
@@ -835,7 +868,10 @@ function findMappingsForFile(absolutePath) {
|
|
|
835
868
|
return [];
|
|
836
869
|
}
|
|
837
870
|
const normalized = relative2.replace(/\\/g, "/");
|
|
838
|
-
|
|
871
|
+
const stripped = stripSrcPrefix(normalized);
|
|
872
|
+
return mappings.filter(
|
|
873
|
+
(m) => matchesFunctionsPattern(normalized, m.filePath) || stripped !== null && matchesFunctionsPattern(stripped, m.filePath)
|
|
874
|
+
);
|
|
839
875
|
}
|
|
840
876
|
function parseFrameFilePath(line) {
|
|
841
877
|
let match = line.match(/\((.+?):\d+:\d+\)/);
|
|
@@ -941,6 +977,9 @@ function registerDevHandler(handler, client, options, _testCallerPath) {
|
|
|
941
977
|
);
|
|
942
978
|
if (!registered) {
|
|
943
979
|
const allMappings = getDevRouteMappings();
|
|
980
|
+
if (allMappings && allMappings.length > 0) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
944
983
|
const cwd = process.cwd();
|
|
945
984
|
let relative2;
|
|
946
985
|
try {
|
|
@@ -948,17 +987,6 @@ function registerDevHandler(handler, client, options, _testCallerPath) {
|
|
|
948
987
|
} catch {
|
|
949
988
|
relative2 = callerPath;
|
|
950
989
|
}
|
|
951
|
-
if (allMappings && allMappings.length > 0) {
|
|
952
|
-
const configuredFiles = Array.from(
|
|
953
|
-
new Set(allMappings.map((m) => m.filePath))
|
|
954
|
-
);
|
|
955
|
-
console.warn(
|
|
956
|
-
`[Dev Mode] handleCallback() in ${relative2} does not match any queue route in vercel.json. This handler won't receive messages.
|
|
957
|
-
Configured queue routes: [${configuredFiles.join(", ")}]
|
|
958
|
-
If this path is a bundled chunk, keep handleCallback()/handleNodeCallback() at module scope and let dev-mode route priming load the mapped file.`
|
|
959
|
-
);
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
990
|
console.warn(
|
|
963
991
|
`[Dev Mode] handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
|
|
964
992
|
|
|
@@ -1084,7 +1112,7 @@ async function invokeWithRetry(handler, request, options) {
|
|
|
1084
1112
|
}
|
|
1085
1113
|
}
|
|
1086
1114
|
function filePathToUrlPath(filePath) {
|
|
1087
|
-
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/^server\//, "/").replace(/^src\/routes\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\/\+server\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
|
|
1115
|
+
let urlPath = filePath.replace(/^src\/app\//, "/").replace(/^src\/pages\//, "/").replace(/^src\/server\//, "/").replace(/^src\/routes\//, "/").replace(/^app\//, "/").replace(/^pages\//, "/").replace(/^server\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\/\+server\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
|
|
1088
1116
|
if (!urlPath.startsWith("/")) {
|
|
1089
1117
|
urlPath = "/" + urlPath;
|
|
1090
1118
|
}
|
|
@@ -1187,13 +1215,101 @@ function isHandlerRegistered(topicName, consumerGroup) {
|
|
|
1187
1215
|
(h) => h.consumerGroup === consumerGroup
|
|
1188
1216
|
);
|
|
1189
1217
|
}
|
|
1190
|
-
|
|
1218
|
+
var DEV_REDELIVERY_MAX_DELAY_S = 10;
|
|
1219
|
+
var DEV_REDELIVERY_DEFAULT_DELAY_S = 2;
|
|
1220
|
+
var DEV_REDELIVERY_MAX_ATTEMPTS = 10;
|
|
1221
|
+
var DEFAULT_RETENTION_S = 86400;
|
|
1222
|
+
function scheduleDevRedelivery(ctx, delayS) {
|
|
1223
|
+
const cappedDelay = Math.min(Math.max(delayS, 0), DEV_REDELIVERY_MAX_DELAY_S);
|
|
1224
|
+
console.log(
|
|
1225
|
+
`[Dev Mode] \u21BB Scheduling re-delivery in ${cappedDelay}s: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
|
|
1226
|
+
);
|
|
1227
|
+
setTimeout(async () => {
|
|
1228
|
+
const nextDeliveryCount = ctx.deliveryCount + 1;
|
|
1229
|
+
const expiresAt = new Date(
|
|
1230
|
+
ctx.createdAt.getTime() + ctx.retentionSeconds * 1e3
|
|
1231
|
+
);
|
|
1232
|
+
if (Date.now() >= expiresAt.getTime()) {
|
|
1233
|
+
console.log(
|
|
1234
|
+
`[Dev Mode] Message expired, stopping retries: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
|
|
1235
|
+
);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (nextDeliveryCount > DEV_REDELIVERY_MAX_ATTEMPTS) {
|
|
1239
|
+
console.log(
|
|
1240
|
+
`[Dev Mode] Max re-deliveries (${DEV_REDELIVERY_MAX_ATTEMPTS}) reached: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
|
|
1241
|
+
);
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const metadata = {
|
|
1245
|
+
messageId: ctx.messageId,
|
|
1246
|
+
deliveryCount: nextDeliveryCount,
|
|
1247
|
+
createdAt: ctx.createdAt,
|
|
1248
|
+
expiresAt,
|
|
1249
|
+
topicName: ctx.topicName,
|
|
1250
|
+
consumerGroup: ctx.consumerGroup,
|
|
1251
|
+
region: ctx.region
|
|
1252
|
+
};
|
|
1253
|
+
console.log(
|
|
1254
|
+
`[Dev Mode] Re-delivering: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}" deliveryCount=${nextDeliveryCount}`
|
|
1255
|
+
);
|
|
1256
|
+
let succeeded = true;
|
|
1257
|
+
let nextRetryAfterS = null;
|
|
1258
|
+
let nextAcknowledged = false;
|
|
1259
|
+
try {
|
|
1260
|
+
await ctx.handler(ctx.payload, metadata);
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
succeeded = false;
|
|
1263
|
+
if (ctx.retry) {
|
|
1264
|
+
let directive;
|
|
1265
|
+
try {
|
|
1266
|
+
directive = ctx.retry(error, metadata);
|
|
1267
|
+
} catch (retryErr) {
|
|
1268
|
+
console.warn("[Dev Mode] retry handler threw:", retryErr);
|
|
1269
|
+
}
|
|
1270
|
+
if (directive && "afterSeconds" in directive) {
|
|
1271
|
+
nextRetryAfterS = directive.afterSeconds;
|
|
1272
|
+
} else if (directive && "acknowledge" in directive) {
|
|
1273
|
+
nextAcknowledged = true;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (!nextAcknowledged) {
|
|
1277
|
+
console.error(
|
|
1278
|
+
`[Dev Mode] \u2717 Handler error on re-delivery: topic="${ctx.topicName}" messageId="${ctx.messageId}"`,
|
|
1279
|
+
error
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (succeeded) {
|
|
1284
|
+
console.log(
|
|
1285
|
+
`[Dev Mode] \u2713 Message processed on re-delivery: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
|
|
1286
|
+
);
|
|
1287
|
+
} else if (nextAcknowledged) {
|
|
1288
|
+
console.log(
|
|
1289
|
+
`[Dev Mode] \u2713 Message acknowledged (will not retry): topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
|
|
1290
|
+
);
|
|
1291
|
+
} else {
|
|
1292
|
+
const nextDelay = nextRetryAfterS ?? ctx.defaultRetryDelayS;
|
|
1293
|
+
scheduleDevRedelivery(
|
|
1294
|
+
{ ...ctx, deliveryCount: nextDeliveryCount },
|
|
1295
|
+
nextDelay
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
}, cappedDelay * 1e3);
|
|
1299
|
+
}
|
|
1300
|
+
function invokeDevHandlers(topicName, messageId, region, delaySeconds, retentionSeconds) {
|
|
1191
1301
|
if (delaySeconds && delaySeconds > 0) {
|
|
1192
1302
|
console.log(
|
|
1193
1303
|
`[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
|
|
1194
1304
|
);
|
|
1195
1305
|
setTimeout(() => {
|
|
1196
|
-
invokeDevHandlers(
|
|
1306
|
+
invokeDevHandlers(
|
|
1307
|
+
topicName,
|
|
1308
|
+
messageId,
|
|
1309
|
+
region,
|
|
1310
|
+
void 0,
|
|
1311
|
+
retentionSeconds
|
|
1312
|
+
);
|
|
1197
1313
|
}, delaySeconds * 1e3);
|
|
1198
1314
|
return;
|
|
1199
1315
|
}
|
|
@@ -1235,7 +1351,34 @@ Ensure vercel.json has a matching experimentalTriggers entry and the route file
|
|
|
1235
1351
|
console.log(
|
|
1236
1352
|
`[Dev Mode] Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
|
|
1237
1353
|
);
|
|
1354
|
+
const effectiveRetention = retentionSeconds ?? DEFAULT_RETENTION_S;
|
|
1238
1355
|
for (const entry of handlers) {
|
|
1356
|
+
let capturedPayload;
|
|
1357
|
+
let capturedCreatedAt = /* @__PURE__ */ new Date();
|
|
1358
|
+
let capturedDeliveryCount = 1;
|
|
1359
|
+
let handlerSucceeded = true;
|
|
1360
|
+
let retryAfterS = null;
|
|
1361
|
+
let retryAcknowledged = false;
|
|
1362
|
+
const wrappedHandler = async (message, metadata) => {
|
|
1363
|
+
capturedPayload = message;
|
|
1364
|
+
capturedCreatedAt = metadata.createdAt;
|
|
1365
|
+
capturedDeliveryCount = metadata.deliveryCount;
|
|
1366
|
+
try {
|
|
1367
|
+
await entry.handler(message, metadata);
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
handlerSucceeded = false;
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
const wrappedRetry = entry.options?.retry ? (error, metadata) => {
|
|
1374
|
+
const directive = entry.options.retry(error, metadata);
|
|
1375
|
+
if (directive && "afterSeconds" in directive) {
|
|
1376
|
+
retryAfterS = directive.afterSeconds;
|
|
1377
|
+
} else if (directive && "acknowledge" in directive) {
|
|
1378
|
+
retryAcknowledged = true;
|
|
1379
|
+
}
|
|
1380
|
+
return directive;
|
|
1381
|
+
} : void 0;
|
|
1239
1382
|
const request = {
|
|
1240
1383
|
queueName: topicName,
|
|
1241
1384
|
consumerGroup: entry.consumerGroup,
|
|
@@ -1245,18 +1388,47 @@ Ensure vercel.json has a matching experimentalTriggers entry and the route file
|
|
|
1245
1388
|
const callbackOptions = {
|
|
1246
1389
|
client: entry.client,
|
|
1247
1390
|
visibilityTimeoutSeconds: entry.options?.visibilityTimeoutSeconds,
|
|
1248
|
-
retry:
|
|
1391
|
+
retry: wrappedRetry
|
|
1249
1392
|
};
|
|
1393
|
+
const consumerDefaultDelay = Math.min(
|
|
1394
|
+
findRetryAfterSeconds(topicName, entry.consumerGroup) ?? DEV_REDELIVERY_DEFAULT_DELAY_S,
|
|
1395
|
+
DEV_REDELIVERY_MAX_DELAY_S
|
|
1396
|
+
);
|
|
1397
|
+
const buildRedeliveryCtx = () => ({
|
|
1398
|
+
handler: entry.handler,
|
|
1399
|
+
retry: entry.options?.retry,
|
|
1400
|
+
payload: capturedPayload,
|
|
1401
|
+
topicName,
|
|
1402
|
+
consumerGroup: entry.consumerGroup,
|
|
1403
|
+
messageId,
|
|
1404
|
+
region,
|
|
1405
|
+
createdAt: capturedCreatedAt,
|
|
1406
|
+
retentionSeconds: effectiveRetention,
|
|
1407
|
+
deliveryCount: capturedDeliveryCount,
|
|
1408
|
+
defaultRetryDelayS: consumerDefaultDelay
|
|
1409
|
+
});
|
|
1250
1410
|
try {
|
|
1251
|
-
await invokeWithRetry(
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1411
|
+
await invokeWithRetry(wrappedHandler, request, callbackOptions);
|
|
1412
|
+
if (handlerSucceeded) {
|
|
1413
|
+
console.log(
|
|
1414
|
+
`[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
|
|
1415
|
+
);
|
|
1416
|
+
} else if (retryAcknowledged) {
|
|
1417
|
+
console.log(
|
|
1418
|
+
`[Dev Mode] \u2713 Message acknowledged (will not retry): topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
|
|
1419
|
+
);
|
|
1420
|
+
} else if (retryAfterS !== null) {
|
|
1421
|
+
const devDelay = Math.min(retryAfterS, DEV_REDELIVERY_MAX_DELAY_S);
|
|
1422
|
+
scheduleDevRedelivery(buildRedeliveryCtx(), devDelay);
|
|
1423
|
+
}
|
|
1255
1424
|
} catch (error) {
|
|
1256
1425
|
console.error(
|
|
1257
1426
|
`[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
|
|
1258
1427
|
error
|
|
1259
1428
|
);
|
|
1429
|
+
if (!handlerSucceeded) {
|
|
1430
|
+
scheduleDevRedelivery(buildRedeliveryCtx(), consumerDefaultDelay);
|
|
1431
|
+
}
|
|
1260
1432
|
}
|
|
1261
1433
|
}
|
|
1262
1434
|
})();
|
|
@@ -1268,6 +1440,10 @@ function clearDevState() {
|
|
|
1268
1440
|
}
|
|
1269
1441
|
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
1270
1442
|
globalThis.__clearDevState = clearDevState;
|
|
1443
|
+
globalThis.__filePathToConsumerGroup = filePathToConsumerGroup;
|
|
1444
|
+
globalThis.__filePathToUrlPath = filePathToUrlPath;
|
|
1445
|
+
globalThis.__matchesFunctionsPattern = matchesFunctionsPattern;
|
|
1446
|
+
globalThis.__stripSrcPrefix = stripSrcPrefix;
|
|
1271
1447
|
}
|
|
1272
1448
|
|
|
1273
1449
|
// src/oidc.ts
|
|
@@ -1311,6 +1487,7 @@ function parseQueueHeaders(headers) {
|
|
|
1311
1487
|
const timestamp = headers.get("Vqs-Timestamp");
|
|
1312
1488
|
const contentType = headers.get("Content-Type") || "application/octet-stream";
|
|
1313
1489
|
const receiptHandle = headers.get("Vqs-Receipt-Handle");
|
|
1490
|
+
const expiresAtStr = headers.get("Vqs-Expires-At");
|
|
1314
1491
|
if (!messageId || !timestamp || !receiptHandle) {
|
|
1315
1492
|
return null;
|
|
1316
1493
|
}
|
|
@@ -1322,6 +1499,7 @@ function parseQueueHeaders(headers) {
|
|
|
1322
1499
|
messageId,
|
|
1323
1500
|
deliveryCount,
|
|
1324
1501
|
createdAt: new Date(timestamp),
|
|
1502
|
+
expiresAt: expiresAtStr ? new Date(expiresAtStr) : void 0,
|
|
1325
1503
|
contentType,
|
|
1326
1504
|
receiptHandle
|
|
1327
1505
|
};
|
|
@@ -1442,7 +1620,7 @@ var ApiClient = class _ApiClient {
|
|
|
1442
1620
|
}
|
|
1443
1621
|
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
1444
1622
|
}
|
|
1445
|
-
init.headers.set("User-Agent", `@vercel/queue/${"0.1.
|
|
1623
|
+
init.headers.set("User-Agent", `@vercel/queue/${"0.1.2"}`);
|
|
1446
1624
|
init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
|
|
1447
1625
|
const response = await fetch(url, init);
|
|
1448
1626
|
if (isDebugEnabled()) {
|
|
@@ -1817,9 +1995,11 @@ function resolveRegion(region) {
|
|
|
1817
1995
|
if (region) return region;
|
|
1818
1996
|
const fromEnv = process.env.VERCEL_REGION;
|
|
1819
1997
|
if (fromEnv) return fromEnv;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1998
|
+
if (!isDevMode()) {
|
|
1999
|
+
console.warn(
|
|
2000
|
+
`[QueueClient] Region not detected \u2014 defaulting to "${DEFAULT_REGION}". On Vercel this is set automatically via VERCEL_REGION. To silence this warning, pass region explicitly: new QueueClient({ region: "iad1" })`
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
1823
2003
|
return DEFAULT_REGION;
|
|
1824
2004
|
}
|
|
1825
2005
|
var QueueClient = class {
|
|
@@ -1857,7 +2037,8 @@ var QueueClient = class {
|
|
|
1857
2037
|
topicName,
|
|
1858
2038
|
result.messageId,
|
|
1859
2039
|
api.getRegion(),
|
|
1860
|
-
options?.delaySeconds
|
|
2040
|
+
options?.delaySeconds,
|
|
2041
|
+
options?.retentionSeconds
|
|
1861
2042
|
);
|
|
1862
2043
|
}
|
|
1863
2044
|
return { messageId: result.messageId };
|