@vercel/queue 0.0.0-alpha.32 → 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 +308 -245
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -245
- 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 +126 -118
- package/dist/nextjs-pages.js.map +1 -1
- package/dist/nextjs-pages.mjs +126 -118
- 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.mjs
CHANGED
|
@@ -137,6 +137,9 @@ var InvalidLimitError = class extends Error {
|
|
|
137
137
|
};
|
|
138
138
|
|
|
139
139
|
// src/client.ts
|
|
140
|
+
function isDebugEnabled() {
|
|
141
|
+
return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
|
|
142
|
+
}
|
|
140
143
|
async function consumeStream(stream) {
|
|
141
144
|
const reader = stream.getReader();
|
|
142
145
|
try {
|
|
@@ -148,6 +151,31 @@ async function consumeStream(stream) {
|
|
|
148
151
|
reader.releaseLock();
|
|
149
152
|
}
|
|
150
153
|
}
|
|
154
|
+
function parseRetryAfter(headers) {
|
|
155
|
+
const retryAfterHeader = headers.get("Retry-After");
|
|
156
|
+
if (retryAfterHeader) {
|
|
157
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
158
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
159
|
+
}
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
|
|
163
|
+
if (status === 400) {
|
|
164
|
+
throw new BadRequestError(errorText || badRequestDefault);
|
|
165
|
+
}
|
|
166
|
+
if (status === 401) {
|
|
167
|
+
throw new UnauthorizedError(errorText || void 0);
|
|
168
|
+
}
|
|
169
|
+
if (status === 403) {
|
|
170
|
+
throw new ForbiddenError(errorText || void 0);
|
|
171
|
+
}
|
|
172
|
+
if (status >= 500) {
|
|
173
|
+
throw new InternalServerError(
|
|
174
|
+
errorText || `Server error: ${status} ${statusText}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
|
|
178
|
+
}
|
|
151
179
|
function parseQueueHeaders(headers) {
|
|
152
180
|
const messageId = headers.get("Vqs-Message-Id");
|
|
153
181
|
const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
|
|
@@ -158,7 +186,7 @@ function parseQueueHeaders(headers) {
|
|
|
158
186
|
return null;
|
|
159
187
|
}
|
|
160
188
|
const deliveryCount = parseInt(deliveryCountStr, 10);
|
|
161
|
-
if (isNaN(deliveryCount)) {
|
|
189
|
+
if (Number.isNaN(deliveryCount)) {
|
|
162
190
|
return null;
|
|
163
191
|
}
|
|
164
192
|
return {
|
|
@@ -172,24 +200,22 @@ function parseQueueHeaders(headers) {
|
|
|
172
200
|
var QueueClient = class {
|
|
173
201
|
baseUrl;
|
|
174
202
|
basePath;
|
|
175
|
-
customHeaders
|
|
203
|
+
customHeaders;
|
|
204
|
+
providedToken;
|
|
176
205
|
/**
|
|
177
206
|
* Create a new Vercel Queue Service client
|
|
178
|
-
* @param options
|
|
207
|
+
* @param options QueueClient configuration options
|
|
179
208
|
*/
|
|
180
209
|
constructor(options = {}) {
|
|
181
210
|
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
182
211
|
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
|
|
183
|
-
|
|
184
|
-
this.
|
|
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
|
-
);
|
|
212
|
+
this.customHeaders = options.headers || {};
|
|
213
|
+
this.providedToken = options.token;
|
|
191
214
|
}
|
|
192
215
|
async getToken() {
|
|
216
|
+
if (this.providedToken) {
|
|
217
|
+
return this.providedToken;
|
|
218
|
+
}
|
|
193
219
|
const token = await getVercelOidcToken();
|
|
194
220
|
if (!token) {
|
|
195
221
|
throw new Error(
|
|
@@ -198,6 +224,45 @@ var QueueClient = class {
|
|
|
198
224
|
}
|
|
199
225
|
return token;
|
|
200
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Internal fetch wrapper that automatically handles debug logging
|
|
229
|
+
* when VERCEL_QUEUE_DEBUG is enabled
|
|
230
|
+
*/
|
|
231
|
+
async fetch(url, init) {
|
|
232
|
+
const method = init.method || "GET";
|
|
233
|
+
if (isDebugEnabled()) {
|
|
234
|
+
const logData = {
|
|
235
|
+
method,
|
|
236
|
+
url,
|
|
237
|
+
headers: init.headers
|
|
238
|
+
};
|
|
239
|
+
const body = init.body;
|
|
240
|
+
if (body !== void 0 && body !== null) {
|
|
241
|
+
if (body instanceof ArrayBuffer) {
|
|
242
|
+
logData.bodySize = body.byteLength;
|
|
243
|
+
} else if (body instanceof Uint8Array) {
|
|
244
|
+
logData.bodySize = body.byteLength;
|
|
245
|
+
} else if (typeof body === "string") {
|
|
246
|
+
logData.bodySize = body.length;
|
|
247
|
+
} else {
|
|
248
|
+
logData.bodyType = typeof body;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
252
|
+
}
|
|
253
|
+
const response = await fetch(url, init);
|
|
254
|
+
if (isDebugEnabled()) {
|
|
255
|
+
const logData = {
|
|
256
|
+
method,
|
|
257
|
+
url,
|
|
258
|
+
status: response.status,
|
|
259
|
+
statusText: response.statusText,
|
|
260
|
+
headers: response.headers
|
|
261
|
+
};
|
|
262
|
+
console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
|
|
263
|
+
}
|
|
264
|
+
return response;
|
|
265
|
+
}
|
|
201
266
|
/**
|
|
202
267
|
* Send a message to a queue
|
|
203
268
|
* @param options Send message options
|
|
@@ -227,32 +292,21 @@ var QueueClient = class {
|
|
|
227
292
|
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
228
293
|
}
|
|
229
294
|
const body = transport.serialize(payload);
|
|
230
|
-
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
295
|
+
const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
|
|
231
296
|
method: "POST",
|
|
232
297
|
body,
|
|
233
298
|
headers
|
|
234
299
|
});
|
|
235
300
|
if (!response.ok) {
|
|
236
|
-
|
|
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
|
-
}
|
|
301
|
+
const errorText = await response.text();
|
|
246
302
|
if (response.status === 409) {
|
|
247
303
|
throw new Error("Duplicate idempotency key detected");
|
|
248
304
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
throw new Error(
|
|
255
|
-
`Failed to send message: ${response.status} ${response.statusText}`
|
|
305
|
+
throwCommonHttpError(
|
|
306
|
+
response.status,
|
|
307
|
+
response.statusText,
|
|
308
|
+
errorText,
|
|
309
|
+
"send message"
|
|
256
310
|
);
|
|
257
311
|
}
|
|
258
312
|
const responseData = await response.json();
|
|
@@ -292,7 +346,7 @@ var QueueClient = class {
|
|
|
292
346
|
if (limit !== void 0) {
|
|
293
347
|
headers.set("Vqs-Limit", limit.toString());
|
|
294
348
|
}
|
|
295
|
-
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
349
|
+
const response = await this.fetch(`${this.baseUrl}${this.basePath}`, {
|
|
296
350
|
method: "GET",
|
|
297
351
|
headers
|
|
298
352
|
});
|
|
@@ -300,32 +354,18 @@ var QueueClient = class {
|
|
|
300
354
|
throw new QueueEmptyError(queueName, consumerGroup);
|
|
301
355
|
}
|
|
302
356
|
if (!response.ok) {
|
|
303
|
-
|
|
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
|
-
}
|
|
357
|
+
const errorText = await response.text();
|
|
313
358
|
if (response.status === 423) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const parsed = parseInt(retryAfterHeader, 10);
|
|
318
|
-
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
319
|
-
}
|
|
320
|
-
throw new MessageLockedError("next message", retryAfter);
|
|
321
|
-
}
|
|
322
|
-
if (response.status >= 500) {
|
|
323
|
-
throw new InternalServerError(
|
|
324
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
359
|
+
throw new MessageLockedError(
|
|
360
|
+
"next message",
|
|
361
|
+
parseRetryAfter(response.headers)
|
|
325
362
|
);
|
|
326
363
|
}
|
|
327
|
-
|
|
328
|
-
|
|
364
|
+
throwCommonHttpError(
|
|
365
|
+
response.status,
|
|
366
|
+
response.statusText,
|
|
367
|
+
errorText,
|
|
368
|
+
"receive messages"
|
|
329
369
|
);
|
|
330
370
|
}
|
|
331
371
|
for await (const multipartMessage of parseMultipartStream(response)) {
|
|
@@ -374,7 +414,7 @@ var QueueClient = class {
|
|
|
374
414
|
if (skipPayload) {
|
|
375
415
|
headers.set("Vqs-Skip-Payload", "1");
|
|
376
416
|
}
|
|
377
|
-
const response = await fetch(
|
|
417
|
+
const response = await this.fetch(
|
|
378
418
|
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
379
419
|
{
|
|
380
420
|
method: "GET",
|
|
@@ -382,38 +422,24 @@ var QueueClient = class {
|
|
|
382
422
|
}
|
|
383
423
|
);
|
|
384
424
|
if (!response.ok) {
|
|
385
|
-
|
|
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
|
-
}
|
|
425
|
+
const errorText = await response.text();
|
|
395
426
|
if (response.status === 404) {
|
|
396
427
|
throw new MessageNotFoundError(messageId);
|
|
397
428
|
}
|
|
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
429
|
if (response.status === 409) {
|
|
408
430
|
throw new MessageNotAvailableError(messageId);
|
|
409
431
|
}
|
|
410
|
-
if (response.status
|
|
411
|
-
throw new
|
|
412
|
-
|
|
432
|
+
if (response.status === 423) {
|
|
433
|
+
throw new MessageLockedError(
|
|
434
|
+
messageId,
|
|
435
|
+
parseRetryAfter(response.headers)
|
|
413
436
|
);
|
|
414
437
|
}
|
|
415
|
-
|
|
416
|
-
|
|
438
|
+
throwCommonHttpError(
|
|
439
|
+
response.status,
|
|
440
|
+
response.statusText,
|
|
441
|
+
errorText,
|
|
442
|
+
"receive message by ID"
|
|
417
443
|
);
|
|
418
444
|
}
|
|
419
445
|
if (skipPayload && response.status === 204) {
|
|
@@ -483,7 +509,7 @@ var QueueClient = class {
|
|
|
483
509
|
*/
|
|
484
510
|
async deleteMessage(options) {
|
|
485
511
|
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
486
|
-
const response = await fetch(
|
|
512
|
+
const response = await this.fetch(
|
|
487
513
|
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
488
514
|
{
|
|
489
515
|
method: "DELETE",
|
|
@@ -497,31 +523,22 @@ var QueueClient = class {
|
|
|
497
523
|
}
|
|
498
524
|
);
|
|
499
525
|
if (!response.ok) {
|
|
500
|
-
|
|
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
|
-
}
|
|
526
|
+
const errorText = await response.text();
|
|
509
527
|
if (response.status === 404) {
|
|
510
528
|
throw new MessageNotFoundError(messageId);
|
|
511
529
|
}
|
|
512
530
|
if (response.status === 409) {
|
|
513
531
|
throw new MessageNotAvailableError(
|
|
514
532
|
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}`
|
|
533
|
+
errorText || "Invalid ticket, message not in correct state, or already processed"
|
|
521
534
|
);
|
|
522
535
|
}
|
|
523
|
-
|
|
524
|
-
|
|
536
|
+
throwCommonHttpError(
|
|
537
|
+
response.status,
|
|
538
|
+
response.statusText,
|
|
539
|
+
errorText,
|
|
540
|
+
"delete message",
|
|
541
|
+
"Missing or invalid ticket"
|
|
525
542
|
);
|
|
526
543
|
}
|
|
527
544
|
return { deleted: true };
|
|
@@ -545,7 +562,7 @@ var QueueClient = class {
|
|
|
545
562
|
ticket,
|
|
546
563
|
visibilityTimeoutSeconds
|
|
547
564
|
} = options;
|
|
548
|
-
const response = await fetch(
|
|
565
|
+
const response = await this.fetch(
|
|
549
566
|
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
550
567
|
{
|
|
551
568
|
method: "PATCH",
|
|
@@ -560,165 +577,28 @@ var QueueClient = class {
|
|
|
560
577
|
}
|
|
561
578
|
);
|
|
562
579
|
if (!response.ok) {
|
|
563
|
-
|
|
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
|
-
}
|
|
580
|
+
const errorText = await response.text();
|
|
574
581
|
if (response.status === 404) {
|
|
575
582
|
throw new MessageNotFoundError(messageId);
|
|
576
583
|
}
|
|
577
584
|
if (response.status === 409) {
|
|
578
585
|
throw new MessageNotAvailableError(
|
|
579
586
|
messageId,
|
|
580
|
-
"Invalid ticket, message not in correct state, or already processed"
|
|
587
|
+
errorText || "Invalid ticket, message not in correct state, or already processed"
|
|
581
588
|
);
|
|
582
589
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
`Failed to change visibility: ${response.status} ${response.statusText}`
|
|
590
|
+
throwCommonHttpError(
|
|
591
|
+
response.status,
|
|
592
|
+
response.statusText,
|
|
593
|
+
errorText,
|
|
594
|
+
"change visibility",
|
|
595
|
+
"Missing ticket or invalid visibility timeout"
|
|
590
596
|
);
|
|
591
597
|
}
|
|
592
598
|
return { updated: true };
|
|
593
599
|
}
|
|
594
600
|
};
|
|
595
601
|
|
|
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;
|
|
608
|
-
}
|
|
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];
|
|
623
|
-
}
|
|
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
|
-
);
|
|
672
|
-
}
|
|
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
|
-
);
|
|
688
|
-
}
|
|
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 }
|
|
698
|
-
);
|
|
699
|
-
}
|
|
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 }
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
if (isDevMode()) {
|
|
717
|
-
registerDevRouteHandler(routeHandler, handlers);
|
|
718
|
-
}
|
|
719
|
-
return routeHandler;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
602
|
// src/dev.ts
|
|
723
603
|
var GLOBAL_KEY = Symbol.for("@vercel/queue.devHandlers");
|
|
724
604
|
function getDevHandlerState() {
|
|
@@ -1183,10 +1063,138 @@ var Topic = class {
|
|
|
1183
1063
|
}
|
|
1184
1064
|
};
|
|
1185
1065
|
|
|
1066
|
+
// src/callback.ts
|
|
1067
|
+
function validateWildcardPattern(pattern) {
|
|
1068
|
+
const firstIndex = pattern.indexOf("*");
|
|
1069
|
+
const lastIndex = pattern.lastIndexOf("*");
|
|
1070
|
+
if (firstIndex !== lastIndex) {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
if (firstIndex === -1) {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
if (firstIndex !== pattern.length - 1) {
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
1082
|
+
const prefix = pattern.slice(0, -1);
|
|
1083
|
+
return topicName.startsWith(prefix);
|
|
1084
|
+
}
|
|
1085
|
+
function findTopicHandler(queueName, handlers) {
|
|
1086
|
+
const exactHandler = handlers[queueName];
|
|
1087
|
+
if (exactHandler) {
|
|
1088
|
+
return exactHandler;
|
|
1089
|
+
}
|
|
1090
|
+
for (const pattern in handlers) {
|
|
1091
|
+
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
1092
|
+
return handlers[pattern];
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
async function parseCallback(request) {
|
|
1098
|
+
const contentType = request.headers.get("content-type");
|
|
1099
|
+
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
"Invalid content type: expected 'application/cloudevents+json'"
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
let cloudEvent;
|
|
1105
|
+
try {
|
|
1106
|
+
cloudEvent = await request.json();
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
1109
|
+
}
|
|
1110
|
+
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
1111
|
+
throw new Error("Invalid CloudEvent: missing required fields");
|
|
1112
|
+
}
|
|
1113
|
+
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
1114
|
+
throw new Error(
|
|
1115
|
+
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
const missingFields = [];
|
|
1119
|
+
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
1120
|
+
if (!("consumerGroup" in cloudEvent.data))
|
|
1121
|
+
missingFields.push("consumerGroup");
|
|
1122
|
+
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
1123
|
+
if (missingFields.length > 0) {
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
1129
|
+
return {
|
|
1130
|
+
queueName,
|
|
1131
|
+
consumerGroup,
|
|
1132
|
+
messageId
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function createCallbackHandler(handlers, client) {
|
|
1136
|
+
for (const topicPattern in handlers) {
|
|
1137
|
+
if (topicPattern.includes("*")) {
|
|
1138
|
+
if (!validateWildcardPattern(topicPattern)) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
const routeHandler = async (request) => {
|
|
1146
|
+
try {
|
|
1147
|
+
const { queueName, consumerGroup, messageId } = await parseCallback(request);
|
|
1148
|
+
const topicHandler = findTopicHandler(queueName, handlers);
|
|
1149
|
+
if (!topicHandler) {
|
|
1150
|
+
const availableTopics = Object.keys(handlers).join(", ");
|
|
1151
|
+
return Response.json(
|
|
1152
|
+
{
|
|
1153
|
+
error: `No handler found for topic: ${queueName}`,
|
|
1154
|
+
availableTopics
|
|
1155
|
+
},
|
|
1156
|
+
{ status: 404 }
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1160
|
+
if (!consumerGroupHandler) {
|
|
1161
|
+
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1162
|
+
return Response.json(
|
|
1163
|
+
{
|
|
1164
|
+
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
1165
|
+
availableGroups
|
|
1166
|
+
},
|
|
1167
|
+
{ status: 404 }
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
const topic = new Topic(client, queueName);
|
|
1171
|
+
const cg = topic.consumerGroup(consumerGroup);
|
|
1172
|
+
await cg.consume(consumerGroupHandler, { messageId });
|
|
1173
|
+
return Response.json({ status: "success" });
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
console.error("Queue callback error:", error);
|
|
1176
|
+
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"))) {
|
|
1177
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
1178
|
+
}
|
|
1179
|
+
return Response.json(
|
|
1180
|
+
{ error: "Failed to process queue message" },
|
|
1181
|
+
{ status: 500 }
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
if (isDevMode()) {
|
|
1186
|
+
registerDevRouteHandler(routeHandler, handlers);
|
|
1187
|
+
}
|
|
1188
|
+
return routeHandler;
|
|
1189
|
+
}
|
|
1190
|
+
function handleCallback(handlers, client) {
|
|
1191
|
+
return createCallbackHandler(handlers, client || new QueueClient());
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1186
1194
|
// src/factory.ts
|
|
1187
1195
|
async function send(topicName, payload, options) {
|
|
1188
1196
|
const transport = options?.transport || new JsonTransport();
|
|
1189
|
-
const client = new QueueClient();
|
|
1197
|
+
const client = options?.client || new QueueClient();
|
|
1190
1198
|
const result = await client.sendMessage(
|
|
1191
1199
|
{
|
|
1192
1200
|
queueName: topicName,
|
|
@@ -1204,9 +1212,14 @@ async function send(topicName, payload, options) {
|
|
|
1204
1212
|
}
|
|
1205
1213
|
async function receive(topicName, consumerGroup, handler, options) {
|
|
1206
1214
|
const transport = options?.transport || new JsonTransport();
|
|
1207
|
-
const client = new QueueClient();
|
|
1215
|
+
const client = options?.client || new QueueClient();
|
|
1208
1216
|
const topic = new Topic(client, topicName, transport);
|
|
1209
|
-
const {
|
|
1217
|
+
const {
|
|
1218
|
+
messageId,
|
|
1219
|
+
skipPayload,
|
|
1220
|
+
client: _,
|
|
1221
|
+
...consumerGroupOptions
|
|
1222
|
+
} = options || {};
|
|
1210
1223
|
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
1211
1224
|
if (messageId) {
|
|
1212
1225
|
if (skipPayload) {
|
|
@@ -1221,9 +1234,58 @@ async function receive(topicName, consumerGroup, handler, options) {
|
|
|
1221
1234
|
return consumer.consume(handler);
|
|
1222
1235
|
}
|
|
1223
1236
|
}
|
|
1237
|
+
|
|
1238
|
+
// src/queue-client.ts
|
|
1239
|
+
var Client = class {
|
|
1240
|
+
client;
|
|
1241
|
+
/**
|
|
1242
|
+
* Create a new Client
|
|
1243
|
+
* @param options QueueClient configuration options
|
|
1244
|
+
*/
|
|
1245
|
+
constructor(options = {}) {
|
|
1246
|
+
this.client = new QueueClient(options);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Send a message to a topic
|
|
1250
|
+
* @param topicName Name of the topic to send to
|
|
1251
|
+
* @param payload The data to send
|
|
1252
|
+
* @param options Optional publish options and transport
|
|
1253
|
+
* @returns Promise with the message ID
|
|
1254
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1255
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1256
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
1257
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1258
|
+
*/
|
|
1259
|
+
async send(topicName, payload, options) {
|
|
1260
|
+
return send(topicName, payload, {
|
|
1261
|
+
...options,
|
|
1262
|
+
client: this.client
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Create a callback handler for processing queue messages
|
|
1267
|
+
* Returns a Next.js route handler function that routes messages to appropriate handlers
|
|
1268
|
+
* @param handlers Object with topic-specific handlers organized by consumer groups
|
|
1269
|
+
* @returns A Next.js route handler function
|
|
1270
|
+
*
|
|
1271
|
+
* @example
|
|
1272
|
+
* ```typescript
|
|
1273
|
+
* export const POST = client.handleCallback({
|
|
1274
|
+
* "user-events": {
|
|
1275
|
+
* "welcome": (user, metadata) => console.log("Welcoming user", user),
|
|
1276
|
+
* "analytics": (user, metadata) => console.log("Tracking user", user),
|
|
1277
|
+
* },
|
|
1278
|
+
* });
|
|
1279
|
+
* ```
|
|
1280
|
+
*/
|
|
1281
|
+
handleCallback(handlers) {
|
|
1282
|
+
return handleCallback(handlers, this.client);
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1224
1285
|
export {
|
|
1225
1286
|
BadRequestError,
|
|
1226
1287
|
BufferTransport,
|
|
1288
|
+
Client,
|
|
1227
1289
|
ForbiddenError,
|
|
1228
1290
|
InternalServerError,
|
|
1229
1291
|
InvalidLimitError,
|