@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.mjs CHANGED
@@ -76,6 +76,7 @@ import { parseMultipartStream } from "mixpart";
76
76
 
77
77
  // src/dev.ts
78
78
  import * as fs from "fs";
79
+ import * as net from "net";
79
80
  import * as path from "path";
80
81
 
81
82
  // src/types.ts
@@ -261,8 +262,8 @@ var ConsumerGroup = class {
261
262
  firstDelayMs = 0;
262
263
  }
263
264
  }
264
- const lifecyclePromise = new Promise((resolve) => {
265
- resolveLifecycle = resolve;
265
+ const lifecyclePromise = new Promise((resolve2) => {
266
+ resolveLifecycle = resolve2;
266
267
  });
267
268
  const safeResolve = () => {
268
269
  if (!isResolved) {
@@ -494,7 +495,7 @@ var Topic = class {
494
495
  headers: options?.headers
495
496
  });
496
497
  if (result.messageId && isDevMode()) {
497
- triggerDevCallbacks(
498
+ invokeDevHandlers(
498
499
  this.topicName,
499
500
  result.messageId,
500
501
  this.client.getRegion()
@@ -703,14 +704,10 @@ async function handleCallback(handler, request, options) {
703
704
  }
704
705
 
705
706
  // src/dev.ts
706
- var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
707
- function filePathToUrlPath(filePath) {
708
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
709
- if (!urlPath.startsWith("/")) {
710
- urlPath = "/" + urlPath;
711
- }
712
- return urlPath;
707
+ function isDevMode() {
708
+ return process.env.NODE_ENV === "development";
713
709
  }
710
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
714
711
  function filePathToConsumerGroup(filePath) {
715
712
  return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
716
713
  }
@@ -734,13 +731,18 @@ function getDevRouteMappings() {
734
731
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
735
732
  if (!config.experimentalTriggers) continue;
736
733
  for (const trigger of config.experimentalTriggers) {
737
- if (trigger.type?.startsWith("queue/") && trigger.topic) {
738
- mappings.push({
739
- urlPath: filePathToUrlPath(filePath),
740
- topic: trigger.topic,
741
- consumer: filePathToConsumerGroup(filePath)
742
- });
734
+ if (!trigger.type?.startsWith("queue/") || !trigger.topic) continue;
735
+ if (trigger.type !== "queue/v2beta") {
736
+ console.warn(
737
+ `[Dev Mode] Unsupported trigger type "${trigger.type}" for topic "${trigger.topic}" in ${filePath}. Use "queue/v2beta" instead.`
738
+ );
739
+ continue;
743
740
  }
741
+ mappings.push({
742
+ filePath,
743
+ topic: trigger.topic,
744
+ consumer: filePathToConsumerGroup(filePath)
745
+ });
744
746
  }
745
747
  }
746
748
  g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
@@ -753,9 +755,7 @@ function getDevRouteMappings() {
753
755
  }
754
756
  function findMatchingRoutes(topicName) {
755
757
  const mappings = getDevRouteMappings();
756
- if (!mappings) {
757
- return [];
758
- }
758
+ if (!mappings) return [];
759
759
  return mappings.filter((mapping) => {
760
760
  if (mapping.topic.includes("*")) {
761
761
  return matchesWildcardPattern(topicName, mapping.topic);
@@ -763,149 +763,450 @@ function findMatchingRoutes(topicName) {
763
763
  return mapping.topic === topicName;
764
764
  });
765
765
  }
766
- function isDevMode() {
767
- return process.env.NODE_ENV === "development";
766
+ function findMappingsForFile(absolutePath) {
767
+ const mappings = getDevRouteMappings();
768
+ if (!mappings) return [];
769
+ const cwd = process.cwd();
770
+ let relative2;
771
+ try {
772
+ relative2 = path.relative(cwd, absolutePath);
773
+ } catch {
774
+ return [];
775
+ }
776
+ const normalized = relative2.replace(/\\/g, "/");
777
+ return mappings.filter((m) => m.filePath === normalized);
778
+ }
779
+ function parseFrameFilePath(line) {
780
+ let match = line.match(/\((.+?):\d+:\d+\)/);
781
+ if (!match) match = line.match(/at\s+(.+?):\d+:\d+/);
782
+ if (!match) return null;
783
+ let filePath = match[1].trim();
784
+ if (filePath === "native" || filePath.startsWith("node:") || filePath.startsWith("internal")) {
785
+ return null;
786
+ }
787
+ if (filePath.startsWith("file://")) {
788
+ try {
789
+ filePath = new URL(filePath).pathname;
790
+ } catch {
791
+ return null;
792
+ }
793
+ }
794
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(filePath)) {
795
+ return null;
796
+ }
797
+ if (filePath.startsWith("./")) {
798
+ filePath = filePath.slice(2);
799
+ }
800
+ return filePath;
801
+ }
802
+ var _sdkPackageDir;
803
+ function getSdkPackageDir() {
804
+ if (_sdkPackageDir) return _sdkPackageDir;
805
+ try {
806
+ const thisDir = typeof __dirname !== "undefined" ? __dirname : path.dirname(new URL(import.meta.url).pathname);
807
+ _sdkPackageDir = path.resolve(thisDir, "..");
808
+ } catch {
809
+ _sdkPackageDir = "";
810
+ }
811
+ return _sdkPackageDir;
812
+ }
813
+ function extractCallerFilePath() {
814
+ const stack = new Error().stack;
815
+ if (!stack) return null;
816
+ const lines = stack.split("\n").slice(1);
817
+ const pkgDir = getSdkPackageDir();
818
+ for (const line of lines) {
819
+ const fp = parseFrameFilePath(line);
820
+ if (!fp) continue;
821
+ const absolute = path.isAbsolute(fp) ? fp : path.resolve(process.cwd(), fp);
822
+ let realFp;
823
+ try {
824
+ realFp = fs.realpathSync(absolute);
825
+ } catch {
826
+ realFp = absolute;
827
+ }
828
+ if (pkgDir && realFp.startsWith(pkgDir)) continue;
829
+ return realFp;
830
+ }
831
+ return null;
832
+ }
833
+ var HANDLER_REGISTRY_KEY = Symbol.for("@vercel/queue.devHandlerRegistry");
834
+ function getHandlerRegistry() {
835
+ const g = globalThis;
836
+ if (!g[HANDLER_REGISTRY_KEY]) {
837
+ g[HANDLER_REGISTRY_KEY] = /* @__PURE__ */ new Map();
838
+ }
839
+ return g[HANDLER_REGISTRY_KEY];
840
+ }
841
+ function registerHandlerForFile(filePath, handler, client, options) {
842
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
843
+ const fileMappings = findMappingsForFile(absolutePath);
844
+ if (fileMappings.length === 0) return false;
845
+ const registry = getHandlerRegistry();
846
+ for (const mapping of fileMappings) {
847
+ const key = mapping.topic;
848
+ const existing = registry.get(key) ?? [];
849
+ const nextEntry = {
850
+ consumerGroup: mapping.consumer,
851
+ handler,
852
+ client,
853
+ options
854
+ };
855
+ const existingIndex = existing.findIndex(
856
+ (e) => e.consumerGroup === mapping.consumer
857
+ );
858
+ if (existingIndex >= 0) {
859
+ existing[existingIndex] = nextEntry;
860
+ } else {
861
+ existing.push(nextEntry);
862
+ }
863
+ registry.set(key, existing);
864
+ }
865
+ return true;
866
+ }
867
+ function registerDevHandler(handler, client, options, _testCallerPath) {
868
+ const callerPath = _testCallerPath ?? extractCallerFilePath();
869
+ if (!callerPath) {
870
+ console.warn(
871
+ "[Dev Mode] Could not determine caller file path for handler registration."
872
+ );
873
+ return;
874
+ }
875
+ const registered = registerHandlerForFile(
876
+ callerPath,
877
+ handler,
878
+ client,
879
+ options
880
+ );
881
+ if (!registered) {
882
+ const allMappings = getDevRouteMappings();
883
+ const cwd = process.cwd();
884
+ let relative2;
885
+ try {
886
+ relative2 = path.relative(cwd, callerPath).replace(/\\/g, "/");
887
+ } catch {
888
+ relative2 = callerPath;
889
+ }
890
+ if (allMappings && allMappings.length > 0) {
891
+ const configuredFiles = Array.from(
892
+ new Set(allMappings.map((m) => m.filePath))
893
+ );
894
+ console.warn(
895
+ `[Dev Mode] handleCallback() in ${relative2} does not match any queue route in vercel.json. This handler won't receive messages.
896
+ Configured queue routes: [${configuredFiles.join(", ")}]
897
+ If this path is a bundled chunk, keep handleCallback()/handleNodeCallback() at module scope and let dev-mode route priming load the mapped file.`
898
+ );
899
+ return;
900
+ }
901
+ console.warn(
902
+ `[Dev Mode] handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
903
+
904
+ Add a trigger to vercel.json:
905
+ "${relative2}": {
906
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "your-topic" }]
907
+ }`
908
+ );
909
+ }
910
+ }
911
+ function lookupHandlers(topicName) {
912
+ const registry = getHandlerRegistry();
913
+ const result = [];
914
+ for (const [pattern, handlers] of registry) {
915
+ const matches = pattern.includes("*") ? matchesWildcardPattern(topicName, pattern) : pattern === topicName;
916
+ if (matches) {
917
+ result.push(...handlers);
918
+ }
919
+ }
920
+ return result;
921
+ }
922
+ var DEV_RETRY_INITIAL_DELAY_MS = 50;
923
+ var DEV_RETRY_MAX_WAIT_MS = 5e3;
924
+ var DEV_RETRY_BACKOFF = 2;
925
+ var PORT_CHECK_TIMEOUT_MS = 250;
926
+ var PRIME_PORT_ENV_KEYS = [
927
+ "PORT",
928
+ "NEXT_PORT",
929
+ "NEXTJS_PORT",
930
+ "NUXT_PORT",
931
+ "NITRO_PORT",
932
+ "SVELTEKIT_PORT",
933
+ "VITE_PORT",
934
+ "DEV_PORT",
935
+ "npm_config_port"
936
+ ];
937
+ var PRIME_URL_ENV_KEYS = [
938
+ "__NEXT_PRIVATE_ORIGIN",
939
+ "NUXT_PUBLIC_SITE_URL",
940
+ "URL"
941
+ ];
942
+ function formatErrorReason(error) {
943
+ if (error instanceof Error) {
944
+ return error.message;
945
+ }
946
+ return String(error);
768
947
  }
769
- var DEV_VISIBILITY_POLL_INTERVAL = 50;
770
- var DEV_VISIBILITY_MAX_WAIT = 5e3;
771
- var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
772
- async function waitForMessageVisibility(topicName, consumerGroup, messageId, region) {
773
- const client = new ApiClient({ region });
948
+ function isMessageNotFoundError(error) {
949
+ if (error instanceof MessageNotFoundError) {
950
+ return true;
951
+ }
952
+ if (error instanceof Error && error.name === "MessageNotFoundError") {
953
+ return true;
954
+ }
955
+ return false;
956
+ }
957
+ function parsePort(value) {
958
+ if (!value) return null;
959
+ const parsed = Number.parseInt(value, 10);
960
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) return null;
961
+ return parsed;
962
+ }
963
+ function parsePortFromUrl(value) {
964
+ if (!value) return null;
965
+ try {
966
+ const parsed = new URL(value).port;
967
+ return parsePort(parsed);
968
+ } catch {
969
+ return null;
970
+ }
971
+ }
972
+ function collectPrimePorts() {
973
+ const result = [];
974
+ const seen = /* @__PURE__ */ new Set();
975
+ const add = (port) => {
976
+ if (port && !seen.has(port)) {
977
+ seen.add(port);
978
+ result.push(port);
979
+ }
980
+ };
981
+ for (const key of PRIME_PORT_ENV_KEYS) {
982
+ add(parsePort(process.env[key]));
983
+ }
984
+ for (const key of PRIME_URL_ENV_KEYS) {
985
+ add(parsePortFromUrl(process.env[key]));
986
+ }
987
+ return result;
988
+ }
989
+ function isPortListening(port) {
990
+ return new Promise((resolve2) => {
991
+ const socket = net.connect({ host: "localhost", port });
992
+ let settled = false;
993
+ const finish = (listening) => {
994
+ if (settled) return;
995
+ settled = true;
996
+ socket.destroy();
997
+ resolve2(listening);
998
+ };
999
+ socket.once("connect", () => finish(true));
1000
+ socket.once("error", () => finish(false));
1001
+ socket.setTimeout(PORT_CHECK_TIMEOUT_MS, () => finish(false));
1002
+ });
1003
+ }
1004
+ async function invokeWithRetry(handler, request, options) {
774
1005
  let elapsed = 0;
775
- let interval = DEV_VISIBILITY_POLL_INTERVAL;
776
- while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
1006
+ let delay = DEV_RETRY_INITIAL_DELAY_MS;
1007
+ while (true) {
777
1008
  try {
778
- await client.receiveMessageById({
779
- queueName: topicName,
780
- consumerGroup,
781
- messageId,
782
- visibilityTimeoutSeconds: 0
783
- });
784
- return true;
1009
+ await handleCallback(handler, request, options);
1010
+ return;
785
1011
  } catch (error) {
786
- if (error instanceof MessageNotFoundError) {
787
- await new Promise((resolve) => setTimeout(resolve, interval));
788
- elapsed += interval;
789
- interval = Math.min(
790
- interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
791
- DEV_VISIBILITY_MAX_WAIT - elapsed
1012
+ if (isMessageNotFoundError(error) && elapsed < DEV_RETRY_MAX_WAIT_MS) {
1013
+ await new Promise((r) => setTimeout(r, delay));
1014
+ elapsed += delay;
1015
+ delay = Math.min(
1016
+ delay * DEV_RETRY_BACKOFF,
1017
+ DEV_RETRY_MAX_WAIT_MS - elapsed
792
1018
  );
793
1019
  continue;
794
1020
  }
795
- if (error instanceof MessageAlreadyProcessedError) {
796
- console.log(
797
- `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
798
- );
799
- return false;
1021
+ throw error;
1022
+ }
1023
+ }
1024
+ }
1025
+ function filePathToUrlPath(filePath) {
1026
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/^server\//, "/").replace(/^src\/routes\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\/\+server\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
1027
+ if (!urlPath.startsWith("/")) {
1028
+ urlPath = "/" + urlPath;
1029
+ }
1030
+ return urlPath;
1031
+ }
1032
+ async function ensureHandlersLoaded(topicName, options = {}) {
1033
+ const diagnostics = {
1034
+ triedPorts: collectPrimePorts(),
1035
+ listeningPorts: [],
1036
+ unavailablePorts: [],
1037
+ importFailures: [],
1038
+ primeFailures: []
1039
+ };
1040
+ const matchingRoutes = findMatchingRoutes(topicName);
1041
+ if (matchingRoutes.length === 0) return diagnostics;
1042
+ const shouldRefreshRegistered = options.refreshRegistered === true;
1043
+ for (const port of diagnostics.triedPorts) {
1044
+ if (await isPortListening(port)) {
1045
+ diagnostics.listeningPorts.push(port);
1046
+ } else {
1047
+ diagnostics.unavailablePorts.push(port);
1048
+ }
1049
+ }
1050
+ for (const route of matchingRoutes) {
1051
+ const alreadyRegistered = isHandlerRegistered(topicName, route.consumer);
1052
+ if (alreadyRegistered && !shouldRefreshRegistered) {
1053
+ continue;
1054
+ }
1055
+ if (!alreadyRegistered) {
1056
+ const absolutePath = path.resolve(process.cwd(), route.filePath);
1057
+ try {
1058
+ await import(absolutePath);
1059
+ } catch (error) {
1060
+ diagnostics.importFailures.push({
1061
+ filePath: route.filePath,
1062
+ reason: formatErrorReason(error)
1063
+ });
1064
+ }
1065
+ if (isHandlerRegistered(topicName, route.consumer)) continue;
1066
+ }
1067
+ for (const port of diagnostics.listeningPorts) {
1068
+ const url = `http://localhost:${port}${filePathToUrlPath(route.filePath)}`;
1069
+ try {
1070
+ const response = await fetch(url, {
1071
+ method: "POST",
1072
+ headers: {
1073
+ "x-vercel-queue-prime": "1",
1074
+ "x-vercel-queue-prime-file": route.filePath
1075
+ }
1076
+ });
1077
+ try {
1078
+ await response.text();
1079
+ } catch {
1080
+ }
1081
+ if (isHandlerRegistered(topicName, route.consumer)) {
1082
+ break;
1083
+ }
1084
+ diagnostics.primeFailures.push({
1085
+ filePath: route.filePath,
1086
+ url,
1087
+ reason: `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`.trim()
1088
+ });
1089
+ } catch (error) {
1090
+ diagnostics.primeFailures.push({
1091
+ filePath: route.filePath,
1092
+ url,
1093
+ reason: formatErrorReason(error)
1094
+ });
800
1095
  }
801
- console.error(
802
- `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
803
- error
804
- );
805
- return false;
806
1096
  }
807
1097
  }
808
- console.warn(
809
- `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
1098
+ return diagnostics;
1099
+ }
1100
+ function buildNoHandlerWarning(topicName, routes, diagnostics) {
1101
+ const files = routes.map((r) => r.filePath);
1102
+ const suggestedPort = diagnostics.listeningPorts[0] ?? diagnostics.triedPorts[0];
1103
+ const suggestedUrls = suggestedPort ? routes.map(
1104
+ (r) => `http://localhost:${suggestedPort}${filePathToUrlPath(r.filePath)}`
1105
+ ) : [];
1106
+ let portSummary;
1107
+ if (diagnostics.triedPorts.length === 0) {
1108
+ portSummary = "No local dev port detected from env. Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT).";
1109
+ } else if (diagnostics.listeningPorts.length === 0) {
1110
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}], but none are listening.`;
1111
+ } else {
1112
+ const unavailable = diagnostics.unavailablePorts.length > 0 ? ` Not listening: [${diagnostics.unavailablePorts.join(", ")}].` : "";
1113
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}]. Listening: [${diagnostics.listeningPorts.join(", ")}].` + unavailable;
1114
+ }
1115
+ const importSummary = diagnostics.importFailures.length > 0 ? `
1116
+ Import failures: ` + diagnostics.importFailures.slice(0, 2).map((f) => `${f.filePath} (${f.reason})`).join("; ") : "";
1117
+ const primeSummary = diagnostics.primeFailures.length > 0 ? `
1118
+ Prime failures: ` + diagnostics.primeFailures.slice(0, 3).map((f) => `${f.url} (${f.reason})`).join("; ") : "";
1119
+ return `[Dev Mode] No registered handler for topic "${topicName}". vercel.json maps this topic to [${files.join(", ")}] but auto-loading failed.
1120
+ ${portSummary}${importSummary}${primeSummary}
1121
+ Ensure your dev server is running, set PORT if needed, and confirm mapped route files call handleCallback()/handleNodeCallback() at module scope.
1122
+ ` + (suggestedUrls.length > 0 ? `Try opening: ${suggestedUrls.join(" or ")}` : "Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT) and try sending again.");
1123
+ }
1124
+ function isHandlerRegistered(topicName, consumerGroup) {
1125
+ return lookupHandlers(topicName).some(
1126
+ (h) => h.consumerGroup === consumerGroup
810
1127
  );
811
- return false;
812
1128
  }
813
- function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
1129
+ function invokeDevHandlers(topicName, messageId, region, delaySeconds) {
814
1130
  if (delaySeconds && delaySeconds > 0) {
815
1131
  console.log(
816
1132
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
817
1133
  );
818
1134
  setTimeout(() => {
819
- triggerDevCallbacks(topicName, messageId, region);
1135
+ invokeDevHandlers(topicName, messageId, region);
820
1136
  }, delaySeconds * 1e3);
821
1137
  return;
822
1138
  }
823
1139
  console.log(
824
1140
  `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
825
1141
  );
826
- const matchingRoutes = findMatchingRoutes(topicName);
827
- if (matchingRoutes.length === 0) {
828
- console.log(
829
- `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
830
- );
831
- return;
832
- }
833
- const consumerGroups = matchingRoutes.map((r) => r.consumer);
834
- console.log(
835
- `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
836
- );
837
1142
  (async () => {
838
- const firstRoute = matchingRoutes[0];
839
- const isVisible = await waitForMessageVisibility(
840
- topicName,
841
- firstRoute.consumer,
842
- messageId,
843
- region
844
- );
845
- if (!isVisible) {
846
- console.warn(
847
- `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
848
- );
1143
+ let handlers = lookupHandlers(topicName);
1144
+ let diagnostics = null;
1145
+ if (handlers.length > 0) {
1146
+ await ensureHandlersLoaded(topicName, { refreshRegistered: true });
1147
+ handlers = lookupHandlers(topicName);
1148
+ } else {
1149
+ diagnostics = await ensureHandlersLoaded(topicName);
1150
+ handlers = lookupHandlers(topicName);
1151
+ }
1152
+ if (handlers.length === 0) {
1153
+ const matchingRoutes = findMatchingRoutes(topicName);
1154
+ if (matchingRoutes.length > 0) {
1155
+ const safeDiagnostics = diagnostics ?? {
1156
+ triedPorts: collectPrimePorts(),
1157
+ listeningPorts: [],
1158
+ unavailablePorts: [],
1159
+ importFailures: [],
1160
+ primeFailures: []
1161
+ };
1162
+ console.warn(
1163
+ buildNoHandlerWarning(topicName, matchingRoutes, safeDiagnostics)
1164
+ );
1165
+ } else {
1166
+ console.warn(
1167
+ `[Dev Mode] No registered handler for topic "${topicName}".
1168
+ Ensure vercel.json has a matching experimentalTriggers entry and the route file calls handleCallback().`
1169
+ );
1170
+ }
849
1171
  return;
850
1172
  }
851
- const port = process.env.PORT || 3e3;
852
- const baseUrl = `http://localhost:${port}`;
853
- for (const route of matchingRoutes) {
854
- const url = `${baseUrl}${route.urlPath}`;
855
- console.log(
856
- `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
857
- );
1173
+ const consumerGroups = handlers.map((h) => h.consumerGroup);
1174
+ console.log(
1175
+ `[Dev Mode] Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
1176
+ );
1177
+ for (const entry of handlers) {
1178
+ const request = {
1179
+ queueName: topicName,
1180
+ consumerGroup: entry.consumerGroup,
1181
+ messageId,
1182
+ region
1183
+ };
1184
+ const callbackOptions = {
1185
+ client: entry.client,
1186
+ visibilityTimeoutSeconds: entry.options?.visibilityTimeoutSeconds,
1187
+ retry: entry.options?.retry
1188
+ };
858
1189
  try {
859
- const response = await fetch(url, {
860
- method: "POST",
861
- headers: {
862
- "ce-type": CLOUD_EVENT_TYPE_V2BETA,
863
- "ce-vqsqueuename": topicName,
864
- "ce-vqsconsumergroup": route.consumer,
865
- "ce-vqsmessageid": messageId,
866
- "ce-vqsregion": region
867
- }
868
- });
869
- if (response.ok) {
870
- try {
871
- const responseData = await response.json();
872
- if (responseData.status === "success") {
873
- console.log(
874
- `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
875
- );
876
- }
877
- } catch {
878
- console.warn(
879
- `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
880
- );
881
- }
882
- } else {
883
- try {
884
- const errorData = await response.json();
885
- console.error(
886
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
887
- );
888
- } catch {
889
- console.error(
890
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
891
- );
892
- }
893
- }
1190
+ await invokeWithRetry(entry.handler, request, callbackOptions);
1191
+ console.log(
1192
+ `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1193
+ );
894
1194
  } catch (error) {
895
1195
  console.error(
896
- `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
1196
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
897
1197
  error
898
1198
  );
899
1199
  }
900
1200
  }
901
1201
  })();
902
1202
  }
903
- function clearDevRouteMappings() {
1203
+ function clearDevState() {
904
1204
  const g = globalThis;
905
1205
  delete g[ROUTE_MAPPINGS_KEY];
1206
+ delete g[HANDLER_REGISTRY_KEY];
906
1207
  }
907
1208
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
908
- globalThis.__clearDevRouteMappings = clearDevRouteMappings;
1209
+ globalThis.__clearDevState = clearDevState;
909
1210
  }
910
1211
 
911
1212
  // src/oidc.ts
@@ -1022,7 +1323,7 @@ var ApiClient = class _ApiClient {
1022
1323
  return;
1023
1324
  }
1024
1325
  throw new Error(
1025
- '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 })'
1326
+ '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 })'
1026
1327
  );
1027
1328
  }
1028
1329
  getSendDeploymentId() {
@@ -1080,7 +1381,7 @@ var ApiClient = class _ApiClient {
1080
1381
  }
1081
1382
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1082
1383
  }
1083
- init.headers.set("User-Agent", `@vercel/queue/${"0.0.2"}`);
1384
+ init.headers.set("User-Agent", `@vercel/queue/${"0.1.1"}`);
1084
1385
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1085
1386
  const response = await fetch(url, init);
1086
1387
  if (isDebugEnabled()) {
@@ -1416,23 +1717,61 @@ var ApiClient = class _ApiClient {
1416
1717
 
1417
1718
  // src/client.ts
1418
1719
  var apiClients = /* @__PURE__ */ new WeakMap();
1419
- function getApiClient(client) {
1720
+ var API_CLIENT_KEY = Symbol.for("@vercel/queue.apiClient");
1721
+ function setApi(client, api) {
1722
+ apiClients.set(client, api);
1723
+ Object.defineProperty(client, API_CLIENT_KEY, {
1724
+ value: api,
1725
+ writable: false,
1726
+ enumerable: false,
1727
+ configurable: false
1728
+ });
1729
+ }
1730
+ function getApi(client) {
1420
1731
  const api = apiClients.get(client);
1421
- if (!api) {
1422
- throw new Error("QueueClient not initialized");
1732
+ if (api) {
1733
+ return api;
1734
+ }
1735
+ const apiFromSymbol = client[API_CLIENT_KEY];
1736
+ if (typeof apiFromSymbol === "object" && apiFromSymbol !== null) {
1737
+ const resolvedApi = apiFromSymbol;
1738
+ apiClients.set(client, resolvedApi);
1739
+ return resolvedApi;
1423
1740
  }
1424
- return api;
1741
+ throw new Error(
1742
+ "QueueClient not initialized. This may happen when multiple bundled copies of @vercel/queue are loaded in local dev."
1743
+ );
1744
+ }
1745
+ function resolveCallbackRequest(input) {
1746
+ if ("request" in input) {
1747
+ return input.request;
1748
+ }
1749
+ return input;
1750
+ }
1751
+ function getApiClient(client) {
1752
+ return getApi(client);
1753
+ }
1754
+ var DEFAULT_REGION = "iad1";
1755
+ function resolveRegion(region) {
1756
+ if (region) return region;
1757
+ const fromEnv = process.env.VERCEL_REGION;
1758
+ if (fromEnv) return fromEnv;
1759
+ console.warn(
1760
+ `[QueueClient] Region not detected \u2014 defaulting to "${DEFAULT_REGION}". On Vercel this is set automatically via VERCEL_REGION. To silence this warning, pass region explicitly: new QueueClient({ region: "iad1" })`
1761
+ );
1762
+ return DEFAULT_REGION;
1425
1763
  }
1426
1764
  var QueueClient = class {
1427
- constructor(options) {
1428
- apiClients.set(this, new ApiClient(options));
1765
+ constructor(options = {}) {
1766
+ const region = resolveRegion(options.region);
1767
+ setApi(this, new ApiClient({ ...options, region }));
1429
1768
  }
1430
1769
  /**
1431
1770
  * Send a message to a topic.
1432
1771
  *
1433
1772
  * This is an arrow function property so it can be destructured:
1434
1773
  * ```typescript
1435
- * const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
1774
+ * const { send } = new QueueClient();
1436
1775
  * await send("my-topic", payload);
1437
1776
  * ```
1438
1777
  *
@@ -1443,7 +1782,7 @@ var QueueClient = class {
1443
1782
  * the message for deferred processing (no ID available yet)
1444
1783
  */
1445
1784
  send = async (topicName, payload, options) => {
1446
- const api = getApiClient(this);
1785
+ const api = getApi(this);
1447
1786
  const result = await api.sendMessage({
1448
1787
  queueName: topicName,
1449
1788
  payload,
@@ -1453,7 +1792,7 @@ var QueueClient = class {
1453
1792
  headers: options?.headers
1454
1793
  });
1455
1794
  if (result.messageId && isDevMode()) {
1456
- triggerDevCallbacks(
1795
+ invokeDevHandlers(
1457
1796
  topicName,
1458
1797
  result.messageId,
1459
1798
  api.getRegion(),
@@ -1462,75 +1801,6 @@ var QueueClient = class {
1462
1801
  }
1463
1802
  return { messageId: result.messageId };
1464
1803
  };
1465
- /**
1466
- * Receive and process messages from a topic.
1467
- *
1468
- * Each message is automatically locked, kept alive via periodic visibility
1469
- * extensions during processing, and acknowledged upon successful handler completion.
1470
- * The handler is not called when the queue is empty — check `result.ok` instead.
1471
- *
1472
- * This is an arrow function property so it can be destructured:
1473
- * ```typescript
1474
- * const { receive } = new QueueClient({ region: process.env.QUEUE_REGION! });
1475
- * const result = await receive("my-topic", "my-group", handler);
1476
- * if (!result.ok) console.log(result.reason);
1477
- * ```
1478
- *
1479
- * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1480
- * @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1481
- * @param handler - Function to process each message payload and metadata.
1482
- * Not called when the queue is empty.
1483
- * @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
1484
- * @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
1485
- */
1486
- receive = async (topicName, consumerGroup, handler, options) => {
1487
- const api = getApiClient(this);
1488
- const topic = new Topic(api, topicName);
1489
- const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
1490
- const consumer = topic.consumerGroup(
1491
- consumerGroup,
1492
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
1493
- );
1494
- try {
1495
- let count;
1496
- const retry = options?.retry;
1497
- if (options && "messageId" in options) {
1498
- count = await consumer.consume(handler, {
1499
- messageId: options.messageId,
1500
- retry
1501
- });
1502
- } else {
1503
- const limit = options && "limit" in options ? options.limit : void 0;
1504
- count = await consumer.consume(handler, {
1505
- ...limit !== void 0 ? { limit } : {},
1506
- retry
1507
- });
1508
- }
1509
- if (count === 0) {
1510
- return { ok: false, reason: "empty" };
1511
- }
1512
- return { ok: true };
1513
- } catch (error) {
1514
- if (options && "messageId" in options && error instanceof MessageNotFoundError) {
1515
- return { ok: false, reason: "not_found", messageId: options.messageId };
1516
- }
1517
- if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
1518
- return {
1519
- ok: false,
1520
- reason: "not_available",
1521
- messageId: options.messageId
1522
- };
1523
- }
1524
- if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
1525
- return {
1526
- ok: false,
1527
- reason: "already_processed",
1528
- messageId: options.messageId
1529
- };
1530
- }
1531
- throw error;
1532
- }
1533
- };
1534
1804
  /**
1535
1805
  * Create a Web API route handler for processing queue callback messages.
1536
1806
  *
@@ -1539,7 +1809,7 @@ var QueueClient = class {
1539
1809
  *
1540
1810
  * This is an arrow function property so it can be destructured:
1541
1811
  * ```typescript
1542
- * const { handleCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1812
+ * const { handleCallback } = new QueueClient();
1543
1813
  * export const POST = handleCallback(handler);
1544
1814
  * ```
1545
1815
  *
@@ -1548,10 +1818,26 @@ var QueueClient = class {
1548
1818
  * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1549
1819
  * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1550
1820
  * reschedule the message for redelivery after N seconds.
1551
- * @returns A `(request: Request) => Promise<Response>` route handler
1821
+ * @returns A route handler that accepts either `Request` or `{ request: Request }`
1552
1822
  */
1553
1823
  handleCallback = (handler, options) => {
1554
- return async (request) => {
1824
+ if (isDevMode()) {
1825
+ registerDevHandler(handler, this, options);
1826
+ }
1827
+ return async (requestOrEvent) => {
1828
+ const request = resolveCallbackRequest(requestOrEvent);
1829
+ if (isDevMode() && request.headers.get("x-vercel-queue-prime") === "1") {
1830
+ const primeFile = request.headers.get("x-vercel-queue-prime-file");
1831
+ if (primeFile) {
1832
+ registerDevHandler(
1833
+ handler,
1834
+ this,
1835
+ options,
1836
+ primeFile
1837
+ );
1838
+ }
1839
+ return Response.json({ status: "primed" });
1840
+ }
1555
1841
  try {
1556
1842
  const parsed = await parseCallback(request);
1557
1843
  await handleCallback(handler, parsed, {
@@ -1581,7 +1867,7 @@ var QueueClient = class {
1581
1867
  *
1582
1868
  * This is an arrow function property so it can be destructured:
1583
1869
  * ```typescript
1584
- * const { handleNodeCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1870
+ * const { handleNodeCallback } = new QueueClient();
1585
1871
  * app.post("/api/queue", handleNodeCallback(handler));
1586
1872
  * ```
1587
1873
  *
@@ -1593,11 +1879,29 @@ var QueueClient = class {
1593
1879
  * @returns A `(req, res) => Promise<void>` route handler
1594
1880
  */
1595
1881
  handleNodeCallback = (handler, options) => {
1882
+ if (isDevMode()) {
1883
+ registerDevHandler(handler, this, options);
1884
+ }
1596
1885
  return async (req, res) => {
1597
1886
  if (req.method !== "POST") {
1598
1887
  res.status(200).end();
1599
1888
  return;
1600
1889
  }
1890
+ const primeHeader = req.headers["x-vercel-queue-prime"];
1891
+ if (isDevMode() && primeHeader === "1") {
1892
+ const primeFileHeader = req.headers["x-vercel-queue-prime-file"];
1893
+ const primeFile = Array.isArray(primeFileHeader) ? primeFileHeader[0] : primeFileHeader;
1894
+ if (primeFile) {
1895
+ registerDevHandler(
1896
+ handler,
1897
+ this,
1898
+ options,
1899
+ primeFile
1900
+ );
1901
+ }
1902
+ res.status(200).json({ status: "primed" });
1903
+ return;
1904
+ }
1601
1905
  try {
1602
1906
  const parsed = parseRawCallback(req.body, req.headers);
1603
1907
  await handleCallback(handler, parsed, {
@@ -1617,6 +1921,126 @@ var QueueClient = class {
1617
1921
  };
1618
1922
  };
1619
1923
  };
1924
+ var PollingQueueClient = class {
1925
+ constructor(options) {
1926
+ setApi(this, new ApiClient(options));
1927
+ }
1928
+ /**
1929
+ * Send a message to a topic.
1930
+ *
1931
+ * This is an arrow function property so it can be destructured:
1932
+ * ```typescript
1933
+ * const { send } = new PollingQueueClient({ region: "iad1" });
1934
+ * await send("my-topic", payload);
1935
+ * ```
1936
+ *
1937
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1938
+ * @param payload - The data to send (serialized via the configured transport)
1939
+ * @param options - Optional send options (idempotencyKey, retentionSeconds, delaySeconds, headers)
1940
+ * @returns `{ messageId }` — `messageId` is `null` when the server accepted
1941
+ * the message for deferred processing (no ID available yet)
1942
+ */
1943
+ send = async (topicName, payload, options) => {
1944
+ const api = getApi(this);
1945
+ const result = await api.sendMessage({
1946
+ queueName: topicName,
1947
+ payload,
1948
+ idempotencyKey: options?.idempotencyKey,
1949
+ retentionSeconds: options?.retentionSeconds,
1950
+ delaySeconds: options?.delaySeconds,
1951
+ headers: options?.headers
1952
+ });
1953
+ return { messageId: result.messageId };
1954
+ };
1955
+ /**
1956
+ * Receive and process messages from a topic.
1957
+ *
1958
+ * Each message is automatically locked, kept alive via periodic visibility
1959
+ * extensions during processing, and acknowledged upon successful handler completion.
1960
+ * The handler is not called when the queue is empty — check `result.ok` instead.
1961
+ *
1962
+ * This is an arrow function property so it can be destructured:
1963
+ * ```typescript
1964
+ * const { receive } = new PollingQueueClient({ region: "iad1" });
1965
+ * const result = await receive("my-topic", "my-group", handler);
1966
+ * if (!result.ok) console.log(result.reason);
1967
+ * ```
1968
+ *
1969
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1970
+ * @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1971
+ * @param handler - Function to process each message payload and metadata.
1972
+ * Not called when the queue is empty.
1973
+ * @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
1974
+ * @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
1975
+ */
1976
+ receive = async (topicName, consumerGroup, handler, options) => {
1977
+ const api = getApi(this);
1978
+ const topic = new Topic(api, topicName);
1979
+ const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
1980
+ const consumer = topic.consumerGroup(
1981
+ consumerGroup,
1982
+ visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
1983
+ );
1984
+ try {
1985
+ let count;
1986
+ const retry = options?.retry;
1987
+ if (options && "messageId" in options) {
1988
+ count = await consumer.consume(handler, {
1989
+ messageId: options.messageId,
1990
+ retry
1991
+ });
1992
+ } else {
1993
+ const limit = options && "limit" in options ? options.limit : void 0;
1994
+ count = await consumer.consume(handler, {
1995
+ ...limit !== void 0 ? { limit } : {},
1996
+ retry
1997
+ });
1998
+ }
1999
+ if (count === 0) {
2000
+ return { ok: false, reason: "empty" };
2001
+ }
2002
+ return { ok: true };
2003
+ } catch (error) {
2004
+ if (options && "messageId" in options && error instanceof MessageNotFoundError) {
2005
+ return { ok: false, reason: "not_found", messageId: options.messageId };
2006
+ }
2007
+ if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
2008
+ return {
2009
+ ok: false,
2010
+ reason: "not_available",
2011
+ messageId: options.messageId
2012
+ };
2013
+ }
2014
+ if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
2015
+ return {
2016
+ ok: false,
2017
+ reason: "already_processed",
2018
+ messageId: options.messageId
2019
+ };
2020
+ }
2021
+ throw error;
2022
+ }
2023
+ };
2024
+ };
2025
+
2026
+ // src/default-client.ts
2027
+ var _defaultClient;
2028
+ function getDefaultClient() {
2029
+ if (!_defaultClient) {
2030
+ _defaultClient = new QueueClient();
2031
+ }
2032
+ return _defaultClient;
2033
+ }
2034
+ function resolveClient(region) {
2035
+ if (!region) return getDefaultClient();
2036
+ return new QueueClient({ region });
2037
+ }
2038
+ async function send(topicName, payload, options) {
2039
+ return resolveClient(options?.region).send(topicName, payload, options);
2040
+ }
2041
+ function handleCallback2(handler, options) {
2042
+ return getDefaultClient().handleCallback(handler, options);
2043
+ }
1620
2044
  export {
1621
2045
  BadRequestError,
1622
2046
  BufferTransport,
@@ -1634,11 +2058,14 @@ export {
1634
2058
  MessageLockedError,
1635
2059
  MessageNotAvailableError,
1636
2060
  MessageNotFoundError,
2061
+ PollingQueueClient,
1637
2062
  QueueClient,
1638
2063
  QueueEmptyError,
1639
2064
  StreamTransport,
1640
2065
  UnauthorizedError,
2066
+ handleCallback2 as handleCallback,
1641
2067
  parseCallback,
1642
- parseRawCallback
2068
+ parseRawCallback,
2069
+ send
1643
2070
  };
1644
2071
  //# sourceMappingURL=index.mjs.map