@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.mjs CHANGED
@@ -19,6 +19,12 @@ var JsonTransport = class {
19
19
  contentType = "application/json";
20
20
  replacer;
21
21
  reviver;
22
+ /**
23
+ * Create a new JsonTransport.
24
+ * @param options - Optional JSON serialization options
25
+ * @param options.replacer - Custom replacer for JSON.stringify
26
+ * @param options.reviver - Custom reviver for JSON.parse
27
+ */
22
28
  constructor(options = {}) {
23
29
  this.replacer = options.replacer;
24
30
  this.reviver = options.reviver;
@@ -48,6 +54,10 @@ var StreamTransport = class {
48
54
  async deserialize(stream) {
49
55
  return stream;
50
56
  }
57
+ /**
58
+ * Consume any remaining stream data to prevent resource leaks.
59
+ * Called automatically by ConsumerGroup; manual call required for direct client usage.
60
+ */
51
61
  async finalize(payload) {
52
62
  const reader = payload.getReader();
53
63
  try {
@@ -64,8 +74,9 @@ var StreamTransport = class {
64
74
  // src/client.ts
65
75
  import { parseMultipartStream } from "mixpart";
66
76
 
67
- // src/oidc.ts
68
- import { getVercelOidcToken } from "@vercel/oidc";
77
+ // src/dev.ts
78
+ import * as fs from "fs";
79
+ import * as path from "path";
69
80
 
70
81
  // src/types.ts
71
82
  var MessageNotFoundError = class extends Error {
@@ -97,6 +108,7 @@ var QueueEmptyError = class extends Error {
97
108
  }
98
109
  };
99
110
  var MessageLockedError = class extends Error {
111
+ /** Suggested retry delay in seconds, if provided by the server. */
100
112
  retryAfter;
101
113
  constructor(messageId, retryAfter) {
102
114
  const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
@@ -135,6 +147,265 @@ var InvalidLimitError = class extends Error {
135
147
  this.name = "InvalidLimitError";
136
148
  }
137
149
  };
150
+ var MessageAlreadyProcessedError = class extends Error {
151
+ constructor(messageId) {
152
+ super(`Message ${messageId} has already been processed`);
153
+ this.name = "MessageAlreadyProcessedError";
154
+ }
155
+ };
156
+ var ConcurrencyLimitError = class extends Error {
157
+ /** Current number of in-flight messages for this consumer group. */
158
+ currentInflight;
159
+ /** Maximum allowed concurrent messages (as configured). */
160
+ maxConcurrency;
161
+ constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
162
+ super(message);
163
+ this.name = "ConcurrencyLimitError";
164
+ this.currentInflight = currentInflight;
165
+ this.maxConcurrency = maxConcurrency;
166
+ }
167
+ };
168
+ var DuplicateMessageError = class extends Error {
169
+ idempotencyKey;
170
+ constructor(message, idempotencyKey) {
171
+ super(message);
172
+ this.name = "DuplicateMessageError";
173
+ this.idempotencyKey = idempotencyKey;
174
+ }
175
+ };
176
+ var ConsumerDiscoveryError = class extends Error {
177
+ deploymentId;
178
+ constructor(message, deploymentId) {
179
+ super(message);
180
+ this.name = "ConsumerDiscoveryError";
181
+ this.deploymentId = deploymentId;
182
+ }
183
+ };
184
+ var ConsumerRegistryNotConfiguredError = class extends Error {
185
+ constructor(message = "Consumer registry not configured") {
186
+ super(message);
187
+ this.name = "ConsumerRegistryNotConfiguredError";
188
+ }
189
+ };
190
+
191
+ // src/dev.ts
192
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
193
+ function filePathToUrlPath(filePath) {
194
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
195
+ if (!urlPath.startsWith("/")) {
196
+ urlPath = "/" + urlPath;
197
+ }
198
+ return urlPath;
199
+ }
200
+ function getDevRouteMappings() {
201
+ const g = globalThis;
202
+ if (ROUTE_MAPPINGS_KEY in g) {
203
+ return g[ROUTE_MAPPINGS_KEY] ?? null;
204
+ }
205
+ try {
206
+ const vercelJsonPath = path.join(process.cwd(), "vercel.json");
207
+ if (!fs.existsSync(vercelJsonPath)) {
208
+ g[ROUTE_MAPPINGS_KEY] = null;
209
+ return null;
210
+ }
211
+ const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
212
+ if (!vercelJson.functions) {
213
+ g[ROUTE_MAPPINGS_KEY] = null;
214
+ return null;
215
+ }
216
+ const mappings = [];
217
+ for (const [filePath, config] of Object.entries(vercelJson.functions)) {
218
+ if (!config.experimentalTriggers) continue;
219
+ for (const trigger of config.experimentalTriggers) {
220
+ if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
221
+ mappings.push({
222
+ urlPath: filePathToUrlPath(filePath),
223
+ topic: trigger.topic,
224
+ consumer: trigger.consumer
225
+ });
226
+ }
227
+ }
228
+ }
229
+ g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
230
+ return g[ROUTE_MAPPINGS_KEY];
231
+ } catch (error) {
232
+ console.warn("[Dev Mode] Failed to read vercel.json:", error);
233
+ g[ROUTE_MAPPINGS_KEY] = null;
234
+ return null;
235
+ }
236
+ }
237
+ function findMatchingRoutes(topicName) {
238
+ const mappings = getDevRouteMappings();
239
+ if (!mappings) {
240
+ return [];
241
+ }
242
+ return mappings.filter((mapping) => {
243
+ if (mapping.topic.includes("*")) {
244
+ return matchesWildcardPattern(topicName, mapping.topic);
245
+ }
246
+ return mapping.topic === topicName;
247
+ });
248
+ }
249
+ function isDevMode() {
250
+ return process.env.NODE_ENV === "development";
251
+ }
252
+ var DEV_VISIBILITY_POLL_INTERVAL = 50;
253
+ var DEV_VISIBILITY_MAX_WAIT = 5e3;
254
+ var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
255
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
256
+ const client = new QueueClient();
257
+ const transport = new JsonTransport();
258
+ let elapsed = 0;
259
+ let interval = DEV_VISIBILITY_POLL_INTERVAL;
260
+ while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
261
+ try {
262
+ await client.receiveMessageById(
263
+ {
264
+ queueName: topicName,
265
+ consumerGroup,
266
+ messageId,
267
+ visibilityTimeoutSeconds: 0
268
+ },
269
+ transport
270
+ );
271
+ return true;
272
+ } catch (error) {
273
+ if (error instanceof MessageNotFoundError) {
274
+ await new Promise((resolve) => setTimeout(resolve, interval));
275
+ elapsed += interval;
276
+ interval = Math.min(
277
+ interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
278
+ DEV_VISIBILITY_MAX_WAIT - elapsed
279
+ );
280
+ continue;
281
+ }
282
+ if (error instanceof MessageAlreadyProcessedError) {
283
+ console.log(
284
+ `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
285
+ );
286
+ return false;
287
+ }
288
+ console.error(
289
+ `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
290
+ error
291
+ );
292
+ return false;
293
+ }
294
+ }
295
+ console.warn(
296
+ `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
297
+ );
298
+ return false;
299
+ }
300
+ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
301
+ if (delaySeconds && delaySeconds > 0) {
302
+ console.log(
303
+ `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
304
+ );
305
+ setTimeout(() => {
306
+ triggerDevCallbacks(topicName, messageId);
307
+ }, delaySeconds * 1e3);
308
+ return;
309
+ }
310
+ console.log(
311
+ `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
312
+ );
313
+ const matchingRoutes = findMatchingRoutes(topicName);
314
+ if (matchingRoutes.length === 0) {
315
+ console.log(
316
+ `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
317
+ );
318
+ return;
319
+ }
320
+ const consumerGroups = matchingRoutes.map((r) => r.consumer);
321
+ console.log(
322
+ `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
323
+ );
324
+ (async () => {
325
+ const firstRoute = matchingRoutes[0];
326
+ const isVisible = await waitForMessageVisibility(
327
+ topicName,
328
+ firstRoute.consumer,
329
+ messageId
330
+ );
331
+ if (!isVisible) {
332
+ console.warn(
333
+ `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
334
+ );
335
+ return;
336
+ }
337
+ const port = process.env.PORT || 3e3;
338
+ const baseUrl = `http://localhost:${port}`;
339
+ for (const route of matchingRoutes) {
340
+ const url = `${baseUrl}${route.urlPath}`;
341
+ console.log(
342
+ `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
343
+ );
344
+ const cloudEvent = {
345
+ type: "com.vercel.queue.v1beta",
346
+ source: `/topic/${topicName}/consumer/${route.consumer}`,
347
+ id: messageId,
348
+ datacontenttype: "application/json",
349
+ data: {
350
+ messageId,
351
+ queueName: topicName,
352
+ consumerGroup: route.consumer
353
+ },
354
+ time: (/* @__PURE__ */ new Date()).toISOString(),
355
+ specversion: "1.0"
356
+ };
357
+ try {
358
+ const response = await fetch(url, {
359
+ method: "POST",
360
+ headers: {
361
+ "Content-Type": "application/cloudevents+json"
362
+ },
363
+ body: JSON.stringify(cloudEvent)
364
+ });
365
+ if (response.ok) {
366
+ try {
367
+ const responseData = await response.json();
368
+ if (responseData.status === "success") {
369
+ console.log(
370
+ `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
371
+ );
372
+ }
373
+ } catch {
374
+ console.warn(
375
+ `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
376
+ );
377
+ }
378
+ } else {
379
+ try {
380
+ const errorData = await response.json();
381
+ console.error(
382
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
383
+ );
384
+ } catch {
385
+ console.error(
386
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
387
+ );
388
+ }
389
+ }
390
+ } catch (error) {
391
+ console.error(
392
+ `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
393
+ error
394
+ );
395
+ }
396
+ }
397
+ })();
398
+ }
399
+ function clearDevRouteMappings() {
400
+ const g = globalThis;
401
+ delete g[ROUTE_MAPPINGS_KEY];
402
+ }
403
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
404
+ globalThis.__clearDevRouteMappings = clearDevRouteMappings;
405
+ }
406
+
407
+ // src/oidc.ts
408
+ import { getVercelOidcToken } from "@vercel/oidc";
138
409
 
