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