@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/dist/index.mjs CHANGED
@@ -78,6 +78,7 @@ import { parseMultipartStream } from "mixpart";
78
78
  import * as fs from "fs";
79
79
  import * as net from "net";
80
80
  import * as path from "path";
81
+ import { minimatch } from "minimatch";
81
82
 
82
83
  // src/types.ts
83
84
  var MessageNotFoundError = class extends Error {
@@ -340,11 +341,12 @@ var ConsumerGroup = class {
340
341
  message.receiptHandle,
341
342
  options
342
343
  );
344
+ const DEFAULT_RETENTION_MS = 864e5;
343
345
  const metadata = {
344
346
  messageId: message.messageId,
345
347
  deliveryCount: message.deliveryCount,
346
348
  createdAt: message.createdAt,
347
- expiresAt: message.expiresAt,
349
+ expiresAt: message.expiresAt ?? new Date(message.createdAt.getTime() + DEFAULT_RETENTION_MS),
348
350
  topicName: this.topicName,
349
351
  consumerGroup: this.consumerGroupName,
350
352
  region: this.client.getRegion()
@@ -498,7 +500,9 @@ var Topic = class {
498
500
  invokeDevHandlers(
499
501
  this.topicName,
500
502
  result.messageId,
501
- this.client.getRegion()
503
+ this.client.getRegion(),
504
+ options?.delaySeconds,
505
+ options?.retentionSeconds
502
506
  );
503
507
  }
504
508
  return { messageId: result.messageId };
@@ -709,7 +713,21 @@ function isDevMode() {
709
713
  }
710
714
  var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
711
715
  function filePathToConsumerGroup(filePath) {
712
- return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
716
+ let result = "";
717
+ for (const char of filePath) {
718
+ if (char === "_") {
719
+ result += "__";
720
+ } else if (char === "/") {
721
+ result += "_S";
722
+ } else if (char === ".") {
723
+ result += "_D";
724
+ } else if (/[A-Za-z0-9-]/.test(char)) {
725
+ result += char;
726
+ } else {
727
+ result += "_" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
728
+ }
729
+ }
730
+ return result;
713
731
  }
714
732
  function getDevRouteMappings() {
715
733
  const g = globalThis;
@@ -741,7 +759,8 @@ function getDevRouteMappings() {
741
759
  mappings.push({
742
760
  filePath,
743
761
  topic: trigger.topic,
744
- consumer: filePathToConsumerGroup(filePath)
762
+ consumer: filePathToConsumerGroup(filePath),
763
+ retryAfterSeconds: trigger.retryAfterSeconds
745
764
  });
746
765
  }
747
766
  }
@@ -763,6 +782,20 @@ function findMatchingRoutes(topicName) {
763
782
  return mapping.topic === topicName;
764
783
  });
765
784
  }
785
+ function findRetryAfterSeconds(topicName, consumerGroup) {
786
+ const routes = findMatchingRoutes(topicName);
787
+ const route = routes.find((r) => r.consumer === consumerGroup);
788
+ return route?.retryAfterSeconds;
789
+ }
790
+ function stripSrcPrefix(filePath) {
791
+ if (/^src\/(app|pages|server)\//.test(filePath)) {
792
+ return filePath.slice(4);
793
+ }
794
+ return null;
795
+ }
796
+ function matchesFunctionsPattern(sourceFile, pattern) {
797
+ return sourceFile === pattern || minimatch(sourceFile, pattern);
798
+ }
766
799
  function findMappingsForFile(absolutePath) {
767
800
  const mappings = getDevRouteMappings();
768
801
  if (!mappings) return [];
@@ -774,7 +807,10 @@ function findMappingsForFile(absolutePath) {
774
807
  return [];
775
808
  }
776
809
  const normalized = relative2.replace(/\\/g, "/");
777
- return mappings.filter((m) => m.filePath === normalized);
810
+ const stripped = stripSrcPrefix(normalized);
811
+ return mappings.filter(
812
+ (m) => matchesFunctionsPattern(normalized, m.filePath) || stripped !== null && matchesFunctionsPattern(stripped, m.filePath)
813
+ );
778
814
  }
