@vercel/queue 0.0.0-alpha.37 → 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);
@@ -136,12 +88,15 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
136
88
  // src/dev.ts
137
89
  var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
138
90
  function filePathToUrlPath(filePath) {
139
- 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)$/, "");
140
92
  if (!urlPath.startsWith("/")) {
141
93
  urlPath = "/" + urlPath;
142
94
  }
143
95
  return urlPath;
144
96
  }
97
+ function filePathToConsumerGroup(filePath) {
98
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
99
+ }
145
100
  function getDevRouteMappings() {
146
101
  const g = globalThis;
147
102
  if (ROUTE_MAPPINGS_KEY in g) {
@@ -162,11 +117,11 @@ function getDevRouteMappings() {
162
117
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
163
118
  if (!config.experimentalTriggers) continue;
164
119
  for (const trigger of config.experimentalTriggers) {
165
- if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
120
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
166
121
  mappings.push({
167
122
  urlPath: filePathToUrlPath(filePath),
168
123
  topic: trigger.topic,
169
- consumer: trigger.consumer
124
+ consumer: filePathToConsumerGroup(filePath)
170
125
  });
171
126
  }
172
127
  }
@@ -199,20 +154,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
199
154
  var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
200
155
  async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
201
156
  const client = new QueueClient();
202
- const transport = new JsonTransport();
203
157
  let elapsed = 0;
204
158
  let interval = DEV_VISIBILITY_POLL_INTERVAL;
205
159
  while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
206
160
  try {
207
- await client.receiveMessageById(
208
- {
209
- queueName: topicName,
210
- consumerGroup,
211
- messageId,
212
- visibilityTimeoutSeconds: 0
213
- },
214
- transport
215
- );
161
+ await client.receiveMessageById({
162
+ queueName: topicName,
163
+ consumerGroup,
164
+ messageId,
165
+ visibilityTimeoutSeconds: 0
166
+ });
216
167
  return true;
217
168
  } catch (error) {
218
169
  if (error instanceof MessageNotFoundError) {
@@ -286,26 +237,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
286
237
  console.log(
287
238
  `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
288
239
  );
289
- const cloudEvent = {
290
- type: "com.vercel.queue.v1beta",
291
- source: `/topic/${topicName}/consumer/${route.consumer}`,
292
- id: messageId,
293
- datacontenttype: "application/json",
294
- data: {
295
- messageId,
296
- queueName: topicName,
297
- consumerGroup: route.consumer
298
- },
299
- time: (/* @__PURE__ */ new Date()).toISOString(),
300
- specversion: "1.0"
301
- };
302
240
  try {
303
241
  const response = await fetch(url, {
304
242
  method: "POST",
305
243
  headers: {
306
- "Content-Type": "application/cloudevents+json"
307
- },
308
- 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
+ }
309
249
  });
310
250
  if (response.ok) {
311
251
  try {
@@ -352,6 +292,46 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
352
292
  // src/oidc.ts
353
293
  import { getVercelOidcToken } from "@vercel/oidc";
354
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
+
355
335
  // src/client.ts
356
336
  function isDebugEnabled() {
357
337
  return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
@@ -412,6 +392,7 @@ var QueueClient = class {
412
392
  providedToken;
413
393
  defaultDeploymentId;
414
394
  pinToDeployment;
395
+ transport;
415
396
  constructor(options = {}) {
416
397
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
417
398
  this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
@@ -419,6 +400,10 @@ var QueueClient = class {
419
400
  this.providedToken = options.token;
420
401
  this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
421
402
  this.pinToDeployment = options.pinToDeployment ?? true;
403
+ this.transport = options.transport || new JsonTransport();
404
+ }
405
+ getTransport() {
406
+ return this.transport;
422
407
  }
423
408
  getSendDeploymentId() {
424
409
  if (isDevMode()) {
@@ -475,6 +460,8 @@ var QueueClient = class {
475
460
  }
476
461
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
477
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());
478
465
  const response = await fetch(url, init);
479
466
  if (isDebugEnabled()) {
480
467
  const logData = {
@@ -497,7 +484,6 @@ var QueueClient = class {
497
484
  * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
498
485
  * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
499
486
  * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
500
- * @param transport - Serializer for the payload
501
487
  * @returns Promise with the generated messageId
502
488
  * @throws {DuplicateMessageError} When idempotency key was already used
503
489
  * @throws {ConsumerDiscoveryError} When consumer discovery fails
@@ -507,7 +493,8 @@ var QueueClient = class {
507
493
  * @throws {ForbiddenError} When access is denied
508
494
  * @throws {InternalServerError} When server encounters an error
509
495
  */
510
- async sendMessage(options, transport) {
496
+ async sendMessage(options) {
497
+ const transport = this.transport;
511
498
  const {
512
499
  queueName,
513
500
  payload,
@@ -589,21 +576,24 @@ var QueueClient = class {
589
576
  /**
590
577
  * Receive messages from a topic as an async generator.
591
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
+ *
592
582
  * @param options - Receive options
593
583
  * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
594
584
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
595
585
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
596
586
  * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
597
- * @param transport - Deserializer for message payloads
598
587
  * @yields Message objects with payload, messageId, receiptHandle, etc.
599
- * @throws {QueueEmptyError} When no messages available
588
+ * Yields nothing if queue is empty.
600
589
  * @throws {InvalidLimitError} When limit is outside 1-10 range
601
590
  * @throws {BadRequestError} When parameters are invalid
602
591
  * @throws {UnauthorizedError} When authentication fails
603
592
  * @throws {ForbiddenError} When access is denied
604
593
  * @throws {InternalServerError} When server encounters an error
605
594
  */
606
- async *receiveMessages(options, transport) {
595
+ async *receiveMessages(options) {
596
+ const transport = this.transport;
607
597
  const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
608
598
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
609
599
  throw new InvalidLimitError(limit);
@@ -634,7 +624,7 @@ var QueueClient = class {
634
624
  }
635
625
  );
636
626
  if (response.status === 204) {
637
- throw new QueueEmptyError(queueName, consumerGroup);
627
+ return;
638
628
  }
639
629
  if (!response.ok) {
640
630
  const errorText = await response.text();
@@ -675,7 +665,6 @@ var QueueClient = class {
675
665
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
676
666
  * @param options.messageId - Message ID to retrieve
677
667
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
678
- * @param transport - Deserializer for the message payload
679
668
  * @returns Promise with the message
680
669
  * @throws {MessageNotFoundError} When message doesn't exist
681
670
  * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
@@ -685,7 +674,8 @@ var QueueClient = class {
685
674
  * @throws {ForbiddenError} When access is denied
686
675
  * @throws {InternalServerError} When server encounters an error
687
676
  */
688
- async receiveMessageById(options, transport) {
677
+ async receiveMessageById(options) {
678
+ const transport = this.transport;
689
679
  const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
690
680
  const headers = new Headers({
691
681
  Authorization: `Bearer ${await this.getToken()}`,
@@ -948,40 +938,90 @@ var QueueClient = class {
948
938
  };
949
939
 
950
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
+ }
951
952
  var ConsumerGroup = class {
952
953
  client;
953
954
  topicName;
954
955
  consumerGroupName;
955
956
  visibilityTimeout;
956
- refreshInterval;
957
- transport;
958
957
  /**
959
958
  * Create a new ConsumerGroup instance.
960
959
  *
961
- * @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)
962
961
  * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
963
962
  * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
964
963
  * @param options - Optional configuration
965
- * @param options.transport - Payload serializer (default: JsonTransport)
966
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
967
- * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
964
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
968
965
  */
969
966
  constructor(client, topicName, consumerGroupName, options = {}) {
970
967
  this.client = client;
971
968
  this.topicName = topicName;
972
969
  this.consumerGroupName = consumerGroupName;
973
- this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
974
- this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
975
- 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;
976
988
  }
977
989
  /**
978
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.
979
1008
  */
980
- startVisibilityExtension(receiptHandle) {
1009
+ startVisibilityExtension(receiptHandle, options) {
981
1010
  let isRunning = true;
982
1011
  let isResolved = false;
983
1012
  let resolveLifecycle;
984
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
+ }
985
1025
  const lifecyclePromise = new Promise((resolve) => {
986
1026
  resolveLifecycle = resolve;
987
1027
  });
@@ -1004,19 +1044,31 @@ var ConsumerGroup = class {
1004
1044
  visibilityTimeoutSeconds: this.visibilityTimeout
1005
1045
  });
1006
1046
  if (isRunning) {
1007
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1047
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
1008
1048
  } else {
1009
1049
  safeResolve();
1010
1050
  }
1011
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
+ }
1012
1060
  console.error(
1013
- `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):`,
1014
1062
  error
1015
1063
  );
1016
- safeResolve();
1064
+ if (isRunning) {
1065
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
1066
+ } else {
1067
+ safeResolve();
1068
+ }
1017
1069
  }
1018
1070
  };
1019
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1071
+ timeoutId = setTimeout(() => extend(), firstDelayMs);
1020
1072
  return async (waitForCompletion = false) => {
1021
1073
  isRunning = false;
1022
1074
  if (timeoutId) {
@@ -1030,8 +1082,11 @@ var ConsumerGroup = class {
1030
1082
  }
1031
1083
  };
1032
1084
  }
1033
- async processMessage(message, handler) {
1034
- const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1085
+ async processMessage(message, handler, options) {
1086
+ const stopExtension = this.startVisibilityExtension(
1087
+ message.receiptHandle,
1088
+ options
1089
+ );
1035
1090
  try {
1036
1091
  await handler(message.payload, {
1037
1092
  messageId: message.messageId,
@@ -1048,9 +1103,10 @@ var ConsumerGroup = class {
1048
1103
  });
1049
1104
  } catch (error) {
1050
1105
  await stopExtension();
1051
- 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) {
1052
1108
  try {
1053
- await this.transport.finalize(message.payload);
1109
+ await transport.finalize(message.payload);
1054
1110
  } catch (finalizeError) {
1055
1111
  console.warn("Failed to finalize message payload:", finalizeError);
1056
1112
  }
@@ -1058,35 +1114,49 @@ var ConsumerGroup = class {
1058
1114
  throw error;
1059
1115
  }
1060
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
+ }
1061
1137
  async consume(handler, options) {
1062
- if (options?.messageId) {
1063
- const response = await this.client.receiveMessageById(
1064
- {
1065
- queueName: this.topicName,
1066
- consumerGroup: this.consumerGroupName,
1067
- messageId: options.messageId,
1068
- visibilityTimeoutSeconds: this.visibilityTimeout
1069
- },
1070
- this.transport
1071
- );
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
+ });
1072
1145
  await this.processMessage(response.message, handler);
1073
1146
  } else {
1147
+ const limit = options && "limit" in options ? options.limit : 1;
1074
1148
  let messageFound = false;
1075
- for await (const message of this.client.receiveMessages(
1076
- {
1077
- queueName: this.topicName,
1078
- consumerGroup: this.consumerGroupName,
1079
- visibilityTimeoutSeconds: this.visibilityTimeout,
1080
- limit: 1
1081
- },
1082
- this.transport
1083
- )) {
1149
+ for await (const message of this.client.receiveMessages({
1150
+ queueName: this.topicName,
1151
+ consumerGroup: this.consumerGroupName,
1152
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1153
+ limit
1154
+ })) {
1084
1155
  messageFound = true;
1085
1156
  await this.processMessage(message, handler);
1086
- break;
1087
1157
  }
1088
1158
  if (!messageFound) {
1089
- throw new Error("No messages available");
1159
+ await handler(null, null);
1090
1160
  }
1091
1161
  }
1092
1162
  }
@@ -1108,17 +1178,14 @@ var ConsumerGroup = class {
1108
1178
  var Topic = class {
1109
1179
  client;
1110
1180
  topicName;
1111
- transport;
1112
1181
  /**
1113
1182
  * Create a new Topic instance
1114
- * @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)
1115
1184
  * @param topicName Name of the topic to work with
1116
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1117
1185
  */
1118
- constructor(client, topicName, transport) {
1186
+ constructor(client, topicName) {
1119
1187
  this.client = client;
1120
1188
  this.topicName = topicName;
1121
- this.transport = transport || new JsonTransport();
1122
1189
  }
1123
1190
  /**
1124
1191
  * Publish a message to the topic
@@ -1131,17 +1198,14 @@ var Topic = class {
1131
1198
  * @throws {InternalServerError} When server encounters an error
1132
1199
  */
1133
1200
  async publish(payload, options) {
1134
- const result = await this.client.sendMessage(
1135
- {
1136
- queueName: this.topicName,
1137
- payload,
1138
- idempotencyKey: options?.idempotencyKey,
1139
- retentionSeconds: options?.retentionSeconds,
1140
- delaySeconds: options?.delaySeconds,
1141
- headers: options?.headers
1142
- },
1143
- this.transport
1144
- );
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
+ });
1145
1209
  if (isDevMode()) {
1146
1210
  triggerDevCallbacks(this.topicName, result.messageId);
1147
1211
  }
@@ -1154,15 +1218,11 @@ var Topic = class {
1154
1218
  * @returns A ConsumerGroup instance
1155
1219
  */
1156
1220
  consumerGroup(consumerGroupName, options) {
1157
- const consumerOptions = {
1158
- ...options,
1159
- transport: options?.transport || this.transport
1160
- };
1161
1221
  return new ConsumerGroup(
1162
1222
  this.client,
1163
1223
  this.topicName,
1164
1224
  consumerGroupName,
1165
- consumerOptions
1225
+ options
1166
1226
  );
1167
1227
  }
1168
1228
  /**
@@ -1171,220 +1231,167 @@ var Topic = class {
1171
1231
  get name() {
1172
1232
  return this.topicName;
1173
1233
  }
1174
- /**
1175
- * Get the transport used by this topic
1176
- */
1177
- get serializer() {
1178
- return this.transport;
1179
- }
1180
1234
  };
1181
1235
 
1182
1236
  // src/callback.ts
1183
- function validateWildcardPattern(pattern) {
1184
- const firstIndex = pattern.indexOf("*");
1185
- const lastIndex = pattern.lastIndexOf("*");
1186
- if (firstIndex !== lastIndex) {
1187
- return false;
1188
- }
1189
- if (firstIndex === -1) {
1190
- return false;
1191
- }
1192
- if (firstIndex !== pattern.length - 1) {
1193
- return false;
1194
- }
1195
- return true;
1196
- }
1237
+ var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
1238
+ var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
1197
1239
  function matchesWildcardPattern(topicName, pattern) {
1198
1240
  const prefix = pattern.slice(0, -1);
1199
1241
  return topicName.startsWith(prefix);
1200
1242
  }
1201
- function findTopicHandler(queueName, handlers) {
1202
- const exactHandler = handlers[queueName];
1203
- if (exactHandler) {
1204
- return exactHandler;
1205
- }
1206
- for (const pattern in handlers) {
1207
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1208
- return handlers[pattern];
1209
- }
1210
- }
1211
- return null;
1243
+ function isRecord(value) {
1244
+ return typeof value === "object" && value !== null;
1212
1245
  }
1213
- async function parseCallback(request) {
1214
- const contentType = request.headers.get("content-type");
1246
+ function parseV1StructuredBody(body, contentType) {
1215
1247
  if (!contentType || !contentType.includes("application/cloudevents+json")) {
1216
1248
  throw new Error(
1217
1249
  "Invalid content type: expected 'application/cloudevents+json'"
1218
1250
  );
1219
1251
  }
1220
- let cloudEvent;
1221
- try {
1222
- cloudEvent = await request.json();
1223
- } catch (error) {
1224
- throw new Error("Failed to parse CloudEvent from request body");
1225
- }
1226
- 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)) {
1227
1253
  throw new Error("Invalid CloudEvent: missing required fields");
1228
1254
  }
1229
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1255
+ if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
1230
1256
  throw new Error(
1231
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1257
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
1232
1258
  );
1233
1259
  }
1260
+ const { data } = body;
1234
1261
  const missingFields = [];
1235
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1236
- if (!("consumerGroup" in cloudEvent.data))
1237
- missingFields.push("consumerGroup");
1238
- 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");
1239
1265
  if (missingFields.length > 0) {
1240
1266
  throw new Error(
1241
1267
  `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1242
1268
  );
1243
1269
  }
1244
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
1245
1270
  return {
1246
- queueName,
1247
- consumerGroup,
1248
- messageId
1271
+ queueName: String(data.queueName),
1272
+ consumerGroup: String(data.consumerGroup),
1273
+ messageId: String(data.messageId)
1249
1274
  };
1250
1275
  }
1251
- function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1252
- for (const topicPattern in handlers) {
1253
- if (topicPattern.includes("*")) {
1254
- if (!validateWildcardPattern(topicPattern)) {
1255
- throw new Error(
1256
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1257
- );
1258
- }
1259
- }
1260
- }
1261
- const routeHandler = async (request) => {
1262
- try {
1263
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
1264
- const topicHandler = findTopicHandler(queueName, handlers);
1265
- if (!topicHandler) {
1266
- const availableTopics = Object.keys(handlers).join(", ");
1267
- return Response.json(
1268
- {
1269
- error: `No handler found for topic: ${queueName}`,
1270
- availableTopics
1271
- },
1272
- { status: 404 }
1273
- );
1274
- }
1275
- const consumerGroupHandler = topicHandler[consumerGroup];
1276
- if (!consumerGroupHandler) {
1277
- const availableGroups = Object.keys(topicHandler).join(", ");
1278
- return Response.json(
1279
- {
1280
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1281
- availableGroups
1282
- },
1283
- { status: 404 }
1284
- );
1285
- }
1286
- const topic = new Topic(client, queueName);
1287
- const cg = topic.consumerGroup(
1288
- consumerGroup,
1289
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1290
- );
1291
- await cg.consume(consumerGroupHandler, { messageId });
1292
- return Response.json({ status: "success" });
1293
- } catch (error) {
1294
- console.error("Queue callback error:", error);
1295
- 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"))) {
1296
- return Response.json({ error: error.message }, { status: 400 });
1297
- }
1298
- return Response.json(
1299
- { error: "Failed to process queue message" },
1300
- { status: 500 }
1301
- );
1302
- }
1303
- };
1304
- return routeHandler;
1305
- }
1306
- function handleCallback(handlers, options) {
1307
- return createCallbackHandler(
1308
- handlers,
1309
- options?.client || new QueueClient(),
1310
- options?.visibilityTimeoutSeconds
1311
- );
1312
- }
1313
-
1314
- // src/nextjs-pages.ts
1315
1276
  function getHeader(headers, name) {
1277
+ if (headers instanceof Headers) {
1278
+ return headers.get(name);
1279
+ }
1316
1280
  const value = headers[name];
1317
- return Array.isArray(value) ? value[0] : value;
1281
+ if (Array.isArray(value)) return value[0] ?? null;
1282
+ return value ?? null;
1318
1283
  }
