@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.
package/dist/index.js CHANGED
@@ -32,7 +32,8 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BadRequestError: () => BadRequestError,
34
34
  BufferTransport: () => BufferTransport,
35
- Client: () => Client,
35
+ CLOUD_EVENT_TYPE_V1BETA: () => CLOUD_EVENT_TYPE_V1BETA,
36
+ CLOUD_EVENT_TYPE_V2BETA: () => CLOUD_EVENT_TYPE_V2BETA,
36
37
  ConsumerDiscoveryError: () => ConsumerDiscoveryError,
37
38
  ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
38
39
  DuplicateMessageError: () => DuplicateMessageError,
@@ -45,11 +46,13 @@ __export(index_exports, {
45
46
  MessageLockedError: () => MessageLockedError,
46
47
  MessageNotAvailableError: () => MessageNotAvailableError,
47
48
  MessageNotFoundError: () => MessageNotFoundError,
49
+ QueueClient: () => QueueClient,
48
50
  QueueEmptyError: () => QueueEmptyError,
49
51
  StreamTransport: () => StreamTransport,
50
52
  UnauthorizedError: () => UnauthorizedError,
51
53
  handleCallback: () => handleCallback,
52
54
  parseCallback: () => parseCallback,
55
+ parseRawCallback: () => parseRawCallback,
53
56
  receive: () => receive,
54
57
  send: () => send
55
58
  });
@@ -233,15 +236,476 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
233
236
  }
234
237
  };
235
238
 
