@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 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, pass the `region` option:
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 exact same lifecycle (receive, visibility extension, ack) as in production.
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", // Optionaltarget a specific region
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(async (message, metadata) => {
132
- // metadata: { messageId, deliveryCount, createdAt, expiresAt?, topicName, consumerGroup, region }
133
- await processMessage(message);
134
- // Throwing an error will automatically retry the message
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.). Requires a `QueueClient` instance:
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 | 1 GB | Smaller messages have lower latency |
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", // Optional target a specific region
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?: Date;
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
- * Only present for messages delivered via the v2beta binary callback path.
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?: Date;
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
- * Only present for messages delivered via the v2beta binary callback path.
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?: Date;
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
- return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
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
- `[Dev Mode] Unsupported trigger type "${trigger.type}" for topic "${trigger.topic}" in ${filePath}. Use "queue/v2beta" instead.`
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("[Dev Mode] Failed to read vercel.json:", error);
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
- return mappings.filter((m) => m.filePath === normalized);
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
- "[Dev Mode] Could not determine caller file path for handler registration."
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
- `[Dev Mode] handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
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 `[Dev Mode] No registered handler for topic "${topicName}". vercel.json maps this topic to [${files.join(", ")}] but auto-loading failed.
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
- function invokeDevHandlers(topicName, messageId, region, delaySeconds) {
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
- `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
1308
+ `${PREFIX} Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
1194
1309
  );
1195
1310
  setTimeout(() => {
1196
- invokeDevHandlers(topicName, messageId, region);
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
- `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
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
- `[Dev Mode] No registered handler for topic "${topicName}".
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
- `[Dev Mode] Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
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: entry.options?.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(entry.handler, request, callbackOptions);
1252
- console.log(
1253
- `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
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
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
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
- const token = await (0, import_oidc.getVercelOidcToken)();
1409
- if (!token) {
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
- "Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment, or provide a token explicitly.\n\nTo set up your environment:\n1. Link your project: 'vercel link'\n2. Pull environment variables: 'vercel env pull'\n3. Run with environment: 'dotenv -e .env.local -- your-command'"
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.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
- console.warn(
1821
- `[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" })`
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 };