@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.d.mts +260 -73
- package/dist/index.d.ts +260 -73
- package/dist/index.js +319 -246
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +318 -246
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs-pages.d.mts +1 -1
- package/dist/nextjs-pages.d.ts +1 -1
- package/dist/nextjs-pages.js +138 -120
- package/dist/nextjs-pages.js.map +1 -1
- package/dist/nextjs-pages.mjs +138 -120
- package/dist/nextjs-pages.mjs.map +1 -1
- package/dist/{types-JvOenjfT.d.mts → types-Dw29Fr9y.d.mts} +126 -2
- package/dist/{types-JvOenjfT.d.ts → types-Dw29Fr9y.d.ts} +126 -2
- package/package.json +1 -1
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
|
|
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
|
-
|
|
226
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
|
453
|
-
throw new
|
|
454
|
-
|
|
475
|
+
if (response.status === 423) {
|
|
476
|
+
throw new MessageLockedError(
|
|
477
|
+
messageId,
|
|
478
|
+
parseRetryAfter(response.headers)
|
|
455
479
|
);
|
|
456
480
|
}
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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/
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
|
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 {
|
|
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,
|