@vercel/queue 0.0.0-alpha.36 → 0.0.0-alpha.38

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,8 +32,8 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BadRequestError: () => BadRequestError,
34
34
  BufferTransport: () => BufferTransport,
35
- Client: () => Client,
36
- ConcurrencyLimitError: () => ConcurrencyLimitError,
35
+ CLOUD_EVENT_TYPE_V1BETA: () => CLOUD_EVENT_TYPE_V1BETA,
36
+ CLOUD_EVENT_TYPE_V2BETA: () => CLOUD_EVENT_TYPE_V2BETA,
37
37
  ConsumerDiscoveryError: () => ConsumerDiscoveryError,
38
38
  ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
39
39
  DuplicateMessageError: () => DuplicateMessageError,
@@ -46,11 +46,13 @@ __export(index_exports, {
46
46
  MessageLockedError: () => MessageLockedError,
47
47
  MessageNotAvailableError: () => MessageNotAvailableError,
48
48
  MessageNotFoundError: () => MessageNotFoundError,
49
+ QueueClient: () => QueueClient,
49
50
  QueueEmptyError: () => QueueEmptyError,
50
51
  StreamTransport: () => StreamTransport,
51
52
  UnauthorizedError: () => UnauthorizedError,
52
53
  handleCallback: () => handleCallback,
53
54
  parseCallback: () => parseCallback,
55
+ parseRawCallback: () => parseRawCallback,
54
56
  receive: () => receive,
55
57
  send: () => send
56
58
  });
@@ -211,18 +213,6 @@ var MessageAlreadyProcessedError = class extends Error {
211
213
  this.name = "MessageAlreadyProcessedError";
212
214
  }
213
215
  };
214
- var ConcurrencyLimitError = class extends Error {
215
- /** Current number of in-flight messages for this consumer group. */
216
- currentInflight;
217
- /** Maximum allowed concurrent messages (as configured). */
218
- maxConcurrency;
219
- constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
220
- super(message);
221
- this.name = "ConcurrencyLimitError";
222
- this.currentInflight = currentInflight;
223
- this.maxConcurrency = maxConcurrency;
224
- }
225
- };
226
216
  var DuplicateMessageError = class extends Error {
227
217
  idempotencyKey;
228
218
  constructor(message, idempotencyKey) {
@@ -246,239 +236,685 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
246
236
  }
247
237
  };
248
238
 
249
- // src/dev.ts
250
- var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
251
- function filePathToUrlPath(filePath) {
252
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
253
- if (!urlPath.startsWith("/")) {
254
- urlPath = "/" + urlPath;
255
- }
256
- return urlPath;
257
- }
258
- function getDevRouteMappings() {
259
- const g = globalThis;
260
- if (ROUTE_MAPPINGS_KEY in g) {
261
- return g[ROUTE_MAPPINGS_KEY] ?? null;
262
- }
263
- try {
264
- const vercelJsonPath = path.join(process.cwd(), "vercel.json");
265
- if (!fs.existsSync(vercelJsonPath)) {
266
- g[ROUTE_MAPPINGS_KEY] = null;
267
- return null;
268
- }
269
- const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
270
- if (!vercelJson.functions) {
271
- g[ROUTE_MAPPINGS_KEY] = null;
272
- return null;
273
- }
274
- const mappings = [];
275
- for (const [filePath, config] of Object.entries(vercelJson.functions)) {
276
- if (!config.experimentalTriggers) continue;
277
- for (const trigger of config.experimentalTriggers) {
278
- if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
279
- mappings.push({
280
- urlPath: filePathToUrlPath(filePath),
281
- topic: trigger.topic,
282
- consumer: trigger.consumer
283
- });
284
- }
285
- }
286
- }
287
- g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
288
- return g[ROUTE_MAPPINGS_KEY];
289
- } catch (error) {
290
- console.warn("[Dev Mode] Failed to read vercel.json:", error);
291
- g[ROUTE_MAPPINGS_KEY] = null;
292
- return null;
293
- }
294
- }
295
- function findMatchingRoutes(topicName) {
296
- const mappings = getDevRouteMappings();
297
- if (!mappings) {
298
- return [];
299
- }
300
- return mappings.filter((mapping) => {
301
- if (mapping.topic.includes("*")) {
302
- return matchesWildcardPattern(topicName, mapping.topic);
303
- }
304
- return mapping.topic === topicName;
305
- });
306
- }
307
- function isDevMode() {
308
- return process.env.NODE_ENV === "development";
309
- }
310
- var DEV_VISIBILITY_POLL_INTERVAL = 50;
311
- var DEV_VISIBILITY_MAX_WAIT = 5e3;
312
- var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
313
- async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
314
- const client = new QueueClient();
315
- const transport = new JsonTransport();
316
- let elapsed = 0;
317
- let interval = DEV_VISIBILITY_POLL_INTERVAL;
318
- while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
319
- try {
320
- await client.receiveMessageById(
321
- {
322
- queueName: topicName,
323
- consumerGroup,
324
- messageId,
325
- visibilityTimeoutSeconds: 0
326
- },
327
- transport
328
- );
329
- return true;
330
- } catch (error) {
331
- if (error instanceof MessageNotFoundError) {
332
- await new Promise((resolve) => setTimeout(resolve, interval));
333
- elapsed += interval;
334
- interval = Math.min(
335
- interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
336
- DEV_VISIBILITY_MAX_WAIT - elapsed
337
- );
338
- continue;
339
- }
340
- if (error instanceof MessageAlreadyProcessedError) {
341
- console.log(
342
- `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
343
- );
344
- return false;
345
- }
346
- console.error(
347
- `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
348
- error
349
- );
350
- return false;
351
- }
352
- }
353
- console.warn(
354
- `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
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)
355
249
  );
356
- return false;
357
250
  }
358
- function triggerDevCallbacks(topicName, messageId, delaySeconds) {
359
- if (delaySeconds && delaySeconds > 0) {
360
- console.log(
361
- `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
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
362
272
  );
