@vercel/queue 0.1.0 → 0.1.1

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,6 +136,7 @@ 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"));
138
141
 
139
142
  // src/types.ts
@@ -319,8 +322,8 @@ var ConsumerGroup = class {
319
322
  firstDelayMs = 0;
320
323
  }
321
324
  }
322
- const lifecyclePromise = new Promise((resolve) => {
323
- resolveLifecycle = resolve;
325
+ const lifecyclePromise = new Promise((resolve2) => {
326
+ resolveLifecycle = resolve2;
324
327
  });
325
328
  const safeResolve = () => {
326
329
  if (!isResolved) {
@@ -552,7 +555,7 @@ var Topic = class {
552
555
  headers: options?.headers
553
556
  });
554
557
  if (result.messageId && isDevMode()) {
555
- triggerDevCallbacks(
558
+ invokeDevHandlers(
556
559
  this.topicName,
557
560
  result.messageId,
558
561
  this.client.getRegion()
@@ -761,14 +764,11 @@ async function handleCallback(handler, request, options) {
761
764
  }
762
765
 
763
766
  // 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;
767
+ var import_meta = {};
768
+ function isDevMode() {
769
+ return process.env.NODE_ENV === "development";
771
770
  }
771
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
772
772
  function filePathToConsumerGroup(filePath) {
773
773
  return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
774
774
  }
@@ -792,13 +792,18 @@ function getDevRouteMappings() {
792
792
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
793
793
  if (!config.experimentalTriggers) continue;
794
794
  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
- });
795
+ if (!trigger.type?.startsWith("queue/") || !trigger.topic) continue;
796
+ if (trigger.type !== "queue/v2beta") {
797
+ console.warn(
798
+ `[Dev Mode] Unsupported trigger type "${trigger.type}" for topic "${trigger.topic}" in ${filePath}. Use "queue/v2beta" instead.`
799
+ );
800
+ continue;
801
801
  }
802
+ mappings.push({
803
+ filePath,
804
+ topic: trigger.topic,
805
+ consumer: filePathToConsumerGroup(filePath)
806
+ });
802
807
  }
803
808
  }
804
809
  g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
@@ -811,9 +816,7 @@ function getDevRouteMappings() {
811
816
  }
812
817
  function findMatchingRoutes(topicName) {
813
818
  const mappings = getDevRouteMappings();
814
- if (!mappings) {
815
- return [];
816
- }
819
+ if (!mappings) return [];
817
820
  return mappings.filter((mapping) => {
818
821
  if (mapping.topic.includes("*")) {
819
822
  return matchesWildcardPattern(topicName, mapping.topic);
@@ -821,149 +824,450 @@ function findMatchingRoutes(topicName) {
821
824
  return mapping.topic === topicName;
822
825
  });
823
826
  }
824
- function isDevMode() {
825
- return process.env.NODE_ENV === "development";
827
+ function findMappingsForFile(absolutePath) {
828
+ const mappings = getDevRouteMappings();
829
+ if (!mappings) return [];
830
+ const cwd = process.cwd();
831
+ let relative2;
832
+ try {
833
+ relative2 = path.relative(cwd, absolutePath);
834
+ } catch {
835
+ return [];
836
+ }
837
+ const normalized = relative2.replace(/\\/g, "/");
838
+ return mappings.filter((m) => m.filePath === normalized);
839
+ }
840
+ function parseFrameFilePath(line) {
841
+ let match = line.match(/\((.+?):\d+:\d+\)/);
842
+ if (!match) match = line.match(/at\s+(.+?):\d+:\d+/);
843
+ if (!match) return null;
844
+ let filePath = match[1].trim();
845
+ if (filePath === "native" || filePath.startsWith("node:") || filePath.startsWith("internal")) {
846
+ return null;
847
+ }
848
+ if (filePath.startsWith("file://")) {
849
+ try {
850
+ filePath = new URL(filePath).pathname;
851
+ } catch {
852
+ return null;
853
+ }
854
+ }
855
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(filePath)) {
856
+ return null;
857
+ }
858
+ if (filePath.startsWith("./")) {
859
+ filePath = filePath.slice(2);
860
+ }
861
+ return filePath;
862
+ }
863
+ var _sdkPackageDir;
864
+ function getSdkPackageDir() {
865
+ if (_sdkPackageDir) return _sdkPackageDir;
866
+ try {
867
+ const thisDir = typeof __dirname !== "undefined" ? __dirname : path.dirname(new URL(import_meta.url).pathname);
868
+ _sdkPackageDir = path.resolve(thisDir, "..");
869
+ } catch {
870
+ _sdkPackageDir = "";
871
+ }
872
+ return _sdkPackageDir;
873
+ }
874
+ function extractCallerFilePath() {
875
+ const stack = new Error().stack;
876
+ if (!stack) return null;
877
+ const lines = stack.split("\n").slice(1);
878
+ const pkgDir = getSdkPackageDir();
879
+ for (const line of lines) {
880
+ const fp = parseFrameFilePath(line);
881
+ if (!fp) continue;
882
+ const absolute = path.isAbsolute(fp) ? fp : path.resolve(process.cwd(), fp);
883
+ let realFp;
884
+ try {
885
+ realFp = fs.realpathSync(absolute);
886
+ } catch {
887
+ realFp = absolute;
888
+ }
889
+ if (pkgDir && realFp.startsWith(pkgDir)) continue;
890
+ return realFp;
891
+ }
892
+ return null;
893
+ }
894
+ var HANDLER_REGISTRY_KEY = Symbol.for("@vercel/queue.devHandlerRegistry");
895
+ function getHandlerRegistry() {
896
+ const g = globalThis;
897
+ if (!g[HANDLER_REGISTRY_KEY]) {
898
+ g[HANDLER_REGISTRY_KEY] = /* @__PURE__ */ new Map();
899
+ }
900
+ return g[HANDLER_REGISTRY_KEY];
901
+ }
902
+ function registerHandlerForFile(filePath, handler, client, options) {
903
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
904
+ const fileMappings = findMappingsForFile(absolutePath);
905
+ if (fileMappings.length === 0) return false;
906
+ const registry = getHandlerRegistry();
907
+ for (const mapping of fileMappings) {
908
+ const key = mapping.topic;
909
+ const existing = registry.get(key) ?? [];
910
+ const nextEntry = {
911
+ consumerGroup: mapping.consumer,
912
+ handler,
913
+ client,
914
+ options
915
+ };
916
+ const existingIndex = existing.findIndex(
917
+ (e) => e.consumerGroup === mapping.consumer
918
+ );
919
+ if (existingIndex >= 0) {
920
+ existing[existingIndex] = nextEntry;
921
+ } else {
922
+ existing.push(nextEntry);
923
+ }
924
+ registry.set(key, existing);
925
+ }
926
+ return true;
826
927
  }
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 });
928
+ function registerDevHandler(handler, client, options, _testCallerPath) {
929
+ const callerPath = _testCallerPath ?? extractCallerFilePath();
930
+ if (!callerPath) {
931
+ console.warn(
932
+ "[Dev Mode] Could not determine caller file path for handler registration."
933
+ );
934
+ return;
935
+ }
936
+ const registered = registerHandlerForFile(
937
+ callerPath,
938
+ handler,
939
+ client,
940
+ options
941
+ );
942
+ if (!registered) {
943
+ const allMappings = getDevRouteMappings();
944
+ const cwd = process.cwd();
945
+ let relative2;
946
+ try {
947
+ relative2 = path.relative(cwd, callerPath).replace(/\\/g, "/");
948
+ } catch {
949
+ relative2 = callerPath;
950
+ }
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
+ console.warn(
963
+ `[Dev Mode] handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
964
+
965
+ Add a trigger to vercel.json:
966
+ "${relative2}": {
967
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "your-topic" }]
968
+ }`
969
+ );
970
+ }
971
+ }
972
+ function lookupHandlers(topicName) {
973
+ const registry = getHandlerRegistry();
974
+ const result = [];
975
+ for (const [pattern, handlers] of registry) {
976
+ const matches = pattern.includes("*") ? matchesWildcardPattern(topicName, pattern) : pattern === topicName;
977
+ if (matches) {
978
+ result.push(...handlers);
979
+ }
980
+ }
981
+ return result;
982
+ }
983
+ var DEV_RETRY_INITIAL_DELAY_MS = 50;
984
+ var DEV_RETRY_MAX_WAIT_MS = 5e3;
985
+ var DEV_RETRY_BACKOFF = 2;
986
+ var PORT_CHECK_TIMEOUT_MS = 250;
987
+ var PRIME_PORT_ENV_KEYS = [
988
+ "PORT",
989
+ "NEXT_PORT",
990
+ "NEXTJS_PORT",
991
+ "NUXT_PORT",
992
+ "NITRO_PORT",
993
+ "SVELTEKIT_PORT",
994
+ "VITE_PORT",
995
+ "DEV_PORT",
996
+ "npm_config_port"
997
+ ];
998
+ var PRIME_URL_ENV_KEYS = [
999
+ "__NEXT_PRIVATE_ORIGIN",
1000
+ "NUXT_PUBLIC_SITE_URL",
1001
+ "URL"
1002
+ ];
1003
+ function formatErrorReason(error) {
1004
+ if (error instanceof Error) {
1005
+ return error.message;
1006
+ }
1007
+ return String(error);
1008
+ }
1009
+ function isMessageNotFoundError(error) {
1010
+ if (error instanceof MessageNotFoundError) {
1011
+ return true;
1012
+ }
1013
+ if (error instanceof Error && error.name === "MessageNotFoundError") {
1014
+ return true;
1015
+ }
1016
+ return false;
1017
+ }
1018
+ function parsePort(value) {
1019
+ if (!value) return null;
1020
+ const parsed = Number.parseInt(value, 10);
1021
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) return null;
1022
+ return parsed;
1023
+ }
1024
+ function parsePortFromUrl(value) {
1025
+ if (!value) return null;
1026
+ try {
1027
+ const parsed = new URL(value).port;
1028
+ return parsePort(parsed);
1029
+ } catch {
1030
+ return null;
1031
+ }
1032
+ }
1033
+ function collectPrimePorts() {
1034
+ const result = [];
1035
+ const seen = /* @__PURE__ */ new Set();
1036
+ const add = (port) => {
1037
+ if (port && !seen.has(port)) {
1038
+ seen.add(port);
1039
+ result.push(port);
1040
+ }
1041
+ };
1042
+ for (const key of PRIME_PORT_ENV_KEYS) {
1043
+ add(parsePort(process.env[key]));
1044
+ }
1045
+ for (const key of PRIME_URL_ENV_KEYS) {
1046
+ add(parsePortFromUrl(process.env[key]));
1047
+ }
1048
+ return result;
1049
+ }
1050
+ function isPortListening(port) {
1051
+ return new Promise((resolve2) => {
1052
+ const socket = net.connect({ host: "localhost", port });
1053
+ let settled = false;
1054
+ const finish = (listening) => {
1055
+ if (settled) return;
1056
+ settled = true;
1057
+ socket.destroy();
1058
+ resolve2(listening);
1059
+ };
1060
+ socket.once("connect", () => finish(true));
1061
+ socket.once("error", () => finish(false));
1062
+ socket.setTimeout(PORT_CHECK_TIMEOUT_MS, () => finish(false));
1063
+ });
1064
+ }
1065
+ async function invokeWithRetry(handler, request, options) {
832
1066
  let elapsed = 0;
833
- let interval = DEV_VISIBILITY_POLL_INTERVAL;
834
- while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
1067
+ let delay = DEV_RETRY_INITIAL_DELAY_MS;
1068
+ while (true) {
835
1069
  try {
836
- await client.receiveMessageById({
837
- queueName: topicName,
838
- consumerGroup,
839
- messageId,
840
- visibilityTimeoutSeconds: 0
841
- });
842
- return true;
1070
+ await handleCallback(handler, request, options);
1071
+ return;
843
1072
  } 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
1073
+ if (isMessageNotFoundError(error) && elapsed < DEV_RETRY_MAX_WAIT_MS) {
1074
+ await new Promise((r) => setTimeout(r, delay));
1075
+ elapsed += delay;
1076
+ delay = Math.min(
1077
+ delay * DEV_RETRY_BACKOFF,
1078
+ DEV_RETRY_MAX_WAIT_MS - elapsed
850
1079
  );
851
1080
  continue;
852
1081
  }
853
- if (error instanceof MessageAlreadyProcessedError) {
854
- console.log(
855
- `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
856
- );
857
- return false;
1082
+ throw error;
1083
+ }
1084
+ }
1085
+ }
1086
+ 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)$/, "");
1088
+ if (!urlPath.startsWith("/")) {
1089
+ urlPath = "/" + urlPath;
1090
+ }
1091
+ return urlPath;
1092
+ }
1093
+ async function ensureHandlersLoaded(topicName, options = {}) {
1094
+ const diagnostics = {
1095
+ triedPorts: collectPrimePorts(),
1096
+ listeningPorts: [],
1097
+ unavailablePorts: [],
1098
+ importFailures: [],
1099
+ primeFailures: []
1100
+ };
1101
+ const matchingRoutes = findMatchingRoutes(topicName);
1102
+ if (matchingRoutes.length === 0) return diagnostics;
1103
+ const shouldRefreshRegistered = options.refreshRegistered === true;
1104
+ for (const port of diagnostics.triedPorts) {
1105
+ if (await isPortListening(port)) {
1106
+ diagnostics.listeningPorts.push(port);
1107
+ } else {
1108
+ diagnostics.unavailablePorts.push(port);
1109
+ }
1110
+ }
1111
+ for (const route of matchingRoutes) {
1112
+ const alreadyRegistered = isHandlerRegistered(topicName, route.consumer);
1113
+ if (alreadyRegistered && !shouldRefreshRegistered) {
1114
+ continue;
1115
+ }
1116
+ if (!alreadyRegistered) {
1117
+ const absolutePath = path.resolve(process.cwd(), route.filePath);
1118
+ try {
1119
+ await import(absolutePath);
1120
+ } catch (error) {
1121
+ diagnostics.importFailures.push({
1122
+ filePath: route.filePath,
1123
+ reason: formatErrorReason(error)
1124
+ });
1125
+ }
1126
+ if (isHandlerRegistered(topicName, route.consumer)) continue;
1127
+ }
1128
+ for (const port of diagnostics.listeningPorts) {
1129
+ const url = `http://localhost:${port}${filePathToUrlPath(route.filePath)}`;
1130
+ try {
1131
+ const response = await fetch(url, {
1132
+ method: "POST",
1133
+ headers: {
1134
+ "x-vercel-queue-prime": "1",
1135
+ "x-vercel-queue-prime-file": route.filePath
1136
+ }
1137
+ });
1138
+ try {
1139
+ await response.text();
1140
+ } catch {
1141
+ }
1142
+ if (isHandlerRegistered(topicName, route.consumer)) {
1143
+ break;
1144
+ }
1145
+ diagnostics.primeFailures.push({
1146
+ filePath: route.filePath,
1147
+ url,
1148
+ reason: `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`.trim()
1149
+ });
1150
+ } catch (error) {
1151
+ diagnostics.primeFailures.push({
1152
+ filePath: route.filePath,
1153
+ url,
1154
+ reason: formatErrorReason(error)
1155
+ });
858
1156
  }
859
- console.error(
860
- `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
861
- error
862
- );
863
- return false;
864
1157
  }
865
1158
  }
866
- console.warn(
867
- `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
1159
+ return diagnostics;
1160
+ }
1161
+ function buildNoHandlerWarning(topicName, routes, diagnostics) {
1162
+ const files = routes.map((r) => r.filePath);
1163
+ const suggestedPort = diagnostics.listeningPorts[0] ?? diagnostics.triedPorts[0];
1164
+ const suggestedUrls = suggestedPort ? routes.map(
1165
+ (r) => `http://localhost:${suggestedPort}${filePathToUrlPath(r.filePath)}`
1166
+ ) : [];
1167
+ let portSummary;
1168
+ if (diagnostics.triedPorts.length === 0) {
1169
+ portSummary = "No local dev port detected from env. Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT).";
1170
+ } else if (diagnostics.listeningPorts.length === 0) {
1171
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}], but none are listening.`;
1172
+ } else {
1173
+ const unavailable = diagnostics.unavailablePorts.length > 0 ? ` Not listening: [${diagnostics.unavailablePorts.join(", ")}].` : "";
1174
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}]. Listening: [${diagnostics.listeningPorts.join(", ")}].` + unavailable;
1175
+ }
1176
+ const importSummary = diagnostics.importFailures.length > 0 ? `
1177
+ Import failures: ` + diagnostics.importFailures.slice(0, 2).map((f) => `${f.filePath} (${f.reason})`).join("; ") : "";
1178
+ const primeSummary = diagnostics.primeFailures.length > 0 ? `
1179
+ 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.
1181
+ ${portSummary}${importSummary}${primeSummary}
1182
+ Ensure your dev server is running, set PORT if needed, and confirm mapped route files call handleCallback()/handleNodeCallback() at module scope.
1183
+ ` + (suggestedUrls.length > 0 ? `Try opening: ${suggestedUrls.join(" or ")}` : "Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT) and try sending again.");
1184
+ }
1185
+ function isHandlerRegistered(topicName, consumerGroup) {
1186
+ return lookupHandlers(topicName).some(
1187
+ (h) => h.consumerGroup === consumerGroup
868
1188
  );
869
- return false;
870
1189
  }
871
- function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
1190
+ function invokeDevHandlers(topicName, messageId, region, delaySeconds) {
872
1191
  if (delaySeconds && delaySeconds > 0) {
873
1192
  console.log(
874
1193
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
875
1194
  );
876
1195
  setTimeout(() => {
877
- triggerDevCallbacks(topicName, messageId, region);
1196
+ invokeDevHandlers(topicName, messageId, region);
878
1197
  }, delaySeconds * 1e3);
879
1198
  return;
880
1199
  }
881
1200
  console.log(
882
1201
  `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
883
1202
  );
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
1203
  (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
- );
1204
+ let handlers = lookupHandlers(topicName);
1205
+ let diagnostics = null;
1206
+ if (handlers.length > 0) {
1207
+ await ensureHandlersLoaded(topicName, { refreshRegistered: true });
1208
+ handlers = lookupHandlers(topicName);
1209
+ } else {
1210
+ diagnostics = await ensureHandlersLoaded(topicName);
1211
+ handlers = lookupHandlers(topicName);
1212
+ }
1213
+ if (handlers.length === 0) {
1214
+ const matchingRoutes = findMatchingRoutes(topicName);
1215
+ if (matchingRoutes.length > 0) {
1216
+ const safeDiagnostics = diagnostics ?? {
1217
+ triedPorts: collectPrimePorts(),
1218
+ listeningPorts: [],
1219
+ unavailablePorts: [],
1220
+ importFailures: [],
1221
+ primeFailures: []
1222
+ };
1223
+ console.warn(
1224
+ buildNoHandlerWarning(topicName, matchingRoutes, safeDiagnostics)
1225
+ );
1226
+ } else {
1227
+ console.warn(
1228
+ `[Dev Mode] No registered handler for topic "${topicName}".
1229
+ Ensure vercel.json has a matching experimentalTriggers entry and the route file calls handleCallback().`
1230
+ );
1231
+ }
907
1232
  return;
908
1233
  }
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}"`
915
- );
1234
+ const consumerGroups = handlers.map((h) => h.consumerGroup);
1235
+ console.log(
1236
+ `[Dev Mode] Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
1237
+ );
1238
+ for (const entry of handlers) {
1239
+ const request = {
1240
+ queueName: topicName,
1241
+ consumerGroup: entry.consumerGroup,
1242
+ messageId,
1243
+ region
1244
+ };
1245
+ const callbackOptions = {
1246
+ client: entry.client,
1247
+ visibilityTimeoutSeconds: entry.options?.visibilityTimeoutSeconds,
1248
+ retry: entry.options?.retry
1249
+ };
916
1250
  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
- }
951
- }
1251
+ await invokeWithRetry(entry.handler, request, callbackOptions);
1252
+ console.log(
1253
+ `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1254
+ );
952
1255
  } catch (error) {
953
1256
  console.error(
954
- `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
1257
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
955
1258
  error
956
1259
  );
957
1260
  }
958
1261
  }
959
1262
  })();
960
1263
  }
961
- function clearDevRouteMappings() {
1264
+ function clearDevState() {
962
1265
  const g = globalThis;
963
1266
  delete g[ROUTE_MAPPINGS_KEY];
1267
+ delete g[HANDLER_REGISTRY_KEY];
964
1268
  }
965
1269
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
966
- globalThis.__clearDevRouteMappings = clearDevRouteMappings;
1270
+ globalThis.__clearDevState = clearDevState;
967
1271
  }
968
1272
 
969
1273
  // src/oidc.ts
@@ -1138,7 +1442,7 @@ var ApiClient = class _ApiClient {
1138
1442
  }
1139
1443
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1140
1444
  }
1141
- init.headers.set("User-Agent", `@vercel/queue/${"0.1.0"}`);
1445
+ init.headers.set("User-Agent", `@vercel/queue/${"0.1.1"}`);
1142
1446
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1143
1447
  const response = await fetch(url, init);
1144
1448
  if (isDebugEnabled()) {
@@ -1474,12 +1778,36 @@ var ApiClient = class _ApiClient {
1474
1778
 
1475
1779
  // src/client.ts
1476
1780
  var apiClients = /* @__PURE__ */ new WeakMap();
1781
+ var API_CLIENT_KEY = Symbol.for("@vercel/queue.apiClient");
1782
+ function setApi(client, api) {
1783
+ apiClients.set(client, api);
1784
+ Object.defineProperty(client, API_CLIENT_KEY, {
1785
+ value: api,
1786
+ writable: false,
1787
+ enumerable: false,
1788
+ configurable: false
1789
+ });
1790
+ }
1477
1791
  function getApi(client) {
1478
1792
  const api = apiClients.get(client);
1479
- if (!api) {
1480
- throw new Error("Client not initialized");
1793
+ if (api) {
1794
+ return api;
1795
+ }
1796
+ const apiFromSymbol = client[API_CLIENT_KEY];
1797
+ if (typeof apiFromSymbol === "object" && apiFromSymbol !== null) {
1798
+ const resolvedApi = apiFromSymbol;
1799
+ apiClients.set(client, resolvedApi);
1800
+ return resolvedApi;
1801
+ }
1802
+ throw new Error(
1803
+ "QueueClient not initialized. This may happen when multiple bundled copies of @vercel/queue are loaded in local dev."
1804
+ );
1805
+ }
1806
+ function resolveCallbackRequest(input) {
1807
+ if ("request" in input) {
1808
+ return input.request;
1481
1809
  }
1482
- return api;
1810
+ return input;
1483
1811
  }
1484
1812
  function getApiClient(client) {
1485
1813
  return getApi(client);
@@ -1497,7 +1825,7 @@ function resolveRegion(region) {
1497
1825
  var QueueClient = class {
1498
1826
  constructor(options = {}) {
1499
1827
  const region = resolveRegion(options.region);
1500
- apiClients.set(this, new ApiClient({ ...options, region }));
1828
+ setApi(this, new ApiClient({ ...options, region }));
1501
1829
  }
1502
1830
  /**
1503
1831
  * Send a message to a topic.
@@ -1525,7 +1853,7 @@ var QueueClient = class {
1525
1853
  headers: options?.headers
1526
1854
  });
1527
1855
  if (result.messageId && isDevMode()) {
1528
- triggerDevCallbacks(
1856
+ invokeDevHandlers(
1529
1857
  topicName,
1530
1858
  result.messageId,
1531
1859
  api.getRegion(),
@@ -1551,10 +1879,26 @@ var QueueClient = class {
1551
1879
  * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1552
1880
  * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1553
1881
  * reschedule the message for redelivery after N seconds.
1554
- * @returns A `(request: Request) => Promise<Response>` route handler
1882
+ * @returns A route handler that accepts either `Request` or `{ request: Request }`
1555
1883
  */
1556
1884
  handleCallback = (handler, options) => {
1557
- return async (request) => {
1885
+ if (isDevMode()) {
1886
+ registerDevHandler(handler, this, options);
1887
+ }
1888
+ return async (requestOrEvent) => {
1889
+ const request = resolveCallbackRequest(requestOrEvent);
1890
+ if (isDevMode() && request.headers.get("x-vercel-queue-prime") === "1") {
1891
+ const primeFile = request.headers.get("x-vercel-queue-prime-file");
1892
+ if (primeFile) {
1893
+ registerDevHandler(
1894
+ handler,
1895
+ this,
1896
+ options,
1897
+ primeFile
1898
+ );
1899
+ }
1900
+ return Response.json({ status: "primed" });
1901
+ }
1558
1902
  try {
1559
1903
  const parsed = await parseCallback(request);
1560
1904
  await handleCallback(handler, parsed, {
@@ -1596,11 +1940,29 @@ var QueueClient = class {
1596
1940
  * @returns A `(req, res) => Promise<void>` route handler
1597
1941
  */
1598
1942
  handleNodeCallback = (handler, options) => {
1943
+ if (isDevMode()) {
1944
+ registerDevHandler(handler, this, options);
1945
+ }
1599
1946
  return async (req, res) => {
1600
1947
  if (req.method !== "POST") {
1601
1948
  res.status(200).end();
1602
1949
  return;
1603
1950
  }
1951
+ const primeHeader = req.headers["x-vercel-queue-prime"];
1952
+ if (isDevMode() && primeHeader === "1") {
1953
+ const primeFileHeader = req.headers["x-vercel-queue-prime-file"];
1954
+ const primeFile = Array.isArray(primeFileHeader) ? primeFileHeader[0] : primeFileHeader;
1955
+ if (primeFile) {
1956
+ registerDevHandler(
1957
+ handler,
1958
+ this,
1959
+ options,
1960
+ primeFile
1961
+ );
1962
+ }
1963
+ res.status(200).json({ status: "primed" });
1964
+ return;
1965
+ }
1604
1966
  try {
1605
1967
  const parsed = parseRawCallback(req.body, req.headers);
1606
1968
  await handleCallback(handler, parsed, {
@@ -1622,7 +1984,7 @@ var QueueClient = class {
1622
1984
  };
1623
1985
  var PollingQueueClient = class {
1624
1986
  constructor(options) {
1625
- apiClients.set(this, new ApiClient(options));
1987
+ setApi(this, new ApiClient(options));
1626
1988
  }
1627
1989
  /**
1628
1990
  * Send a message to a topic.
@@ -1721,6 +2083,25 @@ var PollingQueueClient = class {
1721
2083
  }
1722
2084
  };
1723
2085
  };
2086
+
2087
+ // src/default-client.ts
2088
+ var _defaultClient;
2089
+ function getDefaultClient() {
2090
+ if (!_defaultClient) {
2091
+ _defaultClient = new QueueClient();
2092
+ }
2093
+ return _defaultClient;
2094
+ }
2095
+ function resolveClient(region) {
2096
+ if (!region) return getDefaultClient();
2097
+ return new QueueClient({ region });
2098
+ }
2099
+ async function send(topicName, payload, options) {
2100
+ return resolveClient(options?.region).send(topicName, payload, options);
2101
+ }
2102
+ function handleCallback2(handler, options) {
2103
+ return getDefaultClient().handleCallback(handler, options);
2104
+ }
1724
2105
  // Annotate the CommonJS export names for ESM import in node:
1725
2106
  0 && (module.exports = {
1726
2107
  BadRequestError,
@@ -1744,7 +2125,9 @@ var PollingQueueClient = class {
1744
2125
  QueueEmptyError,
1745
2126
  StreamTransport,
1746
2127
  UnauthorizedError,
2128
+ handleCallback,
1747
2129
  parseCallback,
1748
- parseRawCallback
2130
+ parseRawCallback,
2131
+ send
1749
2132
  });
1750
2133
  //# sourceMappingURL=index.js.map