@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.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;
768
866
  }
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 });
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);
947
+ }
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
@@ -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.1.0"}`);
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,12 +1717,36 @@ var ApiClient = class _ApiClient {
1416
1717
 
1417
1718
  // src/client.ts
1418
1719
  var apiClients = /* @__PURE__ */ new WeakMap();
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
+ }
1419
1730
  function getApi(client) {
1420
1731
  const api = apiClients.get(client);
1421
- if (!api) {
1422
- throw new Error("Client 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;
1740
+ }
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;
1423
1748
  }
1424
- return api;
1749
+ return input;
1425
1750
  }
1426
1751
  function getApiClient(client) {
1427
1752
  return getApi(client);
@@ -1439,7 +1764,7 @@ function resolveRegion(region) {
1439
1764
  var QueueClient = class {
1440
1765
  constructor(options = {}) {
1441
1766
  const region = resolveRegion(options.region);
1442
- apiClients.set(this, new ApiClient({ ...options, region }));
1767
+ setApi(this, new ApiClient({ ...options, region }));
1443
1768
  }
1444
1769
  /**
1445
1770
  * Send a message to a topic.
@@ -1467,7 +1792,7 @@ var QueueClient = class {
1467
1792
  headers: options?.headers
1468
1793
  });
1469
1794
  if (result.messageId && isDevMode()) {
1470
- triggerDevCallbacks(
1795
+ invokeDevHandlers(
1471
1796
  topicName,
1472
1797
  result.messageId,
1473
1798
  api.getRegion(),
@@ -1493,10 +1818,26 @@ var QueueClient = class {
1493
1818
  * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1494
1819
  * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1495
1820
  * reschedule the message for redelivery after N seconds.
1496
- * @returns A `(request: Request) => Promise<Response>` route handler
1821
+ * @returns A route handler that accepts either `Request` or `{ request: Request }`
1497
1822
  */
1498
1823
  handleCallback = (handler, options) => {
1499
- 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
+ }
1500
1841
  try {
1501
1842
  const parsed = await parseCallback(request);
1502
1843
  await handleCallback(handler, parsed, {
@@ -1538,11 +1879,29 @@ var QueueClient = class {
1538
1879
  * @returns A `(req, res) => Promise<void>` route handler
1539
1880
  */
1540
1881
  handleNodeCallback = (handler, options) => {
1882
+ if (isDevMode()) {
1883
+ registerDevHandler(handler, this, options);
1884
+ }
1541
1885
  return async (req, res) => {
1542
1886
  if (req.method !== "POST") {
1543
1887
  res.status(200).end();
1544
1888
  return;
1545
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
+ }
1546
1905
  try {
1547
1906
  const parsed = parseRawCallback(req.body, req.headers);
1548
1907
  await handleCallback(handler, parsed, {
@@ -1564,7 +1923,7 @@ var QueueClient = class {
1564
1923
  };
1565
1924
  var PollingQueueClient = class {
1566
1925
  constructor(options) {
1567
- apiClients.set(this, new ApiClient(options));
1926
+ setApi(this, new ApiClient(options));
1568
1927
  }
1569
1928
  /**
1570
1929
  * Send a message to a topic.
@@ -1663,6 +2022,25 @@ var PollingQueueClient = class {
1663
2022
  }
1664
2023
  };
1665
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
+ }
1666
2044
  export {
1667
2045
  BadRequestError,
1668
2046
  BufferTransport,
@@ -1685,7 +2063,9 @@ export {
1685
2063
  QueueEmptyError,
1686
2064
  StreamTransport,
1687
2065
  UnauthorizedError,
2066
+ handleCallback2 as handleCallback,
1688
2067
  parseCallback,
1689
- parseRawCallback
2068
+ parseRawCallback,
2069
+ send
1690
2070
  };
1691
2071
  //# sourceMappingURL=index.mjs.map