@vercel/queue 0.0.2 → 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
@@ -46,12 +46,15 @@ __export(index_exports, {
46
46
  MessageLockedError: () => MessageLockedError,
47
47
  MessageNotAvailableError: () => MessageNotAvailableError,
48
48
  MessageNotFoundError: () => MessageNotFoundError,
49
+ PollingQueueClient: () => PollingQueueClient,
49
50
  QueueClient: () => QueueClient,
50
51
  QueueEmptyError: () => QueueEmptyError,
51
52
  StreamTransport: () => StreamTransport,
52
53
  UnauthorizedError: () => UnauthorizedError,
54
+ handleCallback: () => handleCallback2,
53
55
  parseCallback: () => parseCallback,
54
- parseRawCallback: () => parseRawCallback
56
+ parseRawCallback: () => parseRawCallback,
57
+ send: () => send
55
58
  });
56
59
  module.exports = __toCommonJS(index_exports);
57
60
 
@@ -133,6 +136,7 @@ var import_mixpart = require("mixpart");
133
136
 
134
137
  // src/dev.ts
135
138
  var fs = __toESM(require("fs"));
139
+ var net = __toESM(require("net"));
136
140
  var path = __toESM(require("path"));
137
141
 
138
142
  // src/types.ts
@@ -318,8 +322,8 @@ var ConsumerGroup = class {
318
322
  firstDelayMs = 0;
319
323
  }
320
324
  }
