@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 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,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
- return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
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
- return mappings.filter((m) => m.filePath === normalized);
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
- function invokeDevHandlers(topicName, messageId, region, delaySeconds) {
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(topicName, messageId, region);
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: entry.options?.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(entry.handler, request, callbackOptions);
1252
- console.log(
1253
- `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
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.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
- 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
- );
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 };