@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.
@@ -5,46 +5,6 @@ import { parseMultipartStream } from "mixpart";
5
5
  import * as fs from "fs";
6
6
  import * as path from "path";
7
7
 
8
- // src/transports.ts
9
- async function streamToBuffer(stream) {
10
- let totalLength = 0;
11
- const reader = stream.getReader();
12
- const chunks = [];
13
- try {
14
- while (true) {
15
- const { done, value } = await reader.read();
16
- if (done) break;
17
- chunks.push(value);
18
- totalLength += value.length;
19
- }
20
- } finally {
21
- reader.releaseLock();
22
- }
23
- return Buffer.concat(chunks, totalLength);
24
- }
25
- var JsonTransport = class {
26
- contentType = "application/json";
27
- replacer;
28
- reviver;
29
- /**
30
- * Create a new JsonTransport.
31
- * @param options - Optional JSON serialization options
32
- * @param options.replacer - Custom replacer for JSON.stringify
33
- * @param options.reviver - Custom reviver for JSON.parse
34
- */
35
- constructor(options = {}) {
36
- this.replacer = options.replacer;
37
- this.reviver = options.reviver;
38
- }
39
- serialize(value) {
40
- return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
41
- }
42
- async deserialize(stream) {
43
- const buffer = await streamToBuffer(stream);
44
- return JSON.parse(buffer.toString("utf8"), this.reviver);
45
- }
46
- };
47
-
48
8
  // src/types.ts
