@vercel/queue 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -76,7 +76,9 @@ 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";
81
+ import { minimatch } from "minimatch";
80
82
 
81
83
  // src/types.ts
82
84
  var MessageNotFoundError = class extends Error {
@@ -261,8 +263,8 @@ var ConsumerGroup = class {
261
263
  firstDelayMs = 0;
262
264
  }
263
265
  }
264
- const lifecyclePromise = new Promise((resolve) => {
265
- resolveLifecycle = resolve;
266
+ const lifecyclePromise = new Promise((resolve2) => {
267
+ resolveLifecycle = resolve2;
266
268
  });
267
269
  const safeResolve = () => {
268
270
  if (!isResolved) {
@@ -339,11 +341,12 @@ var ConsumerGroup = class {
339
341
  message.receiptHandle,
340
342
  options
341
343
  );
344
+ const DEFAULT_RETENTION_MS = 864e5;
342
345
  const metadata = {
343
346
  messageId: message.messageId,
344
347
  deliveryCount: message.deliveryCount,
345
348
  createdAt: message.createdAt,
346
- expiresAt: message.expiresAt,
349
+ expiresAt: message.expiresAt ?? new Date(message.createdAt.getTime() + DEFAULT_RETENTION_MS),
347
350
  topicName: this.topicName,
348
351
  consumerGroup: this.consumerGroupName,
349
352
  region: this.client.getRegion()
@@ -494,10 +497,12 @@ var Topic = class {
494
497
  headers: options?.headers
495
498
  });
496
499
  if (result.messageId && isDevMode()) {
497
- triggerDevCallbacks(
500
+ invokeDevHandlers(
498
501
  this.topicName,
499
502
  result.messageId,
500
- this.client.getRegion()
503
+ this.client.getRegion(),
504
+ options?.delaySeconds,
505
+ options?.retentionSeconds
501
506
  );
502
507
  }
503
508
  return { messageId: result.messageId };
@@ -703,16 +708,26 @@ async function handleCallback(handler, request, options) {
703
708
  }
704
709
 
705
710
  // 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;
711
+ function isDevMode() {
712
+ return process.env.NODE_ENV === "development";
713
713
  }
714
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
714
715
  function filePathToConsumerGroup(filePath) {
715
- return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
716
+ let result = "";
717
+ for (const char of filePath) {
718
+ if (char === "_") {
719
+ result += "__";
720
+ } else if (char === "/") {
721
+ result += "_S";
722
+ } else if (char === ".") {
723
+ result += "_D";
724
+ } else if (/[A-Za-z0-9-]/.test(char)) {
725
+ result += char;
726
+ } else {
727
+ result += "_" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
728
+ }
729
+ }
730
+ return result;
716
731
  }
717
732
  function getDevRouteMappings() {
718
733
  const g = globalThis;
@@ -734,13 +749,19 @@ function getDevRouteMappings() {
734
749
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
735
750
  if (!config.experimentalTriggers) continue;
736
751
  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
- });
752
+ if (!trigger.type?.startsWith("queue/") || !trigger.topic) continue;
753
+ if (trigger.type !== "queue/v2beta") {
754
+ console.warn(
755
+ `[Dev Mode] Unsupported trigger type "${trigger.type}" for topic "${trigger.topic}" in ${filePath}. Use "queue/v2beta" instead.`
756
+ );
757
+ continue;
743
758
  }
759
+ mappings.push({
760
+ filePath,
761
+ topic: trigger.topic,
762
+ consumer: filePathToConsumerGroup(filePath),
763
+ retryAfterSeconds: trigger.retryAfterSeconds
764
+ });
744
765
  }
745
766
  }
746
767
  g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
@@ -753,9 +774,7 @@ function getDevRouteMappings() {
753
774
  }
754
775
  function findMatchingRoutes(topicName) {
755
776
  const mappings = getDevRouteMappings();
756
- if (!mappings) {
757
- return [];
758
- }
777
+ if (!mappings) return [];
759
778
  return mappings.filter((mapping) => {
760
779
  if (mapping.topic.includes("*")) {
761
780
  return matchesWildcardPattern(topicName, mapping.topic);
@@ -763,149 +782,607 @@ function findMatchingRoutes(topicName) {
763
782
  return mapping.topic === topicName;
764
783
  });
765
784
  }
766
- function isDevMode() {
767
- return process.env.NODE_ENV === "development";
785
+ function findRetryAfterSeconds(topicName, consumerGroup) {
786
+ const routes = findMatchingRoutes(topicName);
787
+ const route = routes.find((r) => r.consumer === consumerGroup);
788
+ return route?.retryAfterSeconds;
789
+ }
790
+ function stripSrcPrefix(filePath) {
791
+ if (/^src\/(app|pages|server)\//.test(filePath)) {
792
+ return filePath.slice(4);
793
+ }
794
+ return null;
795
+ }
796
+ function matchesFunctionsPattern(sourceFile, pattern) {
797
+ return sourceFile === pattern || minimatch(sourceFile, pattern);
798
+ }
799
+ function findMappingsForFile(absolutePath) {
800
+ const mappings = getDevRouteMappings();
801
+ if (!mappings) return [];
802
+ const cwd = process.cwd();
803
+ let relative2;
804
+ try {
805
+ relative2 = path.relative(cwd, absolutePath);
806
+ } catch {
807
+ return [];
808
+ }
809
+ const normalized = relative2.replace(/\\/g, "/");
810
+ const stripped = stripSrcPrefix(normalized);
811
+ return mappings.filter(
812
+ (m) => matchesFunctionsPattern(normalized, m.filePath) || stripped !== null && matchesFunctionsPattern(stripped, m.filePath)
813
+ );
814
+ }
815
+ function parseFrameFilePath(line) {
816
+ let match = line.match(/\((.+?):\d+:\d+\)/);
817
+ if (!match) match = line.match(/at\s+(.+?):\d+:\d+/);
818
+ if (!match) return null;
819
+ let filePath = match[1].trim();
820
+ if (filePath === "native" || filePath.startsWith("node:") || filePath.startsWith("internal")) {
821
+ return null;
822
+ }
823
+ if (filePath.startsWith("file://")) {
824
+ try {
825
+ filePath = new URL(filePath).pathname;
826
+ } catch {
827
+ return null;
828
+ }
829
+ }
830
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(filePath)) {
831
+ return null;
832
+ }
833
+ if (filePath.startsWith("./")) {
834
+ filePath = filePath.slice(2);
835
+ }
836
+ return filePath;
837
+ }
838
+ var _sdkPackageDir;
839
+ function getSdkPackageDir() {
840
+ if (_sdkPackageDir) return _sdkPackageDir;
841
+ try {
842
+ const thisDir = typeof __dirname !== "undefined" ? __dirname : path.dirname(new URL(import.meta.url).pathname);
843
+ _sdkPackageDir = path.resolve(thisDir, "..");
844
+ } catch {
845
+ _sdkPackageDir = "";
846
+ }
847
+ return _sdkPackageDir;
848
+ }
849
+ function extractCallerFilePath() {
850
+ const stack = new Error().stack;
851
+ if (!stack) return null;
852
+ const lines = stack.split("\n").slice(1);
853
+ const pkgDir = getSdkPackageDir();
854
+ for (const line of lines) {
855
+ const fp = parseFrameFilePath(line);
856
+ if (!fp) continue;
857
+ const absolute = path.isAbsolute(fp) ? fp : path.resolve(process.cwd(), fp);
858
+ let realFp;
859
+ try {
860
+ realFp = fs.realpathSync(absolute);
861
+ } catch {
862
+ realFp = absolute;
863
+ }
864
+ if (pkgDir && realFp.startsWith(pkgDir)) continue;
865
+ return realFp;
866
+ }
867
+ return null;
868
+ }
869
+ var HANDLER_REGISTRY_KEY = Symbol.for("@vercel/queue.devHandlerRegistry");
870
+ function getHandlerRegistry() {
871
+ const g = globalThis;
872
+ if (!g[HANDLER_REGISTRY_KEY]) {
873
+ g[HANDLER_REGISTRY_KEY] = /* @__PURE__ */ new Map();
874
+ }
875
+ return g[HANDLER_REGISTRY_KEY];
876
+ }
877
+ function registerHandlerForFile(filePath, handler, client, options) {
878
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath);
879
+ const fileMappings = findMappingsForFile(absolutePath);
880
+ if (fileMappings.length === 0) return false;
881
+ const registry = getHandlerRegistry();
882
+ for (const mapping of fileMappings) {
883
+ const key = mapping.topic;
884
+ const existing = registry.get(key) ?? [];
885
+ const nextEntry = {
886
+ consumerGroup: mapping.consumer,
887
+ handler,
888
+ client,
889
+ options
890
+ };
891
+ const existingIndex = existing.findIndex(
892
+ (e) => e.consumerGroup === mapping.consumer
893
+ );
894
+ if (existingIndex >= 0) {
895
+ existing[existingIndex] = nextEntry;
896
+ } else {
897
+ existing.push(nextEntry);
898
+ }
899
+ registry.set(key, existing);
900
+ }
901
+ return true;
902
+ }
903
+ function registerDevHandler(handler, client, options, _testCallerPath) {
904
+ const callerPath = _testCallerPath ?? extractCallerFilePath();
905
+ if (!callerPath) {
906
+ console.warn(
907
+ "[Dev Mode] Could not determine caller file path for handler registration."
908
+ );
909
+ return;
910
+ }
911
+ const registered = registerHandlerForFile(
912
+ callerPath,
913
+ handler,
914
+ client,
915
+ options
916
+ );
917
+ if (!registered) {
918
+ const allMappings = getDevRouteMappings();
919
+ if (allMappings && allMappings.length > 0) {
920
+ return;
921
+ }
922
+ const cwd = process.cwd();
923
+ let relative2;
924
+ try {
925
+ relative2 = path.relative(cwd, callerPath).replace(/\\/g, "/");
926
+ } catch {
927
+ relative2 = callerPath;
928
+ }
929
+ console.warn(
930
+ `[Dev Mode] handleCallback() in ${relative2} has no matching experimentalTriggers in vercel.json. This handler won't receive messages.
931
+
932
+ Add a trigger to vercel.json:
933
+ "${relative2}": {
934
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "your-topic" }]
935
+ }`
936
+ );
937
+ }
938
+ }
939
+ function lookupHandlers(topicName) {
940
+ const registry = getHandlerRegistry();
941
+ const result = [];
942
+ for (const [pattern, handlers] of registry) {
943
+ const matches = pattern.includes("*") ? matchesWildcardPattern(topicName, pattern) : pattern === topicName;
944
+ if (matches) {
945
+ result.push(...handlers);
946
+ }
947
+ }
948
+ return result;
949
+ }
950
+ var DEV_RETRY_INITIAL_DELAY_MS = 50;
951
+ var DEV_RETRY_MAX_WAIT_MS = 5e3;
952
+ var DEV_RETRY_BACKOFF = 2;
953
+ var PORT_CHECK_TIMEOUT_MS = 250;
954
+ var PRIME_PORT_ENV_KEYS = [
955
+ "PORT",
956
+ "NEXT_PORT",
957
+ "NEXTJS_PORT",
958
+ "NUXT_PORT",
959
+ "NITRO_PORT",
960
+ "SVELTEKIT_PORT",
961
+ "VITE_PORT",
962
+ "DEV_PORT",
963
+ "npm_config_port"
964
+ ];
965
+ var PRIME_URL_ENV_KEYS = [
966
+ "__NEXT_PRIVATE_ORIGIN",
967
+ "NUXT_PUBLIC_SITE_URL",
968
+ "URL"
969
+ ];
970
+ function formatErrorReason(error) {
971
+ if (error instanceof Error) {
972
+ return error.message;
973
+ }
974
+ return String(error);
975
+ }
976
+ function isMessageNotFoundError(error) {
977
+ if (error instanceof MessageNotFoundError) {
978
+ return true;
979
+ }
980
+ if (error instanceof Error && error.name === "MessageNotFoundError") {
981
+ return true;
982
+ }
983
+ return false;
984
+ }
985
+ function parsePort(value) {
986
+ if (!value) return null;
987
+ const parsed = Number.parseInt(value, 10);
988
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) return null;
989
+ return parsed;
990
+ }
991
+ function parsePortFromUrl(value) {
992
+ if (!value) return null;
993
+ try {
994
+ const parsed = new URL(value).port;
995
+ return parsePort(parsed);
996
+ } catch {
997
+ return null;
998
+ }
999
+ }
1000
+ function collectPrimePorts() {
1001
+ const result = [];
1002
+ const seen = /* @__PURE__ */ new Set();
1003
+ const add = (port) => {
1004
+ if (port && !seen.has(port)) {
1005
+ seen.add(port);
1006
+ result.push(port);
1007
+ }
1008
+ };
1009
+ for (const key of PRIME_PORT_ENV_KEYS) {
1010
+ add(parsePort(process.env[key]));
1011
+ }
1012
+ for (const key of PRIME_URL_ENV_KEYS) {
1013
+ add(parsePortFromUrl(process.env[key]));
1014
+ }
1015
+ return result;
1016
+ }
1017
+ function isPortListening(port) {
1018
+ return new Promise((resolve2) => {
1019
+ const socket = net.connect({ host: "localhost", port });
1020
+ let settled = false;
1021
+ const finish = (listening) => {
1022
+ if (settled) return;
1023
+ settled = true;
1024
+ socket.destroy();
1025
+ resolve2(listening);
1026
+ };
1027
+ socket.once("connect", () => finish(true));
1028
+ socket.once("error", () => finish(false));
1029
+ socket.setTimeout(PORT_CHECK_TIMEOUT_MS, () => finish(false));
1030
+ });
768
1031
  }
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 });
1032
+ async function invokeWithRetry(handler, request, options) {
774
1033
  let elapsed = 0;
775
- let interval = DEV_VISIBILITY_POLL_INTERVAL;
776
- while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
1034
+ let delay = DEV_RETRY_INITIAL_DELAY_MS;
1035
+ while (true) {
777
1036
  try {
778
- await client.receiveMessageById({
779
- queueName: topicName,
780
- consumerGroup,
781
- messageId,
782
- visibilityTimeoutSeconds: 0
783
- });
784
- return true;
1037
+ await handleCallback(handler, request, options);
1038
+ return;
785
1039
  } 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
1040
+ if (isMessageNotFoundError(error) && elapsed < DEV_RETRY_MAX_WAIT_MS) {
1041
+ await new Promise((r) => setTimeout(r, delay));
1042
+ elapsed += delay;
1043
+ delay = Math.min(
1044
+ delay * DEV_RETRY_BACKOFF,
1045
+ DEV_RETRY_MAX_WAIT_MS - elapsed
792
1046
  );
793
1047
  continue;
794
1048
  }
795
- if (error instanceof MessageAlreadyProcessedError) {
796
- console.log(
797
- `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
798
- );
799
- return false;
1049
+ throw error;
1050
+ }
1051
+ }
1052
+ }
1053
+ function filePathToUrlPath(filePath) {
1054
+ let urlPath = filePath.replace(/^src\/app\//, "/").replace(/^src\/pages\//, "/").replace(/^src\/server\//, "/").replace(/^src\/routes\//, "/").replace(/^app\//, "/").replace(/^pages\//, "/").replace(/^server\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\/\+server\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
1055
+ if (!urlPath.startsWith("/")) {
1056
+ urlPath = "/" + urlPath;
1057
+ }
1058
+ return urlPath;
1059
+ }
1060
+ async function ensureHandlersLoaded(topicName, options = {}) {
1061
+ const diagnostics = {
1062
+ triedPorts: collectPrimePorts(),
1063
+ listeningPorts: [],
1064
+ unavailablePorts: [],
1065
+ importFailures: [],
1066
+ primeFailures: []
1067
+ };
1068
+ const matchingRoutes = findMatchingRoutes(topicName);
1069
+ if (matchingRoutes.length === 0) return diagnostics;
1070
+ const shouldRefreshRegistered = options.refreshRegistered === true;
1071
+ for (const port of diagnostics.triedPorts) {
1072
+ if (await isPortListening(port)) {
1073
+ diagnostics.listeningPorts.push(port);
1074
+ } else {
1075
+ diagnostics.unavailablePorts.push(port);
1076
+ }
1077
+ }
1078
+ for (const route of matchingRoutes) {
1079
+ const alreadyRegistered = isHandlerRegistered(topicName, route.consumer);
1080
+ if (alreadyRegistered && !shouldRefreshRegistered) {
1081
+ continue;
1082
+ }
1083
+ if (!alreadyRegistered) {
1084
+ const absolutePath = path.resolve(process.cwd(), route.filePath);
1085
+ try {
1086
+ await import(absolutePath);
1087
+ } catch (error) {
1088
+ diagnostics.importFailures.push({
1089
+ filePath: route.filePath,
1090
+ reason: formatErrorReason(error)
1091
+ });
1092
+ }
1093
+ if (isHandlerRegistered(topicName, route.consumer)) continue;
1094
+ }
1095
+ for (const port of diagnostics.listeningPorts) {
1096
+ const url = `http://localhost:${port}${filePathToUrlPath(route.filePath)}`;
1097
+ try {
1098
+ const response = await fetch(url, {
1099
+ method: "POST",
1100
+ headers: {
1101
+ "x-vercel-queue-prime": "1",
1102
+ "x-vercel-queue-prime-file": route.filePath
1103
+ }
1104
+ });
1105
+ try {
1106
+ await response.text();
1107
+ } catch {
1108
+ }
1109
+ if (isHandlerRegistered(topicName, route.consumer)) {
1110
+ break;
1111
+ }
1112
+ diagnostics.primeFailures.push({
1113
+ filePath: route.filePath,
1114
+ url,
1115
+ reason: `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`.trim()
1116
+ });
1117
+ } catch (error) {
1118
+ diagnostics.primeFailures.push({
1119
+ filePath: route.filePath,
1120
+ url,
1121
+ reason: formatErrorReason(error)
1122
+ });
800
1123
  }
801
- console.error(
802
- `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
803
- error
804
- );
805
- return false;
806
1124
  }
807
1125
  }
808
- console.warn(
809
- `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
1126
+ return diagnostics;
1127
+ }
1128
+ function buildNoHandlerWarning(topicName, routes, diagnostics) {
1129
+ const files = routes.map((r) => r.filePath);
1130
+ const suggestedPort = diagnostics.listeningPorts[0] ?? diagnostics.triedPorts[0];
1131
+ const suggestedUrls = suggestedPort ? routes.map(
1132
+ (r) => `http://localhost:${suggestedPort}${filePathToUrlPath(r.filePath)}`
1133
+ ) : [];
1134
+ let portSummary;
1135
+ if (diagnostics.triedPorts.length === 0) {
1136
+ portSummary = "No local dev port detected from env. Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT).";
1137
+ } else if (diagnostics.listeningPorts.length === 0) {
1138
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}], but none are listening.`;
1139
+ } else {
1140
+ const unavailable = diagnostics.unavailablePorts.length > 0 ? ` Not listening: [${diagnostics.unavailablePorts.join(", ")}].` : "";
1141
+ portSummary = `Detected env ports: [${diagnostics.triedPorts.join(", ")}]. Listening: [${diagnostics.listeningPorts.join(", ")}].` + unavailable;
1142
+ }
1143
+ const importSummary = diagnostics.importFailures.length > 0 ? `
1144
+ Import failures: ` + diagnostics.importFailures.slice(0, 2).map((f) => `${f.filePath} (${f.reason})`).join("; ") : "";
1145
+ const primeSummary = diagnostics.primeFailures.length > 0 ? `
1146
+ Prime failures: ` + diagnostics.primeFailures.slice(0, 3).map((f) => `${f.url} (${f.reason})`).join("; ") : "";
1147
+ return `[Dev Mode] No registered handler for topic "${topicName}". vercel.json maps this topic to [${files.join(", ")}] but auto-loading failed.
1148
+ ${portSummary}${importSummary}${primeSummary}
1149
+ Ensure your dev server is running, set PORT if needed, and confirm mapped route files call handleCallback()/handleNodeCallback() at module scope.
1150
+ ` + (suggestedUrls.length > 0 ? `Try opening: ${suggestedUrls.join(" or ")}` : "Set PORT (or NEXT_PORT/NUXT_PORT/VITE_PORT) and try sending again.");
1151
+ }
1152
+ function isHandlerRegistered(topicName, consumerGroup) {
1153
+ return lookupHandlers(topicName).some(
1154
+ (h) => h.consumerGroup === consumerGroup
810
1155
  );
811
- return false;
812
1156
  }
813
- function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
1157
+ var DEV_REDELIVERY_MAX_DELAY_S = 10;
1158
+ var DEV_REDELIVERY_DEFAULT_DELAY_S = 2;
1159
+ var DEV_REDELIVERY_MAX_ATTEMPTS = 10;
1160
+ var DEFAULT_RETENTION_S = 86400;
1161
+ function scheduleDevRedelivery(ctx, delayS) {
1162
+ const cappedDelay = Math.min(Math.max(delayS, 0), DEV_REDELIVERY_MAX_DELAY_S);
1163
+ console.log(
1164
+ `[Dev Mode] \u21BB Scheduling re-delivery in ${cappedDelay}s: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
1165
+ );
1166
+ setTimeout(async () => {
1167
+ const nextDeliveryCount = ctx.deliveryCount + 1;
1168
+ const expiresAt = new Date(
1169
+ ctx.createdAt.getTime() + ctx.retentionSeconds * 1e3
1170
+ );
1171
+ if (Date.now() >= expiresAt.getTime()) {
1172
+ console.log(
1173
+ `[Dev Mode] Message expired, stopping retries: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
1174
+ );
1175
+ return;
1176
+ }
1177
+ if (nextDeliveryCount > DEV_REDELIVERY_MAX_ATTEMPTS) {
1178
+ console.log(
1179
+ `[Dev Mode] Max re-deliveries (${DEV_REDELIVERY_MAX_ATTEMPTS}) reached: topic="${ctx.topicName}" messageId="${ctx.messageId}"`
1180
+ );
1181
+ return;
1182
+ }
1183
+ const metadata = {
1184
+ messageId: ctx.messageId,
1185
+ deliveryCount: nextDeliveryCount,
1186
+ createdAt: ctx.createdAt,
1187
+ expiresAt,
1188
+ topicName: ctx.topicName,
1189
+ consumerGroup: ctx.consumerGroup,
1190
+ region: ctx.region
1191
+ };
1192
+ console.log(
1193
+ `[Dev Mode] Re-delivering: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}" deliveryCount=${nextDeliveryCount}`
1194
+ );
1195
+ let succeeded = true;
1196
+ let nextRetryAfterS = null;
1197
+ let nextAcknowledged = false;
1198
+ try {
1199
+ await ctx.handler(ctx.payload, metadata);
1200
+ } catch (error) {
1201
+ succeeded = false;
1202
+ if (ctx.retry) {
1203
+ let directive;
1204
+ try {
1205
+ directive = ctx.retry(error, metadata);
1206
+ } catch (retryErr) {
1207
+ console.warn("[Dev Mode] retry handler threw:", retryErr);
1208
+ }
1209
+ if (directive && "afterSeconds" in directive) {
1210
+ nextRetryAfterS = directive.afterSeconds;
1211
+ } else if (directive && "acknowledge" in directive) {
1212
+ nextAcknowledged = true;
1213
+ }
1214
+ }
1215
+ if (!nextAcknowledged) {
1216
+ console.error(
1217
+ `[Dev Mode] \u2717 Handler error on re-delivery: topic="${ctx.topicName}" messageId="${ctx.messageId}"`,
1218
+ error
1219
+ );
1220
+ }
1221
+ }
1222
+ if (succeeded) {
1223
+ console.log(
1224
+ `[Dev Mode] \u2713 Message processed on re-delivery: topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
1225
+ );
1226
+ } else if (nextAcknowledged) {
1227
+ console.log(
1228
+ `[Dev Mode] \u2713 Message acknowledged (will not retry): topic="${ctx.topicName}" consumer="${ctx.consumerGroup}" messageId="${ctx.messageId}"`
1229
+ );
1230
+ } else {
1231
+ const nextDelay = nextRetryAfterS ?? ctx.defaultRetryDelayS;
1232
+ scheduleDevRedelivery(
1233
+ { ...ctx, deliveryCount: nextDeliveryCount },
1234
+ nextDelay
1235
+ );
1236
+ }
1237
+ }, cappedDelay * 1e3);
1238
+ }
1239
+ function invokeDevHandlers(topicName, messageId, region, delaySeconds, retentionSeconds) {
814
1240
  if (delaySeconds && delaySeconds > 0) {
815
1241
  console.log(
816
1242
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
817
1243
  );
818
1244
  setTimeout(() => {
819
- triggerDevCallbacks(topicName, messageId, region);
1245
+ invokeDevHandlers(
1246
+ topicName,
1247
+ messageId,
1248
+ region,
1249
+ void 0,
1250
+ retentionSeconds
1251
+ );
820
1252
  }, delaySeconds * 1e3);
821
1253
  return;
822
1254
  }
823
1255
  console.log(
824
1256
  `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
825
1257
  );
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
1258
  (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
- );
1259
+ let handlers = lookupHandlers(topicName);
1260
+ let diagnostics = null;
1261
+ if (handlers.length > 0) {
1262
+ await ensureHandlersLoaded(topicName, { refreshRegistered: true });
1263
+ handlers = lookupHandlers(topicName);
1264
+ } else {
1265
+ diagnostics = await ensureHandlersLoaded(topicName);
1266
+ handlers = lookupHandlers(topicName);
1267
+ }
1268
+ if (handlers.length === 0) {
1269
+ const matchingRoutes = findMatchingRoutes(topicName);
1270
+ if (matchingRoutes.length > 0) {
1271
+ const safeDiagnostics = diagnostics ?? {
1272
+ triedPorts: collectPrimePorts(),
1273
+ listeningPorts: [],
1274
+ unavailablePorts: [],
1275
+ importFailures: [],
1276
+ primeFailures: []
1277
+ };
1278
+ console.warn(
1279
+ buildNoHandlerWarning(topicName, matchingRoutes, safeDiagnostics)
1280
+ );
1281
+ } else {
1282
+ console.warn(
1283
+ `[Dev Mode] No registered handler for topic "${topicName}".
1284
+ Ensure vercel.json has a matching experimentalTriggers entry and the route file calls handleCallback().`
1285
+ );
1286
+ }
849
1287
  return;
850
1288
  }
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}"`
1289
+ const consumerGroups = handlers.map((h) => h.consumerGroup);
1290
+ console.log(
1291
+ `[Dev Mode] Invoking handlers for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
1292
+ );
1293
+ const effectiveRetention = retentionSeconds ?? DEFAULT_RETENTION_S;
1294
+ for (const entry of handlers) {
1295
+ let capturedPayload;
1296
+ let capturedCreatedAt = /* @__PURE__ */ new Date();
1297
+ let capturedDeliveryCount = 1;
1298
+ let handlerSucceeded = true;
1299
+ let retryAfterS = null;
1300
+ let retryAcknowledged = false;
1301
+ const wrappedHandler = async (message, metadata) => {
1302
+ capturedPayload = message;
1303
+ capturedCreatedAt = metadata.createdAt;
1304
+ capturedDeliveryCount = metadata.deliveryCount;
1305
+ try {
1306
+ await entry.handler(message, metadata);
1307
+ } catch (error) {
1308
+ handlerSucceeded = false;
1309
+ throw error;
1310
+ }
1311
+ };
1312
+ const wrappedRetry = entry.options?.retry ? (error, metadata) => {
1313
+ const directive = entry.options.retry(error, metadata);
1314
+ if (directive && "afterSeconds" in directive) {
1315
+ retryAfterS = directive.afterSeconds;
1316
+ } else if (directive && "acknowledge" in directive) {
1317
+ retryAcknowledged = true;
1318
+ }
1319
+ return directive;
1320
+ } : void 0;
1321
+ const request = {
1322
+ queueName: topicName,
1323
+ consumerGroup: entry.consumerGroup,
1324
+ messageId,
1325
+ region
1326
+ };
1327
+ const callbackOptions = {
1328
+ client: entry.client,
1329
+ visibilityTimeoutSeconds: entry.options?.visibilityTimeoutSeconds,
1330
+ retry: wrappedRetry
1331
+ };
1332
+ const consumerDefaultDelay = Math.min(
1333
+ findRetryAfterSeconds(topicName, entry.consumerGroup) ?? DEV_REDELIVERY_DEFAULT_DELAY_S,
1334
+ DEV_REDELIVERY_MAX_DELAY_S
857
1335
  );
1336
+ const buildRedeliveryCtx = () => ({
1337
+ handler: entry.handler,
1338
+ retry: entry.options?.retry,
1339
+ payload: capturedPayload,
1340
+ topicName,
1341
+ consumerGroup: entry.consumerGroup,
1342
+ messageId,
1343
+ region,
1344
+ createdAt: capturedCreatedAt,
1345
+ retentionSeconds: effectiveRetention,
1346
+ deliveryCount: capturedDeliveryCount,
1347
+ defaultRetryDelayS: consumerDefaultDelay
1348
+ });
858
1349
  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
- }
1350
+ await invokeWithRetry(wrappedHandler, request, callbackOptions);
1351
+ if (handlerSucceeded) {
1352
+ console.log(
1353
+ `[Dev Mode] \u2713 Message processed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1354
+ );
1355
+ } else if (retryAcknowledged) {
1356
+ console.log(
1357
+ `[Dev Mode] \u2713 Message acknowledged (will not retry): topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`
1358
+ );
1359
+ } else if (retryAfterS !== null) {
1360
+ const devDelay = Math.min(retryAfterS, DEV_REDELIVERY_MAX_DELAY_S);
1361
+ scheduleDevRedelivery(buildRedeliveryCtx(), devDelay);
893
1362
  }
894
1363
  } catch (error) {
895
1364
  console.error(
896
- `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
1365
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${entry.consumerGroup}" messageId="${messageId}"`,
897
1366
  error
898
1367
  );
1368
+ if (!handlerSucceeded) {
1369
+ scheduleDevRedelivery(buildRedeliveryCtx(), consumerDefaultDelay);
1370
+ }
899
1371
  }
900
1372
  }
901
1373
  })();
902
1374
  }
