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