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