1319
- function readBody(req) {
1320
- return new Promise((resolve, reject) => {
1321
- const chunks = [];
1322
- req.on("data", (chunk) => chunks.push(chunk));
1323
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1324
- req.on("error", reject);
1325
- });
1326
- }
1327
- async function getBody(req) {
1328
- if (req.body === void 0) {
1329
- 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
+ );
1290
+ }
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;
1330
1311
  }
1331
- if (typeof req.body === "string") {
1332
- return req.body;
1312
+ const result = { ...base, receiptHandle };
1313
+ const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
1314
+ if (deliveryCount) {
1315
+ result.deliveryCount = parseInt(deliveryCount, 10);
1333
1316
  }
1334
- return JSON.stringify(req.body);
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;
1324
+ }
1325
+ const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
1326
+ if (visibilityDeadline) {
1327
+ result.visibilityDeadline = visibilityDeadline;
1328
+ }
1329
+ return result;
1335
1330
  }
1336
- async function createRequestFromNextApi(req) {
1337
- const protocol = getHeader(req.headers, "x-forwarded-proto") ?? "https";
1338
- const host = getHeader(req.headers, "host");
1339
- if (!host) {
1340
- throw new Error("Missing host header");
1341
- }
1342
- const url = `${protocol}://${host}${req.url}`;
1343
- const headers = new Headers();
1344
- for (const [key, value] of Object.entries(req.headers)) {
1345
- if (value) {
1346
- if (Array.isArray(value)) {
1347
- value.forEach((v) => headers.append(key, v));
1348
- } else {
1349
- headers.set(key, value);
1350
- }
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;
1351
1337
  }
1338
+ return result;
1352
1339
  }
1353
- const body = await getBody(req);
1354
- return new Request(url, {
1355
- method: req.method || "POST",
1356
- headers,
1357
- body
1358
- });
1340
+ return parseV1StructuredBody(body, getHeader(headers, "content-type"));
1359
1341
  }
1360
- async function sendResponseToNextApi(response, res) {
1361
- res.status(response.status);
1362
- response.headers.forEach((value, key) => {
1363
- res.setHeader(key, value);
1364
- });
1365
- const contentType = response.headers.get("content-type");
1366
- if (contentType?.includes("application/json")) {
1367
- const data = await response.json();
1368
- 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 });
1369
1372
  } else {
1370
- const text = await response.text();
1371
- res.send(text);
1373
+ await cg.consume(handler, { messageId });
1372
1374
  }
1373
1375
  }
1374
- function handleCallback2(handlers) {
1375
- const webHandler = handleCallback(handlers);
1376
+
1377
+ // src/nextjs-pages.ts
1378
+ function handleCallback2(handler, options) {
1376
1379
  return async (req, res) => {
1377
1380
  if (req.method !== "POST") {
1378
1381
  res.status(200).end();
1379
1382
  return;
1380
1383
  }
1381
1384
  try {
1382
- const request = await createRequestFromNextApi(req);
1383
- const response = await webHandler(request);
1384
- 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" });
1385
1388
  } catch (error) {
1386
- console.error("Pages Router adapter error:", error);
1387
- 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" });
1388
1395
  }
1389
1396
  };
1390
1397
  }