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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -50,11 +50,8 @@ __export(index_exports, {
50
50
  QueueEmptyError: () => QueueEmptyError,
51
51
  StreamTransport: () => StreamTransport,
52
52
  UnauthorizedError: () => UnauthorizedError,
53
- handleCallback: () => handleCallback,
54
53
  parseCallback: () => parseCallback,
55
- parseRawCallback: () => parseRawCallback,
56
- receive: () => receive,
57
- send: () => send
54
+ parseRawCallback: () => parseRawCallback
58
55
  });
59
56
  module.exports = __toCommonJS(index_exports);
60
57
 
@@ -131,7 +128,7 @@ var StreamTransport = class {
131
128
  }
132
129
  };
133
130
 
134
- // src/client.ts
131
+ // src/api-client.ts
135
132
  var import_mixpart = require("mixpart");
136
133
 
137
134
  // src/dev.ts
@@ -256,7 +253,7 @@ var ConsumerGroup = class {
256
253
  /**
257
254
  * Create a new ConsumerGroup instance.
258
255
  *
259
- * @param client - QueueClient instance to use for API calls (transport is configured on the client)
256
+ * @param client - ApiClient instance to use for API calls (transport is configured on the client)
260
257
  * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
261
258
  * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
262
259
  * @param options - Optional configuration
@@ -381,35 +378,84 @@ var ConsumerGroup = class {
381
378
  }
382
379
  };
383
380
  }
381
+ /**
382
+ * Clean up the message payload if the transport supports it and payload exists.
383
+ */
384
+ async finalizePayload(payload) {
385
+ const transport = this.client.getTransport();
386
+ if (transport.finalize && payload !== void 0 && payload !== null) {
387
+ try {
388
+ await transport.finalize(payload);
389
+ } catch (finalizeError) {
390
+ console.warn("Failed to finalize message payload:", finalizeError);
391
+ }
392
+ }
393
+ }
384
394
  async processMessage(message, handler, options) {
385
395
  const stopExtension = this.startVisibilityExtension(
386
396
  message.receiptHandle,
387
397
  options
388
398
  );
399
+ const metadata = {
400
+ messageId: message.messageId,
401
+ deliveryCount: message.deliveryCount,
402
+ createdAt: message.createdAt,
403
+ expiresAt: message.expiresAt,
404
+ topicName: this.topicName,
405
+ consumerGroup: this.consumerGroupName,
406
+ region: this.client.getRegion()
407
+ };
389
408
  try {
390
- await handler(message.payload, {
391
- messageId: message.messageId,
392
- deliveryCount: message.deliveryCount,
393
- createdAt: message.createdAt,
394
- topicName: this.topicName,
395
- consumerGroup: this.consumerGroupName
396
- });
409
+ await handler(message.payload, metadata);
397
410
  await stopExtension();
398
- await this.client.deleteMessage({
411
+ await this.client.acknowledgeMessage({
399
412
  queueName: this.topicName,
400
413
  consumerGroup: this.consumerGroupName,
401
414
  receiptHandle: message.receiptHandle
402
415
  });
403
416
  } catch (error) {
404
417
  await stopExtension();
405
- const transport = this.client.getTransport();
406
- if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
418
+ if (options?.retry) {
419
+ let directive;
407
420
  try {
408
- await transport.finalize(message.payload);
409
- } catch (finalizeError) {
410
- console.warn("Failed to finalize message payload:", finalizeError);
421
+ directive = options.retry(error, metadata);
422
+ } catch (retryError) {
423
+ console.warn("retry handler threw:", retryError);
424
+ }
425
+ if (directive) {
426
+ if ("acknowledge" in directive && directive.acknowledge) {
427
+ try {
428
+ await this.client.acknowledgeMessage({
429
+ queueName: this.topicName,
430
+ consumerGroup: this.consumerGroupName,
431
+ receiptHandle: message.receiptHandle
432
+ });
433
+ } catch (ackError) {
434
+ console.warn("Failed to acknowledge message:", ackError);
435
+ }
436
+ await this.finalizePayload(message.payload);
437
+ return;
438
+ }
439
+ if ("afterSeconds" in directive && typeof directive.afterSeconds === "number") {
440
+ try {
441
+ await this.client.changeVisibility({
442
+ queueName: this.topicName,
443
+ consumerGroup: this.consumerGroupName,
444
+ receiptHandle: message.receiptHandle,
445
+ visibilityTimeoutSeconds: directive.afterSeconds
446
+ });
447
+ } catch (changeError) {
448
+ console.warn(
449
+ "Failed to reschedule message for retry:",
450
+ changeError
451
+ );
452
+ }
453
+ await this.finalizePayload(message.payload);
454
+ return;
455
+ }
411
456
  }
412
457
  }
458
+ await this.finalizePayload(message.payload);
413
459
  throw error;
414
460
  }
415
461
  }
@@ -420,7 +466,7 @@ var ConsumerGroup = class {
420
466
  * pushes the full message payload in the callback request. The message is
421
467
  * processed with the same lifecycle guarantees as `consume()`:
422
468
  * - Visibility timeout is extended periodically during processing
423
- * - Message is deleted on successful handler completion
469
+ * - Message is acknowledged on successful handler completion
424
470
  * - Payload is finalized on error if the transport supports it
425
471
  *
426
472
  * @param handler - Function to process the message payload and metadata
@@ -434,6 +480,7 @@ var ConsumerGroup = class {
434
480
  await this.processMessage(message, handler, options);
435
481
  }
436
482
  async consume(handler, options) {
483
+ const retry = options?.retry;
437
484
  if (options && "messageId" in options) {
438
485
  const response = await this.client.receiveMessageById({
439
486
  queueName: this.topicName,
@@ -441,22 +488,21 @@ var ConsumerGroup = class {
441
488
  messageId: options.messageId,
442
489
  visibilityTimeoutSeconds: this.visibilityTimeout
443
490
  });
444
- await this.processMessage(response.message, handler);
491
+ await this.processMessage(response.message, handler, { retry });
492
+ return 1;
445
493
  } else {
446
494
  const limit = options && "limit" in options ? options.limit : 1;
447
- let messageFound = false;
495
+ let messagesProcessed = 0;
448
496
  for await (const message of this.client.receiveMessages({
449
497
  queueName: this.topicName,
450
498
  consumerGroup: this.consumerGroupName,
451
499
  visibilityTimeoutSeconds: this.visibilityTimeout,
452
500
  limit
453
501
  })) {
454
- messageFound = true;
455
- await this.processMessage(message, handler);
456
- }
457
- if (!messageFound) {
458
- await handler(null, null);
502
+ messagesProcessed++;
503
+ await this.processMessage(message, handler, { retry });
459
504
  }
505
+ return messagesProcessed;
460
506
  }
461
507
  }
462
508
  /**
@@ -478,8 +524,7 @@ var Topic = class {
478
524
  client;
479
525
  topicName;
480
526
  /**
481
- * Create a new Topic instance
482
- * @param client QueueClient instance to use for API calls (transport is configured on the client)
527
+ * @param client ApiClient instance to use for API calls
483
528
  * @param topicName Name of the topic to work with
484
529
  */
485
530
  constructor(client, topicName) {
@@ -490,7 +535,7 @@ var Topic = class {
490
535
  * Publish a message to the topic
491
536
  * @param payload The data to publish
492
537
  * @param options Optional publish options
493
- * @returns An object containing the message ID
538
+ * @returns `{ messageId }` `messageId` is `null` when deferred
494
539
  * @throws {BadRequestError} When request parameters are invalid
495
540
  * @throws {UnauthorizedError} When authentication fails
496
541
  * @throws {ForbiddenError} When access is denied (environment mismatch)
@@ -505,8 +550,12 @@ var Topic = class {
505
550
  delaySeconds: options?.delaySeconds,
506
551
  headers: options?.headers
507
552
  });
508
- if (isDevMode()) {
509
- triggerDevCallbacks(this.topicName, result.messageId);
553
+ if (result.messageId && isDevMode()) {
554
+ triggerDevCallbacks(
555
+ this.topicName,
556
+ result.messageId,
557
+ this.client.getRegion()
558
+ );
510
559
  }
511
560
  return { messageId: result.messageId };
512
561
  }
@@ -599,10 +648,12 @@ function parseBinaryHeaders(headers) {
599
648
  `Missing required CloudEvent headers: ${missingFields.join(", ")}`
600
649
  );
601
650
  }
651
+ const region = getHeader(headers, "ce-vqsregion") ?? void 0;
602
652
  const base = {
603
653
  queueName,
604
654
  consumerGroup,
605
- messageId
655
+ messageId,
656
+ region
606
657
  };
607
658
  const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
608
659
  if (!receiptHandle) {
@@ -617,6 +668,10 @@ function parseBinaryHeaders(headers) {
617
668
  if (createdAt) {
618
669
  result.createdAt = createdAt;
619
670
  }
671
+ const expiresAt = getHeader(headers, "ce-vqsexpiresat");
672
+ if (expiresAt) {
673
+ result.expiresAt = expiresAt;
674
+ }
620
675
  const contentType = getHeader(headers, "content-type");
621
676
  if (contentType) {
622
677
  result.contentType = contentType;
@@ -661,14 +716,20 @@ async function parseCallback(request) {
661
716
  }
662
717
  async function handleCallback(handler, request, options) {
663
718
  const { queueName, consumerGroup, messageId } = request;
664
- const client = options?.client || new QueueClient();
665
- const topic = new Topic(client, queueName);
719
+ if (!options?.client) {
720
+ throw new Error("HandleCallbackOptions.client is required");
721
+ }
722
+ let api = getApiClient(options.client);
723
+ if (request.region) {
724
+ api = api.withRegion(request.region);
725
+ }
726
+ const topic = new Topic(api, queueName);
666
727
  const cg = topic.consumerGroup(
667
728
  consumerGroup,
668
729
  options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
669
730
  );
670
731
  if ("receiptHandle" in request) {
671
- const transport = client.getTransport();
732
+ const transport = api.getTransport();
672
733
  let payload;
673
734
  if (request.rawBody) {
674
735
  payload = await transport.deserialize(request.rawBody);
@@ -684,13 +745,17 @@ async function handleCallback(handler, request, options) {
684
745
  payload,
685
746
  deliveryCount: request.deliveryCount ?? 1,
686
747
  createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
748
+ expiresAt: request.expiresAt ? new Date(request.expiresAt) : void 0,
687
749
  contentType: request.contentType ?? transport.contentType,
688
750
  receiptHandle: request.receiptHandle
689
751
  };
690
752
  const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
691
- await cg.consumeMessage(handler, message, { visibilityDeadline });
753
+ await cg.consumeMessage(handler, message, {
754
+ visibilityDeadline,
755
+ retry: options?.retry
756
+ });
692
757
  } else {
693
- await cg.consume(handler, { messageId });
758
+ await cg.consume(handler, { messageId, retry: options?.retry });
694
759
  }
695
760
  }
696
761
 
@@ -761,8 +826,8 @@ function isDevMode() {
761
826
  var DEV_VISIBILITY_POLL_INTERVAL = 50;
762
827
  var DEV_VISIBILITY_MAX_WAIT = 5e3;
763
828
  var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
764
- async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
765
- const client = new QueueClient();
829
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId, region) {
830
+ const client = new ApiClient({ region });
766
831
  let elapsed = 0;
767
832
  let interval = DEV_VISIBILITY_POLL_INTERVAL;
768
833
  while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
@@ -802,13 +867,13 @@ async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
802
867
  );
803
868
  return false;
804
869
  }
805
- function triggerDevCallbacks(topicName, messageId, delaySeconds) {
870
+ function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
806
871
  if (delaySeconds && delaySeconds > 0) {
807
872
  console.log(
808
873
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
809
874
  );
810
875
  setTimeout(() => {
811
- triggerDevCallbacks(topicName, messageId);
876
+ triggerDevCallbacks(topicName, messageId, region);
812
877
  }, delaySeconds * 1e3);
813
878
  return;
814
879
  }
@@ -831,7 +896,8 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
831
896
  const isVisible = await waitForMessageVisibility(
832
897
  topicName,
833
898
  firstRoute.consumer,
834
- messageId
899
+ messageId,
900
+ region
835
901
  );
836
902
  if (!isVisible) {
837
903
  console.warn(
@@ -853,7 +919,8 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
853
919
  "ce-type": CLOUD_EVENT_TYPE_V2BETA,
854
920
  "ce-vqsqueuename": topicName,
855
921
  "ce-vqsconsumergroup": route.consumer,
856
- "ce-vqsmessageid": messageId
922
+ "ce-vqsmessageid": messageId,
923
+ "ce-vqsregion": region
857
924
  }
858
925
  });
859
926
  if (response.ok) {
@@ -901,7 +968,7 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
901
968
  // src/oidc.ts
902
969
  var import_oidc = require("@vercel/oidc");
903
970
 
904
- // src/client.ts
971
+ // src/api-client.ts
905
972
  function isDebugEnabled() {
906
973
  return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
907
974
  }
@@ -954,40 +1021,80 @@ function parseQueueHeaders(headers) {
954
1021
  receiptHandle
955
1022
  };
956
1023
  }
957
- var QueueClient = class {
1024
+ var DEFAULT_BASE_URL_RESOLVER = (region) => `https://${region}.vercel-queue.com`;
1025
+ function resolveBaseUrl(region, resolver) {
1026
+ return (resolver ?? DEFAULT_BASE_URL_RESOLVER)(region);
1027
+ }
1028
+ var BASE_PATH = "/api/v3/topic";
1029
+ var ApiClient = class _ApiClient {
958
1030
  baseUrl;
959
- basePath;
960
1031
  customHeaders;
961
1032
  providedToken;
962
- defaultDeploymentId;
963
- pinToDeployment;
1033
+ resolvedDeploymentId;
1034
+ pinSends;
1035
+ explicitlyUnpinned;
964
1036
  transport;
965
- constructor(options = {}) {
966
- this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
967
- this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
1037
+ region;
1038
+ baseUrlResolver;
1039
+ constructor(options) {
1040
+ this.region = options.region;
1041
+ this.baseUrlResolver = options.resolveBaseUrl;
1042
+ this.baseUrl = resolveBaseUrl(this.region, this.baseUrlResolver);
968
1043
  this.customHeaders = options.headers || {};
969
1044
  this.providedToken = options.token;
970
- this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
971
- this.pinToDeployment = options.pinToDeployment ?? true;
972
1045
  this.transport = options.transport || new JsonTransport();
1046
+ if (options.deploymentId === null) {
1047
+ this.pinSends = false;
1048
+ this.explicitlyUnpinned = true;
1049
+ } else {
1050
+ this.resolvedDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
1051
+ this.pinSends = true;
1052
+ this.explicitlyUnpinned = false;
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Return a new ApiClient targeting the given region, sharing all other
1057
+ * configuration (token, transport, headers, deployment ID, resolver).
1058
+ * Used internally by handleCallback to route follow-up API calls to the
1059
+ * region indicated by the incoming `ce-vqsregion` header.
1060
+ */
1061
+ withRegion(region) {
1062
+ return new _ApiClient({
1063
+ region,
1064
+ resolveBaseUrl: this.baseUrlResolver,
1065
+ token: this.providedToken,
1066
+ headers: { ...this.customHeaders },
1067
+ deploymentId: this.explicitlyUnpinned ? null : this.resolvedDeploymentId,
1068
+ transport: this.transport
1069
+ });
1070
+ }
1071
+ getRegion() {
1072
+ return this.region;
973
1073
  }
974
1074
  getTransport() {
975
1075
  return this.transport;
976
1076
  }
1077
+ requireDeploymentId() {
1078
+ if (isDevMode() || this.explicitlyUnpinned || this.resolvedDeploymentId) {
1079
+ return;
1080
+ }
1081
+ throw new Error(
1082
+ 'No deployment ID available. VERCEL_DEPLOYMENT_ID is not set.\n\nThis usually means the code is running outside a Vercel deployment (e.g. during build or in a non-Vercel environment).\n\nTo fix this, create a new QueueClient with an explicit deploymentId:\n new QueueClient({ region: "iad1", deploymentId: "dpl_xxx" })\nOr explicitly opt out of deployment pinning:\n new QueueClient({ region: "iad1", deploymentId: null })'
1083
+ );
1084
+ }
977
1085
  getSendDeploymentId() {
978
1086
  if (isDevMode()) {
979
1087
  return void 0;
980
1088
  }
981
- if (this.pinToDeployment) {
982
- return this.defaultDeploymentId;
983
- }
984
- return void 0;
1089
+ this.requireDeploymentId();
1090
+ return this.pinSends ? this.resolvedDeploymentId : void 0;
985
1091
  }
986
1092
  getConsumeDeploymentId() {
987
1093
  if (isDevMode()) {
988
1094
  return void 0;
989
1095
  }
990
- return this.defaultDeploymentId;
1096
+ this.requireDeploymentId();
1097
+ return this.resolvedDeploymentId;
991
1098
  }
992
1099
  async getToken() {
993
1100
  if (this.providedToken) {
@@ -1005,7 +1112,7 @@ var QueueClient = class {
1005
1112
  const encodedQueue = encodeURIComponent(queueName);
1006
1113
  const segments = pathSegments.map((s) => encodeURIComponent(s));
1007
1114
  const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
1008
- return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
1115
+ return `${this.baseUrl}${BASE_PATH}/${encodedQueue}${path2}`;
1009
1116
  }
1010
1117
  async fetch(url, init) {
1011
1118
  const method = init.method || "GET";
@@ -1029,7 +1136,7 @@ var QueueClient = class {
1029
1136
  }
1030
1137
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1031
1138
  }
1032
- init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.39"}`);
1139
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.40"}`);
1033
1140
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1034
1141
  const response = await fetch(url, init);
1035
1142
  if (isDebugEnabled()) {
@@ -1044,24 +1151,6 @@ var QueueClient = class {
1044
1151
  }
1045
1152
  return response;
1046
1153
  }
1047
- /**
1048
- * Send a message to a topic.
1049
- *
1050
- * @param options - Message options including queue name, payload, and optional settings
1051
- * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
1052
- * @param options.payload - Message payload
1053
- * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
1054
- * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
1055
- * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
1056
- * @returns Promise with the generated messageId
1057
- * @throws {DuplicateMessageError} When idempotency key was already used
1058
- * @throws {ConsumerDiscoveryError} When consumer discovery fails
1059
- * @throws {ConsumerRegistryNotConfiguredError} When registry not configured
1060
- * @throws {BadRequestError} When parameters are invalid
1061
- * @throws {UnauthorizedError} When authentication fails
1062
- * @throws {ForbiddenError} When access is denied
1063
- * @throws {InternalServerError} When server encounters an error
1064
- */
1065
1154
  async sendMessage(options) {
1066
1155
  const transport = this.transport;
1067
1156
  const {
@@ -1139,28 +1228,12 @@ var QueueClient = class {
1139
1228
  "send message"
1140
1229
  );
1141
1230
  }
1231
+ if (response.status === 202) {
1232
+ return { messageId: null };
1233
+ }
1142
1234
  const responseData = await response.json();
1143
1235
  return responseData;
1144
1236
  }
1145
- /**
1146
- * Receive messages from a topic as an async generator.
1147
- *
1148
- * When the queue is empty, the generator completes without yielding any
1149
- * messages. Callers should handle the case where no messages are yielded.
1150
- *
1151
- * @param options - Receive options
1152
- * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
1153
- * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
1154
- * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
1155
- * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
1156
- * @yields Message objects with payload, messageId, receiptHandle, etc.
1157
- * Yields nothing if queue is empty.
1158
- * @throws {InvalidLimitError} When limit is outside 1-10 range
1159
- * @throws {BadRequestError} When parameters are invalid
1160
- * @throws {UnauthorizedError} When authentication fails
1161
- * @throws {ForbiddenError} When access is denied
1162
- * @throws {InternalServerError} When server encounters an error
1163
- */
1164
1237
  async *receiveMessages(options) {
1165
1238
  const transport = this.transport;
1166
1239
  const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
@@ -1226,23 +1299,6 @@ var QueueClient = class {
1226
1299
  }
1227
1300
  }
1228
1301
  }
1229
- /**
1230
- * Receive a specific message by its ID.
1231
- *
1232
- * @param options - Receive options
1233
- * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
1234
- * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
1235
- * @param options.messageId - Message ID to retrieve
1236
- * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
1237
- * @returns Promise with the message
1238
- * @throws {MessageNotFoundError} When message doesn't exist
1239
- * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
1240
- * @throws {MessageAlreadyProcessedError} When message was already processed
1241
- * @throws {BadRequestError} When parameters are invalid
1242
- * @throws {UnauthorizedError} When authentication fails
1243
- * @throws {ForbiddenError} When access is denied
1244
- * @throws {InternalServerError} When server encounters an error
1245
- */
1246
1302
  async receiveMessageById(options) {
1247
1303
  const transport = this.transport;
1248
1304
  const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
@@ -1317,22 +1373,7 @@ var QueueClient = class {
1317
1373
  }
1318
1374
  throw new MessageNotFoundError(messageId);
1319
1375
  }
1320
- /**
1321
- * Delete (acknowledge) a message after successful processing.
1322
- *
1323
- * @param options - Delete options
1324
- * @param options.queueName - Topic name
1325
- * @param options.consumerGroup - Consumer group name
1326
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
1327
- * @returns Promise indicating deletion success
1328
- * @throws {MessageNotFoundError} When receipt handle not found
1329
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
1330
- * @throws {BadRequestError} When parameters are invalid
1331
- * @throws {UnauthorizedError} When authentication fails
1332
- * @throws {ForbiddenError} When access is denied
1333
- * @throws {InternalServerError} When server encounters an error
1334
- */
1335
- async deleteMessage(options) {
1376
+ async acknowledgeMessage(options) {
1336
1377
  const { queueName, consumerGroup, receiptHandle } = options;
1337
1378
  const headers = new Headers({
1338
1379
  Authorization: `Bearer ${await this.getToken()}`,
@@ -1370,29 +1411,12 @@ var QueueClient = class {
1370
1411
  response.status,
1371
1412
  response.statusText,
1372
1413
  errorText,
1373
- "delete message",
1414
+ "acknowledge message",
1374
1415
  "Missing or invalid receipt handle"
1375
1416
  );
1376
1417
  }
1377
- return { deleted: true };
1418
+ return { acknowledged: true };
1378
1419
  }
1379
- /**
1380
- * Extend or change the visibility timeout of a message.
1381
- * Used to prevent message redelivery while still processing.
1382
- *
1383
- * @param options - Visibility options
1384
- * @param options.queueName - Topic name
1385
- * @param options.consumerGroup - Consumer group name
1386
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
1387
- * @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
1388
- * @returns Promise indicating success
1389
- * @throws {MessageNotFoundError} When receipt handle not found
1390
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
1391
- * @throws {BadRequestError} When parameters are invalid
1392
- * @throws {UnauthorizedError} When authentication fails
1393
- * @throws {ForbiddenError} When access is denied
1394
- * @throws {InternalServerError} When server encounters an error
1395
- */
1396
1420
  async changeVisibility(options) {
1397
1421
  const {
1398
1422
  queueName,
@@ -1444,100 +1468,211 @@ var QueueClient = class {
1444
1468
  }
1445
1469
  return { success: true };
1446
1470
  }
1471
+ };
1472
+
1473
+ // src/client.ts
1474
+ var apiClients = /* @__PURE__ */ new WeakMap();
1475
+ function getApiClient(client) {
1476
+ const api = apiClients.get(client);
1477
+ if (!api) {
1478
+ throw new Error("QueueClient not initialized");
1479
+ }
1480
+ return api;
1481
+ }
1482
+ var QueueClient = class {
1483
+ constructor(options) {
1484
+ apiClients.set(this, new ApiClient(options));
1485
+ }
1447
1486
  /**
1448
- * Alternative endpoint for changing message visibility timeout.
1449
- * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
1450
- * Functionally equivalent to changeVisibility but follows an alternative API pattern.
1487
+ * Send a message to a topic.
1488
+ *
1489
+ * This is an arrow function property so it can be destructured:
1490
+ * ```typescript
1491
+ * const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
1492
+ * await send("my-topic", payload);
1493
+ * ```
1451
1494
  *
1452
- * @param options - Options for changing visibility
1453
- * @returns Promise resolving to change visibility response
1495
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1496
+ * @param payload - The data to send (serialized via the configured transport)
1497
+ * @param options - Optional send options (idempotencyKey, retentionSeconds, delaySeconds, headers)
1498
+ * @returns `{ messageId }` — `messageId` is `null` when the server accepted
1499
+ * the message for deferred processing (no ID available yet)
1454
1500
  */
1455
- async changeVisibilityAlt(options) {
1456
- const {
1457
- queueName,
1458
- consumerGroup,
1459
- receiptHandle,
1460
- visibilityTimeoutSeconds
1461
- } = options;
1462
- const headers = new Headers({
1463
- Authorization: `Bearer ${await this.getToken()}`,
1464
- "Content-Type": "application/json",
1465
- ...this.customHeaders
1501
+ send = async (topicName, payload, options) => {
1502
+ const api = getApiClient(this);
1503
+ const result = await api.sendMessage({
1504
+ queueName: topicName,
1505
+ payload,
1506
+ idempotencyKey: options?.idempotencyKey,
1507
+ retentionSeconds: options?.retentionSeconds,
1508
+ delaySeconds: options?.delaySeconds,
1509
+ headers: options?.headers
1466
1510
  });
1467
- const effectiveDeploymentId = this.getConsumeDeploymentId();
1468
- if (effectiveDeploymentId) {
1469
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1511
+ if (result.messageId && isDevMode()) {
1512
+ triggerDevCallbacks(
1513
+ topicName,
1514
+ result.messageId,
1515
+ api.getRegion(),
1516
+ options?.delaySeconds
1517
+ );
1470
1518
  }
1471
- const response = await this.fetch(
1472
- this.buildUrl(
1473
- queueName,
1474
- "consumer",
1475
- consumerGroup,
1476
- "lease",
1477
- receiptHandle,
1478
- "visibility"
1479
- ),
1480
- {
1481
- method: "PATCH",
1482
- headers,
1483
- body: JSON.stringify({ visibilityTimeoutSeconds })
1484
- }
1519
+ return { messageId: result.messageId };
1520
+ };
1521
+ /**
1522
+ * Receive and process messages from a topic.
1523
+ *
1524
+ * Each message is automatically locked, kept alive via periodic visibility
1525
+ * extensions during processing, and acknowledged upon successful handler completion.
1526
+ * The handler is not called when the queue is empty — check `result.ok` instead.
1527
+ *
1528
+ * This is an arrow function property so it can be destructured:
1529
+ * ```typescript
1530
+ * const { receive } = new QueueClient({ region: process.env.QUEUE_REGION! });
1531
+ * const result = await receive("my-topic", "my-group", handler);
1532
+ * if (!result.ok) console.log(result.reason);
1533
+ * ```
1534
+ *
1535
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1536
+ * @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1537
+ * @param handler - Function to process each message payload and metadata.
1538
+ * Not called when the queue is empty.
1539
+ * @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
1540
+ * @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
1541
+ */
1542
+ receive = async (topicName, consumerGroup, handler, options) => {
1543
+ const api = getApiClient(this);
1544
+ const topic = new Topic(api, topicName);
1545
+ const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
1546
+ const consumer = topic.consumerGroup(
1547
+ consumerGroup,
1548
+ visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
1485
1549
  );
1486
- if (!response.ok) {
1487
- const errorText = await response.text();
1488
- if (response.status === 404) {
1489
- throw new MessageNotFoundError(receiptHandle);
1550
+ try {
1551
+ let count;
1552
+ const retry = options?.retry;
1553
+ if (options && "messageId" in options) {
1554
+ count = await consumer.consume(handler, {
1555
+ messageId: options.messageId,
1556
+ retry
1557
+ });
1558
+ } else {
1559
+ const limit = options && "limit" in options ? options.limit : void 0;
1560
+ count = await consumer.consume(handler, {
1561
+ ...limit !== void 0 ? { limit } : {},
1562
+ retry
1563
+ });
1490
1564
  }
1491
- if (response.status === 409) {
1492
- throw new MessageNotAvailableError(
1493
- receiptHandle,
1494
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
1495
- );
1565
+ if (count === 0) {
1566
+ return { ok: false, reason: "empty" };
1496
1567
  }
1497
- throwCommonHttpError(
1498
- response.status,
1499
- response.statusText,
1500
- errorText,
1501
- "change visibility (alt)",
1502
- "Missing receipt handle or invalid visibility timeout"
1503
- );
1568
+ return { ok: true };
1569
+ } catch (error) {
1570
+ if (options && "messageId" in options && error instanceof MessageNotFoundError) {
1571
+ return { ok: false, reason: "not_found", messageId: options.messageId };
1572
+ }
1573
+ if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
1574
+ return {
1575
+ ok: false,
1576
+ reason: "not_available",
1577
+ messageId: options.messageId
1578
+ };
1579
+ }
1580
+ if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
1581
+ return {
1582
+ ok: false,
1583
+ reason: "already_processed",
1584
+ messageId: options.messageId
1585
+ };
1586
+ }
1587
+ throw error;
1504
1588
  }
1505
- return { success: true };
1506
- }
1589
+ };
1590
+ /**
1591
+ * Create a Web API route handler for processing queue callback messages.
1592
+ *
1593
+ * Parses incoming `Request` as a CloudEvent and invokes the handler.
1594
+ * For use on Vercel — Vercel invokes this route when messages are available.
1595
+ *
1596
+ * This is an arrow function property so it can be destructured:
1597
+ * ```typescript
1598
+ * const { handleCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1599
+ * export const POST = handleCallback(handler);
1600
+ * ```
1601
+ *
1602
+ * @param handler - Function to process the message payload and metadata
1603
+ * @param options - Optional configuration
1604
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1605
+ * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1606
+ * reschedule the message for redelivery after N seconds.
1607
+ * @returns A `(request: Request) => Promise<Response>` route handler
1608
+ */
1609
+ handleCallback = (handler, options) => {
1610
+ return async (request) => {
1611
+ try {
1612
+ const parsed = await parseCallback(request);
1613
+ await handleCallback(handler, parsed, {
1614
+ client: this,
1615
+ visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
1616
+ retry: options?.retry
1617
+ });
1618
+ return Response.json({ status: "success" });
1619
+ } catch (error) {
1620
+ console.error("Queue callback error:", error);
1621
+ 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"))) {
1622
+ return Response.json({ error: error.message }, { status: 400 });
1623
+ }
1624
+ return Response.json(
1625
+ { error: "Failed to process queue message" },
1626
+ { status: 500 }
1627
+ );
1628
+ }
1629
+ };
1630
+ };
1631
+ /**
1632
+ * Create a Connect-style route handler for processing queue callback messages.
1633
+ * For use on Vercel — Vercel invokes this route when messages are available.
1634
+ *
1635
+ * For frameworks using the `(req, res)` middleware pattern where `req.body`
1636
+ * is pre-parsed (Next.js Pages Router, etc.).
1637
+ *
1638
+ * This is an arrow function property so it can be destructured:
1639
+ * ```typescript
1640
+ * const { handleNodeCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1641
+ * app.post("/api/queue", handleNodeCallback(handler));
1642
+ * ```
1643
+ *
1644
+ * @param handler - Function to process the message payload and metadata
1645
+ * @param options - Optional configuration
1646
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1647
+ * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1648
+ * reschedule the message for redelivery after N seconds.
1649
+ * @returns A `(req, res) => Promise<void>` route handler
1650
+ */
1651
+ handleNodeCallback = (handler, options) => {
1652
+ return async (req, res) => {
1653
+ if (req.method !== "POST") {
1654
+ res.status(200).end();
1655
+ return;
1656
+ }
1657
+ try {
1658
+ const parsed = parseRawCallback(req.body, req.headers);
1659
+ await handleCallback(handler, parsed, {
1660
+ client: this,
1661
+ visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
1662
+ retry: options?.retry
1663
+ });
1664
+ res.status(200).json({ status: "success" });
1665
+ } catch (error) {
1666
+ console.error("Queue callback error:", error);
1667
+ 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"))) {
1668
+ res.status(400).json({ error: error.message });
1669
+ return;
1670
+ }
1671
+ res.status(500).json({ error: "Failed to process queue message" });
1672
+ }
1673
+ };
1674
+ };
1507
1675
  };
1508
-
1509
- // src/factory.ts
1510
- async function send(topicName, payload, options) {
1511
- const client = options?.client || new QueueClient();
1512
- const result = await client.sendMessage({
1513
- queueName: topicName,
1514
- payload,
1515
- idempotencyKey: options?.idempotencyKey,
1516
- retentionSeconds: options?.retentionSeconds,
1517
- delaySeconds: options?.delaySeconds,
1518
- headers: options?.headers
1519
- });
1520
- if (isDevMode()) {
1521
- triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1522
- }
1523
- return { messageId: result.messageId };
1524
- }
1525
- async function receive(topicName, consumerGroup, handler, options) {
1526
- const client = options?.client || new QueueClient();
1527
- const topic = new Topic(client, topicName);
1528
- const { client: _, ...rest } = options || {};
1529
- const { visibilityTimeoutSeconds } = rest;
1530
- const consumer = topic.consumerGroup(
1531
- consumerGroup,
1532
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
1533
- );
1534
- if (options && "messageId" in options) {
1535
- return consumer.consume(handler, { messageId: options.messageId });
1536
- } else {
1537
- const limit = options && "limit" in options ? options.limit : void 0;
1538
- return consumer.consume(handler, limit !== void 0 ? { limit } : {});
1539
- }
1540
- }
1541
1676
  // Annotate the CommonJS export names for ESM import in node:
1542
1677
  0 && (module.exports = {
1543
1678
  BadRequestError,
@@ -1560,10 +1695,7 @@ async function receive(topicName, consumerGroup, handler, options) {
1560
1695
  QueueEmptyError,
1561
1696
  StreamTransport,
1562
1697
  UnauthorizedError,
1563
- handleCallback,
1564
1698
  parseCallback,
1565
- parseRawCallback,
1566
- receive,
1567
- send
1699
+ parseRawCallback
1568
1700
  });
1569
1701
  //# sourceMappingURL=index.js.map