@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.
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -22,10 +32,16 @@ var index_exports = {};
22
32
  __export(index_exports, {
23
33
  BadRequestError: () => BadRequestError,
24
34
  BufferTransport: () => BufferTransport,
35
+ Client: () => Client,
36
+ ConcurrencyLimitError: () => ConcurrencyLimitError,
37
+ ConsumerDiscoveryError: () => ConsumerDiscoveryError,
38
+ ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
39
+ DuplicateMessageError: () => DuplicateMessageError,
25
40
  ForbiddenError: () => ForbiddenError,
26
41
  InternalServerError: () => InternalServerError,
27
42
  InvalidLimitError: () => InvalidLimitError,
28
43
  JsonTransport: () => JsonTransport,
44
+ MessageAlreadyProcessedError: () => MessageAlreadyProcessedError,
29
45
  MessageCorruptedError: () => MessageCorruptedError,
30
46
  MessageLockedError: () => MessageLockedError,
31
47
  MessageNotAvailableError: () => MessageNotAvailableError,
@@ -106,8 +122,9 @@ var StreamTransport = class {
106
122
  // src/client.ts
107
123
  var import_mixpart = require("mixpart");
108
124
 
109
- // src/oidc.ts
110
- var import_oidc = require("@vercel/oidc");
125
+ // src/dev.ts
126
+ var fs = __toESM(require("fs"));
127
+ var path = __toESM(require("path"));
111
128
 
112
129
  // src/types.ts
113
130
  var MessageNotFoundError = class extends Error {
@@ -177,8 +194,268 @@ var InvalidLimitError = class extends Error {
177
194
  this.name = "InvalidLimitError";
178
195
  }
179
196
  };
197
+ var MessageAlreadyProcessedError = class extends Error {
198
+ constructor(messageId) {
199
+ super(`Message ${messageId} has already been processed`);
200
+ this.name = "MessageAlreadyProcessedError";
201
+ }
202
+ };
203
+ var ConcurrencyLimitError = class extends Error {
204
+ currentInflight;
205
+ maxConcurrency;
206
+ constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
207
+ super(message);
208
+ this.name = "ConcurrencyLimitError";
209
+ this.currentInflight = currentInflight;
210
+ this.maxConcurrency = maxConcurrency;
211
+ }
212
+ };
213
+ var DuplicateMessageError = class extends Error {
214
+ idempotencyKey;
215
+ constructor(message, idempotencyKey) {
216
+ super(message);
217
+ this.name = "DuplicateMessageError";
218
+ this.idempotencyKey = idempotencyKey;
219
+ }
220
+ };
221
+ var ConsumerDiscoveryError = class extends Error {
222
+ deploymentId;
223
+ constructor(message, deploymentId) {
224
+ super(message);
225
+ this.name = "ConsumerDiscoveryError";
226
+ this.deploymentId = deploymentId;
227
+ }
228
+ };
229
+ var ConsumerRegistryNotConfiguredError = class extends Error {
230
+ constructor(message = "Consumer registry not configured") {
231
+ super(message);
232
+ this.name = "ConsumerRegistryNotConfiguredError";
233
+ }
234
+ };
235
+
236
+ // src/dev.ts
237
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
238
+ function filePathToUrlPath(filePath) {
239
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
240
+ if (!urlPath.startsWith("/")) {
241
+ urlPath = "/" + urlPath;
242
+ }
243
+ return urlPath;
244
+ }
245
+ function getDevRouteMappings() {
246
+ const g = globalThis;
247
+ if (ROUTE_MAPPINGS_KEY in g) {
248
+ return g[ROUTE_MAPPINGS_KEY] ?? null;
249
+ }
250
+ try {
251
+ const vercelJsonPath = path.join(process.cwd(), "vercel.json");
252
+ if (!fs.existsSync(vercelJsonPath)) {
253
+ g[ROUTE_MAPPINGS_KEY] = null;
254
+ return null;
255
+ }
256
+ const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
257
+ if (!vercelJson.functions) {
258
+ g[ROUTE_MAPPINGS_KEY] = null;
259
+ return null;
260
+ }
261
+ const mappings = [];
262
+ for (const [filePath, config] of Object.entries(vercelJson.functions)) {
263
+ if (!config.experimentalTriggers) continue;
264
+ for (const trigger of config.experimentalTriggers) {
265
+ if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
266
+ mappings.push({
267
+ urlPath: filePathToUrlPath(filePath),
268
+ topic: trigger.topic,
269
+ consumer: trigger.consumer
270
+ });
271
+ }
272
+ }
273
+ }
274
+ g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
275
+ return g[ROUTE_MAPPINGS_KEY];
276
+ } catch (error) {
277
+ console.warn("[Dev Mode] Failed to read vercel.json:", error);
278
+ g[ROUTE_MAPPINGS_KEY] = null;
279
+ return null;
280
+ }
281
+ }
282
+ function findMatchingRoutes(topicName) {
283
+ const mappings = getDevRouteMappings();
284
+ if (!mappings) {
285
+ return [];
286
+ }
287
+ return mappings.filter((mapping) => {
288
+ if (mapping.topic.includes("*")) {
289
+ return matchesWildcardPattern(topicName, mapping.topic);
290
+ }
291
+ return mapping.topic === topicName;
292
+ });
293
+ }
294
+ function isDevMode() {
295
+ return process.env.NODE_ENV === "development";
296
+ }
297
+ var DEV_VISIBILITY_POLL_INTERVAL = 50;
298
+ var DEV_VISIBILITY_MAX_WAIT = 5e3;
299
+ var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
300
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
301
+ const client = new QueueClient();
302
+ const transport = new JsonTransport();
303
+ let elapsed = 0;
304
+ let interval = DEV_VISIBILITY_POLL_INTERVAL;
305
+ while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
306
+ try {
307
+ await client.receiveMessageById(
308
+ {
309
+ queueName: topicName,
310
+ consumerGroup,
311
+ messageId,
312
+ visibilityTimeoutSeconds: 0
313
+ },
314
+ transport
315
+ );
316
+ return true;
317
+ } catch (error) {
318
+ if (error instanceof MessageNotFoundError) {
319
+ await new Promise((resolve) => setTimeout(resolve, interval));
320
+ elapsed += interval;
321
+ interval = Math.min(
322
+ interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
323
+ DEV_VISIBILITY_MAX_WAIT - elapsed
324
+ );
325
+ continue;
326
+ }
327
+ if (error instanceof MessageAlreadyProcessedError) {
328
+ console.log(
329
+ `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
330
+ );
331
+ return false;
332
+ }
333
+ console.error(
334
+ `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
335
+ error
336
+ );
337
+ return false;
338
+ }
339
+ }
340
+ console.warn(
341
+ `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
342
+ );
343
+ return false;
344
+ }
345
+ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
346
+ if (delaySeconds && delaySeconds > 0) {
347
+ console.log(
348
+ `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
349
+ );
350
+ setTimeout(() => {
351
+ triggerDevCallbacks(topicName, messageId);
352
+ }, delaySeconds * 1e3);
353
+ return;
354
+ }
355
+ console.log(
356
+ `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
357
+ );
358
+ const matchingRoutes = findMatchingRoutes(topicName);
359
+ if (matchingRoutes.length === 0) {
360
+ console.log(
361
+ `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
362
+ );
363
+ return;
364
+ }
365
+ const consumerGroups = matchingRoutes.map((r) => r.consumer);
366
+ console.log(
367
+ `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
368
+ );
369
+ (async () => {
370
+ const firstRoute = matchingRoutes[0];
371
+ const isVisible = await waitForMessageVisibility(
372
+ topicName,
373
+ firstRoute.consumer,
374
+ messageId
375
+ );
376
+ if (!isVisible) {
377
+ console.warn(
378
+ `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
379
+ );
380
+ return;
381
+ }
382
+ const port = process.env.PORT || 3e3;
383
+ const baseUrl = `http://localhost:${port}`;
384
+ for (const route of matchingRoutes) {
385
+ const url = `${baseUrl}${route.urlPath}`;
386
+ console.log(
387
+ `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
388
+ );
389
+ const cloudEvent = {
390
+ type: "com.vercel.queue.v1beta",
391
+ source: `/topic/${topicName}/consumer/${route.consumer}`,
392
+ id: messageId,
393
+ datacontenttype: "application/json",
394
+ data: {
395
+ messageId,
396
+ queueName: topicName,
397
+ consumerGroup: route.consumer
398
+ },
399
+ time: (/* @__PURE__ */ new Date()).toISOString(),
400
+ specversion: "1.0"
401
+ };
402
+ try {
403
+ const response = await fetch(url, {
404
+ method: "POST",
405
+ headers: {
406
+ "Content-Type": "application/cloudevents+json"
407
+ },
408
+ body: JSON.stringify(cloudEvent)
409
+ });
410
+ if (response.ok) {
411
+ try {
412
+ const responseData = await response.json();
413
+ if (responseData.status === "success") {
414
+ console.log(
415
+ `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
416
+ );
417
+ }
418
+ } catch {
419
+ console.warn(
420
+ `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
421
+ );
422
+ }
423
+ } else {
424
+ try {
425
+ const errorData = await response.json();
426
+ console.error(
427
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
428
+ );
429
+ } catch {
430
+ console.error(
431
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
432
+ );
433
+ }
434
+ }
435
+ } catch (error) {
436
+ console.error(
437
+ `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
438
+ error
439
+ );
440
+ }
441
+ }
442
+ })();
443
+ }
444
+ function clearDevRouteMappings() {
445
+ const g = globalThis;
446
+ delete g[ROUTE_MAPPINGS_KEY];
447
+ }
448
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
449
+ globalThis.__clearDevRouteMappings = clearDevRouteMappings;
450
+ }
451
+
452
+ // src/oidc.ts
453
+ var import_oidc = require("@vercel/oidc");
180
454
 
