@vercel/queue 0.0.0-alpha.33 → 0.0.0-alpha.35

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
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -23,10 +33,15 @@ __export(index_exports, {
23
33
  BadRequestError: () => BadRequestError,
24
34
  BufferTransport: () => BufferTransport,
25
35
  Client: () => Client,
36
+ ConcurrencyLimitError: () => ConcurrencyLimitError,
37
+ ConsumerDiscoveryError: () => ConsumerDiscoveryError,
38
+ ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
39
+ DuplicateMessageError: () => DuplicateMessageError,
26
40
  ForbiddenError: () => ForbiddenError,
27
41
  InternalServerError: () => InternalServerError,
28
42
  InvalidLimitError: () => InvalidLimitError,
29
43
  JsonTransport: () => JsonTransport,
44
+ MessageAlreadyProcessedError: () => MessageAlreadyProcessedError,
30
45
  MessageCorruptedError: () => MessageCorruptedError,
31
46
  MessageLockedError: () => MessageLockedError,
32
47
  MessageNotAvailableError: () => MessageNotAvailableError,
@@ -62,6 +77,12 @@ var JsonTransport = class {
62
77
  contentType = "application/json";
63
78
  replacer;
64
79
  reviver;
80
+ /**
81
+ * Create a new JsonTransport.
82
+ * @param options - Optional JSON serialization options
83
+ * @param options.replacer - Custom replacer for JSON.stringify
84
+ * @param options.reviver - Custom reviver for JSON.parse
85
+ */
65
86
  constructor(options = {}) {
66
87
  this.replacer = options.replacer;
67
88
  this.reviver = options.reviver;
@@ -91,6 +112,10 @@ var StreamTransport = class {
91
112
  async deserialize(stream) {
92
113
  return stream;
93
114
  }
115
+ /**
116
+ * Consume any remaining stream data to prevent resource leaks.
117
+ * Called automatically by ConsumerGroup; manual call required for direct client usage.
118
+ */
94
119
  async finalize(payload) {
95
120
  const reader = payload.getReader();
96
121
  try {
@@ -107,8 +132,9 @@ var StreamTransport = class {
107
132
  // src/client.ts
108
133
  var import_mixpart = require("mixpart");
109
134
 
110
- // src/oidc.ts
111
- var import_oidc = require("@vercel/oidc");
135
+ // src/dev.ts
136
+ var fs = __toESM(require("fs"));
137
+ var path = __toESM(require("path"));
112
138
 
113
139
  // src/types.ts
114
140
  var MessageNotFoundError = class extends Error {
@@ -140,6 +166,7 @@ var QueueEmptyError = class extends Error {
140
166
  }
141
167
  };
142
168
  var MessageLockedError = class extends Error {
169
+ /** Suggested retry delay in seconds, if provided by the server. */
143
170
  retryAfter;
144
171
  constructor(messageId, retryAfter) {
145
172
  const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
@@ -178,6 +205,265 @@ var InvalidLimitError = class extends Error {
178
205
  this.name = "InvalidLimitError";
179
206
  }
180
207
  };
208
+ var MessageAlreadyProcessedError = class extends Error {
209
+ constructor(messageId) {
210
+ super(`Message ${messageId} has already been processed`);
211
+ this.name = "MessageAlreadyProcessedError";
212
+ }
213
+ };
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
+ var DuplicateMessageError = class extends Error {
227
+ idempotencyKey;
228
+ constructor(message, idempotencyKey) {
229
+ super(message);
230
+ this.name = "DuplicateMessageError";
231
+ this.idempotencyKey = idempotencyKey;
232
+ }
233
+ };
234
+ var ConsumerDiscoveryError = class extends Error {
235
+ deploymentId;
236
+ constructor(message, deploymentId) {
237
+ super(message);
238
+ this.name = "ConsumerDiscoveryError";
239
+ this.deploymentId = deploymentId;
240
+ }
241
+ };
242
+ var ConsumerRegistryNotConfiguredError = class extends Error {
243
+ constructor(message = "Consumer registry not configured") {
244
+ super(message);
245
+ this.name = "ConsumerRegistryNotConfiguredError";
246
+ }
247
+ };
248
+
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}"`
355
+ );
356
+ return false;
357
+ }
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`
362
+ );
363
+ setTimeout(() => {
364
+ triggerDevCallbacks(topicName, messageId);
365
+ }, delaySeconds * 1e3);
366
+ return;
367
+ }
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;
377
+ }
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;
394
+ }
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
+ };
415
+ try {
416
+ const response = await fetch(url, {
417
+ method: "POST",
418
+ headers: {
419
+ "Content-Type": "application/cloudevents+json"
420
+ },
421
+ body: JSON.stringify(cloudEvent)
422
+ });
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
+ }
436
+ } 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
+ }
447
+ }
448
+ } catch (error) {
449
+ console.error(
450
+ `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
451
+ error
452
+ );
453
+ }
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");
181
467
 
182
468
  // src/client.ts
183
469
  function isDebugEnabled() {
@@ -194,14 +480,6 @@ async function consumeStream(stream) {
194
480
  reader.releaseLock();
195
481
  }
196
482
  }
197
- function parseRetryAfter(headers) {
198
- const retryAfterHeader = headers.get("Retry-After");
199
- if (retryAfterHeader) {
200
- const parsed = parseInt(retryAfterHeader, 10);
201
- return Number.isNaN(parsed) ? void 0 : parsed;
202
- }
203
- return void 0;
204
- }
205
483
  function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
206
484
  if (status === 400) {
207
485
  throw new BadRequestError(errorText || badRequestDefault);
@@ -224,8 +502,8 @@ function parseQueueHeaders(headers) {
224
502
  const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
225
503
  const timestamp = headers.get("Vqs-Timestamp");
226
504
  const contentType = headers.get("Content-Type") || "application/octet-stream";
227
- const ticket = headers.get("Vqs-Ticket");
228
- if (!messageId || !timestamp || !ticket) {
505
+ const receiptHandle = headers.get("Vqs-Receipt-Handle");
506
+ if (!messageId || !timestamp || !receiptHandle) {
229
507
  return null;
230
508
  }
231
509
  const deliveryCount = parseInt(deliveryCountStr, 10);
@@ -237,7 +515,7 @@ function parseQueueHeaders(headers) {
237
515
  deliveryCount,
238
516
  createdAt: new Date(timestamp),
239
517
  contentType,
240
- ticket
518
+ receiptHandle
241
519
  };
242
520
  }
243
521
  var QueueClient = class {
@@ -245,15 +523,30 @@ var QueueClient = class {
245
523
  basePath;
246
524
  customHeaders;
247
525
  providedToken;
248
- /**
249
- * Create a new Vercel Queue Service client
250
- * @param options QueueClient configuration options
251
- */
526
+ defaultDeploymentId;
527
+ pinToDeployment;
252
528
  constructor(options = {}) {
253
529
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
254
- this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
530
+ this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
255
531
  this.customHeaders = options.headers || {};
256
532
  this.providedToken = options.token;
533
+ this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
534
+ this.pinToDeployment = options.pinToDeployment ?? true;
535
+ }
536
+ getSendDeploymentId() {
537
+ if (isDevMode()) {
538
+ return void 0;
539
+ }
540
+ if (this.pinToDeployment) {
541
+ return this.defaultDeploymentId;
542
+ }
543
+ return void 0;
544
+ }
545
+ getConsumeDeploymentId() {
546
+ if (isDevMode()) {
547
+ return void 0;
548
+ }
549
+ return this.defaultDeploymentId;
257
550
  }
258
551
  async getToken() {
259
552
  if (this.providedToken) {
@@ -267,10 +560,12 @@ var QueueClient = class {
267
560
  }
268
561
  return token;
269
562
  }
270
- /**
271
- * Internal fetch wrapper that automatically handles debug logging
272
- * when VERCEL_QUEUE_DEBUG is enabled
273
- */
563
+ buildUrl(queueName, ...pathSegments) {
564
+ const encodedQueue = encodeURIComponent(queueName);
565
+ const segments = pathSegments.map((s) => encodeURIComponent(s));
566
+ const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
567
+ return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
568
+ }
274
569
  async fetch(url, init) {
275
570
  const method = init.method || "GET";
276
571
  if (isDebugEnabled()) {
@@ -307,24 +602,38 @@ var QueueClient = class {
307
602
  return response;
308
603
  }
309
604
  /**
310
- * Send a message to a queue
311
- * @param options Send message options
312
- * @param transport Serializer/deserializer for the payload
313
- * @returns Promise with the message ID
314
- * @throws {BadRequestError} When request parameters are invalid
605
+ * Send a message to a topic.
606
+ *
607
+ * @param options - Message options including queue name, payload, and optional settings
608
+ * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
609
+ * @param options.payload - Message payload
610
+ * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
611
+ * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
612
+ * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
613
+ * @param transport - Serializer for the payload
614
+ * @returns Promise with the generated messageId
615
+ * @throws {DuplicateMessageError} When idempotency key was already used
616
+ * @throws {ConsumerDiscoveryError} When consumer discovery fails
617
+ * @throws {ConsumerRegistryNotConfiguredError} When registry not configured
618
+ * @throws {BadRequestError} When parameters are invalid
315
619
  * @throws {UnauthorizedError} When authentication fails
316
- * @throws {ForbiddenError} When access is denied (environment mismatch)
620
+ * @throws {ForbiddenError} When access is denied
317
621
  * @throws {InternalServerError} When server encounters an error
318
622
  */
319
623
  async sendMessage(options, transport) {
320
- const { queueName, payload, idempotencyKey, retentionSeconds } = options;
624
+ const {
625
+ queueName,
626
+ payload,
627
+ idempotencyKey,
628
+ retentionSeconds,
629
+ delaySeconds
630
+ } = options;
321
631
  const headers = new Headers({
322
632
  Authorization: `Bearer ${await this.getToken()}`,
323
- "Vqs-Queue-Name": queueName,
324
633
  "Content-Type": transport.contentType,
325
634
  ...this.customHeaders
326
635
  });
327
- const deploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
636
+ const deploymentId = this.getSendDeploymentId();
328
637
  if (deploymentId) {
329
638
  headers.set("Vqs-Deployment-Id", deploymentId);
330
639
  }
@@ -334,8 +643,12 @@ var QueueClient = class {
334
643
  if (retentionSeconds !== void 0) {
335
644
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
336
645
  }
337
- const body = transport.serialize(payload);
338
- const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
646
+ if (delaySeconds !== void 0) {
647
+ headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
648
+ }
649
+ const serialized = transport.serialize(payload);
650
+ const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
651
+ const response = await this.fetch(this.buildUrl(queueName), {
339
652
  method: "POST",
340
653
  body,
341
654
  headers
@@ -343,7 +656,21 @@ var QueueClient = class {
343
656
  if (!response.ok) {
344
657
  const errorText = await response.text();
345
658
  if (response.status === 409) {
346
- throw new Error("Duplicate idempotency key detected");
659
+ throw new DuplicateMessageError(
660
+ errorText || "Duplicate idempotency key detected",
661
+ idempotencyKey
662
+ );
663
+ }
664
+ if (response.status === 502) {
665
+ throw new ConsumerDiscoveryError(
666
+ errorText || "Consumer discovery failed",
667
+ deploymentId
668
+ );
669
+ }
670
+ if (response.status === 503) {
671
+ throw new ConsumerRegistryNotConfiguredError(
672
+ errorText || "Consumer registry not configured"
673
+ );
347
674
  }
348
675
  throwCommonHttpError(
349
676
  response.status,
@@ -356,52 +683,78 @@ var QueueClient = class {
356
683
  return responseData;
357
684
  }
358
685
  /**
359
- * Receive messages from a queue
360
- * @param options Receive messages options
361
- * @param transport Serializer/deserializer for the payload
362
- * @returns AsyncGenerator that yields messages as they arrive
363
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
364
- * @throws {QueueEmptyError} When no messages are available (204)
365
- * @throws {MessageLockedError} When messages are temporarily locked (423)
366
- * @throws {BadRequestError} When request parameters are invalid
686
+ * Receive messages from a topic as an async generator.
687
+ *
688
+ * @param options - Receive options
689
+ * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
690
+ * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
691
+ * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
692
+ * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
693
+ * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
694
+ * @param transport - Deserializer for message payloads
695
+ * @yields Message objects with payload, messageId, receiptHandle, etc.
696
+ * @throws {QueueEmptyError} When no messages available
697
+ * @throws {InvalidLimitError} When limit is outside 1-10 range
698
+ * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
699
+ * @throws {BadRequestError} When parameters are invalid
367
700
  * @throws {UnauthorizedError} When authentication fails
368
- * @throws {ForbiddenError} When access is denied (environment mismatch)
701
+ * @throws {ForbiddenError} When access is denied
369
702
  * @throws {InternalServerError} When server encounters an error
370
703
  */
371
704
  async *receiveMessages(options, transport) {
372
- const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
705
+ const {
706
+ queueName,
707
+ consumerGroup,
708
+ visibilityTimeoutSeconds,
709
+ limit,
710
+ maxConcurrency
711
+ } = options;
373
712
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
374
713
  throw new InvalidLimitError(limit);
375
714
  }
376
715
  const headers = new Headers({
377
716
  Authorization: `Bearer ${await this.getToken()}`,
378
- "Vqs-Queue-Name": queueName,
379
- "Vqs-Consumer-Group": consumerGroup,
380
717
  Accept: "multipart/mixed",
381
718
  ...this.customHeaders
382
719
  });
383
720
  if (visibilityTimeoutSeconds !== void 0) {
384
721
  headers.set(
385
- "Vqs-Visibility-Timeout",
722
+ "Vqs-Visibility-Timeout-Seconds",
386
723
  visibilityTimeoutSeconds.toString()
387
724
  );
388
725
  }
389
726
  if (limit !== void 0) {
390
- headers.set("Vqs-Limit", limit.toString());
727
+ headers.set("Vqs-Max-Messages", limit.toString());
391
728
  }
392
- const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
393
- method: "GET",
394
- headers
395
- });
729
+ if (maxConcurrency !== void 0) {
730
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
731
+ }
732
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
733
+ if (effectiveDeploymentId) {
734
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
735
+ }
736
+ const response = await this.fetch(
737
+ this.buildUrl(queueName, "consumer", consumerGroup),
738
+ {
739
+ method: "POST",
740
+ headers
741
+ }
742
+ );
396
743
  if (response.status === 204) {
397
744
  throw new QueueEmptyError(queueName, consumerGroup);
398
745
  }
399
746
  if (!response.ok) {
400
747
  const errorText = await response.text();
401
- if (response.status === 423) {
402
- throw new MessageLockedError(
403
- "next message",
404
- parseRetryAfter(response.headers)
748
+ if (response.status === 429) {
749
+ let errorData = {};
750
+ try {
751
+ errorData = JSON.parse(errorText);
752
+ } catch {
753
+ }
754
+ throw new ConcurrencyLimitError(
755
+ errorData.error || "Concurrency limit exceeded or throttled",
756
+ errorData.currentInflight,
757
+ errorData.maxConcurrency
405
758
  );
406
759
  }
407
760
  throwCommonHttpError(
@@ -433,34 +786,56 @@ var QueueClient = class {
433
786
  }
434
787
  }
435
788
  }
789
+ /**
790
+ * Receive a specific message by its ID.
791
+ *
792
+ * @param options - Receive options
793
+ * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
794
+ * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
795
+ * @param options.messageId - Message ID to retrieve
796
+ * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
797
+ * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
798
+ * @param transport - Deserializer for the message payload
799
+ * @returns Promise with the message
800
+ * @throws {MessageNotFoundError} When message doesn't exist
801
+ * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
802
+ * @throws {MessageAlreadyProcessedError} When message was already processed
803
+ * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
804
+ * @throws {BadRequestError} When parameters are invalid
805
+ * @throws {UnauthorizedError} When authentication fails
806
+ * @throws {ForbiddenError} When access is denied
807
+ * @throws {InternalServerError} When server encounters an error
808
+ */
436
809
  async receiveMessageById(options, transport) {
437
810
  const {
438
811
  queueName,
439
812
  consumerGroup,
440
813
  messageId,
441
814
  visibilityTimeoutSeconds,
442
- skipPayload
815
+ maxConcurrency
443
816
  } = options;
444
817
  const headers = new Headers({
445
818
  Authorization: `Bearer ${await this.getToken()}`,
446
- "Vqs-Queue-Name": queueName,
447
- "Vqs-Consumer-Group": consumerGroup,
448
819
  Accept: "multipart/mixed",
449
820
  ...this.customHeaders
450
821
  });
451
822
  if (visibilityTimeoutSeconds !== void 0) {
452
823
  headers.set(
453
- "Vqs-Visibility-Timeout",
824
+ "Vqs-Visibility-Timeout-Seconds",
454
825
  visibilityTimeoutSeconds.toString()
455
826
  );
456
827
  }
457
- if (skipPayload) {
458
- headers.set("Vqs-Skip-Payload", "1");
828
+ if (maxConcurrency !== void 0) {
829
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
830
+ }
831
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
832
+ if (effectiveDeploymentId) {
833
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
459
834
  }
460
835
  const response = await this.fetch(
461
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
836
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
462
837
  {
463
- method: "GET",
838
+ method: "POST",
464
839
  headers
465
840
  }
466
841
  );
@@ -470,12 +845,32 @@ var QueueClient = class {
470
845
  throw new MessageNotFoundError(messageId);
471
846
  }
472
847
  if (response.status === 409) {
848
+ let errorData = {};
849
+ try {
850
+ errorData = JSON.parse(errorText);
851
+ } catch {
852
+ }
853
+ if (errorData.originalMessageId) {
854
+ throw new MessageNotAvailableError(
855
+ messageId,
856
+ `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
857
+ );
858
+ }
473
859
  throw new MessageNotAvailableError(messageId);
474
860
  }
475
- if (response.status === 423) {
476
- throw new MessageLockedError(
477
- messageId,
478
- parseRetryAfter(response.headers)
861
+ if (response.status === 410) {
862
+ throw new MessageAlreadyProcessedError(messageId);
863
+ }
864
+ if (response.status === 429) {
865
+ let errorData = {};
866
+ try {
867
+ errorData = JSON.parse(errorText);
868
+ } catch {
869
+ }
870
+ throw new ConcurrencyLimitError(
871
+ errorData.error || "Concurrency limit exceeded or throttled",
872
+ errorData.currentInflight,
873
+ errorData.maxConcurrency
479
874
  );
480
875
  }
481
876
  throwCommonHttpError(
@@ -485,95 +880,73 @@ var QueueClient = class {
485
880
  "receive message by ID"
486
881
  );
487
882
  }
488
- if (skipPayload && response.status === 204) {
489
- const parsedHeaders = parseQueueHeaders(response.headers);
883
+ for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
884
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
490
885
  if (!parsedHeaders) {
886
+ await consumeStream(multipartMessage.payload);
491
887
  throw new MessageCorruptedError(
492
888
  messageId,
493
- "Missing required queue headers in 204 response"
889
+ "Missing required queue headers in response"
494
890
  );
495
891
  }
892
+ const deserializedPayload = await transport.deserialize(
893
+ multipartMessage.payload
894
+ );
496
895
  const message = {
497
896
  ...parsedHeaders,
498
- payload: void 0
897
+ payload: deserializedPayload
499
898
  };
500
899
  return { message };
501
900
  }
502
- if (!transport) {
503
- throw new Error("Transport is required when skipPayload is not true");
504
- }
505
- try {
506
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
507
- try {
508
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
509
- if (!parsedHeaders) {
510
- console.warn("Missing required queue headers in multipart part");
511
- await consumeStream(multipartMessage.payload);
512
- continue;
513
- }
514
- const deserializedPayload = await transport.deserialize(
515
- multipartMessage.payload
516
- );
517
- const message = {
518
- ...parsedHeaders,
519
- payload: deserializedPayload
520
- };
521
- return { message };
522
- } catch (error) {
523
- console.warn("Failed to deserialize message by ID:", error);
524
- await consumeStream(multipartMessage.payload);
525
- throw new MessageCorruptedError(
526
- messageId,
527
- `Failed to deserialize payload: ${error}`
528
- );
529
- }
530
- }
531
- } catch (error) {
532
- if (error instanceof MessageCorruptedError) {
533
- throw error;
534
- }
535
- throw new MessageCorruptedError(
536
- messageId,
537
- `Failed to parse multipart response: ${error}`
538
- );
539
- }
540
901
  throw new MessageNotFoundError(messageId);
541
902
  }
542
903
  /**
543
- * Delete a message (acknowledge processing)
544
- * @param options Delete message options
545
- * @returns Promise with delete status
546
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
547
- * @throws {MessageNotAvailableError} When message can't be deleted (409)
548
- * @throws {BadRequestError} When ticket is missing or invalid (400)
904
+ * Delete (acknowledge) a message after successful processing.
905
+ *
906
+ * @param options - Delete options
907
+ * @param options.queueName - Topic name
908
+ * @param options.consumerGroup - Consumer group name
909
+ * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
910
+ * @returns Promise indicating deletion success
911
+ * @throws {MessageNotFoundError} When receipt handle not found
912
+ * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
913
+ * @throws {BadRequestError} When parameters are invalid
549
914
  * @throws {UnauthorizedError} When authentication fails
550
- * @throws {ForbiddenError} When access is denied (environment mismatch)
915
+ * @throws {ForbiddenError} When access is denied
551
916
  * @throws {InternalServerError} When server encounters an error
552
917
  */
553
918
  async deleteMessage(options) {
554
- const { queueName, consumerGroup, messageId, ticket } = options;
919
+ const { queueName, consumerGroup, receiptHandle } = options;
920
+ const headers = new Headers({
921
+ Authorization: `Bearer ${await this.getToken()}`,
922
+ ...this.customHeaders
923
+ });
924
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
925
+ if (effectiveDeploymentId) {
926
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
927
+ }
555
928
  const response = await this.fetch(
556
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
929
+ this.buildUrl(
930
+ queueName,
931
+ "consumer",
932
+ consumerGroup,
933
+ "lease",
934
+ receiptHandle
935
+ ),
557
936
  {
558
937
  method: "DELETE",
559
- headers: new Headers({
560
- Authorization: `Bearer ${await this.getToken()}`,
561
- "Vqs-Queue-Name": queueName,
562
- "Vqs-Consumer-Group": consumerGroup,
563
- "Vqs-Ticket": ticket,
564
- ...this.customHeaders
565
- })
938
+ headers
566
939
  }
567
940
  );
568
941
  if (!response.ok) {
569
942
  const errorText = await response.text();
570
943
  if (response.status === 404) {
571
- throw new MessageNotFoundError(messageId);
944
+ throw new MessageNotFoundError(receiptHandle);
572
945
  }
573
946
  if (response.status === 409) {
574
947
  throw new MessageNotAvailableError(
575
- messageId,
576
- errorText || "Invalid ticket, message not in correct state, or already processed"
948
+ receiptHandle,
949
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
577
950
  );
578
951
  }
579
952
  throwCommonHttpError(
@@ -581,53 +954,67 @@ var QueueClient = class {
581
954
  response.statusText,
582
955
  errorText,
583
956
  "delete message",
584
- "Missing or invalid ticket"
957
+ "Missing or invalid receipt handle"
585
958
  );
586
959
  }
587
960
  return { deleted: true };
588
961
  }
589
962
  /**
590
- * Change the visibility timeout of a message
591
- * @param options Change visibility options
592
- * @returns Promise with update status
593
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
594
- * @throws {MessageNotAvailableError} When message can't be updated (409)
595
- * @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
963
+ * Extend or change the visibility timeout of a message.
964
+ * Used to prevent message redelivery while still processing.
965
+ *
966
+ * @param options - Visibility options
967
+ * @param options.queueName - Topic name
968
+ * @param options.consumerGroup - Consumer group name
969
+ * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
970
+ * @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
971
+ * @returns Promise indicating success
972
+ * @throws {MessageNotFoundError} When receipt handle not found
973
+ * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
974
+ * @throws {BadRequestError} When parameters are invalid
596
975
  * @throws {UnauthorizedError} When authentication fails
597
- * @throws {ForbiddenError} When access is denied (environment mismatch)
976
+ * @throws {ForbiddenError} When access is denied
598
977
  * @throws {InternalServerError} When server encounters an error
599
978
  */
600
979
  async changeVisibility(options) {
601
980
  const {
602
981
  queueName,
603
982
  consumerGroup,
604
- messageId,
605
- ticket,
983
+ receiptHandle,
606
984
  visibilityTimeoutSeconds
607
985
  } = options;
986
+ const headers = new Headers({
987
+ Authorization: `Bearer ${await this.getToken()}`,
988
+ "Content-Type": "application/json",
989
+ ...this.customHeaders
990
+ });
991
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
992
+ if (effectiveDeploymentId) {
993
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
994
+ }
608
995
  const response = await this.fetch(
609
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
996
+ this.buildUrl(
997
+ queueName,
998
+ "consumer",
999
+ consumerGroup,
1000
+ "lease",
1001
+ receiptHandle
1002
+ ),
610
1003
  {
611
1004
  method: "PATCH",
612
- headers: new Headers({
613
- Authorization: `Bearer ${await this.getToken()}`,
614
- "Vqs-Queue-Name": queueName,
615
- "Vqs-Consumer-Group": consumerGroup,
616
- "Vqs-Ticket": ticket,
617
- "Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
618
- ...this.customHeaders
619
- })
1005
+ headers,
1006
+ body: JSON.stringify({ visibilityTimeoutSeconds })
620
1007
  }
621
1008
  );
622
1009
  if (!response.ok) {
623
1010
  const errorText = await response.text();
624
1011
  if (response.status === 404) {
625
- throw new MessageNotFoundError(messageId);
1012
+ throw new MessageNotFoundError(receiptHandle);
626
1013
  }
627
1014
  if (response.status === 409) {
628
1015
  throw new MessageNotAvailableError(
629
- messageId,
630
- errorText || "Invalid ticket, message not in correct state, or already processed"
1016
+ receiptHandle,
1017
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
631
1018
  );
632
1019
  }
633
1020
  throwCommonHttpError(
@@ -635,194 +1022,72 @@ var QueueClient = class {
635
1022
  response.statusText,
636
1023
  errorText,
637
1024
  "change visibility",
638
- "Missing ticket or invalid visibility timeout"
1025
+ "Missing receipt handle or invalid visibility timeout"
639
1026
  );
640
1027
  }
641
- return { updated: true };
642
- }
643
- };
644
-
645
- // src/dev.ts
646
- var GLOBAL_KEY = Symbol.for("@vercel/queue.devHandlers");
647
- function getDevHandlerState() {
648
- const g = globalThis;
649
- if (!g[GLOBAL_KEY]) {
650
- g[GLOBAL_KEY] = {
651
- devRouteHandlers: /* @__PURE__ */ new Map(),
652
- wildcardRouteHandlers: /* @__PURE__ */ new Map()
653
- };
654
- }
655
- return g[GLOBAL_KEY];
656
- }
657
- var { devRouteHandlers, wildcardRouteHandlers } = getDevHandlerState();
658
- function cleanupDeadRefs(key, refs) {
659
- const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
660
- if (aliveRefs.length === 0) {
661
- wildcardRouteHandlers.delete(key);
662
- } else if (aliveRefs.length < refs.length) {
663
- wildcardRouteHandlers.set(key, aliveRefs);
1028
+ return { success: true };
664
1029
  }
665
- }
666
- function isDevMode() {
667
- return process.env.NODE_ENV === "development";
668
- }
669
- function registerDevRouteHandler(routeHandler, handlers) {
670
- for (const topicName in handlers) {
671
- for (const consumerGroup in handlers[topicName]) {
672
- const key = `${topicName}:${consumerGroup}`;
673
- if (topicName.includes("*")) {
674
- const existing = wildcardRouteHandlers.get(key) || [];
675
- cleanupDeadRefs(key, existing);
676
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
677
- const weakRef = new WeakRef(routeHandler);
678
- cleanedRefs.push(weakRef);
679
- wildcardRouteHandlers.set(key, cleanedRefs);
680
- } else {
681
- devRouteHandlers.set(key, {
682
- routeHandler,
683
- topicPattern: topicName
684
- });
685
- }
1030
+ /**
1031
+ * Alternative endpoint for changing message visibility timeout.
1032
+ * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
1033
+ * Functionally equivalent to changeVisibility but follows an alternative API pattern.
1034
+ *
1035
+ * @param options - Options for changing visibility
1036
+ * @returns Promise resolving to change visibility response
1037
+ */
1038
+ async changeVisibilityAlt(options) {
1039
+ const {
1040
+ queueName,
1041
+ consumerGroup,
1042
+ receiptHandle,
1043
+ visibilityTimeoutSeconds
1044
+ } = options;
1045
+ const headers = new Headers({
1046
+ Authorization: `Bearer ${await this.getToken()}`,
1047
+ "Content-Type": "application/json",
1048
+ ...this.customHeaders
1049
+ });
1050
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1051
+ if (effectiveDeploymentId) {
1052
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
686
1053
  }
687
- }
688
- }
689
- function findRouteHandlersForTopic(topicName) {
690
- const handlersMap = /* @__PURE__ */ new Map();
691
- for (const [
692
- key,
693
- { routeHandler, topicPattern }
694
- ] of devRouteHandlers.entries()) {
695
- const [_, consumerGroup] = key.split(":");
696
- if (topicPattern === topicName) {
697
- if (!handlersMap.has(routeHandler)) {
698
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
1054
+ const response = await this.fetch(
1055
+ this.buildUrl(
1056
+ queueName,
1057
+ "consumer",
1058
+ consumerGroup,
1059
+ "lease",
1060
+ receiptHandle,
1061
+ "visibility"
1062
+ ),
1063
+ {
1064
+ method: "PATCH",
1065
+ headers,
1066
+ body: JSON.stringify({ visibilityTimeoutSeconds })
699
1067
  }
700
- handlersMap.get(routeHandler).add(consumerGroup);
701
- }
702
- }
703
- for (const [key, refs] of wildcardRouteHandlers.entries()) {
704
- const [pattern, consumerGroup] = key.split(":");
705
- if (matchesWildcardPattern(topicName, pattern)) {
706
- cleanupDeadRefs(key, refs);
707
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
708
- for (const ref of cleanedRefs) {
709
- const routeHandler = ref.deref();
710
- if (routeHandler) {
711
- if (!handlersMap.has(routeHandler)) {
712
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
713
- }
714
- handlersMap.get(routeHandler).add(consumerGroup);
715
- }
1068
+ );
1069
+ if (!response.ok) {
1070
+ const errorText = await response.text();
1071
+ if (response.status === 404) {
1072
+ throw new MessageNotFoundError(receiptHandle);
716
1073
  }
717
- }
718
- }
719
- return handlersMap;
720
- }
721
- function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
722
- const cloudEvent = {
723
- type: "com.vercel.queue.v1beta",
724
- source: `/topic/${topicName}/consumer/${consumerGroup}`,
725
- id: messageId,
726
- datacontenttype: "application/json",
727
- data: {
728
- messageId,
729
- queueName: topicName,
730
- consumerGroup
731
- },
732
- time: (/* @__PURE__ */ new Date()).toISOString(),
733
- specversion: "1.0"
734
- };
735
- return new Request("https://localhost/api/queue/callback", {
736
- method: "POST",
737
- headers: {
738
- "Content-Type": "application/cloudevents+json"
739
- },
740
- body: JSON.stringify(cloudEvent)
741
- });
742
- }
743
- var DEV_CALLBACK_DELAY = 1e3;
744
- function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
745
- console.log(
746
- `[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
747
- );
748
- setTimeout(
749
- () => {
750
- console.log(
751
- `[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
752
- );
753
- triggerDevCallbacks(topicName, messageId);
754
- },
755
- timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
756
- );
757
- }
758
- function triggerDevCallbacks(topicName, messageId) {
759
- const handlersMap = findRouteHandlersForTopic(topicName);
760
- if (handlersMap.size === 0) {
761
- return;
762
- }
763
- const consumerGroups = Array.from(
764
- new Set(
765
- Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
766
- )
767
- );
768
- console.log(
769
- `[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
770
- );
771
- setTimeout(async () => {
772
- for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
773
- for (const consumerGroup of consumerGroups2) {
774
- try {
775
- const request = createMockCloudEventRequest(
776
- topicName,
777
- consumerGroup,
778
- messageId
779
- );
780
- const response = await routeHandler(request);
781
- if (response.ok) {
782
- try {
783
- const responseData = await response.json();
784
- if (responseData.status === "success") {
785
- console.log(
786
- `[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
787
- );
788
- }
789
- } catch (jsonError) {
790
- console.error(
791
- `[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
792
- jsonError
793
- );
794
- }
795
- } else {
796
- try {
797
- const errorData = await response.json();
798
- console.error(
799
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
800
- errorData.error || response.statusText
801
- );
802
- } catch (jsonError) {
803
- console.error(
804
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
805
- response.statusText
806
- );
807
- }
808
- }
809
- } catch (error) {
810
- console.error(
811
- `[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
812
- error
813
- );
814
- }
1074
+ if (response.status === 409) {
1075
+ throw new MessageNotAvailableError(
1076
+ receiptHandle,
1077
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
1078
+ );
815
1079
  }
1080
+ throwCommonHttpError(
1081
+ response.status,
1082
+ response.statusText,
1083
+ errorText,
1084
+ "change visibility (alt)",
1085
+ "Missing receipt handle or invalid visibility timeout"
1086
+ );
816
1087
  }
817
- }, DEV_CALLBACK_DELAY);
818
- }
819
- function clearDevHandlers() {
820
- devRouteHandlers.clear();
821
- wildcardRouteHandlers.clear();
822
- }
823
- if (process.env.NODE_ENV === "test" || process.env.VITEST) {
824
- globalThis.__clearDevHandlers = clearDevHandlers;
825
- }
1088
+ return { success: true };
1089
+ }
1090
+ };
826
1091
 
827
1092
  // src/consumer-group.ts
828
1093
  var ConsumerGroup = class {
@@ -833,69 +1098,64 @@ var ConsumerGroup = class {
833
1098
  refreshInterval;
834
1099
  transport;
835
1100
  /**
836
- * Create a new ConsumerGroup instance
837
- * @param client QueueClient instance to use for API calls
838
- * @param topicName Name of the topic to consume from
839
- * @param consumerGroupName Name of the consumer group
840
- * @param options Optional configuration
1101
+ * Create a new ConsumerGroup instance.
1102
+ *
1103
+ * @param client - QueueClient instance to use for API calls
1104
+ * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
1105
+ * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1106
+ * @param options - Optional configuration
1107
+ * @param options.transport - Payload serializer (default: JsonTransport)
1108
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1109
+ * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
841
1110
  */
842
1111
  constructor(client, topicName, consumerGroupName, options = {}) {
843
1112
  this.client = client;
844
1113
  this.topicName = topicName;
845
1114
  this.consumerGroupName = consumerGroupName;
846
- this.visibilityTimeout = options.visibilityTimeoutSeconds || 30;
847
- this.refreshInterval = options.refreshInterval || 10;
1115
+ this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1116
+ this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
848
1117
  this.transport = options.transport || new JsonTransport();
849
1118
  }
850
1119
  /**
851
1120
  * Starts a background loop that periodically extends the visibility timeout for a message.
852
- * This prevents the message from becoming visible to other consumers while it's being processed.
853
- *
854
- * The extension loop runs every `refreshInterval` seconds and updates the message's
855
- * visibility timeout to `visibilityTimeout` seconds from the current time.
856
- *
857
- * @param messageId - The unique identifier of the message to extend visibility for
858
- * @param ticket - The receipt ticket that proves ownership of the message
859
- * @returns A function that when called will stop the extension loop
860
- *
861
- * @remarks
862
- * - The first extension attempt occurs after `refreshInterval` seconds, not immediately
863
- * - If an extension fails, the loop terminates with an error logged to console
864
- * - The returned stop function is idempotent - calling it multiple times is safe
865
- * - By default, the stop function returns immediately without waiting for in-flight
866
- * - Pass `true` to the stop function to wait for any in-flight extension to complete
867
1121
  */
868
- startVisibilityExtension(messageId, ticket) {
1122
+ startVisibilityExtension(receiptHandle) {
869
1123
  let isRunning = true;
1124
+ let isResolved = false;
870
1125
  let resolveLifecycle;
871
1126
  let timeoutId = null;
872
1127
  const lifecyclePromise = new Promise((resolve) => {
873
1128
  resolveLifecycle = resolve;
874
1129
  });
1130
+ const safeResolve = () => {
1131
+ if (!isResolved) {
1132
+ isResolved = true;
1133
+ resolveLifecycle();
1134
+ }
1135
+ };
875
1136
  const extend = async () => {
876
1137
  if (!isRunning) {
877
- resolveLifecycle();
1138
+ safeResolve();
878
1139
  return;
879
1140
  }
880
1141
  try {
881
1142
  await this.client.changeVisibility({
882
1143
  queueName: this.topicName,
883
1144
  consumerGroup: this.consumerGroupName,
884
- messageId,
885
- ticket,
1145
+ receiptHandle,
886
1146
  visibilityTimeoutSeconds: this.visibilityTimeout
887
1147
  });
888
1148
  if (isRunning) {
889
1149
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
890
1150
  } else {
891
- resolveLifecycle();
1151
+ safeResolve();
892
1152
  }
893
1153
  } catch (error) {
894
1154
  console.error(
895
- `Failed to extend visibility for message ${messageId}:`,
1155
+ `Failed to extend visibility for receipt handle ${receiptHandle}:`,
896
1156
  error
897
1157
  );
898
- resolveLifecycle();
1158
+ safeResolve();
899
1159
  }
900
1160
  };
901
1161
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
@@ -908,22 +1168,14 @@ var ConsumerGroup = class {
908
1168
  if (waitForCompletion) {
909
1169
  await lifecyclePromise;
910
1170
  } else {
911
- resolveLifecycle();
1171
+ safeResolve();
912
1172
  }
913
1173
  };
914
1174
  }
915
- /**
916
- * Process a single message with the given handler
917
- * @param message The message to process
918
- * @param handler Function to process the message
919
- */
920
1175
  async processMessage(message, handler) {
921
- const stopExtension = this.startVisibilityExtension(
922
- message.messageId,
923
- message.ticket
924
- );
1176
+ const stopExtension = this.startVisibilityExtension(message.receiptHandle);
925
1177
  try {
926
- const result = await handler(message.payload, {
1178
+ await handler(message.payload, {
927
1179
  messageId: message.messageId,
928
1180
  deliveryCount: message.deliveryCount,
929
1181
  createdAt: message.createdAt,
@@ -931,29 +1183,11 @@ var ConsumerGroup = class {
931
1183
  consumerGroup: this.consumerGroupName
932
1184
  });
933
1185
  await stopExtension();
934
- if (result && "timeoutSeconds" in result) {
935
- await this.client.changeVisibility({
936
- queueName: this.topicName,
937
- consumerGroup: this.consumerGroupName,
938
- messageId: message.messageId,
939
- ticket: message.ticket,
940
- visibilityTimeoutSeconds: result.timeoutSeconds
941
- });
942
- if (isDevMode()) {
943
- scheduleDevTimeout(
944
- this.topicName,
945
- message.messageId,
946
- result.timeoutSeconds
947
- );
948
- }
949
- } else {
950
- await this.client.deleteMessage({
951
- queueName: this.topicName,
952
- consumerGroup: this.consumerGroupName,
953
- messageId: message.messageId,
954
- ticket: message.ticket
955
- });
956
- }
1186
+ await this.client.deleteMessage({
1187
+ queueName: this.topicName,
1188
+ consumerGroup: this.consumerGroupName,
1189
+ receiptHandle: message.receiptHandle
1190
+ });
957
1191
  } catch (error) {
958
1192
  await stopExtension();
959
1193
  if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
@@ -968,36 +1202,16 @@ var ConsumerGroup = class {
968
1202
  }
969
1203
  async consume(handler, options) {
970
1204
  if (options?.messageId) {
971
- if (options.skipPayload) {
972
- const response = await this.client.receiveMessageById(
973
- {
974
- queueName: this.topicName,
975
- consumerGroup: this.consumerGroupName,
976
- messageId: options.messageId,
977
- visibilityTimeoutSeconds: this.visibilityTimeout,
978
- skipPayload: true
979
- },
980
- this.transport
981
- );
982
- await this.processMessage(
983
- response.message,
984
- handler
985
- );
986
- } else {
987
- const response = await this.client.receiveMessageById(
988
- {
989
- queueName: this.topicName,
990
- consumerGroup: this.consumerGroupName,
991
- messageId: options.messageId,
992
- visibilityTimeoutSeconds: this.visibilityTimeout
993
- },
994
- this.transport
995
- );
996
- await this.processMessage(
997
- response.message,
998
- handler
999
- );
1000
- }
1205
+ const response = await this.client.receiveMessageById(
1206
+ {
1207
+ queueName: this.topicName,
1208
+ consumerGroup: this.consumerGroupName,
1209
+ messageId: options.messageId,
1210
+ visibilityTimeoutSeconds: this.visibilityTimeout
1211
+ },
1212
+ this.transport
1213
+ );
1214
+ await this.processMessage(response.message, handler);
1001
1215
  } else {
1002
1216
  let messageFound = false;
1003
1217
  for await (const message of this.client.receiveMessages(
@@ -1065,7 +1279,7 @@ var Topic = class {
1065
1279
  payload,
1066
1280
  idempotencyKey: options?.idempotencyKey,
1067
1281
  retentionSeconds: options?.retentionSeconds,
1068
- deploymentId: options?.deploymentId
1282
+ delaySeconds: options?.delaySeconds
1069
1283
  },
1070
1284
  this.transport
1071
1285
  );
@@ -1175,7 +1389,7 @@ async function parseCallback(request) {
1175
1389
  messageId
1176
1390
  };
1177
1391
  }
1178
- function createCallbackHandler(handlers, client) {
1392
+ function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1179
1393
  for (const topicPattern in handlers) {
1180
1394
  if (topicPattern.includes("*")) {
1181
1395
  if (!validateWildcardPattern(topicPattern)) {
@@ -1211,7 +1425,10 @@ function createCallbackHandler(handlers, client) {
1211
1425
  );
1212
1426
  }
1213
1427
  const topic = new Topic(client, queueName);
1214
- const cg = topic.consumerGroup(consumerGroup);
1428
+ const cg = topic.consumerGroup(
1429
+ consumerGroup,
1430
+ visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1431
+ );
1215
1432
  await cg.consume(consumerGroupHandler, { messageId });
1216
1433
  return Response.json({ status: "success" });
1217
1434
  } catch (error) {
@@ -1225,13 +1442,14 @@ function createCallbackHandler(handlers, client) {
1225
1442
  );
1226
1443
  }
1227
1444
  };
1228
- if (isDevMode()) {
1229
- registerDevRouteHandler(routeHandler, handlers);
1230
- }
1231
1445
  return routeHandler;
1232
1446
  }
1233
- function handleCallback(handlers, client) {
1234
- return createCallbackHandler(handlers, client || new QueueClient());
1447
+ function handleCallback(handlers, options) {
1448
+ return createCallbackHandler(
1449
+ handlers,
1450
+ options?.client || new QueueClient(),
1451
+ options?.visibilityTimeoutSeconds
1452
+ );
1235
1453
  }
1236
1454
 
1237
1455
  // src/factory.ts
@@ -1244,12 +1462,12 @@ async function send(topicName, payload, options) {
1244
1462
  payload,
1245
1463
  idempotencyKey: options?.idempotencyKey,
1246
1464
  retentionSeconds: options?.retentionSeconds,
1247
- deploymentId: options?.deploymentId
1465
+ delaySeconds: options?.delaySeconds
1248
1466
  },
1249
1467
  transport
1250
1468
  );
1251
1469
  if (isDevMode()) {
1252
- triggerDevCallbacks(topicName, result.messageId);
1470
+ triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1253
1471
  }
1254
1472
  return { messageId: result.messageId };
1255
1473
  }
@@ -1257,22 +1475,10 @@ async function receive(topicName, consumerGroup, handler, options) {
1257
1475
  const transport = options?.transport || new JsonTransport();
1258
1476
  const client = options?.client || new QueueClient();
1259
1477
  const topic = new Topic(client, topicName, transport);
1260
- const {
1261
- messageId,
1262
- skipPayload,
1263
- client: _,
1264
- ...consumerGroupOptions
1265
- } = options || {};
1478
+ const { messageId, client: _, ...consumerGroupOptions } = options || {};
1266
1479
  const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1267
1480
  if (messageId) {
1268
- if (skipPayload) {
1269
- return consumer.consume(handler, {
1270
- messageId,
1271
- skipPayload: true
1272
- });
1273
- } else {
1274
- return consumer.consume(handler, { messageId });
1275
- }
1481
+ return consumer.consume(handler, { messageId });
1276
1482
  } else {
1277
1483
  return consumer.consume(handler);
1278
1484
  }
@@ -1306,23 +1512,39 @@ var Client = class {
1306
1512
  });
1307
1513
  }
1308
1514
  /**
1309
- * Create a callback handler for processing queue messages
1310
- * Returns a Next.js route handler function that routes messages to appropriate handlers
1311
- * @param handlers Object with topic-specific handlers organized by consumer groups
1515
+ * Create a callback handler for processing queue messages.
1516
+ * Returns a Next.js route handler function that routes messages to appropriate handlers.
1517
+ *
1518
+ * @param handlers - Object with topic-specific handlers organized by consumer groups
1519
+ * @param options - Optional configuration
1520
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1312
1521
  * @returns A Next.js route handler function
1313
1522
  *
1314
1523
  * @example
1315
1524
  * ```typescript
1525
+ * // Basic usage
1316
1526
  * export const POST = client.handleCallback({
1317
1527
  * "user-events": {
1318
1528
  * "welcome": (user, metadata) => console.log("Welcoming user", user),
1319
1529
  * "analytics": (user, metadata) => console.log("Tracking user", user),
1320
1530
  * },
1321
1531
  * });
1532
+ *
1533
+ * // With custom visibility timeout
1534
+ * export const POST = client.handleCallback({
1535
+ * "video-processing": {
1536
+ * "transcode": async (video) => await transcodeVideo(video),
1537
+ * },
1538
+ * }, {
1539
+ * visibilityTimeoutSeconds: 300, // 5 minutes for long operations
1540
+ * });
1322
1541
  * ```
1323
1542
  */
1324
- handleCallback(handlers) {
1325
- return handleCallback(handlers, this.client);
1543
+ handleCallback(handlers, options) {
1544
+ return handleCallback(handlers, {
1545
+ ...options,
1546
+ client: this.client
1547
+ });
1326
1548
  }
1327
1549
  };
1328
1550
  // Annotate the CommonJS export names for ESM import in node:
@@ -1330,10 +1552,15 @@ var Client = class {
1330
1552
  BadRequestError,
1331
1553
  BufferTransport,
1332
1554
  Client,
1555
+ ConcurrencyLimitError,
1556
+ ConsumerDiscoveryError,
1557
+ ConsumerRegistryNotConfiguredError,
1558
+ DuplicateMessageError,
1333
1559
  ForbiddenError,
1334
1560
  InternalServerError,
1335
1561
  InvalidLimitError,
1336
1562
  JsonTransport,
1563
+ MessageAlreadyProcessedError,
1337
1564
  MessageCorruptedError,
1338
1565
  MessageLockedError,
1339
1566
  MessageNotAvailableError,