363
- setTimeout(() => {
364
- triggerDevCallbacks(topicName, messageId);
365
- }, delaySeconds * 1e3);
366
- return;
367
273
  }
368
- console.log(
369
- `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
370
- );
371
- const matchingRoutes = findMatchingRoutes(topicName);
372
- if (matchingRoutes.length === 0) {
373
- console.log(
374
- `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
375
- );
376
- return;
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;
377
287
  }
378
- const consumerGroups = matchingRoutes.map((r) => r.consumer);
379
- console.log(
380
- `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
381
- );
382
- (async () => {
383
- const firstRoute = matchingRoutes[0];
384
- const isVisible = await waitForMessageVisibility(
385
- topicName,
386
- firstRoute.consumer,
387
- messageId
388
- );
389
- if (!isVisible) {
390
- console.warn(
391
- `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
392
- );
393
- return;
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
+ }
394
323
  }
395
- const port = process.env.PORT || 3e3;
396
- const baseUrl = `http://localhost:${port}`;
397
- for (const route of matchingRoutes) {
398
- const url = `${baseUrl}${route.urlPath}`;
399
- console.log(
400
- `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
401
- );
402
- const cloudEvent = {
403
- type: "com.vercel.queue.v1beta",
404
- source: `/topic/${topicName}/consumer/${route.consumer}`,
405
- id: messageId,
406
- datacontenttype: "application/json",
407
- data: {
408
- messageId,
409
- queueName: topicName,
410
- consumerGroup: route.consumer
411
- },
412
- time: (/* @__PURE__ */ new Date()).toISOString(),
413
- specversion: "1.0"
414
- };
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
+ }
415
338
  try {
416
- const response = await fetch(url, {
417
- method: "POST",
418
- headers: {
419
- "Content-Type": "application/cloudevents+json"
420
- },
421
- body: JSON.stringify(cloudEvent)
339
+ await this.client.changeVisibility({
340
+ queueName: this.topicName,
341
+ consumerGroup: this.consumerGroupName,
342
+ receiptHandle,
343
+ visibilityTimeoutSeconds: this.visibilityTimeout
422
344
  });
423
- if (response.ok) {
424
- try {
425
- const responseData = await response.json();
426
- if (responseData.status === "success") {
427
- console.log(
428
- `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
429
- );
430
- }
431
- } catch {
432
- console.warn(
433
- `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
434
- );
435
- }
345
+ if (isRunning) {
346
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
436
347
  } else {
437
- try {
438
- const errorData = await response.json();
439
- console.error(
440
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
441
- );
442
- } catch {
443
- console.error(
444
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
445
- );
446
- }
348
+ safeResolve();
447
349
  }
448
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
+ }
449
359
  console.error(
450
- `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
360
+ `Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
451
361
  error
452
362
  );
363
+ if (isRunning) {
364
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
365
+ } else {
366
+ safeResolve();
367
+ }
453
368
  }
454
- }
455
- })();
456
- }
457
- function clearDevRouteMappings() {
458
- const g = globalThis;
459
- delete g[ROUTE_MAPPINGS_KEY];
460
- }
461
- if (process.env.NODE_ENV === "test" || process.env.VITEST) {
462
- globalThis.__clearDevRouteMappings = clearDevRouteMappings;
463
- }
464
-
465
- // src/oidc.ts
466
- var import_oidc = require("@vercel/oidc");
467
-
468
- // src/client.ts
469
- function isDebugEnabled() {
470
- return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
471
- }
472
- async function consumeStream(stream) {
473
- const reader = stream.getReader();
474
- try {
475
- while (true) {
476
- const { done } = await reader.read();
477
- if (done) break;
478
- }
479
- } finally {
480
- reader.releaseLock();
481
- }
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
+
697
+ // src/dev.ts
698
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
699
+ function filePathToUrlPath(filePath) {
700
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
701
+ if (!urlPath.startsWith("/")) {
702
+ urlPath = "/" + urlPath;
703
+ }
704
+ return urlPath;
705
+ }
706
+ function filePathToConsumerGroup(filePath) {
707
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
708
+ }
709
+ function getDevRouteMappings() {
710
+ const g = globalThis;
711
+ if (ROUTE_MAPPINGS_KEY in g) {
712
+ return g[ROUTE_MAPPINGS_KEY] ?? null;
713
+ }
714
+ try {
715
+ const vercelJsonPath = path.join(process.cwd(), "vercel.json");
716
+ if (!fs.existsSync(vercelJsonPath)) {
717
+ g[ROUTE_MAPPINGS_KEY] = null;
718
+ return null;
719
+ }
720
+ const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
721
+ if (!vercelJson.functions) {
722
+ g[ROUTE_MAPPINGS_KEY] = null;
723
+ return null;
724
+ }
725
+ const mappings = [];
726
+ for (const [filePath, config] of Object.entries(vercelJson.functions)) {
727
+ if (!config.experimentalTriggers) continue;
728
+ for (const trigger of config.experimentalTriggers) {
729
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
730
+ mappings.push({
731
+ urlPath: filePathToUrlPath(filePath),
732
+ topic: trigger.topic,
733
+ consumer: filePathToConsumerGroup(filePath)
734
+ });
735
+ }
736
+ }
737
+ }
738
+ g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
739
+ return g[ROUTE_MAPPINGS_KEY];
740
+ } catch (error) {
741
+ console.warn("[Dev Mode] Failed to read vercel.json:", error);
742
+ g[ROUTE_MAPPINGS_KEY] = null;
743
+ return null;
744
+ }
745
+ }
746
+ function findMatchingRoutes(topicName) {
747
+ const mappings = getDevRouteMappings();
748
+ if (!mappings) {
749
+ return [];
750
+ }
751
+ return mappings.filter((mapping) => {
752
+ if (mapping.topic.includes("*")) {
753
+ return matchesWildcardPattern(topicName, mapping.topic);
754
+ }
755
+ return mapping.topic === topicName;
756
+ });
757
+ }
758
+ function isDevMode() {
759
+ return process.env.NODE_ENV === "development";
760
+ }
761
+ var DEV_VISIBILITY_POLL_INTERVAL = 50;
762
+ var DEV_VISIBILITY_MAX_WAIT = 5e3;
763
+ var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
764
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
765
+ const client = new QueueClient();
766
+ let elapsed = 0;
767
+ let interval = DEV_VISIBILITY_POLL_INTERVAL;
768
+ while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
769
+ try {
770
+ await client.receiveMessageById({
771
+ queueName: topicName,
772
+ consumerGroup,
773
+ messageId,
774
+ visibilityTimeoutSeconds: 0
775
+ });
776
+ return true;
777
+ } catch (error) {
778
+ if (error instanceof MessageNotFoundError) {
779
+ await new Promise((resolve) => setTimeout(resolve, interval));
780
+ elapsed += interval;
781
+ interval = Math.min(
782
+ interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
783
+ DEV_VISIBILITY_MAX_WAIT - elapsed
784
+ );
785
+ continue;
786
+ }
787
+ if (error instanceof MessageAlreadyProcessedError) {
788
+ console.log(
789
+ `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
790
+ );
791
+ return false;
792
+ }
793
+ console.error(
794
+ `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
795
+ error
796
+ );
797
+ return false;
798
+ }
799
+ }
800
+ console.warn(
801
+ `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
802
+ );
803
+ return false;
804
+ }
805
+ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
806
+ if (delaySeconds && delaySeconds > 0) {
807
+ console.log(
808
+ `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
809
+ );
810
+ setTimeout(() => {
811
+ triggerDevCallbacks(topicName, messageId);
812
+ }, delaySeconds * 1e3);
813
+ return;
814
+ }
815
+ console.log(
816
+ `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
817
+ );
818
+ const matchingRoutes = findMatchingRoutes(topicName);
819
+ if (matchingRoutes.length === 0) {
820
+ console.log(
821
+ `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
822
+ );
823
+ return;
824
+ }
825
+ const consumerGroups = matchingRoutes.map((r) => r.consumer);
826
+ console.log(
827
+ `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
828
+ );
829
+ (async () => {
830
+ const firstRoute = matchingRoutes[0];
831
+ const isVisible = await waitForMessageVisibility(
832
+ topicName,
833
+ firstRoute.consumer,
834
+ messageId
835
+ );
836
+ if (!isVisible) {
837
+ console.warn(
838
+ `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
839
+ );
840
+ return;
841
+ }
842
+ const port = process.env.PORT || 3e3;
843
+ const baseUrl = `http://localhost:${port}`;
844
+ for (const route of matchingRoutes) {
845
+ const url = `${baseUrl}${route.urlPath}`;
846
+ console.log(
847
+ `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
848
+ );
849
+ try {
850
+ const response = await fetch(url, {
851
+ method: "POST",
852
+ headers: {
853
+ "ce-type": CLOUD_EVENT_TYPE_V2BETA,
854
+ "ce-vqsqueuename": topicName,
855
+ "ce-vqsconsumergroup": route.consumer,
856
+ "ce-vqsmessageid": messageId
857
+ }
858
+ });
859
+ if (response.ok) {
860
+ try {
861
+ const responseData = await response.json();
862
+ if (responseData.status === "success") {
863
+ console.log(
864
+ `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
865
+ );
866
+ }
867
+ } catch {
868
+ console.warn(
869
+ `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
870
+ );
871
+ }
872
+ } else {
873
+ try {
874
+ const errorData = await response.json();
875
+ console.error(
876
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
877
+ );
878
+ } catch {
879
+ console.error(
880
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
881
+ );
882
+ }
883
+ }
884
+ } catch (error) {
885
+ console.error(
886
+ `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
887
+ error
888
+ );
889
+ }
890
+ }
891
+ })();
892
+ }
893
+ function clearDevRouteMappings() {
894
+ const g = globalThis;
895
+ delete g[ROUTE_MAPPINGS_KEY];
896
+ }
897
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
898
+ globalThis.__clearDevRouteMappings = clearDevRouteMappings;
899
+ }
900
+
901
+ // src/oidc.ts
902
+ var import_oidc = require("@vercel/oidc");
903
+
904
+ // src/client.ts
905
+ function isDebugEnabled() {
906
+ return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
907
+ }
908
+ async function consumeStream(stream) {
909
+ const reader = stream.getReader();
910
+ try {
911
+ while (true) {
912
+ const { done } = await reader.read();
913
+ if (done) break;
914
+ }
915
+ } finally {
916
+ reader.releaseLock();
917
+ }
482
918
  }