139
410
  // src/client.ts
140
411
  function isDebugEnabled() {
@@ -151,14 +422,6 @@ async function consumeStream(stream) {
151
422
  reader.releaseLock();
152
423
  }
153
424
  }
154
- function parseRetryAfter(headers) {
155
- const retryAfterHeader = headers.get("Retry-After");
156
- if (retryAfterHeader) {
157
- const parsed = parseInt(retryAfterHeader, 10);
158
- return Number.isNaN(parsed) ? void 0 : parsed;
159
- }
160
- return void 0;
161
- }
162
425
  function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
163
426
  if (status === 400) {
164
427
  throw new BadRequestError(errorText || badRequestDefault);
@@ -181,8 +444,8 @@ function parseQueueHeaders(headers) {
181
444
  const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
182
445
  const timestamp = headers.get("Vqs-Timestamp");
183
446
  const contentType = headers.get("Content-Type") || "application/octet-stream";
184
- const ticket = headers.get("Vqs-Ticket");
185
- if (!messageId || !timestamp || !ticket) {
447
+ const receiptHandle = headers.get("Vqs-Receipt-Handle");
448
+ if (!messageId || !timestamp || !receiptHandle) {
186
449
  return null;
187
450
  }
188
451
  const deliveryCount = parseInt(deliveryCountStr, 10);
@@ -194,7 +457,7 @@ function parseQueueHeaders(headers) {
194
457
  deliveryCount,
195
458
  createdAt: new Date(timestamp),
196
459
  contentType,
197
- ticket
460
+ receiptHandle
198
461
  };
199
462
  }
200
463
  var QueueClient = class {
@@ -202,15 +465,30 @@ var QueueClient = class {
202
465
  basePath;
203
466
  customHeaders;
204
467
  providedToken;
205
- /**
206
- * Create a new Vercel Queue Service client
207
- * @param options QueueClient configuration options
208
- */
468
+ defaultDeploymentId;
469
+ pinToDeployment;
209
470
  constructor(options = {}) {
210
471
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
211
- this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
472
+ this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
212
473
  this.customHeaders = options.headers || {};
213
474
  this.providedToken = options.token;
475
+ this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
476
+ this.pinToDeployment = options.pinToDeployment ?? true;
477
+ }
478
+ getSendDeploymentId() {
479
+ if (isDevMode()) {
480
+ return void 0;
481
+ }
482
+ if (this.pinToDeployment) {
483
+ return this.defaultDeploymentId;
484
+ }
485
+ return void 0;
486
+ }
487
+ getConsumeDeploymentId() {
488
+ if (isDevMode()) {
489
+ return void 0;
490
+ }
491
+ return this.defaultDeploymentId;
214
492
  }
215
493
  async getToken() {
216
494
  if (this.providedToken) {
@@ -224,10 +502,12 @@ var QueueClient = class {
224
502
  }
225
503
  return token;
226
504
  }
227
- /**
228
- * Internal fetch wrapper that automatically handles debug logging
229
- * when VERCEL_QUEUE_DEBUG is enabled
230
- */
505
+ buildUrl(queueName, ...pathSegments) {
506
+ const encodedQueue = encodeURIComponent(queueName);
507
+ const segments = pathSegments.map((s) => encodeURIComponent(s));
508
+ const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
509
+ return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
510
+ }
231
511
  async fetch(url, init) {
232
512
  const method = init.method || "GET";
233
513
  if (isDebugEnabled()) {
@@ -264,24 +544,38 @@ var QueueClient = class {
264
544
  return response;
265
545
  }
266
546
  /**
267
- * Send a message to a queue
268
- * @param options Send message options
269
- * @param transport Serializer/deserializer for the payload
270
- * @returns Promise with the message ID
271
- * @throws {BadRequestError} When request parameters are invalid
547
+ * Send a message to a topic.
548
+ *
549
+ * @param options - Message options including queue name, payload, and optional settings
550
+ * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
551
+ * @param options.payload - Message payload
552
+ * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
553
+ * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
554
+ * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
555
+ * @param transport - Serializer for the payload
556
+ * @returns Promise with the generated messageId
557
+ * @throws {DuplicateMessageError} When idempotency key was already used
558
+ * @throws {ConsumerDiscoveryError} When consumer discovery fails
559
+ * @throws {ConsumerRegistryNotConfiguredError} When registry not configured
560
+ * @throws {BadRequestError} When parameters are invalid
272
561
  * @throws {UnauthorizedError} When authentication fails
273
- * @throws {ForbiddenError} When access is denied (environment mismatch)
562
+ * @throws {ForbiddenError} When access is denied
274
563
  * @throws {InternalServerError} When server encounters an error
275
564
  */
276
565
  async sendMessage(options, transport) {
277
- const { queueName, payload, idempotencyKey, retentionSeconds } = options;
566
+ const {
567
+ queueName,
568
+ payload,
569
+ idempotencyKey,
570
+ retentionSeconds,
571
+ delaySeconds
572
+ } = options;
278
573
  const headers = new Headers({
279
574
  Authorization: `Bearer ${await this.getToken()}`,
280
- "Vqs-Queue-Name": queueName,
281
575
  "Content-Type": transport.contentType,
282
576
  ...this.customHeaders
283
577
  });
284
- const deploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
578
+ const deploymentId = this.getSendDeploymentId();
285
579
  if (deploymentId) {
286
580
  headers.set("Vqs-Deployment-Id", deploymentId);
287
581
  }
@@ -291,8 +585,12 @@ var QueueClient = class {
291
585
  if (retentionSeconds !== void 0) {
292
586
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
293
587
  }
294
- const body = transport.serialize(payload);
295
- const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
588
+ if (delaySeconds !== void 0) {
589
+ headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
590
+ }
591
+ const serialized = transport.serialize(payload);
592
+ const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
593
+ const response = await this.fetch(this.buildUrl(queueName), {
296
594
  method: "POST",
297
595
  body,
298
596
  headers
@@ -300,7 +598,21 @@ var QueueClient = class {
300
598
  if (!response.ok) {
301
599
  const errorText = await response.text();
302
600
  if (response.status === 409) {
303
- throw new Error("Duplicate idempotency key detected");
601
+ throw new DuplicateMessageError(
602
+ errorText || "Duplicate idempotency key detected",
603
+ idempotencyKey
604
+ );
605
+ }
606
+ if (response.status === 502) {
607
+ throw new ConsumerDiscoveryError(
608
+ errorText || "Consumer discovery failed",
609
+ deploymentId
610
+ );
611
+ }
612
+ if (response.status === 503) {
613
+ throw new ConsumerRegistryNotConfiguredError(
614
+ errorText || "Consumer registry not configured"
615
+ );
304
616
  }
305
617
  throwCommonHttpError(
306
618
  response.status,
@@ -313,52 +625,78 @@ var QueueClient = class {
313
625
  return responseData;
314
626
  }
315
627
  /**
316
- * Receive messages from a queue
317
- * @param options Receive messages options
318
- * @param transport Serializer/deserializer for the payload
319
- * @returns AsyncGenerator that yields messages as they arrive
320
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
321
- * @throws {QueueEmptyError} When no messages are available (204)
322
- * @throws {MessageLockedError} When messages are temporarily locked (423)
323
- * @throws {BadRequestError} When request parameters are invalid
628
+ * Receive messages from a topic as an async generator.
629
+ *
630
+ * @param options - Receive options
631
+ * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
632
+ * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
633
+ * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
634
+ * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
635
+ * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
636
+ * @param transport - Deserializer for message payloads
637
+ * @yields Message objects with payload, messageId, receiptHandle, etc.
638
+ * @throws {QueueEmptyError} When no messages available
639
+ * @throws {InvalidLimitError} When limit is outside 1-10 range
640
+ * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
641
+ * @throws {BadRequestError} When parameters are invalid
324
642
  * @throws {UnauthorizedError} When authentication fails
325
- * @throws {ForbiddenError} When access is denied (environment mismatch)
643
+ * @throws {ForbiddenError} When access is denied
326
644
  * @throws {InternalServerError} When server encounters an error
327
645
  */
328
646
  async *receiveMessages(options, transport) {
329
- const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
647
+ const {
648
+ queueName,
649
+ consumerGroup,
650
+ visibilityTimeoutSeconds,
651
+ limit,
652
+ maxConcurrency
653
+ } = options;
330
654
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
331
655
  throw new InvalidLimitError(limit);
332
656
  }
333
657
  const headers = new Headers({
334
658
  Authorization: `Bearer ${await this.getToken()}`,
335
- "Vqs-Queue-Name": queueName,
336
- "Vqs-Consumer-Group": consumerGroup,
337
659
  Accept: "multipart/mixed",
338
660
  ...this.customHeaders
339
661
  });
340
662
  if (visibilityTimeoutSeconds !== void 0) {
341
663
  headers.set(
342
- "Vqs-Visibility-Timeout",
664
+ "Vqs-Visibility-Timeout-Seconds",
343
665
  visibilityTimeoutSeconds.toString()
344
666
  );
345
667
  }
346
668
  if (limit !== void 0) {
347
- headers.set("Vqs-Limit", limit.toString());
669
+ headers.set("Vqs-Max-Messages", limit.toString());
348
670
  }
349
- const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
350
- method: "GET",
351
- headers
352
- });
671
+ if (maxConcurrency !== void 0) {
672
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
673
+ }
674
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
675
+ if (effectiveDeploymentId) {
676
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
677
+ }
678
+ const response = await this.fetch(
679
+ this.buildUrl(queueName, "consumer", consumerGroup),
680
+ {
681
+ method: "POST",
682
+ headers
683
+ }
684
+ );
353
685
  if (response.status === 204) {
354
686
  throw new QueueEmptyError(queueName, consumerGroup);
355
687
  }
356
688
  if (!response.ok) {
357
689
  const errorText = await response.text();
358
- if (response.status === 423) {
359
- throw new MessageLockedError(
360
- "next message",
361
- parseRetryAfter(response.headers)
690
+ if (response.status === 429) {
691
+ let errorData = {};
692
+ try {
693
+ errorData = JSON.parse(errorText);
694
+ } catch {
695
+ }
696
+ throw new ConcurrencyLimitError(
697
+ errorData.error || "Concurrency limit exceeded or throttled",
698
+ errorData.currentInflight,
699
+ errorData.maxConcurrency
362
700
  );
363
701
  }
364
702
  throwCommonHttpError(
@@ -390,34 +728,56 @@ var QueueClient = class {
390
728
  }
391
729
  }
392
730
  }
731
+ /**
732
+ * Receive a specific message by its ID.
733
+ *
734
+ * @param options - Receive options
735
+ * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
736
+ * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
737
+ * @param options.messageId - Message ID to retrieve
738
+ * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
739
+ * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
740
+ * @param transport - Deserializer for the message payload
741
+ * @returns Promise with the message
742
+ * @throws {MessageNotFoundError} When message doesn't exist
743
+ * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
744
+ * @throws {MessageAlreadyProcessedError} When message was already processed
745
+ * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
746
+ * @throws {BadRequestError} When parameters are invalid
747
+ * @throws {UnauthorizedError} When authentication fails
748
+ * @throws {ForbiddenError} When access is denied
749
+ * @throws {InternalServerError} When server encounters an error
750
+ */
393
751
  async receiveMessageById(options, transport) {
394
752
  const {
395
753
  queueName,
396
754
  consumerGroup,
397
755
  messageId,
398
756
  visibilityTimeoutSeconds,
399
- skipPayload
757
+ maxConcurrency
400
758
  } = options;
401
759
  const headers = new Headers({
402
760
  Authorization: `Bearer ${await this.getToken()}`,
403
- "Vqs-Queue-Name": queueName,
404
- "Vqs-Consumer-Group": consumerGroup,
405
761
  Accept: "multipart/mixed",
406
762
  ...this.customHeaders
407
763
  });
408
764
  if (visibilityTimeoutSeconds !== void 0) {
409
765
  headers.set(
410
- "Vqs-Visibility-Timeout",
766
+ "Vqs-Visibility-Timeout-Seconds",
411
767
  visibilityTimeoutSeconds.toString()
412
768
  );
413
769
  }
414
- if (skipPayload) {
415
- headers.set("Vqs-Skip-Payload", "1");
770
+ if (maxConcurrency !== void 0) {
771
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
772
+ }
773
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
774
+ if (effectiveDeploymentId) {
775
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
416
776
  }
417
777
  const response = await this.fetch(
418
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
778
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
419
779
  {
420
- method: "GET",
780
+ method: "POST",
421
781
  headers
422
782
  }
423
783
  );
@@ -427,12 +787,32 @@ var QueueClient = class {
427
787
  throw new MessageNotFoundError(messageId);
428
788
  }
429
789
  if (response.status === 409) {
790
+ let errorData = {};
791
+ try {
792
+ errorData = JSON.parse(errorText);
793
+ } catch {
794
+ }
795
+ if (errorData.originalMessageId) {
796
+ throw new MessageNotAvailableError(
797
+ messageId,
798
+ `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
799
+ );
800
+ }
430
801
  throw new MessageNotAvailableError(messageId);
431
802
  }
432
- if (response.status === 423) {
433
- throw new MessageLockedError(
434
- messageId,
435
- parseRetryAfter(response.headers)
803
+ if (response.status === 410) {
804
+ throw new MessageAlreadyProcessedError(messageId);
805
+ }
806
+ if (response.status === 429) {
807
+ let errorData = {};
808
+ try {
809
+ errorData = JSON.parse(errorText);
810
+ } catch {
811
+ }
812
+ throw new ConcurrencyLimitError(
813
+ errorData.error || "Concurrency limit exceeded or throttled",
814
+ errorData.currentInflight,
815
+ errorData.maxConcurrency
436
816
  );
437
817
  }
438
818
  throwCommonHttpError(
@@ -442,95 +822,73 @@ var QueueClient = class {
442
822
  "receive message by ID"
443
823
  );
444
824
  }
445
- if (skipPayload && response.status === 204) {
446
- const parsedHeaders = parseQueueHeaders(response.headers);
825
+ for await (const multipartMessage of parseMultipartStream(response)) {
826
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
447
827
  if (!parsedHeaders) {
828
+ await consumeStream(multipartMessage.payload);
448
829
  throw new MessageCorruptedError(
449
830
  messageId,
450
- "Missing required queue headers in 204 response"
831
+ "Missing required queue headers in response"
451
832
  );
452
833
  }
834
+ const deserializedPayload = await transport.deserialize(
835
+ multipartMessage.payload
836
+ );
453
837
  const message = {
454
838
  ...parsedHeaders,
455
- payload: void 0
839
+ payload: deserializedPayload
456
840
  };
457
841
  return { message };
458
842
  }
459
- if (!transport) {
460
- throw new Error("Transport is required when skipPayload is not true");
461
- }
462
- try {
463
- for await (const multipartMessage of parseMultipartStream(response)) {
464
- try {
465
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
466
- if (!parsedHeaders) {
467
- console.warn("Missing required queue headers in multipart part");
468
- await consumeStream(multipartMessage.payload);
469
- continue;
470
- }
471
- const deserializedPayload = await transport.deserialize(
472
- multipartMessage.payload
473
- );
474
- const message = {
475
- ...parsedHeaders,
476
- payload: deserializedPayload
477
- };
478
- return { message };
479
- } catch (error) {
480
- console.warn("Failed to deserialize message by ID:", error);
481
- await consumeStream(multipartMessage.payload);
482
- throw new MessageCorruptedError(
483
- messageId,
484
- `Failed to deserialize payload: ${error}`
485
- );
486
- }
487
- }
488
- } catch (error) {
489
- if (error instanceof MessageCorruptedError) {
490
- throw error;
491
- }
492
- throw new MessageCorruptedError(
493
- messageId,
494
- `Failed to parse multipart response: ${error}`
495
- );
496
- }
497
843
  throw new MessageNotFoundError(messageId);
498
844
  }
499
845
  /**
500
- * Delete a message (acknowledge processing)
501
- * @param options Delete message options
502
- * @returns Promise with delete status
503
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
504
- * @throws {MessageNotAvailableError} When message can't be deleted (409)
505
- * @throws {BadRequestError} When ticket is missing or invalid (400)
846
+ * Delete (acknowledge) a message after successful processing.
847
+ *
848
+ * @param options - Delete options
849
+ * @param options.queueName - Topic name
850
+ * @param options.consumerGroup - Consumer group name
851
+ * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
852
+ * @returns Promise indicating deletion success
853
+ * @throws {MessageNotFoundError} When receipt handle not found
854
+ * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
855
+ * @throws {BadRequestError} When parameters are invalid
506
856
  * @throws {UnauthorizedError} When authentication fails
507
- * @throws {ForbiddenError} When access is denied (environment mismatch)
857
+ * @throws {ForbiddenError} When access is denied
508
858
  * @throws {InternalServerError} When server encounters an error
509
859
  */
510
860
  async deleteMessage(options) {
511
- const { queueName, consumerGroup, messageId, ticket } = options;
861
+ const { queueName, consumerGroup, receiptHandle } = options;
862
+ const headers = new Headers({
863
+ Authorization: `Bearer ${await this.getToken()}`,
864
+ ...this.customHeaders
865
+ });
866
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
867
+ if (effectiveDeploymentId) {
868
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
869
+ }
512
870
  const response = await this.fetch(
513
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
871
+ this.buildUrl(
872
+ queueName,
873
+ "consumer",
874
+ consumerGroup,
875
+ "lease",
876
+ receiptHandle
877
+ ),
514
878
  {
515
879
  method: "DELETE",
516
- headers: new Headers({
517
- Authorization: `Bearer ${await this.getToken()}`,
518
- "Vqs-Queue-Name": queueName,
519
- "Vqs-Consumer-Group": consumerGroup,
520
- "Vqs-Ticket": ticket,
521
- ...this.customHeaders
522
- })
880
+ headers
523
881
  }
524
882
  );
525
883
  if (!response.ok) {
526
884
  const errorText = await response.text();
527
885
  if (response.status === 404) {
528
- throw new MessageNotFoundError(messageId);
886
+ throw new MessageNotFoundError(receiptHandle);
529
887
  }
530
888
  if (response.status === 409) {
531
889
  throw new MessageNotAvailableError(
532
- messageId,
533
- errorText || "Invalid ticket, message not in correct state, or already processed"
890
+ receiptHandle,
891
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
534
892
  );
535
893
  }
536
894
  throwCommonHttpError(
@@ -538,53 +896,67 @@ var QueueClient = class {
538
896
  response.statusText,
539
897
  errorText,
540
898
  "delete message",
541
- "Missing or invalid ticket"
899
+ "Missing or invalid receipt handle"
542
900
  );
543
901
  }
544
902
  return { deleted: true };
545
903
  }
546
904
  /**
547
- * Change the visibility timeout of a message
548
- * @param options Change visibility options
549
- * @returns Promise with update status
550
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
551
- * @throws {MessageNotAvailableError} When message can't be updated (409)
552
- * @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
905
+ * Extend or change the visibility timeout of a message.
906
+ * Used to prevent message redelivery while still processing.
907
+ *
908
+ * @param options - Visibility options
909
+ * @param options.queueName - Topic name
910
+ * @param options.consumerGroup - Consumer group name
911
+ * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
912
+ * @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
913
+ * @returns Promise indicating success
914
+ * @throws {MessageNotFoundError} When receipt handle not found
915
+ * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
916
+ * @throws {BadRequestError} When parameters are invalid
553
917
  * @throws {UnauthorizedError} When authentication fails
554
- * @throws {ForbiddenError} When access is denied (environment mismatch)
918
+ * @throws {ForbiddenError} When access is denied
555
919
  * @throws {InternalServerError} When server encounters an error
556
920
  */
557
921
  async changeVisibility(options) {
558
922
  const {
559
923
  queueName,
560
924
  consumerGroup,
561
- messageId,
562
- ticket,
925
+ receiptHandle,
563
926
  visibilityTimeoutSeconds
564
927
  } = options;
928
+ const headers = new Headers({
929
+ Authorization: `Bearer ${await this.getToken()}`,
930
+ "Content-Type": "application/json",
931
+ ...this.customHeaders
932
+ });
933
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
934
+ if (effectiveDeploymentId) {
935
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
936
+ }
565
937
  const response = await this.fetch(
566
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
938
+ this.buildUrl(
939
+ queueName,
940
+ "consumer",
941
+ consumerGroup,
942
+ "lease",
943
+ receiptHandle
944
+ ),
567
945
  {
568
946
  method: "PATCH",
569
- headers: new Headers({
570
- Authorization: `Bearer ${await this.getToken()}`,
571
- "Vqs-Queue-Name": queueName,
572
- "Vqs-Consumer-Group": consumerGroup,
573
- "Vqs-Ticket": ticket,
574
- "Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
575
- ...this.customHeaders
576
- })
947
+ headers,
948
+ body: JSON.stringify({ visibilityTimeoutSeconds })
577
949
  }
578
950
  );
579
951
  if (!response.ok) {
580
952
  const errorText = await response.text();
581
953
  if (response.status === 404) {
582
- throw new MessageNotFoundError(messageId);
954
+ throw new MessageNotFoundError(receiptHandle);
583
955
  }
584
956
  if (response.status === 409) {
585
957
  throw new MessageNotAvailableError(
586
- messageId,
587
- errorText || "Invalid ticket, message not in correct state, or already processed"
958
+ receiptHandle,
959
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
588
960
  );
589
961
  }
590
962
  throwCommonHttpError(
@@ -592,194 +964,72 @@ var QueueClient = class {
592
964
  response.statusText,
593
965
  errorText,
594
966
  "change visibility",
595
- "Missing ticket or invalid visibility timeout"
967
+ "Missing receipt handle or invalid visibility timeout"
596
968
  );
597
969
  }
598
- return { updated: true };
599
- }
600
- };
601
-
602
- // src/dev.ts
603
- var GLOBAL_KEY = Symbol.for("@vercel/queue.devHandlers");
604
- function getDevHandlerState() {
605
- const g = globalThis;
606
- if (!g[GLOBAL_KEY]) {
607
- g[GLOBAL_KEY] = {
608
- devRouteHandlers: /* @__PURE__ */ new Map(),
609
- wildcardRouteHandlers: /* @__PURE__ */ new Map()
610
- };
611
- }
612
- return g[GLOBAL_KEY];
613
- }
614
- var { devRouteHandlers, wildcardRouteHandlers } = getDevHandlerState();
615
- function cleanupDeadRefs(key, refs) {
616
- const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
617
- if (aliveRefs.length === 0) {
618
- wildcardRouteHandlers.delete(key);
619
- } else if (aliveRefs.length < refs.length) {
620
- wildcardRouteHandlers.set(key, aliveRefs);
970
+ return { success: true };
621
971
  }
622
- }
623
- function isDevMode() {
624
- return process.env.NODE_ENV === "development";
625
- }
626
- function registerDevRouteHandler(routeHandler, handlers) {
627
- for (const topicName in handlers) {
628
- for (const consumerGroup in handlers[topicName]) {
629
- const key = `${topicName}:${consumerGroup}`;
630
- if (topicName.includes("*")) {
631
- const existing = wildcardRouteHandlers.get(key) || [];
632
- cleanupDeadRefs(key, existing);
633
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
634
- const weakRef = new WeakRef(routeHandler);
635
- cleanedRefs.push(weakRef);
636
- wildcardRouteHandlers.set(key, cleanedRefs);
637
- } else {
638
- devRouteHandlers.set(key, {
639
- routeHandler,
640
- topicPattern: topicName
641
- });
642
- }
972
+ /**
973
+ * Alternative endpoint for changing message visibility timeout.
974
+ * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
975
+ * Functionally equivalent to changeVisibility but follows an alternative API pattern.
976
+ *
977
+ * @param options - Options for changing visibility
978
+ * @returns Promise resolving to change visibility response
979
+ */
980
+ async changeVisibilityAlt(options) {
981
+ const {
982
+ queueName,
983
+ consumerGroup,
984
+ receiptHandle,
985
+ visibilityTimeoutSeconds
986
+ } = options;
987
+ const headers = new Headers({
988
+ Authorization: `Bearer ${await this.getToken()}`,
989
+ "Content-Type": "application/json",
990
+ ...this.customHeaders
991
+ });
992
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
993
+ if (effectiveDeploymentId) {
994
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
643
995
  }
644
- }
645
- }
646
- function findRouteHandlersForTopic(topicName) {
647
- const handlersMap = /* @__PURE__ */ new Map();
648
- for (const [
649
- key,
650
- { routeHandler, topicPattern }
651
- ] of devRouteHandlers.entries()) {
652
- const [_, consumerGroup] = key.split(":");
653
- if (topicPattern === topicName) {
654
- if (!handlersMap.has(routeHandler)) {
655
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
996
+ const response = await this.fetch(
997
+ this.buildUrl(
998
+ queueName,
999
+ "consumer",
1000
+ consumerGroup,
1001
+ "lease",
1002
+ receiptHandle,
1003
+ "visibility"
1004
+ ),
1005
+ {
1006
+ method: "PATCH",
1007
+ headers,
1008
+ body: JSON.stringify({ visibilityTimeoutSeconds })
656
1009
  }
657
- handlersMap.get(routeHandler).add(consumerGroup);
658
- }
659
- }
660
- for (const [key, refs] of wildcardRouteHandlers.entries()) {
661
- const [pattern, consumerGroup] = key.split(":");
662
- if (matchesWildcardPattern(topicName, pattern)) {
663
- cleanupDeadRefs(key, refs);
664
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
665
- for (const ref of cleanedRefs) {
666
- const routeHandler = ref.deref();
667
- if (routeHandler) {
668
- if (!handlersMap.has(routeHandler)) {
669
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
670
- }
671
- handlersMap.get(routeHandler).add(consumerGroup);
672
- }
1010
+ );
1011
+ if (!response.ok) {
1012
+ const errorText = await response.text();
1013
+ if (response.status === 404) {
1014
+ throw new MessageNotFoundError(receiptHandle);
673
1015
  }
674
- }
675
- }
676
- return handlersMap;
677
- }
678
- function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
679
- const cloudEvent = {
680
- type: "com.vercel.queue.v1beta",
681
- source: `/topic/${topicName}/consumer/${consumerGroup}`,
682
- id: messageId,
683
- datacontenttype: "application/json",
684
- data: {
685
- messageId,
686
- queueName: topicName,
687
- consumerGroup
688
- },
689
- time: (/* @__PURE__ */ new Date()).toISOString(),
690
- specversion: "1.0"
691
- };
692
- return new Request("https://localhost/api/queue/callback", {
693
- method: "POST",
694
- headers: {
695
- "Content-Type": "application/cloudevents+json"
696
- },
697
- body: JSON.stringify(cloudEvent)
698
- });
699
- }
700
- var DEV_CALLBACK_DELAY = 1e3;
701
- function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
702
- console.log(
703
- `[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
704
- );
705
- setTimeout(
706
- () => {
707
- console.log(
708
- `[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
709
- );
710
- triggerDevCallbacks(topicName, messageId);
711
- },
712
- timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
713
- );
714
- }
715
- function triggerDevCallbacks(topicName, messageId) {
716
- const handlersMap = findRouteHandlersForTopic(topicName);
717
- if (handlersMap.size === 0) {
718
- return;
719
- }
720
- const consumerGroups = Array.from(
721
- new Set(
722
- Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
723
- )
724
- );
725
- console.log(
726
- `[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
727
- );
728
- setTimeout(async () => {
729
- for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
730
- for (const consumerGroup of consumerGroups2) {
731
- try {
732
- const request = createMockCloudEventRequest(
733
- topicName,
734
- consumerGroup,
735
- messageId
736
- );
737
- const response = await routeHandler(request);
738
- if (response.ok) {
739
- try {
740
- const responseData = await response.json();
741
- if (responseData.status === "success") {
742
- console.log(
743
- `[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
744
- );
745
- }
746
- } catch (jsonError) {
747
- console.error(
748
- `[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
749
- jsonError
750
- );
751
- }
752
- } else {
753
- try {
754
- const errorData = await response.json();
755
- console.error(
756
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
757
- errorData.error || response.statusText
758
- );
759
- } catch (jsonError) {
760
- console.error(
761
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
762
- response.statusText
763
- );
764
- }
765
- }
766
- } catch (error) {
767
- console.error(
768
- `[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
769
- error
770
- );
771
- }
1016
+ if (response.status === 409) {
1017
+ throw new MessageNotAvailableError(
1018
+ receiptHandle,
1019
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
1020
+ );
772
1021
  }
1022
+ throwCommonHttpError(
1023
+ response.status,
1024
+ response.statusText,
1025
+ errorText,
1026
+ "change visibility (alt)",
1027
+ "Missing receipt handle or invalid visibility timeout"
1028
+ );
773
1029
  }
774
- }, DEV_CALLBACK_DELAY);
775
- }
776
- function clearDevHandlers() {
777
- devRouteHandlers.clear();
778
- wildcardRouteHandlers.clear();
779
- }
780
- if (process.env.NODE_ENV === "test" || process.env.VITEST) {
781
- globalThis.__clearDevHandlers = clearDevHandlers;
782
- }
1030
+ return { success: true };
1031
+ }
1032
+ };
783
1033
 
784
1034
  // src/consumer-group.ts
785
1035
  var ConsumerGroup = class {
@@ -790,69 +1040,64 @@ var ConsumerGroup = class {
790
1040
  refreshInterval;
791
1041
  transport;
792
1042
  /**
793
- * Create a new ConsumerGroup instance
794
- * @param client QueueClient instance to use for API calls
795
- * @param topicName Name of the topic to consume from
796
- * @param consumerGroupName Name of the consumer group
797
- * @param options Optional configuration
1043
+ * Create a new ConsumerGroup instance.
1044
+ *
1045
+ * @param client - QueueClient instance to use for API calls
1046
+ * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
1047
+ * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1048
+ * @param options - Optional configuration
1049
+ * @param options.transport - Payload serializer (default: JsonTransport)
1050
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1051
+ * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
798
1052
  */
799
1053
  constructor(client, topicName, consumerGroupName, options = {}) {
800
1054
  this.client = client;
801
1055
  this.topicName = topicName;
802
1056
  this.consumerGroupName = consumerGroupName;
803
- this.visibilityTimeout = options.visibilityTimeoutSeconds || 30;
804
- this.refreshInterval = options.refreshInterval || 10;
1057
+ this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1058
+ this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
805
1059
  this.transport = options.transport || new JsonTransport();
806
1060
  }
807
1061
  /**
808
1062
  * Starts a background loop that periodically extends the visibility timeout for a message.
809
- * This prevents the message from becoming visible to other consumers while it's being processed.
810
- *
811
- * The extension loop runs every `refreshInterval` seconds and updates the message's
812
- * visibility timeout to `visibilityTimeout` seconds from the current time.
813
- *
814
- * @param messageId - The unique identifier of the message to extend visibility for
815
- * @param ticket - The receipt ticket that proves ownership of the message
816
- * @returns A function that when called will stop the extension loop
817
- *
818
- * @remarks
819
- * - The first extension attempt occurs after `refreshInterval` seconds, not immediately
820
- * - If an extension fails, the loop terminates with an error logged to console
821
- * - The returned stop function is idempotent - calling it multiple times is safe
822
- * - By default, the stop function returns immediately without waiting for in-flight
823
- * - Pass `true` to the stop function to wait for any in-flight extension to complete
824
1063
  */
825
- startVisibilityExtension(messageId, ticket) {
1064
+ startVisibilityExtension(receiptHandle) {
826
1065
  let isRunning = true;
1066
+ let isResolved = false;
827
1067
  let resolveLifecycle;
828
1068
  let timeoutId = null;
829
1069
  const lifecyclePromise = new Promise((resolve) => {
830
1070
  resolveLifecycle = resolve;
831
1071
  });
1072
+ const safeResolve = () => {
1073
+ if (!isResolved) {
1074
+ isResolved = true;
1075
+ resolveLifecycle();
1076
+ }
1077
+ };
832
1078
  const extend = async () => {
833
1079
  if (!isRunning) {
834
- resolveLifecycle();
1080
+ safeResolve();
835
1081
  return;
836
1082
  }
837
1083
  try {
838
1084
  await this.client.changeVisibility({
839
1085
  queueName: this.topicName,
840
1086
  consumerGroup: this.consumerGroupName,
841
- messageId,
842
- ticket,
1087
+ receiptHandle,
843
1088
  visibilityTimeoutSeconds: this.visibilityTimeout
844
1089
  });
845
1090
  if (isRunning) {
846
1091
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
847
1092
  } else {
848
- resolveLifecycle();
1093
+ safeResolve();
849
1094
  }
850
1095
  } catch (error) {
851
1096
  console.error(
852
- `Failed to extend visibility for message ${messageId}:`,
1097
+ `Failed to extend visibility for receipt handle ${receiptHandle}:`,
853
1098
  error
854
1099
  );
855
- resolveLifecycle();
1100
+ safeResolve();
856
1101
  }
857
1102
  };
858
1103
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
@@ -865,22 +1110,14 @@ var ConsumerGroup = class {
865
1110
  if (waitForCompletion) {
866
1111
  await lifecyclePromise;
867
1112
  } else {
868
- resolveLifecycle();
1113
+ safeResolve();
869
1114
  }
870
1115
  };
871
1116
  }
872
- /**
873
- * Process a single message with the given handler
874
- * @param message The message to process
875
- * @param handler Function to process the message
876
- */
877
1117
  async processMessage(message, handler) {
878
- const stopExtension = this.startVisibilityExtension(
879
- message.messageId,
880
- message.ticket
881
- );
1118
+ const stopExtension = this.startVisibilityExtension(message.receiptHandle);
882
1119
  try {
883
- const result = await handler(message.payload, {
1120
+ await handler(message.payload, {
884
1121
  messageId: message.messageId,
885
1122
  deliveryCount: message.deliveryCount,
886
1123
  createdAt: message.createdAt,
@@ -888,29 +1125,11 @@ var ConsumerGroup = class {
888
1125
  consumerGroup: this.consumerGroupName
889
1126
  });
890
1127
  await stopExtension();
891
- if (result && "timeoutSeconds" in result) {
892
- await this.client.changeVisibility({
893
- queueName: this.topicName,
894
- consumerGroup: this.consumerGroupName,
895
- messageId: message.messageId,
896
- ticket: message.ticket,
897
- visibilityTimeoutSeconds: result.timeoutSeconds
898
- });
899
- if (isDevMode()) {
900
- scheduleDevTimeout(
901
- this.topicName,
902
- message.messageId,
903
- result.timeoutSeconds
904
- );
905
- }
906
- } else {
907
- await this.client.deleteMessage({
908
- queueName: this.topicName,
909
- consumerGroup: this.consumerGroupName,
910
- messageId: message.messageId,
911
- ticket: message.ticket
912
- });
913
- }
1128
+ await this.client.deleteMessage({
1129
+ queueName: this.topicName,
1130
+ consumerGroup: this.consumerGroupName,
1131
+ receiptHandle: message.receiptHandle
1132
+ });
914
1133
  } catch (error) {
915
1134
  await stopExtension();
916
1135
  if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
@@ -925,36 +1144,16 @@ var ConsumerGroup = class {
925
1144
  }
926
1145
  async consume(handler, options) {
927
1146
  if (options?.messageId) {
928
- if (options.skipPayload) {
929
- const response = await this.client.receiveMessageById(
930
- {
931
- queueName: this.topicName,
932
- consumerGroup: this.consumerGroupName,
933
- messageId: options.messageId,
934
- visibilityTimeoutSeconds: this.visibilityTimeout,
935
- skipPayload: true
936
- },
937
- this.transport
938
- );
939
- await this.processMessage(
940
- response.message,
941
- handler
942
- );
943
- } else {
944
- const response = await this.client.receiveMessageById(
945
- {
946
- queueName: this.topicName,
947
- consumerGroup: this.consumerGroupName,
948
- messageId: options.messageId,
949
- visibilityTimeoutSeconds: this.visibilityTimeout
950
- },
951
- this.transport
952
- );
953
- await this.processMessage(
954
- response.message,
955
- handler
956
- );
957
- }
1147
+ const response = await this.client.receiveMessageById(
1148
+ {
1149
+ queueName: this.topicName,
1150
+ consumerGroup: this.consumerGroupName,
1151
+ messageId: options.messageId,
1152
+ visibilityTimeoutSeconds: this.visibilityTimeout
1153
+ },
1154
+ this.transport
1155
+ );
1156
+ await this.processMessage(response.message, handler);
958
1157
  } else {
959
1158
  let messageFound = false;
960
1159
  for await (const message of this.client.receiveMessages(
@@ -1022,7 +1221,7 @@ var Topic = class {
1022
1221
  payload,
1023
1222
  idempotencyKey: options?.idempotencyKey,
1024
1223
  retentionSeconds: options?.retentionSeconds,
1025
- deploymentId: options?.deploymentId
1224
+ delaySeconds: options?.delaySeconds
1026
1225
  },
1027
1226
  this.transport
1028
1227
  );
@@ -1132,7 +1331,7 @@ async function parseCallback(request) {
1132
1331
  messageId
1133
1332
  };
1134
1333
  }
1135
- function createCallbackHandler(handlers, client) {
1334
+ function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1136
1335
  for (const topicPattern in handlers) {
1137
1336
  if (topicPattern.includes("*")) {
1138
1337
  if (!validateWildcardPattern(topicPattern)) {
@@ -1168,7 +1367,10 @@ function createCallbackHandler(handlers, client) {
1168
1367
  );
1169
1368
  }
1170
1369
  const topic = new Topic(client, queueName);
1171
- const cg = topic.consumerGroup(consumerGroup);
1370
+ const cg = topic.consumerGroup(
1371
+ consumerGroup,
1372
+ visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1373
+ );
1172
1374
  await cg.consume(consumerGroupHandler, { messageId });
1173
1375
  return Response.json({ status: "success" });
1174
1376
  } catch (error) {
@@ -1182,13 +1384,14 @@ function createCallbackHandler(handlers, client) {
1182
1384
  );
1183
1385
  }
1184
1386
  };
1185
- if (isDevMode()) {
1186
- registerDevRouteHandler(routeHandler, handlers);
1187
- }
1188
1387
  return routeHandler;
1189
1388
  }
1190
- function handleCallback(handlers, client) {
1191
- return createCallbackHandler(handlers, client || new QueueClient());
1389
+ function handleCallback(handlers, options) {
1390
+ return createCallbackHandler(
1391
+ handlers,
1392
+ options?.client || new QueueClient(),
1393
+ options?.visibilityTimeoutSeconds
1394
+ );
1192
1395
  }
1193
1396
 
1194
1397
  // src/factory.ts
@@ -1201,12 +1404,12 @@ async function send(topicName, payload, options) {
1201
1404
  payload,
1202
1405
  idempotencyKey: options?.idempotencyKey,
1203
1406
  retentionSeconds: options?.retentionSeconds,
1204
- deploymentId: options?.deploymentId
1407
+ delaySeconds: options?.delaySeconds
1205
1408
  },
1206
1409
  transport
1207
1410
  );
1208
1411
  if (isDevMode()) {
1209
- triggerDevCallbacks(topicName, result.messageId);
1412
+ triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1210
1413
  }
1211
1414
  return { messageId: result.messageId };
1212
1415
  }
@@ -1214,22 +1417,10 @@ async function receive(topicName, consumerGroup, handler, options) {
1214
1417
  const transport = options?.transport || new JsonTransport();
1215
1418
  const client = options?.client || new QueueClient();
1216
1419
  const topic = new Topic(client, topicName, transport);
1217
- const {
1218
- messageId,
1219
- skipPayload,
1220
- client: _,
1221
- ...consumerGroupOptions
1222
- } = options || {};
1420
+ const { messageId, client: _, ...consumerGroupOptions } = options || {};
1223
1421
  const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1224
1422
  if (messageId) {
1225
- if (skipPayload) {
1226
- return consumer.consume(handler, {
1227
- messageId,
1228
- skipPayload: true
1229
- });
1230
- } else {
1231
- return consumer.consume(handler, { messageId });
1232
- }
1423
+ return consumer.consume(handler, { messageId });
1233
1424
  } else {
1234
1425
  return consumer.consume(handler);
1235
1426
  }
@@ -1263,33 +1454,54 @@ var Client = class {
1263
1454
  });
1264
1455
  }
1265
1456
  /**
1266
- * Create a callback handler for processing queue messages
1267
- * Returns a Next.js route handler function that routes messages to appropriate handlers
1268
- * @param handlers Object with topic-specific handlers organized by consumer groups
1457
+ * Create a callback handler for processing queue messages.
1458
+ * Returns a Next.js route handler function that routes messages to appropriate handlers.
1459
+ *
1460
+ * @param handlers - Object with topic-specific handlers organized by consumer groups
1461
+ * @param options - Optional configuration
1462
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1269
1463
  * @returns A Next.js route handler function
1270
1464
  *
1271
1465
  * @example
1272
1466
  * ```typescript
1467
+ * // Basic usage
1273
1468
  * export const POST = client.handleCallback({
1274
1469
  * "user-events": {
1275
1470
  * "welcome": (user, metadata) => console.log("Welcoming user", user),
1276
1471
  * "analytics": (user, metadata) => console.log("Tracking user", user),
1277
1472
  * },
1278
1473
  * });
1474
+ *
1475
+ * // With custom visibility timeout
1476
+ * export const POST = client.handleCallback({
1477
+ * "video-processing": {
1478
+ * "transcode": async (video) => await transcodeVideo(video),
1479
+ * },
1480
+ * }, {
1481
+ * visibilityTimeoutSeconds: 300, // 5 minutes for long operations
1482
+ * });
1279
1483
  * ```
1280
1484
  */
1281
- handleCallback(handlers) {
1282
- return handleCallback(handlers, this.client);
1485
+ handleCallback(handlers, options) {
1486
+ return handleCallback(handlers, {
1487
+ ...options,
1488
+ client: this.client
1489
+ });
1283
1490
  }
1284
1491
  };
1285
1492
  export {
1286
1493
  BadRequestError,
1287
1494
  BufferTransport,
1288
1495
  Client,
1496
+ ConcurrencyLimitError,
1497
+ ConsumerDiscoveryError,
1498
+ ConsumerRegistryNotConfiguredError,
1499
+ DuplicateMessageError,
1289
1500
  ForbiddenError,
1290
1501
  InternalServerError,
1291
1502
  InvalidLimitError,
1292
1503
  JsonTransport,
1504
+ MessageAlreadyProcessedError,
1293
1505
  MessageCorruptedError,
1294
1506
  MessageLockedError,
1295
1507
  MessageNotAvailableError,