49
9
  var MessageNotFoundError = class extends Error {
50
10
  constructor(messageId) {
@@ -66,14 +26,6 @@ var MessageCorruptedError = class extends Error {
66
26
  this.name = "MessageCorruptedError";
67
27
  }
68
28
  };
69
- var QueueEmptyError = class extends Error {
70
- constructor(queueName, consumerGroup) {
71
- super(
72
- `No messages available in queue "${queueName}" for consumer group "${consumerGroup}"`
73
- );
74
- this.name = "QueueEmptyError";
75
- }
76
- };
77
29
  var UnauthorizedError = class extends Error {
78
30
  constructor(message = "Missing or invalid authentication token") {
79
31
  super(message);
@@ -110,18 +62,6 @@ var MessageAlreadyProcessedError = class extends Error {
110
62
  this.name = "MessageAlreadyProcessedError";
111
63
  }
112
64
  };
113
- var ConcurrencyLimitError = class extends Error {
114
- /** Current number of in-flight messages for this consumer group. */
115
- currentInflight;
116
- /** Maximum allowed concurrent messages (as configured). */
117
- maxConcurrency;
118
- constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
119
- super(message);
120
- this.name = "ConcurrencyLimitError";
121
- this.currentInflight = currentInflight;
122
- this.maxConcurrency = maxConcurrency;
123
- }
124
- };
125
65
  var DuplicateMessageError = class extends Error {
126
66
  idempotencyKey;
127
67
  constructor(message, idempotencyKey) {
@@ -148,12 +88,15 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
148
88
  // src/dev.ts
149
89
  var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
150
90
  function filePathToUrlPath(filePath) {
151
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
91
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
152
92
  if (!urlPath.startsWith("/")) {
153
93
  urlPath = "/" + urlPath;
154
94
  }
155
95
  return urlPath;
156
96
  }
97
+ function filePathToConsumerGroup(filePath) {
98
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
99
+ }
157
100
  function getDevRouteMappings() {
158
101
  const g = globalThis;
159
102
  if (ROUTE_MAPPINGS_KEY in g) {
@@ -174,11 +117,11 @@ function getDevRouteMappings() {
174
117
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
175
118
  if (!config.experimentalTriggers) continue;
176
119
  for (const trigger of config.experimentalTriggers) {
177
- if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
120
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
178
121
  mappings.push({
179
122
  urlPath: filePathToUrlPath(filePath),
180
123
  topic: trigger.topic,
181
- consumer: trigger.consumer
124
+ consumer: filePathToConsumerGroup(filePath)
182
125
  });
183
126
  }
184
127
  }
@@ -211,20 +154,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
211
154
  var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
212
155
  async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
213
156
  const client = new QueueClient();
214
- const transport = new JsonTransport();
215
157
  let elapsed = 0;
216
158
  let interval = DEV_VISIBILITY_POLL_INTERVAL;
217
159
  while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
218
160
  try {
219
- await client.receiveMessageById(
220
- {
221
- queueName: topicName,
222
- consumerGroup,
223
- messageId,
224
- visibilityTimeoutSeconds: 0
225
- },
226
- transport
227
- );
161
+ await client.receiveMessageById({
162
+ queueName: topicName,
163
+ consumerGroup,
164
+ messageId,
165
+ visibilityTimeoutSeconds: 0
166
+ });
228
167
  return true;
229
168
  } catch (error) {
230
169
  if (error instanceof MessageNotFoundError) {
@@ -298,26 +237,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
298
237
  console.log(
299
238
  `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
300
239
  );
301
- const cloudEvent = {
302
- type: "com.vercel.queue.v1beta",
303
- source: `/topic/${topicName}/consumer/${route.consumer}`,
304
- id: messageId,
305
- datacontenttype: "application/json",
306
- data: {
307
- messageId,
308
- queueName: topicName,
309
- consumerGroup: route.consumer
310
- },
311
- time: (/* @__PURE__ */ new Date()).toISOString(),
312
- specversion: "1.0"
313
- };
314
240
  try {
315
241
  const response = await fetch(url, {
316
242
  method: "POST",
317
243
  headers: {
318
- "Content-Type": "application/cloudevents+json"
319
- },
320
- body: JSON.stringify(cloudEvent)
244
+ "ce-type": CLOUD_EVENT_TYPE_V2BETA,
245
+ "ce-vqsqueuename": topicName,
246
+ "ce-vqsconsumergroup": route.consumer,
247
+ "ce-vqsmessageid": messageId
248
+ }
321
249
  });
322
250
  if (response.ok) {
323
251
  try {
@@ -364,6 +292,46 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
364
292
  // src/oidc.ts
365
293
  import { getVercelOidcToken } from "@vercel/oidc";
366
294
 
295
+ // src/transports.ts
296
+ async function streamToBuffer(stream) {
297
+ let totalLength = 0;
298
+ const reader = stream.getReader();
299
+ const chunks = [];
300
+ try {
301
+ while (true) {
302
+ const { done, value } = await reader.read();
303
+ if (done) break;
304
+ chunks.push(value);
305
+ totalLength += value.length;
306
+ }
307
+ } finally {
308
+ reader.releaseLock();
309
+ }
310
+ return Buffer.concat(chunks, totalLength);
311
+ }
312
+ var JsonTransport = class {
313
+ contentType = "application/json";
314
+ replacer;
315
+ reviver;
316
+ /**
317
+ * Create a new JsonTransport.
318
+ * @param options - Optional JSON serialization options
319
+ * @param options.replacer - Custom replacer for JSON.stringify
320
+ * @param options.reviver - Custom reviver for JSON.parse
321
+ */
322
+ constructor(options = {}) {
323
+ this.replacer = options.replacer;
324
+ this.reviver = options.reviver;
325
+ }
326
+ serialize(value) {
327
+ return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
328
+ }
329
+ async deserialize(stream) {
330
+ const buffer = await streamToBuffer(stream);
331
+ return JSON.parse(buffer.toString("utf8"), this.reviver);
332
+ }
333
+ };
334
+
367
335
  // src/client.ts
368
336
  function isDebugEnabled() {
369
337
  return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
@@ -424,6 +392,7 @@ var QueueClient = class {
424
392
  providedToken;
425
393
  defaultDeploymentId;
426
394
  pinToDeployment;
395
+ transport;
427
396
  constructor(options = {}) {
428
397
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
429
398
  this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
@@ -431,6 +400,10 @@ var QueueClient = class {
431
400
  this.providedToken = options.token;
432
401
  this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
433
402
  this.pinToDeployment = options.pinToDeployment ?? true;
403
+ this.transport = options.transport || new JsonTransport();
404
+ }
405
+ getTransport() {
406
+ return this.transport;
434
407
  }
435
408
  getSendDeploymentId() {
436
409
  if (isDevMode()) {
@@ -487,6 +460,8 @@ var QueueClient = class {
487
460
  }
488
461
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
489
462
  }
463
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.38"}`);
464
+ init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
490
465
  const response = await fetch(url, init);
491
466
  if (isDebugEnabled()) {
492
467
  const logData = {
@@ -509,7 +484,6 @@ var QueueClient = class {
509
484
  * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
510
485
  * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
511
486
  * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
512
- * @param transport - Serializer for the payload
513
487
  * @returns Promise with the generated messageId
514
488
  * @throws {DuplicateMessageError} When idempotency key was already used
515
489
  * @throws {ConsumerDiscoveryError} When consumer discovery fails
@@ -519,7 +493,8 @@ var QueueClient = class {
519
493
  * @throws {ForbiddenError} When access is denied
520
494
  * @throws {InternalServerError} When server encounters an error
521
495
  */
522
- async sendMessage(options, transport) {
496
+ async sendMessage(options) {
497
+ const transport = this.transport;
523
498
  const {
524
499
  queueName,
525
500
  payload,
@@ -601,30 +576,25 @@ var QueueClient = class {
601
576
  /**
602
577
  * Receive messages from a topic as an async generator.
603
578
  *
579
+ * When the queue is empty, the generator completes without yielding any
580
+ * messages. Callers should handle the case where no messages are yielded.
581
+ *
604
582
  * @param options - Receive options
605
583
  * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
606
584
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
607
585
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
608
586
  * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
609
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
610
- * @param transport - Deserializer for message payloads
611
587
  * @yields Message objects with payload, messageId, receiptHandle, etc.
612
- * @throws {QueueEmptyError} When no messages available
588
+ * Yields nothing if queue is empty.
613
589
  * @throws {InvalidLimitError} When limit is outside 1-10 range
614
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
615
590
  * @throws {BadRequestError} When parameters are invalid
616
591
  * @throws {UnauthorizedError} When authentication fails
617
592
  * @throws {ForbiddenError} When access is denied
618
593
  * @throws {InternalServerError} When server encounters an error
619
594
  */
620
- async *receiveMessages(options, transport) {
621
- const {
622
- queueName,
623
- consumerGroup,
624
- visibilityTimeoutSeconds,
625
- limit,
626
- maxConcurrency
627
- } = options;
595
+ async *receiveMessages(options) {
596
+ const transport = this.transport;
597
+ const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
628
598
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
629
599
  throw new InvalidLimitError(limit);
630
600
  }
@@ -642,9 +612,6 @@ var QueueClient = class {
642
612
  if (limit !== void 0) {
643
613
  headers.set("Vqs-Max-Messages", limit.toString());
644
614
  }
645
- if (maxConcurrency !== void 0) {
646
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
647
- }
648
615
  const effectiveDeploymentId = this.getConsumeDeploymentId();
649
616
  if (effectiveDeploymentId) {
650
617
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
@@ -657,22 +624,10 @@ var QueueClient = class {
657
624
  }
658
625
  );
659
626
  if (response.status === 204) {
660
- throw new QueueEmptyError(queueName, consumerGroup);
627
+ return;
661
628
  }
662
629
  if (!response.ok) {
663
630
  const errorText = await response.text();
664
- if (response.status === 429) {
665
- let errorData = {};
666
- try {
667
- errorData = JSON.parse(errorText);
668
- } catch {
669
- }
670
- throw new ConcurrencyLimitError(
671
- errorData.error || "Concurrency limit exceeded or throttled",
672
- errorData.currentInflight,
673
- errorData.maxConcurrency
674
- );
675
- }
676
631
  throwCommonHttpError(
677
632
  response.status,
678
633
  response.statusText,
@@ -710,26 +665,18 @@ var QueueClient = class {
710
665
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
711
666
  * @param options.messageId - Message ID to retrieve
712
667
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
713
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
714
- * @param transport - Deserializer for the message payload
715
668
  * @returns Promise with the message
716
669
  * @throws {MessageNotFoundError} When message doesn't exist
717
670
  * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
718
671
  * @throws {MessageAlreadyProcessedError} When message was already processed
719
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
720
672
  * @throws {BadRequestError} When parameters are invalid
721
673
  * @throws {UnauthorizedError} When authentication fails
722
674
  * @throws {ForbiddenError} When access is denied
723
675
  * @throws {InternalServerError} When server encounters an error
724
676
  */
725
- async receiveMessageById(options, transport) {
726
- const {
727
- queueName,
728
- consumerGroup,
729
- messageId,
730
- visibilityTimeoutSeconds,
731
- maxConcurrency
732
- } = options;
677
+ async receiveMessageById(options) {
678
+ const transport = this.transport;
679
+ const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
733
680
  const headers = new Headers({
734
681
  Authorization: `Bearer ${await this.getToken()}`,
735
682
  Accept: "multipart/mixed",
@@ -741,9 +688,6 @@ var QueueClient = class {
741
688
  visibilityTimeoutSeconds.toString()
742
689
  );
743
690
  }
744
- if (maxConcurrency !== void 0) {
745
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
746
- }
747
691
  const effectiveDeploymentId = this.getConsumeDeploymentId();
748
692
  if (effectiveDeploymentId) {
749
693
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
@@ -777,18 +721,6 @@ var QueueClient = class {
777
721
  if (response.status === 410) {
778
722
  throw new MessageAlreadyProcessedError(messageId);
779
723
  }
780
- if (response.status === 429) {
781
- let errorData = {};
782
- try {
783
- errorData = JSON.parse(errorText);
784
- } catch {
785
- }
786
- throw new ConcurrencyLimitError(
787
- errorData.error || "Concurrency limit exceeded or throttled",
788
- errorData.currentInflight,
789
- errorData.maxConcurrency
790
- );
791
- }
792
724
  throwCommonHttpError(
793
725
  response.status,
794
726
  response.statusText,
@@ -1006,40 +938,90 @@ var QueueClient = class {
1006
938
  };
1007
939
 
1008
940
  // src/consumer-group.ts
941
+ var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
942
+ var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
943
+ var MAX_RENEWAL_INTERVAL_SECONDS = 60;
944
+ var MIN_RENEWAL_INTERVAL_SECONDS = 10;
945
+ var RETRY_INTERVAL_MS = 3e3;
946
+ function calculateRenewalInterval(visibilityTimeoutSeconds) {
947
+ return Math.min(
948
+ MAX_RENEWAL_INTERVAL_SECONDS,
949
+ Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
950
+ );
951
+ }
1009
952
  var ConsumerGroup = class {
1010
953
  client;
1011
954
  topicName;
1012
955
  consumerGroupName;
1013
956
  visibilityTimeout;
1014
- refreshInterval;
1015
- transport;
1016
957
  /**
1017
958
  * Create a new ConsumerGroup instance.
1018
959
  *
1019
- * @param client - QueueClient instance to use for API calls
960
+ * @param client - QueueClient instance to use for API calls (transport is configured on the client)
1020
961
  * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
1021
962
  * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1022
963
  * @param options - Optional configuration
1023
- * @param options.transport - Payload serializer (default: JsonTransport)
1024
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1025
- * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
964
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1026
965
  */
1027
966
  constructor(client, topicName, consumerGroupName, options = {}) {
1028
967
  this.client = client;
1029
968
  this.topicName = topicName;
1030
969
  this.consumerGroupName = consumerGroupName;
1031
- this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1032
- this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
1033
- this.transport = options.transport || new JsonTransport();
970
+ this.visibilityTimeout = Math.max(
971
+ MIN_VISIBILITY_TIMEOUT_SECONDS,
972
+ options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
973
+ );
974
+ }
975
+ /**
976
+ * Check if an error is a 4xx client error that should stop retries.
977
+ * 4xx errors indicate the request is fundamentally invalid and retrying won't help.
978
+ * - 409: Ticket mismatch (lost ownership to another consumer)
979
+ * - 404: Message/receipt handle not found
980
+ * - 400, 401, 403: Other client errors
981
+ */
982
+ isClientError(error) {
983
+ return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
984
+ error instanceof MessageNotFoundError || // 404 - receipt handle not found
985
+ error instanceof BadRequestError || // 400 - invalid parameters
986
+ error instanceof UnauthorizedError || // 401 - auth failed
987
+ error instanceof ForbiddenError;
1034
988
  }
1035
989
  /**
1036
990
  * Starts a background loop that periodically extends the visibility timeout for a message.
991
+ *
992
+ * Timing strategy:
993
+ * - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
994
+ * - Extensions request the same duration as the initial visibility timeout
995
+ * - When `visibilityDeadline` is provided (binary mode small body), the first
996
+ * extension delay is calculated from the time remaining until the deadline
997
+ * using the same renewal formula, ensuring the first extension fires before
998
+ * the server-assigned lease expires. Subsequent renewals use the standard interval.
999
+ *
1000
+ * Retry strategy:
1001
+ * - On transient failures (5xx, network errors): retry every 3 seconds
1002
+ * - On 4xx client errors: stop retrying (the lease is lost or invalid)
1003
+ *
1004
+ * @param receiptHandle - The receipt handle to extend visibility for
1005
+ * @param options - Optional configuration
1006
+ * @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
1007
+ * when the current visibility timeout expires. Used to calculate the first extension delay.
1037
1008
  */
1038
- startVisibilityExtension(receiptHandle) {
1009
+ startVisibilityExtension(receiptHandle, options) {
1039
1010
  let isRunning = true;
1040
1011
  let isResolved = false;
1041
1012
  let resolveLifecycle;
1042
1013
  let timeoutId = null;
1014
+ const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
1015
+ let firstDelayMs = renewalIntervalMs;
1016
+ if (options?.visibilityDeadline) {
1017
+ const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
1018
+ if (timeRemainingMs > 0) {
1019
+ const timeRemainingSeconds = timeRemainingMs / 1e3;
1020
+ firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
1021
+ } else {
1022
+ firstDelayMs = 0;
1023
+ }
1024
+ }
1043
1025
  const lifecyclePromise = new Promise((resolve) => {
1044
1026
  resolveLifecycle = resolve;
1045
1027
  });
@@ -1062,19 +1044,31 @@ var ConsumerGroup = class {
1062
1044
  visibilityTimeoutSeconds: this.visibilityTimeout
1063
1045
  });
1064
1046
  if (isRunning) {
1065
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1047
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
1066
1048
  } else {
1067
1049
  safeResolve();
1068
1050
  }
1069
1051
  } catch (error) {
1052
+ if (this.isClientError(error)) {
1053
+ console.error(
1054
+ `Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
1055
+ error
1056
+ );
1057
+ safeResolve();
1058
+ return;
1059
+ }
1070
1060
  console.error(
1071
- `Failed to extend visibility for receipt handle ${receiptHandle}:`,
1061
+ `Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
1072
1062
  error
1073
1063
  );
1074
- safeResolve();
1064
+ if (isRunning) {
1065
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
1066
+ } else {
1067
+ safeResolve();
1068
+ }
1075
1069
  }
1076
1070
  };
1077
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1071
+ timeoutId = setTimeout(() => extend(), firstDelayMs);
1078
1072
  return async (waitForCompletion = false) => {
1079
1073
  isRunning = false;
1080
1074
  if (timeoutId) {
@@ -1088,8 +1082,11 @@ var ConsumerGroup = class {
1088
1082
  }
1089
1083
  };
1090
1084
  }
1091
- async processMessage(message, handler) {
1092
- const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1085
+ async processMessage(message, handler, options) {
1086
+ const stopExtension = this.startVisibilityExtension(
1087
+ message.receiptHandle,
1088
+ options
1089
+ );
1093
1090
  try {
1094
1091
  await handler(message.payload, {
1095
1092
  messageId: message.messageId,
@@ -1106,9 +1103,10 @@ var ConsumerGroup = class {
1106
1103
  });
1107
1104
  } catch (error) {
1108
1105
  await stopExtension();
1109
- if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
1106
+ const transport = this.client.getTransport();
1107
+ if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
1110
1108
  try {
1111
- await this.transport.finalize(message.payload);
1109
+ await transport.finalize(message.payload);
1112
1110
  } catch (finalizeError) {
1113
1111
  console.warn("Failed to finalize message payload:", finalizeError);
1114
1112
  }
@@ -1116,35 +1114,49 @@ var ConsumerGroup = class {
1116
1114
  throw error;
1117
1115
  }
1118
1116
  }
1117
+ /**
1118
+ * Process a pre-fetched message directly, without calling `receiveMessageById`.
1119
+ *
1120
+ * Used by the binary mode (v2beta) small body fast path, where the server
1121
+ * pushes the full message payload in the callback request. The message is
1122
+ * processed with the same lifecycle guarantees as `consume()`:
1123
+ * - Visibility timeout is extended periodically during processing
1124
+ * - Message is deleted on successful handler completion
1125
+ * - Payload is finalized on error if the transport supports it
1126
+ *
1127
+ * @param handler - Function to process the message payload and metadata
1128
+ * @param message - The complete message including payload and receipt handle
1129
+ * @param options - Optional configuration
1130
+ * @param options.visibilityDeadline - Absolute deadline when the server-assigned
1131
+ * visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
1132
+ * schedule the first visibility extension before the lease expires.
1133
+ */
1134
+ async consumeMessage(handler, message, options) {
1135
+ await this.processMessage(message, handler, options);
1136
+ }
1119
1137
  async consume(handler, options) {
1120
- if (options?.messageId) {
1121
- const response = await this.client.receiveMessageById(
1122
- {
1123
- queueName: this.topicName,
1124
- consumerGroup: this.consumerGroupName,
1125
- messageId: options.messageId,
1126
- visibilityTimeoutSeconds: this.visibilityTimeout
1127
- },
1128
- this.transport
1129
- );
1138
+ if (options && "messageId" in options) {
1139
+ const response = await this.client.receiveMessageById({
1140
+ queueName: this.topicName,
1141
+ consumerGroup: this.consumerGroupName,
1142
+ messageId: options.messageId,
1143
+ visibilityTimeoutSeconds: this.visibilityTimeout
1144
+ });
1130
1145
  await this.processMessage(response.message, handler);
1131
1146
  } else {
1147
+ const limit = options && "limit" in options ? options.limit : 1;
1132
1148
  let messageFound = false;
1133
- for await (const message of this.client.receiveMessages(
1134
- {
1135
- queueName: this.topicName,
1136
- consumerGroup: this.consumerGroupName,
1137
- visibilityTimeoutSeconds: this.visibilityTimeout,
1138
- limit: 1
1139
- },
1140
- this.transport
1141
- )) {
1149
+ for await (const message of this.client.receiveMessages({
1150
+ queueName: this.topicName,
1151
+ consumerGroup: this.consumerGroupName,
1152
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1153
+ limit
1154
+ })) {
1142
1155
  messageFound = true;
1143
1156
  await this.processMessage(message, handler);
1144
- break;
1145
1157
  }
1146
1158
  if (!messageFound) {
1147
- throw new Error("No messages available");
1159
+ await handler(null, null);
1148
1160
  }
1149
1161
  }
1150
1162
  }
@@ -1166,17 +1178,14 @@ var ConsumerGroup = class {
1166
1178
  var Topic = class {
1167
1179
  client;
1168
1180
  topicName;
1169
- transport;
1170
1181
  /**
1171
1182
  * Create a new Topic instance
1172
- * @param client QueueClient instance to use for API calls
1183
+ * @param client QueueClient instance to use for API calls (transport is configured on the client)
1173
1184
  * @param topicName Name of the topic to work with
1174
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1175
1185
  */
1176
- constructor(client, topicName, transport) {
1186
+ constructor(client, topicName) {
1177
1187
  this.client = client;
1178
1188
  this.topicName = topicName;
1179
- this.transport = transport || new JsonTransport();
1180
1189
  }
1181
1190
  /**
1182
1191
  * Publish a message to the topic
@@ -1189,17 +1198,14 @@ var Topic = class {
1189
1198
  * @throws {InternalServerError} When server encounters an error
1190
1199
  */
1191
1200
  async publish(payload, options) {
1192
- const result = await this.client.sendMessage(
1193
- {
1194
- queueName: this.topicName,
1195
- payload,
1196
- idempotencyKey: options?.idempotencyKey,
1197
- retentionSeconds: options?.retentionSeconds,
1198
- delaySeconds: options?.delaySeconds,
1199
- headers: options?.headers
1200
- },
1201
- this.transport
1202
- );
1201
+ const result = await this.client.sendMessage({
1202
+ queueName: this.topicName,
1203
+ payload,
1204
+ idempotencyKey: options?.idempotencyKey,
1205
+ retentionSeconds: options?.retentionSeconds,
1206
+ delaySeconds: options?.delaySeconds,
1207
+ headers: options?.headers
1208
+ });
1203
1209
  if (isDevMode()) {
1204
1210
  triggerDevCallbacks(this.topicName, result.messageId);
1205
1211
  }
@@ -1212,15 +1218,11 @@ var Topic = class {
1212
1218
  * @returns A ConsumerGroup instance
1213
1219
  */
1214
1220
  consumerGroup(consumerGroupName, options) {
1215
- const consumerOptions = {
1216
- ...options,
1217
- transport: options?.transport || this.transport
1218
- };
1219
1221
  return new ConsumerGroup(
1220
1222
  this.client,
1221
1223
  this.topicName,
1222
1224
  consumerGroupName,
1223
- consumerOptions
1225
+ options
1224
1226
  );
1225
1227
  }
1226
1228
  /**
@@ -1229,220 +1231,167 @@ var Topic = class {
1229
1231
  get name() {
1230
1232
  return this.topicName;
1231
1233
  }
1232
- /**
1233
- * Get the transport used by this topic
1234
- */
1235
- get serializer() {
1236
- return this.transport;
1237
- }
1238
1234
  };
1239
1235
 
1240
1236
  // src/callback.ts
1241
- function validateWildcardPattern(pattern) {
1242
- const firstIndex = pattern.indexOf("*");
1243
- const lastIndex = pattern.lastIndexOf("*");
1244
- if (firstIndex !== lastIndex) {
1245
- return false;
1246
- }
1247
- if (firstIndex === -1) {
1248
- return false;
1249
- }
1250
- if (firstIndex !== pattern.length - 1) {
1251
- return false;
1252
- }
1253
- return true;
1254
- }
1237
+ var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
1238
+ var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
1255
1239
  function matchesWildcardPattern(topicName, pattern) {
1256
1240
  const prefix = pattern.slice(0, -1);
1257
1241
  return topicName.startsWith(prefix);
1258
1242
  }
1259
- function findTopicHandler(queueName, handlers) {
1260
- const exactHandler = handlers[queueName];
1261
- if (exactHandler) {
1262
- return exactHandler;
1263
- }
1264
- for (const pattern in handlers) {
1265
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1266
- return handlers[pattern];
1267
- }
1268
- }
1269
- return null;
1243
+ function isRecord(value) {
1244
+ return typeof value === "object" && value !== null;
1270
1245
  }
1271
- async function parseCallback(request) {
1272
- const contentType = request.headers.get("content-type");
1246
+ function parseV1StructuredBody(body, contentType) {
1273
1247
  if (!contentType || !contentType.includes("application/cloudevents+json")) {
1274
1248
  throw new Error(
1275
1249
  "Invalid content type: expected 'application/cloudevents+json'"
1276
1250
  );
1277
1251
  }
1278
- let cloudEvent;
1279
- try {
1280
- cloudEvent = await request.json();
1281
- } catch (error) {
1282
- throw new Error("Failed to parse CloudEvent from request body");
1283
- }
1284
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
1252
+ if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
1285
1253
  throw new Error("Invalid CloudEvent: missing required fields");
1286
1254
  }
1287
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1255
+ if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
1288
1256
  throw new Error(
1289
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1257
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
1290
1258
  );
1291
1259
  }
1260
+ const { data } = body;
1292
1261
  const missingFields = [];
1293
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1294
- if (!("consumerGroup" in cloudEvent.data))
1295
- missingFields.push("consumerGroup");
1296
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
1262
+ if (!("queueName" in data)) missingFields.push("queueName");
1263
+ if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
1264
+ if (!("messageId" in data)) missingFields.push("messageId");
1297
1265
  if (missingFields.length > 0) {
1298
1266
  throw new Error(
1299
1267
  `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1300
1268
  );
1301
1269
  }
1302
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
1303
1270
  return {
1304
- queueName,
1305
- consumerGroup,
1306
- messageId
1271
+ queueName: String(data.queueName),
1272
+ consumerGroup: String(data.consumerGroup),
1273
+ messageId: String(data.messageId)
1307
1274
  };
1308
1275
  }
1309
- function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1310
- for (const topicPattern in handlers) {
1311
- if (topicPattern.includes("*")) {
1312
- if (!validateWildcardPattern(topicPattern)) {
1313
- throw new Error(
1314
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1315
- );
1316
- }
1317
- }
1318
- }
1319
- const routeHandler = async (request) => {
1320
- try {
1321
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
1322
- const topicHandler = findTopicHandler(queueName, handlers);
1323
- if (!topicHandler) {
1324
- const availableTopics = Object.keys(handlers).join(", ");
1325
- return Response.json(
1326
- {
1327
- error: `No handler found for topic: ${queueName}`,
1328
- availableTopics
1329
- },
1330
- { status: 404 }
1331
- );
1332
- }
1333
- const consumerGroupHandler = topicHandler[consumerGroup];
1334
- if (!consumerGroupHandler) {
1335
- const availableGroups = Object.keys(topicHandler).join(", ");
1336
- return Response.json(
1337
- {
1338
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1339
- availableGroups
1340
- },
1341
- { status: 404 }
1342
- );
1343
- }
1344
- const topic = new Topic(client, queueName);
1345
- const cg = topic.consumerGroup(
1346
- consumerGroup,
1347
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1348
- );
1349
- await cg.consume(consumerGroupHandler, { messageId });
1350
- return Response.json({ status: "success" });
1351
- } catch (error) {
1352
- console.error("Queue callback error:", error);
1353
- 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"))) {
1354
- return Response.json({ error: error.message }, { status: 400 });
1355
- }
1356
- return Response.json(
1357
- { error: "Failed to process queue message" },
1358
- { status: 500 }
1359
- );
1360
- }
1361
- };
1362
- return routeHandler;
1363
- }
1364
- function handleCallback(handlers, options) {
1365
- return createCallbackHandler(
1366
- handlers,
1367
- options?.client || new QueueClient(),
1368
- options?.visibilityTimeoutSeconds
1369
- );
1370
- }
1371
-
1372
- // src/nextjs-pages.ts
1373
1276
  function getHeader(headers, name) {
1277
+ if (headers instanceof Headers) {
1278
+ return headers.get(name);
1279
+ }
1374
1280
  const value = headers[name];
1375
- return Array.isArray(value) ? value[0] : value;
1281
+ if (Array.isArray(value)) return value[0] ?? null;
1282
+ return value ?? null;
1376
1283
  }
1377
- function readBody(req) {
1378
- return new Promise((resolve, reject) => {
1379
- const chunks = [];
1380
- req.on("data", (chunk) => chunks.push(chunk));
1381
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1382
- req.on("error", reject);
1383
- });
1384
- }
1385
- async function getBody(req) {
1386
- if (req.body === void 0) {
1387
- return readBody(req);
1284
+ function parseBinaryHeaders(headers) {
1285
+ const ceType = getHeader(headers, "ce-type");
1286
+ if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
1287
+ throw new Error(
1288
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
1289
+ );
1388
1290
  }
1389
- if (typeof req.body === "string") {
1390
- return req.body;
1291
+ const queueName = getHeader(headers, "ce-vqsqueuename");
1292
+ const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
1293
+ const messageId = getHeader(headers, "ce-vqsmessageid");
1294
+ const missingFields = [];
1295
+ if (!queueName) missingFields.push("ce-vqsqueuename");
1296
+ if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
1297
+ if (!messageId) missingFields.push("ce-vqsmessageid");
1298
+ if (missingFields.length > 0) {
1299
+ throw new Error(
1300
+ `Missing required CloudEvent headers: ${missingFields.join(", ")}`
1301
+ );
1302
+ }
1303
+ const base = {
1304
+ queueName,
1305
+ consumerGroup,
1306
+ messageId
1307
+ };
1308
+ const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
1309
+ if (!receiptHandle) {
1310
+ return base;
1311
+ }
1312
+ const result = { ...base, receiptHandle };
1313
+ const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
1314
+ if (deliveryCount) {
1315
+ result.deliveryCount = parseInt(deliveryCount, 10);
1316
+ }
1317
+ const createdAt = getHeader(headers, "ce-vqscreatedat");
1318
+ if (createdAt) {
1319
+ result.createdAt = createdAt;
1320
+ }
1321
+ const contentType = getHeader(headers, "content-type");
1322
+ if (contentType) {
1323
+ result.contentType = contentType;
1391
1324
  }
1392
- return JSON.stringify(req.body);
1325
+ const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
1326
+ if (visibilityDeadline) {
1327
+ result.visibilityDeadline = visibilityDeadline;
1328
+ }
1329
+ return result;
1393
1330
  }
1394
- async function createRequestFromNextApi(req) {
1395
- const protocol = getHeader(req.headers, "x-forwarded-proto") ?? "https";
1396
- const host = getHeader(req.headers, "host");
1397
- if (!host) {
1398
- throw new Error("Missing host header");
1399
- }
1400
- const url = `${protocol}://${host}${req.url}`;
1401
- const headers = new Headers();
1402
- for (const [key, value] of Object.entries(req.headers)) {
1403
- if (value) {
1404
- if (Array.isArray(value)) {
1405
- value.forEach((v) => headers.append(key, v));
1406
- } else {
1407
- headers.set(key, value);
1408
- }
1331
+ function parseRawCallback(body, headers) {
1332
+ const ceType = getHeader(headers, "ce-type");
1333
+ if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
1334
+ const result = parseBinaryHeaders(headers);
1335
+ if ("receiptHandle" in result) {
1336
+ result.parsedPayload = body;
1409
1337
  }
1338
+ return result;
1410
1339
  }
1411
- const body = await getBody(req);
1412
- return new Request(url, {
1413
- method: req.method || "POST",
1414
- headers,
1415
- body
1416
- });
1340
+ return parseV1StructuredBody(body, getHeader(headers, "content-type"));
1417
1341
  }
1418
- async function sendResponseToNextApi(response, res) {
1419
- res.status(response.status);
1420
- response.headers.forEach((value, key) => {
1421
- res.setHeader(key, value);
1422
- });
1423
- const contentType = response.headers.get("content-type");
1424
- if (contentType?.includes("application/json")) {
1425
- const data = await response.json();
1426
- res.json(data);
1342
+ async function handleCallback(handler, request, options) {
1343
+ const { queueName, consumerGroup, messageId } = request;
1344
+ const client = options?.client || new QueueClient();
1345
+ const topic = new Topic(client, queueName);
1346
+ const cg = topic.consumerGroup(
1347
+ consumerGroup,
1348
+ options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
1349
+ );
1350
+ if ("receiptHandle" in request) {
1351
+ const transport = client.getTransport();
1352
+ let payload;
1353
+ if (request.rawBody) {
1354
+ payload = await transport.deserialize(request.rawBody);
1355
+ } else if (request.parsedPayload !== void 0) {
1356
+ payload = request.parsedPayload;
1357
+ } else {
1358
+ throw new Error(
1359
+ "Binary mode callback with receipt handle is missing payload"
1360
+ );
1361
+ }
1362
+ const message = {
1363
+ messageId,
1364
+ payload,
1365
+ deliveryCount: request.deliveryCount ?? 1,
1366
+ createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
1367
+ contentType: request.contentType ?? transport.contentType,
1368
+ receiptHandle: request.receiptHandle
1369
+ };
1370
+ const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
1371
+ await cg.consumeMessage(handler, message, { visibilityDeadline });
1427
1372
  } else {
1428
- const text = await response.text();
1429
- res.send(text);
1373
+ await cg.consume(handler, { messageId });
1430
1374
  }
1431
1375
  }
1432
- function handleCallback2(handlers) {
1433
- const webHandler = handleCallback(handlers);
1376
+
1377
+ // src/nextjs-pages.ts
1378
+ function handleCallback2(handler, options) {
1434
1379
  return async (req, res) => {
1435
1380
  if (req.method !== "POST") {
1436
1381
  res.status(200).end();
1437
1382
  return;
1438
1383
  }
1439
1384
  try {
1440
- const request = await createRequestFromNextApi(req);
1441
- const response = await webHandler(request);
1442
- await sendResponseToNextApi(response, res);
1385
+ const parsed = parseRawCallback(req.body, req.headers);
1386
+ await handleCallback(handler, parsed, options);
1387
+ res.status(200).json({ status: "success" });
1443
1388
  } catch (error) {
1444
- console.error("Pages Router adapter error:", error);
1445
- res.status(500).json({ error: "Internal server error" });
1389
+ console.error("Queue callback error:", error);
1390
+ 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"))) {
1391
+ res.status(400).json({ error: error.message });
1392
+ return;
1393
+ }
1394
+ res.status(500).json({ error: "Failed to process queue message" });
1446
1395
  }
1447
1396
  };
1448
1397
  }