483
919
  function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
484
920
  if (status === 400) {
@@ -525,6 +961,7 @@ var QueueClient = class {
525
961
  providedToken;
526
962
  defaultDeploymentId;
527
963
  pinToDeployment;
964
+ transport;
528
965
  constructor(options = {}) {
529
966
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
530
967
  this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
@@ -532,6 +969,10 @@ var QueueClient = class {
532
969
  this.providedToken = options.token;
533
970
  this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
534
971
  this.pinToDeployment = options.pinToDeployment ?? true;
972
+ this.transport = options.transport || new JsonTransport();
973
+ }
974
+ getTransport() {
975
+ return this.transport;
535
976
  }
536
977
  getSendDeploymentId() {
537
978
  if (isDevMode()) {
@@ -588,6 +1029,8 @@ var QueueClient = class {
588
1029
  }
589
1030
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
590
1031
  }
1032
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.38"}`);
1033
+ init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
591
1034
  const response = await fetch(url, init);
592
1035
  if (isDebugEnabled()) {
593
1036
  const logData = {
@@ -610,7 +1053,6 @@ var QueueClient = class {
610
1053
  * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
611
1054
  * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
612
1055
  * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
613
- * @param transport - Serializer for the payload
614
1056
  * @returns Promise with the generated messageId
615
1057
  * @throws {DuplicateMessageError} When idempotency key was already used
616
1058
  * @throws {ConsumerDiscoveryError} When consumer discovery fails
@@ -620,7 +1062,8 @@ var QueueClient = class {
620
1062
  * @throws {ForbiddenError} When access is denied
621
1063
  * @throws {InternalServerError} When server encounters an error
622
1064
  */
623
- async sendMessage(options, transport) {
1065
+ async sendMessage(options) {
1066
+ const transport = this.transport;
624
1067
  const {
625
1068
  queueName,
626
1069
  payload,
@@ -702,30 +1145,25 @@ var QueueClient = class {
702
1145
  /**
703
1146
  * Receive messages from a topic as an async generator.
704
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
+ *
705
1151
  * @param options - Receive options
706
1152
  * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
707
1153
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
708
1154
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
709
1155
  * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
710
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
711
- * @param transport - Deserializer for message payloads
712
1156
  * @yields Message objects with payload, messageId, receiptHandle, etc.
713
- * @throws {QueueEmptyError} When no messages available
1157
+ * Yields nothing if queue is empty.
714
1158
  * @throws {InvalidLimitError} When limit is outside 1-10 range
715
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
716
1159
  * @throws {BadRequestError} When parameters are invalid
717
1160
  * @throws {UnauthorizedError} When authentication fails
718
1161
  * @throws {ForbiddenError} When access is denied
719
1162
  * @throws {InternalServerError} When server encounters an error
720
1163
  */
721
- async *receiveMessages(options, transport) {
722
- const {
723
- queueName,
724
- consumerGroup,
725
- visibilityTimeoutSeconds,
726
- limit,
727
- maxConcurrency
728
- } = options;
1164
+ async *receiveMessages(options) {
1165
+ const transport = this.transport;
1166
+ const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
729
1167
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
730
1168
  throw new InvalidLimitError(limit);
731
1169
  }
@@ -743,9 +1181,6 @@ var QueueClient = class {
743
1181
  if (limit !== void 0) {
744
1182
  headers.set("Vqs-Max-Messages", limit.toString());
745
1183
  }
746
- if (maxConcurrency !== void 0) {
747
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
748
- }
749
1184
  const effectiveDeploymentId = this.getConsumeDeploymentId();
750
1185
  if (effectiveDeploymentId) {
751
1186
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
@@ -758,22 +1193,10 @@ var QueueClient = class {
758
1193
  }
759
1194
  );
760
1195
  if (response.status === 204) {
761
- throw new QueueEmptyError(queueName, consumerGroup);
1196
+ return;
762
1197
  }
763
1198
  if (!response.ok) {
764
1199
  const errorText = await response.text();
765
- if (response.status === 429) {
766
- let errorData = {};
767
- try {
768
- errorData = JSON.parse(errorText);
769
- } catch {
770
- }
771
- throw new ConcurrencyLimitError(
772
- errorData.error || "Concurrency limit exceeded or throttled",
773
- errorData.currentInflight,
774
- errorData.maxConcurrency
775
- );
776
- }
777
1200
  throwCommonHttpError(
778
1201
  response.status,
779
1202
  response.statusText,
@@ -811,26 +1234,18 @@ var QueueClient = class {
811
1234
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
812
1235
  * @param options.messageId - Message ID to retrieve
813
1236
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
814
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
815
- * @param transport - Deserializer for the message payload
816
1237
  * @returns Promise with the message
817
1238
  * @throws {MessageNotFoundError} When message doesn't exist
818
1239
  * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
819
1240
  * @throws {MessageAlreadyProcessedError} When message was already processed
820
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
821
1241
  * @throws {BadRequestError} When parameters are invalid
822
1242
  * @throws {UnauthorizedError} When authentication fails
823
1243
  * @throws {ForbiddenError} When access is denied
824
1244
  * @throws {InternalServerError} When server encounters an error
825
1245
  */
826
- async receiveMessageById(options, transport) {
827
- const {
828
- queueName,
829
- consumerGroup,
830
- messageId,
831
- visibilityTimeoutSeconds,
832
- maxConcurrency
833
- } = options;
1246
+ async receiveMessageById(options) {
1247
+ const transport = this.transport;
1248
+ const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
834
1249
  const headers = new Headers({
835
1250
  Authorization: `Bearer ${await this.getToken()}`,
836
1251
  Accept: "multipart/mixed",
@@ -841,227 +1256,86 @@ var QueueClient = class {
841
1256
  "Vqs-Visibility-Timeout-Seconds",
842
1257
  visibilityTimeoutSeconds.toString()
843
1258
  );
844
- }
845
- if (maxConcurrency !== void 0) {
846
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
847
- }
848
- const effectiveDeploymentId = this.getConsumeDeploymentId();
849
- if (effectiveDeploymentId) {
850
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
851
- }
852
- const response = await this.fetch(
853
- this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
854
- {
855
- method: "POST",
856
- headers
857
- }
858
- );
859
- if (!response.ok) {
860
- const errorText = await response.text();
861
- if (response.status === 404) {
862
- throw new MessageNotFoundError(messageId);
863
- }
864
- if (response.status === 409) {
865
- let errorData = {};
866
- try {
867
- errorData = JSON.parse(errorText);
868
- } catch {
869
- }
870
- if (errorData.originalMessageId) {
871
- throw new MessageNotAvailableError(
872
- messageId,
873
- `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
874
- );
875
- }
876
- throw new MessageNotAvailableError(messageId);
877
- }
878
- if (response.status === 410) {
879
- throw new MessageAlreadyProcessedError(messageId);
880
- }
881
- if (response.status === 429) {
882
- let errorData = {};
883
- try {
884
- errorData = JSON.parse(errorText);
885
- } catch {
886
- }
887
- throw new ConcurrencyLimitError(
888
- errorData.error || "Concurrency limit exceeded or throttled",
889
- errorData.currentInflight,
890
- errorData.maxConcurrency
891
- );
892
- }
893
- throwCommonHttpError(
894
- response.status,
895
- response.statusText,
896
- errorText,
897
- "receive message by ID"
898
- );
899
- }
900
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
901
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
902
- if (!parsedHeaders) {
903
- await consumeStream(multipartMessage.payload);
904
- throw new MessageCorruptedError(
905
- messageId,
906
- "Missing required queue headers in response"
907
- );
908
- }
909
- const deserializedPayload = await transport.deserialize(
910
- multipartMessage.payload
911
- );
912
- const message = {
913
- ...parsedHeaders,
914
- payload: deserializedPayload
915
- };
916
- return { message };
917
- }
918
- throw new MessageNotFoundError(messageId);
919
- }
920
- /**
921
- * Delete (acknowledge) a message after successful processing.
922
- *
923
- * @param options - Delete options
924
- * @param options.queueName - Topic name
925
- * @param options.consumerGroup - Consumer group name
926
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
927
- * @returns Promise indicating deletion success
928
- * @throws {MessageNotFoundError} When receipt handle not found
929
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
930
- * @throws {BadRequestError} When parameters are invalid
931
- * @throws {UnauthorizedError} When authentication fails
932
- * @throws {ForbiddenError} When access is denied
933
- * @throws {InternalServerError} When server encounters an error
934
- */
935
- async deleteMessage(options) {
936
- const { queueName, consumerGroup, receiptHandle } = options;
937
- const headers = new Headers({
938
- Authorization: `Bearer ${await this.getToken()}`,
939
- ...this.customHeaders
940
- });
941
- const effectiveDeploymentId = this.getConsumeDeploymentId();
942
- if (effectiveDeploymentId) {
943
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
944
- }
945
- const response = await this.fetch(
946
- this.buildUrl(
947
- queueName,
948
- "consumer",
949
- consumerGroup,
950
- "lease",
951
- receiptHandle
952
- ),
953
- {
954
- method: "DELETE",
955
- headers
956
- }
957
- );
958
- if (!response.ok) {
959
- const errorText = await response.text();
960
- if (response.status === 404) {
961
- throw new MessageNotFoundError(receiptHandle);
962
- }
963
- if (response.status === 409) {
964
- throw new MessageNotAvailableError(
965
- receiptHandle,
966
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
967
- );
968
- }
969
- throwCommonHttpError(
970
- response.status,
971
- response.statusText,
972
- errorText,
973
- "delete message",
974
- "Missing or invalid receipt handle"
975
- );
976
- }
977
- return { deleted: true };
978
- }
979
- /**
980
- * Extend or change the visibility timeout of a message.
981
- * Used to prevent message redelivery while still processing.
982
- *
983
- * @param options - Visibility options
984
- * @param options.queueName - Topic name
985
- * @param options.consumerGroup - Consumer group name
986
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
987
- * @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
988
- * @returns Promise indicating success
989
- * @throws {MessageNotFoundError} When receipt handle not found
990
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
991
- * @throws {BadRequestError} When parameters are invalid
992
- * @throws {UnauthorizedError} When authentication fails
993
- * @throws {ForbiddenError} When access is denied
994
- * @throws {InternalServerError} When server encounters an error
995
- */
996
- async changeVisibility(options) {
997
- const {
998
- queueName,
999
- consumerGroup,
1000
- receiptHandle,
1001
- visibilityTimeoutSeconds
1002
- } = options;
1003
- const headers = new Headers({
1004
- Authorization: `Bearer ${await this.getToken()}`,
1005
- "Content-Type": "application/json",
1006
- ...this.customHeaders
1007
- });
1259
+ }
1008
1260
  const effectiveDeploymentId = this.getConsumeDeploymentId();
1009
1261
  if (effectiveDeploymentId) {
1010
1262
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1011
1263
  }
1012
1264
  const response = await this.fetch(
1013
- this.buildUrl(
1014
- queueName,
1015
- "consumer",
1016
- consumerGroup,
1017
- "lease",
1018
- receiptHandle
1019
- ),
1265
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
1020
1266
  {
1021
- method: "PATCH",
1022
- headers,
1023
- body: JSON.stringify({ visibilityTimeoutSeconds })
1267
+ method: "POST",
1268
+ headers
1024
1269
  }
1025
1270
  );
1026
1271
  if (!response.ok) {
1027
1272
  const errorText = await response.text();
1028
1273
  if (response.status === 404) {
1029
- throw new MessageNotFoundError(receiptHandle);
1274
+ throw new MessageNotFoundError(messageId);
1030
1275
  }
1031
1276
  if (response.status === 409) {
1032
- throw new MessageNotAvailableError(
1033
- receiptHandle,
1034
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
1035
- );
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);
1036
1292
  }
1037
1293
  throwCommonHttpError(
1038
1294
  response.status,
1039
1295
  response.statusText,
1040
1296
  errorText,
1041
- "change visibility",
1042
- "Missing receipt handle or invalid visibility timeout"
1297
+ "receive message by ID"
1043
1298
  );
1044
1299
  }
1045
- 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);
1046
1319
  }
1047
1320
  /**
1048
- * Alternative endpoint for changing message visibility timeout.
1049
- * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
1050
- * Functionally equivalent to changeVisibility but follows an alternative API pattern.
1321
+ * Delete (acknowledge) a message after successful processing.
1051
1322
  *
1052
- * @param options - Options for changing visibility
1053
- * @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
1054
1334
  */
1055
- async changeVisibilityAlt(options) {
1056
- const {
1057
- queueName,
1058
- consumerGroup,
1059
- receiptHandle,
1060
- visibilityTimeoutSeconds
1061
- } = options;
1335
+ async deleteMessage(options) {
1336
+ const { queueName, consumerGroup, receiptHandle } = options;
1062
1337
  const headers = new Headers({
1063
1338
  Authorization: `Bearer ${await this.getToken()}`,
1064
- "Content-Type": "application/json",
1065
1339
  ...this.customHeaders
1066
1340
  });
1067
1341
  const effectiveDeploymentId = this.getConsumeDeploymentId();
@@ -1074,504 +1348,202 @@ var QueueClient = class {
1074
1348
  "consumer",
1075
1349
  consumerGroup,
1076
1350
  "lease",
1077
- receiptHandle,
1078
- "visibility"
1079
- ),
1080
- {
1081
- method: "PATCH",
1082
- headers,
1083
- body: JSON.stringify({ visibilityTimeoutSeconds })
1084
- }
1085
- );
1086
- if (!response.ok) {
1087
- const errorText = await response.text();
1088
- if (response.status === 404) {
1089
- throw new MessageNotFoundError(receiptHandle);
1090
- }
1091
- if (response.status === 409) {
1092
- throw new MessageNotAvailableError(
1093
- receiptHandle,
1094
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
1095
- );
1096
- }
1097
- throwCommonHttpError(
1098
- response.status,
1099
- response.statusText,
1100
- errorText,
1101
- "change visibility (alt)",
1102
- "Missing receipt handle or invalid visibility timeout"
1103
- );
1104
- }
1105
- return { success: true };
1106
- }
1107
- };
1108
-
1109
- // src/consumer-group.ts
1110
- var ConsumerGroup = class {
1111
- client;
1112
- topicName;
1113
- consumerGroupName;
1114
- visibilityTimeout;
1115
- refreshInterval;
1116
- transport;
1117
- /**
1118
- * Create a new ConsumerGroup instance.
1119
- *
1120
- * @param client - QueueClient instance to use for API calls
1121
- * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
1122
- * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1123
- * @param options - Optional configuration
1124
- * @param options.transport - Payload serializer (default: JsonTransport)
1125
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1126
- * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
1127
- */
1128
- constructor(client, topicName, consumerGroupName, options = {}) {
1129
- this.client = client;
1130
- this.topicName = topicName;
1131
- this.consumerGroupName = consumerGroupName;
1132
- this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1133
- this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
1134
- this.transport = options.transport || new JsonTransport();
1135
- }
1136
- /**
1137
- * Starts a background loop that periodically extends the visibility timeout for a message.
1138
- */
1139
- startVisibilityExtension(receiptHandle) {
1140
- let isRunning = true;
1141
- let isResolved = false;
1142
- let resolveLifecycle;
1143
- let timeoutId = null;
1144
- const lifecyclePromise = new Promise((resolve) => {
1145
- resolveLifecycle = resolve;
1146
- });
1147
- const safeResolve = () => {
1148
- if (!isResolved) {
1149
- isResolved = true;
1150
- resolveLifecycle();
1151
- }
1152
- };
1153
- const extend = async () => {
1154
- if (!isRunning) {
1155
- safeResolve();
1156
- return;
1157
- }
1158
- try {
1159
- await this.client.changeVisibility({
1160
- queueName: this.topicName,
1161
- consumerGroup: this.consumerGroupName,
1162
- receiptHandle,
1163
- visibilityTimeoutSeconds: this.visibilityTimeout
1164
- });
1165
- if (isRunning) {
1166
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1167
- } else {
1168
- safeResolve();
1169
- }
1170
- } catch (error) {
1171
- console.error(
1172
- `Failed to extend visibility for receipt handle ${receiptHandle}:`,
1173
- error
1174
- );
1175
- safeResolve();
1176
- }
1177
- };
1178
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1179
- return async (waitForCompletion = false) => {
1180
- isRunning = false;
1181
- if (timeoutId) {
1182
- clearTimeout(timeoutId);
1183
- timeoutId = null;
1184
- }
1185
- if (waitForCompletion) {
1186
- await lifecyclePromise;
1187
- } else {
1188
- safeResolve();
1189
- }
1190
- };
1191
- }
1192
- async processMessage(message, handler) {
1193
- const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1194
- try {
1195
- await handler(message.payload, {
1196
- messageId: message.messageId,
1197
- deliveryCount: message.deliveryCount,
1198
- createdAt: message.createdAt,
1199
- topicName: this.topicName,
1200
- consumerGroup: this.consumerGroupName
1201
- });
1202
- await stopExtension();
1203
- await this.client.deleteMessage({
1204
- queueName: this.topicName,
1205
- consumerGroup: this.consumerGroupName,
1206
- receiptHandle: message.receiptHandle
1207
- });
1208
- } catch (error) {
1209
- await stopExtension();
1210
- if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
1211
- try {
1212
- await this.transport.finalize(message.payload);
1213
- } catch (finalizeError) {
1214
- console.warn("Failed to finalize message payload:", finalizeError);
1215
- }
1216
- }
1217
- throw error;
1218
- }
1219
- }
1220
- async consume(handler, options) {
1221
- if (options?.messageId) {
1222
- const response = await this.client.receiveMessageById(
1223
- {
1224
- queueName: this.topicName,
1225
- consumerGroup: this.consumerGroupName,
1226
- messageId: options.messageId,
1227
- visibilityTimeoutSeconds: this.visibilityTimeout
1228
- },
1229
- this.transport
1230
- );
1231
- await this.processMessage(response.message, handler);
1232
- } else {
1233
- let messageFound = false;
1234
- for await (const message of this.client.receiveMessages(
1235
- {
1236
- queueName: this.topicName,
1237
- consumerGroup: this.consumerGroupName,
1238
- visibilityTimeoutSeconds: this.visibilityTimeout,
1239
- limit: 1
1240
- },
1241
- this.transport
1242
- )) {
1243
- messageFound = true;
1244
- await this.processMessage(message, handler);
1245
- break;
1246
- }
1247
- if (!messageFound) {
1248
- throw new Error("No messages available");
1249
- }
1250
- }
1251
- }
1252
- /**
1253
- * Get the consumer group name
1254
- */
1255
- get name() {
1256
- return this.consumerGroupName;
1257
- }
1258
- /**
1259
- * Get the topic name this consumer group is subscribed to
1260
- */
1261
- get topic() {
1262
- return this.topicName;
1263
- }
1264
- };
1265
-
1266
- // src/topic.ts
1267
- var Topic = class {
1268
- client;
1269
- topicName;
1270
- transport;
1271
- /**
1272
- * Create a new Topic instance
1273
- * @param client QueueClient instance to use for API calls
1274
- * @param topicName Name of the topic to work with
1275
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1276
- */
1277
- constructor(client, topicName, transport) {
1278
- this.client = client;
1279
- this.topicName = topicName;
1280
- 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 };
1281
1378
  }
1282
1379
  /**
1283
- * Publish a message to the topic
1284
- * @param payload The data to publish
1285
- * @param options Optional publish options
1286
- * @returns An object containing the message ID
1287
- * @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
1288
1392
  * @throws {UnauthorizedError} When authentication fails
1289
- * @throws {ForbiddenError} When access is denied (environment mismatch)
1393
+ * @throws {ForbiddenError} When access is denied
1290
1394
  * @throws {InternalServerError} When server encounters an error
1291
1395
  */
1292
- async publish(payload, options) {
1293
- 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
+ ),
1294
1420
  {
1295
- queueName: this.topicName,
1296
- payload,
1297
- idempotencyKey: options?.idempotencyKey,
1298
- retentionSeconds: options?.retentionSeconds,
1299
- delaySeconds: options?.delaySeconds,
1300
- headers: options?.headers
1301
- },
1302
- this.transport
1421
+ method: "PATCH",
1422
+ headers,
1423
+ body: JSON.stringify({ visibilityTimeoutSeconds })
1424
+ }
1303
1425
  );
1304
- if (isDevMode()) {
1305
- 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
+ );
1306
1444
  }
1307
- return { messageId: result.messageId };
1308
- }
1309
- /**
1310
- * Create a consumer group for this topic
1311
- * @param consumerGroupName Name of the consumer group
1312
- * @param options Optional configuration for the consumer group
1313
- * @returns A ConsumerGroup instance
1314
- */
1315
- consumerGroup(consumerGroupName, options) {
1316
- const consumerOptions = {
1317
- ...options,
1318
- transport: options?.transport || this.transport
1319
- };
1320
- return new ConsumerGroup(
1321
- this.client,
1322
- this.topicName,
1323
- consumerGroupName,
1324
- consumerOptions
1325
- );
1326
- }
1327
- /**
1328
- * Get the topic name
1329
- */
1330
- get name() {
1331
- return this.topicName;
1445
+ return { success: true };
1332
1446
  }
1333
1447
  /**
1334
- * 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
1335
1454
  */
1336
- get serializer() {
1337
- return this.transport;
1338
- }
1339
- };
1340
-
1341
- // src/callback.ts
1342
- function validateWildcardPattern(pattern) {
1343
- const firstIndex = pattern.indexOf("*");
1344
- const lastIndex = pattern.lastIndexOf("*");
1345
- if (firstIndex !== lastIndex) {
1346
- return false;
1347
- }
1348
- if (firstIndex === -1) {
1349
- return false;
1350
- }
1351
- if (firstIndex !== pattern.length - 1) {
1352
- return false;
1353
- }
1354
- return true;
1355
- }
1356
- function matchesWildcardPattern(topicName, pattern) {
1357
- const prefix = pattern.slice(0, -1);
1358
- return topicName.startsWith(prefix);
1359
- }
1360
- function findTopicHandler(queueName, handlers) {
1361
- const exactHandler = handlers[queueName];
1362
- if (exactHandler) {
1363
- return exactHandler;
1364
- }
1365
- for (const pattern in handlers) {
1366
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1367
- 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);
1368
1470
  }
1369
- }
1370
- return null;
1371
- }
1372
- async function parseCallback(request) {
1373
- const contentType = request.headers.get("content-type");
1374
- if (!contentType || !contentType.includes("application/cloudevents+json")) {
1375
- throw new Error(
1376
- "Invalid content type: expected 'application/cloudevents+json'"
1377
- );
1378
- }
1379
- let cloudEvent;
1380
- try {
1381
- cloudEvent = await request.json();
1382
- } catch (error) {
1383
- throw new Error("Failed to parse CloudEvent from request body");
1384
- }
1385
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
1386
- throw new Error("Invalid CloudEvent: missing required fields");
1387
- }
1388
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1389
- throw new Error(
1390
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1391
- );
1392
- }
1393
- const missingFields = [];
1394
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1395
- if (!("consumerGroup" in cloudEvent.data))
1396
- missingFields.push("consumerGroup");
1397
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
1398
- if (missingFields.length > 0) {
1399
- throw new Error(
1400
- `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1401
- );
1402
- }
1403
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
1404
- return {
1405
- queueName,
1406
- consumerGroup,
1407
- messageId
1408
- };
1409
- }
1410
- function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1411
- for (const topicPattern in handlers) {
1412
- if (topicPattern.includes("*")) {
1413
- if (!validateWildcardPattern(topicPattern)) {
1414
- throw new Error(
1415
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1416
- );
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 })
1417
1484
  }
1418
- }
1419
- }
1420
- const routeHandler = async (request) => {
1421
- try {
1422
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
1423
- const topicHandler = findTopicHandler(queueName, handlers);
1424
- if (!topicHandler) {
1425
- const availableTopics = Object.keys(handlers).join(", ");
1426
- return Response.json(
1427
- {
1428
- error: `No handler found for topic: ${queueName}`,
1429
- availableTopics
1430
- },
1431
- { status: 404 }
1432
- );
1485
+ );
1486
+ if (!response.ok) {
1487
+ const errorText = await response.text();
1488
+ if (response.status === 404) {
1489
+ throw new MessageNotFoundError(receiptHandle);
1433
1490
  }
1434
- const consumerGroupHandler = topicHandler[consumerGroup];
1435
- if (!consumerGroupHandler) {
1436
- const availableGroups = Object.keys(topicHandler).join(", ");
1437
- return Response.json(
1438
- {
1439
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1440
- availableGroups
1441
- },
1442
- { 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"
1443
1495
  );
1444
1496
  }
1445
- const topic = new Topic(client, queueName);
1446
- const cg = topic.consumerGroup(
1447
- consumerGroup,
1448
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1449
- );
1450
- await cg.consume(consumerGroupHandler, { messageId });
1451
- return Response.json({ status: "success" });
1452
- } catch (error) {
1453
- console.error("Queue callback error:", error);
1454
- 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"))) {
1455
- return Response.json({ error: error.message }, { status: 400 });
1456
- }
1457
- return Response.json(
1458
- { error: "Failed to process queue message" },
1459
- { status: 500 }
1497
+ throwCommonHttpError(
1498
+ response.status,
1499
+ response.statusText,
1500
+ errorText,
1501
+ "change visibility (alt)",
1502
+ "Missing receipt handle or invalid visibility timeout"
1460
1503
  );
1461
1504
  }
1462
- };
1463
- return routeHandler;
1464
- }
1465
- function handleCallback(handlers, options) {
1466
- return createCallbackHandler(
1467
- handlers,
1468
- options?.client || new QueueClient(),
1469
- options?.visibilityTimeoutSeconds
1470
- );
1471
- }
1505
+ return { success: true };
1506
+ }
1507
+ };
1472
1508
 
1473
1509
  // src/factory.ts
1474
1510
  async function send(topicName, payload, options) {
1475
- const transport = options?.transport || new JsonTransport();
1476
1511
  const client = options?.client || new QueueClient();
1477
- const result = await client.sendMessage(
1478
- {
1479
- queueName: topicName,
1480
- payload,
1481
- idempotencyKey: options?.idempotencyKey,
1482
- retentionSeconds: options?.retentionSeconds,
1483
- delaySeconds: options?.delaySeconds,
1484
- headers: options?.headers
1485
- },
1486
- transport
1487
- );
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
+ });
1488
1520
  if (isDevMode()) {
1489
1521
  triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1490
1522
  }
1491
1523
  return { messageId: result.messageId };
1492
1524
  }
1493
1525
  async function receive(topicName, consumerGroup, handler, options) {
1494
- const transport = options?.transport || new JsonTransport();
1495
1526
  const client = options?.client || new QueueClient();
1496
- const topic = new Topic(client, topicName, transport);
1497
- const { messageId, client: _, ...consumerGroupOptions } = options || {};
1498
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1499
- if (messageId) {
1500
- 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 });
1501
1536
  } else {
1502
- return consumer.consume(handler);
1537
+ const limit = options && "limit" in options ? options.limit : void 0;
1538
+ return consumer.consume(handler, limit !== void 0 ? { limit } : {});
1503
1539
  }
1504
1540
  }
1505
-
1506
- // src/queue-client.ts
1507
- var Client = class {
1508
- client;
1509
- /**
1510
- * Create a new Client
1511
- * @param options QueueClient configuration options
1512
- */
1513
- constructor(options = {}) {
1514
- this.client = new QueueClient(options);
1515
- }
1516
- /**
1517
- * Send a message to a topic
1518
- * @param topicName Name of the topic to send to
1519
- * @param payload The data to send
1520
- * @param options Optional publish options and transport
1521
- * @returns Promise with the message ID
1522
- * @throws {BadRequestError} When request parameters are invalid
1523
- * @throws {UnauthorizedError} When authentication fails
1524
- * @throws {ForbiddenError} When access is denied (environment mismatch)
1525
- * @throws {InternalServerError} When server encounters an error
1526
- */
1527
- async send(topicName, payload, options) {
1528
- return send(topicName, payload, {
1529
- ...options,
1530
- client: this.client
1531
- });
1532
- }
1533
- /**
1534
- * Create a callback handler for processing queue messages.
1535
- * Returns a Next.js route handler function that routes messages to appropriate handlers.
1536
- *
1537
- * @param handlers - Object with topic-specific handlers organized by consumer groups
1538
- * @param options - Optional configuration
1539
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1540
- * @returns A Next.js route handler function
1541
- *
1542
- * @example
1543
- * ```typescript
1544
- * // Basic usage
1545
- * export const POST = client.handleCallback({
1546
- * "user-events": {
1547
- * "welcome": (user, metadata) => console.log("Welcoming user", user),
1548
- * "analytics": (user, metadata) => console.log("Tracking user", user),
1549
- * },
1550
- * });
1551
- *
1552
- * // With custom visibility timeout
1553
- * export const POST = client.handleCallback({
1554
- * "video-processing": {
1555
- * "transcode": async (video) => await transcodeVideo(video),
1556
- * },
1557
- * }, {
1558
- * visibilityTimeoutSeconds: 300, // 5 minutes for long operations
1559
- * });
1560
- * ```
1561
- */
1562
- handleCallback(handlers, options) {
1563
- return handleCallback(handlers, {
1564
- ...options,
1565
- client: this.client
1566
- });
1567
- }
1568
- };
1569
1541
  // Annotate the CommonJS export names for ESM import in node:
1570
1542
  0 && (module.exports = {
1571
1543
  BadRequestError,
1572
1544
  BufferTransport,
1573
- Client,
1574
- ConcurrencyLimitError,
1545
+ CLOUD_EVENT_TYPE_V1BETA,
1546
+ CLOUD_EVENT_TYPE_V2BETA,
1575
1547
  ConsumerDiscoveryError,
1576
1548
  ConsumerRegistryNotConfiguredError,
1577
1549
  DuplicateMessageError,
@@ -1584,11 +1556,13 @@ var Client = class {
1584
1556
  MessageLockedError,
1585
1557
  MessageNotAvailableError,
1586
1558
  MessageNotFoundError,
1559
+ QueueClient,
1587
1560
  QueueEmptyError,
1588
1561
  StreamTransport,
1589
1562
  UnauthorizedError,
1590
1563
  handleCallback,
1591
1564
  parseCallback,
1565
+ parseRawCallback,
1592
1566
  receive,
1593
1567
  send
1594
1568
  });