239
+ // src/consumer-group.ts
240
+ var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
241
+ var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
242
+ var MAX_RENEWAL_INTERVAL_SECONDS = 60;
243
+ var MIN_RENEWAL_INTERVAL_SECONDS = 10;
244
+ var RETRY_INTERVAL_MS = 3e3;
245
+ function calculateRenewalInterval(visibilityTimeoutSeconds) {
246
+ return Math.min(
247
+ MAX_RENEWAL_INTERVAL_SECONDS,
248
+ Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
249
+ );
250
+ }
251
+ var ConsumerGroup = class {
252
+ client;
253
+ topicName;
254
+ consumerGroupName;
255
+ visibilityTimeout;
256
+ /**
257
+ * Create a new ConsumerGroup instance.
258
+ *
259
+ * @param client - QueueClient instance to use for API calls (transport is configured on the client)
260
+ * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
261
+ * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
262
+ * @param options - Optional configuration
263
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
264
+ */
265
+ constructor(client, topicName, consumerGroupName, options = {}) {
266
+ this.client = client;
267
+ this.topicName = topicName;
268
+ this.consumerGroupName = consumerGroupName;
269
+ this.visibilityTimeout = Math.max(
270
+ MIN_VISIBILITY_TIMEOUT_SECONDS,
271
+ options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
272
+ );
273
+ }
274
+ /**
275
+ * Check if an error is a 4xx client error that should stop retries.
276
+ * 4xx errors indicate the request is fundamentally invalid and retrying won't help.
277
+ * - 409: Ticket mismatch (lost ownership to another consumer)
278
+ * - 404: Message/receipt handle not found
279
+ * - 400, 401, 403: Other client errors
280
+ */
281
+ isClientError(error) {
282
+ return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
283
+ error instanceof MessageNotFoundError || // 404 - receipt handle not found
284
+ error instanceof BadRequestError || // 400 - invalid parameters
285
+ error instanceof UnauthorizedError || // 401 - auth failed
286
+ error instanceof ForbiddenError;
287
+ }
288
+ /**
289
+ * Starts a background loop that periodically extends the visibility timeout for a message.
290
+ *
291
+ * Timing strategy:
292
+ * - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
293
+ * - Extensions request the same duration as the initial visibility timeout
294
+ * - When `visibilityDeadline` is provided (binary mode small body), the first
295
+ * extension delay is calculated from the time remaining until the deadline
296
+ * using the same renewal formula, ensuring the first extension fires before
297
+ * the server-assigned lease expires. Subsequent renewals use the standard interval.
298
+ *
299
+ * Retry strategy:
300
+ * - On transient failures (5xx, network errors): retry every 3 seconds
301
+ * - On 4xx client errors: stop retrying (the lease is lost or invalid)
302
+ *
303
+ * @param receiptHandle - The receipt handle to extend visibility for
304
+ * @param options - Optional configuration
305
+ * @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
306
+ * when the current visibility timeout expires. Used to calculate the first extension delay.
307
+ */
308
+ startVisibilityExtension(receiptHandle, options) {
309
+ let isRunning = true;
310
+ let isResolved = false;
311
+ let resolveLifecycle;
312
+ let timeoutId = null;
313
+ const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
314
+ let firstDelayMs = renewalIntervalMs;
315
+ if (options?.visibilityDeadline) {
316
+ const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
317
+ if (timeRemainingMs > 0) {
318
+ const timeRemainingSeconds = timeRemainingMs / 1e3;
319
+ firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
320
+ } else {
321
+ firstDelayMs = 0;
322
+ }
323
+ }
324
+ const lifecyclePromise = new Promise((resolve) => {
325
+ resolveLifecycle = resolve;
326
+ });
327
+ const safeResolve = () => {
328
+ if (!isResolved) {
329
+ isResolved = true;
330
+ resolveLifecycle();
331
+ }
332
+ };
333
+ const extend = async () => {
334
+ if (!isRunning) {
335
+ safeResolve();
336
+ return;
337
+ }
338
+ try {
339
+ await this.client.changeVisibility({
340
+ queueName: this.topicName,
341
+ consumerGroup: this.consumerGroupName,
342
+ receiptHandle,
343
+ visibilityTimeoutSeconds: this.visibilityTimeout
344
+ });
345
+ if (isRunning) {
346
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
347
+ } else {
348
+ safeResolve();
349
+ }
350
+ } catch (error) {
351
+ if (this.isClientError(error)) {
352
+ console.error(
353
+ `Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
354
+ error
355
+ );
356
+ safeResolve();
357
+ return;
358
+ }
359
+ console.error(
360
+ `Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
361
+ error
362
+ );
363
+ if (isRunning) {
364
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
365
+ } else {
366
+ safeResolve();
367
+ }
368
+ }
369
+ };
370
+ timeoutId = setTimeout(() => extend(), firstDelayMs);
371
+ return async (waitForCompletion = false) => {
372
+ isRunning = false;
373
+ if (timeoutId) {
374
+ clearTimeout(timeoutId);
375
+ timeoutId = null;
376
+ }
377
+ if (waitForCompletion) {
378
+ await lifecyclePromise;
379
+ } else {
380
+ safeResolve();
381
+ }
382
+ };
383
+ }
384
+ async processMessage(message, handler, options) {
385
+ const stopExtension = this.startVisibilityExtension(
386
+ message.receiptHandle,
387
+ options
388
+ );
389
+ 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
+ });
397
+ await stopExtension();
398
+ await this.client.deleteMessage({
399
+ queueName: this.topicName,
400
+ consumerGroup: this.consumerGroupName,
401
+ receiptHandle: message.receiptHandle
402
+ });
403
+ } catch (error) {
404
+ await stopExtension();
405
+ const transport = this.client.getTransport();
406
+ if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
407
+ try {
408
+ await transport.finalize(message.payload);
409
+ } catch (finalizeError) {
410
+ console.warn("Failed to finalize message payload:", finalizeError);
411
+ }
412
+ }
413
+ throw error;
414
+ }
415
+ }
416
+ /**
417
+ * Process a pre-fetched message directly, without calling `receiveMessageById`.
418
+ *
419
+ * Used by the binary mode (v2beta) small body fast path, where the server
420
+ * pushes the full message payload in the callback request. The message is
421
+ * processed with the same lifecycle guarantees as `consume()`:
422
+ * - Visibility timeout is extended periodically during processing
423
+ * - Message is deleted on successful handler completion
424
+ * - Payload is finalized on error if the transport supports it
425
+ *
426
+ * @param handler - Function to process the message payload and metadata
427
+ * @param message - The complete message including payload and receipt handle
428
+ * @param options - Optional configuration
429
+ * @param options.visibilityDeadline - Absolute deadline when the server-assigned
430
+ * visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
431
+ * schedule the first visibility extension before the lease expires.
432
+ */
433
+ async consumeMessage(handler, message, options) {
434
+ await this.processMessage(message, handler, options);
435
+ }
436
+ async consume(handler, options) {
437
+ if (options && "messageId" in options) {
438
+ const response = await this.client.receiveMessageById({
439
+ queueName: this.topicName,
440
+ consumerGroup: this.consumerGroupName,
441
+ messageId: options.messageId,
442
+ visibilityTimeoutSeconds: this.visibilityTimeout
443
+ });
444
+ await this.processMessage(response.message, handler);
445
+ } else {
446
+ const limit = options && "limit" in options ? options.limit : 1;
447
+ let messageFound = false;
448
+ for await (const message of this.client.receiveMessages({
449
+ queueName: this.topicName,
450
+ consumerGroup: this.consumerGroupName,
451
+ visibilityTimeoutSeconds: this.visibilityTimeout,
452
+ limit
453
+ })) {
454
+ messageFound = true;
455
+ await this.processMessage(message, handler);
456
+ }
457
+ if (!messageFound) {
458
+ await handler(null, null);
459
+ }
460
+ }
461
+ }
462
+ /**
463
+ * Get the consumer group name
464
+ */
465
+ get name() {
466
+ return this.consumerGroupName;
467
+ }
468
+ /**
469
+ * Get the topic name this consumer group is subscribed to
470
+ */
471
+ get topic() {
472
+ return this.topicName;
473
+ }
474
+ };
475
+
476
+ // src/topic.ts
477
+ var Topic = class {
478
+ client;
479
+ topicName;
480
+ /**
481
+ * Create a new Topic instance
482
+ * @param client QueueClient instance to use for API calls (transport is configured on the client)
483
+ * @param topicName Name of the topic to work with
484
+ */
485
+ constructor(client, topicName) {
486
+ this.client = client;
487
+ this.topicName = topicName;
488
+ }
489
+ /**
490
+ * Publish a message to the topic
491
+ * @param payload The data to publish
492
+ * @param options Optional publish options
493
+ * @returns An object containing the message ID
494
+ * @throws {BadRequestError} When request parameters are invalid
495
+ * @throws {UnauthorizedError} When authentication fails
496
+ * @throws {ForbiddenError} When access is denied (environment mismatch)
497
+ * @throws {InternalServerError} When server encounters an error
498
+ */
499
+ async publish(payload, options) {
500
+ const result = await this.client.sendMessage({
501
+ queueName: this.topicName,
502
+ payload,
503
+ idempotencyKey: options?.idempotencyKey,
504
+ retentionSeconds: options?.retentionSeconds,
505
+ delaySeconds: options?.delaySeconds,
506
+ headers: options?.headers
507
+ });
508
+ if (isDevMode()) {
509
+ triggerDevCallbacks(this.topicName, result.messageId);
510
+ }
511
+ return { messageId: result.messageId };
512
+ }
513
+ /**
514
+ * Create a consumer group for this topic
515
+ * @param consumerGroupName Name of the consumer group
516
+ * @param options Optional configuration for the consumer group
517
+ * @returns A ConsumerGroup instance
518
+ */
519
+ consumerGroup(consumerGroupName, options) {
520
+ return new ConsumerGroup(
521
+ this.client,
522
+ this.topicName,
523
+ consumerGroupName,
524
+ options
525
+ );
526
+ }
527
+ /**
528
+ * Get the topic name
529
+ */
530
+ get name() {
531
+ return this.topicName;
532
+ }
533
+ };
534
+
535
+ // src/callback.ts
536
+ var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
537
+ var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
538
+ function matchesWildcardPattern(topicName, pattern) {
539
+ const prefix = pattern.slice(0, -1);
540
+ return topicName.startsWith(prefix);
541
+ }
542
+ function isRecord(value) {
543
+ return typeof value === "object" && value !== null;
544
+ }
545
+ function parseV1StructuredBody(body, contentType) {
546
+ if (!contentType || !contentType.includes("application/cloudevents+json")) {
547
+ throw new Error(
548
+ "Invalid content type: expected 'application/cloudevents+json'"
549
+ );
550
+ }
551
+ if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
552
+ throw new Error("Invalid CloudEvent: missing required fields");
553
+ }
554
+ if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
555
+ throw new Error(
556
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
557
+ );
558
+ }
559
+ const { data } = body;
560
+ const missingFields = [];
561
+ if (!("queueName" in data)) missingFields.push("queueName");
562
+ if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
563
+ if (!("messageId" in data)) missingFields.push("messageId");
564
+ if (missingFields.length > 0) {
565
+ throw new Error(
566
+ `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
567
+ );
568
+ }
569
+ return {
570
+ queueName: String(data.queueName),
571
+ consumerGroup: String(data.consumerGroup),
572
+ messageId: String(data.messageId)
573
+ };
574
+ }
575
+ function getHeader(headers, name) {
576
+ if (headers instanceof Headers) {
577
+ return headers.get(name);
578
+ }
579
+ const value = headers[name];
580
+ if (Array.isArray(value)) return value[0] ?? null;
581
+ return value ?? null;
582
+ }
583
+ function parseBinaryHeaders(headers) {
584
+ const ceType = getHeader(headers, "ce-type");
585
+ if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
586
+ throw new Error(
587
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
588
+ );
589
+ }
590
+ const queueName = getHeader(headers, "ce-vqsqueuename");
591
+ const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
592
+ const messageId = getHeader(headers, "ce-vqsmessageid");
593
+ const missingFields = [];
594
+ if (!queueName) missingFields.push("ce-vqsqueuename");
595
+ if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
596
+ if (!messageId) missingFields.push("ce-vqsmessageid");
597
+ if (missingFields.length > 0) {
598
+ throw new Error(
599
+ `Missing required CloudEvent headers: ${missingFields.join(", ")}`
600
+ );
601
+ }
602
+ const base = {
603
+ queueName,
604
+ consumerGroup,
605
+ messageId
606
+ };
607
+ const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
608
+ if (!receiptHandle) {
609
+ return base;
610
+ }
611
+ const result = { ...base, receiptHandle };
612
+ const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
613
+ if (deliveryCount) {
614
+ result.deliveryCount = parseInt(deliveryCount, 10);
615
+ }
616
+ const createdAt = getHeader(headers, "ce-vqscreatedat");
617
+ if (createdAt) {
618
+ result.createdAt = createdAt;
619
+ }
620
+ const contentType = getHeader(headers, "content-type");
621
+ if (contentType) {
622
+ result.contentType = contentType;
623
+ }
624
+ const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
625
+ if (visibilityDeadline) {
626
+ result.visibilityDeadline = visibilityDeadline;
627
+ }
628
+ return result;
629
+ }
630
+ function parseRawCallback(body, headers) {
631
+ const ceType = getHeader(headers, "ce-type");
632
+ if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
633
+ const result = parseBinaryHeaders(headers);
634
+ if ("receiptHandle" in result) {
635
+ result.parsedPayload = body;
636
+ }
637
+ return result;
638
+ }
639
+ return parseV1StructuredBody(body, getHeader(headers, "content-type"));
640
+ }
641
+ async function parseCallback(request) {
642
+ const ceType = request.headers.get("ce-type");
643
+ if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
644
+ const result = parseBinaryHeaders(request.headers);
645
+ if ("receiptHandle" in result && request.body) {
646
+ result.rawBody = request.body;
647
+ }
648
+ return result;
649
+ }
650
+ let body;
651
+ try {
652
+ body = await request.json();
653
+ } catch {
654
+ throw new Error("Failed to parse CloudEvent from request body");
655
+ }
656
+ const headers = {};
657
+ request.headers.forEach((value, key) => {
658
+ headers[key] = value;
659
+ });
660
+ return parseRawCallback(body, headers);
661
+ }
662
+ async function handleCallback(handler, request, options) {
663
+ const { queueName, consumerGroup, messageId } = request;
664
+ const client = options?.client || new QueueClient();
665
+ const topic = new Topic(client, queueName);
666
+ const cg = topic.consumerGroup(
667
+ consumerGroup,
668
+ options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
669
+ );
670
+ if ("receiptHandle" in request) {
671
+ const transport = client.getTransport();
672
+ let payload;
673
+ if (request.rawBody) {
674
+ payload = await transport.deserialize(request.rawBody);
675
+ } else if (request.parsedPayload !== void 0) {
676
+ payload = request.parsedPayload;
677
+ } else {
678
+ throw new Error(
679
+ "Binary mode callback with receipt handle is missing payload"
680
+ );
681
+ }
682
+ const message = {
683
+ messageId,
684
+ payload,
685
+ deliveryCount: request.deliveryCount ?? 1,
686
+ createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
687
+ contentType: request.contentType ?? transport.contentType,
688
+ receiptHandle: request.receiptHandle
689
+ };
690
+ const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
691
+ await cg.consumeMessage(handler, message, { visibilityDeadline });
692
+ } else {
693
+ await cg.consume(handler, { messageId });
694
+ }
695
+ }
696
+
236
697
  // src/dev.ts
237
698
  var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
238
699
  function filePathToUrlPath(filePath) {
239
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
700
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
240
701
  if (!urlPath.startsWith("/")) {
241
702
  urlPath = "/" + urlPath;
242
703
  }
243
704
  return urlPath;
244
705
  }
706
+ function filePathToConsumerGroup(filePath) {
707
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
708
+ }
245
709
  function getDevRouteMappings() {
246
710
  const g = globalThis;
247
711
  if (ROUTE_MAPPINGS_KEY in g) {
@@ -262,11 +726,11 @@ function getDevRouteMappings() {
262
726
  for (const [filePath, config] of Object.entries(vercelJson.functions)) {
263
727
  if (!config.experimentalTriggers) continue;
264
728
  for (const trigger of config.experimentalTriggers) {
265
- if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
729
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
266
730
  mappings.push({
267
731
  urlPath: filePathToUrlPath(filePath),
268
732
  topic: trigger.topic,
269
- consumer: trigger.consumer
733
+ consumer: filePathToConsumerGroup(filePath)
270
734
  });
271
735
  }
272
736
  }
@@ -299,20 +763,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
299
763
  var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
300
764
  async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
301
765
  const client = new QueueClient();
302
- const transport = new JsonTransport();
303
766
  let elapsed = 0;
304
767
  let interval = DEV_VISIBILITY_POLL_INTERVAL;
305
768
  while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
306
769
  try {
307
- await client.receiveMessageById(
308
- {
309
- queueName: topicName,
310
- consumerGroup,
311
- messageId,
312
- visibilityTimeoutSeconds: 0
313
- },
314
- transport
315
- );
770
+ await client.receiveMessageById({
771
+ queueName: topicName,
772
+ consumerGroup,
773
+ messageId,
774
+ visibilityTimeoutSeconds: 0
775
+ });
316
776
  return true;
317
777
  } catch (error) {
318
778
  if (error instanceof MessageNotFoundError) {
@@ -386,26 +846,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
386
846
  console.log(
387
847
  `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
388
848
  );
389
- const cloudEvent = {
390
- type: "com.vercel.queue.v1beta",
391
- source: `/topic/${topicName}/consumer/${route.consumer}`,
392
- id: messageId,
393
- datacontenttype: "application/json",
394
- data: {
395
- messageId,
396
- queueName: topicName,
397
- consumerGroup: route.consumer
398
- },
399
- time: (/* @__PURE__ */ new Date()).toISOString(),
400
- specversion: "1.0"
401
- };
402
849
  try {
403
850
  const response = await fetch(url, {
404
851
  method: "POST",
405
852
  headers: {
406
- "Content-Type": "application/cloudevents+json"
407
- },
408
- body: JSON.stringify(cloudEvent)
853
+ "ce-type": CLOUD_EVENT_TYPE_V2BETA,
854
+ "ce-vqsqueuename": topicName,
855
+ "ce-vqsconsumergroup": route.consumer,
856
+ "ce-vqsmessageid": messageId
857
+ }
409
858
  });
410
859
  if (response.ok) {
411
860
  try {
@@ -512,6 +961,7 @@ var QueueClient = class {
512
961
  providedToken;
513
962
  defaultDeploymentId;
514
963
  pinToDeployment;
964
+ transport;
515
965
  constructor(options = {}) {
516
966
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
517
967
  this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
@@ -519,6 +969,10 @@ var QueueClient = class {
519
969
  this.providedToken = options.token;
520
970
  this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
521
971
  this.pinToDeployment = options.pinToDeployment ?? true;
972
+ this.transport = options.transport || new JsonTransport();
973
+ }
974
+ getTransport() {
975
+ return this.transport;
522
976
  }
523
977
  getSendDeploymentId() {
524
978
  if (isDevMode()) {
@@ -575,6 +1029,8 @@ var QueueClient = class {
575
1029
  }
576
1030
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
577
1031
  }
1032
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.39"}`);
1033
+ init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
578
1034
  const response = await fetch(url, init);
579
1035
  if (isDebugEnabled()) {
580
1036
  const logData = {
@@ -597,7 +1053,6 @@ var QueueClient = class {
597
1053
  * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
598
1054
  * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
599
1055
  * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
600
- * @param transport - Serializer for the payload
601
1056
  * @returns Promise with the generated messageId
602
1057
  * @throws {DuplicateMessageError} When idempotency key was already used
603
1058
  * @throws {ConsumerDiscoveryError} When consumer discovery fails
@@ -607,7 +1062,8 @@ var QueueClient = class {
607
1062
  * @throws {ForbiddenError} When access is denied
608
1063
  * @throws {InternalServerError} When server encounters an error
609
1064
  */
610
- async sendMessage(options, transport) {
1065
+ async sendMessage(options) {
1066
+ const transport = this.transport;
611
1067
  const {
612
1068
  queueName,
613
1069
  payload,
@@ -689,21 +1145,24 @@ var QueueClient = class {
689
1145
  /**
690
1146
  * Receive messages from a topic as an async generator.
691
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
+ *
692
1151
  * @param options - Receive options
693
1152
  * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
694
1153
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
695
1154
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
696
1155
  * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
697
- * @param transport - Deserializer for message payloads
698
1156
  * @yields Message objects with payload, messageId, receiptHandle, etc.
699
- * @throws {QueueEmptyError} When no messages available
1157
+ * Yields nothing if queue is empty.
700
1158
  * @throws {InvalidLimitError} When limit is outside 1-10 range
701
1159
  * @throws {BadRequestError} When parameters are invalid
702
1160
  * @throws {UnauthorizedError} When authentication fails
703
1161
  * @throws {ForbiddenError} When access is denied
704
1162
  * @throws {InternalServerError} When server encounters an error
705
1163
  */
706
- async *receiveMessages(options, transport) {
1164
+ async *receiveMessages(options) {
1165
+ const transport = this.transport;
707
1166
  const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
708
1167
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
709
1168
  throw new InvalidLimitError(limit);
@@ -734,7 +1193,7 @@ var QueueClient = class {
734
1193
  }
735
1194
  );
736
1195
  if (response.status === 204) {
737
- throw new QueueEmptyError(queueName, consumerGroup);
1196
+ return;
738
1197
  }
739
1198
  if (!response.ok) {
740
1199
  const errorText = await response.text();
@@ -775,7 +1234,6 @@ var QueueClient = class {
775
1234
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
776
1235
  * @param options.messageId - Message ID to retrieve
777
1236
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
778
- * @param transport - Deserializer for the message payload
779
1237
  * @returns Promise with the message
780
1238
  * @throws {MessageNotFoundError} When message doesn't exist
781
1239
  * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
@@ -785,7 +1243,8 @@ var QueueClient = class {
785
1243
  * @throws {ForbiddenError} When access is denied
786
1244
  * @throws {InternalServerError} When server encounters an error
787
1245
  */
788
- async receiveMessageById(options, transport) {
1246
+ async receiveMessageById(options) {
1247
+ const transport = this.transport;
789
1248
  const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
790
1249
  const headers = new Headers({
791
1250
  Authorization: `Bearer ${await this.getToken()}`,
@@ -797,212 +1256,86 @@ var QueueClient = class {
797
1256
  "Vqs-Visibility-Timeout-Seconds",
798
1257
  visibilityTimeoutSeconds.toString()
799
1258
  );
800
- }
801
- const effectiveDeploymentId = this.getConsumeDeploymentId();
802
- if (effectiveDeploymentId) {
803
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
804
- }
805
- const response = await this.fetch(
806
- this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
807
- {
808
- method: "POST",
809
- headers
810
- }
811
- );
812
- if (!response.ok) {
813
- const errorText = await response.text();
814
- if (response.status === 404) {
815
- throw new MessageNotFoundError(messageId);
816
- }
817
- if (response.status === 409) {
818
- let errorData = {};
819
- try {
820
- errorData = JSON.parse(errorText);
821
- } catch {
822
- }
823
- if (errorData.originalMessageId) {
824
- throw new MessageNotAvailableError(
825
- messageId,
826
- `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
827
- );
828
- }
829
- throw new MessageNotAvailableError(messageId);
830
- }
831
- if (response.status === 410) {
832
- throw new MessageAlreadyProcessedError(messageId);
833
- }
834
- throwCommonHttpError(
835
- response.status,
836
- response.statusText,
837
- errorText,
838
- "receive message by ID"
839
- );
840
- }
841
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
842
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
843
- if (!parsedHeaders) {
844
- await consumeStream(multipartMessage.payload);
845
- throw new MessageCorruptedError(
846
- messageId,
847
- "Missing required queue headers in response"
848
- );
849
- }
850
- const deserializedPayload = await transport.deserialize(
851
- multipartMessage.payload
852
- );
853
- const message = {
854
- ...parsedHeaders,
855
- payload: deserializedPayload
856
- };
857
- return { message };
858
- }
859
- throw new MessageNotFoundError(messageId);
860
- }
861
- /**
862
- * Delete (acknowledge) a message after successful processing.
863
- *
864
- * @param options - Delete options
865
- * @param options.queueName - Topic name
866
- * @param options.consumerGroup - Consumer group name
867
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
868
- * @returns Promise indicating deletion success
869
- * @throws {MessageNotFoundError} When receipt handle not found
870
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
871
- * @throws {BadRequestError} When parameters are invalid
872
- * @throws {UnauthorizedError} When authentication fails
873
- * @throws {ForbiddenError} When access is denied
874
- * @throws {InternalServerError} When server encounters an error
875
- */
876
- async deleteMessage(options) {
877
- const { queueName, consumerGroup, receiptHandle } = options;
878
- const headers = new Headers({
879
- Authorization: `Bearer ${await this.getToken()}`,
880
- ...this.customHeaders
881
- });
882
- const effectiveDeploymentId = this.getConsumeDeploymentId();
883
- if (effectiveDeploymentId) {
884
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
885
- }
886
- const response = await this.fetch(
887
- this.buildUrl(
888
- queueName,
889
- "consumer",
890
- consumerGroup,
891
- "lease",
892
- receiptHandle
893
- ),
894
- {
895
- method: "DELETE",
896
- headers
897
- }
898
- );
899
- if (!response.ok) {
900
- const errorText = await response.text();
901
- if (response.status === 404) {
902
- throw new MessageNotFoundError(receiptHandle);
903
- }
904
- if (response.status === 409) {
905
- throw new MessageNotAvailableError(
906
- receiptHandle,
907
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
908
- );
909
- }
910
- throwCommonHttpError(
911
- response.status,
912
- response.statusText,
913
- errorText,
914
- "delete message",
915
- "Missing or invalid receipt handle"
916
- );
917
- }
918
- return { deleted: true };
919
- }
920
- /**
921
- * Extend or change the visibility timeout of a message.
922
- * Used to prevent message redelivery while still processing.
923
- *
924
- * @param options - Visibility options
925
- * @param options.queueName - Topic name
926
- * @param options.consumerGroup - Consumer group name
927
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
928
- * @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
929
- * @returns Promise indicating success
930
- * @throws {MessageNotFoundError} When receipt handle not found
931
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
932
- * @throws {BadRequestError} When parameters are invalid
933
- * @throws {UnauthorizedError} When authentication fails
934
- * @throws {ForbiddenError} When access is denied
935
- * @throws {InternalServerError} When server encounters an error
936
- */
937
- async changeVisibility(options) {
938
- const {
939
- queueName,
940
- consumerGroup,
941
- receiptHandle,
942
- visibilityTimeoutSeconds
943
- } = options;
944
- const headers = new Headers({
945
- Authorization: `Bearer ${await this.getToken()}`,
946
- "Content-Type": "application/json",
947
- ...this.customHeaders
948
- });
1259
+ }
949
1260
  const effectiveDeploymentId = this.getConsumeDeploymentId();
950
1261
  if (effectiveDeploymentId) {
951
1262
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
952
1263
  }
953
1264
  const response = await this.fetch(
954
- this.buildUrl(
955
- queueName,
956
- "consumer",
957
- consumerGroup,
958
- "lease",
959
- receiptHandle
960
- ),
1265
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
961
1266
  {
962
- method: "PATCH",
963
- headers,
964
- body: JSON.stringify({ visibilityTimeoutSeconds })
1267
+ method: "POST",
1268
+ headers
965
1269
  }
966
1270
  );
967
1271
  if (!response.ok) {
968
1272
  const errorText = await response.text();
969
1273
  if (response.status === 404) {
970
- throw new MessageNotFoundError(receiptHandle);
1274
+ throw new MessageNotFoundError(messageId);
971
1275
  }
972
1276
  if (response.status === 409) {
973
- throw new MessageNotAvailableError(
974
- receiptHandle,
975
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
976
- );
1277
+ let errorData = {};
1278
+ try {
1279
+ errorData = JSON.parse(errorText);
1280
+ } catch {
1281
+ }
1282
+ if (errorData.originalMessageId) {
1283
+ throw new MessageNotAvailableError(
1284
+ messageId,
1285
+ `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
1286
+ );
1287
+ }
1288
+ throw new MessageNotAvailableError(messageId);
1289
+ }
1290
+ if (response.status === 410) {
1291
+ throw new MessageAlreadyProcessedError(messageId);
977
1292
  }
978
1293
  throwCommonHttpError(
979
1294
  response.status,
980
1295
  response.statusText,
981
1296
  errorText,
982
- "change visibility",
983
- "Missing receipt handle or invalid visibility timeout"
1297
+ "receive message by ID"
984
1298
  );
985
1299
  }
986
- return { success: true };
1300
+ for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
1301
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
1302
+ if (!parsedHeaders) {
1303
+ await consumeStream(multipartMessage.payload);
1304
+ throw new MessageCorruptedError(
1305
+ messageId,
1306
+ "Missing required queue headers in response"
1307
+ );
1308
+ }
1309
+ const deserializedPayload = await transport.deserialize(
1310
+ multipartMessage.payload
1311
+ );
1312
+ const message = {
1313
+ ...parsedHeaders,
1314
+ payload: deserializedPayload
1315
+ };
1316
+ return { message };
1317
+ }
1318
+ throw new MessageNotFoundError(messageId);
987
1319
  }
988
1320
  /**
989
- * Alternative endpoint for changing message visibility timeout.
990
- * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
991
- * Functionally equivalent to changeVisibility but follows an alternative API pattern.
1321
+ * Delete (acknowledge) a message after successful processing.
992
1322
  *
993
- * @param options - Options for changing visibility
994
- * @returns Promise resolving to change visibility response
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
995
1334
  */
996
- async changeVisibilityAlt(options) {
997
- const {
998
- queueName,
999
- consumerGroup,
1000
- receiptHandle,
1001
- visibilityTimeoutSeconds
1002
- } = options;
1335
+ async deleteMessage(options) {
1336
+ const { queueName, consumerGroup, receiptHandle } = options;
1003
1337
  const headers = new Headers({
1004
1338
  Authorization: `Bearer ${await this.getToken()}`,
1005
- "Content-Type": "application/json",
1006
1339
  ...this.customHeaders
1007
1340
  });
1008
1341
  const effectiveDeploymentId = this.getConsumeDeploymentId();
@@ -1015,503 +1348,202 @@ var QueueClient = class {
1015
1348
  "consumer",
1016
1349
  consumerGroup,
1017
1350
  "lease",
1018
- receiptHandle,
1019
- "visibility"
1020
- ),
1021
- {
1022
- method: "PATCH",
1023
- headers,
1024
- body: JSON.stringify({ visibilityTimeoutSeconds })
1025
- }
1026
- );
1027
- if (!response.ok) {
1028
- const errorText = await response.text();
1029
- if (response.status === 404) {
1030
- throw new MessageNotFoundError(receiptHandle);
1031
- }
1032
- if (response.status === 409) {
1033
- throw new MessageNotAvailableError(
1034
- receiptHandle,
1035
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
1036
- );
1037
- }
1038
- throwCommonHttpError(
1039
- response.status,
1040
- response.statusText,
1041
- errorText,
1042
- "change visibility (alt)",
1043
- "Missing receipt handle or invalid visibility timeout"
1044
- );
1045
- }
1046
- return { success: true };
1047
- }
1048
- };
1049
-
1050
- // src/consumer-group.ts
1051
- var ConsumerGroup = class {
1052
- client;
1053
- topicName;
1054
- consumerGroupName;
1055
- visibilityTimeout;
1056
- refreshInterval;
1057
- transport;
1058
- /**
1059
- * Create a new ConsumerGroup instance.
1060
- *
1061
- * @param client - QueueClient instance to use for API calls
1062
- * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
1063
- * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1064
- * @param options - Optional configuration
1065
- * @param options.transport - Payload serializer (default: JsonTransport)
1066
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1067
- * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
1068
- */
1069
- constructor(client, topicName, consumerGroupName, options = {}) {
1070
- this.client = client;
1071
- this.topicName = topicName;
1072
- this.consumerGroupName = consumerGroupName;
1073
- this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1074
- this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
1075
- this.transport = options.transport || new JsonTransport();
1076
- }
1077
- /**
1078
- * Starts a background loop that periodically extends the visibility timeout for a message.
1079
- */
1080
- startVisibilityExtension(receiptHandle) {
1081
- let isRunning = true;
1082
- let isResolved = false;
1083
- let resolveLifecycle;
1084
- let timeoutId = null;
1085
- const lifecyclePromise = new Promise((resolve) => {
1086
- resolveLifecycle = resolve;
1087
- });
1088
- const safeResolve = () => {
1089
- if (!isResolved) {
1090
- isResolved = true;
1091
- resolveLifecycle();
1092
- }
1093
- };
1094
- const extend = async () => {
1095
- if (!isRunning) {
1096
- safeResolve();
1097
- return;
1098
- }
1099
- try {
1100
- await this.client.changeVisibility({
1101
- queueName: this.topicName,
1102
- consumerGroup: this.consumerGroupName,
1103
- receiptHandle,
1104
- visibilityTimeoutSeconds: this.visibilityTimeout
1105
- });
1106
- if (isRunning) {
1107
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1108
- } else {
1109
- safeResolve();
1110
- }
1111
- } catch (error) {
1112
- console.error(
1113
- `Failed to extend visibility for receipt handle ${receiptHandle}:`,
1114
- error
1115
- );
1116
- safeResolve();
1117
- }
1118
- };
1119
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1120
- return async (waitForCompletion = false) => {
1121
- isRunning = false;
1122
- if (timeoutId) {
1123
- clearTimeout(timeoutId);
1124
- timeoutId = null;
1125
- }
1126
- if (waitForCompletion) {
1127
- await lifecyclePromise;
1128
- } else {
1129
- safeResolve();
1130
- }
1131
- };
1132
- }
1133
- async processMessage(message, handler) {
1134
- const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1135
- try {
1136
- await handler(message.payload, {
1137
- messageId: message.messageId,
1138
- deliveryCount: message.deliveryCount,
1139
- createdAt: message.createdAt,
1140
- topicName: this.topicName,
1141
- consumerGroup: this.consumerGroupName
1142
- });
1143
- await stopExtension();
1144
- await this.client.deleteMessage({
1145
- queueName: this.topicName,
1146
- consumerGroup: this.consumerGroupName,
1147
- receiptHandle: message.receiptHandle
1148
- });
1149
- } catch (error) {
1150
- await stopExtension();
1151
- if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
1152
- try {
1153
- await this.transport.finalize(message.payload);
1154
- } catch (finalizeError) {
1155
- console.warn("Failed to finalize message payload:", finalizeError);
1156
- }
1157
- }
1158
- throw error;
1159
- }
1160
- }
1161
- async consume(handler, options) {
1162
- if (options?.messageId) {
1163
- const response = await this.client.receiveMessageById(
1164
- {
1165
- queueName: this.topicName,
1166
- consumerGroup: this.consumerGroupName,
1167
- messageId: options.messageId,
1168
- visibilityTimeoutSeconds: this.visibilityTimeout
1169
- },
1170
- this.transport
1171
- );
1172
- await this.processMessage(response.message, handler);
1173
- } else {
1174
- let messageFound = false;
1175
- for await (const message of this.client.receiveMessages(
1176
- {
1177
- queueName: this.topicName,
1178
- consumerGroup: this.consumerGroupName,
1179
- visibilityTimeoutSeconds: this.visibilityTimeout,
1180
- limit: 1
1181
- },
1182
- this.transport
1183
- )) {
1184
- messageFound = true;
1185
- await this.processMessage(message, handler);
1186
- break;
1187
- }
1188
- if (!messageFound) {
1189
- throw new Error("No messages available");
1190
- }
1191
- }
1192
- }
1193
- /**
1194
- * Get the consumer group name
1195
- */
1196
- get name() {
1197
- return this.consumerGroupName;
1198
- }
1199
- /**
1200
- * Get the topic name this consumer group is subscribed to
1201
- */
1202
- get topic() {
1203
- return this.topicName;
1204
- }
1205
- };
1206
-
1207
- // src/topic.ts
1208
- var Topic = class {
1209
- client;
1210
- topicName;
1211
- transport;
1212
- /**
1213
- * Create a new Topic instance
1214
- * @param client QueueClient instance to use for API calls
1215
- * @param topicName Name of the topic to work with
1216
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1217
- */
1218
- constructor(client, topicName, transport) {
1219
- this.client = client;
1220
- this.topicName = topicName;
1221
- this.transport = transport || new JsonTransport();
1351
+ receiptHandle
1352
+ ),
1353
+ {
1354
+ method: "DELETE",
1355
+ headers
1356
+ }
1357
+ );
1358
+ if (!response.ok) {
1359
+ const errorText = await response.text();
1360
+ if (response.status === 404) {
1361
+ throw new MessageNotFoundError(receiptHandle);
1362
+ }
1363
+ if (response.status === 409) {
1364
+ throw new MessageNotAvailableError(
1365
+ receiptHandle,
1366
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
1367
+ );
1368
+ }
1369
+ throwCommonHttpError(
1370
+ response.status,
1371
+ response.statusText,
1372
+ errorText,
1373
+ "delete message",
1374
+ "Missing or invalid receipt handle"
1375
+ );
1376
+ }
1377
+ return { deleted: true };
1222
1378
  }
1223
1379
  /**
1224
- * Publish a message to the topic
1225
- * @param payload The data to publish
1226
- * @param options Optional publish options
1227
- * @returns An object containing the message ID
1228
- * @throws {BadRequestError} When request parameters are invalid
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
1229
1392
  * @throws {UnauthorizedError} When authentication fails
1230
- * @throws {ForbiddenError} When access is denied (environment mismatch)
1393
+ * @throws {ForbiddenError} When access is denied
1231
1394
  * @throws {InternalServerError} When server encounters an error
1232
1395
  */
1233
- async publish(payload, options) {
1234
- const result = await this.client.sendMessage(
1396
+ async changeVisibility(options) {
1397
+ const {
1398
+ queueName,
1399
+ consumerGroup,
1400
+ receiptHandle,
1401
+ visibilityTimeoutSeconds
1402
+ } = options;
1403
+ const headers = new Headers({
1404
+ Authorization: `Bearer ${await this.getToken()}`,
1405
+ "Content-Type": "application/json",
1406
+ ...this.customHeaders
1407
+ });
1408
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1409
+ if (effectiveDeploymentId) {
1410
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1411
+ }
1412
+ const response = await this.fetch(
1413
+ this.buildUrl(
1414
+ queueName,
1415
+ "consumer",
1416
+ consumerGroup,
1417
+ "lease",
1418
+ receiptHandle
1419
+ ),
1235
1420
  {
1236
- queueName: this.topicName,
1237
- payload,
1238
- idempotencyKey: options?.idempotencyKey,
1239
- retentionSeconds: options?.retentionSeconds,
1240
- delaySeconds: options?.delaySeconds,
1241
- headers: options?.headers
1242
- },
1243
- this.transport
1421
+ method: "PATCH",
1422
+ headers,
1423
+ body: JSON.stringify({ visibilityTimeoutSeconds })
1424
+ }
1244
1425
  );
1245
- if (isDevMode()) {
1246
- triggerDevCallbacks(this.topicName, result.messageId);
1426
+ if (!response.ok) {
1427
+ const errorText = await response.text();
1428
+ if (response.status === 404) {
1429
+ throw new MessageNotFoundError(receiptHandle);
1430
+ }
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
+ );
1436
+ }
1437
+ throwCommonHttpError(
1438
+ response.status,
1439
+ response.statusText,
1440
+ errorText,
1441
+ "change visibility",
1442
+ "Missing receipt handle or invalid visibility timeout"
1443
+ );
1247
1444
  }
1248
- return { messageId: result.messageId };
1249
- }
1250
- /**
1251
- * Create a consumer group for this topic
1252
- * @param consumerGroupName Name of the consumer group
1253
- * @param options Optional configuration for the consumer group
1254
- * @returns A ConsumerGroup instance
1255
- */
1256
- consumerGroup(consumerGroupName, options) {
1257
- const consumerOptions = {
1258
- ...options,
1259
- transport: options?.transport || this.transport
1260
- };
1261
- return new ConsumerGroup(
1262
- this.client,
1263
- this.topicName,
1264
- consumerGroupName,
1265
- consumerOptions
1266
- );
1267
- }
1268
- /**
1269
- * Get the topic name
1270
- */
1271
- get name() {
1272
- return this.topicName;
1445
+ return { success: true };
1273
1446
  }
1274
1447
  /**
1275
- * Get the transport used by this topic
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.
1451
+ *
1452
+ * @param options - Options for changing visibility
1453
+ * @returns Promise resolving to change visibility response
1276
1454
  */
1277
- get serializer() {
1278
- return this.transport;
1279
- }
1280
- };
1281
-
1282
- // src/callback.ts
1283
- function validateWildcardPattern(pattern) {
1284
- const firstIndex = pattern.indexOf("*");
1285
- const lastIndex = pattern.lastIndexOf("*");
1286
- if (firstIndex !== lastIndex) {
1287
- return false;
1288
- }
1289
- if (firstIndex === -1) {
1290
- return false;
1291
- }
1292
- if (firstIndex !== pattern.length - 1) {
1293
- return false;
1294
- }
1295
- return true;
1296
- }
1297
- function matchesWildcardPattern(topicName, pattern) {
1298
- const prefix = pattern.slice(0, -1);
1299
- return topicName.startsWith(prefix);
1300
- }
1301
- function findTopicHandler(queueName, handlers) {
1302
- const exactHandler = handlers[queueName];
1303
- if (exactHandler) {
1304
- return exactHandler;
1305
- }
1306
- for (const pattern in handlers) {
1307
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1308
- return handlers[pattern];
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
1466
+ });
1467
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1468
+ if (effectiveDeploymentId) {
1469
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1309
1470
  }
1310
- }
1311
- return null;
1312
- }
1313
- async function parseCallback(request) {
1314
- const contentType = request.headers.get("content-type");
1315
- if (!contentType || !contentType.includes("application/cloudevents+json")) {
1316
- throw new Error(
1317
- "Invalid content type: expected 'application/cloudevents+json'"
1318
- );
1319
- }
1320
- let cloudEvent;
1321
- try {
1322
- cloudEvent = await request.json();
1323
- } catch (error) {
1324
- throw new Error("Failed to parse CloudEvent from request body");
1325
- }
1326
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
1327
- throw new Error("Invalid CloudEvent: missing required fields");
1328
- }
1329
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1330
- throw new Error(
1331
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1332
- );
1333
- }
1334
- const missingFields = [];
1335
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1336
- if (!("consumerGroup" in cloudEvent.data))
1337
- missingFields.push("consumerGroup");
1338
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
1339
- if (missingFields.length > 0) {
1340
- throw new Error(
1341
- `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1342
- );
1343
- }
1344
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
1345
- return {
1346
- queueName,
1347
- consumerGroup,
1348
- messageId
1349
- };
1350
- }
1351
- function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1352
- for (const topicPattern in handlers) {
1353
- if (topicPattern.includes("*")) {
1354
- if (!validateWildcardPattern(topicPattern)) {
1355
- throw new Error(
1356
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1357
- );
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 })
1358
1484
  }
1359
- }
1360
- }
1361
- const routeHandler = async (request) => {
1362
- try {
1363
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
1364
- const topicHandler = findTopicHandler(queueName, handlers);
1365
- if (!topicHandler) {
1366
- const availableTopics = Object.keys(handlers).join(", ");
1367
- return Response.json(
1368
- {
1369
- error: `No handler found for topic: ${queueName}`,
1370
- availableTopics
1371
- },
1372
- { status: 404 }
1373
- );
1485
+ );
1486
+ if (!response.ok) {
1487
+ const errorText = await response.text();
1488
+ if (response.status === 404) {
1489
+ throw new MessageNotFoundError(receiptHandle);
1374
1490
  }
1375
- const consumerGroupHandler = topicHandler[consumerGroup];
1376
- if (!consumerGroupHandler) {
1377
- const availableGroups = Object.keys(topicHandler).join(", ");
1378
- return Response.json(
1379
- {
1380
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1381
- availableGroups
1382
- },
1383
- { status: 404 }
1491
+ if (response.status === 409) {
1492
+ throw new MessageNotAvailableError(
1493
+ receiptHandle,
1494
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
1384
1495
  );
1385
1496
  }
1386
- const topic = new Topic(client, queueName);
1387
- const cg = topic.consumerGroup(
1388
- consumerGroup,
1389
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1390
- );
1391
- await cg.consume(consumerGroupHandler, { messageId });
1392
- return Response.json({ status: "success" });
1393
- } catch (error) {
1394
- console.error("Queue callback error:", error);
1395
- 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"))) {
1396
- return Response.json({ error: error.message }, { status: 400 });
1397
- }
1398
- return Response.json(
1399
- { error: "Failed to process queue message" },
1400
- { status: 500 }
1497
+ throwCommonHttpError(
1498
+ response.status,
1499
+ response.statusText,
1500
+ errorText,
1501
+ "change visibility (alt)",
1502
+ "Missing receipt handle or invalid visibility timeout"
1401
1503
  );
1402
1504
  }
1403
- };
1404
- return routeHandler;
1405
- }
1406
- function handleCallback(handlers, options) {
1407
- return createCallbackHandler(
1408
- handlers,
1409
- options?.client || new QueueClient(),
1410
- options?.visibilityTimeoutSeconds
1411
- );
1412
- }
1505
+ return { success: true };
1506
+ }
1507
+ };
1413
1508
 
1414
1509
  // src/factory.ts
1415
1510
  async function send(topicName, payload, options) {
1416
- const transport = options?.transport || new JsonTransport();
1417
1511
  const client = options?.client || new QueueClient();
1418
- const result = await client.sendMessage(
1419
- {
1420
- queueName: topicName,
1421
- payload,
1422
- idempotencyKey: options?.idempotencyKey,
1423
- retentionSeconds: options?.retentionSeconds,
1424
- delaySeconds: options?.delaySeconds,
1425
- headers: options?.headers
1426
- },
1427
- transport
1428
- );
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
+ });
1429
1520
  if (isDevMode()) {
1430
1521
  triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1431
1522
  }
1432
1523
  return { messageId: result.messageId };
1433
1524
  }
1434
1525
  async function receive(topicName, consumerGroup, handler, options) {
1435
- const transport = options?.transport || new JsonTransport();
1436
1526
  const client = options?.client || new QueueClient();
1437
- const topic = new Topic(client, topicName, transport);
1438
- const { messageId, client: _, ...consumerGroupOptions } = options || {};
1439
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1440
- if (messageId) {
1441
- return consumer.consume(handler, { messageId });
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 });
1442
1536
  } else {
1443
- return consumer.consume(handler);
1537
+ const limit = options && "limit" in options ? options.limit : void 0;
1538
+ return consumer.consume(handler, limit !== void 0 ? { limit } : {});
1444
1539
  }
1445
1540
  }
1446
-
1447
- // src/queue-client.ts
1448
- var Client = class {
1449
- client;
1450
- /**
1451
- * Create a new Client
1452
- * @param options QueueClient configuration options
1453
- */
1454
- constructor(options = {}) {
1455
- this.client = new QueueClient(options);
1456
- }
1457
- /**
1458
- * Send a message to a topic
1459
- * @param topicName Name of the topic to send to
1460
- * @param payload The data to send
1461
- * @param options Optional publish options and transport
1462
- * @returns Promise with the message ID
1463
- * @throws {BadRequestError} When request parameters are invalid
1464
- * @throws {UnauthorizedError} When authentication fails
1465
- * @throws {ForbiddenError} When access is denied (environment mismatch)
1466
- * @throws {InternalServerError} When server encounters an error
1467
- */
1468
- async send(topicName, payload, options) {
1469
- return send(topicName, payload, {
1470
- ...options,
1471
- client: this.client
1472
- });
1473
- }
1474
- /**
1475
- * Create a callback handler for processing queue messages.
1476
- * Returns a Next.js route handler function that routes messages to appropriate handlers.
1477
- *
1478
- * @param handlers - Object with topic-specific handlers organized by consumer groups
1479
- * @param options - Optional configuration
1480
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1481
- * @returns A Next.js route handler function
1482
- *
1483
- * @example
1484
- * ```typescript
1485
- * // Basic usage
1486
- * export const POST = client.handleCallback({
1487
- * "user-events": {
1488
- * "welcome": (user, metadata) => console.log("Welcoming user", user),
1489
- * "analytics": (user, metadata) => console.log("Tracking user", user),
1490
- * },
1491
- * });
1492
- *
1493
- * // With custom visibility timeout
1494
- * export const POST = client.handleCallback({
1495
- * "video-processing": {
1496
- * "transcode": async (video) => await transcodeVideo(video),
1497
- * },
1498
- * }, {
1499
- * visibilityTimeoutSeconds: 300, // 5 minutes for long operations
1500
- * });
1501
- * ```
1502
- */
1503
- handleCallback(handlers, options) {
1504
- return handleCallback(handlers, {
1505
- ...options,
1506
- client: this.client
1507
- });
1508
- }
1509
- };
1510
1541
  // Annotate the CommonJS export names for ESM import in node:
1511
1542
  0 && (module.exports = {
1512
1543
  BadRequestError,
1513
1544
  BufferTransport,
1514
- Client,
1545
+ CLOUD_EVENT_TYPE_V1BETA,
1546
+ CLOUD_EVENT_TYPE_V2BETA,
1515
1547
  ConsumerDiscoveryError,
1516
1548
  ConsumerRegistryNotConfiguredError,
1517
1549
  DuplicateMessageError,
@@ -1524,11 +1556,13 @@ var Client = class {
1524
1556
  MessageLockedError,
1525
1557
  MessageNotAvailableError,
1526
1558
  MessageNotFoundError,
1559
+ QueueClient,
1527
1560
  QueueEmptyError,
1528
1561
  StreamTransport,
1529
1562
  UnauthorizedError,
1530
1563
  handleCallback,
1531
1564
  parseCallback,
1565
+ parseRawCallback,
1532
1566
  receive,
1533
1567
  send
1534
1568
  });