@vercel/queue 0.0.0-alpha.36 → 0.0.0-alpha.38

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.
@@ -41,46 +41,6 @@ var import_mixpart = require("mixpart");
41
41
  var fs = __toESM(require("fs"));
42
42
  var path = __toESM(require("path"));
43
43
 
44
- // src/transports.ts
45
- async function streamToBuffer(stream) {
46
- let totalLength = 0;
47
- const reader = stream.getReader();
48
- const chunks = [];
49
- try {
50
- while (true) {
51
- const { done, value } = await reader.read();
52
- if (done) break;
53
- chunks.push(value);
54
- totalLength += value.length;
55
- }
56
- } finally {
57
- reader.releaseLock();
58
- }
59
- return Buffer.concat(chunks, totalLength);
60
- }
61
- var JsonTransport = class {
62
- contentType = "application/json";
63
- replacer;
64
- reviver;
65
- /**
66
- * Create a new JsonTransport.
67
- * @param options - Optional JSON serialization options
68
- * @param options.replacer - Custom replacer for JSON.stringify
69
- * @param options.reviver - Custom reviver for JSON.parse
70
- */
71
- constructor(options = {}) {
72
- this.replacer = options.replacer;
73
- this.reviver = options.reviver;
74
- }
75
- serialize(value) {
76
- return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
77
- }
78
- async deserialize(stream) {
79
- const buffer = await streamToBuffer(stream);
80
- return JSON.parse(buffer.toString("utf8"), this.reviver);
81
- }
82
- };
83
-
84
44
  // src/types.ts
