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