@vercel/queue 0.0.0-alpha.31 → 0.0.0-alpha.33

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
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BadRequestError: () => BadRequestError,
24
24
  BufferTransport: () => BufferTransport,
25
+ Client: () => Client,
25
26
  ForbiddenError: () => ForbiddenError,
26
27
  InternalServerError: () => InternalServerError,
27
28
  InvalidLimitError: () => InvalidLimitError,
@@ -179,6 +180,9 @@ var InvalidLimitError = class extends Error {
179
180
  };
180
181
 
181
182
  // src/client.ts
183
+ function isDebugEnabled() {
184
+ return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
185
+ }
182
186
  async function consumeStream(stream) {
183
187
  const reader = stream.getReader();
184
188
  try {
@@ -190,6 +194,31 @@ async function consumeStream(stream) {
190
194
  reader.releaseLock();
191
195
  }
192
196
  }
197
+ function parseRetryAfter(headers) {
198
+ const retryAfterHeader = headers.get("Retry-After");
199
+ if (retryAfterHeader) {
200
+ const parsed = parseInt(retryAfterHeader, 10);
201
+ return Number.isNaN(parsed) ? void 0 : parsed;
202
+ }
203
+ return void 0;
204
+ }
205
+ function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
206
+ if (status === 400) {
207
+ throw new BadRequestError(errorText || badRequestDefault);
208
+ }
209
+ if (status === 401) {
210
+ throw new UnauthorizedError(errorText || void 0);
211
+ }
212
+ if (status === 403) {
213
+ throw new ForbiddenError(errorText || void 0);
214
+ }
215
+ if (status >= 500) {
216
+ throw new InternalServerError(
217
+ errorText || `Server error: ${status} ${statusText}`
218
+ );
219
+ }
220
+ throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
221
+ }
193
222
  function parseQueueHeaders(headers) {
194
223
  const messageId = headers.get("Vqs-Message-Id");
195
224
  const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
@@ -200,7 +229,7 @@ function parseQueueHeaders(headers) {
200
229
  return null;
201
230
  }
202
231
  const deliveryCount = parseInt(deliveryCountStr, 10);
203
- if (isNaN(deliveryCount)) {
232
+ if (Number.isNaN(deliveryCount)) {
204
233
  return null;
205
234
  }
206
235
  return {
@@ -214,24 +243,22 @@ function parseQueueHeaders(headers) {
214
243
  var QueueClient = class {
215
244
  baseUrl;
216
245
  basePath;
217
- customHeaders = {};
246
+ customHeaders;
247
+ providedToken;
218
248
  /**
219
249
  * Create a new Vercel Queue Service client
220
- * @param options Client configuration options
250
+ * @param options QueueClient configuration options
221
251
  */
222
252
  constructor(options = {}) {
223
253
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
224
254
  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
- );
255
+ this.customHeaders = options.headers || {};
256
+ this.providedToken = options.token;
233
257
  }
234
258
  async getToken() {
259
+ if (this.providedToken) {
260
+ return this.providedToken;
261
+ }
235
262
  const token = await (0, import_oidc.getVercelOidcToken)();
236
263
  if (!token) {
237
264
  throw new Error(
@@ -240,6 +267,45 @@ var QueueClient = class {
240
267
  }
241
268
  return token;
242
269
  }
270
+ /**
271
+ * Internal fetch wrapper that automatically handles debug logging
272
+ * when VERCEL_QUEUE_DEBUG is enabled
273
+ */
274
+ async fetch(url, init) {
275
+ const method = init.method || "GET";
276
+ if (isDebugEnabled()) {
277
+ const logData = {
278
+ method,
279
+ url,
280
+ headers: init.headers
281
+ };
282
+ const body = init.body;
283
+ if (body !== void 0 && body !== null) {
284
+ if (body instanceof ArrayBuffer) {
285
+ logData.bodySize = body.byteLength;
286
+ } else if (body instanceof Uint8Array) {
287
+ logData.bodySize = body.byteLength;
288
+ } else if (typeof body === "string") {
289
+ logData.bodySize = body.length;
290
+ } else {
291
+ logData.bodyType = typeof body;
292
+ }
293
+ }
294
+ console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
295
+ }
296
+ const response = await fetch(url, init);
297
+ if (isDebugEnabled()) {
298
+ const logData = {
299
+ method,
300
+ url,
301
+ status: response.status,
302
+ statusText: response.statusText,
303
+ headers: response.headers
304
+ };
305
+ console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
306
+ }
307
+ return response;
308
+ }
243
309
  /**
244
310
  * Send a message to a queue
245
311
  * @param options Send message options
@@ -269,32 +335,21 @@ var QueueClient = class {
269
335
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
270
336
  }
271
337
  const body = transport.serialize(payload);
272
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
338
+ const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
273
339
  method: "POST",
274
340
  body,
275
341
  headers
276
342
  });
277
343
  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
- }
344
+ const errorText = await response.text();
288
345
  if (response.status === 409) {
289
346
  throw new Error("Duplicate idempotency key detected");
290
347
  }
291
- if (response.status >= 500) {
292
- throw new InternalServerError(
293
- `Server error: ${response.status} ${response.statusText}`
294
- );
295
- }
296
- throw new Error(
297
- `Failed to send message: ${response.status} ${response.statusText}`
348
+ throwCommonHttpError(
349
+ response.status,
350
+ response.statusText,
351
+ errorText,
352
+ "send message"
298
353
  );
299
354
  }
300
355
  const responseData = await response.json();
@@ -334,7 +389,7 @@ var QueueClient = class {
334
389
  if (limit !== void 0) {
335
390
  headers.set("Vqs-Limit", limit.toString());
336
391
  }
337
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
392
+ const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
338
393
  method: "GET",
339
394
  headers
340
395
  });
@@ -342,32 +397,18 @@ var QueueClient = class {
342
397
  throw new QueueEmptyError(queueName, consumerGroup);
343
398
  }
344
399
  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
- }
400
+ const errorText = await response.text();
355
401
  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;
361
- }
362
- throw new MessageLockedError("next message", retryAfter);
363
- }
364
- if (response.status >= 500) {
365
- throw new InternalServerError(
366
- `Server error: ${response.status} ${response.statusText}`
402
+ throw new MessageLockedError(
403
+ "next message",
404
+ parseRetryAfter(response.headers)
367
405
  );
368
406
  }
369
- throw new Error(
370
- `Failed to receive messages: ${response.status} ${response.statusText}`
407
+ throwCommonHttpError(
408
+ response.status,
409
+ response.statusText,
410
+ errorText,
411
+ "receive messages"
371
412
  );
372
413
  }
373
414
  for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
@@ -416,7 +457,7 @@ var QueueClient = class {
416
457
  if (skipPayload) {
417
458
  headers.set("Vqs-Skip-Payload", "1");
418
459
  }
419
- const response = await fetch(
460
+ const response = await this.fetch(
420
461
  `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
421
462
  {
422
463
  method: "GET",
@@ -424,38 +465,24 @@ var QueueClient = class {
424
465
  }
425
466
  );
426
467
  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
- }
468
+ const errorText = await response.text();
437
469
  if (response.status === 404) {
438
470
  throw new MessageNotFoundError(messageId);
439
471
  }
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
472
  if (response.status === 409) {
450
473
  throw new MessageNotAvailableError(messageId);
451
474
  }
452
- if (response.status >= 500) {
453
- throw new InternalServerError(
454
- `Server error: ${response.status} ${response.statusText}`
475
+ if (response.status === 423) {
476
+ throw new MessageLockedError(
477
+ messageId,
478
+ parseRetryAfter(response.headers)
455
479
  );
456
480
  }
457
- throw new Error(
458
- `Failed to receive message by ID: ${response.status} ${response.statusText}`
481
+ throwCommonHttpError(
482
+ response.status,
483
+ response.statusText,
484
+ errorText,
485
+ "receive message by ID"
459
486
  );
460
487
  }
461
488
  if (skipPayload && response.status === 204) {
@@ -525,7 +552,7 @@ var QueueClient = class {
525
552
  */
526
553
  async deleteMessage(options) {
527
554
  const { queueName, consumerGroup, messageId, ticket } = options;
528
- const response = await fetch(
555
+ const response = await this.fetch(
529
556
  `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
530
557
  {
531
558
  method: "DELETE",
@@ -539,31 +566,22 @@ var QueueClient = class {
539
566
  }
540
567
  );
541
568
  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
- }
569
+ const errorText = await response.text();
551
570
  if (response.status === 404) {
552
571
  throw new MessageNotFoundError(messageId);
553
572
  }
554
573
  if (response.status === 409) {
555
574
  throw new MessageNotAvailableError(
556
575
  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}`
576
+ errorText || "Invalid ticket, message not in correct state, or already processed"
563
577
  );
564
578
  }
565
- throw new Error(
566
- `Failed to delete message: ${response.status} ${response.statusText}`
579
+ throwCommonHttpError(
580
+ response.status,
581
+ response.statusText,
582
+ errorText,
583
+ "delete message",
584
+ "Missing or invalid ticket"
567
585
  );
568
586
  }
569
587
  return { deleted: true };
@@ -587,7 +605,7 @@ var QueueClient = class {
587
605
  ticket,
588
606
  visibilityTimeoutSeconds
589
607
  } = options;
590
- const response = await fetch(
608
+ const response = await this.fetch(
591
609
  `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
592
610
  {
593
611
  method: "PATCH",
@@ -602,168 +620,41 @@ var QueueClient = class {
602
620
  }
603
621
  );
604
622
  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
- }
623
+ const errorText = await response.text();
616
624
  if (response.status === 404) {
617
625
  throw new MessageNotFoundError(messageId);
618
626
  }
619
627
  if (response.status === 409) {
620
628
  throw new MessageNotAvailableError(
621
629
  messageId,
622
- "Invalid ticket, message not in correct state, or already processed"
630
+ errorText || "Invalid ticket, message not in correct state, or already processed"
623
631
  );
624
632
  }
625
- if (response.status >= 500) {
626
- throw new InternalServerError(
627
- `Server error: ${response.status} ${response.statusText}`
628
- );
629
- }
630
- throw new Error(
631
- `Failed to change visibility: ${response.status} ${response.statusText}`
633
+ throwCommonHttpError(
634
+ response.status,
635
+ response.statusText,
636
+ errorText,
637
+ "change visibility",
638
+ "Missing ticket or invalid visibility timeout"
632
639
  );
633
640
  }
634
641
  return { updated: true };
635
642
  }
636
643
  };
637
644
 
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;
650
- }
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];
665
- }
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
- );
714
- }
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
- );
730
- }
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 }
740
- );
741
- }
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 }
755
- );
756
- }
757
- };
758
- if (isDevMode()) {
759
- registerDevRouteHandler(routeHandler, handlers);
645
+ // src/dev.ts
646
+ var GLOBAL_KEY = Symbol.for("@vercel/queue.devHandlers");
647
+ function getDevHandlerState() {
648
+ const g = globalThis;
649
+ if (!g[GLOBAL_KEY]) {
650
+ g[GLOBAL_KEY] = {
651
+ devRouteHandlers: /* @__PURE__ */ new Map(),
652
+ wildcardRouteHandlers: /* @__PURE__ */ new Map()
653
+ };
760
654
  }
761
- return routeHandler;
655
+ return g[GLOBAL_KEY];
762
656
  }
763
-
764
- // src/dev.ts
765
- var devRouteHandlers = /* @__PURE__ */ new Map();
766
- var wildcardRouteHandlers = /* @__PURE__ */ new Map();
657
+ var { devRouteHandlers, wildcardRouteHandlers } = getDevHandlerState();
767
658
  function cleanupDeadRefs(key, refs) {
768
659
  const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
769
660
  if (aliveRefs.length === 0) {
@@ -1215,10 +1106,138 @@ var Topic = class {
1215
1106
  }
1216
1107
  };
1217
1108
 
1109
+ // src/callback.ts
1110
+ function validateWildcardPattern(pattern) {
1111
+ const firstIndex = pattern.indexOf("*");
1112
+ const lastIndex = pattern.lastIndexOf("*");
1113
+ if (firstIndex !== lastIndex) {
1114
+ return false;
1115
+ }
1116
+ if (firstIndex === -1) {
1117
+ return false;
1118
+ }
1119
+ if (firstIndex !== pattern.length - 1) {
1120
+ return false;
1121
+ }
1122
+ return true;
1123
+ }
1124
+ function matchesWildcardPattern(topicName, pattern) {
1125
+ const prefix = pattern.slice(0, -1);
1126
+ return topicName.startsWith(prefix);
1127
+ }
1128
+ function findTopicHandler(queueName, handlers) {
1129
+ const exactHandler = handlers[queueName];
1130
+ if (exactHandler) {
1131
+ return exactHandler;
1132
+ }
1133
+ for (const pattern in handlers) {
1134
+ if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1135
+ return handlers[pattern];
1136
+ }
1137
+ }
1138
+ return null;
1139
+ }
1140
+ async function parseCallback(request) {
1141
+ const contentType = request.headers.get("content-type");
1142
+ if (!contentType || !contentType.includes("application/cloudevents+json")) {
1143
+ throw new Error(
1144
+ "Invalid content type: expected 'application/cloudevents+json'"
1145
+ );
1146
+ }
1147
+ let cloudEvent;
1148
+ try {
1149
+ cloudEvent = await request.json();
1150
+ } catch (error) {
1151
+ throw new Error("Failed to parse CloudEvent from request body");
1152
+ }
1153
+ if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
1154
+ throw new Error("Invalid CloudEvent: missing required fields");
1155
+ }
1156
+ if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1157
+ throw new Error(
1158
+ `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1159
+ );
1160
+ }
1161
+ const missingFields = [];
1162
+ if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1163
+ if (!("consumerGroup" in cloudEvent.data))
1164
+ missingFields.push("consumerGroup");
1165
+ if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
1166
+ if (missingFields.length > 0) {
1167
+ throw new Error(
1168
+ `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1169
+ );
1170
+ }
1171
+ const { messageId, queueName, consumerGroup } = cloudEvent.data;
1172
+ return {
1173
+ queueName,
1174
+ consumerGroup,
1175
+ messageId
1176
+ };
1177
+ }
1178
+ function createCallbackHandler(handlers, client) {
1179
+ for (const topicPattern in handlers) {
1180
+ if (topicPattern.includes("*")) {
1181
+ if (!validateWildcardPattern(topicPattern)) {
1182
+ throw new Error(
1183
+ `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1184
+ );
1185
+ }
1186
+ }
1187
+ }
1188
+ const routeHandler = async (request) => {
1189
+ try {
1190
+ const { queueName, consumerGroup, messageId } = await parseCallback(request);
1191
+ const topicHandler = findTopicHandler(queueName, handlers);
1192
+ if (!topicHandler) {
1193
+ const availableTopics = Object.keys(handlers).join(", ");
1194
+ return Response.json(
1195
+ {
1196
+ error: `No handler found for topic: ${queueName}`,
1197
+ availableTopics
1198
+ },
1199
+ { status: 404 }
1200
+ );
1201
+ }
1202
+ const consumerGroupHandler = topicHandler[consumerGroup];
1203
+ if (!consumerGroupHandler) {
1204
+ const availableGroups = Object.keys(topicHandler).join(", ");
1205
+ return Response.json(
1206
+ {
1207
+ error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1208
+ availableGroups
1209
+ },
1210
+ { status: 404 }
1211
+ );
1212
+ }
1213
+ const topic = new Topic(client, queueName);
1214
+ const cg = topic.consumerGroup(consumerGroup);
1215
+ await cg.consume(consumerGroupHandler, { messageId });
1216
+ return Response.json({ status: "success" });
1217
+ } catch (error) {
1218
+ console.error("Queue callback error:", error);
1219
+ 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"))) {
1220
+ return Response.json({ error: error.message }, { status: 400 });
1221
+ }
1222
+ return Response.json(
1223
+ { error: "Failed to process queue message" },
1224
+ { status: 500 }
1225
+ );
1226
+ }
1227
+ };
1228
+ if (isDevMode()) {
1229
+ registerDevRouteHandler(routeHandler, handlers);
1230
+ }
1231
+ return routeHandler;
1232
+ }
1233
+ function handleCallback(handlers, client) {
1234
+ return createCallbackHandler(handlers, client || new QueueClient());
1235
+ }
1236
+
1218
1237
  // src/factory.ts
1219
1238
  async function send(topicName, payload, options) {
1220
1239
  const transport = options?.transport || new JsonTransport();
1221
- const client = new QueueClient();
1240
+ const client = options?.client || new QueueClient();
1222
1241
  const result = await client.sendMessage(
1223
1242
  {
1224
1243
  queueName: topicName,
@@ -1236,9 +1255,14 @@ async function send(topicName, payload, options) {
1236
1255
  }
1237
1256
  async function receive(topicName, consumerGroup, handler, options) {
1238
1257
  const transport = options?.transport || new JsonTransport();
1239
- const client = new QueueClient();
1258
+ const client = options?.client || new QueueClient();
1240
1259
  const topic = new Topic(client, topicName, transport);
1241
- const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
1260
+ const {
1261
+ messageId,
1262
+ skipPayload,
1263
+ client: _,
1264
+ ...consumerGroupOptions
1265
+ } = options || {};
1242
1266
  const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1243
1267
  if (messageId) {
1244
1268
  if (skipPayload) {
@@ -1253,10 +1277,59 @@ async function receive(topicName, consumerGroup, handler, options) {
1253
1277
  return consumer.consume(handler);
1254
1278
  }
1255
1279
  }
1280
+
1281
+ // src/queue-client.ts
1282
+ var Client = class {
1283
+ client;
1284
+ /**
1285
+ * Create a new Client
1286
+ * @param options QueueClient configuration options
1287
+ */
1288
+ constructor(options = {}) {
1289
+ this.client = new QueueClient(options);
1290
+ }
1291
+ /**
1292
+ * Send a message to a topic
1293
+ * @param topicName Name of the topic to send to
1294
+ * @param payload The data to send
1295
+ * @param options Optional publish options and transport
1296
+ * @returns Promise with the message ID
1297
+ * @throws {BadRequestError} When request parameters are invalid
1298
+ * @throws {UnauthorizedError} When authentication fails
1299
+ * @throws {ForbiddenError} When access is denied (environment mismatch)
1300
+ * @throws {InternalServerError} When server encounters an error
1301
+ */
1302
+ async send(topicName, payload, options) {
1303
+ return send(topicName, payload, {
1304
+ ...options,
1305
+ client: this.client
1306
+ });
1307
+ }
1308
+ /**
1309
+ * Create a callback handler for processing queue messages
1310
+ * Returns a Next.js route handler function that routes messages to appropriate handlers
1311
+ * @param handlers Object with topic-specific handlers organized by consumer groups
1312
+ * @returns A Next.js route handler function
1313
+ *
1314
+ * @example
1315
+ * ```typescript
1316
+ * export const POST = client.handleCallback({
1317
+ * "user-events": {
1318
+ * "welcome": (user, metadata) => console.log("Welcoming user", user),
1319
+ * "analytics": (user, metadata) => console.log("Tracking user", user),
1320
+ * },
1321
+ * });
1322
+ * ```
1323
+ */
1324
+ handleCallback(handlers) {
1325
+ return handleCallback(handlers, this.client);
1326
+ }
1327
+ };
1256
1328
  // Annotate the CommonJS export names for ESM import in node:
1257
1329
  0 && (module.exports = {
1258
1330
  BadRequestError,
1259
1331
  BufferTransport,
1332
+ Client,
1260
1333
  ForbiddenError,
1261
1334
  InternalServerError,
1262
1335
  InvalidLimitError,