779
815
  function parseFrameFilePath(line) {
780
816
  let match = line.match(/\((.+?):\d+:\d+\)/);
@@ -880,6 +916,9 @@ function registerDevHandler(handler, client, options, _testCallerPath) {
880
916
  );
881
917
  if (!registered) {
882
918
  const allMappings = getDevRouteMappings();
919
+ if (allMappings && allMappings.length > 0) {
920
+ return;
921
+ }
883
922
  const cwd = process.cwd();
884
923
  let relative2;
885
924
  try {
@@ -887,17 +926,6 @@ function registerDevHandler(handler, client, options, _testCallerPath) {
887
926
  } catch {
888
927
  relative2 = callerPath;
889
928
  }
890
- if (allMappings && allMappings.length > 0) {
891
- const configuredFiles = Array.from(
892
- new Set(allMappings.map((m) => m.filePath))
893
- );
894
- console.warn(
895
- `[Dev Mode] handleCallback() in ${relative2} does not match any queue route in vercel.json. This handler won't receive messages.
896
- Configured queue routes: [${configuredFiles.join(", ")}]
897
- If this path is a bundled chunk, keep handleCallback()/handleNodeCallback() at module scope and let dev-mode route priming load the mapped file.`
898
- );
899
- return;
900
- }
901
929
  console.warn(
902
930
  `[Dev Mode] handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
903
931
 
@@ -1023,7 +1051,7 @@ async function invokeWithRetry(handler, request, options) {
1023
1051
  }
1024
1052
  }
1025
1053
  function filePathToUrlPath(filePath) {
1026
- 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)$/, "");
1054
+ 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)$/, "");
1027
1055
  if (!urlPath.startsWith("/")) {
1028
1056
  urlPath = "/" + urlPath;
1029
1057
  }
@@ -1126,13 +1154,101 @@ function isHandlerRegistered(topicName, consumerGroup) {
1126
1154
  (h) => h.consumerGroup === consumerGroup
1127
1155
  );
1128
1156
  }
1129
- function invokeDevHandlers(topicName, messageId, region, delaySeconds) {
1157
+ var DEV_REDELIVERY_MAX_DELAY_S = 10;
1158
+ var DEV_REDELIVERY_DEFAULT_DELAY_S = 2;
1159
+ var DEV_REDELIVERY_MAX_ATTEMPTS = 10;
1160
+ var DEFAULT_RETENTION_S = 86400;
1161
+ function scheduleDevRedelivery(ctx, delayS) {
1162
+ const cappedDelay = Math.min(Math.max(delayS, 0), DEV_REDELIVERY_MAX_DELAY_S);
1163
+ console.log(
1164
+ `[Dev Mode] \u21BB Scheduling re-delivery in ${cappedDelay}s: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
1165
+ );
1166
+ setTimeout(async () => {
1167
+ const nextDeliveryCount = ctx.deliveryCount + 1;
1168
+ const expiresAt = new Date(
1169
+ ctx.createdAt.getTime() + ctx.retentionSeconds * 1e3
1170
+ );
1171
+ if (Date.now() >= expiresAt.getTime()) {
1172
+ console.log(
1173
+ `[Dev Mode] Message expired, stopping retries: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
1174
+ );
1175
+ return;
1176
+ }
1177
+ if (nextDeliveryCount > DEV_REDELIVERY_MAX_ATTEMPTS) {
1178
+ console.log(
1179
+ `[Dev Mode] Max re-deliveries (${DEV_REDELIVERY_MAX_ATTEMPTS}) reached: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
1180
+ );
1181
+ return;
1182
+ }
1183
+ const metadata = {
1184
+ messageId: ctx.messageId,
1185
+ deliveryCount: nextDeliveryCount,
1186
+ createdAt: ctx.createdAt,
1187
+ expiresAt,
1188
+ topicName: ctx.topicName,
1189
+ consumerGroup: ctx.consumerGroup,
1190
+ region: ctx.region
1191
+ };
1192
+ console.log(
1193
+ `[Dev Mode] Re-delivering: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}" deliveryCount=${nextDeliveryCount}`
1194
+ );
1195
+ let succeeded = true;
1196
+ let nextRetryAfterS = null;
1197
+ let nextAcknowledged = false;
1198
+ try {
1199
+ await ctx.handler(ctx.payload, metadata);
1200
+ } catch (error) {
1201
+ succeeded = false;
1202
+ if (ctx.retry) {
1203
+ let directive;
1204
+ try {
1205
+ directive = ctx.retry(error, metadata);
1206
+ } catch (retryErr) {
1207
+ console.warn("[Dev Mode] retry handler threw:", retryErr);
1208
+ }
1209
+ if (directive && "afterSeconds" in directive) {
1210
+ nextRetryAfterS = directive.afterSeconds;
1211
+ } else if (directive && "acknowledge" in directive) {
1212
+ nextAcknowledged = true;
1213
+ }
1214
+ }
1215
+ if (!nextAcknowledged) {
1216
+ console.error(
1217
+ `[Dev Mode] \u2717 Handler error on re-delivery: topic="${ctx.topicName}" messageId="${ctx.messageId}"`,
1218
+ error
1219
+ );
1220
+ }
1221
+ }
1222
+ if (succeeded) {
1223
+ console.log(
1224
+ `[Dev Mode] \u2713 Message processed on re-delivery: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
1225
+ );
1226
+ } else if (nextAcknowledged) {
1227
+ console.log(
1228
+ `[Dev Mode] \u2713 Message acknowledged (will not retry): topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
1229
+ );
1230
+ } else {
1231
+ const nextDelay = nextRetryAfterS ?? ctx.defaultRetryDelayS;
1232
+ scheduleDevRedelivery(
1233
+ { ...ctx, deliveryCount: nextDeliveryCount },
1234
+ nextDelay
1235
+ );
1236
+ }
1237
+ }, cappedDelay * 1e3);
1238
+ }
1239
+ function invokeDevHandlers(topicName, messageId, region, delaySeconds, retentionSeconds) {
1130
1240
  if (delaySeconds && delaySeconds > 0) {
1131
1241
  console.log(
1132
1242
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
1133
1243
  );
1134
1244
  setTimeout(() => {
1135
- invokeDevHandlers(topicName, messageId, region);
1245
+ invokeDevHandlers(
1246
+ topicName,
1247
+ messageId,
1248
+ region,
1249
+ void 0,
1250
+ retentionSeconds
1251
+ );
1136
1252
  }, delaySeconds * 1e3);
1137
1253
  return;
1138
1254
  }
@@ -1174,7 +1290,34 @@ Ensure vercel.json has a matching experimentalTriggers entry and the route file
1174
1290
  console.log(
1175
1291
  `[Dev Mode] Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
1176
1292
  );
1293
+ const effectiveRetention = retentionSeconds ?? DEFAULT_RETENTION_S;
1177
1294
  for (const entry of handlers) {
1295
+ let capturedPayload;
1296
+ let capturedCreatedAt = /* @__PURE__ */ new Date();
1297
+ let capturedDeliveryCount = 1;
1298
+ let handlerSucceeded = true;
1299
+ let retryAfterS = null;
1300
+ let retryAcknowledged = false;
1301
+ const wrappedHandler = async (message, metadata) => {
1302
+ capturedPayload = message;
1303
+ capturedCreatedAt = metadata.createdAt;
1304
+ capturedDeliveryCount = metadata.deliveryCount;
1305
+ try {
1306
+ await entry.handler(message, metadata);
1307
+ } catch (error) {
1308
+ handlerSucceeded = false;
1309
+ throw error;
1310
+ }
1311
+ };
1312
+ const wrappedRetry = entry.options?.retry ? (error, metadata) => {
1313
+ const directive = entry.options.retry(error, metadata);
1314
+ if (directive && "afterSeconds" in directive) {
1315
+ retryAfterS = directive.afterSeconds;
1316
+ } else if (directive && "acknowledge" in directive) {
1317
+ retryAcknowledged = true;
1318
+ }
1319
+ return directive;
1320
+ } : void 0;
1178
1321
  const request = {
1179
1322
  queueName: topicName,
1180
1323
  consumerGroup: entry.consumerGroup,
@@ -1184,18 +1327,47 @@ Ensure vercel.json has a matching experimentalTriggers entry and the route file
1184
1327
  const callbackOptions = {
1185
1328
  client: entry.client,
1186
1329
  visibilityTimeoutSeconds: entry.options?.visibilityTimeoutSeconds,
1187
- retry: entry.options?.retry
1330
+ retry: wrappedRetry
1188
1331
  };
1332
+ const consumerDefaultDelay = Math.min(
1333
+ findRetryAfterSeconds(topicName, entry.consumerGroup) ?? DEV_REDELIVERY_DEFAULT_DELAY_S,
1334
+ DEV_REDELIVERY_MAX_DELAY_S
1335
+ );
1336
+ const buildRedeliveryCtx = () => ({
1337
+ handler: entry.handler,
1338
+ retry: entry.options?.retry,
1339
+ payload: capturedPayload,
1340
+ topicName,
1341
+ consumerGroup: entry.consumerGroup,
1342
+ messageId,
1343
+ region,
1344
+ createdAt: capturedCreatedAt,
1345
+ retentionSeconds: effectiveRetention,
1346
+ deliveryCount: capturedDeliveryCount,
1347
+ defaultRetryDelayS: consumerDefaultDelay
1348
+ });
1189
1349
  try {
1190
- await invokeWithRetry(entry.handler, request, callbackOptions);
1191
- console.log(
1192
- `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1193
- );
1350
+ await invokeWithRetry(wrappedHandler, request, callbackOptions);
1351
+ if (handlerSucceeded) {
1352
+ console.log(
1353
+ `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1354
+ );
1355
+ } else if (retryAcknowledged) {
1356
+ console.log(
1357
+ `[Dev Mode] \u2713 Message acknowledged (will not retry): topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1358
+ );
1359
+ } else if (retryAfterS !== null) {
1360
+ const devDelay = Math.min(retryAfterS, DEV_REDELIVERY_MAX_DELAY_S);
1361
+ scheduleDevRedelivery(buildRedeliveryCtx(), devDelay);
1362
+ }
1194
1363
  } catch (error) {
1195
1364
  console.error(
1196
1365
  `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
1197
1366
  error
1198
1367
  );
1368
+ if (!handlerSucceeded) {
1369
+ scheduleDevRedelivery(buildRedeliveryCtx(), consumerDefaultDelay);
1370
+ }
1199
1371
  }
1200
1372
  }
1201
1373
  })();
@@ -1207,6 +1379,10 @@ function clearDevState() {
1207
1379
  }
1208
1380
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
1209
1381
  globalThis.__clearDevState = clearDevState;
1382
+ globalThis.__filePathToConsumerGroup = filePathToConsumerGroup;
1383
+ globalThis.__filePathToUrlPath = filePathToUrlPath;
1384
+ globalThis.__matchesFunctionsPattern = matchesFunctionsPattern;
1385
+ globalThis.__stripSrcPrefix = stripSrcPrefix;
1210
1386
  }
1211
1387
 
1212
1388
  // src/oidc.ts
@@ -1250,6 +1426,7 @@ function parseQueueHeaders(headers) {
1250
1426
  const timestamp = headers.get("Vqs-Timestamp");
1251
1427
  const contentType = headers.get("Content-Type") || "application/octet-stream";
1252
1428
  const receiptHandle = headers.get("Vqs-Receipt-Handle");
1429
+ const expiresAtStr = headers.get("Vqs-Expires-At");
1253
1430
  if (!messageId || !timestamp || !receiptHandle) {
1254
1431
  return null;
1255
1432
  }
@@ -1261,6 +1438,7 @@ function parseQueueHeaders(headers) {
1261
1438
  messageId,
1262
1439
  deliveryCount,
1263
1440
  createdAt: new Date(timestamp),
1441
+ expiresAt: expiresAtStr ? new Date(expiresAtStr) : void 0,
1264
1442
  contentType,
1265
1443
  receiptHandle
1266
1444
  };
@@ -1381,7 +1559,7 @@ var ApiClient = class _ApiClient {
1381
1559
  }
1382
1560
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1383
1561
  }
1384
- init.headers.set("User-Agent", `@vercel/queue/${"0.1.1"}`);
1562
+ init.headers.set("User-Agent", `@vercel/queue/${"0.1.2"}`);
1385
1563
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1386
1564
  const response = await fetch(url, init);
1387
1565
  if (isDebugEnabled()) {
@@ -1756,9 +1934,11 @@ function resolveRegion(region) {
1756
1934
  if (region) return region;
1757
1935
  const fromEnv = process.env.VERCEL_REGION;
1758
1936
  if (fromEnv) return fromEnv;
1759
- console.warn(
1760
- `[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" })`
1761
- );
1937
+ if (!isDevMode()) {
1938
+ console.warn(
1939
+ `[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" })`
1940
+ );
1941
+ }
1762
1942
  return DEFAULT_REGION;
1763
1943
  }
1764
1944
  var QueueClient = class {
@@ -1796,7 +1976,8 @@ var QueueClient = class {
1796
1976
  topicName,
1797
1977
  result.messageId,
1798
1978
  api.getRegion(),
1799
- options?.delaySeconds
1979
+ options?.delaySeconds,
1980
+ options?.retentionSeconds
1800
1981
  );
1801
1982
  }
1802
1983
  return { messageId: result.messageId };