321
- const lifecyclePromise = new Promise((resolve) => {
322
- resolveLifecycle = resolve;
325
+ const lifecyclePromise = new Promise((resolve2) => {
326
+ resolveLifecycle = resolve2;
323
327
  });
324
328
  const safeResolve = () => {
325
329
  if (!isResolved) {
@@ -551,7 +555,7 @@ var Topic = class {
551
555
  headers: options?.headers
552
556
  });
553
557
  if (result.messageId && isDevMode()) {
554
- triggerDevCallbacks(
558
+ invokeDevHandlers(
555
559
  this.topicName,
556
560
  result.messageId,
557
561
  this.client.getRegion()
@@ -760,14 +764,11 @@ async function handleCallback(handler, request, options) {
760
764
  }
761
765
 
762
766
  // src/dev.ts
763
- var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
764
- function filePathToUrlPath(filePath) {
765
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
766
- if (!urlPath.startsWith("/")) {
767
- urlPath = "/" + urlPath;
768
- }
769
- return urlPath;
767
+ var import_meta = {};
768
+ function isDevMode() {
769
+ return process.env.NODE_ENV === "development";
770
770
  }
771
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
771
772
  function filePathToConsumerGroup(filePath) {
772
773
  return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
773
774
  }
@@ -791,13 +792,18 @@ function getDevRouteMappings() {
791
792
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
792
793
  if (!config.experimentalTriggers) continue;
793
794
  for (const trigger of config.experimentalTriggers) {
794
- if (trigger.type?.startsWith("queue/") && trigger.topic) {
795
- mappings.push({
796
- urlPath: filePathToUrlPath(filePath),
797
- topic: trigger.topic,
798
- consumer: filePathToConsumerGroup(filePath)
799
- });
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;
800
801
  }
802
+ mappings.push({
803
+ filePath,
804
+ topic: trigger.topic,
805
+ consumer: filePathToConsumerGroup(filePath)
806
+ });
801
807
  }
802
808
  }
803
809
  g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
@@ -810,9 +816,7 @@ function getDevRouteMappings() {
810
816
  }
811
817
  function findMatchingRoutes(topicName) {
812
818
  const mappings = getDevRouteMappings();
813
- if (!mappings) {
814
- return [];
815
- }
819
+ if (!mappings) return [];
816
820
  return mappings.filter((mapping) => {
817
821
  if (mapping.topic.includes("*")) {
818
822
  return matchesWildcardPattern(topicName, mapping.topic);
@@ -820,149 +824,450 @@ function findMatchingRoutes(topicName) {
820
824
  return mapping.topic === topicName;
821
825
  });
822
826
  }
823
- function isDevMode() {
824
- 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;
927
+ }
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);
825
1008
  }
826
- var DEV_VISIBILITY_POLL_INTERVAL = 50;
827
- var DEV_VISIBILITY_MAX_WAIT = 5e3;
828
- var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
829
- async function waitForMessageVisibility(topicName, consumerGroup, messageId, region) {
830
- const client = new ApiClient({ region });
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) {
831
1066
  let elapsed = 0;
832
- let interval = DEV_VISIBILITY_POLL_INTERVAL;
833
- while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
1067
+ let delay = DEV_RETRY_INITIAL_DELAY_MS;
1068
+ while (true) {
834
1069
  try {
835
- await client.receiveMessageById({
836
- queueName: topicName,
837
- consumerGroup,
838
- messageId,
839
- visibilityTimeoutSeconds: 0
840
- });
841
- return true;
1070
+ await handleCallback(handler, request, options);
1071
+ return;
842
1072
  } catch (error) {
843
- if (error instanceof MessageNotFoundError) {
844
- await new Promise((resolve) => setTimeout(resolve, interval));
845
- elapsed += interval;
846
- interval = Math.min(
847
- interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
848
- 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
849
1079
  );
850
1080
  continue;
851
1081
  }
852
- if (error instanceof MessageAlreadyProcessedError) {
853
- console.log(
854
- `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
855
- );
856
- 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
+ });
857
1156
  }
858
- console.error(
859
- `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
860
- error
861
- );
862
- return false;
863
1157
  }
864
1158
  }
865
- console.warn(
866
- `[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
867
1188
  );
868
- return false;
869
1189
  }
870
- function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
1190
+ function invokeDevHandlers(topicName, messageId, region, delaySeconds) {
871
1191
  if (delaySeconds && delaySeconds > 0) {
872
1192
  console.log(
873
1193
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
874
1194
  );
875
1195
  setTimeout(() => {
876
- triggerDevCallbacks(topicName, messageId, region);
1196
+ invokeDevHandlers(topicName, messageId, region);
877
1197
  }, delaySeconds * 1e3);
878
1198
  return;
879
1199
  }
880
1200
  console.log(
881
1201
  `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
882
1202
  );
883
- const matchingRoutes = findMatchingRoutes(topicName);
884
- if (matchingRoutes.length === 0) {
885
- console.log(
886
- `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
887
- );
888
- return;
889
- }
890
- const consumerGroups = matchingRoutes.map((r) => r.consumer);
891
- console.log(
892
- `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
893
- );
894
1203
  (async () => {
895
- const firstRoute = matchingRoutes[0];
896
- const isVisible = await waitForMessageVisibility(
897
- topicName,
898
- firstRoute.consumer,
899
- messageId,
900
- region
901
- );
902
- if (!isVisible) {
903
- console.warn(
904
- `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
905
- );
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
+ }
906
1232
  return;
907
1233
  }
908
- const port = process.env.PORT || 3e3;
909
- const baseUrl = `http://localhost:${port}`;
910
- for (const route of matchingRoutes) {
911
- const url = `${baseUrl}${route.urlPath}`;
912
- console.log(
913
- `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
914
- );
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
+ };
915
1250
  try {
916
- const response = await fetch(url, {
917
- method: "POST",
918
- headers: {
919
- "ce-type": CLOUD_EVENT_TYPE_V2BETA,
920
- "ce-vqsqueuename": topicName,
921
- "ce-vqsconsumergroup": route.consumer,
922
- "ce-vqsmessageid": messageId,
923
- "ce-vqsregion": region
924
- }
925
- });
926
- if (response.ok) {
927
- try {
928
- const responseData = await response.json();
929
- if (responseData.status === "success") {
930
- console.log(
931
- `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
932
- );
933
- }
934
- } catch {
935
- console.warn(
936
- `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
937
- );
938
- }
939
- } else {
940
- try {
941
- const errorData = await response.json();
942
- console.error(
943
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
944
- );
945
- } catch {
946
- console.error(
947
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
948
- );
949
- }
950
- }
1251
+ await invokeWithRetry(entry.handler, request, callbackOptions);
1252
+ console.log(
1253
+ `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1254
+ );
951
1255
  } catch (error) {
952
1256
  console.error(
953
- `[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}"`,
954
1258
  error
955
1259
  );
956
1260
  }
957
1261
  }
958
1262
  })();
959
1263
  }
960
- function clearDevRouteMappings() {
1264
+ function clearDevState() {
961
1265
  const g = globalThis;
962
1266
  delete g[ROUTE_MAPPINGS_KEY];
1267
+ delete g[HANDLER_REGISTRY_KEY];
963
1268
  }
964
1269
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
965
- globalThis.__clearDevRouteMappings = clearDevRouteMappings;
1270
+ globalThis.__clearDevState = clearDevState;
966
1271
  }
967
1272
 
968
1273
  // src/oidc.ts
@@ -1079,7 +1384,7 @@ var ApiClient = class _ApiClient {
1079
1384
  return;
1080
1385
  }
1081
1386
  throw new Error(
1082
- 'No deployment ID available. VERCEL_DEPLOYMENT_ID is not set.\n\nThis usually means the code is running outside a Vercel deployment (e.g. during build or in a non-Vercel environment).\n\nTo fix this, create a new QueueClient with an explicit deploymentId:\n new QueueClient({ region: "iad1", deploymentId: "dpl_xxx" })\nOr explicitly opt out of deployment pinning:\n new QueueClient({ region: "iad1", deploymentId: null })'
1387
+ 'No deployment ID available. VERCEL_DEPLOYMENT_ID is not set.\n\nThis usually means the code is running outside a Vercel deployment (e.g. during build or in a non-Vercel environment).\n\nTo fix this, create a client with an explicit deploymentId:\n new QueueClient({ deploymentId: "dpl_xxx" })\nOr explicitly opt out of deployment pinning:\n new QueueClient({ deploymentId: null })'
1083
1388
  );
1084
1389
  }
1085
1390
  getSendDeploymentId() {
@@ -1137,7 +1442,7 @@ var ApiClient = class _ApiClient {
1137
1442
  }
1138
1443
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1139
1444
  }
1140
- init.headers.set("User-Agent", `@vercel/queue/${"0.0.2"}`);
1445
+ init.headers.set("User-Agent", `@vercel/queue/${"0.1.1"}`);
1141
1446
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1142
1447
  const response = await fetch(url, init);
1143
1448
  if (isDebugEnabled()) {
@@ -1473,23 +1778,61 @@ var ApiClient = class _ApiClient {
1473
1778
 
1474
1779
  // src/client.ts
1475
1780
  var apiClients = /* @__PURE__ */ new WeakMap();
1476
- function getApiClient(client) {
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
+ }
1791
+ function getApi(client) {
1477
1792
  const api = apiClients.get(client);
1478
- if (!api) {
1479
- throw new Error("QueueClient 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;
1480
1801
  }
1481
- return api;
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;
1809
+ }
1810
+ return input;
1811
+ }
1812
+ function getApiClient(client) {
1813
+ return getApi(client);
1814
+ }
1815
+ var DEFAULT_REGION = "iad1";
1816
+ function resolveRegion(region) {
1817
+ if (region) return region;
1818
+ const fromEnv = process.env.VERCEL_REGION;
1819
+ 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
+ );
1823
+ return DEFAULT_REGION;
1482
1824
  }
1483
1825
  var QueueClient = class {
1484
- constructor(options) {
1485
- apiClients.set(this, new ApiClient(options));
1826
+ constructor(options = {}) {
1827
+ const region = resolveRegion(options.region);
1828
+ setApi(this, new ApiClient({ ...options, region }));
1486
1829
  }
1487
1830
  /**
1488
1831
  * Send a message to a topic.
1489
1832
  *
1490
1833
  * This is an arrow function property so it can be destructured:
1491
1834
  * ```typescript
1492
- * const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
1835
+ * const { send } = new QueueClient();
1493
1836
  * await send("my-topic", payload);
1494
1837
  * ```
1495
1838
  *
@@ -1500,7 +1843,7 @@ var QueueClient = class {
1500
1843
  * the message for deferred processing (no ID available yet)
1501
1844
  */
1502
1845
  send = async (topicName, payload, options) => {
1503
- const api = getApiClient(this);
1846
+ const api = getApi(this);
1504
1847
  const result = await api.sendMessage({
1505
1848
  queueName: topicName,
1506
1849
  payload,
@@ -1510,7 +1853,7 @@ var QueueClient = class {
1510
1853
  headers: options?.headers
1511
1854
  });
1512
1855
  if (result.messageId && isDevMode()) {
1513
- triggerDevCallbacks(
1856
+ invokeDevHandlers(
1514
1857
  topicName,
1515
1858
  result.messageId,
1516
1859
  api.getRegion(),
@@ -1519,75 +1862,6 @@ var QueueClient = class {
1519
1862
  }
1520
1863
  return { messageId: result.messageId };
1521
1864
  };
1522
- /**
1523
- * Receive and process messages from a topic.
1524
- *
1525
- * Each message is automatically locked, kept alive via periodic visibility
1526
- * extensions during processing, and acknowledged upon successful handler completion.
1527
- * The handler is not called when the queue is empty — check `result.ok` instead.
1528
- *
1529
- * This is an arrow function property so it can be destructured:
1530
- * ```typescript
1531
- * const { receive } = new QueueClient({ region: process.env.QUEUE_REGION! });
1532
- * const result = await receive("my-topic", "my-group", handler);
1533
- * if (!result.ok) console.log(result.reason);
1534
- * ```
1535
- *
1536
- * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1537
- * @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1538
- * @param handler - Function to process each message payload and metadata.
1539
- * Not called when the queue is empty.
1540
- * @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
1541
- * @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
1542
- */
1543
- receive = async (topicName, consumerGroup, handler, options) => {
1544
- const api = getApiClient(this);
1545
- const topic = new Topic(api, topicName);
1546
- const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
1547
- const consumer = topic.consumerGroup(
1548
- consumerGroup,
1549
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
1550
- );
1551
- try {
1552
- let count;
1553
- const retry = options?.retry;
1554
- if (options && "messageId" in options) {
1555
- count = await consumer.consume(handler, {
1556
- messageId: options.messageId,
1557
- retry
1558
- });
1559
- } else {
1560
- const limit = options && "limit" in options ? options.limit : void 0;
1561
- count = await consumer.consume(handler, {
1562
- ...limit !== void 0 ? { limit } : {},
1563
- retry
1564
- });
1565
- }
1566
- if (count === 0) {
1567
- return { ok: false, reason: "empty" };
1568
- }
1569
- return { ok: true };
1570
- } catch (error) {
1571
- if (options && "messageId" in options && error instanceof MessageNotFoundError) {
1572
- return { ok: false, reason: "not_found", messageId: options.messageId };
1573
- }
1574
- if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
1575
- return {
1576
- ok: false,
1577
- reason: "not_available",
1578
- messageId: options.messageId
1579
- };
1580
- }
1581
- if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
1582
- return {
1583
- ok: false,
1584
- reason: "already_processed",
1585
- messageId: options.messageId
1586
- };
1587
- }
1588
- throw error;
1589
- }
1590
- };
1591
1865
  /**
1592
1866
  * Create a Web API route handler for processing queue callback messages.
1593
1867
  *
@@ -1596,7 +1870,7 @@ var QueueClient = class {
1596
1870
  *
1597
1871
  * This is an arrow function property so it can be destructured:
1598
1872
  * ```typescript
1599
- * const { handleCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1873
+ * const { handleCallback } = new QueueClient();
1600
1874
  * export const POST = handleCallback(handler);
1601
1875
  * ```
1602
1876
  *
@@ -1605,10 +1879,26 @@ var QueueClient = class {
1605
1879
  * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1606
1880
  * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1607
1881
  * reschedule the message for redelivery after N seconds.
1608
- * @returns A `(request: Request) => Promise<Response>` route handler
1882
+ * @returns A route handler that accepts either `Request` or `{ request: Request }`
1609
1883
  */
1610
1884
  handleCallback = (handler, options) => {
1611
- 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
+ }
1612
1902
  try {
1613
1903
  const parsed = await parseCallback(request);
1614
1904
  await handleCallback(handler, parsed, {
@@ -1638,7 +1928,7 @@ var QueueClient = class {
1638
1928
  *
1639
1929
  * This is an arrow function property so it can be destructured:
1640
1930
  * ```typescript
1641
- * const { handleNodeCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1931
+ * const { handleNodeCallback } = new QueueClient();
1642
1932
  * app.post("/api/queue", handleNodeCallback(handler));
1643
1933
  * ```
1644
1934
  *
@@ -1650,11 +1940,29 @@ var QueueClient = class {
1650
1940
  * @returns A `(req, res) => Promise<void>` route handler
1651
1941
  */
1652
1942
  handleNodeCallback = (handler, options) => {
1943
+ if (isDevMode()) {
1944
+ registerDevHandler(handler, this, options);
1945
+ }
1653
1946
  return async (req, res) => {
1654
1947
  if (req.method !== "POST") {
1655
1948
  res.status(200).end();
1656
1949
  return;
1657
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
+ }
1658
1966
  try {
1659
1967
  const parsed = parseRawCallback(req.body, req.headers);
1660
1968
  await handleCallback(handler, parsed, {
@@ -1674,6 +1982,126 @@ var QueueClient = class {
1674
1982
  };
1675
1983
  };
1676
1984
  };
1985
+ var PollingQueueClient = class {
1986
+ constructor(options) {
1987
+ setApi(this, new ApiClient(options));
1988
+ }
1989
+ /**
1990
+ * Send a message to a topic.
1991
+ *
1992
+ * This is an arrow function property so it can be destructured:
1993
+ * ```typescript
1994
+ * const { send } = new PollingQueueClient({ region: "iad1" });
1995
+ * await send("my-topic", payload);
1996
+ * ```
1997
+ *
1998
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1999
+ * @param payload - The data to send (serialized via the configured transport)
2000
+ * @param options - Optional send options (idempotencyKey, retentionSeconds, delaySeconds, headers)
2001
+ * @returns `{ messageId }` — `messageId` is `null` when the server accepted
2002
+ * the message for deferred processing (no ID available yet)
2003
+ */
2004
+ send = async (topicName, payload, options) => {
2005
+ const api = getApi(this);
2006
+ const result = await api.sendMessage({
2007
+ queueName: topicName,
2008
+ payload,
2009
+ idempotencyKey: options?.idempotencyKey,
2010
+ retentionSeconds: options?.retentionSeconds,
2011
+ delaySeconds: options?.delaySeconds,
2012
+ headers: options?.headers
2013
+ });
2014
+ return { messageId: result.messageId };
2015
+ };
2016
+ /**
2017
+ * Receive and process messages from a topic.
2018
+ *
2019
+ * Each message is automatically locked, kept alive via periodic visibility
2020
+ * extensions during processing, and acknowledged upon successful handler completion.
2021
+ * The handler is not called when the queue is empty — check `result.ok` instead.
2022
+ *
2023
+ * This is an arrow function property so it can be destructured:
2024
+ * ```typescript
2025
+ * const { receive } = new PollingQueueClient({ region: "iad1" });
2026
+ * const result = await receive("my-topic", "my-group", handler);
2027
+ * if (!result.ok) console.log(result.reason);
2028
+ * ```
2029
+ *
2030
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
2031
+ * @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
2032
+ * @param handler - Function to process each message payload and metadata.
2033
+ * Not called when the queue is empty.
2034
+ * @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
2035
+ * @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
2036
+ */
2037
+ receive = async (topicName, consumerGroup, handler, options) => {
2038
+ const api = getApi(this);
2039
+ const topic = new Topic(api, topicName);
2040
+ const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
2041
+ const consumer = topic.consumerGroup(
2042
+ consumerGroup,
2043
+ visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
2044
+ );
2045
+ try {
2046
+ let count;
2047
+ const retry = options?.retry;
2048
+ if (options && "messageId" in options) {
2049
+ count = await consumer.consume(handler, {
2050
+ messageId: options.messageId,
2051
+ retry
2052
+ });
2053
+ } else {
2054
+ const limit = options && "limit" in options ? options.limit : void 0;
2055
+ count = await consumer.consume(handler, {
2056
+ ...limit !== void 0 ? { limit } : {},
2057
+ retry
2058
+ });
2059
+ }
2060
+ if (count === 0) {
2061
+ return { ok: false, reason: "empty" };
2062
+ }
2063
+ return { ok: true };
2064
+ } catch (error) {
2065
+ if (options && "messageId" in options && error instanceof MessageNotFoundError) {
2066
+ return { ok: false, reason: "not_found", messageId: options.messageId };
2067
+ }
2068
+ if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
2069
+ return {
2070
+ ok: false,
2071
+ reason: "not_available",
2072
+ messageId: options.messageId
2073
+ };
2074
+ }
2075
+ if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
2076
+ return {
2077
+ ok: false,
2078
+ reason: "already_processed",
2079
+ messageId: options.messageId
2080
+ };
2081
+ }
2082
+ throw error;
2083
+ }
2084
+ };
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
+ }
1677
2105
  // Annotate the CommonJS export names for ESM import in node:
1678
2106
  0 && (module.exports = {
1679
2107
  BadRequestError,
@@ -1692,11 +2120,14 @@ var QueueClient = class {
1692
2120
  MessageLockedError,
1693
2121
  MessageNotAvailableError,
1694
2122
  MessageNotFoundError,
2123
+ PollingQueueClient,
1695
2124
  QueueClient,
1696
2125
  QueueEmptyError,
1697
2126
  StreamTransport,
1698
2127
  UnauthorizedError,
2128
+ handleCallback,
1699
2129
  parseCallback,
1700
- parseRawCallback
2130
+ parseRawCallback,
2131
+ send
1701
2132
  });
1702
2133
  //# sourceMappingURL=index.js.map