@vercel/queue 0.1.0 → 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.js CHANGED
@@ -51,8 +51,10 @@ __export(index_exports, {
51
51
  QueueEmptyError: () => QueueEmptyError,
52
52
  StreamTransport: () => StreamTransport,
53
53
  UnauthorizedError: () => UnauthorizedError,
54
+ handleCallback: () => handleCallback2,
54
55
  parseCallback: () => parseCallback,
55
- parseRawCallback: () => parseRawCallback
56
+ parseRawCallback: () => parseRawCallback,
57
+ send: () => send
56
58
  });
57
59
  module.exports = __toCommonJS(index_exports);
58
60
 
@@ -134,7 +136,9 @@ var import_mixpart = require("mixpart");
134
136
 
135
137
  // src/dev.ts
136
138
  var fs = __toESM(require("fs"));
139
+ var net = __toESM(require("net"));
137
140
  var path = __toESM(require("path"));
141
+ var import_minimatch = require("minimatch");
138
142
 
139
143
  // src/types.ts
140
144
  var MessageNotFoundError = class extends Error {
@@ -319,8 +323,8 @@ var ConsumerGroup = class {
319
323
  firstDelayMs = 0;
320
324
  }
321
325
  }
322
- const lifecyclePromise = new Promise((resolve) => {
323
- resolveLifecycle = resolve;
326
+ const lifecyclePromise = new Promise((resolve2) => {
327
+ resolveLifecycle = resolve2;
324
328
  });
325
329
  const safeResolve = () => {
326
330
  if (!isResolved) {
@@ -397,11 +401,12 @@ var ConsumerGroup = class {
397
401
  message.receiptHandle,
398
402
  options
399
403
  );
404
+ const DEFAULT_RETENTION_MS = 864e5;
400
405
  const metadata = {
401
406
  messageId: message.messageId,
402
407
  deliveryCount: message.deliveryCount,
403
408
  createdAt: message.createdAt,
404
- expiresAt: message.expiresAt,
409
+ expiresAt: message.expiresAt ?? new Date(message.createdAt.getTime() + DEFAULT_RETENTION_MS),
405
410
  topicName: this.topicName,
406
411
  consumerGroup: this.consumerGroupName,
407
412
  region: this.client.getRegion()
@@ -552,10 +557,12 @@ var Topic = class {
552
557
  headers: options?.headers
553
558
  });
554
559
  if (result.messageId && isDevMode()) {
555
- triggerDevCallbacks(
560
+ invokeDevHandlers(
556
561
  this.topicName,
557
562
  result.messageId,
558
- this.client.getRegion()
563
+ this.client.getRegion(),
564
+ options?.delaySeconds,
565
+ options?.retentionSeconds
559
566
  );
560
567
  }
561
568
  return { messageId: result.messageId };
@@ -761,16 +768,27 @@ async function handleCallback(handler, request, options) {
761
768
  }
762
769
 
763
770
  // src/dev.ts
764
- var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
765
- function filePathToUrlPath(filePath) {
766
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
767
- if (!urlPath.startsWith("/")) {
768
- urlPath = "/" + urlPath;
769
- }
770
- return urlPath;
771
+ var import_meta = {};
772
+ function isDevMode() {
773
+ return process.env.NODE_ENV === "development";
771
774
  }
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;
@@ -792,13 +810,19 @@ function getDevRouteMappings() {
792
810
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
793
811
  if (!config.experimentalTriggers) continue;
794
812
  for (const trigger of config.experimentalTriggers) {
795
- if (trigger.type?.startsWith("queue/") && trigger.topic) {
796
- mappings.push({
797
- urlPath: filePathToUrlPath(filePath),
798
- topic: trigger.topic,
799
- consumer: filePathToConsumerGroup(filePath)
800
- });
813
+ if (!trigger.type?.startsWith("queue/") || !trigger.topic) continue;
814
+ if (trigger.type !== "queue/v2beta") {
815
+ console.warn(
816
+ `[Dev Mode] Unsupported trigger type "${trigger.type}" for topic "${trigger.topic}" in ${filePath}. Use "queue/v2beta" instead.`
817
+ );
818
+ continue;
801
819
  }
820
+ mappings.push({
821
+ filePath,
822
+ topic: trigger.topic,
823
+ consumer: filePathToConsumerGroup(filePath),
824
+ retryAfterSeconds: trigger.retryAfterSeconds
825
+ });
802
826
  }
803
827
  }
804
828
  g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
@@ -811,9 +835,7 @@ function getDevRouteMappings() {
811
835
  }
812
836
  function findMatchingRoutes(topicName) {
813
837
  const mappings = getDevRouteMappings();
814
- if (!mappings) {
815
- return [];
816
- }
838
+ if (!mappings) return [];
817
839
  return mappings.filter((mapping) => {
818
840
  if (mapping.topic.includes("*")) {
819
841
  return matchesWildcardPattern(topicName, mapping.topic);
@@ -821,149 +843,607 @@ function findMatchingRoutes(topicName) {
821
843
  return mapping.topic === topicName;
822
844
  });
823
845
  }
824
- function isDevMode() {
825
- return process.env.NODE_ENV === "development";
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
+ }
860
+ function findMappingsForFile(absolutePath) {
861
+ const mappings = getDevRouteMappings();
862
+ if (!mappings) return [];
863
+ const cwd = process.cwd();
864
+ let relative2;
865
+ try {
866
+ relative2 = path.relative(cwd, absolutePath);
867
+ } catch {
868
+ return [];
869
+ }
870
+ const normalized = relative2.replace(/\\/g, "/");
871
+ const stripped = stripSrcPrefix(normalized);
872
+ return mappings.filter(
873
+ (m) => matchesFunctionsPattern(normalized, m.filePath) || stripped !== null && matchesFunctionsPattern(stripped, m.filePath)
874
+ );
875
+ }
876
+ function parseFrameFilePath(line) {
877
+ let match = line.match(/\((.+?):\d+:\d+\)/);
878
+ if (!match) match = line.match(/at\s+(.+?):\d+:\d+/);
879
+ if (!match) return null;
880
+ let filePath = match[1].trim();
881
+ if (filePath === "native" || filePath.startsWith("node:") || filePath.startsWith("internal")) {
882
+ return null;
883
+ }
884
+ if (filePath.startsWith("file://")) {
885
+ try {
886
+ filePath = new URL(filePath).pathname;
887
+ } catch {
888
+ return null;
889
+ }
890
+ }
891
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(filePath)) {
892
+ return null;
893
+ }
894
+ if (filePath.startsWith("./")) {
895
+ filePath = filePath.slice(2);
896
+ }
897
+ return filePath;
898
+ }
899
+ var _sdkPackageDir;
900
+ function getSdkPackageDir() {
901
+ if (_sdkPackageDir) return _sdkPackageDir;
902
+ try {
903
+ const thisDir = typeof __dirname !== "undefined" ? __dirname : path.dirname(new URL(import_meta.url).pathname);
904
+ _sdkPackageDir = path.resolve(thisDir, "..");
905
+ } catch {
906
+ _sdkPackageDir = "";
907
+ }
908
+ return _sdkPackageDir;
909
+ }
910
+ function extractCallerFilePath() {
911
+ const stack = new Error().stack;
912
+ if (!stack) return null;
913
+ const lines = stack.split("\n").slice(1);
914
+ const pkgDir = getSdkPackageDir();
915
+ for (const line of lines) {
916
+ const fp = parseFrameFilePath(line);
917
+ if (!fp) continue;
918
+ const absolute = path.isAbsolute(fp) ? fp : path.resolve(process.cwd(), fp);
919
+ let realFp;
920
+ try {
921
+ realFp = fs.realpathSync(absolute);
922
+ } catch {
923
+ realFp = absolute;
924
+ }
925
+ if (pkgDir && realFp.startsWith(pkgDir)) continue;
926
+ return realFp;
927
+ }
928
+ return null;
929
+ }
930
+ var HANDLER_REGISTRY_KEY = Symbol.for("@vercel/queue.devHandlerRegistry");
931
+ function getHandlerRegistry() {
932
+ const g = globalThis;
933
+ if (!g[HANDLER_REGISTRY_KEY]) {
934
+ g[HANDLER_REGISTRY_KEY] = /* @__PURE__ */ new Map();
935
+ }
936
+ return g[HANDLER_REGISTRY_KEY];
937
+ }
938
+ function registerHandlerForFile(filePath, handler, client, options) {
939
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
940
+ const fileMappings = findMappingsForFile(absolutePath);
941
+ if (fileMappings.length === 0) return false;
942
+ const registry = getHandlerRegistry();
943
+ for (const mapping of fileMappings) {
944
+ const key = mapping.topic;
945
+ const existing = registry.get(key) ?? [];
946
+ const nextEntry = {
947
+ consumerGroup: mapping.consumer,
948
+ handler,
949
+ client,
950
+ options
951
+ };
952
+ const existingIndex = existing.findIndex(
953
+ (e) => e.consumerGroup === mapping.consumer
954
+ );
955
+ if (existingIndex >= 0) {
956
+ existing[existingIndex] = nextEntry;
957
+ } else {
958
+ existing.push(nextEntry);
959
+ }
960
+ registry.set(key, existing);
961
+ }
962
+ return true;
963
+ }
964
+ function registerDevHandler(handler, client, options, _testCallerPath) {
965
+ const callerPath = _testCallerPath ?? extractCallerFilePath();
966
+ if (!callerPath) {
967
+ console.warn(
968
+ "[Dev Mode] Could not determine caller file path for handler registration."
969
+ );
970
+ return;
971
+ }
972
+ const registered = registerHandlerForFile(
973
+ callerPath,
974
+ handler,
975
+ client,
976
+ options
977
+ );
978
+ if (!registered) {
979
+ const allMappings = getDevRouteMappings();
980
+ if (allMappings && allMappings.length > 0) {
981
+ return;
982
+ }
983
+ const cwd = process.cwd();
984
+ let relative2;
985
+ try {
986
+ relative2 = path.relative(cwd, callerPath).replace(/\\/g, "/");
987
+ } catch {
988
+ relative2 = callerPath;
989
+ }
990
+ console.warn(
991
+ `[Dev Mode] handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
992
+
993
+ Add a trigger to vercel.json:
994
+ "${relative2}": {
995
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "your-topic" }]
996
+ }`
997
+ );
998
+ }
999
+ }
1000
+ function lookupHandlers(topicName) {
1001
+ const registry = getHandlerRegistry();
1002
+ const result = [];
1003
+ for (const [pattern, handlers] of registry) {
1004
+ const matches = pattern.includes("*") ? matchesWildcardPattern(topicName, pattern) : pattern === topicName;
1005
+ if (matches) {
1006
+ result.push(...handlers);
1007
+ }
1008
+ }
1009
+ return result;
1010
+ }
1011
+ var DEV_RETRY_INITIAL_DELAY_MS = 50;
1012
+ var DEV_RETRY_MAX_WAIT_MS = 5e3;
1013
+ var DEV_RETRY_BACKOFF = 2;
1014
+ var PORT_CHECK_TIMEOUT_MS = 250;
1015
+ var PRIME_PORT_ENV_KEYS = [
1016
+ "PORT",
1017
+ "NEXT_PORT",
1018
+ "NEXTJS_PORT",
1019
+ "NUXT_PORT",
1020
+ "NITRO_PORT",
1021
+ "SVELTEKIT_PORT",
1022
+ "VITE_PORT",
1023
+ "DEV_PORT",
1024
+ "npm_config_port"
1025
+ ];
1026
+ var PRIME_URL_ENV_KEYS = [
1027
+ "__NEXT_PRIVATE_ORIGIN",
1028
+ "NUXT_PUBLIC_SITE_URL",
1029
+ "URL"
1030
+ ];
1031
+ function formatErrorReason(error) {
1032
+ if (error instanceof Error) {
1033
+ return error.message;
1034
+ }
1035
+ return String(error);
1036
+ }
1037
+ function isMessageNotFoundError(error) {
1038
+ if (error instanceof MessageNotFoundError) {
1039
+ return true;
1040
+ }
1041
+ if (error instanceof Error && error.name === "MessageNotFoundError") {
1042
+ return true;
1043
+ }
1044
+ return false;
1045
+ }
1046
+ function parsePort(value) {
1047
+ if (!value) return null;
1048
+ const parsed = Number.parseInt(value, 10);
1049
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) return null;
1050
+ return parsed;
1051
+ }
1052
+ function parsePortFromUrl(value) {
1053
+ if (!value) return null;
1054
+ try {
1055
+ const parsed = new URL(value).port;
1056
+ return parsePort(parsed);
1057
+ } catch {
1058
+ return null;
1059
+ }
1060
+ }
1061
+ function collectPrimePorts() {
1062
+ const result = [];
1063
+ const seen = /* @__PURE__ */ new Set();
1064
+ const add = (port) => {
1065
+ if (port && !seen.has(port)) {
1066
+ seen.add(port);
1067
+ result.push(port);
1068
+ }
1069
+ };
1070
+ for (const key of PRIME_PORT_ENV_KEYS) {
1071
+ add(parsePort(process.env[key]));
1072
+ }
1073
+ for (const key of PRIME_URL_ENV_KEYS) {
1074
+ add(parsePortFromUrl(process.env[key]));
1075
+ }
1076
+ return result;
1077
+ }
1078
+ function isPortListening(port) {
1079
+ return new Promise((resolve2) => {
1080
+ const socket = net.connect({ host: "localhost", port });
1081
+ let settled = false;
1082
+ const finish = (listening) => {
1083
+ if (settled) return;
1084
+ settled = true;
1085
+ socket.destroy();
1086
+ resolve2(listening);
1087
+ };
1088
+ socket.once("connect", () => finish(true));
1089
+ socket.once("error", () => finish(false));
1090
+ socket.setTimeout(PORT_CHECK_TIMEOUT_MS, () => finish(false));
1091
+ });
826
1092
  }
827
- var DEV_VISIBILITY_POLL_INTERVAL = 50;
828
- var DEV_VISIBILITY_MAX_WAIT = 5e3;
829
- var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
830
- async function waitForMessageVisibility(topicName, consumerGroup, messageId, region) {
831
- const client = new ApiClient({ region });
1093
+ async function invokeWithRetry(handler, request, options) {
832
1094
  let elapsed = 0;
833
- let interval = DEV_VISIBILITY_POLL_INTERVAL;
834
- while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
1095
+ let delay = DEV_RETRY_INITIAL_DELAY_MS;
1096
+ while (true) {
835
1097
  try {
836
- await client.receiveMessageById({
837
- queueName: topicName,
838
- consumerGroup,
839
- messageId,
840
- visibilityTimeoutSeconds: 0
841
- });
842
- return true;
1098
+ await handleCallback(handler, request, options);
1099
+ return;
843
1100
  } catch (error) {
844
- if (error instanceof MessageNotFoundError) {
845
- await new Promise((resolve) => setTimeout(resolve, interval));
846
- elapsed += interval;
847
- interval = Math.min(
848
- interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
849
- DEV_VISIBILITY_MAX_WAIT - elapsed
1101
+ if (isMessageNotFoundError(error) && elapsed < DEV_RETRY_MAX_WAIT_MS) {
1102
+ await new Promise((r) => setTimeout(r, delay));
1103
+ elapsed += delay;
1104
+ delay = Math.min(
1105
+ delay * DEV_RETRY_BACKOFF,
1106
+ DEV_RETRY_MAX_WAIT_MS - elapsed
850
1107
  );
851
1108
  continue;
852
1109
  }
853
- if (error instanceof MessageAlreadyProcessedError) {
854
- console.log(
855
- `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
856
- );
857
- return false;
1110
+ throw error;
1111
+ }
1112
+ }
1113
+ }
1114
+ function filePathToUrlPath(filePath) {
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)$/, "");
1116
+ if (!urlPath.startsWith("/")) {
1117
+ urlPath = "/" + urlPath;
1118
+ }
1119
+ return urlPath;
1120
+ }
1121
+ async function ensureHandlersLoaded(topicName, options = {}) {
1122
+ const diagnostics = {
1123
+ triedPorts: collectPrimePorts(),
1124
+ listeningPorts: [],
1125
+ unavailablePorts: [],
1126
+ importFailures: [],
1127
+ primeFailures: []
1128
+ };
1129
+ const matchingRoutes = findMatchingRoutes(topicName);
1130
+ if (matchingRoutes.length === 0) return diagnostics;
1131
+ const shouldRefreshRegistered = options.refreshRegistered === true;
1132
+ for (const port of diagnostics.triedPorts) {
1133
+ if (await isPortListening(port)) {
1134
+ diagnostics.listeningPorts.push(port);
1135
+ } else {
1136
+ diagnostics.unavailablePorts.push(port);
1137
+ }
1138
+ }
1139
+ for (const route of matchingRoutes) {
1140
+ const alreadyRegistered = isHandlerRegistered(topicName, route.consumer);
1141
+ if (alreadyRegistered && !shouldRefreshRegistered) {
1142
+ continue;
1143
+ }
1144
+ if (!alreadyRegistered) {
1145
+ const absolutePath = path.resolve(process.cwd(), route.filePath);
1146
+ try {
1147
+ await import(absolutePath);
1148
+ } catch (error) {
1149
+ diagnostics.importFailures.push({
1150
+ filePath: route.filePath,
1151
+ reason: formatErrorReason(error)
1152
+ });
1153
+ }
1154
+ if (isHandlerRegistered(topicName, route.consumer)) continue;
1155
+ }
1156
+ for (const port of diagnostics.listeningPorts) {
1157
+ const url = `http://localhost:${port}${filePathToUrlPath(route.filePath)}`;
1158
+ try {
1159
+ const response = await fetch(url, {
1160
+ method: "POST",
1161
+ headers: {
1162
+ "x-vercel-queue-prime": "1",
1163
+ "x-vercel-queue-prime-file": route.filePath
1164
+ }
1165
+ });
1166
+ try {
1167
+ await response.text();
1168
+ } catch {
1169
+ }
1170
+ if (isHandlerRegistered(topicName, route.consumer)) {
1171
+ break;
1172
+ }
1173
+ diagnostics.primeFailures.push({
1174
+ filePath: route.filePath,
1175
+ url,
1176
+ reason: `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`.trim()
1177
+ });
1178
+ } catch (error) {
1179
+ diagnostics.primeFailures.push({
1180
+ filePath: route.filePath,
1181
+ url,
1182
+ reason: formatErrorReason(error)
1183
+ });
858
1184
  }
859
- console.error(
860
- `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
861
- error
862
- );
863
- return false;
864
1185
  }
865
1186
  }
866
- console.warn(
867
- `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
1187
+ return diagnostics;
1188
+ }
1189
+ function buildNoHandlerWarning(topicName, routes, diagnostics) {
1190
+ const files = routes.map((r) => r.filePath);
1191
+ const suggestedPort = diagnostics.listeningPorts[0] ?? diagnostics.triedPorts[0];
1192
+ const suggestedUrls = suggestedPort ? routes.map(
1193
+ (r) => `http://localhost:${suggestedPort}${filePathToUrlPath(r.filePath)}`
1194
+ ) : [];
1195
+ let portSummary;
1196
+ if (diagnostics.triedPorts.length === 0) {
1197
+ portSummary = "No local dev port detected from env. Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT).";
1198
+ } else if (diagnostics.listeningPorts.length === 0) {
1199
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}], but none are listening.`;
1200
+ } else {
1201
+ const unavailable = diagnostics.unavailablePorts.length > 0 ? ` Not listening: [${diagnostics.unavailablePorts.join(", ")}].` : "";
1202
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}]. Listening: [${diagnostics.listeningPorts.join(", ")}].` + unavailable;
1203
+ }
1204
+ const importSummary = diagnostics.importFailures.length > 0 ? `
1205
+ Import failures: ` + diagnostics.importFailures.slice(0, 2).map((f) => `${f.filePath} (${f.reason})`).join("; ") : "";
1206
+ const primeSummary = diagnostics.primeFailures.length > 0 ? `
1207
+ Prime failures: ` + diagnostics.primeFailures.slice(0, 3).map((f) => `${f.url} (${f.reason})`).join("; ") : "";
1208
+ return `[Dev Mode] No registered handler for topic "${topicName}". vercel.json maps this topic to [${files.join(", ")}] but auto-loading failed.
1209
+ ${portSummary}${importSummary}${primeSummary}
1210
+ Ensure your dev server is running, set PORT if needed, and confirm mapped route files call handleCallback()/handleNodeCallback() at module scope.
1211
+ ` + (suggestedUrls.length > 0 ? `Try opening: ${suggestedUrls.join(" or ")}` : "Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT) and try sending again.");
1212
+ }
1213
+ function isHandlerRegistered(topicName, consumerGroup) {
1214
+ return lookupHandlers(topicName).some(
1215
+ (h) => h.consumerGroup === consumerGroup
868
1216
  );
869
- return false;
870
1217
  }
871
- function triggerDevCallbacks(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) {
872
1301
  if (delaySeconds && delaySeconds > 0) {
873
1302
  console.log(
874
1303
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
875
1304
  );
876
1305
  setTimeout(() => {
877
- triggerDevCallbacks(topicName, messageId, region);
1306
+ invokeDevHandlers(
1307
+ topicName,
1308
+ messageId,
1309
+ region,
1310
+ void 0,
1311
+ retentionSeconds
1312
+ );
878
1313
  }, delaySeconds * 1e3);
879
1314
  return;
880
1315
  }
881
1316
  console.log(
882
1317
  `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
883
1318
  );
884
- const matchingRoutes = findMatchingRoutes(topicName);
885
- if (matchingRoutes.length === 0) {
886
- console.log(
887
- `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
888
- );
889
- return;
890
- }
891
- const consumerGroups = matchingRoutes.map((r) => r.consumer);
892
- console.log(
893
- `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
894
- );
895
1319
  (async () => {
896
- const firstRoute = matchingRoutes[0];
897
- const isVisible = await waitForMessageVisibility(
898
- topicName,
899
- firstRoute.consumer,
900
- messageId,
901
- region
902
- );
903
- if (!isVisible) {
904
- console.warn(
905
- `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
906
- );
1320
+ let handlers = lookupHandlers(topicName);
1321
+ let diagnostics = null;
1322
+ if (handlers.length > 0) {
1323
+ await ensureHandlersLoaded(topicName, { refreshRegistered: true });
1324
+ handlers = lookupHandlers(topicName);
1325
+ } else {
1326
+ diagnostics = await ensureHandlersLoaded(topicName);
1327
+ handlers = lookupHandlers(topicName);
1328
+ }
1329
+ if (handlers.length === 0) {
1330
+ const matchingRoutes = findMatchingRoutes(topicName);
1331
+ if (matchingRoutes.length > 0) {
1332
+ const safeDiagnostics = diagnostics ?? {
1333
+ triedPorts: collectPrimePorts(),
1334
+ listeningPorts: [],
1335
+ unavailablePorts: [],
1336
+ importFailures: [],
1337
+ primeFailures: []
1338
+ };
1339
+ console.warn(
1340
+ buildNoHandlerWarning(topicName, matchingRoutes, safeDiagnostics)
1341
+ );
1342
+ } else {
1343
+ console.warn(
1344
+ `[Dev Mode] No registered handler for topic "${topicName}".
1345
+ Ensure vercel.json has a matching experimentalTriggers entry and the route file calls handleCallback().`
1346
+ );
1347
+ }
907
1348
  return;
908
1349
  }
909
- const port = process.env.PORT || 3e3;
910
- const baseUrl = `http://localhost:${port}`;
911
- for (const route of matchingRoutes) {
912
- const url = `${baseUrl}${route.urlPath}`;
913
- console.log(
914
- `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
1350
+ const consumerGroups = handlers.map((h) => h.consumerGroup);
1351
+ console.log(
1352
+ `[Dev Mode] Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
1353
+ );
1354
+ const effectiveRetention = retentionSeconds ?? DEFAULT_RETENTION_S;
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;
1382
+ const request = {
1383
+ queueName: topicName,
1384
+ consumerGroup: entry.consumerGroup,
1385
+ messageId,
1386
+ region
1387
+ };
1388
+ const callbackOptions = {
1389
+ client: entry.client,
1390
+ visibilityTimeoutSeconds: entry.options?.visibilityTimeoutSeconds,
1391
+ retry: wrappedRetry
1392
+ };
1393
+ const consumerDefaultDelay = Math.min(
1394
+ findRetryAfterSeconds(topicName, entry.consumerGroup) ?? DEV_REDELIVERY_DEFAULT_DELAY_S,
1395
+ DEV_REDELIVERY_MAX_DELAY_S
915
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
+ });
916
1410
  try {
917
- const response = await fetch(url, {
918
- method: "POST",
919
- headers: {
920
- "ce-type": CLOUD_EVENT_TYPE_V2BETA,
921
- "ce-vqsqueuename": topicName,
922
- "ce-vqsconsumergroup": route.consumer,
923
- "ce-vqsmessageid": messageId,
924
- "ce-vqsregion": region
925
- }
926
- });
927
- if (response.ok) {
928
- try {
929
- const responseData = await response.json();
930
- if (responseData.status === "success") {
931
- console.log(
932
- `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
933
- );
934
- }
935
- } catch {
936
- console.warn(
937
- `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
938
- );
939
- }
940
- } else {
941
- try {
942
- const errorData = await response.json();
943
- console.error(
944
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
945
- );
946
- } catch {
947
- console.error(
948
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
949
- );
950
- }
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);
951
1423
  }
952
1424
  } catch (error) {
953
1425
  console.error(
954
- `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
1426
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
955
1427
  error
956
1428
  );
1429
+ if (!handlerSucceeded) {
1430
+ scheduleDevRedelivery(buildRedeliveryCtx(), consumerDefaultDelay);
1431
+ }
957
1432
  }
958
1433
  }
959
1434
  })();
960
1435
  }
961
- function clearDevRouteMappings() {
1436
+ function clearDevState() {
962
1437
  const g = globalThis;
963
1438
  delete g[ROUTE_MAPPINGS_KEY];
1439
+ delete g[HANDLER_REGISTRY_KEY];
964
1440
  }
965
1441
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
966
- globalThis.__clearDevRouteMappings = clearDevRouteMappings;
1442
+ globalThis.__clearDevState = clearDevState;
1443
+ globalThis.__filePathToConsumerGroup = filePathToConsumerGroup;
1444
+ globalThis.__filePathToUrlPath = filePathToUrlPath;
1445
+ globalThis.__matchesFunctionsPattern = matchesFunctionsPattern;
1446
+ globalThis.__stripSrcPrefix = stripSrcPrefix;
967
1447
  }
968
1448
 
969
1449
  // src/oidc.ts
@@ -1007,6 +1487,7 @@ function parseQueueHeaders(headers) {
1007
1487
  const timestamp = headers.get("Vqs-Timestamp");
1008
1488
  const contentType = headers.get("Content-Type") || "application/octet-stream";
1009
1489
  const receiptHandle = headers.get("Vqs-Receipt-Handle");
1490
+ const expiresAtStr = headers.get("Vqs-Expires-At");
1010
1491
  if (!messageId || !timestamp || !receiptHandle) {
1011
1492
  return null;
1012
1493
  }
@@ -1018,6 +1499,7 @@ function parseQueueHeaders(headers) {
1018
1499
  messageId,
1019
1500
  deliveryCount,
1020
1501
  createdAt: new Date(timestamp),
1502
+ expiresAt: expiresAtStr ? new Date(expiresAtStr) : void 0,
1021
1503
  contentType,
1022
1504
  receiptHandle
1023
1505
  };
@@ -1138,7 +1620,7 @@ var ApiClient = class _ApiClient {
1138
1620
  }
1139
1621
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1140
1622
  }
1141
- init.headers.set("User-Agent", `@vercel/queue/${"0.1.0"}`);
1623
+ init.headers.set("User-Agent", `@vercel/queue/${"0.1.2"}`);
1142
1624
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1143
1625
  const response = await fetch(url, init);
1144
1626
  if (isDebugEnabled()) {
@@ -1474,12 +1956,36 @@ var ApiClient = class _ApiClient {
1474
1956
 
1475
1957
  // src/client.ts
1476
1958
  var apiClients = /* @__PURE__ */ new WeakMap();
1959
+ var API_CLIENT_KEY = Symbol.for("@vercel/queue.apiClient");
1960
+ function setApi(client, api) {
1961
+ apiClients.set(client, api);
1962
+ Object.defineProperty(client, API_CLIENT_KEY, {
1963
+ value: api,
1964
+ writable: false,
1965
+ enumerable: false,
1966
+ configurable: false
1967
+ });
1968
+ }
1477
1969
  function getApi(client) {
1478
1970
  const api = apiClients.get(client);
1479
- if (!api) {
1480
- throw new Error("Client not initialized");
1971
+ if (api) {
1972
+ return api;
1481
1973
  }
1482
- return api;
1974
+ const apiFromSymbol = client[API_CLIENT_KEY];
1975
+ if (typeof apiFromSymbol === "object" && apiFromSymbol !== null) {
1976
+ const resolvedApi = apiFromSymbol;
1977
+ apiClients.set(client, resolvedApi);
1978
+ return resolvedApi;
1979
+ }
1980
+ throw new Error(
1981
+ "QueueClient not initialized. This may happen when multiple bundled copies of @vercel/queue are loaded in local dev."
1982
+ );
1983
+ }
1984
+ function resolveCallbackRequest(input) {
1985
+ if ("request" in input) {
1986
+ return input.request;
1987
+ }
1988
+ return input;
1483
1989
  }
1484
1990
  function getApiClient(client) {
1485
1991
  return getApi(client);
@@ -1489,15 +1995,17 @@ function resolveRegion(region) {
1489
1995
  if (region) return region;
1490
1996
  const fromEnv = process.env.VERCEL_REGION;
1491
1997
  if (fromEnv) return fromEnv;
1492
- console.warn(
1493
- `[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" })`
1494
- );
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
+ }
1495
2003
  return DEFAULT_REGION;
1496
2004
  }
1497
2005
  var QueueClient = class {
1498
2006
  constructor(options = {}) {
1499
2007
  const region = resolveRegion(options.region);
1500
- apiClients.set(this, new ApiClient({ ...options, region }));
2008
+ setApi(this, new ApiClient({ ...options, region }));
1501
2009
  }
1502
2010
  /**
1503
2011
  * Send a message to a topic.
@@ -1525,11 +2033,12 @@ var QueueClient = class {
1525
2033
  headers: options?.headers
1526
2034
  });
1527
2035
  if (result.messageId && isDevMode()) {
1528
- triggerDevCallbacks(
2036
+ invokeDevHandlers(
1529
2037
  topicName,
1530
2038
  result.messageId,
1531
2039
  api.getRegion(),
1532
- options?.delaySeconds
2040
+ options?.delaySeconds,
2041
+ options?.retentionSeconds
1533
2042
  );
1534
2043
  }
1535
2044
  return { messageId: result.messageId };
@@ -1551,10 +2060,26 @@ var QueueClient = class {
1551
2060
  * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1552
2061
  * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1553
2062
  * reschedule the message for redelivery after N seconds.
1554
- * @returns A `(request: Request) => Promise<Response>` route handler
2063
+ * @returns A route handler that accepts either `Request` or `{ request: Request }`
1555
2064
  */
1556
2065
  handleCallback = (handler, options) => {
1557
- return async (request) => {
2066
+ if (isDevMode()) {
2067
+ registerDevHandler(handler, this, options);
2068
+ }
2069
+ return async (requestOrEvent) => {
2070
+ const request = resolveCallbackRequest(requestOrEvent);
2071
+ if (isDevMode() && request.headers.get("x-vercel-queue-prime") === "1") {
2072
+ const primeFile = request.headers.get("x-vercel-queue-prime-file");
2073
+ if (primeFile) {
2074
+ registerDevHandler(
2075
+ handler,
2076
+ this,
2077
+ options,
2078
+ primeFile
2079
+ );
2080
+ }
2081
+ return Response.json({ status: "primed" });
2082
+ }
1558
2083
  try {
1559
2084
  const parsed = await parseCallback(request);
1560
2085
  await handleCallback(handler, parsed, {
@@ -1596,11 +2121,29 @@ var QueueClient = class {
1596
2121
  * @returns A `(req, res) => Promise<void>` route handler
1597
2122
  */
1598
2123
  handleNodeCallback = (handler, options) => {
2124
+ if (isDevMode()) {
2125
+ registerDevHandler(handler, this, options);
2126
+ }
1599
2127
  return async (req, res) => {
1600
2128
  if (req.method !== "POST") {
1601
2129
  res.status(200).end();
1602
2130
  return;
1603
2131
  }
2132
+ const primeHeader = req.headers["x-vercel-queue-prime"];
2133
+ if (isDevMode() && primeHeader === "1") {
2134
+ const primeFileHeader = req.headers["x-vercel-queue-prime-file"];
2135
+ const primeFile = Array.isArray(primeFileHeader) ? primeFileHeader[0] : primeFileHeader;
2136
+ if (primeFile) {
2137
+ registerDevHandler(
2138
+ handler,
2139
+ this,
2140
+ options,
2141
+ primeFile
2142
+ );
2143
+ }
2144
+ res.status(200).json({ status: "primed" });
2145
+ return;
2146
+ }
1604
2147
  try {
1605
2148
  const parsed = parseRawCallback(req.body, req.headers);
1606
2149
  await handleCallback(handler, parsed, {
@@ -1622,7 +2165,7 @@ var QueueClient = class {
1622
2165
  };
1623
2166
  var PollingQueueClient = class {
1624
2167
  constructor(options) {
1625
- apiClients.set(this, new ApiClient(options));
2168
+ setApi(this, new ApiClient(options));
1626
2169
  }
1627
2170
  /**
1628
2171
  * Send a message to a topic.
@@ -1721,6 +2264,25 @@ var PollingQueueClient = class {
1721
2264
  }
1722
2265
  };
1723
2266
  };
2267
+
2268
+ // src/default-client.ts
2269
+ var _defaultClient;
2270
+ function getDefaultClient() {
2271
+ if (!_defaultClient) {
2272
+ _defaultClient = new QueueClient();
2273
+ }
2274
+ return _defaultClient;
2275
+ }
2276
+ function resolveClient(region) {
2277
+ if (!region) return getDefaultClient();
2278
+ return new QueueClient({ region });
2279
+ }
2280
+ async function send(topicName, payload, options) {
2281
+ return resolveClient(options?.region).send(topicName, payload, options);
2282
+ }
2283
+ function handleCallback2(handler, options) {
2284
+ return getDefaultClient().handleCallback(handler, options);
2285
+ }
1724
2286
  // Annotate the CommonJS export names for ESM import in node:
1725
2287
  0 && (module.exports = {
1726
2288
  BadRequestError,
@@ -1744,7 +2306,9 @@ var PollingQueueClient = class {
1744
2306
  QueueEmptyError,
1745
2307
  StreamTransport,
1746
2308
  UnauthorizedError,
2309
+ handleCallback,
1747
2310
  parseCallback,
1748
- parseRawCallback
2311
+ parseRawCallback,
2312
+ send
1749
2313
  });
1750
2314
  //# sourceMappingURL=index.js.map