181
455
  // src/client.ts
456
+ function isDebugEnabled() {
457
+ return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
458
+ }
182
459
  async function consumeStream(stream) {
183
460
  const reader = stream.getReader();
184
461
  try {
@@ -190,17 +467,34 @@ async function consumeStream(stream) {
190
467
  reader.releaseLock();
191
468
  }
192
469
  }
470
+ function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
471
+ if (status === 400) {
472
+ throw new BadRequestError(errorText || badRequestDefault);
473
+ }
474
+ if (status === 401) {
475
+ throw new UnauthorizedError(errorText || void 0);
476
+ }
477
+ if (status === 403) {
478
+ throw new ForbiddenError(errorText || void 0);
479
+ }
480
+ if (status >= 500) {
481
+ throw new InternalServerError(
482
+ errorText || `Server error: ${status} ${statusText}`
483
+ );
484
+ }
485
+ throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
486
+ }
193
487
  function parseQueueHeaders(headers) {
194
488
  const messageId = headers.get("Vqs-Message-Id");
195
489
  const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
196
490
  const timestamp = headers.get("Vqs-Timestamp");
197
491
  const contentType = headers.get("Content-Type") || "application/octet-stream";
198
- const ticket = headers.get("Vqs-Ticket");
199
- if (!messageId || !timestamp || !ticket) {
492
+ const receiptHandle = headers.get("Vqs-Receipt-Handle");
493
+ if (!messageId || !timestamp || !receiptHandle) {
200
494
  return null;
201
495
  }
202
496
  const deliveryCount = parseInt(deliveryCountStr, 10);
203
- if (isNaN(deliveryCount)) {
497
+ if (Number.isNaN(deliveryCount)) {
204
498
  return null;
205
499
  }
206
500
  return {
@@ -208,30 +502,43 @@ function parseQueueHeaders(headers) {
208
502
  deliveryCount,
209
503
  createdAt: new Date(timestamp),
210
504
  contentType,
211
- ticket
505
+ receiptHandle
212
506
  };
213
507
  }
214
508
  var QueueClient = class {
215
509
  baseUrl;
216
510
  basePath;
217
- customHeaders = {};
218
- /**
219
- * Create a new Vercel Queue Service client
220
- * @param options Client configuration options
221
- */
511
+ customHeaders;
512
+ providedToken;
513
+ defaultDeploymentId;
514
+ pinToDeployment;
222
515
  constructor(options = {}) {
223
516
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
224
- this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
225
- const VERCEL_QUEUE_HEADER_PREFIX = "VERCEL_QUEUE_HEADER_";
226
- this.customHeaders = Object.fromEntries(
227
- Object.entries(process.env).filter(([key]) => key.startsWith(VERCEL_QUEUE_HEADER_PREFIX)).map(([key, value]) => [
228
- // This allows headers to use dashes independent of shell used
229
- key.replace(VERCEL_QUEUE_HEADER_PREFIX, "").replaceAll("__", "-"),
230
- value || ""
231
- ])
232
- );
517
+ this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
518
+ this.customHeaders = options.headers || {};
519
+ this.providedToken = options.token;
520
+ this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
521
+ this.pinToDeployment = options.pinToDeployment ?? true;
522
+ }
523
+ getSendDeploymentId() {
524
+ if (isDevMode()) {
525
+ return void 0;
526
+ }
527
+ if (this.pinToDeployment) {
528
+ return this.defaultDeploymentId;
529
+ }
530
+ return void 0;
531
+ }
532
+ getConsumeDeploymentId() {
533
+ if (isDevMode()) {
534
+ return void 0;
535
+ }
536
+ return this.defaultDeploymentId;
233
537
  }
234
538
  async getToken() {
539
+ if (this.providedToken) {
540
+ return this.providedToken;
541
+ }
235
542
  const token = await (0, import_oidc.getVercelOidcToken)();
236
543
  if (!token) {
237
544
  throw new Error(
@@ -240,25 +547,61 @@ var QueueClient = class {
240
547
  }
241
548
  return token;
242
549
  }
243
- /**
244
- * Send a message to a queue
245
- * @param options Send message options
246
- * @param transport Serializer/deserializer for the payload
247
- * @returns Promise with the message ID
248
- * @throws {BadRequestError} When request parameters are invalid
249
- * @throws {UnauthorizedError} When authentication fails
250
- * @throws {ForbiddenError} When access is denied (environment mismatch)
251
- * @throws {InternalServerError} When server encounters an error
252
- */
550
+ buildUrl(queueName, ...pathSegments) {
551
+ const encodedQueue = encodeURIComponent(queueName);
552
+ const segments = pathSegments.map((s) => encodeURIComponent(s));
553
+ const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
554
+ return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
555
+ }
556
+ async fetch(url, init) {
557
+ const method = init.method || "GET";
558
+ if (isDebugEnabled()) {
559
+ const logData = {
560
+ method,
561
+ url,
562
+ headers: init.headers
563
+ };
564
+ const body = init.body;
565
+ if (body !== void 0 && body !== null) {
566
+ if (body instanceof ArrayBuffer) {
567
+ logData.bodySize = body.byteLength;
568
+ } else if (body instanceof Uint8Array) {
569
+ logData.bodySize = body.byteLength;
570
+ } else if (typeof body === "string") {
571
+ logData.bodySize = body.length;
572
+ } else {
573
+ logData.bodyType = typeof body;
574
+ }
575
+ }
576
+ console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
577
+ }
578
+ const response = await fetch(url, init);
579
+ if (isDebugEnabled()) {
580
+ const logData = {
581
+ method,
582
+ url,
583
+ status: response.status,
584
+ statusText: response.statusText,
585
+ headers: response.headers
586
+ };
587
+ console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
588
+ }
589
+ return response;
590
+ }
253
591
  async sendMessage(options, transport) {
254
- const { queueName, payload, idempotencyKey, retentionSeconds } = options;
592
+ const {
593
+ queueName,
594
+ payload,
595
+ idempotencyKey,
596
+ retentionSeconds,
597
+ delaySeconds
598
+ } = options;
255
599
  const headers = new Headers({
256
600
  Authorization: `Bearer ${await this.getToken()}`,
257
- "Vqs-Queue-Name": queueName,
258
601
  "Content-Type": transport.contentType,
259
602
  ...this.customHeaders
260
603
  });
261
- const deploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
604
+ const deploymentId = this.getSendDeploymentId();
262
605
  if (deploymentId) {
263
606
  headers.set("Vqs-Deployment-Id", deploymentId);
264
607
  }
@@ -268,106 +611,106 @@ var QueueClient = class {
268
611
  if (retentionSeconds !== void 0) {
269
612
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
270
613
  }
271
- const body = transport.serialize(payload);
272
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
614
+ if (delaySeconds !== void 0) {
615
+ headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
616
+ }
617
+ const serialized = transport.serialize(payload);
618
+ const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
619
+ const response = await this.fetch(this.buildUrl(queueName), {
273
620
  method: "POST",
274
621
  body,
275
622
  headers
276
623
  });
277
624
  if (!response.ok) {
278
- if (response.status === 400) {
279
- const errorText = await response.text();
280
- throw new BadRequestError(errorText || "Invalid parameters");
281
- }
282
- if (response.status === 401) {
283
- throw new UnauthorizedError();
284
- }
285
- if (response.status === 403) {
286
- throw new ForbiddenError();
287
- }
625
+ const errorText = await response.text();
288
626
  if (response.status === 409) {
289
- throw new Error("Duplicate idempotency key detected");
627
+ throw new DuplicateMessageError(
628
+ errorText || "Duplicate idempotency key detected",
629
+ idempotencyKey
630
+ );
290
631
  }
291
- if (response.status >= 500) {
292
- throw new InternalServerError(
293
- `Server error: ${response.status} ${response.statusText}`
632
+ if (response.status === 502) {
633
+ throw new ConsumerDiscoveryError(
634
+ errorText || "Consumer discovery failed",
635
+ deploymentId
294
636
  );
295
637
  }
296
- throw new Error(
297
- `Failed to send message: ${response.status} ${response.statusText}`
638
+ if (response.status === 503) {
639
+ throw new ConsumerRegistryNotConfiguredError(
640
+ errorText || "Consumer registry not configured"
641
+ );
642
+ }
643
+ throwCommonHttpError(
644
+ response.status,
645
+ response.statusText,
646
+ errorText,
647
+ "send message"
298
648
  );
299
649
  }
300
650
  const responseData = await response.json();
301
651
  return responseData;
302
652
  }
303
- /**
304
- * Receive messages from a queue
305
- * @param options Receive messages options
306
- * @param transport Serializer/deserializer for the payload
307
- * @returns AsyncGenerator that yields messages as they arrive
308
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
309
- * @throws {QueueEmptyError} When no messages are available (204)
310
- * @throws {MessageLockedError} When messages are temporarily locked (423)
311
- * @throws {BadRequestError} When request parameters are invalid
312
- * @throws {UnauthorizedError} When authentication fails
313
- * @throws {ForbiddenError} When access is denied (environment mismatch)
314
- * @throws {InternalServerError} When server encounters an error
315
- */
316
653
  async *receiveMessages(options, transport) {
317
- const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
654
+ const {
655
+ queueName,
656
+ consumerGroup,
657
+ visibilityTimeoutSeconds,
658
+ limit,
659
+ maxConcurrency
660
+ } = options;
318
661
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
319
662
  throw new InvalidLimitError(limit);
320
663
  }
321
664
  const headers = new Headers({
322
665
  Authorization: `Bearer ${await this.getToken()}`,
323
- "Vqs-Queue-Name": queueName,
324
- "Vqs-Consumer-Group": consumerGroup,
325
666
  Accept: "multipart/mixed",
326
667
  ...this.customHeaders
327
668
  });
328
669
  if (visibilityTimeoutSeconds !== void 0) {
329
670
  headers.set(
330
- "Vqs-Visibility-Timeout",
671
+ "Vqs-Visibility-Timeout-Seconds",
331
672
  visibilityTimeoutSeconds.toString()
332
673
  );
333
674
  }
334
675
  if (limit !== void 0) {
335
- headers.set("Vqs-Limit", limit.toString());
676
+ headers.set("Vqs-Max-Messages", limit.toString());
336
677
  }
337
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
338
- method: "GET",
339
- headers
340
- });
678
+ if (maxConcurrency !== void 0) {
679
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
680
+ }
681
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
682
+ if (effectiveDeploymentId) {
683
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
684
+ }
685
+ const response = await this.fetch(
686
+ this.buildUrl(queueName, "consumer", consumerGroup),
687
+ {
688
+ method: "POST",
689
+ headers
690
+ }
691
+ );
341
692
  if (response.status === 204) {
342
693
  throw new QueueEmptyError(queueName, consumerGroup);
343
694
  }
344
695
  if (!response.ok) {
345
- if (response.status === 400) {
346
- const errorText = await response.text();
347
- throw new BadRequestError(errorText || "Invalid parameters");
348
- }
349
- if (response.status === 401) {
350
- throw new UnauthorizedError();
351
- }
352
- if (response.status === 403) {
353
- throw new ForbiddenError();
354
- }
355
- if (response.status === 423) {
356
- const retryAfterHeader = response.headers.get("Retry-After");
357
- let retryAfter;
358
- if (retryAfterHeader) {
359
- const parsed = parseInt(retryAfterHeader, 10);
360
- retryAfter = isNaN(parsed) ? void 0 : parsed;
696
+ const errorText = await response.text();
697
+ if (response.status === 429) {
698
+ let errorData = {};
699
+ try {
700
+ errorData = JSON.parse(errorText);
701
+ } catch {
361
702
  }
362
- throw new MessageLockedError("next message", retryAfter);
363
- }
364
- if (response.status >= 500) {
365
- throw new InternalServerError(
366
- `Server error: ${response.status} ${response.statusText}`
703
+ throw new ConcurrencyLimitError(
704
+ errorData.error || "Concurrency limit exceeded or throttled",
705
+ errorData.currentInflight,
706
+ errorData.maxConcurrency
367
707
  );
368
708
  }
369
- throw new Error(
370
- `Failed to receive messages: ${response.status} ${response.statusText}`
709
+ throwCommonHttpError(
710
+ response.status,
711
+ response.statusText,
712
+ errorText,
713
+ "receive messages"
371
714
  );
372
715
  }
373
716
  for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
@@ -398,550 +741,250 @@ var QueueClient = class {
398
741
  consumerGroup,
399
742
  messageId,
400
743
  visibilityTimeoutSeconds,
401
- skipPayload
744
+ maxConcurrency
402
745
  } = options;
403
746
  const headers = new Headers({
404
747
  Authorization: `Bearer ${await this.getToken()}`,
405
- "Vqs-Queue-Name": queueName,
406
- "Vqs-Consumer-Group": consumerGroup,
407
748
  Accept: "multipart/mixed",
408
749
  ...this.customHeaders
409
750
  });
410
751
  if (visibilityTimeoutSeconds !== void 0) {
411
752
  headers.set(
412
- "Vqs-Visibility-Timeout",
753
+ "Vqs-Visibility-Timeout-Seconds",
413
754
  visibilityTimeoutSeconds.toString()
414
755
  );
415
756
  }
416
- if (skipPayload) {
417
- headers.set("Vqs-Skip-Payload", "1");
757
+ if (maxConcurrency !== void 0) {
758
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
418
759
  }
419
- const response = await fetch(
420
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
760
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
761
+ if (effectiveDeploymentId) {
762
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
763
+ }
764
+ const response = await this.fetch(
765
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
421
766
  {
422
- method: "GET",
767
+ method: "POST",
423
768
  headers
424
769
  }
425
770
  );
426
771
  if (!response.ok) {
427
- if (response.status === 400) {
428
- const errorText = await response.text();
429
- throw new BadRequestError(errorText || "Invalid parameters");
430
- }
431
- if (response.status === 401) {
432
- throw new UnauthorizedError();
433
- }
434
- if (response.status === 403) {
435
- throw new ForbiddenError();
436
- }
772
+ const errorText = await response.text();
437
773
  if (response.status === 404) {
438
774
  throw new MessageNotFoundError(messageId);
439
775
  }
440
- if (response.status === 423) {
441
- const retryAfterHeader = response.headers.get("Retry-After");
442
- let retryAfter;
443
- if (retryAfterHeader) {
444
- const parsed = parseInt(retryAfterHeader, 10);
445
- retryAfter = isNaN(parsed) ? void 0 : parsed;
446
- }
447
- throw new MessageLockedError(messageId, retryAfter);
448
- }
449
776
  if (response.status === 409) {
777
+ let errorData = {};
778
+ try {
779
+ errorData = JSON.parse(errorText);
780
+ } catch {
781
+ }
782
+ if (errorData.originalMessageId) {
783
+ throw new MessageNotAvailableError(
784
+ messageId,
785
+ `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
786
+ );
787
+ }
450
788
  throw new MessageNotAvailableError(messageId);
451
789
  }
452
- if (response.status >= 500) {
453
- throw new InternalServerError(
454
- `Server error: ${response.status} ${response.statusText}`
790
+ if (response.status === 410) {
791
+ throw new MessageAlreadyProcessedError(messageId);
792
+ }
793
+ if (response.status === 429) {
794
+ let errorData = {};
795
+ try {
796
+ errorData = JSON.parse(errorText);
797
+ } catch {
798
+ }
799
+ throw new ConcurrencyLimitError(
800
+ errorData.error || "Concurrency limit exceeded or throttled",
801
+ errorData.currentInflight,
802
+ errorData.maxConcurrency
455
803
  );
456
804
  }
457
- throw new Error(
458
- `Failed to receive message by ID: ${response.status} ${response.statusText}`
805
+ throwCommonHttpError(
806
+ response.status,
807
+ response.statusText,
808
+ errorText,
809
+ "receive message by ID"
459
810
  );
460
811
  }
461
- if (skipPayload && response.status === 204) {
462
- const parsedHeaders = parseQueueHeaders(response.headers);
812
+ for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
813
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
463
814
  if (!parsedHeaders) {
815
+ await consumeStream(multipartMessage.payload);
464
816
  throw new MessageCorruptedError(
465
817
  messageId,
466
- "Missing required queue headers in 204 response"
818
+ "Missing required queue headers in response"
467
819
  );
468
820
  }
821
+ const deserializedPayload = await transport.deserialize(
822
+ multipartMessage.payload
823
+ );
469
824
  const message = {
470
825
  ...parsedHeaders,
471
- payload: void 0
826
+ payload: deserializedPayload
472
827
  };
473
828
  return { message };
474
829
  }
475
- if (!transport) {
476
- throw new Error("Transport is required when skipPayload is not true");
477
- }
478
- try {
479
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
480
- try {
481
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
482
- if (!parsedHeaders) {
483
- console.warn("Missing required queue headers in multipart part");
484
- await consumeStream(multipartMessage.payload);
485
- continue;
486
- }
487
- const deserializedPayload = await transport.deserialize(
488
- multipartMessage.payload
489
- );
490
- const message = {
491
- ...parsedHeaders,
492
- payload: deserializedPayload
493
- };
494
- return { message };
495
- } catch (error) {
496
- console.warn("Failed to deserialize message by ID:", error);
497
- await consumeStream(multipartMessage.payload);
498
- throw new MessageCorruptedError(
499
- messageId,
500
- `Failed to deserialize payload: ${error}`
501
- );
502
- }
503
- }
504
- } catch (error) {
505
- if (error instanceof MessageCorruptedError) {
506
- throw error;
507
- }
508
- throw new MessageCorruptedError(
509
- messageId,
510
- `Failed to parse multipart response: ${error}`
511
- );
512
- }
513
830
  throw new MessageNotFoundError(messageId);
514
831
  }
515
- /**
516
- * Delete a message (acknowledge processing)
517
- * @param options Delete message options
518
- * @returns Promise with delete status
519
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
520
- * @throws {MessageNotAvailableError} When message can't be deleted (409)
521
- * @throws {BadRequestError} When ticket is missing or invalid (400)
522
- * @throws {UnauthorizedError} When authentication fails
523
- * @throws {ForbiddenError} When access is denied (environment mismatch)
524
- * @throws {InternalServerError} When server encounters an error
525
- */
526
832
  async deleteMessage(options) {
527
- const { queueName, consumerGroup, messageId, ticket } = options;
528
- const response = await fetch(
529
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
833
+ const { queueName, consumerGroup, receiptHandle } = options;
834
+ const headers = new Headers({
835
+ Authorization: `Bearer ${await this.getToken()}`,
836
+ ...this.customHeaders
837
+ });
838
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
839
+ if (effectiveDeploymentId) {
840
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
841
+ }
842
+ const response = await this.fetch(
843
+ this.buildUrl(
844
+ queueName,
845
+ "consumer",
846
+ consumerGroup,
847
+ "lease",
848
+ receiptHandle
849
+ ),
530
850
  {
531
851
  method: "DELETE",
532
- headers: new Headers({
533
- Authorization: `Bearer ${await this.getToken()}`,
534
- "Vqs-Queue-Name": queueName,
535
- "Vqs-Consumer-Group": consumerGroup,
536
- "Vqs-Ticket": ticket,
537
- ...this.customHeaders
538
- })
852
+ headers
539
853
  }
540
854
  );
541
855
  if (!response.ok) {
542
- if (response.status === 400) {
543
- throw new BadRequestError("Missing or invalid ticket");
544
- }
545
- if (response.status === 401) {
546
- throw new UnauthorizedError();
547
- }
548
- if (response.status === 403) {
549
- throw new ForbiddenError();
550
- }
856
+ const errorText = await response.text();
551
857
  if (response.status === 404) {
552
- throw new MessageNotFoundError(messageId);
858
+ throw new MessageNotFoundError(receiptHandle);
553
859
  }
554
860
  if (response.status === 409) {
555
861
  throw new MessageNotAvailableError(
556
- messageId,
557
- "Invalid ticket, message not in correct state, or already processed"
558
- );
559
- }
560
- if (response.status >= 500) {
561
- throw new InternalServerError(
562
- `Server error: ${response.status} ${response.statusText}`
862
+ receiptHandle,
863
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
563
864
  );
564
865
  }
565
- throw new Error(
566
- `Failed to delete message: ${response.status} ${response.statusText}`
866
+ throwCommonHttpError(
867
+ response.status,
868
+ response.statusText,
869
+ errorText,
870
+ "delete message",
871
+ "Missing or invalid receipt handle"
567
872
  );
568
873
  }
569
874
  return { deleted: true };
570
875
  }
571
- /**
572
- * Change the visibility timeout of a message
573
- * @param options Change visibility options
574
- * @returns Promise with update status
575
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
576
- * @throws {MessageNotAvailableError} When message can't be updated (409)
577
- * @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
578
- * @throws {UnauthorizedError} When authentication fails
579
- * @throws {ForbiddenError} When access is denied (environment mismatch)
580
- * @throws {InternalServerError} When server encounters an error
581
- */
582
876
  async changeVisibility(options) {
583
877
  const {
584
878
  queueName,
585
879
  consumerGroup,
586
- messageId,
587
- ticket,
880
+ receiptHandle,
588
881
  visibilityTimeoutSeconds
589
882
  } = options;
590
- const response = await fetch(
591
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
883
+ const headers = new Headers({
884
+ Authorization: `Bearer ${await this.getToken()}`,
885
+ "Content-Type": "application/json",
886
+ ...this.customHeaders
887
+ });
888
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
889
+ if (effectiveDeploymentId) {
890
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
891
+ }
892
+ const response = await this.fetch(
893
+ this.buildUrl(
894
+ queueName,
895
+ "consumer",
896
+ consumerGroup,
897
+ "lease",
898
+ receiptHandle
899
+ ),
592
900
  {
593
901
  method: "PATCH",
594
- headers: new Headers({
595
- Authorization: `Bearer ${await this.getToken()}`,
596
- "Vqs-Queue-Name": queueName,
597
- "Vqs-Consumer-Group": consumerGroup,
598
- "Vqs-Ticket": ticket,
599
- "Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
600
- ...this.customHeaders
601
- })
902
+ headers,
903
+ body: JSON.stringify({ visibilityTimeoutSeconds })
602
904
  }
603
905
  );
604
906
  if (!response.ok) {
605
- if (response.status === 400) {
606
- throw new BadRequestError(
607
- "Missing ticket or invalid visibility timeout"
608
- );
609
- }
610
- if (response.status === 401) {
611
- throw new UnauthorizedError();
612
- }
613
- if (response.status === 403) {
614
- throw new ForbiddenError();
615
- }
907
+ const errorText = await response.text();
616
908
  if (response.status === 404) {
617
- throw new MessageNotFoundError(messageId);
909
+ throw new MessageNotFoundError(receiptHandle);
618
910
  }
619
911
  if (response.status === 409) {
620
912
  throw new MessageNotAvailableError(
621
- messageId,
622
- "Invalid ticket, message not in correct state, or already processed"
623
- );
624
- }
625
- if (response.status >= 500) {
626
- throw new InternalServerError(
627
- `Server error: ${response.status} ${response.statusText}`
913
+ receiptHandle,
914
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
628
915
  );
629
916
  }
630
- throw new Error(
631
- `Failed to change visibility: ${response.status} ${response.statusText}`
917
+ throwCommonHttpError(
918
+ response.status,
919
+ response.statusText,
920
+ errorText,
921
+ "change visibility",
922
+ "Missing receipt handle or invalid visibility timeout"
632
923
  );
633
924
  }
634
- return { updated: true };
635
- }
636
- };
637
-
638
- // src/callback.ts
639
- function validateWildcardPattern(pattern) {
640
- const firstIndex = pattern.indexOf("*");
641
- const lastIndex = pattern.lastIndexOf("*");
642
- if (firstIndex !== lastIndex) {
643
- return false;
644
- }
645
- if (firstIndex === -1) {
646
- return false;
647
- }
648
- if (firstIndex !== pattern.length - 1) {
649
- return false;
925
+ return { success: true };
650
926
  }
651
- return true;
652
- }
653
- function matchesWildcardPattern(topicName, pattern) {
654
- const prefix = pattern.slice(0, -1);
655
- return topicName.startsWith(prefix);
656
- }
657
- function findTopicHandler(queueName, handlers) {
658
- const exactHandler = handlers[queueName];
659
- if (exactHandler) {
660
- return exactHandler;
661
- }
662
- for (const pattern in handlers) {
663
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
664
- return handlers[pattern];
927
+ /**
928
+ * Alternative endpoint for changing message visibility timeout.
929
+ * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
930
+ * Functionally equivalent to changeVisibility but follows an alternative API pattern.
931
+ *
932
+ * @param options - Options for changing visibility
933
+ * @returns Promise resolving to change visibility response
934
+ */
935
+ async changeVisibilityAlt(options) {
936
+ const {
937
+ queueName,
938
+ consumerGroup,
939
+ receiptHandle,
940
+ visibilityTimeoutSeconds
941
+ } = options;
942
+ const headers = new Headers({
943
+ Authorization: `Bearer ${await this.getToken()}`,
944
+ "Content-Type": "application/json",
945
+ ...this.customHeaders
946
+ });
947
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
948
+ if (effectiveDeploymentId) {
949
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
665
950
  }
666
- }
667
- return null;
668
- }
669
- async function parseCallback(request) {
670
- const contentType = request.headers.get("content-type");
671
- if (!contentType || !contentType.includes("application/cloudevents+json")) {
672
- throw new Error(
673
- "Invalid content type: expected 'application/cloudevents+json'"
674
- );
675
- }
676
- let cloudEvent;
677
- try {
678
- cloudEvent = await request.json();
679
- } catch (error) {
680
- throw new Error("Failed to parse CloudEvent from request body");
681
- }
682
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
683
- throw new Error("Invalid CloudEvent: missing required fields");
684
- }
685
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
686
- throw new Error(
687
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
688
- );
689
- }
690
- const missingFields = [];
691
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
692
- if (!("consumerGroup" in cloudEvent.data))
693
- missingFields.push("consumerGroup");
694
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
695
- if (missingFields.length > 0) {
696
- throw new Error(
697
- `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
698
- );
699
- }
700
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
701
- return {
702
- queueName,
703
- consumerGroup,
704
- messageId
705
- };
706
- }
707
- function handleCallback(handlers) {
708
- for (const topicPattern in handlers) {
709
- if (topicPattern.includes("*")) {
710
- if (!validateWildcardPattern(topicPattern)) {
711
- throw new Error(
712
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
713
- );
951
+ const response = await this.fetch(
952
+ this.buildUrl(
953
+ queueName,
954
+ "consumer",
955
+ consumerGroup,
956
+ "lease",
957
+ receiptHandle,
958
+ "visibility"
959
+ ),
960
+ {
961
+ method: "PATCH",
962
+ headers,
963
+ body: JSON.stringify({ visibilityTimeoutSeconds })
714
964
  }
715
- }
716
- }
717
- const routeHandler = async (request) => {
718
- try {
719
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
720
- const topicHandler = findTopicHandler(queueName, handlers);
721
- if (!topicHandler) {
722
- const availableTopics = Object.keys(handlers).join(", ");
723
- return Response.json(
724
- {
725
- error: `No handler found for topic: ${queueName}`,
726
- availableTopics
727
- },
728
- { status: 404 }
729
- );
965
+ );
966
+ if (!response.ok) {
967
+ const errorText = await response.text();
968
+ if (response.status === 404) {
969
+ throw new MessageNotFoundError(receiptHandle);
730
970
  }
731
- const consumerGroupHandler = topicHandler[consumerGroup];
732
- if (!consumerGroupHandler) {
733
- const availableGroups = Object.keys(topicHandler).join(", ");
734
- return Response.json(
735
- {
736
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
737
- availableGroups
738
- },
739
- { status: 404 }
971
+ if (response.status === 409) {
972
+ throw new MessageNotAvailableError(
973
+ receiptHandle,
974
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
740
975
  );
741
976
  }
742
- const client = new QueueClient();
743
- const topic = new Topic(client, queueName);
744
- const cg = topic.consumerGroup(consumerGroup);
745
- await cg.consume(consumerGroupHandler, { messageId });
746
- return Response.json({ status: "success" });
747
- } catch (error) {
748
- console.error("Queue callback error:", error);
749
- if (error instanceof Error && (error.message.includes("Missing required CloudEvent data fields") || error.message.includes("Invalid CloudEvent") || error.message.includes("Invalid CloudEvent type") || error.message.includes("Invalid content type") || error.message.includes("Failed to parse CloudEvent"))) {
750
- return Response.json({ error: error.message }, { status: 400 });
751
- }
752
- return Response.json(
753
- { error: "Failed to process queue message" },
754
- { status: 500 }
977
+ throwCommonHttpError(
978
+ response.status,
979
+ response.statusText,
980
+ errorText,
981
+ "change visibility (alt)",
982
+ "Missing receipt handle or invalid visibility timeout"
755
983
  );
756
984
  }
757
- };
758
- if (isDevMode()) {
759
- registerDevRouteHandler(routeHandler, handlers);
760
- }
761
- return routeHandler;
762
- }
763
-
764
- // src/dev.ts
765
- var GLOBAL_KEY = Symbol.for("@vercel/queue.devHandlers");
766
- function getDevHandlerState() {
767
- const g = globalThis;
768
- if (!g[GLOBAL_KEY]) {
769
- g[GLOBAL_KEY] = {
770
- devRouteHandlers: /* @__PURE__ */ new Map(),
771
- wildcardRouteHandlers: /* @__PURE__ */ new Map()
772
- };
773
- }
774
- return g[GLOBAL_KEY];
775
- }
776
- var { devRouteHandlers, wildcardRouteHandlers } = getDevHandlerState();
777
- function cleanupDeadRefs(key, refs) {
778
- const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
779
- if (aliveRefs.length === 0) {
780
- wildcardRouteHandlers.delete(key);
781
- } else if (aliveRefs.length < refs.length) {
782
- wildcardRouteHandlers.set(key, aliveRefs);
783
- }
784
- }
785
- function isDevMode() {
786
- return process.env.NODE_ENV === "development";
787
- }
788
- function registerDevRouteHandler(routeHandler, handlers) {
789
- for (const topicName in handlers) {
790
- for (const consumerGroup in handlers[topicName]) {
791
- const key = `${topicName}:${consumerGroup}`;
792
- if (topicName.includes("*")) {
793
- const existing = wildcardRouteHandlers.get(key) || [];
794
- cleanupDeadRefs(key, existing);
795
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
796
- const weakRef = new WeakRef(routeHandler);
797
- cleanedRefs.push(weakRef);
798
- wildcardRouteHandlers.set(key, cleanedRefs);
799
- } else {
800
- devRouteHandlers.set(key, {
801
- routeHandler,
802
- topicPattern: topicName
803
- });
804
- }
805
- }
806
- }
807
- }
808
- function findRouteHandlersForTopic(topicName) {
809
- const handlersMap = /* @__PURE__ */ new Map();
810
- for (const [
811
- key,
812
- { routeHandler, topicPattern }
813
- ] of devRouteHandlers.entries()) {
814
- const [_, consumerGroup] = key.split(":");
815
- if (topicPattern === topicName) {
816
- if (!handlersMap.has(routeHandler)) {
817
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
818
- }
819
- handlersMap.get(routeHandler).add(consumerGroup);
820
- }
985
+ return { success: true };
821
986
  }
822
- for (const [key, refs] of wildcardRouteHandlers.entries()) {
823
- const [pattern, consumerGroup] = key.split(":");
824
- if (matchesWildcardPattern(topicName, pattern)) {
825
- cleanupDeadRefs(key, refs);
826
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
827
- for (const ref of cleanedRefs) {
828
- const routeHandler = ref.deref();
829
- if (routeHandler) {
830
- if (!handlersMap.has(routeHandler)) {
831
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
832
- }
833
- handlersMap.get(routeHandler).add(consumerGroup);
834
- }
835
- }
836
- }
837
- }
838
- return handlersMap;
839
- }
840
- function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
841
- const cloudEvent = {
842
- type: "com.vercel.queue.v1beta",
843
- source: `/topic/${topicName}/consumer/${consumerGroup}`,
844
- id: messageId,
845
- datacontenttype: "application/json",
846
- data: {
847
- messageId,
848
- queueName: topicName,
849
- consumerGroup
850
- },
851
- time: (/* @__PURE__ */ new Date()).toISOString(),
852
- specversion: "1.0"
853
- };
854
- return new Request("https://localhost/api/queue/callback", {
855
- method: "POST",
856
- headers: {
857
- "Content-Type": "application/cloudevents+json"
858
- },
859
- body: JSON.stringify(cloudEvent)
860
- });
861
- }
862
- var DEV_CALLBACK_DELAY = 1e3;
863
- function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
864
- console.log(
865
- `[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
866
- );
867
- setTimeout(
868
- () => {
869
- console.log(
870
- `[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
871
- );
872
- triggerDevCallbacks(topicName, messageId);
873
- },
874
- timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
875
- );
876
- }
877
- function triggerDevCallbacks(topicName, messageId) {
878
- const handlersMap = findRouteHandlersForTopic(topicName);
879
- if (handlersMap.size === 0) {
880
- return;
881
- }
882
- const consumerGroups = Array.from(
883
- new Set(
884
- Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
885
- )
886
- );
887
- console.log(
888
- `[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
889
- );
890
- setTimeout(async () => {
891
- for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
892
- for (const consumerGroup of consumerGroups2) {
893
- try {
894
- const request = createMockCloudEventRequest(
895
- topicName,
896
- consumerGroup,
897
- messageId
898
- );
899
- const response = await routeHandler(request);
900
- if (response.ok) {
901
- try {
902
- const responseData = await response.json();
903
- if (responseData.status === "success") {
904
- console.log(
905
- `[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
906
- );
907
- }
908
- } catch (jsonError) {
909
- console.error(
910
- `[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
911
- jsonError
912
- );
913
- }
914
- } else {
915
- try {
916
- const errorData = await response.json();
917
- console.error(
918
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
919
- errorData.error || response.statusText
920
- );
921
- } catch (jsonError) {
922
- console.error(
923
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
924
- response.statusText
925
- );
926
- }
927
- }
928
- } catch (error) {
929
- console.error(
930
- `[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
931
- error
932
- );
933
- }
934
- }
935
- }
936
- }, DEV_CALLBACK_DELAY);
937
- }
938
- function clearDevHandlers() {
939
- devRouteHandlers.clear();
940
- wildcardRouteHandlers.clear();
941
- }
942
- if (process.env.NODE_ENV === "test" || process.env.VITEST) {
943
- globalThis.__clearDevHandlers = clearDevHandlers;
944
- }
987
+ };
945
988
 
946
989
  // src/consumer-group.ts
947
990
  var ConsumerGroup = class {
@@ -973,8 +1016,7 @@ var ConsumerGroup = class {
973
1016
  * The extension loop runs every `refreshInterval` seconds and updates the message's
974
1017
  * visibility timeout to `visibilityTimeout` seconds from the current time.
975
1018
  *
976
- * @param messageId - The unique identifier of the message to extend visibility for
977
- * @param ticket - The receipt ticket that proves ownership of the message
1019
+ * @param receiptHandle - The receipt handle that proves ownership of the message
978
1020
  * @returns A function that when called will stop the extension loop
979
1021
  *
980
1022
  * @remarks
@@ -984,37 +1026,43 @@ var ConsumerGroup = class {
984
1026
  * - By default, the stop function returns immediately without waiting for in-flight
985
1027
  * - Pass `true` to the stop function to wait for any in-flight extension to complete
986
1028
  */
987
- startVisibilityExtension(messageId, ticket) {
1029
+ startVisibilityExtension(receiptHandle) {
988
1030
  let isRunning = true;
1031
+ let isResolved = false;
989
1032
  let resolveLifecycle;
990
1033
  let timeoutId = null;
991
1034
  const lifecyclePromise = new Promise((resolve) => {
992
1035
  resolveLifecycle = resolve;
993
1036
  });
1037
+ const safeResolve = () => {
1038
+ if (!isResolved) {
1039
+ isResolved = true;
1040
+ resolveLifecycle();
1041
+ }
1042
+ };
994
1043
  const extend = async () => {
995
1044
  if (!isRunning) {
996
- resolveLifecycle();
1045
+ safeResolve();
997
1046
  return;
998
1047
  }
999
1048
  try {
1000
1049
  await this.client.changeVisibility({
1001
1050
  queueName: this.topicName,
1002
1051
  consumerGroup: this.consumerGroupName,
1003
- messageId,
1004
- ticket,
1052
+ receiptHandle,
1005
1053
  visibilityTimeoutSeconds: this.visibilityTimeout
1006
1054
  });
1007
1055
  if (isRunning) {
1008
1056
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1009
1057
  } else {
1010
- resolveLifecycle();
1058
+ safeResolve();
1011
1059
  }
1012
1060
  } catch (error) {
1013
1061
  console.error(
1014
- `Failed to extend visibility for message ${messageId}:`,
1062
+ `Failed to extend visibility for receipt handle ${receiptHandle}:`,
1015
1063
  error
1016
1064
  );
1017
- resolveLifecycle();
1065
+ safeResolve();
1018
1066
  }
1019
1067
  };
1020
1068
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
@@ -1027,22 +1075,14 @@ var ConsumerGroup = class {
1027
1075
  if (waitForCompletion) {
1028
1076
  await lifecyclePromise;
1029
1077
  } else {
1030
- resolveLifecycle();
1078
+ safeResolve();
1031
1079
  }
1032
1080
  };
1033
1081
  }
1034
- /**
1035
- * Process a single message with the given handler
1036
- * @param message The message to process
1037
- * @param handler Function to process the message
1038
- */
1039
1082
  async processMessage(message, handler) {
1040
- const stopExtension = this.startVisibilityExtension(
1041
- message.messageId,
1042
- message.ticket
1043
- );
1083
+ const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1044
1084
  try {
1045
- const result = await handler(message.payload, {
1085
+ await handler(message.payload, {
1046
1086
  messageId: message.messageId,
1047
1087
  deliveryCount: message.deliveryCount,
1048
1088
  createdAt: message.createdAt,
@@ -1050,29 +1090,11 @@ var ConsumerGroup = class {
1050
1090
  consumerGroup: this.consumerGroupName
1051
1091
  });
1052
1092
  await stopExtension();
1053
- if (result && "timeoutSeconds" in result) {
1054
- await this.client.changeVisibility({
1055
- queueName: this.topicName,
1056
- consumerGroup: this.consumerGroupName,
1057
- messageId: message.messageId,
1058
- ticket: message.ticket,
1059
- visibilityTimeoutSeconds: result.timeoutSeconds
1060
- });
1061
- if (isDevMode()) {
1062
- scheduleDevTimeout(
1063
- this.topicName,
1064
- message.messageId,
1065
- result.timeoutSeconds
1066
- );
1067
- }
1068
- } else {
1069
- await this.client.deleteMessage({
1070
- queueName: this.topicName,
1071
- consumerGroup: this.consumerGroupName,
1072
- messageId: message.messageId,
1073
- ticket: message.ticket
1074
- });
1075
- }
1093
+ await this.client.deleteMessage({
1094
+ queueName: this.topicName,
1095
+ consumerGroup: this.consumerGroupName,
1096
+ receiptHandle: message.receiptHandle
1097
+ });
1076
1098
  } catch (error) {
1077
1099
  await stopExtension();
1078
1100
  if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
@@ -1087,36 +1109,16 @@ var ConsumerGroup = class {
1087
1109
  }
1088
1110
  async consume(handler, options) {
1089
1111
  if (options?.messageId) {
1090
- if (options.skipPayload) {
1091
- const response = await this.client.receiveMessageById(
1092
- {
1093
- queueName: this.topicName,
1094
- consumerGroup: this.consumerGroupName,
1095
- messageId: options.messageId,
1096
- visibilityTimeoutSeconds: this.visibilityTimeout,
1097
- skipPayload: true
1098
- },
1099
- this.transport
1100
- );
1101
- await this.processMessage(
1102
- response.message,
1103
- handler
1104
- );
1105
- } else {
1106
- const response = await this.client.receiveMessageById(
1107
- {
1108
- queueName: this.topicName,
1109
- consumerGroup: this.consumerGroupName,
1110
- messageId: options.messageId,
1111
- visibilityTimeoutSeconds: this.visibilityTimeout
1112
- },
1113
- this.transport
1114
- );
1115
- await this.processMessage(
1116
- response.message,
1117
- handler
1118
- );
1119
- }
1112
+ const response = await this.client.receiveMessageById(
1113
+ {
1114
+ queueName: this.topicName,
1115
+ consumerGroup: this.consumerGroupName,
1116
+ messageId: options.messageId,
1117
+ visibilityTimeoutSeconds: this.visibilityTimeout
1118
+ },
1119
+ this.transport
1120
+ );
1121
+ await this.processMessage(response.message, handler);
1120
1122
  } else {
1121
1123
  let messageFound = false;
1122
1124
  for await (const message of this.client.receiveMessages(
@@ -1184,7 +1186,7 @@ var Topic = class {
1184
1186
  payload,
1185
1187
  idempotencyKey: options?.idempotencyKey,
1186
1188
  retentionSeconds: options?.retentionSeconds,
1187
- deploymentId: options?.deploymentId
1189
+ delaySeconds: options?.delaySeconds
1188
1190
  },
1189
1191
  this.transport
1190
1192
  );
@@ -1225,52 +1227,224 @@ var Topic = class {
1225
1227
  }
1226
1228
  };
1227
1229
 
1230
+ // src/callback.ts
1231
+ function validateWildcardPattern(pattern) {
1232
+ const firstIndex = pattern.indexOf("*");
1233
+ const lastIndex = pattern.lastIndexOf("*");
1234
+ if (firstIndex !== lastIndex) {
1235
+ return false;
1236
+ }
1237
+ if (firstIndex === -1) {
1238
+ return false;
1239
+ }
1240
+ if (firstIndex !== pattern.length - 1) {
1241
+ return false;
1242
+ }
1243
+ return true;
1244
+ }
1245
+ function matchesWildcardPattern(topicName, pattern) {
1246
+ const prefix = pattern.slice(0, -1);
1247
+ return topicName.startsWith(prefix);
1248
+ }
1249
+ function findTopicHandler(queueName, handlers) {
1250
+ const exactHandler = handlers[queueName];
1251
+ if (exactHandler) {
1252
+ return exactHandler;
1253
+ }
1254
+ for (const pattern in handlers) {
1255
+ if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1256
+ return handlers[pattern];
1257
+ }
1258
+ }
1259
+ return null;
1260
+ }
1261
+ async function parseCallback(request) {
1262
+ const contentType = request.headers.get("content-type");
1263
+ if (!contentType || !contentType.includes("application/cloudevents+json")) {
1264
+ throw new Error(
1265
+ "Invalid content type: expected 'application/cloudevents+json'"
1266
+ );
1267
+ }
1268
+ let cloudEvent;
1269
+ try {
1270
+ cloudEvent = await request.json();
1271
+ } catch (error) {
1272
+ throw new Error("Failed to parse CloudEvent from request body");
1273
+ }
1274
+ if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
1275
+ throw new Error("Invalid CloudEvent: missing required fields");
1276
+ }
1277
+ if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1278
+ throw new Error(
1279
+ `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1280
+ );
1281
+ }
1282
+ const missingFields = [];
1283
+ if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1284
+ if (!("consumerGroup" in cloudEvent.data))
1285
+ missingFields.push("consumerGroup");
1286
+ if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
1287
+ if (missingFields.length > 0) {
1288
+ throw new Error(
1289
+ `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1290
+ );
1291
+ }
1292
+ const { messageId, queueName, consumerGroup } = cloudEvent.data;
1293
+ return {
1294
+ queueName,
1295
+ consumerGroup,
1296
+ messageId
1297
+ };
1298
+ }
1299
+ function createCallbackHandler(handlers, client) {
1300
+ for (const topicPattern in handlers) {
1301
+ if (topicPattern.includes("*")) {
1302
+ if (!validateWildcardPattern(topicPattern)) {
1303
+ throw new Error(
1304
+ `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1305
+ );
1306
+ }
1307
+ }
1308
+ }
1309
+ const routeHandler = async (request) => {
1310
+ try {
1311
+ const { queueName, consumerGroup, messageId } = await parseCallback(request);
1312
+ const topicHandler = findTopicHandler(queueName, handlers);
1313
+ if (!topicHandler) {
1314
+ const availableTopics = Object.keys(handlers).join(", ");
1315
+ return Response.json(
1316
+ {
1317
+ error: `No handler found for topic: ${queueName}`,
1318
+ availableTopics
1319
+ },
1320
+ { status: 404 }
1321
+ );
1322
+ }
1323
+ const consumerGroupHandler = topicHandler[consumerGroup];
1324
+ if (!consumerGroupHandler) {
1325
+ const availableGroups = Object.keys(topicHandler).join(", ");
1326
+ return Response.json(
1327
+ {
1328
+ error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1329
+ availableGroups
1330
+ },
1331
+ { status: 404 }
1332
+ );
1333
+ }
1334
+ const topic = new Topic(client, queueName);
1335
+ const cg = topic.consumerGroup(consumerGroup);
1336
+ await cg.consume(consumerGroupHandler, { messageId });
1337
+ return Response.json({ status: "success" });
1338
+ } catch (error) {
1339
+ console.error("Queue callback error:", error);
1340
+ if (error instanceof Error && (error.message.includes("Missing required CloudEvent data fields") || error.message.includes("Invalid CloudEvent") || error.message.includes("Invalid CloudEvent type") || error.message.includes("Invalid content type") || error.message.includes("Failed to parse CloudEvent"))) {
1341
+ return Response.json({ error: error.message }, { status: 400 });
1342
+ }
1343
+ return Response.json(
1344
+ { error: "Failed to process queue message" },
1345
+ { status: 500 }
1346
+ );
1347
+ }
1348
+ };
1349
+ return routeHandler;
1350
+ }
1351
+ function handleCallback(handlers, client) {
1352
+ return createCallbackHandler(handlers, client || new QueueClient());
1353
+ }
1354
+
1228
1355
  // src/factory.ts
1229
1356
  async function send(topicName, payload, options) {
1230
1357
  const transport = options?.transport || new JsonTransport();
1231
- const client = new QueueClient();
1358
+ const client = options?.client || new QueueClient();
1232
1359
  const result = await client.sendMessage(
1233
1360
  {
1234
1361
  queueName: topicName,
1235
1362
  payload,
1236
1363
  idempotencyKey: options?.idempotencyKey,
1237
1364
  retentionSeconds: options?.retentionSeconds,
1238
- deploymentId: options?.deploymentId
1365
+ delaySeconds: options?.delaySeconds
1239
1366
  },
1240
1367
  transport
1241
1368
  );
1242
1369
  if (isDevMode()) {
1243
- triggerDevCallbacks(topicName, result.messageId);
1370
+ triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1244
1371
  }
1245
1372
  return { messageId: result.messageId };
1246
1373
  }
1247
1374
  async function receive(topicName, consumerGroup, handler, options) {
1248
1375
  const transport = options?.transport || new JsonTransport();
1249
- const client = new QueueClient();
1376
+ const client = options?.client || new QueueClient();
1250
1377
  const topic = new Topic(client, topicName, transport);
1251
- const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
1378
+ const { messageId, client: _, ...consumerGroupOptions } = options || {};
1252
1379
  const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1253
1380
  if (messageId) {
1254
- if (skipPayload) {
1255
- return consumer.consume(handler, {
1256
- messageId,
1257
- skipPayload: true
1258
- });
1259
- } else {
1260
- return consumer.consume(handler, { messageId });
1261
- }
1381
+ return consumer.consume(handler, { messageId });
1262
1382
  } else {
1263
1383
  return consumer.consume(handler);
1264
1384
  }
1265
1385
  }
1386
+
1387
+ // src/queue-client.ts
1388
+ var Client = class {
1389
+ client;
1390
+ /**
1391
+ * Create a new Client
1392
+ * @param options QueueClient configuration options
1393
+ */
1394
+ constructor(options = {}) {
1395
+ this.client = new QueueClient(options);
1396
+ }
1397
+ /**
1398
+ * Send a message to a topic
1399
+ * @param topicName Name of the topic to send to
1400
+ * @param payload The data to send
1401
+ * @param options Optional publish options and transport
1402
+ * @returns Promise with the message ID
1403
+ * @throws {BadRequestError} When request parameters are invalid
1404
+ * @throws {UnauthorizedError} When authentication fails
1405
+ * @throws {ForbiddenError} When access is denied (environment mismatch)
1406
+ * @throws {InternalServerError} When server encounters an error
1407
+ */
1408
+ async send(topicName, payload, options) {
1409
+ return send(topicName, payload, {
1410
+ ...options,
1411
+ client: this.client
1412
+ });
1413
+ }
1414
+ /**
1415
+ * Create a callback handler for processing queue messages
1416
+ * Returns a Next.js route handler function that routes messages to appropriate handlers
1417
+ * @param handlers Object with topic-specific handlers organized by consumer groups
1418
+ * @returns A Next.js route handler function
1419
+ *
1420
+ * @example
1421
+ * ```typescript
1422
+ * export const POST = client.handleCallback({
1423
+ * "user-events": {
1424
+ * "welcome": (user, metadata) => console.log("Welcoming user", user),
1425
+ * "analytics": (user, metadata) => console.log("Tracking user", user),
1426
+ * },
1427
+ * });
1428
+ * ```
1429
+ */
1430
+ handleCallback(handlers) {
1431
+ return handleCallback(handlers, this.client);
1432
+ }
1433
+ };
1266
1434
  // Annotate the CommonJS export names for ESM import in node:
1267
1435
  0 && (module.exports = {
1268
1436
  BadRequestError,
1269
1437
  BufferTransport,
1438
+ Client,
1439
+ ConcurrencyLimitError,
1440
+ ConsumerDiscoveryError,
1441
+ ConsumerRegistryNotConfiguredError,
1442
+ DuplicateMessageError,
1270
1443
  ForbiddenError,
1271
1444
  InternalServerError,
1272
1445
  InvalidLimitError,
1273
1446
  JsonTransport,
1447
+ MessageAlreadyProcessedError,
1274
1448
  MessageCorruptedError,
1275
1449
  MessageLockedError,
1276
1450
  MessageNotAvailableError,