903
- function clearDevRouteMappings() {
1375
+ function clearDevState() {
904
1376
  const g = globalThis;
905
1377
  delete g[ROUTE_MAPPINGS_KEY];
1378
+ delete g[HANDLER_REGISTRY_KEY];
906
1379
  }
907
1380
  if (process.env.NODE_ENV === "test" || process.env.VITEST) {
908
- globalThis.__clearDevRouteMappings = clearDevRouteMappings;
1381
+ globalThis.__clearDevState = clearDevState;
1382
+ globalThis.__filePathToConsumerGroup = filePathToConsumerGroup;
1383
+ globalThis.__filePathToUrlPath = filePathToUrlPath;
1384
+ globalThis.__matchesFunctionsPattern = matchesFunctionsPattern;
1385
+ globalThis.__stripSrcPrefix = stripSrcPrefix;
909
1386
  }
910
1387
 
911
1388
  // src/oidc.ts
@@ -949,6 +1426,7 @@ function parseQueueHeaders(headers) {
949
1426
  const timestamp = headers.get("Vqs-Timestamp");
950
1427
  const contentType = headers.get("Content-Type") || "application/octet-stream";
951
1428
  const receiptHandle = headers.get("Vqs-Receipt-Handle");
1429
+ const expiresAtStr = headers.get("Vqs-Expires-At");
952
1430
  if (!messageId || !timestamp || !receiptHandle) {
953
1431
  return null;
954
1432
  }
@@ -960,6 +1438,7 @@ function parseQueueHeaders(headers) {
960
1438
  messageId,
961
1439
  deliveryCount,
962
1440
  createdAt: new Date(timestamp),
1441
+ expiresAt: expiresAtStr ? new Date(expiresAtStr) : void 0,
963
1442
  contentType,
964
1443
  receiptHandle
965
1444
  };
@@ -1080,7 +1559,7 @@ var ApiClient = class _ApiClient {
1080
1559
  }
1081
1560
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1082
1561
  }
1083
- init.headers.set("User-Agent", `@vercel/queue/${"0.1.0"}`);
1562
+ init.headers.set("User-Agent", `@vercel/queue/${"0.1.2"}`);
1084
1563
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1085
1564
  const response = await fetch(url, init);
1086
1565
  if (isDebugEnabled()) {
@@ -1416,12 +1895,36 @@ var ApiClient = class _ApiClient {
1416
1895
 
1417
1896
  // src/client.ts
1418
1897
  var apiClients = /* @__PURE__ */ new WeakMap();
1898
+ var API_CLIENT_KEY = Symbol.for("@vercel/queue.apiClient");
1899
+ function setApi(client, api) {
1900
+ apiClients.set(client, api);
1901
+ Object.defineProperty(client, API_CLIENT_KEY, {
1902
+ value: api,
1903
+ writable: false,
1904
+ enumerable: false,
1905
+ configurable: false
1906
+ });
1907
+ }
1419
1908
  function getApi(client) {
1420
1909
  const api = apiClients.get(client);
1421
- if (!api) {
1422
- throw new Error("Client not initialized");
1910
+ if (api) {
1911
+ return api;
1423
1912
  }
1424
- return api;
1913
+ const apiFromSymbol = client[API_CLIENT_KEY];
1914
+ if (typeof apiFromSymbol === "object" && apiFromSymbol !== null) {
1915
+ const resolvedApi = apiFromSymbol;
1916
+ apiClients.set(client, resolvedApi);
1917
+ return resolvedApi;
1918
+ }
1919
+ throw new Error(
1920
+ "QueueClient not initialized. This may happen when multiple bundled copies of @vercel/queue are loaded in local dev."
1921
+ );
1922
+ }
1923
+ function resolveCallbackRequest(input) {
1924
+ if ("request" in input) {
1925
+ return input.request;
1926
+ }
1927
+ return input;
1425
1928
  }
1426
1929
  function getApiClient(client) {
1427
1930
  return getApi(client);
@@ -1431,15 +1934,17 @@ function resolveRegion(region) {
1431
1934
  if (region) return region;
1432
1935
  const fromEnv = process.env.VERCEL_REGION;
1433
1936
  if (fromEnv) return fromEnv;
1434
- console.warn(
1435
- `[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" })`
1436
- );
1937
+ if (!isDevMode()) {
1938
+ console.warn(
1939
+ `[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" })`
1940
+ );
1941
+ }
1437
1942
  return DEFAULT_REGION;
1438
1943
  }
1439
1944
  var QueueClient = class {
1440
1945
  constructor(options = {}) {
1441
1946
  const region = resolveRegion(options.region);
1442
- apiClients.set(this, new ApiClient({ ...options, region }));
1947
+ setApi(this, new ApiClient({ ...options, region }));
1443
1948
  }
1444
1949
  /**
1445
1950
  * Send a message to a topic.
@@ -1467,11 +1972,12 @@ var QueueClient = class {
1467
1972
  headers: options?.headers
1468
1973
  });
1469
1974
  if (result.messageId && isDevMode()) {
1470
- triggerDevCallbacks(
1975
+ invokeDevHandlers(
1471
1976
  topicName,
1472
1977
  result.messageId,
1473
1978
  api.getRegion(),
1474
- options?.delaySeconds
1979
+ options?.delaySeconds,
1980
+ options?.retentionSeconds
1475
1981
  );
1476
1982
  }
1477
1983
  return { messageId: result.messageId };
@@ -1493,10 +1999,26 @@ var QueueClient = class {
1493
1999
  * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1494
2000
  * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1495
2001
  * reschedule the message for redelivery after N seconds.
1496
- * @returns A `(request: Request) => Promise<Response>` route handler
2002
+ * @returns A route handler that accepts either `Request` or `{ request: Request }`
1497
2003
  */
1498
2004
  handleCallback = (handler, options) => {
1499
- return async (request) => {
2005
+ if (isDevMode()) {
2006
+ registerDevHandler(handler, this, options);
2007
+ }
2008
+ return async (requestOrEvent) => {
2009
+ const request = resolveCallbackRequest(requestOrEvent);
2010
+ if (isDevMode() && request.headers.get("x-vercel-queue-prime") === "1") {
2011
+ const primeFile = request.headers.get("x-vercel-queue-prime-file");
2012
+ if (primeFile) {
2013
+ registerDevHandler(
2014
+ handler,
2015
+ this,
2016
+ options,
2017
+ primeFile
2018
+ );
2019
+ }
2020
+ return Response.json({ status: "primed" });
2021
+ }
1500
2022
  try {
1501
2023
  const parsed = await parseCallback(request);
1502
2024
  await handleCallback(handler, parsed, {
@@ -1538,11 +2060,29 @@ var QueueClient = class {
1538
2060
  * @returns A `(req, res) => Promise<void>` route handler
1539
2061
  */
1540
2062
  handleNodeCallback = (handler, options) => {
2063
+ if (isDevMode()) {
2064
+ registerDevHandler(handler, this, options);
2065
+ }
1541
2066
  return async (req, res) => {
1542
2067
  if (req.method !== "POST") {
1543
2068
  res.status(200).end();
1544
2069
  return;
1545
2070
  }
2071
+ const primeHeader = req.headers["x-vercel-queue-prime"];
2072
+ if (isDevMode() && primeHeader === "1") {
2073
+ const primeFileHeader = req.headers["x-vercel-queue-prime-file"];
2074
+ const primeFile = Array.isArray(primeFileHeader) ? primeFileHeader[0] : primeFileHeader;
2075
+ if (primeFile) {
2076
+ registerDevHandler(
2077
+ handler,
2078
+ this,
2079
+ options,
2080
+ primeFile
2081
+ );
2082
+ }
2083
+ res.status(200).json({ status: "primed" });
2084
+ return;
2085
+ }
1546
2086
  try {
1547
2087
  const parsed = parseRawCallback(req.body, req.headers);
1548
2088
  await handleCallback(handler, parsed, {
@@ -1564,7 +2104,7 @@ var QueueClient = class {
1564
2104
  };
1565
2105
  var PollingQueueClient = class {
1566
2106
  constructor(options) {
1567
- apiClients.set(this, new ApiClient(options));
2107
+ setApi(this, new ApiClient(options));
1568
2108
  }
1569
2109
  /**
1570
2110
  * Send a message to a topic.
@@ -1663,6 +2203,25 @@ var PollingQueueClient = class {
1663
2203
  }
1664
2204
  };
1665
2205
  };
2206
+
2207
+ // src/default-client.ts
2208
+ var _defaultClient;
2209
+ function getDefaultClient() {
2210
+ if (!_defaultClient) {
2211
+ _defaultClient = new QueueClient();
2212
+ }
2213
+ return _defaultClient;
2214
+ }
2215
+ function resolveClient(region) {
2216
+ if (!region) return getDefaultClient();
2217
+ return new QueueClient({ region });
2218
+ }
2219
+ async function send(topicName, payload, options) {
2220
+ return resolveClient(options?.region).send(topicName, payload, options);
2221
+ }
2222
+ function handleCallback2(handler, options) {
2223
+ return getDefaultClient().handleCallback(handler, options);
2224
+ }
1666
2225
  export {
1667
2226
  BadRequestError,
1668
2227
  BufferTransport,
@@ -1685,7 +2244,9 @@ export {
1685
2244
  QueueEmptyError,
1686
2245
  StreamTransport,
1687
2246
  UnauthorizedError,
2247
+ handleCallback2 as handleCallback,
1688
2248
  parseCallback,
1689
- parseRawCallback
2249
+ parseRawCallback,
2250
+ send
1690
2251
  };
1691
2252
  //# sourceMappingURL=index.mjs.map