@vercel/queue 0.0.0-alpha.32 → 0.0.0-alpha.34

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