@vercel/queue 0.1.1 → 0.1.3
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 +241 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +241 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -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,8 @@ 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");
|
|
142
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
141
143
|
|
|
142
144
|
// src/types.ts
|
|
143
145
|
var MessageNotFoundError = class extends Error {
|
|
@@ -400,11 +402,12 @@ var ConsumerGroup = class {
|
|
|
400
402
|
message.receiptHandle,
|
|
401
403
|
options
|
|
402
404
|
);
|
|
405
|
+
const DEFAULT_RETENTION_MS = 864e5;
|
|
403
406
|
const metadata = {
|
|
404
407
|
messageId: message.messageId,
|
|
405
408
|
deliveryCount: message.deliveryCount,
|
|
406
409
|
createdAt: message.createdAt,
|
|
407
|
-
expiresAt: message.expiresAt,
|
|
410
|
+
expiresAt: message.expiresAt ?? new Date(message.createdAt.getTime() + DEFAULT_RETENTION_MS),
|
|
408
411
|
topicName: this.topicName,
|
|
409
412
|
consumerGroup: this.consumerGroupName,
|
|
410
413
|
region: this.client.getRegion()
|
|
@@ -558,7 +561,9 @@ var Topic = class {
|
|
|
558
561
|
invokeDevHandlers(
|
|
559
562
|
this.topicName,
|
|
560
563
|
result.messageId,
|
|
561
|
-
this.client.getRegion()
|
|
564
|
+
this.client.getRegion(),
|
|
565
|
+
options?.delaySeconds,
|
|
566
|
+
options?.retentionSeconds
|
|
562
567
|
);
|
|
563
568
|
}
|
|
564
569
|
return { messageId: result.messageId };
|
|
@@ -765,12 +770,30 @@ async function handleCallback(handler, request, options) {
|
|
|
765
770
|
|
|
766
771
|
// src/dev.ts
|
|
767
772
|
var import_meta = {};
|
|
773
|
+
var PREFIX = import_picocolors.default.cyan("[queue]");
|
|
774
|
+
var OK = import_picocolors.default.green("\u2713");
|
|
775
|
+
var FAIL = import_picocolors.default.red("\u2717");
|
|
776
|
+
var RETRY = import_picocolors.default.yellow("\u21BB");
|
|
768
777
|
function isDevMode() {
|
|
769
778
|
return process.env.NODE_ENV === "development";
|
|
770
779
|
}
|
|
771
780
|
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
772
781
|
function filePathToConsumerGroup(filePath) {
|
|
773
|
-
|
|
782
|
+
let result = "";
|
|
783
|
+
for (const char of filePath) {
|
|
784
|
+
if (char === "_") {
|
|
785
|
+
result += "__";
|
|
786
|
+
} else if (char === "/") {
|
|
787
|
+
result += "_S";
|
|
788
|
+
} else if (char === ".") {
|
|
789
|
+
result += "_D";
|
|
790
|
+
} else if (/[A-Za-z0-9-]/.test(char)) {
|
|
791
|
+
result += char;
|
|
792
|
+
} else {
|
|
793
|
+
result += "_" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return result;
|
|
774
797
|
}
|
|
775
798
|
function getDevRouteMappings() {
|
|
776
799
|
const g = globalThis;
|
|
@@ -795,21 +818,22 @@ function getDevRouteMappings() {
|
|
|
795
818
|
if (!trigger.type?.startsWith("queue/") || !trigger.topic) continue;
|
|
796
819
|
if (trigger.type !== "queue/v2beta") {
|
|
797
820
|
console.warn(
|
|
798
|
-
|
|
821
|
+
`${PREFIX} Unsupported trigger type "${trigger.type}" for topic "${trigger.topic}" in ${filePath}. Use "queue/v2beta" instead.`
|
|
799
822
|
);
|
|
800
823
|
continue;
|
|
801
824
|
}
|
|
802
825
|
mappings.push({
|
|
803
826
|
filePath,
|
|
804
827
|
topic: trigger.topic,
|
|
805
|
-
consumer: filePathToConsumerGroup(filePath)
|
|
828
|
+
consumer: filePathToConsumerGroup(filePath),
|
|
829
|
+
retryAfterSeconds: trigger.retryAfterSeconds
|
|
806
830
|
});
|
|
807
831
|
}
|
|
808
832
|
}
|
|
809
833
|
g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
|
|
810
834
|
return g[ROUTE_MAPPINGS_KEY];
|
|
811
835
|
} catch (error) {
|
|
812
|
-
console.warn(
|
|
836
|
+
console.warn(`${PREFIX} Failed to read vercel.json:`, error);
|
|
813
837
|
g[ROUTE_MAPPINGS_KEY] = null;
|
|
814
838
|
return null;
|
|
815
839
|
}
|
|
@@ -824,6 +848,20 @@ function findMatchingRoutes(topicName) {
|
|
|
824
848
|
return mapping.topic === topicName;
|
|
825
849
|
});
|
|
826
850
|
}
|
|
851
|
+
function findRetryAfterSeconds(topicName, consumerGroup) {
|
|
852
|
+
const routes = findMatchingRoutes(topicName);
|
|
853
|
+
const route = routes.find((r) => r.consumer === consumerGroup);
|
|
854
|
+
return route?.retryAfterSeconds;
|
|
855
|
+
}
|
|
856
|
+
function stripSrcPrefix(filePath) {
|
|
857
|
+
if (/^src\/(app|pages|server)\//.test(filePath)) {
|
|
858
|
+
return filePath.slice(4);
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
function matchesFunctionsPattern(sourceFile, pattern) {
|
|
863
|
+
return sourceFile === pattern || (0, import_minimatch.minimatch)(sourceFile, pattern);
|
|
864
|
+
}
|
|
827
865
|
function findMappingsForFile(absolutePath) {
|
|
828
866
|
const mappings = getDevRouteMappings();
|
|
829
867
|
if (!mappings) return [];
|
|
@@ -835,7 +873,10 @@ function findMappingsForFile(absolutePath) {
|
|
|
835
873
|
return [];
|
|
836
874
|
}
|
|
837
875
|
const normalized = relative2.replace(/\\/g, "/");
|
|
838
|
-
|
|
876
|
+
const stripped = stripSrcPrefix(normalized);
|
|
877
|
+
return mappings.filter(
|
|
878
|
+
(m) => matchesFunctionsPattern(normalized, m.filePath) || stripped !== null && matchesFunctionsPattern(stripped, m.filePath)
|
|
879
|
+
);
|
|
839
880
|
}
|
|
840
881
|
function parseFrameFilePath(line) {
|
|
841
882
|
let match = line.match(/\((.+?):\d+:\d+\)/);
|
|
@@ -929,7 +970,7 @@ function registerDevHandler(handler, client, options, _testCallerPath) {
|
|
|
929
970
|
const callerPath = _testCallerPath ?? extractCallerFilePath();
|
|
930
971
|
if (!callerPath) {
|
|
931
972
|
console.warn(
|
|
932
|
-
|
|
973
|
+
`${PREFIX} Could not determine caller file path for handler registration.`
|
|
933
974
|
);
|
|
934
975
|
return;
|
|
935
976
|
}
|
|
@@ -941,6 +982,9 @@ function registerDevHandler(handler, client, options, _testCallerPath) {
|
|
|
941
982
|
);
|
|
942
983
|
if (!registered) {
|
|
943
984
|
const allMappings = getDevRouteMappings();
|
|
985
|
+
if (allMappings && allMappings.length > 0) {
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
944
988
|
const cwd = process.cwd();
|
|
945
989
|
let relative2;
|
|
946
990
|
try {
|
|
@@ -948,19 +992,8 @@ function registerDevHandler(handler, client, options, _testCallerPath) {
|
|
|
948
992
|
} catch {
|
|
949
993
|
relative2 = callerPath;
|
|
950
994
|
}
|
|
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
995
|
console.warn(
|
|
963
|
-
|
|
996
|
+
`${PREFIX} handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
|
|
964
997
|
|
|
965
998
|
Add a trigger to vercel.json:
|
|
966
999
|
"${relative2}": {
|
|
@@ -1084,7 +1117,7 @@ async function invokeWithRetry(handler, request, options) {
|
|
|
1084
1117
|
}
|
|
1085
1118
|
}
|
|
1086
1119
|
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)$/, "");
|
|
1120
|
+
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
1121
|
if (!urlPath.startsWith("/")) {
|
|
1089
1122
|
urlPath = "/" + urlPath;
|
|
1090
1123
|
}
|
|
@@ -1177,7 +1210,7 @@ function buildNoHandlerWarning(topicName, routes, diagnostics) {
|
|
|
1177
1210
|
Import failures: ` + diagnostics.importFailures.slice(0, 2).map((f) => `${f.filePath} (${f.reason})`).join("; ") : "";
|
|
1178
1211
|
const primeSummary = diagnostics.primeFailures.length > 0 ? `
|
|
1179
1212
|
Prime failures: ` + diagnostics.primeFailures.slice(0, 3).map((f) => `${f.url} (${f.reason})`).join("; ") : "";
|
|
1180
|
-
return
|
|
1213
|
+
return `${PREFIX} No registered handler for topic "${topicName}". vercel.json maps this topic to [${files.join(", ")}] but auto-loading failed.
|
|
1181
1214
|
${portSummary}${importSummary}${primeSummary}
|
|
1182
1215
|
Ensure your dev server is running, set PORT if needed, and confirm mapped route files call handleCallback()/handleNodeCallback() at module scope.
|
|
1183
1216
|
` + (suggestedUrls.length > 0 ? `Try opening: ${suggestedUrls.join(" or ")}` : "Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT) and try sending again.");
|
|
@@ -1187,18 +1220,106 @@ function isHandlerRegistered(topicName, consumerGroup) {
|
|
|
1187
1220
|
(h) => h.consumerGroup === consumerGroup
|
|
1188
1221
|
);
|
|
1189
1222
|
}
|
|
1190
|
-
|
|
1223
|
+
var DEV_REDELIVERY_MAX_DELAY_S = 10;
|
|
1224
|
+
var DEV_REDELIVERY_DEFAULT_DELAY_S = 2;
|
|
1225
|
+
var DEV_REDELIVERY_MAX_ATTEMPTS = 10;
|
|
1226
|
+
var DEFAULT_RETENTION_S = 86400;
|
|
1227
|
+
function scheduleDevRedelivery(ctx, delayS) {
|
|
1228
|
+
const cappedDelay = Math.min(Math.max(delayS, 0), DEV_REDELIVERY_MAX_DELAY_S);
|
|
1229
|
+
console.log(
|
|
1230
|
+
`${PREFIX} ${RETRY} Scheduling re-delivery in ${cappedDelay}s: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
|
|
1231
|
+
);
|
|
1232
|
+
setTimeout(async () => {
|
|
1233
|
+
const nextDeliveryCount = ctx.deliveryCount + 1;
|
|
1234
|
+
const expiresAt = new Date(
|
|
1235
|
+
ctx.createdAt.getTime() + ctx.retentionSeconds * 1e3
|
|
1236
|
+
);
|
|
1237
|
+
if (Date.now() >= expiresAt.getTime()) {
|
|
1238
|
+
console.log(
|
|
1239
|
+
`${PREFIX} Message expired, stopping retries: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
|
|
1240
|
+
);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (nextDeliveryCount > DEV_REDELIVERY_MAX_ATTEMPTS) {
|
|
1244
|
+
console.log(
|
|
1245
|
+
`${PREFIX} Max re-deliveries (${DEV_REDELIVERY_MAX_ATTEMPTS}) reached: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
|
|
1246
|
+
);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const metadata = {
|
|
1250
|
+
messageId: ctx.messageId,
|
|
1251
|
+
deliveryCount: nextDeliveryCount,
|
|
1252
|
+
createdAt: ctx.createdAt,
|
|
1253
|
+
expiresAt,
|
|
1254
|
+
topicName: ctx.topicName,
|
|
1255
|
+
consumerGroup: ctx.consumerGroup,
|
|
1256
|
+
region: ctx.region
|
|
1257
|
+
};
|
|
1258
|
+
console.log(
|
|
1259
|
+
`${PREFIX} Re-delivering: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}" deliveryCount=${nextDeliveryCount}`
|
|
1260
|
+
);
|
|
1261
|
+
let succeeded = true;
|
|
1262
|
+
let nextRetryAfterS = null;
|
|
1263
|
+
let nextAcknowledged = false;
|
|
1264
|
+
try {
|
|
1265
|
+
await ctx.handler(ctx.payload, metadata);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
succeeded = false;
|
|
1268
|
+
if (ctx.retry) {
|
|
1269
|
+
let directive;
|
|
1270
|
+
try {
|
|
1271
|
+
directive = ctx.retry(error, metadata);
|
|
1272
|
+
} catch (retryErr) {
|
|
1273
|
+
console.warn(`${PREFIX} retry handler threw:`, retryErr);
|
|
1274
|
+
}
|
|
1275
|
+
if (directive && "afterSeconds" in directive) {
|
|
1276
|
+
nextRetryAfterS = directive.afterSeconds;
|
|
1277
|
+
} else if (directive && "acknowledge" in directive) {
|
|
1278
|
+
nextAcknowledged = true;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (!nextAcknowledged) {
|
|
1282
|
+
console.error(
|
|
1283
|
+
`${PREFIX} ${FAIL} Handler error on re-delivery: topic="${ctx.topicName}" messageId="${ctx.messageId}"`,
|
|
1284
|
+
error
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (succeeded) {
|
|
1289
|
+
console.log(
|
|
1290
|
+
`${PREFIX} ${OK} Message processed on re-delivery: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
|
|
1291
|
+
);
|
|
1292
|
+
} else if (nextAcknowledged) {
|
|
1293
|
+
console.log(
|
|
1294
|
+
`${PREFIX} ${OK} Message acknowledged (will not retry): topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
|
|
1295
|
+
);
|
|
1296
|
+
} else {
|
|
1297
|
+
const nextDelay = nextRetryAfterS ?? ctx.defaultRetryDelayS;
|
|
1298
|
+
scheduleDevRedelivery(
|
|
1299
|
+
{ ...ctx, deliveryCount: nextDeliveryCount },
|
|
1300
|
+
nextDelay
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}, cappedDelay * 1e3);
|
|
1304
|
+
}
|
|
1305
|
+
function invokeDevHandlers(topicName, messageId, region, delaySeconds, retentionSeconds) {
|
|
1191
1306
|
if (delaySeconds && delaySeconds > 0) {
|
|
1192
1307
|
console.log(
|
|
1193
|
-
|
|
1308
|
+
`${PREFIX} Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
|
|
1194
1309
|
);
|
|
1195
1310
|
setTimeout(() => {
|
|
1196
|
-
invokeDevHandlers(
|
|
1311
|
+
invokeDevHandlers(
|
|
1312
|
+
topicName,
|
|
1313
|
+
messageId,
|
|
1314
|
+
region,
|
|
1315
|
+
void 0,
|
|
1316
|
+
retentionSeconds
|
|
1317
|
+
);
|
|
1197
1318
|
}, delaySeconds * 1e3);
|
|
1198
1319
|
return;
|
|
1199
1320
|
}
|
|
1200
1321
|
console.log(
|
|
1201
|
-
|
|
1322
|
+
`${PREFIX} Message sent: topic="${topicName}" messageId="${messageId}"`
|
|
1202
1323
|
);
|
|
1203
1324
|
(async () => {
|
|
1204
1325
|
let handlers = lookupHandlers(topicName);
|
|
@@ -1225,7 +1346,7 @@ function invokeDevHandlers(topicName, messageId, region, delaySeconds) {
|
|
|
1225
1346
|
);
|
|
1226
1347
|
} else {
|
|
1227
1348
|
console.warn(
|
|
1228
|
-
|
|
1349
|
+
`${PREFIX} No registered handler for topic "${topicName}".
|
|
1229
1350
|
Ensure vercel.json has a matching experimentalTriggers entry and the route file calls handleCallback().`
|
|
1230
1351
|
);
|
|
1231
1352
|
}
|
|
@@ -1233,9 +1354,36 @@ Ensure vercel.json has a matching experimentalTriggers entry and the route file
|
|
|
1233
1354
|
}
|
|
1234
1355
|
const consumerGroups = handlers.map((h) => h.consumerGroup);
|
|
1235
1356
|
console.log(
|
|
1236
|
-
|
|
1357
|
+
`${PREFIX} Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
|
|
1237
1358
|
);
|
|
1359
|
+
const effectiveRetention = retentionSeconds ?? DEFAULT_RETENTION_S;
|
|
1238
1360
|
for (const entry of handlers) {
|
|
1361
|
+
let capturedPayload;
|
|
1362
|
+
let capturedCreatedAt = /* @__PURE__ */ new Date();
|
|
1363
|
+
let capturedDeliveryCount = 1;
|
|
1364
|
+
let handlerSucceeded = true;
|
|
1365
|
+
let retryAfterS = null;
|
|
1366
|
+
let retryAcknowledged = false;
|
|
1367
|
+
const wrappedHandler = async (message, metadata) => {
|
|
1368
|
+
capturedPayload = message;
|
|
1369
|
+
capturedCreatedAt = metadata.createdAt;
|
|
1370
|
+
capturedDeliveryCount = metadata.deliveryCount;
|
|
1371
|
+
try {
|
|
1372
|
+
await entry.handler(message, metadata);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
handlerSucceeded = false;
|
|
1375
|
+
throw error;
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
const wrappedRetry = entry.options?.retry ? (error, metadata) => {
|
|
1379
|
+
const directive = entry.options.retry(error, metadata);
|
|
1380
|
+
if (directive && "afterSeconds" in directive) {
|
|
1381
|
+
retryAfterS = directive.afterSeconds;
|
|
1382
|
+
} else if (directive && "acknowledge" in directive) {
|
|
1383
|
+
retryAcknowledged = true;
|
|
1384
|
+
}
|
|
1385
|
+
return directive;
|
|
1386
|
+
} : void 0;
|
|
1239
1387
|
const request = {
|
|
1240
1388
|
queueName: topicName,
|
|
1241
1389
|
consumerGroup: entry.consumerGroup,
|
|
@@ -1245,18 +1393,47 @@ Ensure vercel.json has a matching experimentalTriggers entry and the route file
|
|
|
1245
1393
|
const callbackOptions = {
|
|
1246
1394
|
client: entry.client,
|
|
1247
1395
|
visibilityTimeoutSeconds: entry.options?.visibilityTimeoutSeconds,
|
|
1248
|
-
retry:
|
|
1396
|
+
retry: wrappedRetry
|
|
1249
1397
|
};
|
|
1398
|
+
const consumerDefaultDelay = Math.min(
|
|
1399
|
+
findRetryAfterSeconds(topicName, entry.consumerGroup) ?? DEV_REDELIVERY_DEFAULT_DELAY_S,
|
|
1400
|
+
DEV_REDELIVERY_MAX_DELAY_S
|
|
1401
|
+
);
|
|
1402
|
+
const buildRedeliveryCtx = () => ({
|
|
1403
|
+
handler: entry.handler,
|
|
1404
|
+
retry: entry.options?.retry,
|
|
1405
|
+
payload: capturedPayload,
|
|
1406
|
+
topicName,
|
|
1407
|
+
consumerGroup: entry.consumerGroup,
|
|
1408
|
+
messageId,
|
|
1409
|
+
region,
|
|
1410
|
+
createdAt: capturedCreatedAt,
|
|
1411
|
+
retentionSeconds: effectiveRetention,
|
|
1412
|
+
deliveryCount: capturedDeliveryCount,
|
|
1413
|
+
defaultRetryDelayS: consumerDefaultDelay
|
|
1414
|
+
});
|
|
1250
1415
|
try {
|
|
1251
|
-
await invokeWithRetry(
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1416
|
+
await invokeWithRetry(wrappedHandler, request, callbackOptions);
|
|
1417
|
+
if (handlerSucceeded) {
|
|
1418
|
+
console.log(
|
|
1419
|
+
`${PREFIX} ${OK} Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
|
|
1420
|
+
);
|
|
1421
|
+
} else if (retryAcknowledged) {
|
|
1422
|
+
console.log(
|
|
1423
|
+
`${PREFIX} ${OK} Message acknowledged (will not retry): topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
|
|
1424
|
+
);
|
|
1425
|
+
} else if (retryAfterS !== null) {
|
|
1426
|
+
const devDelay = Math.min(retryAfterS, DEV_REDELIVERY_MAX_DELAY_S);
|
|
1427
|
+
scheduleDevRedelivery(buildRedeliveryCtx(), devDelay);
|
|
1428
|
+
}
|
|
1255
1429
|
} catch (error) {
|
|
1256
1430
|
console.error(
|
|
1257
|
-
|
|
1431
|
+
`${PREFIX} ${FAIL} Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
|
|
1258
1432
|
error
|
|
1259
1433
|
);
|
|
1434
|
+
if (!handlerSucceeded) {
|
|
1435
|
+
scheduleDevRedelivery(buildRedeliveryCtx(), consumerDefaultDelay);
|
|
1436
|
+
}
|
|
1260
1437
|
}
|
|
1261
1438
|
}
|
|
1262
1439
|
})();
|
|
@@ -1268,6 +1445,10 @@ function clearDevState() {
|
|
|
1268
1445
|
}
|
|
1269
1446
|
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
1270
1447
|
globalThis.__clearDevState = clearDevState;
|
|
1448
|
+
globalThis.__filePathToConsumerGroup = filePathToConsumerGroup;
|
|
1449
|
+
globalThis.__filePathToUrlPath = filePathToUrlPath;
|
|
1450
|
+
globalThis.__matchesFunctionsPattern = matchesFunctionsPattern;
|
|
1451
|
+
globalThis.__stripSrcPrefix = stripSrcPrefix;
|
|
1271
1452
|
}
|
|
1272
1453
|
|
|
1273
1454
|
// src/oidc.ts
|
|
@@ -1311,6 +1492,7 @@ function parseQueueHeaders(headers) {
|
|
|
1311
1492
|
const timestamp = headers.get("Vqs-Timestamp");
|
|
1312
1493
|
const contentType = headers.get("Content-Type") || "application/octet-stream";
|
|
1313
1494
|
const receiptHandle = headers.get("Vqs-Receipt-Handle");
|
|
1495
|
+
const expiresAtStr = headers.get("Vqs-Expires-At");
|
|
1314
1496
|
if (!messageId || !timestamp || !receiptHandle) {
|
|
1315
1497
|
return null;
|
|
1316
1498
|
}
|
|
@@ -1322,6 +1504,7 @@ function parseQueueHeaders(headers) {
|
|
|
1322
1504
|
messageId,
|
|
1323
1505
|
deliveryCount,
|
|
1324
1506
|
createdAt: new Date(timestamp),
|
|
1507
|
+
expiresAt: expiresAtStr ? new Date(expiresAtStr) : void 0,
|
|
1325
1508
|
contentType,
|
|
1326
1509
|
receiptHandle
|
|
1327
1510
|
};
|
|
@@ -1405,13 +1588,25 @@ var ApiClient = class _ApiClient {
|
|
|
1405
1588
|
if (this.providedToken) {
|
|
1406
1589
|
return this.providedToken;
|
|
1407
1590
|
}
|
|
1408
|
-
|
|
1409
|
-
|
|
1591
|
+
try {
|
|
1592
|
+
return await (0, import_oidc.getVercelOidcToken)();
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
1410
1595
|
throw new Error(
|
|
1411
|
-
|
|
1596
|
+
isDevMode() ? `Failed to get OIDC token for local development.
|
|
1597
|
+
|
|
1598
|
+
To fix this, pull your environment variables with Vercel CLI:
|
|
1599
|
+
\`vercel env pull\`
|
|
1600
|
+
|
|
1601
|
+
Cause: ${cause}` : `Failed to get OIDC token. This usually means the function is running outside of a Vercel Function environment.
|
|
1602
|
+
|
|
1603
|
+
To fix this, either:
|
|
1604
|
+
- Deploy to Vercel (OIDC tokens are provisioned automatically)
|
|
1605
|
+
- Provide a token explicitly: \`new QueueClient({ token: '...' })\`
|
|
1606
|
+
|
|
1607
|
+
Cause: ${cause}`
|
|
1412
1608
|
);
|
|
1413
1609
|
}
|
|
1414
|
-
return token;
|
|
1415
1610
|
}
|
|
1416
1611
|
buildUrl(queueName, ...pathSegments) {
|
|
1417
1612
|
const encodedQueue = encodeURIComponent(queueName);
|
|
@@ -1442,7 +1637,7 @@ var ApiClient = class _ApiClient {
|
|
|
1442
1637
|
}
|
|
1443
1638
|
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
1444
1639
|
}
|
|
1445
|
-
init.headers.set("User-Agent", `@vercel/queue/${"0.1.
|
|
1640
|
+
init.headers.set("User-Agent", `@vercel/queue/${"0.1.3"}`);
|
|
1446
1641
|
init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
|
|
1447
1642
|
const response = await fetch(url, init);
|
|
1448
1643
|
if (isDebugEnabled()) {
|
|
@@ -1817,9 +2012,11 @@ function resolveRegion(region) {
|
|
|
1817
2012
|
if (region) return region;
|
|
1818
2013
|
const fromEnv = process.env.VERCEL_REGION;
|
|
1819
2014
|
if (fromEnv) return fromEnv;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
2015
|
+
if (!isDevMode()) {
|
|
2016
|
+
console.warn(
|
|
2017
|
+
`[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" })`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
1823
2020
|
return DEFAULT_REGION;
|
|
1824
2021
|
}
|
|
1825
2022
|
var QueueClient = class {
|
|
@@ -1857,7 +2054,8 @@ var QueueClient = class {
|
|
|
1857
2054
|
topicName,
|
|
1858
2055
|
result.messageId,
|
|
1859
2056
|
api.getRegion(),
|
|
1860
|
-
options?.delaySeconds
|
|
2057
|
+
options?.delaySeconds,
|
|
2058
|
+
options?.retentionSeconds
|
|
1861
2059
|
);
|
|
1862
2060
|
}
|
|
1863
2061
|
return { messageId: result.messageId };
|