85
45
  var MessageNotFoundError = class extends Error {
86
46
  constructor(messageId) {
@@ -102,14 +62,6 @@ var MessageCorruptedError = class extends Error {
102
62
  this.name = "MessageCorruptedError";
103
63
  }
104
64
  };
105
- var QueueEmptyError = class extends Error {
106
- constructor(queueName, consumerGroup) {
107
- super(
108
- `No messages available in queue "${queueName}" for consumer group "${consumerGroup}"`
109
- );
110
- this.name = "QueueEmptyError";
111
- }
112
- };
113
65
  var UnauthorizedError = class extends Error {
114
66
  constructor(message = "Missing or invalid authentication token") {
115
67
  super(message);
@@ -146,18 +98,6 @@ var MessageAlreadyProcessedError = class extends Error {
146
98
  this.name = "MessageAlreadyProcessedError";
147
99
  }
148
100
  };
149
- var ConcurrencyLimitError = class extends Error {
150
- /** Current number of in-flight messages for this consumer group. */
151
- currentInflight;
152
- /** Maximum allowed concurrent messages (as configured). */
153
- maxConcurrency;
154
- constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
155
- super(message);
156
- this.name = "ConcurrencyLimitError";
157
- this.currentInflight = currentInflight;
158
- this.maxConcurrency = maxConcurrency;
159
- }
160
- };
161
101
  var DuplicateMessageError = class extends Error {
162
102
  idempotencyKey;
163
103
  constructor(message, idempotencyKey) {
@@ -184,12 +124,15 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
184
124
  // src/dev.ts
185
125
  var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
186
126
  function filePathToUrlPath(filePath) {
187
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
127
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
188
128
  if (!urlPath.startsWith("/")) {
189
129
  urlPath = "/" + urlPath;
190
130
  }
191
131
  return urlPath;
192
132
  }
133
+ function filePathToConsumerGroup(filePath) {
134
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
135
+ }
193
136
  function getDevRouteMappings() {
194
137
  const g = globalThis;
195
138
  if (ROUTE_MAPPINGS_KEY in g) {
@@ -210,11 +153,11 @@ function getDevRouteMappings() {
210
153
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
211
154
  if (!config.experimentalTriggers) continue;
212
155
  for (const trigger of config.experimentalTriggers) {
213
- if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
156
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
214
157
  mappings.push({
215
158
  urlPath: filePathToUrlPath(filePath),
216
159
  topic: trigger.topic,
217
- consumer: trigger.consumer
160
+ consumer: filePathToConsumerGroup(filePath)
218
161
  });
219
162
  }
220
163
  }
@@ -247,20 +190,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
247
190
  var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
248
191
  async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
249
192
  const client = new QueueClient();
250
- const transport = new JsonTransport();
251
193
  let elapsed = 0;
252
194
  let interval = DEV_VISIBILITY_POLL_INTERVAL;
253
195
  while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
254
196
  try {
255
- await client.receiveMessageById(
256
- {
257
- queueName: topicName,
258
- consumerGroup,
259
- messageId,
260
- visibilityTimeoutSeconds: 0
261
- },
262
- transport
263
- );
197
+ await client.receiveMessageById({
198
+ queueName: topicName,
199
+ consumerGroup,
200
+ messageId,
201
+ visibilityTimeoutSeconds: 0
202
+ });
264
203
  return true;
265
204
  } catch (error) {
266
205
  if (error instanceof MessageNotFoundError) {
@@ -334,26 +273,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
334
273
  console.log(
335
274
  `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
336
275
  );
337
- const cloudEvent = {
338
- type: "com.vercel.queue.v1beta",
339
- source: `/topic/${topicName}/consumer/${route.consumer}`,
340
- id: messageId,
341
- datacontenttype: "application/json",
342
- data: {
343
- messageId,
344
- queueName: topicName,
345
- consumerGroup: route.consumer
346
- },
347
- time: (/* @__PURE__ */ new Date()).toISOString(),
348
- specversion: "1.0"
349
- };
350
276
  try {
351
277
  const response = await fetch(url, {
352
278
  method: "POST",
353
279
  headers: {
354
- "Content-Type": "application/cloudevents+json"
355
- },
356
- body: JSON.stringify(cloudEvent)
280
+ "ce-type": CLOUD_EVENT_TYPE_V2BETA,
281
+ "ce-vqsqueuename": topicName,
282
+ "ce-vqsconsumergroup": route.consumer,
283
+ "ce-vqsmessageid": messageId
284
+ }
357
285
  });
358
286
  if (response.ok) {
359
287
  try {
@@ -400,6 +328,46 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
400
328
  // src/oidc.ts
401
329
  var import_oidc = require("@vercel/oidc");
402
330
 
331
+ // src/transports.ts
332
+ async function streamToBuffer(stream) {
333
+ let totalLength = 0;
334
+ const reader = stream.getReader();
335
+ const chunks = [];
336
+ try {
337
+ while (true) {
338
+ const { done, value } = await reader.read();
339
+ if (done) break;
340
+ chunks.push(value);
341
+ totalLength += value.length;
342
+ }
343
+ } finally {
344
+ reader.releaseLock();
345
+ }
346
+ return Buffer.concat(chunks, totalLength);
347
+ }
348
+ var JsonTransport = class {
349
+ contentType = "application/json";
350
+ replacer;
351
+ reviver;
352
+ /**
353
+ * Create a new JsonTransport.
354
+ * @param options - Optional JSON serialization options
355
+ * @param options.replacer - Custom replacer for JSON.stringify
356
+ * @param options.reviver - Custom reviver for JSON.parse
357
+ */
358
+ constructor(options = {}) {
359
+ this.replacer = options.replacer;
360
+ this.reviver = options.reviver;
361
+ }
362
+ serialize(value) {
363
+ return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
364
+ }
365
+ async deserialize(stream) {
366
+ const buffer = await streamToBuffer(stream);
367
+ return JSON.parse(buffer.toString("utf8"), this.reviver);
368
+ }
369
+ };
370
+
403
371
  // src/client.ts
404
372
  function isDebugEnabled() {
405
373
  return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
@@ -460,6 +428,7 @@ var QueueClient = class {
460
428
  providedToken;
461
429
  defaultDeploymentId;
462
430
  pinToDeployment;
431
+ transport;
463
432
  constructor(options = {}) {
464
433
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
465
434
  this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
@@ -467,6 +436,10 @@ var QueueClient = class {
467
436
  this.providedToken = options.token;
468
437
  this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
469
438
  this.pinToDeployment = options.pinToDeployment ?? true;
439
+ this.transport = options.transport || new JsonTransport();
440
+ }
441
+ getTransport() {
442
+ return this.transport;
470
443
  }
471
444
  getSendDeploymentId() {
472
445
  if (isDevMode()) {
@@ -523,6 +496,8 @@ var QueueClient = class {
523
496
  }
524
497
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
525
498
  }
499
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.38"}`);
500
+ init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
526
501
  const response = await fetch(url, init);
527
502
  if (isDebugEnabled()) {
528
503
  const logData = {
@@ -545,7 +520,6 @@ var QueueClient = class {
545
520
  * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
546
521
  * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
547
522
  * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
548
- * @param transport - Serializer for the payload
549
523
  * @returns Promise with the generated messageId
550
524
  * @throws {DuplicateMessageError} When idempotency key was already used
551
525
  * @throws {ConsumerDiscoveryError} When consumer discovery fails
@@ -555,7 +529,8 @@ var QueueClient = class {
555
529
  * @throws {ForbiddenError} When access is denied
556
530
  * @throws {InternalServerError} When server encounters an error
557
531
  */
558
- async sendMessage(options, transport) {
532
+ async sendMessage(options) {
533
+ const transport = this.transport;
559
534
  const {
560
535
  queueName,
561
536
  payload,
@@ -637,30 +612,25 @@ var QueueClient = class {
637
612
  /**
638
613
  * Receive messages from a topic as an async generator.
639
614
  *
615
+ * When the queue is empty, the generator completes without yielding any
616
+ * messages. Callers should handle the case where no messages are yielded.
617
+ *
640
618
  * @param options - Receive options
641
619
  * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
642
620
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
643
621
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
644
622
  * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
645
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
646
- * @param transport - Deserializer for message payloads
647
623
  * @yields Message objects with payload, messageId, receiptHandle, etc.
648
- * @throws {QueueEmptyError} When no messages available
624
+ * Yields nothing if queue is empty.
649
625
  * @throws {InvalidLimitError} When limit is outside 1-10 range
650
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
651
626
  * @throws {BadRequestError} When parameters are invalid
652
627
  * @throws {UnauthorizedError} When authentication fails
653
628
  * @throws {ForbiddenError} When access is denied
654
629
  * @throws {InternalServerError} When server encounters an error
655
630
  */
656
- async *receiveMessages(options, transport) {
657
- const {
658
- queueName,
659
- consumerGroup,
660
- visibilityTimeoutSeconds,
661
- limit,
662
- maxConcurrency
663
- } = options;
631
+ async *receiveMessages(options) {
632
+ const transport = this.transport;
633
+ const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
664
634
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
665
635
  throw new InvalidLimitError(limit);
666
636
  }
@@ -678,9 +648,6 @@ var QueueClient = class {
678
648
  if (limit !== void 0) {
679
649
  headers.set("Vqs-Max-Messages", limit.toString());
680
650
  }
681
- if (maxConcurrency !== void 0) {
682
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
683
- }
684
651
  const effectiveDeploymentId = this.getConsumeDeploymentId();
685
652
  if (effectiveDeploymentId) {
686
653
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
@@ -693,22 +660,10 @@ var QueueClient = class {
693
660
  }
694
661
  );
695
662
  if (response.status === 204) {
696
- throw new QueueEmptyError(queueName, consumerGroup);
663
+ return;
697
664
  }
698
665
  if (!response.ok) {
699
666
  const errorText = await response.text();
700
- if (response.status === 429) {
701
- let errorData = {};
702
- try {
703
- errorData = JSON.parse(errorText);
704
- } catch {
705
- }
706
- throw new ConcurrencyLimitError(
707
- errorData.error || "Concurrency limit exceeded or throttled",
708
- errorData.currentInflight,
709
- errorData.maxConcurrency
710
- );
711
- }
712
667
  throwCommonHttpError(
713
668
  response.status,
714
669
  response.statusText,
@@ -746,26 +701,18 @@ var QueueClient = class {
746
701
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
747
702
  * @param options.messageId - Message ID to retrieve
748
703
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
749
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
750
- * @param transport - Deserializer for the message payload
751
704
  * @returns Promise with the message
752
705
  * @throws {MessageNotFoundError} When message doesn't exist
753
706
  * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
754
707
  * @throws {MessageAlreadyProcessedError} When message was already processed
755
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
756
708
  * @throws {BadRequestError} When parameters are invalid
757
709
  * @throws {UnauthorizedError} When authentication fails
758
710
  * @throws {ForbiddenError} When access is denied
759
711
  * @throws {InternalServerError} When server encounters an error
760
712
  */
761
- async receiveMessageById(options, transport) {
762
- const {
763
- queueName,
764
- consumerGroup,
765
- messageId,
766
- visibilityTimeoutSeconds,
767
- maxConcurrency
768
- } = options;
713
+ async receiveMessageById(options) {
714
+ const transport = this.transport;
715
+ const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
769
716
  const headers = new Headers({
770
717
  Authorization: `Bearer ${await this.getToken()}`,
771
718
  Accept: "multipart/mixed",
@@ -777,9 +724,6 @@ var QueueClient = class {
777
724
  visibilityTimeoutSeconds.toString()
778
725
  );
779
726
  }
780
- if (maxConcurrency !== void 0) {
781
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
782
- }
783
727
  const effectiveDeploymentId = this.getConsumeDeploymentId();
784
728
  if (effectiveDeploymentId) {
785
729
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
@@ -813,18 +757,6 @@ var QueueClient = class {
813
757
  if (response.status === 410) {
814
758
  throw new MessageAlreadyProcessedError(messageId);
815
759
  }
816
- if (response.status === 429) {
817
- let errorData = {};
818
- try {
819
- errorData = JSON.parse(errorText);
820
- } catch {
821
- }
822
- throw new ConcurrencyLimitError(
823
- errorData.error || "Concurrency limit exceeded or throttled",
824
- errorData.currentInflight,
825
- errorData.maxConcurrency
826
- );
827
- }
828
760
  throwCommonHttpError(
829
761
  response.status,
830
762
  response.statusText,
@@ -1042,40 +974,90 @@ var QueueClient = class {
1042
974
  };
1043
975
 
1044
976
  // src/consumer-group.ts
977
+ var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
978
+ var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
979
+ var MAX_RENEWAL_INTERVAL_SECONDS = 60;
980
+ var MIN_RENEWAL_INTERVAL_SECONDS = 10;
981
+ var RETRY_INTERVAL_MS = 3e3;
982
+ function calculateRenewalInterval(visibilityTimeoutSeconds) {
983
+ return Math.min(
984
+ MAX_RENEWAL_INTERVAL_SECONDS,
985
+ Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
986
+ );
987
+ }
1045
988
  var ConsumerGroup = class {
1046
989
  client;
1047
990
  topicName;
1048
991
  consumerGroupName;
1049
992
  visibilityTimeout;
1050
- refreshInterval;
1051
- transport;
1052
993
  /**
1053
994
  * Create a new ConsumerGroup instance.
1054
995
  *
1055
- * @param client - QueueClient instance to use for API calls
996
+ * @param client - QueueClient instance to use for API calls (transport is configured on the client)
1056
997
  * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
1057
998
  * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1058
999
  * @param options - Optional configuration
1059
- * @param options.transport - Payload serializer (default: JsonTransport)
1060
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1061
- * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
1000
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1062
1001
  */
1063
1002
  constructor(client, topicName, consumerGroupName, options = {}) {
1064
1003
  this.client = client;
1065
1004
  this.topicName = topicName;
1066
1005
  this.consumerGroupName = consumerGroupName;
1067
- this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1068
- this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
1069
- this.transport = options.transport || new JsonTransport();
1006
+ this.visibilityTimeout = Math.max(
1007
+ MIN_VISIBILITY_TIMEOUT_SECONDS,
1008
+ options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
1009
+ );
1010
+ }
1011
+ /**
1012
+ * Check if an error is a 4xx client error that should stop retries.
1013
+ * 4xx errors indicate the request is fundamentally invalid and retrying won't help.
1014
+ * - 409: Ticket mismatch (lost ownership to another consumer)
1015
+ * - 404: Message/receipt handle not found
1016
+ * - 400, 401, 403: Other client errors
1017
+ */
1018
+ isClientError(error) {
1019
+ return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
1020
+ error instanceof MessageNotFoundError || // 404 - receipt handle not found
1021
+ error instanceof BadRequestError || // 400 - invalid parameters
1022
+ error instanceof UnauthorizedError || // 401 - auth failed
1023
+ error instanceof ForbiddenError;
1070
1024
  }
1071
1025
  /**
1072
1026
  * Starts a background loop that periodically extends the visibility timeout for a message.
1027
+ *
1028
+ * Timing strategy:
1029
+ * - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
1030
+ * - Extensions request the same duration as the initial visibility timeout
1031
+ * - When `visibilityDeadline` is provided (binary mode small body), the first
1032
+ * extension delay is calculated from the time remaining until the deadline
1033
+ * using the same renewal formula, ensuring the first extension fires before
1034
+ * the server-assigned lease expires. Subsequent renewals use the standard interval.
1035
+ *
1036
+ * Retry strategy:
1037
+ * - On transient failures (5xx, network errors): retry every 3 seconds
1038
+ * - On 4xx client errors: stop retrying (the lease is lost or invalid)
1039
+ *
1040
+ * @param receiptHandle - The receipt handle to extend visibility for
1041
+ * @param options - Optional configuration
1042
+ * @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
1043
+ * when the current visibility timeout expires. Used to calculate the first extension delay.
1073
1044
  */
1074
- startVisibilityExtension(receiptHandle) {
1045
+ startVisibilityExtension(receiptHandle, options) {
1075
1046
  let isRunning = true;
1076
1047
  let isResolved = false;
1077
1048
  let resolveLifecycle;
1078
1049
  let timeoutId = null;
1050
+ const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
1051
+ let firstDelayMs = renewalIntervalMs;
1052
+ if (options?.visibilityDeadline) {
1053
+ const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
1054
+ if (timeRemainingMs > 0) {
1055
+ const timeRemainingSeconds = timeRemainingMs / 1e3;
1056
+ firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
1057
+ } else {
1058
+ firstDelayMs = 0;
1059
+ }
1060
+ }
1079
1061
  const lifecyclePromise = new Promise((resolve) => {
1080
1062
  resolveLifecycle = resolve;
1081
1063
  });
@@ -1098,19 +1080,31 @@ var ConsumerGroup = class {
1098
1080
  visibilityTimeoutSeconds: this.visibilityTimeout
1099
1081
  });
1100
1082
  if (isRunning) {
1101
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1083
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
1102
1084
  } else {
1103
1085
  safeResolve();
1104
1086
  }
1105
1087
  } catch (error) {
1088
+ if (this.isClientError(error)) {
1089
+ console.error(
1090
+ `Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
1091
+ error
1092
+ );
1093
+ safeResolve();
1094
+ return;
1095
+ }
1106
1096
  console.error(
1107
- `Failed to extend visibility for receipt handle ${receiptHandle}:`,
1097
+ `Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
1108
1098
  error
1109
1099
  );
1110
- safeResolve();
1100
+ if (isRunning) {
1101
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
1102
+ } else {
1103
+ safeResolve();
1104
+ }
1111
1105
  }
1112
1106
  };
1113
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1107
+ timeoutId = setTimeout(() => extend(), firstDelayMs);
1114
1108
  return async (waitForCompletion = false) => {
1115
1109
  isRunning = false;
1116
1110
  if (timeoutId) {
@@ -1124,8 +1118,11 @@ var ConsumerGroup = class {
1124
1118
  }
1125
1119
  };
1126
1120
  }
1127
- async processMessage(message, handler) {
1128
- const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1121
+ async processMessage(message, handler, options) {
1122
+ const stopExtension = this.startVisibilityExtension(
1123
+ message.receiptHandle,
1124
+ options
1125
+ );
1129
1126
  try {
1130
1127
  await handler(message.payload, {
1131
1128
  messageId: message.messageId,
@@ -1142,9 +1139,10 @@ var ConsumerGroup = class {
1142
1139
  });
1143
1140
  } catch (error) {
1144
1141
  await stopExtension();
1145
- if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
1142
+ const transport = this.client.getTransport();
1143
+ if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
1146
1144
  try {
1147
- await this.transport.finalize(message.payload);
1145
+ await transport.finalize(message.payload);
1148
1146
  } catch (finalizeError) {
1149
1147
  console.warn("Failed to finalize message payload:", finalizeError);
1150
1148
  }
@@ -1152,35 +1150,49 @@ var ConsumerGroup = class {
1152
1150
  throw error;
1153
1151
  }
1154
1152
  }
1153
+ /**
1154
+ * Process a pre-fetched message directly, without calling `receiveMessageById`.
1155
+ *
1156
+ * Used by the binary mode (v2beta) small body fast path, where the server
1157
+ * pushes the full message payload in the callback request. The message is
1158
+ * processed with the same lifecycle guarantees as `consume()`:
1159
+ * - Visibility timeout is extended periodically during processing
1160
+ * - Message is deleted on successful handler completion
1161
+ * - Payload is finalized on error if the transport supports it
1162
+ *
1163
+ * @param handler - Function to process the message payload and metadata
1164
+ * @param message - The complete message including payload and receipt handle
1165
+ * @param options - Optional configuration
1166
+ * @param options.visibilityDeadline - Absolute deadline when the server-assigned
1167
+ * visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
1168
+ * schedule the first visibility extension before the lease expires.
1169
+ */
1170
+ async consumeMessage(handler, message, options) {
1171
+ await this.processMessage(message, handler, options);
1172
+ }
1155
1173
  async consume(handler, options) {
1156
- if (options?.messageId) {
1157
- const response = await this.client.receiveMessageById(
1158
- {
1159
- queueName: this.topicName,
1160
- consumerGroup: this.consumerGroupName,
1161
- messageId: options.messageId,
1162
- visibilityTimeoutSeconds: this.visibilityTimeout
1163
- },
1164
- this.transport
1165
- );
1174
+ if (options && "messageId" in options) {
1175
+ const response = await this.client.receiveMessageById({
1176
+ queueName: this.topicName,
1177
+ consumerGroup: this.consumerGroupName,
1178
+ messageId: options.messageId,
1179
+ visibilityTimeoutSeconds: this.visibilityTimeout
1180
+ });
1166
1181
  await this.processMessage(response.message, handler);
1167
1182
  } else {
1183
+ const limit = options && "limit" in options ? options.limit : 1;
1168
1184
  let messageFound = false;
1169
- for await (const message of this.client.receiveMessages(
1170
- {
1171
- queueName: this.topicName,
1172
- consumerGroup: this.consumerGroupName,
1173
- visibilityTimeoutSeconds: this.visibilityTimeout,
1174
- limit: 1
1175
- },
1176
- this.transport
1177
- )) {
1185
+ for await (const message of this.client.receiveMessages({
1186
+ queueName: this.topicName,
1187
+ consumerGroup: this.consumerGroupName,
1188
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1189
+ limit
1190
+ })) {
1178
1191
  messageFound = true;
1179
1192
  await this.processMessage(message, handler);
1180
- break;
1181
1193
  }
1182
1194
  if (!messageFound) {
1183
- throw new Error("No messages available");
1195
+ await handler(null, null);
1184
1196
  }
1185
1197
  }
1186
1198
  }
@@ -1202,17 +1214,14 @@ var ConsumerGroup = class {
1202
1214
  var Topic = class {
1203
1215
  client;
1204
1216
  topicName;
1205
- transport;
1206
1217
  /**
1207
1218
  * Create a new Topic instance
1208
- * @param client QueueClient instance to use for API calls
1219
+ * @param client QueueClient instance to use for API calls (transport is configured on the client)
1209
1220
  * @param topicName Name of the topic to work with
1210
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1211
1221
  */
1212
- constructor(client, topicName, transport) {
1222
+ constructor(client, topicName) {
1213
1223
  this.client = client;
1214
1224
  this.topicName = topicName;
1215
- this.transport = transport || new JsonTransport();
1216
1225
  }
1217
1226
  /**
1218
1227
  * Publish a message to the topic
@@ -1225,17 +1234,14 @@ var Topic = class {
1225
1234
  * @throws {InternalServerError} When server encounters an error
1226
1235
  */
1227
1236
  async publish(payload, options) {
1228
- const result = await this.client.sendMessage(
1229
- {
1230
- queueName: this.topicName,
1231
- payload,
1232
- idempotencyKey: options?.idempotencyKey,
1233
- retentionSeconds: options?.retentionSeconds,
1234
- delaySeconds: options?.delaySeconds,
1235
- headers: options?.headers
1236
- },
1237
- this.transport
1238
- );
1237
+ const result = await this.client.sendMessage({
1238
+ queueName: this.topicName,
1239
+ payload,
1240
+ idempotencyKey: options?.idempotencyKey,
1241
+ retentionSeconds: options?.retentionSeconds,
1242
+ delaySeconds: options?.delaySeconds,
1243
+ headers: options?.headers
1244
+ });
1239
1245
  if (isDevMode()) {
1240
1246
  triggerDevCallbacks(this.topicName, result.messageId);
1241
1247
  }
@@ -1248,15 +1254,11 @@ var Topic = class {
1248
1254
  * @returns A ConsumerGroup instance
1249
1255
  */
1250
1256
  consumerGroup(consumerGroupName, options) {
1251
- const consumerOptions = {
1252
- ...options,
1253
- transport: options?.transport || this.transport
1254
- };
1255
1257
  return new ConsumerGroup(
1256
1258
  this.client,
1257
1259
  this.topicName,
1258
1260
  consumerGroupName,
1259
- consumerOptions
1261
+ options
1260
1262
  );
1261
1263
  }
1262
1264
  /**
@@ -1265,220 +1267,167 @@ var Topic = class {
1265
1267
  get name() {
1266
1268
  return this.topicName;
1267
1269
  }
1268
- /**
1269
- * Get the transport used by this topic
1270
- */
1271
- get serializer() {
1272
- return this.transport;
1273
- }
1274
1270
  };
1275
1271
 
1276
1272
  // src/callback.ts
1277
- function validateWildcardPattern(pattern) {
1278
- const firstIndex = pattern.indexOf("*");
1279
- const lastIndex = pattern.lastIndexOf("*");
1280
- if (firstIndex !== lastIndex) {
1281
- return false;
1282
- }
1283
- if (firstIndex === -1) {
1284
- return false;
1285
- }
1286
- if (firstIndex !== pattern.length - 1) {
1287
- return false;
1288
- }
1289
- return true;
1290
- }
1273
+ var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
1274
+ var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
1291
1275
  function matchesWildcardPattern(topicName, pattern) {
1292
1276
  const prefix = pattern.slice(0, -1);
1293
1277
  return topicName.startsWith(prefix);
1294
1278
  }
1295
- function findTopicHandler(queueName, handlers) {
1296
- const exactHandler = handlers[queueName];
1297
- if (exactHandler) {
1298
- return exactHandler;
1299
- }
1300
- for (const pattern in handlers) {
1301
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1302
- return handlers[pattern];
1303
- }
1304
- }
1305
- return null;
1279
+ function isRecord(value) {
1280
+ return typeof value === "object" && value !== null;
1306
1281
  }
1307
- async function parseCallback(request) {
1308
- const contentType = request.headers.get("content-type");
1282
+ function parseV1StructuredBody(body, contentType) {
1309
1283
  if (!contentType || !contentType.includes("application/cloudevents+json")) {
1310
1284
  throw new Error(
1311
1285
  "Invalid content type: expected 'application/cloudevents+json'"
1312
1286
  );
1313
1287
  }
1314
- let cloudEvent;
1315
- try {
1316
- cloudEvent = await request.json();
1317
- } catch (error) {
1318
- throw new Error("Failed to parse CloudEvent from request body");
1319
- }
1320
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
1288
+ if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
1321
1289
  throw new Error("Invalid CloudEvent: missing required fields");
1322
1290
  }
1323
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1291
+ if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
1324
1292
  throw new Error(
1325
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1293
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
1326
1294
  );
1327
1295
  }
1296
+ const { data } = body;
1328
1297
  const missingFields = [];
1329
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1330
- if (!("consumerGroup" in cloudEvent.data))
1331
- missingFields.push("consumerGroup");
1332
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
1298
+ if (!("queueName" in data)) missingFields.push("queueName");
1299
+ if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
1300
+ if (!("messageId" in data)) missingFields.push("messageId");
1333
1301
  if (missingFields.length > 0) {
1334
1302
  throw new Error(
1335
1303
  `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1336
1304
  );
1337
1305
  }
1338
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
1339
1306
  return {
1340
- queueName,
1341
- consumerGroup,
1342
- messageId
1307
+ queueName: String(data.queueName),
1308
+ consumerGroup: String(data.consumerGroup),
1309
+ messageId: String(data.messageId)
1343
1310
  };
1344
1311
  }
1345
- function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1346
- for (const topicPattern in handlers) {
1347
- if (topicPattern.includes("*")) {
1348
- if (!validateWildcardPattern(topicPattern)) {
1349
- throw new Error(
1350
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1351
- );
1352
- }
1353
- }
1354
- }
1355
- const routeHandler = async (request) => {
1356
- try {
1357
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
1358
- const topicHandler = findTopicHandler(queueName, handlers);
1359
- if (!topicHandler) {
1360
- const availableTopics = Object.keys(handlers).join(", ");
1361
- return Response.json(
1362
- {
1363
- error: `No handler found for topic: ${queueName}`,
1364
- availableTopics
1365
- },
1366
- { status: 404 }
1367
- );
1368
- }
1369
- const consumerGroupHandler = topicHandler[consumerGroup];
1370
- if (!consumerGroupHandler) {
1371
- const availableGroups = Object.keys(topicHandler).join(", ");
1372
- return Response.json(
1373
- {
1374
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1375
- availableGroups
1376
- },
1377
- { status: 404 }
1378
- );
1379
- }
1380
- const topic = new Topic(client, queueName);
1381
- const cg = topic.consumerGroup(
1382
- consumerGroup,
1383
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1384
- );
1385
- await cg.consume(consumerGroupHandler, { messageId });
1386
- return Response.json({ status: "success" });
1387
- } catch (error) {
1388
- console.error("Queue callback error:", error);
1389
- if (error instanceof Error && (error.message.includes("Missing required CloudEvent data fields") || error.message.includes("Invalid CloudEvent") || error.message.includes("Invalid CloudEvent type") || error.message.includes("Invalid content type") || error.message.includes("Failed to parse CloudEvent"))) {
1390
- return Response.json({ error: error.message }, { status: 400 });
1391
- }
1392
- return Response.json(
1393
- { error: "Failed to process queue message" },
1394
- { status: 500 }
1395
- );
1396
- }
1397
- };
1398
- return routeHandler;
1399
- }
1400
- function handleCallback(handlers, options) {
1401
- return createCallbackHandler(
1402
- handlers,
1403
- options?.client || new QueueClient(),
1404
- options?.visibilityTimeoutSeconds
1405
- );
1406
- }
1407
-
1408
- // src/nextjs-pages.ts
1409
1312
  function getHeader(headers, name) {
1313
+ if (headers instanceof Headers) {
1314
+ return headers.get(name);
1315
+ }
1410
1316
  const value = headers[name];
1411
- return Array.isArray(value) ? value[0] : value;
1317
+ if (Array.isArray(value)) return value[0] ?? null;
1318
+ return value ?? null;
1412
1319
  }
1413
- function readBody(req) {
1414
- return new Promise((resolve, reject) => {
1415
- const chunks = [];
1416
- req.on("data", (chunk) => chunks.push(chunk));
1417
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1418
- req.on("error", reject);
1419
- });
1420
- }
1421
- async function getBody(req) {
1422
- if (req.body === void 0) {
1423
- return readBody(req);
1320
+ function parseBinaryHeaders(headers) {
1321
+ const ceType = getHeader(headers, "ce-type");
1322
+ if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
1323
+ throw new Error(
1324
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
1325
+ );
1424
1326
  }
1425
- if (typeof req.body === "string") {
1426
- return req.body;
1327
+ const queueName = getHeader(headers, "ce-vqsqueuename");
1328
+ const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
1329
+ const messageId = getHeader(headers, "ce-vqsmessageid");
1330
+ const missingFields = [];
1331
+ if (!queueName) missingFields.push("ce-vqsqueuename");
1332
+ if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
1333
+ if (!messageId) missingFields.push("ce-vqsmessageid");
1334
+ if (missingFields.length > 0) {
1335
+ throw new Error(
1336
+ `Missing required CloudEvent headers: ${missingFields.join(", ")}`
1337
+ );
1338
+ }
1339
+ const base = {
1340
+ queueName,
1341
+ consumerGroup,
1342
+ messageId
1343
+ };
1344
+ const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
1345
+ if (!receiptHandle) {
1346
+ return base;
1347
+ }
1348
+ const result = { ...base, receiptHandle };
1349
+ const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
1350
+ if (deliveryCount) {
1351
+ result.deliveryCount = parseInt(deliveryCount, 10);
1352
+ }
1353
+ const createdAt = getHeader(headers, "ce-vqscreatedat");
1354
+ if (createdAt) {
1355
+ result.createdAt = createdAt;
1356
+ }
1357
+ const contentType = getHeader(headers, "content-type");
1358
+ if (contentType) {
1359
+ result.contentType = contentType;
1427
1360
  }
1428
- return JSON.stringify(req.body);
1361
+ const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
1362
+ if (visibilityDeadline) {
1363
+ result.visibilityDeadline = visibilityDeadline;
1364
+ }
1365
+ return result;
1429
1366
  }
1430
- async function createRequestFromNextApi(req) {
1431
- const protocol = getHeader(req.headers, "x-forwarded-proto") ?? "https";
1432
- const host = getHeader(req.headers, "host");
1433
- if (!host) {
1434
- throw new Error("Missing host header");
1435
- }
1436
- const url = `${protocol}://${host}${req.url}`;
1437
- const headers = new Headers();
1438
- for (const [key, value] of Object.entries(req.headers)) {
1439
- if (value) {
1440
- if (Array.isArray(value)) {
1441
- value.forEach((v) => headers.append(key, v));
1442
- } else {
1443
- headers.set(key, value);
1444
- }
1367
+ function parseRawCallback(body, headers) {
1368
+ const ceType = getHeader(headers, "ce-type");
1369
+ if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
1370
+ const result = parseBinaryHeaders(headers);
1371
+ if ("receiptHandle" in result) {
1372
+ result.parsedPayload = body;
1445
1373
  }
1374
+ return result;
1446
1375
  }
1447
- const body = await getBody(req);
1448
- return new Request(url, {
1449
- method: req.method || "POST",
1450
- headers,
1451
- body
1452
- });
1376
+ return parseV1StructuredBody(body, getHeader(headers, "content-type"));
1453
1377
  }
1454
- async function sendResponseToNextApi(response, res) {
1455
- res.status(response.status);
1456
- response.headers.forEach((value, key) => {
1457
- res.setHeader(key, value);
1458
- });
1459
- const contentType = response.headers.get("content-type");
1460
- if (contentType?.includes("application/json")) {
1461
- const data = await response.json();
1462
- res.json(data);
1378
+ async function handleCallback(handler, request, options) {
1379
+ const { queueName, consumerGroup, messageId } = request;
1380
+ const client = options?.client || new QueueClient();
1381
+ const topic = new Topic(client, queueName);
1382
+ const cg = topic.consumerGroup(
1383
+ consumerGroup,
1384
+ options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
1385
+ );
1386
+ if ("receiptHandle" in request) {
1387
+ const transport = client.getTransport();
1388
+ let payload;
1389
+ if (request.rawBody) {
1390
+ payload = await transport.deserialize(request.rawBody);
1391
+ } else if (request.parsedPayload !== void 0) {
1392
+ payload = request.parsedPayload;
1393
+ } else {
1394
+ throw new Error(
1395
+ "Binary mode callback with receipt handle is missing payload"
1396
+ );
1397
+ }
1398
+ const message = {
1399
+ messageId,
1400
+ payload,
1401
+ deliveryCount: request.deliveryCount ?? 1,
1402
+ createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
1403
+ contentType: request.contentType ?? transport.contentType,
1404
+ receiptHandle: request.receiptHandle
1405
+ };
1406
+ const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
1407
+ await cg.consumeMessage(handler, message, { visibilityDeadline });
1463
1408
  } else {
1464
- const text = await response.text();
1465
- res.send(text);
1409
+ await cg.consume(handler, { messageId });
1466
1410
  }
1467
1411
  }
1468
- function handleCallback2(handlers) {
1469
- const webHandler = handleCallback(handlers);
1412
+
1413
+ // src/nextjs-pages.ts
1414
+ function handleCallback2(handler, options) {
1470
1415
  return async (req, res) => {
1471
1416
  if (req.method !== "POST") {
1472
1417
  res.status(200).end();
1473
1418
  return;
1474
1419
  }
1475
1420
  try {
1476
- const request = await createRequestFromNextApi(req);
1477
- const response = await webHandler(request);
1478
- await sendResponseToNextApi(response, res);
1421
+ const parsed = parseRawCallback(req.body, req.headers);
1422
+ await handleCallback(handler, parsed, options);
1423
+ res.status(200).json({ status: "success" });
1479
1424
  } catch (error) {
1480
- console.error("Pages Router adapter error:", error);
1481
- res.status(500).json({ error: "Internal server error" });
1425
+ console.error("Queue callback error:", error);
1426
+ if (error instanceof Error && (error.message.includes("Invalid content type") || error.message.includes("Invalid CloudEvent") || error.message.includes("Missing required CloudEvent") || error.message.includes("Failed to parse CloudEvent") || error.message.includes("Binary mode callback"))) {
1427
+ res.status(400).json({ error: error.message });
1428
+ return;
1429
+ }
1430
+ res.status(500).json({ error: "Failed to process queue message" });
1482
1431
  }
1483
1432
  };
1484
1433
  }