@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.mjs CHANGED
@@ -71,7 +71,7 @@ var StreamTransport = class {
71
71
  }
72
72
  };
73
73
 
74
- // src/client.ts
74
+ // src/api-client.ts
75
75
  import { parseMultipartStream } from "mixpart";
76
76
 
77
77
  // src/dev.ts
@@ -196,7 +196,7 @@ var ConsumerGroup = class {
196
196
  /**
197
197
  * Create a new ConsumerGroup instance.
198
198
  *
199
- * @param client - QueueClient instance to use for API calls (transport is configured on the client)
199
+ * @param client - ApiClient instance to use for API calls (transport is configured on the client)
200
200
  * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
201
201
  * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
202
202
  * @param options - Optional configuration
@@ -321,35 +321,84 @@ var ConsumerGroup = class {
321
321
  }
322
322
  };
323
323
  }
324
+ /**
325
+ * Clean up the message payload if the transport supports it and payload exists.
326
+ */
327
+ async finalizePayload(payload) {
328
+ const transport = this.client.getTransport();
329
+ if (transport.finalize && payload !== void 0 && payload !== null) {
330
+ try {
331
+ await transport.finalize(payload);
332
+ } catch (finalizeError) {
333
+ console.warn("Failed to finalize message payload:", finalizeError);
334
+ }
335
+ }
336
+ }
324
337
  async processMessage(message, handler, options) {
325
338
  const stopExtension = this.startVisibilityExtension(
326
339
  message.receiptHandle,
327
340
  options
328
341
  );
342
+ const metadata = {
343
+ messageId: message.messageId,
344
+ deliveryCount: message.deliveryCount,
345
+ createdAt: message.createdAt,
346
+ expiresAt: message.expiresAt,
347
+ topicName: this.topicName,
348
+ consumerGroup: this.consumerGroupName,
349
+ region: this.client.getRegion()
350
+ };
329
351
  try {
330
- await handler(message.payload, {
331
- messageId: message.messageId,
332
- deliveryCount: message.deliveryCount,
333
- createdAt: message.createdAt,
334
- topicName: this.topicName,
335
- consumerGroup: this.consumerGroupName
336
- });
352
+ await handler(message.payload, metadata);
337
353
  await stopExtension();
338
- await this.client.deleteMessage({
354
+ await this.client.acknowledgeMessage({
339
355
  queueName: this.topicName,
340
356
  consumerGroup: this.consumerGroupName,
341
357
  receiptHandle: message.receiptHandle
342
358
  });
343
359
  } catch (error) {
344
360
  await stopExtension();
345
- const transport = this.client.getTransport();
346
- if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
361
+ if (options?.retry) {
362
+ let directive;
347
363
  try {
348
- await transport.finalize(message.payload);
349
- } catch (finalizeError) {
350
- console.warn("Failed to finalize message payload:", finalizeError);
364
+ directive = options.retry(error, metadata);
365
+ } catch (retryError) {
366
+ console.warn("retry handler threw:", retryError);
367
+ }
368
+ if (directive) {
369
+ if ("acknowledge" in directive && directive.acknowledge) {
370
+ try {
371
+ await this.client.acknowledgeMessage({
372
+ queueName: this.topicName,
373
+ consumerGroup: this.consumerGroupName,
374
+ receiptHandle: message.receiptHandle
375
+ });
376
+ } catch (ackError) {
377
+ console.warn("Failed to acknowledge message:", ackError);
378
+ }
379
+ await this.finalizePayload(message.payload);
380
+ return;
381
+ }
382
+ if ("afterSeconds" in directive && typeof directive.afterSeconds === "number") {
383
+ try {
384
+ await this.client.changeVisibility({
385
+ queueName: this.topicName,
386
+ consumerGroup: this.consumerGroupName,
387
+ receiptHandle: message.receiptHandle,
388
+ visibilityTimeoutSeconds: directive.afterSeconds
389
+ });
390
+ } catch (changeError) {
391
+ console.warn(
392
+ "Failed to reschedule message for retry:",
393
+ changeError
394
+ );
395
+ }
396
+ await this.finalizePayload(message.payload);
397
+ return;
398
+ }
351
399
  }
352
400
  }
401
+ await this.finalizePayload(message.payload);
353
402
  throw error;
354
403
  }
355
404
  }
@@ -360,7 +409,7 @@ var ConsumerGroup = class {
360
409
  * pushes the full message payload in the callback request. The message is
361
410
  * processed with the same lifecycle guarantees as `consume()`:
362
411
  * - Visibility timeout is extended periodically during processing
363
- * - Message is deleted on successful handler completion
412
+ * - Message is acknowledged on successful handler completion
364
413
  * - Payload is finalized on error if the transport supports it
365
414
  *
366
415
  * @param handler - Function to process the message payload and metadata
@@ -374,6 +423,7 @@ var ConsumerGroup = class {
374
423
  await this.processMessage(message, handler, options);
375
424
  }
376
425
  async consume(handler, options) {
426
+ const retry = options?.retry;
377
427
  if (options && "messageId" in options) {
378
428
  const response = await this.client.receiveMessageById({
379
429
  queueName: this.topicName,
@@ -381,22 +431,21 @@ var ConsumerGroup = class {
381
431
  messageId: options.messageId,
382
432
  visibilityTimeoutSeconds: this.visibilityTimeout
383
433
  });
384
- await this.processMessage(response.message, handler);
434
+ await this.processMessage(response.message, handler, { retry });
435
+ return 1;
385
436
  } else {
386
437
  const limit = options && "limit" in options ? options.limit : 1;
387
- let messageFound = false;
438
+ let messagesProcessed = 0;
388
439
  for await (const message of this.client.receiveMessages({
389
440
  queueName: this.topicName,
390
441
  consumerGroup: this.consumerGroupName,
391
442
  visibilityTimeoutSeconds: this.visibilityTimeout,
392
443
  limit
393
444
  })) {
394
- messageFound = true;
395
- await this.processMessage(message, handler);
396
- }
397
- if (!messageFound) {
398
- await handler(null, null);
445
+ messagesProcessed++;
446
+ await this.processMessage(message, handler, { retry });
399
447
  }
448
+ return messagesProcessed;
400
449
  }
401
450
  }
402
451
  /**
@@ -418,8 +467,7 @@ var Topic = class {
418
467
  client;
419
468
  topicName;
420
469
  /**
421
- * Create a new Topic instance
422
- * @param client QueueClient instance to use for API calls (transport is configured on the client)
470
+ * @param client ApiClient instance to use for API calls
423
471
  * @param topicName Name of the topic to work with
424
472
  */
425
473
  constructor(client, topicName) {
@@ -430,7 +478,7 @@ var Topic = class {
430
478
  * Publish a message to the topic
431
479
  * @param payload The data to publish
432
480
  * @param options Optional publish options
433
- * @returns An object containing the message ID
481
+ * @returns `{ messageId }` `messageId` is `null` when deferred
434
482
  * @throws {BadRequestError} When request parameters are invalid
435
483
  * @throws {UnauthorizedError} When authentication fails
436
484
  * @throws {ForbiddenError} When access is denied (environment mismatch)
@@ -445,8 +493,12 @@ var Topic = class {
445
493
  delaySeconds: options?.delaySeconds,
446
494
  headers: options?.headers
447
495
  });
448
- if (isDevMode()) {
449
- triggerDevCallbacks(this.topicName, result.messageId);
496
+ if (result.messageId && isDevMode()) {
497
+ triggerDevCallbacks(
498
+ this.topicName,
499
+ result.messageId,
500
+ this.client.getRegion()
501
+ );
450
502
  }
451
503
  return { messageId: result.messageId };
452
504
  }
@@ -539,10 +591,12 @@ function parseBinaryHeaders(headers) {
539
591
  `Missing required CloudEvent headers: ${missingFields.join(", ")}`
540
592
  );
541
593
  }
594
+ const region = getHeader(headers, "ce-vqsregion") ?? void 0;
542
595
  const base = {
543
596
  queueName,
544
597
  consumerGroup,
545
- messageId
598
+ messageId,
599
+ region
546
600
  };
547
601
  const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
548
602
  if (!receiptHandle) {
@@ -557,6 +611,10 @@ function parseBinaryHeaders(headers) {
557
611
  if (createdAt) {
558
612
  result.createdAt = createdAt;
559
613
  }
614
+ const expiresAt = getHeader(headers, "ce-vqsexpiresat");
615
+ if (expiresAt) {
616
+ result.expiresAt = expiresAt;
617
+ }
560
618
  const contentType = getHeader(headers, "content-type");
561
619
  if (contentType) {
562
620
  result.contentType = contentType;
@@ -601,14 +659,20 @@ async function parseCallback(request) {
601
659
  }
602
660
  async function handleCallback(handler, request, options) {
603
661
  const { queueName, consumerGroup, messageId } = request;
604
- const client = options?.client || new QueueClient();
605
- const topic = new Topic(client, queueName);
662
+ if (!options?.client) {
663
+ throw new Error("HandleCallbackOptions.client is required");
664
+ }
665
+ let api = getApiClient(options.client);
666
+ if (request.region) {
667
+ api = api.withRegion(request.region);
668
+ }
669
+ const topic = new Topic(api, queueName);
606
670
  const cg = topic.consumerGroup(
607
671
  consumerGroup,
608
672
  options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
609
673
  );
610
674
  if ("receiptHandle" in request) {
611
- const transport = client.getTransport();
675
+ const transport = api.getTransport();
612
676
  let payload;
613
677
  if (request.rawBody) {
614
678
  payload = await transport.deserialize(request.rawBody);
@@ -624,13 +688,17 @@ async function handleCallback(handler, request, options) {
624
688
  payload,
625
689
  deliveryCount: request.deliveryCount ?? 1,
626
690
  createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
691
+ expiresAt: request.expiresAt ? new Date(request.expiresAt) : void 0,
627
692
  contentType: request.contentType ?? transport.contentType,
628
693
  receiptHandle: request.receiptHandle
629
694
  };
630
695
  const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
631
- await cg.consumeMessage(handler, message, { visibilityDeadline });
696
+ await cg.consumeMessage(handler, message, {
697
+ visibilityDeadline,
698
+ retry: options?.retry
699
+ });
632
700
  } else {
633
- await cg.consume(handler, { messageId });
701
+ await cg.consume(handler, { messageId, retry: options?.retry });
634
702
  }
635
703
  }
636
704
 
@@ -701,8 +769,8 @@ function isDevMode() {
701
769
  var DEV_VISIBILITY_POLL_INTERVAL = 50;
702
770
  var DEV_VISIBILITY_MAX_WAIT = 5e3;
703
771
  var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
704
- async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
705
- const client = new QueueClient();
772
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId, region) {
773
+ const client = new ApiClient({ region });
706
774
  let elapsed = 0;
707
775
  let interval = DEV_VISIBILITY_POLL_INTERVAL;
708
776
  while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
@@ -742,13 +810,13 @@ async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
742
810
  );
743
811
  return false;
744
812
  }
745
- function triggerDevCallbacks(topicName, messageId, delaySeconds) {
813
+ function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
746
814
  if (delaySeconds && delaySeconds > 0) {
747
815
  console.log(
748
816
  `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
749
817
  );
750
818
  setTimeout(() => {
751
- triggerDevCallbacks(topicName, messageId);
819
+ triggerDevCallbacks(topicName, messageId, region);
752
820
  }, delaySeconds * 1e3);
753
821
  return;
754
822
  }
@@ -771,7 +839,8 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
771
839
  const isVisible = await waitForMessageVisibility(
772
840
  topicName,
773
841
  firstRoute.consumer,
774
- messageId
842
+ messageId,
843
+ region
775
844
  );
776
845
  if (!isVisible) {
777
846
  console.warn(
@@ -793,7 +862,8 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
793
862
  "ce-type": CLOUD_EVENT_TYPE_V2BETA,
794
863
  "ce-vqsqueuename": topicName,
795
864
  "ce-vqsconsumergroup": route.consumer,
796
- "ce-vqsmessageid": messageId
865
+ "ce-vqsmessageid": messageId,
866
+ "ce-vqsregion": region
797
867
  }
798
868
  });
799
869
  if (response.ok) {
@@ -841,7 +911,7 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
841
911
  // src/oidc.ts
842
912
  import { getVercelOidcToken } from "@vercel/oidc";
843
913
 
844
- // src/client.ts
914
+ // src/api-client.ts
845
915
  function isDebugEnabled() {
846
916
  return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
847
917
  }
@@ -894,40 +964,80 @@ function parseQueueHeaders(headers) {
894
964
  receiptHandle
895
965
  };
896
966
  }
897
- var QueueClient = class {
967
+ var DEFAULT_BASE_URL_RESOLVER = (region) => `https://${region}.vercel-queue.com`;
968
+ function resolveBaseUrl(region, resolver) {
969
+ return (resolver ?? DEFAULT_BASE_URL_RESOLVER)(region);
970
+ }
971
+ var BASE_PATH = "/api/v3/topic";
972
+ var ApiClient = class _ApiClient {
898
973
  baseUrl;
899
- basePath;
900
974
  customHeaders;
901
975
  providedToken;
902
- defaultDeploymentId;
903
- pinToDeployment;
976
+ resolvedDeploymentId;
977
+ pinSends;
978
+ explicitlyUnpinned;
904
979
  transport;
905
- constructor(options = {}) {
906
- this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
907
- this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
980
+ region;
981
+ baseUrlResolver;
982
+ constructor(options) {
983
+ this.region = options.region;
984
+ this.baseUrlResolver = options.resolveBaseUrl;
985
+ this.baseUrl = resolveBaseUrl(this.region, this.baseUrlResolver);
908
986
  this.customHeaders = options.headers || {};
909
987
  this.providedToken = options.token;
910
- this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
911
- this.pinToDeployment = options.pinToDeployment ?? true;
912
988
  this.transport = options.transport || new JsonTransport();
989
+ if (options.deploymentId === null) {
990
+ this.pinSends = false;
991
+ this.explicitlyUnpinned = true;
992
+ } else {
993
+ this.resolvedDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
994
+ this.pinSends = true;
995
+ this.explicitlyUnpinned = false;
996
+ }
997
+ }
998
+ /**
999
+ * Return a new ApiClient targeting the given region, sharing all other
1000
+ * configuration (token, transport, headers, deployment ID, resolver).
1001
+ * Used internally by handleCallback to route follow-up API calls to the
1002
+ * region indicated by the incoming `ce-vqsregion` header.
1003
+ */
1004
+ withRegion(region) {
1005
+ return new _ApiClient({
1006
+ region,
1007
+ resolveBaseUrl: this.baseUrlResolver,
1008
+ token: this.providedToken,
1009
+ headers: { ...this.customHeaders },
1010
+ deploymentId: this.explicitlyUnpinned ? null : this.resolvedDeploymentId,
1011
+ transport: this.transport
1012
+ });
1013
+ }
1014
+ getRegion() {
1015
+ return this.region;
913
1016
  }
914
1017
  getTransport() {
915
1018
  return this.transport;
916
1019
  }
1020
+ requireDeploymentId() {
1021
+ if (isDevMode() || this.explicitlyUnpinned || this.resolvedDeploymentId) {
1022
+ return;
1023
+ }
1024
+ throw new Error(
1025
+ '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 })'
1026
+ );
1027
+ }
917
1028
  getSendDeploymentId() {
918
1029
  if (isDevMode()) {
919
1030
  return void 0;
920
1031
  }
921
- if (this.pinToDeployment) {
922
- return this.defaultDeploymentId;
923
- }
924
- return void 0;
1032
+ this.requireDeploymentId();
1033
+ return this.pinSends ? this.resolvedDeploymentId : void 0;
925
1034
  }
926
1035
  getConsumeDeploymentId() {
927
1036
  if (isDevMode()) {
928
1037
  return void 0;
929
1038
  }
930
- return this.defaultDeploymentId;
1039
+ this.requireDeploymentId();
1040
+ return this.resolvedDeploymentId;
931
1041
  }
932
1042
  async getToken() {
933
1043
  if (this.providedToken) {
@@ -945,7 +1055,7 @@ var QueueClient = class {
945
1055
  const encodedQueue = encodeURIComponent(queueName);
946
1056
  const segments = pathSegments.map((s) => encodeURIComponent(s));
947
1057
  const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
948
- return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
1058
+ return `${this.baseUrl}${BASE_PATH}/${encodedQueue}${path2}`;
949
1059
  }
950
1060
  async fetch(url, init) {
951
1061
  const method = init.method || "GET";
@@ -969,7 +1079,7 @@ var QueueClient = class {
969
1079
  }
970
1080
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
971
1081
  }
972
- init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.39"}`);
1082
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.40"}`);
973
1083
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
974
1084
  const response = await fetch(url, init);
975
1085
  if (isDebugEnabled()) {
@@ -984,24 +1094,6 @@ var QueueClient = class {
984
1094
  }
985
1095
  return response;
986
1096
  }
987
- /**
988
- * Send a message to a topic.
989
- *
990
- * @param options - Message options including queue name, payload, and optional settings
991
- * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
992
- * @param options.payload - Message payload
993
- * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
994
- * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
995
- * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
996
- * @returns Promise with the generated messageId
997
- * @throws {DuplicateMessageError} When idempotency key was already used
998
- * @throws {ConsumerDiscoveryError} When consumer discovery fails
999
- * @throws {ConsumerRegistryNotConfiguredError} When registry not configured
1000
- * @throws {BadRequestError} When parameters are invalid
1001
- * @throws {UnauthorizedError} When authentication fails
1002
- * @throws {ForbiddenError} When access is denied
1003
- * @throws {InternalServerError} When server encounters an error
1004
- */
1005
1097
  async sendMessage(options) {
1006
1098
  const transport = this.transport;
1007
1099
  const {
@@ -1079,28 +1171,12 @@ var QueueClient = class {
1079
1171
  "send message"
1080
1172
  );
1081
1173
  }
1174
+ if (response.status === 202) {
1175
+ return { messageId: null };
1176
+ }
1082
1177
  const responseData = await response.json();
1083
1178
  return responseData;
1084
1179
  }
1085
- /**
1086
- * Receive messages from a topic as an async generator.
1087
- *
1088
- * When the queue is empty, the generator completes without yielding any
1089
- * messages. Callers should handle the case where no messages are yielded.
1090
- *
1091
- * @param options - Receive options
1092
- * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
1093
- * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
1094
- * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
1095
- * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
1096
- * @yields Message objects with payload, messageId, receiptHandle, etc.
1097
- * Yields nothing if queue is empty.
1098
- * @throws {InvalidLimitError} When limit is outside 1-10 range
1099
- * @throws {BadRequestError} When parameters are invalid
1100
- * @throws {UnauthorizedError} When authentication fails
1101
- * @throws {ForbiddenError} When access is denied
1102
- * @throws {InternalServerError} When server encounters an error
1103
- */
1104
1180
  async *receiveMessages(options) {
1105
1181
  const transport = this.transport;
1106
1182
  const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
@@ -1166,23 +1242,6 @@ var QueueClient = class {
1166
1242
  }
1167
1243
  }
1168
1244
  }
1169
- /**
1170
- * Receive a specific message by its ID.
1171
- *
1172
- * @param options - Receive options
1173
- * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
1174
- * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
1175
- * @param options.messageId - Message ID to retrieve
1176
- * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
1177
- * @returns Promise with the message
1178
- * @throws {MessageNotFoundError} When message doesn't exist
1179
- * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
1180
- * @throws {MessageAlreadyProcessedError} When message was already processed
1181
- * @throws {BadRequestError} When parameters are invalid
1182
- * @throws {UnauthorizedError} When authentication fails
1183
- * @throws {ForbiddenError} When access is denied
1184
- * @throws {InternalServerError} When server encounters an error
1185
- */
1186
1245
  async receiveMessageById(options) {
1187
1246
  const transport = this.transport;
1188
1247
  const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
@@ -1257,22 +1316,7 @@ var QueueClient = class {
1257
1316
  }
1258
1317
  throw new MessageNotFoundError(messageId);
1259
1318
  }
1260
- /**
1261
- * Delete (acknowledge) a message after successful processing.
1262
- *
1263
- * @param options - Delete options
1264
- * @param options.queueName - Topic name
1265
- * @param options.consumerGroup - Consumer group name
1266
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
1267
- * @returns Promise indicating deletion success
1268
- * @throws {MessageNotFoundError} When receipt handle not found
1269
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
1270
- * @throws {BadRequestError} When parameters are invalid
1271
- * @throws {UnauthorizedError} When authentication fails
1272
- * @throws {ForbiddenError} When access is denied
1273
- * @throws {InternalServerError} When server encounters an error
1274
- */
1275
- async deleteMessage(options) {
1319
+ async acknowledgeMessage(options) {
1276
1320
  const { queueName, consumerGroup, receiptHandle } = options;
1277
1321
  const headers = new Headers({
1278
1322
  Authorization: `Bearer ${await this.getToken()}`,
@@ -1310,29 +1354,12 @@ var QueueClient = class {
1310
1354
  response.status,
1311
1355
  response.statusText,
1312
1356
  errorText,
1313
- "delete message",
1357
+ "acknowledge message",
1314
1358
  "Missing or invalid receipt handle"
1315
1359
  );
1316
1360
  }
1317
- return { deleted: true };
1361
+ return { acknowledged: true };
1318
1362
  }
1319
- /**
1320
- * Extend or change the visibility timeout of a message.
1321
- * Used to prevent message redelivery while still processing.
1322
- *
1323
- * @param options - Visibility 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
- * @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
1328
- * @returns Promise indicating success
1329
- * @throws {MessageNotFoundError} When receipt handle not found
1330
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
1331
- * @throws {BadRequestError} When parameters are invalid
1332
- * @throws {UnauthorizedError} When authentication fails
1333
- * @throws {ForbiddenError} When access is denied
1334
- * @throws {InternalServerError} When server encounters an error
1335
- */
1336
1363
  async changeVisibility(options) {
1337
1364
  const {
1338
1365
  queueName,
@@ -1384,100 +1411,211 @@ var QueueClient = class {
1384
1411
  }
1385
1412
  return { success: true };
1386
1413
  }
1414
+ };
1415
+
1416
+ // src/client.ts
1417
+ var apiClients = /* @__PURE__ */ new WeakMap();
1418
+ function getApiClient(client) {
1419
+ const api = apiClients.get(client);
1420
+ if (!api) {
1421
+ throw new Error("QueueClient not initialized");
1422
+ }
1423
+ return api;
1424
+ }
1425
+ var QueueClient = class {
1426
+ constructor(options) {
1427
+ apiClients.set(this, new ApiClient(options));
1428
+ }
1387
1429
  /**
1388
- * Alternative endpoint for changing message visibility timeout.
1389
- * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
1390
- * Functionally equivalent to changeVisibility but follows an alternative API pattern.
1430
+ * Send a message to a topic.
1431
+ *
1432
+ * This is an arrow function property so it can be destructured:
1433
+ * ```typescript
1434
+ * const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
1435
+ * await send("my-topic", payload);
1436
+ * ```
1391
1437
  *
1392
- * @param options - Options for changing visibility
1393
- * @returns Promise resolving to change visibility response
1438
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1439
+ * @param payload - The data to send (serialized via the configured transport)
1440
+ * @param options - Optional send options (idempotencyKey, retentionSeconds, delaySeconds, headers)
1441
+ * @returns `{ messageId }` — `messageId` is `null` when the server accepted
1442
+ * the message for deferred processing (no ID available yet)
1394
1443
  */
1395
- async changeVisibilityAlt(options) {
1396
- const {
1397
- queueName,
1398
- consumerGroup,
1399
- receiptHandle,
1400
- visibilityTimeoutSeconds
1401
- } = options;
1402
- const headers = new Headers({
1403
- Authorization: `Bearer ${await this.getToken()}`,
1404
- "Content-Type": "application/json",
1405
- ...this.customHeaders
1444
+ send = async (topicName, payload, options) => {
1445
+ const api = getApiClient(this);
1446
+ const result = await api.sendMessage({
1447
+ queueName: topicName,
1448
+ payload,
1449
+ idempotencyKey: options?.idempotencyKey,
1450
+ retentionSeconds: options?.retentionSeconds,
1451
+ delaySeconds: options?.delaySeconds,
1452
+ headers: options?.headers
1406
1453
  });
1407
- const effectiveDeploymentId = this.getConsumeDeploymentId();
1408
- if (effectiveDeploymentId) {
1409
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1454
+ if (result.messageId && isDevMode()) {
1455
+ triggerDevCallbacks(
1456
+ topicName,
1457
+ result.messageId,
1458
+ api.getRegion(),
1459
+ options?.delaySeconds
1460
+ );
1410
1461
  }
1411
- const response = await this.fetch(
1412
- this.buildUrl(
1413
- queueName,
1414
- "consumer",
1415
- consumerGroup,
1416
- "lease",
1417
- receiptHandle,
1418
- "visibility"
1419
- ),
1420
- {
1421
- method: "PATCH",
1422
- headers,
1423
- body: JSON.stringify({ visibilityTimeoutSeconds })
1424
- }
1462
+ return { messageId: result.messageId };
1463
+ };
1464
+ /**
1465
+ * Receive and process messages from a topic.
1466
+ *
1467
+ * Each message is automatically locked, kept alive via periodic visibility
1468
+ * extensions during processing, and acknowledged upon successful handler completion.
1469
+ * The handler is not called when the queue is empty — check `result.ok` instead.
1470
+ *
1471
+ * This is an arrow function property so it can be destructured:
1472
+ * ```typescript
1473
+ * const { receive } = new QueueClient({ region: process.env.QUEUE_REGION! });
1474
+ * const result = await receive("my-topic", "my-group", handler);
1475
+ * if (!result.ok) console.log(result.reason);
1476
+ * ```
1477
+ *
1478
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1479
+ * @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1480
+ * @param handler - Function to process each message payload and metadata.
1481
+ * Not called when the queue is empty.
1482
+ * @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
1483
+ * @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
1484
+ */
1485
+ receive = async (topicName, consumerGroup, handler, options) => {
1486
+ const api = getApiClient(this);
1487
+ const topic = new Topic(api, topicName);
1488
+ const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
1489
+ const consumer = topic.consumerGroup(
1490
+ consumerGroup,
1491
+ visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
1425
1492
  );
1426
- if (!response.ok) {
1427
- const errorText = await response.text();
1428
- if (response.status === 404) {
1429
- throw new MessageNotFoundError(receiptHandle);
1493
+ try {
1494
+ let count;
1495
+ const retry = options?.retry;
1496
+ if (options && "messageId" in options) {
1497
+ count = await consumer.consume(handler, {
1498
+ messageId: options.messageId,
1499
+ retry
1500
+ });
1501
+ } else {
1502
+ const limit = options && "limit" in options ? options.limit : void 0;
1503
+ count = await consumer.consume(handler, {
1504
+ ...limit !== void 0 ? { limit } : {},
1505
+ retry
1506
+ });
1430
1507
  }
1431
- if (response.status === 409) {
1432
- throw new MessageNotAvailableError(
1433
- receiptHandle,
1434
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
1435
- );
1508
+ if (count === 0) {
1509
+ return { ok: false, reason: "empty" };
1436
1510
  }
1437
- throwCommonHttpError(
1438
- response.status,
1439
- response.statusText,
1440
- errorText,
1441
- "change visibility (alt)",
1442
- "Missing receipt handle or invalid visibility timeout"
1443
- );
1511
+ return { ok: true };
1512
+ } catch (error) {
1513
+ if (options && "messageId" in options && error instanceof MessageNotFoundError) {
1514
+ return { ok: false, reason: "not_found", messageId: options.messageId };
1515
+ }
1516
+ if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
1517
+ return {
1518
+ ok: false,
1519
+ reason: "not_available",
1520
+ messageId: options.messageId
1521
+ };
1522
+ }
1523
+ if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
1524
+ return {
1525
+ ok: false,
1526
+ reason: "already_processed",
1527
+ messageId: options.messageId
1528
+ };
1529
+ }
1530
+ throw error;
1444
1531
  }
1445
- return { success: true };
1446
- }
1532
+ };
1533
+ /**
1534
+ * Create a Web API route handler for processing queue callback messages.
1535
+ *
1536
+ * Parses incoming `Request` as a CloudEvent and invokes the handler.
1537
+ * For use on Vercel — Vercel invokes this route when messages are available.
1538
+ *
1539
+ * This is an arrow function property so it can be destructured:
1540
+ * ```typescript
1541
+ * const { handleCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1542
+ * export const POST = handleCallback(handler);
1543
+ * ```
1544
+ *
1545
+ * @param handler - Function to process the message payload and metadata
1546
+ * @param options - Optional configuration
1547
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1548
+ * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1549
+ * reschedule the message for redelivery after N seconds.
1550
+ * @returns A `(request: Request) => Promise<Response>` route handler
1551
+ */
1552
+ handleCallback = (handler, options) => {
1553
+ return async (request) => {
1554
+ try {
1555
+ const parsed = await parseCallback(request);
1556
+ await handleCallback(handler, parsed, {
1557
+ client: this,
1558
+ visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
1559
+ retry: options?.retry
1560
+ });
1561
+ return Response.json({ status: "success" });
1562
+ } catch (error) {
1563
+ console.error("Queue callback error:", error);
1564
+ 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"))) {
1565
+ return Response.json({ error: error.message }, { status: 400 });
1566
+ }
1567
+ return Response.json(
1568
+ { error: "Failed to process queue message" },
1569
+ { status: 500 }
1570
+ );
1571
+ }
1572
+ };
1573
+ };
1574
+ /**
1575
+ * Create a Connect-style route handler for processing queue callback messages.
1576
+ * For use on Vercel — Vercel invokes this route when messages are available.
1577
+ *
1578
+ * For frameworks using the `(req, res)` middleware pattern where `req.body`
1579
+ * is pre-parsed (Next.js Pages Router, etc.).
1580
+ *
1581
+ * This is an arrow function property so it can be destructured:
1582
+ * ```typescript
1583
+ * const { handleNodeCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1584
+ * app.post("/api/queue", handleNodeCallback(handler));
1585
+ * ```
1586
+ *
1587
+ * @param handler - Function to process the message payload and metadata
1588
+ * @param options - Optional configuration
1589
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1590
+ * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1591
+ * reschedule the message for redelivery after N seconds.
1592
+ * @returns A `(req, res) => Promise<void>` route handler
1593
+ */
1594
+ handleNodeCallback = (handler, options) => {
1595
+ return async (req, res) => {
1596
+ if (req.method !== "POST") {
1597
+ res.status(200).end();
1598
+ return;
1599
+ }
1600
+ try {
1601
+ const parsed = parseRawCallback(req.body, req.headers);
1602
+ await handleCallback(handler, parsed, {
1603
+ client: this,
1604
+ visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
1605
+ retry: options?.retry
1606
+ });
1607
+ res.status(200).json({ status: "success" });
1608
+ } catch (error) {
1609
+ console.error("Queue callback error:", error);
1610
+ 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"))) {
1611
+ res.status(400).json({ error: error.message });
1612
+ return;
1613
+ }
1614
+ res.status(500).json({ error: "Failed to process queue message" });
1615
+ }
1616
+ };
1617
+ };
1447
1618
  };
1448
-
1449
- // src/factory.ts
1450
- async function send(topicName, payload, options) {
1451
- const client = options?.client || new QueueClient();
1452
- const result = await client.sendMessage({
1453
- queueName: topicName,
1454
- payload,
1455
- idempotencyKey: options?.idempotencyKey,
1456
- retentionSeconds: options?.retentionSeconds,
1457
- delaySeconds: options?.delaySeconds,
1458
- headers: options?.headers
1459
- });
1460
- if (isDevMode()) {
1461
- triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1462
- }
1463
- return { messageId: result.messageId };
1464
- }
1465
- async function receive(topicName, consumerGroup, handler, options) {
1466
- const client = options?.client || new QueueClient();
1467
- const topic = new Topic(client, topicName);
1468
- const { client: _, ...rest } = options || {};
1469
- const { visibilityTimeoutSeconds } = rest;
1470
- const consumer = topic.consumerGroup(
1471
- consumerGroup,
1472
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
1473
- );
1474
- if (options && "messageId" in options) {
1475
- return consumer.consume(handler, { messageId: options.messageId });
1476
- } else {
1477
- const limit = options && "limit" in options ? options.limit : void 0;
1478
- return consumer.consume(handler, limit !== void 0 ? { limit } : {});
1479
- }
1480
- }
1481
1619
  export {
1482
1620
  BadRequestError,
1483
1621
  BufferTransport,
@@ -1499,10 +1637,7 @@ export {
1499
1637
  QueueEmptyError,
1500
1638
  StreamTransport,
1501
1639
  UnauthorizedError,
1502
- handleCallback,
1503
1640
  parseCallback,
1504
- parseRawCallback,
1505
- receive,
1506
- send
1641
+ parseRawCallback
1507
1642
  };
1508
1643
  //# sourceMappingURL=index.mjs.map