@vercel/queue 0.0.0-alpha.37 → 0.0.0-alpha.39

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);
@@ -172,12 +124,15 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
172
124
  // src/dev.ts
173
125
  var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
174
126
  function filePathToUrlPath(filePath) {
175
- 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)$/, "");
176
128
  if (!urlPath.startsWith("/")) {
177
129
  urlPath = "/" + urlPath;
178
130
  }
179
131
  return urlPath;
180
132
  }
133
+ function filePathToConsumerGroup(filePath) {
134
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
135
+ }
181
136
  function getDevRouteMappings() {
182
137
  const g = globalThis;
183
138
  if (ROUTE_MAPPINGS_KEY in g) {
@@ -198,11 +153,11 @@ function getDevRouteMappings() {
198
153
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
199
154
  if (!config.experimentalTriggers) continue;
200
155
  for (const trigger of config.experimentalTriggers) {
201
- if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
156
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
202
157
  mappings.push({
203
158
  urlPath: filePathToUrlPath(filePath),
204
159
  topic: trigger.topic,
205
- consumer: trigger.consumer
160
+ consumer: filePathToConsumerGroup(filePath)
206
161
  });
207
162
  }
208
163
  }
@@ -235,20 +190,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
235
190
  var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
236
191
  async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
237
192
  const client = new QueueClient();
238
- const transport = new JsonTransport();
239
193
  let elapsed = 0;
240
194
  let interval = DEV_VISIBILITY_POLL_INTERVAL;
241
195
  while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
242
196
  try {
243
- await client.receiveMessageById(
244
- {
245
- queueName: topicName,
246
- consumerGroup,
247
- messageId,
248
- visibilityTimeoutSeconds: 0
249
- },
250
- transport
251
- );
197
+ await client.receiveMessageById({
198
+ queueName: topicName,
199
+ consumerGroup,
200
+ messageId,
201
+ visibilityTimeoutSeconds: 0
202
+ });
252
203
  return true;
253
204
  } catch (error) {
254
205
  if (error instanceof MessageNotFoundError) {
@@ -322,26 +273,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
322
273
  console.log(
323
274
  `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
324
275
  );
325
- const cloudEvent = {
326
- type: "com.vercel.queue.v1beta",
327
- source: `/topic/${topicName}/consumer/${route.consumer}`,
328
- id: messageId,
329
- datacontenttype: "application/json",
330
- data: {
331
- messageId,
332
- queueName: topicName,
333
- consumerGroup: route.consumer
334
- },
335
- time: (/* @__PURE__ */ new Date()).toISOString(),
336
- specversion: "1.0"
337
- };
338
276
  try {
339
277
  const response = await fetch(url, {
340
278
  method: "POST",
341
279
  headers: {
342
- "Content-Type": "application/cloudevents+json"
343
- },
344
- 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
+ }
345
285
  });
346
286
  if (response.ok) {
347
287
  try {
@@ -388,6 +328,46 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
388
328
  // src/oidc.ts
389
329
  var import_oidc = require("@vercel/oidc");
390
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
+
391
371
  // src/client.ts
392
372
  function isDebugEnabled() {
393
373
  return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
@@ -448,6 +428,7 @@ var QueueClient = class {
448
428
  providedToken;
449
429
  defaultDeploymentId;
450
430
  pinToDeployment;
431
+ transport;
451
432
  constructor(options = {}) {
452
433
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
453
434
  this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
@@ -455,6 +436,10 @@ var QueueClient = class {
455
436
  this.providedToken = options.token;
456
437
  this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
457
438
  this.pinToDeployment = options.pinToDeployment ?? true;
439
+ this.transport = options.transport || new JsonTransport();
440
+ }
441
+ getTransport() {
442
+ return this.transport;
458
443
  }
459
444
  getSendDeploymentId() {
460
445
  if (isDevMode()) {
@@ -511,6 +496,8 @@ var QueueClient = class {
511
496
  }
512
497
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
513
498
  }
499
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.39"}`);
500
+ init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
514
501
  const response = await fetch(url, init);
515
502
  if (isDebugEnabled()) {
516
503
  const logData = {
@@ -533,7 +520,6 @@ var QueueClient = class {
533
520
  * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
534
521
  * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
535
522
  * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
536
- * @param transport - Serializer for the payload
537
523
  * @returns Promise with the generated messageId
538
524
  * @throws {DuplicateMessageError} When idempotency key was already used
539
525
  * @throws {ConsumerDiscoveryError} When consumer discovery fails
@@ -543,7 +529,8 @@ var QueueClient = class {
543
529
  * @throws {ForbiddenError} When access is denied
544
530
  * @throws {InternalServerError} When server encounters an error
545
531
  */
546
- async sendMessage(options, transport) {
532
+ async sendMessage(options) {
533
+ const transport = this.transport;
547
534
  const {
548
535
  queueName,
549
536
  payload,
@@ -625,21 +612,24 @@ var QueueClient = class {
625
612
  /**
626
613
  * Receive messages from a topic as an async generator.
627
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
+ *
628
618
  * @param options - Receive options
629
619
  * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
630
620
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
631
621
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
632
622
  * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
633
- * @param transport - Deserializer for message payloads
634
623
  * @yields Message objects with payload, messageId, receiptHandle, etc.
635
- * @throws {QueueEmptyError} When no messages available
624
+ * Yields nothing if queue is empty.
636
625
  * @throws {InvalidLimitError} When limit is outside 1-10 range
637
626
  * @throws {BadRequestError} When parameters are invalid
638
627
  * @throws {UnauthorizedError} When authentication fails
639
628
  * @throws {ForbiddenError} When access is denied
640
629
  * @throws {InternalServerError} When server encounters an error
641
630
  */
642
- async *receiveMessages(options, transport) {
631
+ async *receiveMessages(options) {
632
+ const transport = this.transport;
643
633
  const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
644
634
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
645
635
  throw new InvalidLimitError(limit);
@@ -670,7 +660,7 @@ var QueueClient = class {
670
660
  }
671
661
  );
672
662
  if (response.status === 204) {
673
- throw new QueueEmptyError(queueName, consumerGroup);
663
+ return;
674
664
  }
675
665
  if (!response.ok) {
676
666
  const errorText = await response.text();
@@ -711,7 +701,6 @@ var QueueClient = class {
711
701
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
712
702
  * @param options.messageId - Message ID to retrieve
713
703
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
714
- * @param transport - Deserializer for the message payload
715
704
  * @returns Promise with the message
716
705
  * @throws {MessageNotFoundError} When message doesn't exist
717
706
  * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
@@ -721,7 +710,8 @@ var QueueClient = class {
721
710
  * @throws {ForbiddenError} When access is denied
722
711
  * @throws {InternalServerError} When server encounters an error
723
712
  */
724
- async receiveMessageById(options, transport) {
713
+ async receiveMessageById(options) {
714
+ const transport = this.transport;
725
715
  const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
726
716
  const headers = new Headers({
727
717
  Authorization: `Bearer ${await this.getToken()}`,
@@ -984,40 +974,90 @@ var QueueClient = class {
984
974
  };
985
975
 
986
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
+ }
987
988
  var ConsumerGroup = class {
988
989
  client;
989
990
  topicName;
990
991
  consumerGroupName;
991
992
  visibilityTimeout;
992
- refreshInterval;
993
- transport;
994
993
  /**
995
994
  * Create a new ConsumerGroup instance.
996
995
  *
997
- * @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)
998
997
  * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
999
998
  * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1000
999
  * @param options - Optional configuration
1001
- * @param options.transport - Payload serializer (default: JsonTransport)
1002
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1003
- * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
1000
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1004
1001
  */
1005
1002
  constructor(client, topicName, consumerGroupName, options = {}) {
1006
1003
  this.client = client;
1007
1004
  this.topicName = topicName;
1008
1005
  this.consumerGroupName = consumerGroupName;
1009
- this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1010
- this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
1011
- 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;
1012
1024
  }
1013
1025
  /**
1014
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.
1015
1044
  */
1016
- startVisibilityExtension(receiptHandle) {
1045
+ startVisibilityExtension(receiptHandle, options) {
1017
1046
  let isRunning = true;
1018
1047
  let isResolved = false;
1019
1048
  let resolveLifecycle;
1020
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
+ }
1021
1061
  const lifecyclePromise = new Promise((resolve) => {
1022
1062
  resolveLifecycle = resolve;
1023
1063
  });
@@ -1040,19 +1080,31 @@ var ConsumerGroup = class {
1040
1080
  visibilityTimeoutSeconds: this.visibilityTimeout
1041
1081
  });
1042
1082
  if (isRunning) {
1043
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1083
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
1044
1084
  } else {
1045
1085
  safeResolve();
1046
1086
  }
1047
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
+ }
1048
1096
  console.error(
1049
- `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):`,
1050
1098
  error
1051
1099
  );
1052
- safeResolve();
1100
+ if (isRunning) {
1101
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
1102
+ } else {
1103
+ safeResolve();
1104
+ }
1053
1105
  }
1054
1106
  };
1055
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1107
+ timeoutId = setTimeout(() => extend(), firstDelayMs);
1056
1108
  return async (waitForCompletion = false) => {
1057
1109
  isRunning = false;
1058
1110
  if (timeoutId) {
@@ -1066,8 +1118,11 @@ var ConsumerGroup = class {
1066
1118
  }
1067
1119
  };
1068
1120
  }
1069
- async processMessage(message, handler) {
1070
- const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1121
+ async processMessage(message, handler, options) {
1122
+ const stopExtension = this.startVisibilityExtension(
1123
+ message.receiptHandle,
1124
+ options
1125
+ );
1071
1126
  try {
1072
1127
  await handler(message.payload, {
1073
1128
  messageId: message.messageId,
@@ -1084,9 +1139,10 @@ var ConsumerGroup = class {
1084
1139
  });
1085
1140
  } catch (error) {
1086
1141
  await stopExtension();
1087
- 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) {
1088
1144
  try {
1089
- await this.transport.finalize(message.payload);
1145
+ await transport.finalize(message.payload);
1090
1146
  } catch (finalizeError) {
1091
1147
  console.warn("Failed to finalize message payload:", finalizeError);
1092
1148
  }
@@ -1094,35 +1150,49 @@ var ConsumerGroup = class {
1094
1150
  throw error;
1095
1151
  }
1096
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
+ }
1097
1173
  async consume(handler, options) {
1098
- if (options?.messageId) {
1099
- const response = await this.client.receiveMessageById(
1100
- {
1101
- queueName: this.topicName,
1102
- consumerGroup: this.consumerGroupName,
1103
- messageId: options.messageId,
1104
- visibilityTimeoutSeconds: this.visibilityTimeout
1105
- },
1106
- this.transport
1107
- );
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
+ });
1108
1181
  await this.processMessage(response.message, handler);
1109
1182
  } else {
1183
+ const limit = options && "limit" in options ? options.limit : 1;
1110
1184
  let messageFound = false;
1111
- for await (const message of this.client.receiveMessages(
1112
- {
1113
- queueName: this.topicName,
1114
- consumerGroup: this.consumerGroupName,
1115
- visibilityTimeoutSeconds: this.visibilityTimeout,
1116
- limit: 1
1117
- },
1118
- this.transport
1119
- )) {
1185
+ for await (const message of this.client.receiveMessages({
1186
+ queueName: this.topicName,
1187
+ consumerGroup: this.consumerGroupName,
1188
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1189
+ limit
1190
+ })) {
1120
1191
  messageFound = true;
1121
1192
  await this.processMessage(message, handler);
1122
- break;
1123
1193
  }
1124
1194
  if (!messageFound) {
1125
- throw new Error("No messages available");
1195
+ await handler(null, null);
1126
1196
  }
1127
1197
  }
1128
1198
  }
@@ -1144,17 +1214,14 @@ var ConsumerGroup = class {
1144
1214
  var Topic = class {
1145
1215
  client;
1146
1216
  topicName;
1147
- transport;
1148
1217
  /**
1149
1218
  * Create a new Topic instance
1150
- * @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)
1151
1220
  * @param topicName Name of the topic to work with
1152
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1153
1221
  */
1154
- constructor(client, topicName, transport) {
1222
+ constructor(client, topicName) {
1155
1223
  this.client = client;
1156
1224
  this.topicName = topicName;
1157
- this.transport = transport || new JsonTransport();
1158
1225
  }
1159
1226
  /**
1160
1227
  * Publish a message to the topic
@@ -1167,17 +1234,14 @@ var Topic = class {
1167
1234
  * @throws {InternalServerError} When server encounters an error
1168
1235
  */
1169
1236
  async publish(payload, options) {
1170
- const result = await this.client.sendMessage(
1171
- {
1172
- queueName: this.topicName,
1173
- payload,
1174
- idempotencyKey: options?.idempotencyKey,
1175
- retentionSeconds: options?.retentionSeconds,
1176
- delaySeconds: options?.delaySeconds,
1177
- headers: options?.headers
1178
- },
1179
- this.transport
1180
- );
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
+ });
1181
1245
  if (isDevMode()) {
1182
1246
  triggerDevCallbacks(this.topicName, result.messageId);
1183
1247
  }
@@ -1190,15 +1254,11 @@ var Topic = class {
1190
1254
  * @returns A ConsumerGroup instance
1191
1255
  */
1192
1256
  consumerGroup(consumerGroupName, options) {
1193
- const consumerOptions = {
1194
- ...options,
1195
- transport: options?.transport || this.transport
1196
- };
1197
1257
  return new ConsumerGroup(
1198
1258
  this.client,
1199
1259
  this.topicName,
1200
1260
  consumerGroupName,
1201
- consumerOptions
1261
+ options
1202
1262
  );
1203
1263
  }
1204
1264
  /**
@@ -1207,220 +1267,167 @@ var Topic = class {
1207
1267
  get name() {
1208
1268
  return this.topicName;
1209
1269
  }
1210
- /**
1211
- * Get the transport used by this topic
1212
- */
1213
- get serializer() {
1214
- return this.transport;
1215
- }
1216
1270
  };
1217
1271
 
1218
1272
  // src/callback.ts
1219
- function validateWildcardPattern(pattern) {
1220
- const firstIndex = pattern.indexOf("*");
1221
- const lastIndex = pattern.lastIndexOf("*");
1222
- if (firstIndex !== lastIndex) {
1223
- return false;
1224
- }
1225
- if (firstIndex === -1) {
1226
- return false;
1227
- }
1228
- if (firstIndex !== pattern.length - 1) {
1229
- return false;
1230
- }
1231
- return true;
1232
- }
1273
+ var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
1274
+ var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
1233
1275
  function matchesWildcardPattern(topicName, pattern) {
1234
1276
  const prefix = pattern.slice(0, -1);
1235
1277
  return topicName.startsWith(prefix);
1236
1278
  }
1237
- function findTopicHandler(queueName, handlers) {
1238
- const exactHandler = handlers[queueName];
1239
- if (exactHandler) {
1240
- return exactHandler;
1241
- }
1242
- for (const pattern in handlers) {
1243
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1244
- return handlers[pattern];
1245
- }
1246
- }
1247
- return null;
1279
+ function isRecord(value) {
1280
+ return typeof value === "object" && value !== null;
1248
1281
  }
1249
- async function parseCallback(request) {
1250
- const contentType = request.headers.get("content-type");
1282
+ function parseV1StructuredBody(body, contentType) {
1251
1283
  if (!contentType || !contentType.includes("application/cloudevents+json")) {
1252
1284
  throw new Error(
1253
1285
  "Invalid content type: expected 'application/cloudevents+json'"
1254
1286
  );
1255
1287
  }
1256
- let cloudEvent;
1257
- try {
1258
- cloudEvent = await request.json();
1259
- } catch (error) {
1260
- throw new Error("Failed to parse CloudEvent from request body");
1261
- }
1262
- 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)) {
1263
1289
  throw new Error("Invalid CloudEvent: missing required fields");
1264
1290
  }
1265
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1291
+ if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
1266
1292
  throw new Error(
1267
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1293
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
1268
1294
  );
1269
1295
  }
1296
+ const { data } = body;
1270
1297
  const missingFields = [];
1271
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1272
- if (!("consumerGroup" in cloudEvent.data))
1273
- missingFields.push("consumerGroup");
1274
- 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");
1275
1301
  if (missingFields.length > 0) {
1276
1302
  throw new Error(
1277
1303
  `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1278
1304
  );
1279
1305
  }
1280
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
1281
1306
  return {
1282
- queueName,
1283
- consumerGroup,
1284
- messageId
1307
+ queueName: String(data.queueName),
1308
+ consumerGroup: String(data.consumerGroup),
1309
+ messageId: String(data.messageId)
1285
1310
  };
1286
1311
  }
1287
- function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1288
- for (const topicPattern in handlers) {
1289
- if (topicPattern.includes("*")) {
1290
- if (!validateWildcardPattern(topicPattern)) {
1291
- throw new Error(
1292
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1293
- );
1294
- }
1295
- }
1296
- }
1297
- const routeHandler = async (request) => {
1298
- try {
1299
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
1300
- const topicHandler = findTopicHandler(queueName, handlers);
1301
- if (!topicHandler) {
1302
- const availableTopics = Object.keys(handlers).join(", ");
1303
- return Response.json(
1304
- {
1305
- error: `No handler found for topic: ${queueName}`,
1306
- availableTopics
1307
- },
1308
- { status: 404 }
1309
- );
1310
- }
1311
- const consumerGroupHandler = topicHandler[consumerGroup];
1312
- if (!consumerGroupHandler) {
1313
- const availableGroups = Object.keys(topicHandler).join(", ");
1314
- return Response.json(
1315
- {
1316
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1317
- availableGroups
1318
- },
1319
- { status: 404 }
1320
- );
1321
- }
1322
- const topic = new Topic(client, queueName);
1323
- const cg = topic.consumerGroup(
1324
- consumerGroup,
1325
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1326
- );
1327
- await cg.consume(consumerGroupHandler, { messageId });
1328
- return Response.json({ status: "success" });
1329
- } catch (error) {
1330
- console.error("Queue callback error:", error);
1331
- 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"))) {
1332
- return Response.json({ error: error.message }, { status: 400 });
1333
- }
1334
- return Response.json(
1335
- { error: "Failed to process queue message" },
1336
- { status: 500 }
1337
- );
1338
- }
1339
- };
1340
- return routeHandler;
1341
- }
1342
- function handleCallback(handlers, options) {
1343
- return createCallbackHandler(
1344
- handlers,
1345
- options?.client || new QueueClient(),
1346
- options?.visibilityTimeoutSeconds
1347
- );
1348
- }
1349
-
1350
- // src/nextjs-pages.ts
1351
1312
  function getHeader(headers, name) {
1313
+ if (headers instanceof Headers) {
1314
+ return headers.get(name);
1315
+ }
1352
1316
  const value = headers[name];
1353
- return Array.isArray(value) ? value[0] : value;
1317
+ if (Array.isArray(value)) return value[0] ?? null;
1318
+ return value ?? null;
1354
1319
  }
1355
- function readBody(req) {
1356
- return new Promise((resolve, reject) => {
1357
- const chunks = [];
1358
- req.on("data", (chunk) => chunks.push(chunk));
1359
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1360
- req.on("error", reject);
1361
- });
1362
- }
1363
- async function getBody(req) {
1364
- if (req.body === void 0) {
1365
- 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
+ );
1326
+ }
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;
1366
1347
  }
1367
- if (typeof req.body === "string") {
1368
- return req.body;
1348
+ const result = { ...base, receiptHandle };
1349
+ const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
1350
+ if (deliveryCount) {
1351
+ result.deliveryCount = parseInt(deliveryCount, 10);
1369
1352
  }
1370
- return JSON.stringify(req.body);
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;
1360
+ }
1361
+ const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
1362
+ if (visibilityDeadline) {
1363
+ result.visibilityDeadline = visibilityDeadline;
1364
+ }
1365
+ return result;
1371
1366
  }
1372
- async function createRequestFromNextApi(req) {
1373
- const protocol = getHeader(req.headers, "x-forwarded-proto") ?? "https";
1374
- const host = getHeader(req.headers, "host");
1375
- if (!host) {
1376
- throw new Error("Missing host header");
1377
- }
1378
- const url = `${protocol}://${host}${req.url}`;
1379
- const headers = new Headers();
1380
- for (const [key, value] of Object.entries(req.headers)) {
1381
- if (value) {
1382
- if (Array.isArray(value)) {
1383
- value.forEach((v) => headers.append(key, v));
1384
- } else {
1385
- headers.set(key, value);
1386
- }
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;
1387
1373
  }
1374
+ return result;
1388
1375
  }
1389
- const body = await getBody(req);
1390
- return new Request(url, {
1391
- method: req.method || "POST",
1392
- headers,
1393
- body
1394
- });
1376
+ return parseV1StructuredBody(body, getHeader(headers, "content-type"));
1395
1377
  }
1396
- async function sendResponseToNextApi(response, res) {
1397
- res.status(response.status);
1398
- response.headers.forEach((value, key) => {
1399
- res.setHeader(key, value);
1400
- });
1401
- const contentType = response.headers.get("content-type");
1402
- if (contentType?.includes("application/json")) {
1403
- const data = await response.json();
1404
- 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 });
1405
1408
  } else {
1406
- const text = await response.text();
1407
- res.send(text);
1409
+ await cg.consume(handler, { messageId });
1408
1410
  }
1409
1411
  }
1410
- function handleCallback2(handlers) {
1411
- const webHandler = handleCallback(handlers);
1412
+
1413
+ // src/nextjs-pages.ts
1414
+ function handleCallback2(handler, options) {
1412
1415
  return async (req, res) => {
1413
1416
  if (req.method !== "POST") {
1414
1417
  res.status(200).end();
1415
1418
  return;
1416
1419
  }
1417
1420
  try {
1418
- const request = await createRequestFromNextApi(req);
1419
- const response = await webHandler(request);
1420
- 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" });
1421
1424
  } catch (error) {
1422
- console.error("Pages Router adapter error:", error);
1423
- 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" });
1424
1431
  }
1425
1432
  };
1426
1433
  }