@vercel/queue 0.0.0-alpha.36 → 0.0.0-alpha.38
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/README.md +212 -182
- package/dist/{types-C7IKe67P.d.mts → callback-lq_sorrn.d.mts} +249 -40
- package/dist/{types-C7IKe67P.d.ts → callback-lq_sorrn.d.ts} +249 -40
- package/dist/index.d.mts +74 -338
- package/dist/index.d.ts +74 -338
- package/dist/index.js +904 -930
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +900 -928
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs-pages.d.mts +47 -27
- package/dist/nextjs-pages.d.ts +47 -27
- package/dist/nextjs-pages.js +322 -373
- package/dist/nextjs-pages.js.map +1 -1
- package/dist/nextjs-pages.mjs +322 -373
- package/dist/nextjs-pages.mjs.map +1 -1
- package/dist/web.d.mts +60 -0
- package/dist/web.d.ts +60 -0
- package/dist/web.js +1457 -0
- package/dist/web.js.map +1 -0
- package/dist/web.mjs +1420 -0
- package/dist/web.mjs.map +1 -0
- package/package.json +11 -1
package/dist/nextjs-pages.mjs
CHANGED
|
@@ -5,46 +5,6 @@ import { parseMultipartStream } from "mixpart";
|
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as path from "path";
|
|
7
7
|
|
|
8
|
-
// src/transports.ts
|
|
9
|
-
async function streamToBuffer(stream) {
|
|
10
|
-
let totalLength = 0;
|
|
11
|
-
const reader = stream.getReader();
|
|
12
|
-
const chunks = [];
|
|
13
|
-
try {
|
|
14
|
-
while (true) {
|
|
15
|
-
const { done, value } = await reader.read();
|
|
16
|
-
if (done) break;
|
|
17
|
-
chunks.push(value);
|
|
18
|
-
totalLength += value.length;
|
|
19
|
-
}
|
|
20
|
-
} finally {
|
|
21
|
-
reader.releaseLock();
|
|
22
|
-
}
|
|
23
|
-
return Buffer.concat(chunks, totalLength);
|
|
24
|
-
}
|
|
25
|
-
var JsonTransport = class {
|
|
26
|
-
contentType = "application/json";
|
|
27
|
-
replacer;
|
|
28
|
-
reviver;
|
|
29
|
-
/**
|
|
30
|
-
* Create a new JsonTransport.
|
|
31
|
-
* @param options - Optional JSON serialization options
|
|
32
|
-
* @param options.replacer - Custom replacer for JSON.stringify
|
|
33
|
-
* @param options.reviver - Custom reviver for JSON.parse
|
|
34
|
-
*/
|
|
35
|
-
constructor(options = {}) {
|
|
36
|
-
this.replacer = options.replacer;
|
|
37
|
-
this.reviver = options.reviver;
|
|
38
|
-
}
|
|
39
|
-
serialize(value) {
|
|
40
|
-
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
41
|
-
}
|
|
42
|
-
async deserialize(stream) {
|
|
43
|
-
const buffer = await streamToBuffer(stream);
|
|
44
|
-
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
8
|
// src/types.ts
|
|
49
9
|
var MessageNotFoundError = class extends Error {
|
|
50
10
|
constructor(messageId) {
|
|
@@ -66,14 +26,6 @@ var MessageCorruptedError = class extends Error {
|
|
|
66
26
|
this.name = "MessageCorruptedError";
|
|
67
27
|
}
|
|
68
28
|
};
|
|
69
|
-
var QueueEmptyError = class extends Error {
|
|
70
|
-
constructor(queueName, consumerGroup) {
|
|
71
|
-
super(
|
|
72
|
-
`No messages available in queue "${queueName}" for consumer group "${consumerGroup}"`
|
|
73
|
-
);
|
|
74
|
-
this.name = "QueueEmptyError";
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
29
|
var UnauthorizedError = class extends Error {
|
|
78
30
|
constructor(message = "Missing or invalid authentication token") {
|
|
79
31
|
super(message);
|
|
@@ -110,18 +62,6 @@ var MessageAlreadyProcessedError = class extends Error {
|
|
|
110
62
|
this.name = "MessageAlreadyProcessedError";
|
|
111
63
|
}
|
|
112
64
|
};
|
|
113
|
-
var ConcurrencyLimitError = class extends Error {
|
|
114
|
-
/** Current number of in-flight messages for this consumer group. */
|
|
115
|
-
currentInflight;
|
|
116
|
-
/** Maximum allowed concurrent messages (as configured). */
|
|
117
|
-
maxConcurrency;
|
|
118
|
-
constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
|
|
119
|
-
super(message);
|
|
120
|
-
this.name = "ConcurrencyLimitError";
|
|
121
|
-
this.currentInflight = currentInflight;
|
|
122
|
-
this.maxConcurrency = maxConcurrency;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
65
|
var DuplicateMessageError = class extends Error {
|
|
126
66
|
idempotencyKey;
|
|
127
67
|
constructor(message, idempotencyKey) {
|
|
@@ -148,12 +88,15 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
|
|
|
148
88
|
// src/dev.ts
|
|
149
89
|
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
150
90
|
function filePathToUrlPath(filePath) {
|
|
151
|
-
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
91
|
+
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
|
|
152
92
|
if (!urlPath.startsWith("/")) {
|
|
153
93
|
urlPath = "/" + urlPath;
|
|
154
94
|
}
|
|
155
95
|
return urlPath;
|
|
156
96
|
}
|
|
97
|
+
function filePathToConsumerGroup(filePath) {
|
|
98
|
+
return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
|
|
99
|
+
}
|
|
157
100
|
function getDevRouteMappings() {
|
|
158
101
|
const g = globalThis;
|
|
159
102
|
if (ROUTE_MAPPINGS_KEY in g) {
|
|
@@ -174,11 +117,11 @@ function getDevRouteMappings() {
|
|
|
174
117
|
for (const [filePath, config] of Object.entries(vercelJson.functions)) {
|
|
175
118
|
if (!config.experimentalTriggers) continue;
|
|
176
119
|
for (const trigger of config.experimentalTriggers) {
|
|
177
|
-
if (trigger.type?.startsWith("queue/") && trigger.topic
|
|
120
|
+
if (trigger.type?.startsWith("queue/") && trigger.topic) {
|
|
178
121
|
mappings.push({
|
|
179
122
|
urlPath: filePathToUrlPath(filePath),
|
|
180
123
|
topic: trigger.topic,
|
|
181
|
-
consumer:
|
|
124
|
+
consumer: filePathToConsumerGroup(filePath)
|
|
182
125
|
});
|
|
183
126
|
}
|
|
184
127
|
}
|
|
@@ -211,20 +154,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
|
|
|
211
154
|
var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
|
|
212
155
|
async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
|
|
213
156
|
const client = new QueueClient();
|
|
214
|
-
const transport = new JsonTransport();
|
|
215
157
|
let elapsed = 0;
|
|
216
158
|
let interval = DEV_VISIBILITY_POLL_INTERVAL;
|
|
217
159
|
while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
|
|
218
160
|
try {
|
|
219
|
-
await client.receiveMessageById(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
},
|
|
226
|
-
transport
|
|
227
|
-
);
|
|
161
|
+
await client.receiveMessageById({
|
|
162
|
+
queueName: topicName,
|
|
163
|
+
consumerGroup,
|
|
164
|
+
messageId,
|
|
165
|
+
visibilityTimeoutSeconds: 0
|
|
166
|
+
});
|
|
228
167
|
return true;
|
|
229
168
|
} catch (error) {
|
|
230
169
|
if (error instanceof MessageNotFoundError) {
|
|
@@ -298,26 +237,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
|
|
|
298
237
|
console.log(
|
|
299
238
|
`[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
|
|
300
239
|
);
|
|
301
|
-
const cloudEvent = {
|
|
302
|
-
type: "com.vercel.queue.v1beta",
|
|
303
|
-
source: `/topic/${topicName}/consumer/${route.consumer}`,
|
|
304
|
-
id: messageId,
|
|
305
|
-
datacontenttype: "application/json",
|
|
306
|
-
data: {
|
|
307
|
-
messageId,
|
|
308
|
-
queueName: topicName,
|
|
309
|
-
consumerGroup: route.consumer
|
|
310
|
-
},
|
|
311
|
-
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
312
|
-
specversion: "1.0"
|
|
313
|
-
};
|
|
314
240
|
try {
|
|
315
241
|
const response = await fetch(url, {
|
|
316
242
|
method: "POST",
|
|
317
243
|
headers: {
|
|
318
|
-
"
|
|
319
|
-
|
|
320
|
-
|
|
244
|
+
"ce-type": CLOUD_EVENT_TYPE_V2BETA,
|
|
245
|
+
"ce-vqsqueuename": topicName,
|
|
246
|
+
"ce-vqsconsumergroup": route.consumer,
|
|
247
|
+
"ce-vqsmessageid": messageId
|
|
248
|
+
}
|
|
321
249
|
});
|
|
322
250
|
if (response.ok) {
|
|
323
251
|
try {
|
|
@@ -364,6 +292,46 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
|
364
292
|
// src/oidc.ts
|
|
365
293
|
import { getVercelOidcToken } from "@vercel/oidc";
|
|
366
294
|
|
|
295
|
+
// src/transports.ts
|
|
296
|
+
async function streamToBuffer(stream) {
|
|
297
|
+
let totalLength = 0;
|
|
298
|
+
const reader = stream.getReader();
|
|
299
|
+
const chunks = [];
|
|
300
|
+
try {
|
|
301
|
+
while (true) {
|
|
302
|
+
const { done, value } = await reader.read();
|
|
303
|
+
if (done) break;
|
|
304
|
+
chunks.push(value);
|
|
305
|
+
totalLength += value.length;
|
|
306
|
+
}
|
|
307
|
+
} finally {
|
|
308
|
+
reader.releaseLock();
|
|
309
|
+
}
|
|
310
|
+
return Buffer.concat(chunks, totalLength);
|
|
311
|
+
}
|
|
312
|
+
var JsonTransport = class {
|
|
313
|
+
contentType = "application/json";
|
|
314
|
+
replacer;
|
|
315
|
+
reviver;
|
|
316
|
+
/**
|
|
317
|
+
* Create a new JsonTransport.
|
|
318
|
+
* @param options - Optional JSON serialization options
|
|
319
|
+
* @param options.replacer - Custom replacer for JSON.stringify
|
|
320
|
+
* @param options.reviver - Custom reviver for JSON.parse
|
|
321
|
+
*/
|
|
322
|
+
constructor(options = {}) {
|
|
323
|
+
this.replacer = options.replacer;
|
|
324
|
+
this.reviver = options.reviver;
|
|
325
|
+
}
|
|
326
|
+
serialize(value) {
|
|
327
|
+
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
328
|
+
}
|
|
329
|
+
async deserialize(stream) {
|
|
330
|
+
const buffer = await streamToBuffer(stream);
|
|
331
|
+
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
367
335
|
// src/client.ts
|
|
368
336
|
function isDebugEnabled() {
|
|
369
337
|
return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
|
|
@@ -424,6 +392,7 @@ var QueueClient = class {
|
|
|
424
392
|
providedToken;
|
|
425
393
|
defaultDeploymentId;
|
|
426
394
|
pinToDeployment;
|
|
395
|
+
transport;
|
|
427
396
|
constructor(options = {}) {
|
|
428
397
|
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
429
398
|
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
|
|
@@ -431,6 +400,10 @@ var QueueClient = class {
|
|
|
431
400
|
this.providedToken = options.token;
|
|
432
401
|
this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
433
402
|
this.pinToDeployment = options.pinToDeployment ?? true;
|
|
403
|
+
this.transport = options.transport || new JsonTransport();
|
|
404
|
+
}
|
|
405
|
+
getTransport() {
|
|
406
|
+
return this.transport;
|
|
434
407
|
}
|
|
435
408
|
getSendDeploymentId() {
|
|
436
409
|
if (isDevMode()) {
|
|
@@ -487,6 +460,8 @@ var QueueClient = class {
|
|
|
487
460
|
}
|
|
488
461
|
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
489
462
|
}
|
|
463
|
+
init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.38"}`);
|
|
464
|
+
init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
|
|
490
465
|
const response = await fetch(url, init);
|
|
491
466
|
if (isDebugEnabled()) {
|
|
492
467
|
const logData = {
|
|
@@ -509,7 +484,6 @@ var QueueClient = class {
|
|
|
509
484
|
* @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
|
|
510
485
|
* @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
|
|
511
486
|
* @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
|
|
512
|
-
* @param transport - Serializer for the payload
|
|
513
487
|
* @returns Promise with the generated messageId
|
|
514
488
|
* @throws {DuplicateMessageError} When idempotency key was already used
|
|
515
489
|
* @throws {ConsumerDiscoveryError} When consumer discovery fails
|
|
@@ -519,7 +493,8 @@ var QueueClient = class {
|
|
|
519
493
|
* @throws {ForbiddenError} When access is denied
|
|
520
494
|
* @throws {InternalServerError} When server encounters an error
|
|
521
495
|
*/
|
|
522
|
-
async sendMessage(options
|
|
496
|
+
async sendMessage(options) {
|
|
497
|
+
const transport = this.transport;
|
|
523
498
|
const {
|
|
524
499
|
queueName,
|
|
525
500
|
payload,
|
|
@@ -601,30 +576,25 @@ var QueueClient = class {
|
|
|
601
576
|
/**
|
|
602
577
|
* Receive messages from a topic as an async generator.
|
|
603
578
|
*
|
|
579
|
+
* When the queue is empty, the generator completes without yielding any
|
|
580
|
+
* messages. Callers should handle the case where no messages are yielded.
|
|
581
|
+
*
|
|
604
582
|
* @param options - Receive options
|
|
605
583
|
* @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
|
|
606
584
|
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
607
585
|
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
608
586
|
* @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
|
|
609
|
-
* @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
|
|
610
|
-
* @param transport - Deserializer for message payloads
|
|
611
587
|
* @yields Message objects with payload, messageId, receiptHandle, etc.
|
|
612
|
-
*
|
|
588
|
+
* Yields nothing if queue is empty.
|
|
613
589
|
* @throws {InvalidLimitError} When limit is outside 1-10 range
|
|
614
|
-
* @throws {ConcurrencyLimitError} When maxConcurrency exceeded
|
|
615
590
|
* @throws {BadRequestError} When parameters are invalid
|
|
616
591
|
* @throws {UnauthorizedError} When authentication fails
|
|
617
592
|
* @throws {ForbiddenError} When access is denied
|
|
618
593
|
* @throws {InternalServerError} When server encounters an error
|
|
619
594
|
*/
|
|
620
|
-
async *receiveMessages(options
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
consumerGroup,
|
|
624
|
-
visibilityTimeoutSeconds,
|
|
625
|
-
limit,
|
|
626
|
-
maxConcurrency
|
|
627
|
-
} = options;
|
|
595
|
+
async *receiveMessages(options) {
|
|
596
|
+
const transport = this.transport;
|
|
597
|
+
const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
|
|
628
598
|
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
629
599
|
throw new InvalidLimitError(limit);
|
|
630
600
|
}
|
|
@@ -642,9 +612,6 @@ var QueueClient = class {
|
|
|
642
612
|
if (limit !== void 0) {
|
|
643
613
|
headers.set("Vqs-Max-Messages", limit.toString());
|
|
644
614
|
}
|
|
645
|
-
if (maxConcurrency !== void 0) {
|
|
646
|
-
headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
|
|
647
|
-
}
|
|
648
615
|
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
649
616
|
if (effectiveDeploymentId) {
|
|
650
617
|
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
@@ -657,22 +624,10 @@ var QueueClient = class {
|
|
|
657
624
|
}
|
|
658
625
|
);
|
|
659
626
|
if (response.status === 204) {
|
|
660
|
-
|
|
627
|
+
return;
|
|
661
628
|
}
|
|
662
629
|
if (!response.ok) {
|
|
663
630
|
const errorText = await response.text();
|
|
664
|
-
if (response.status === 429) {
|
|
665
|
-
let errorData = {};
|
|
666
|
-
try {
|
|
667
|
-
errorData = JSON.parse(errorText);
|
|
668
|
-
} catch {
|
|
669
|
-
}
|
|
670
|
-
throw new ConcurrencyLimitError(
|
|
671
|
-
errorData.error || "Concurrency limit exceeded or throttled",
|
|
672
|
-
errorData.currentInflight,
|
|
673
|
-
errorData.maxConcurrency
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
631
|
throwCommonHttpError(
|
|
677
632
|
response.status,
|
|
678
633
|
response.statusText,
|
|
@@ -710,26 +665,18 @@ var QueueClient = class {
|
|
|
710
665
|
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
711
666
|
* @param options.messageId - Message ID to retrieve
|
|
712
667
|
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
713
|
-
* @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
|
|
714
|
-
* @param transport - Deserializer for the message payload
|
|
715
668
|
* @returns Promise with the message
|
|
716
669
|
* @throws {MessageNotFoundError} When message doesn't exist
|
|
717
670
|
* @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
|
|
718
671
|
* @throws {MessageAlreadyProcessedError} When message was already processed
|
|
719
|
-
* @throws {ConcurrencyLimitError} When maxConcurrency exceeded
|
|
720
672
|
* @throws {BadRequestError} When parameters are invalid
|
|
721
673
|
* @throws {UnauthorizedError} When authentication fails
|
|
722
674
|
* @throws {ForbiddenError} When access is denied
|
|
723
675
|
* @throws {InternalServerError} When server encounters an error
|
|
724
676
|
*/
|
|
725
|
-
async receiveMessageById(options
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
consumerGroup,
|
|
729
|
-
messageId,
|
|
730
|
-
visibilityTimeoutSeconds,
|
|
731
|
-
maxConcurrency
|
|
732
|
-
} = options;
|
|
677
|
+
async receiveMessageById(options) {
|
|
678
|
+
const transport = this.transport;
|
|
679
|
+
const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
|
|
733
680
|
const headers = new Headers({
|
|
734
681
|
Authorization: `Bearer ${await this.getToken()}`,
|
|
735
682
|
Accept: "multipart/mixed",
|
|
@@ -741,9 +688,6 @@ var QueueClient = class {
|
|
|
741
688
|
visibilityTimeoutSeconds.toString()
|
|
742
689
|
);
|
|
743
690
|
}
|
|
744
|
-
if (maxConcurrency !== void 0) {
|
|
745
|
-
headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
|
|
746
|
-
}
|
|
747
691
|
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
748
692
|
if (effectiveDeploymentId) {
|
|
749
693
|
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
@@ -777,18 +721,6 @@ var QueueClient = class {
|
|
|
777
721
|
if (response.status === 410) {
|
|
778
722
|
throw new MessageAlreadyProcessedError(messageId);
|
|
779
723
|
}
|
|
780
|
-
if (response.status === 429) {
|
|
781
|
-
let errorData = {};
|
|
782
|
-
try {
|
|
783
|
-
errorData = JSON.parse(errorText);
|
|
784
|
-
} catch {
|
|
785
|
-
}
|
|
786
|
-
throw new ConcurrencyLimitError(
|
|
787
|
-
errorData.error || "Concurrency limit exceeded or throttled",
|
|
788
|
-
errorData.currentInflight,
|
|
789
|
-
errorData.maxConcurrency
|
|
790
|
-
);
|
|
791
|
-
}
|
|
792
724
|
throwCommonHttpError(
|
|
793
725
|
response.status,
|
|
794
726
|
response.statusText,
|
|
@@ -1006,40 +938,90 @@ var QueueClient = class {
|
|
|
1006
938
|
};
|
|
1007
939
|
|
|
1008
940
|
// src/consumer-group.ts
|
|
941
|
+
var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
|
|
942
|
+
var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
|
|
943
|
+
var MAX_RENEWAL_INTERVAL_SECONDS = 60;
|
|
944
|
+
var MIN_RENEWAL_INTERVAL_SECONDS = 10;
|
|
945
|
+
var RETRY_INTERVAL_MS = 3e3;
|
|
946
|
+
function calculateRenewalInterval(visibilityTimeoutSeconds) {
|
|
947
|
+
return Math.min(
|
|
948
|
+
MAX_RENEWAL_INTERVAL_SECONDS,
|
|
949
|
+
Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
|
|
950
|
+
);
|
|
951
|
+
}
|
|
1009
952
|
var ConsumerGroup = class {
|
|
1010
953
|
client;
|
|
1011
954
|
topicName;
|
|
1012
955
|
consumerGroupName;
|
|
1013
956
|
visibilityTimeout;
|
|
1014
|
-
refreshInterval;
|
|
1015
|
-
transport;
|
|
1016
957
|
/**
|
|
1017
958
|
* Create a new ConsumerGroup instance.
|
|
1018
959
|
*
|
|
1019
|
-
* @param client - QueueClient instance to use for API calls
|
|
960
|
+
* @param client - QueueClient instance to use for API calls (transport is configured on the client)
|
|
1020
961
|
* @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
|
|
1021
962
|
* @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
|
|
1022
963
|
* @param options - Optional configuration
|
|
1023
|
-
* @param options.
|
|
1024
|
-
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
|
|
1025
|
-
* @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
|
|
964
|
+
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
|
|
1026
965
|
*/
|
|
1027
966
|
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
1028
967
|
this.client = client;
|
|
1029
968
|
this.topicName = topicName;
|
|
1030
969
|
this.consumerGroupName = consumerGroupName;
|
|
1031
|
-
this.visibilityTimeout =
|
|
1032
|
-
|
|
1033
|
-
|
|
970
|
+
this.visibilityTimeout = Math.max(
|
|
971
|
+
MIN_VISIBILITY_TIMEOUT_SECONDS,
|
|
972
|
+
options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Check if an error is a 4xx client error that should stop retries.
|
|
977
|
+
* 4xx errors indicate the request is fundamentally invalid and retrying won't help.
|
|
978
|
+
* - 409: Ticket mismatch (lost ownership to another consumer)
|
|
979
|
+
* - 404: Message/receipt handle not found
|
|
980
|
+
* - 400, 401, 403: Other client errors
|
|
981
|
+
*/
|
|
982
|
+
isClientError(error) {
|
|
983
|
+
return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
|
|
984
|
+
error instanceof MessageNotFoundError || // 404 - receipt handle not found
|
|
985
|
+
error instanceof BadRequestError || // 400 - invalid parameters
|
|
986
|
+
error instanceof UnauthorizedError || // 401 - auth failed
|
|
987
|
+
error instanceof ForbiddenError;
|
|
1034
988
|
}
|
|
1035
989
|
/**
|
|
1036
990
|
* Starts a background loop that periodically extends the visibility timeout for a message.
|
|
991
|
+
*
|
|
992
|
+
* Timing strategy:
|
|
993
|
+
* - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
|
|
994
|
+
* - Extensions request the same duration as the initial visibility timeout
|
|
995
|
+
* - When `visibilityDeadline` is provided (binary mode small body), the first
|
|
996
|
+
* extension delay is calculated from the time remaining until the deadline
|
|
997
|
+
* using the same renewal formula, ensuring the first extension fires before
|
|
998
|
+
* the server-assigned lease expires. Subsequent renewals use the standard interval.
|
|
999
|
+
*
|
|
1000
|
+
* Retry strategy:
|
|
1001
|
+
* - On transient failures (5xx, network errors): retry every 3 seconds
|
|
1002
|
+
* - On 4xx client errors: stop retrying (the lease is lost or invalid)
|
|
1003
|
+
*
|
|
1004
|
+
* @param receiptHandle - The receipt handle to extend visibility for
|
|
1005
|
+
* @param options - Optional configuration
|
|
1006
|
+
* @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
|
|
1007
|
+
* when the current visibility timeout expires. Used to calculate the first extension delay.
|
|
1037
1008
|
*/
|
|
1038
|
-
startVisibilityExtension(receiptHandle) {
|
|
1009
|
+
startVisibilityExtension(receiptHandle, options) {
|
|
1039
1010
|
let isRunning = true;
|
|
1040
1011
|
let isResolved = false;
|
|
1041
1012
|
let resolveLifecycle;
|
|
1042
1013
|
let timeoutId = null;
|
|
1014
|
+
const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
|
|
1015
|
+
let firstDelayMs = renewalIntervalMs;
|
|
1016
|
+
if (options?.visibilityDeadline) {
|
|
1017
|
+
const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
|
|
1018
|
+
if (timeRemainingMs > 0) {
|
|
1019
|
+
const timeRemainingSeconds = timeRemainingMs / 1e3;
|
|
1020
|
+
firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
|
|
1021
|
+
} else {
|
|
1022
|
+
firstDelayMs = 0;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1043
1025
|
const lifecyclePromise = new Promise((resolve) => {
|
|
1044
1026
|
resolveLifecycle = resolve;
|
|
1045
1027
|
});
|
|
@@ -1062,19 +1044,31 @@ var ConsumerGroup = class {
|
|
|
1062
1044
|
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1063
1045
|
});
|
|
1064
1046
|
if (isRunning) {
|
|
1065
|
-
timeoutId = setTimeout(() => extend(),
|
|
1047
|
+
timeoutId = setTimeout(() => extend(), renewalIntervalMs);
|
|
1066
1048
|
} else {
|
|
1067
1049
|
safeResolve();
|
|
1068
1050
|
}
|
|
1069
1051
|
} catch (error) {
|
|
1052
|
+
if (this.isClientError(error)) {
|
|
1053
|
+
console.error(
|
|
1054
|
+
`Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
|
|
1055
|
+
error
|
|
1056
|
+
);
|
|
1057
|
+
safeResolve();
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1070
1060
|
console.error(
|
|
1071
|
-
`Failed to extend visibility for receipt handle ${receiptHandle}:`,
|
|
1061
|
+
`Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
|
|
1072
1062
|
error
|
|
1073
1063
|
);
|
|
1074
|
-
|
|
1064
|
+
if (isRunning) {
|
|
1065
|
+
timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
|
|
1066
|
+
} else {
|
|
1067
|
+
safeResolve();
|
|
1068
|
+
}
|
|
1075
1069
|
}
|
|
1076
1070
|
};
|
|
1077
|
-
timeoutId = setTimeout(() => extend(),
|
|
1071
|
+
timeoutId = setTimeout(() => extend(), firstDelayMs);
|
|
1078
1072
|
return async (waitForCompletion = false) => {
|
|
1079
1073
|
isRunning = false;
|
|
1080
1074
|
if (timeoutId) {
|
|
@@ -1088,8 +1082,11 @@ var ConsumerGroup = class {
|
|
|
1088
1082
|
}
|
|
1089
1083
|
};
|
|
1090
1084
|
}
|
|
1091
|
-
async processMessage(message, handler) {
|
|
1092
|
-
const stopExtension = this.startVisibilityExtension(
|
|
1085
|
+
async processMessage(message, handler, options) {
|
|
1086
|
+
const stopExtension = this.startVisibilityExtension(
|
|
1087
|
+
message.receiptHandle,
|
|
1088
|
+
options
|
|
1089
|
+
);
|
|
1093
1090
|
try {
|
|
1094
1091
|
await handler(message.payload, {
|
|
1095
1092
|
messageId: message.messageId,
|
|
@@ -1106,9 +1103,10 @@ var ConsumerGroup = class {
|
|
|
1106
1103
|
});
|
|
1107
1104
|
} catch (error) {
|
|
1108
1105
|
await stopExtension();
|
|
1109
|
-
|
|
1106
|
+
const transport = this.client.getTransport();
|
|
1107
|
+
if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
1110
1108
|
try {
|
|
1111
|
-
await
|
|
1109
|
+
await transport.finalize(message.payload);
|
|
1112
1110
|
} catch (finalizeError) {
|
|
1113
1111
|
console.warn("Failed to finalize message payload:", finalizeError);
|
|
1114
1112
|
}
|
|
@@ -1116,35 +1114,49 @@ var ConsumerGroup = class {
|
|
|
1116
1114
|
throw error;
|
|
1117
1115
|
}
|
|
1118
1116
|
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Process a pre-fetched message directly, without calling `receiveMessageById`.
|
|
1119
|
+
*
|
|
1120
|
+
* Used by the binary mode (v2beta) small body fast path, where the server
|
|
1121
|
+
* pushes the full message payload in the callback request. The message is
|
|
1122
|
+
* processed with the same lifecycle guarantees as `consume()`:
|
|
1123
|
+
* - Visibility timeout is extended periodically during processing
|
|
1124
|
+
* - Message is deleted on successful handler completion
|
|
1125
|
+
* - Payload is finalized on error if the transport supports it
|
|
1126
|
+
*
|
|
1127
|
+
* @param handler - Function to process the message payload and metadata
|
|
1128
|
+
* @param message - The complete message including payload and receipt handle
|
|
1129
|
+
* @param options - Optional configuration
|
|
1130
|
+
* @param options.visibilityDeadline - Absolute deadline when the server-assigned
|
|
1131
|
+
* visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
|
|
1132
|
+
* schedule the first visibility extension before the lease expires.
|
|
1133
|
+
*/
|
|
1134
|
+
async consumeMessage(handler, message, options) {
|
|
1135
|
+
await this.processMessage(message, handler, options);
|
|
1136
|
+
}
|
|
1119
1137
|
async consume(handler, options) {
|
|
1120
|
-
if (options
|
|
1121
|
-
const response = await this.client.receiveMessageById(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
},
|
|
1128
|
-
this.transport
|
|
1129
|
-
);
|
|
1138
|
+
if (options && "messageId" in options) {
|
|
1139
|
+
const response = await this.client.receiveMessageById({
|
|
1140
|
+
queueName: this.topicName,
|
|
1141
|
+
consumerGroup: this.consumerGroupName,
|
|
1142
|
+
messageId: options.messageId,
|
|
1143
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1144
|
+
});
|
|
1130
1145
|
await this.processMessage(response.message, handler);
|
|
1131
1146
|
} else {
|
|
1147
|
+
const limit = options && "limit" in options ? options.limit : 1;
|
|
1132
1148
|
let messageFound = false;
|
|
1133
|
-
for await (const message of this.client.receiveMessages(
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
},
|
|
1140
|
-
this.transport
|
|
1141
|
-
)) {
|
|
1149
|
+
for await (const message of this.client.receiveMessages({
|
|
1150
|
+
queueName: this.topicName,
|
|
1151
|
+
consumerGroup: this.consumerGroupName,
|
|
1152
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1153
|
+
limit
|
|
1154
|
+
})) {
|
|
1142
1155
|
messageFound = true;
|
|
1143
1156
|
await this.processMessage(message, handler);
|
|
1144
|
-
break;
|
|
1145
1157
|
}
|
|
1146
1158
|
if (!messageFound) {
|
|
1147
|
-
|
|
1159
|
+
await handler(null, null);
|
|
1148
1160
|
}
|
|
1149
1161
|
}
|
|
1150
1162
|
}
|
|
@@ -1166,17 +1178,14 @@ var ConsumerGroup = class {
|
|
|
1166
1178
|
var Topic = class {
|
|
1167
1179
|
client;
|
|
1168
1180
|
topicName;
|
|
1169
|
-
transport;
|
|
1170
1181
|
/**
|
|
1171
1182
|
* Create a new Topic instance
|
|
1172
|
-
* @param client QueueClient instance to use for API calls
|
|
1183
|
+
* @param client QueueClient instance to use for API calls (transport is configured on the client)
|
|
1173
1184
|
* @param topicName Name of the topic to work with
|
|
1174
|
-
* @param transport Optional serializer/deserializer for the payload (defaults to JSON)
|
|
1175
1185
|
*/
|
|
1176
|
-
constructor(client, topicName
|
|
1186
|
+
constructor(client, topicName) {
|
|
1177
1187
|
this.client = client;
|
|
1178
1188
|
this.topicName = topicName;
|
|
1179
|
-
this.transport = transport || new JsonTransport();
|
|
1180
1189
|
}
|
|
1181
1190
|
/**
|
|
1182
1191
|
* Publish a message to the topic
|
|
@@ -1189,17 +1198,14 @@ var Topic = class {
|
|
|
1189
1198
|
* @throws {InternalServerError} When server encounters an error
|
|
1190
1199
|
*/
|
|
1191
1200
|
async publish(payload, options) {
|
|
1192
|
-
const result = await this.client.sendMessage(
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
},
|
|
1201
|
-
this.transport
|
|
1202
|
-
);
|
|
1201
|
+
const result = await this.client.sendMessage({
|
|
1202
|
+
queueName: this.topicName,
|
|
1203
|
+
payload,
|
|
1204
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1205
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1206
|
+
delaySeconds: options?.delaySeconds,
|
|
1207
|
+
headers: options?.headers
|
|
1208
|
+
});
|
|
1203
1209
|
if (isDevMode()) {
|
|
1204
1210
|
triggerDevCallbacks(this.topicName, result.messageId);
|
|
1205
1211
|
}
|
|
@@ -1212,15 +1218,11 @@ var Topic = class {
|
|
|
1212
1218
|
* @returns A ConsumerGroup instance
|
|
1213
1219
|
*/
|
|
1214
1220
|
consumerGroup(consumerGroupName, options) {
|
|
1215
|
-
const consumerOptions = {
|
|
1216
|
-
...options,
|
|
1217
|
-
transport: options?.transport || this.transport
|
|
1218
|
-
};
|
|
1219
1221
|
return new ConsumerGroup(
|
|
1220
1222
|
this.client,
|
|
1221
1223
|
this.topicName,
|
|
1222
1224
|
consumerGroupName,
|
|
1223
|
-
|
|
1225
|
+
options
|
|
1224
1226
|
);
|
|
1225
1227
|
}
|
|
1226
1228
|
/**
|
|
@@ -1229,220 +1231,167 @@ var Topic = class {
|
|
|
1229
1231
|
get name() {
|
|
1230
1232
|
return this.topicName;
|
|
1231
1233
|
}
|
|
1232
|
-
/**
|
|
1233
|
-
* Get the transport used by this topic
|
|
1234
|
-
*/
|
|
1235
|
-
get serializer() {
|
|
1236
|
-
return this.transport;
|
|
1237
|
-
}
|
|
1238
1234
|
};
|
|
1239
1235
|
|
|
1240
1236
|
// src/callback.ts
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
const lastIndex = pattern.lastIndexOf("*");
|
|
1244
|
-
if (firstIndex !== lastIndex) {
|
|
1245
|
-
return false;
|
|
1246
|
-
}
|
|
1247
|
-
if (firstIndex === -1) {
|
|
1248
|
-
return false;
|
|
1249
|
-
}
|
|
1250
|
-
if (firstIndex !== pattern.length - 1) {
|
|
1251
|
-
return false;
|
|
1252
|
-
}
|
|
1253
|
-
return true;
|
|
1254
|
-
}
|
|
1237
|
+
var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
|
|
1238
|
+
var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
|
|
1255
1239
|
function matchesWildcardPattern(topicName, pattern) {
|
|
1256
1240
|
const prefix = pattern.slice(0, -1);
|
|
1257
1241
|
return topicName.startsWith(prefix);
|
|
1258
1242
|
}
|
|
1259
|
-
function
|
|
1260
|
-
|
|
1261
|
-
if (exactHandler) {
|
|
1262
|
-
return exactHandler;
|
|
1263
|
-
}
|
|
1264
|
-
for (const pattern in handlers) {
|
|
1265
|
-
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
1266
|
-
return handlers[pattern];
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
return null;
|
|
1243
|
+
function isRecord(value) {
|
|
1244
|
+
return typeof value === "object" && value !== null;
|
|
1270
1245
|
}
|
|
1271
|
-
|
|
1272
|
-
const contentType = request.headers.get("content-type");
|
|
1246
|
+
function parseV1StructuredBody(body, contentType) {
|
|
1273
1247
|
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
1274
1248
|
throw new Error(
|
|
1275
1249
|
"Invalid content type: expected 'application/cloudevents+json'"
|
|
1276
1250
|
);
|
|
1277
1251
|
}
|
|
1278
|
-
|
|
1279
|
-
try {
|
|
1280
|
-
cloudEvent = await request.json();
|
|
1281
|
-
} catch (error) {
|
|
1282
|
-
throw new Error("Failed to parse CloudEvent from request body");
|
|
1283
|
-
}
|
|
1284
|
-
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
1252
|
+
if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
|
|
1285
1253
|
throw new Error("Invalid CloudEvent: missing required fields");
|
|
1286
1254
|
}
|
|
1287
|
-
if (
|
|
1255
|
+
if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
|
|
1288
1256
|
throw new Error(
|
|
1289
|
-
`Invalid CloudEvent type: expected '
|
|
1257
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
|
|
1290
1258
|
);
|
|
1291
1259
|
}
|
|
1260
|
+
const { data } = body;
|
|
1292
1261
|
const missingFields = [];
|
|
1293
|
-
if (!("queueName" in
|
|
1294
|
-
if (!("consumerGroup" in
|
|
1295
|
-
|
|
1296
|
-
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
1262
|
+
if (!("queueName" in data)) missingFields.push("queueName");
|
|
1263
|
+
if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
|
|
1264
|
+
if (!("messageId" in data)) missingFields.push("messageId");
|
|
1297
1265
|
if (missingFields.length > 0) {
|
|
1298
1266
|
throw new Error(
|
|
1299
1267
|
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1300
1268
|
);
|
|
1301
1269
|
}
|
|
1302
|
-
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
1303
1270
|
return {
|
|
1304
|
-
queueName,
|
|
1305
|
-
consumerGroup,
|
|
1306
|
-
messageId
|
|
1271
|
+
queueName: String(data.queueName),
|
|
1272
|
+
consumerGroup: String(data.consumerGroup),
|
|
1273
|
+
messageId: String(data.messageId)
|
|
1307
1274
|
};
|
|
1308
1275
|
}
|
|
1309
|
-
function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
|
|
1310
|
-
for (const topicPattern in handlers) {
|
|
1311
|
-
if (topicPattern.includes("*")) {
|
|
1312
|
-
if (!validateWildcardPattern(topicPattern)) {
|
|
1313
|
-
throw new Error(
|
|
1314
|
-
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
const routeHandler = async (request) => {
|
|
1320
|
-
try {
|
|
1321
|
-
const { queueName, consumerGroup, messageId } = await parseCallback(request);
|
|
1322
|
-
const topicHandler = findTopicHandler(queueName, handlers);
|
|
1323
|
-
if (!topicHandler) {
|
|
1324
|
-
const availableTopics = Object.keys(handlers).join(", ");
|
|
1325
|
-
return Response.json(
|
|
1326
|
-
{
|
|
1327
|
-
error: `No handler found for topic: ${queueName}`,
|
|
1328
|
-
availableTopics
|
|
1329
|
-
},
|
|
1330
|
-
{ status: 404 }
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1334
|
-
if (!consumerGroupHandler) {
|
|
1335
|
-
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1336
|
-
return Response.json(
|
|
1337
|
-
{
|
|
1338
|
-
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
1339
|
-
availableGroups
|
|
1340
|
-
},
|
|
1341
|
-
{ status: 404 }
|
|
1342
|
-
);
|
|
1343
|
-
}
|
|
1344
|
-
const topic = new Topic(client, queueName);
|
|
1345
|
-
const cg = topic.consumerGroup(
|
|
1346
|
-
consumerGroup,
|
|
1347
|
-
visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
|
|
1348
|
-
);
|
|
1349
|
-
await cg.consume(consumerGroupHandler, { messageId });
|
|
1350
|
-
return Response.json({ status: "success" });
|
|
1351
|
-
} catch (error) {
|
|
1352
|
-
console.error("Queue callback error:", error);
|
|
1353
|
-
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"))) {
|
|
1354
|
-
return Response.json({ error: error.message }, { status: 400 });
|
|
1355
|
-
}
|
|
1356
|
-
return Response.json(
|
|
1357
|
-
{ error: "Failed to process queue message" },
|
|
1358
|
-
{ status: 500 }
|
|
1359
|
-
);
|
|
1360
|
-
}
|
|
1361
|
-
};
|
|
1362
|
-
return routeHandler;
|
|
1363
|
-
}
|
|
1364
|
-
function handleCallback(handlers, options) {
|
|
1365
|
-
return createCallbackHandler(
|
|
1366
|
-
handlers,
|
|
1367
|
-
options?.client || new QueueClient(),
|
|
1368
|
-
options?.visibilityTimeoutSeconds
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// src/nextjs-pages.ts
|
|
1373
1276
|
function getHeader(headers, name) {
|
|
1277
|
+
if (headers instanceof Headers) {
|
|
1278
|
+
return headers.get(name);
|
|
1279
|
+
}
|
|
1374
1280
|
const value = headers[name];
|
|
1375
|
-
|
|
1281
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
1282
|
+
return value ?? null;
|
|
1376
1283
|
}
|
|
1377
|
-
function
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
});
|
|
1384
|
-
}
|
|
1385
|
-
async function getBody(req) {
|
|
1386
|
-
if (req.body === void 0) {
|
|
1387
|
-
return readBody(req);
|
|
1284
|
+
function parseBinaryHeaders(headers) {
|
|
1285
|
+
const ceType = getHeader(headers, "ce-type");
|
|
1286
|
+
if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
|
|
1287
|
+
throw new Error(
|
|
1288
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
|
|
1289
|
+
);
|
|
1388
1290
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1291
|
+
const queueName = getHeader(headers, "ce-vqsqueuename");
|
|
1292
|
+
const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
|
|
1293
|
+
const messageId = getHeader(headers, "ce-vqsmessageid");
|
|
1294
|
+
const missingFields = [];
|
|
1295
|
+
if (!queueName) missingFields.push("ce-vqsqueuename");
|
|
1296
|
+
if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
|
|
1297
|
+
if (!messageId) missingFields.push("ce-vqsmessageid");
|
|
1298
|
+
if (missingFields.length > 0) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
`Missing required CloudEvent headers: ${missingFields.join(", ")}`
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
const base = {
|
|
1304
|
+
queueName,
|
|
1305
|
+
consumerGroup,
|
|
1306
|
+
messageId
|
|
1307
|
+
};
|
|
1308
|
+
const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
|
|
1309
|
+
if (!receiptHandle) {
|
|
1310
|
+
return base;
|
|
1311
|
+
}
|
|
1312
|
+
const result = { ...base, receiptHandle };
|
|
1313
|
+
const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
|
|
1314
|
+
if (deliveryCount) {
|
|
1315
|
+
result.deliveryCount = parseInt(deliveryCount, 10);
|
|
1316
|
+
}
|
|
1317
|
+
const createdAt = getHeader(headers, "ce-vqscreatedat");
|
|
1318
|
+
if (createdAt) {
|
|
1319
|
+
result.createdAt = createdAt;
|
|
1320
|
+
}
|
|
1321
|
+
const contentType = getHeader(headers, "content-type");
|
|
1322
|
+
if (contentType) {
|
|
1323
|
+
result.contentType = contentType;
|
|
1391
1324
|
}
|
|
1392
|
-
|
|
1325
|
+
const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
|
|
1326
|
+
if (visibilityDeadline) {
|
|
1327
|
+
result.visibilityDeadline = visibilityDeadline;
|
|
1328
|
+
}
|
|
1329
|
+
return result;
|
|
1393
1330
|
}
|
|
1394
|
-
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const url = `${protocol}://${host}${req.url}`;
|
|
1401
|
-
const headers = new Headers();
|
|
1402
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
1403
|
-
if (value) {
|
|
1404
|
-
if (Array.isArray(value)) {
|
|
1405
|
-
value.forEach((v) => headers.append(key, v));
|
|
1406
|
-
} else {
|
|
1407
|
-
headers.set(key, value);
|
|
1408
|
-
}
|
|
1331
|
+
function parseRawCallback(body, headers) {
|
|
1332
|
+
const ceType = getHeader(headers, "ce-type");
|
|
1333
|
+
if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
|
|
1334
|
+
const result = parseBinaryHeaders(headers);
|
|
1335
|
+
if ("receiptHandle" in result) {
|
|
1336
|
+
result.parsedPayload = body;
|
|
1409
1337
|
}
|
|
1338
|
+
return result;
|
|
1410
1339
|
}
|
|
1411
|
-
|
|
1412
|
-
return new Request(url, {
|
|
1413
|
-
method: req.method || "POST",
|
|
1414
|
-
headers,
|
|
1415
|
-
body
|
|
1416
|
-
});
|
|
1340
|
+
return parseV1StructuredBody(body, getHeader(headers, "content-type"));
|
|
1417
1341
|
}
|
|
1418
|
-
async function
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1342
|
+
async function handleCallback(handler, request, options) {
|
|
1343
|
+
const { queueName, consumerGroup, messageId } = request;
|
|
1344
|
+
const client = options?.client || new QueueClient();
|
|
1345
|
+
const topic = new Topic(client, queueName);
|
|
1346
|
+
const cg = topic.consumerGroup(
|
|
1347
|
+
consumerGroup,
|
|
1348
|
+
options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
|
|
1349
|
+
);
|
|
1350
|
+
if ("receiptHandle" in request) {
|
|
1351
|
+
const transport = client.getTransport();
|
|
1352
|
+
let payload;
|
|
1353
|
+
if (request.rawBody) {
|
|
1354
|
+
payload = await transport.deserialize(request.rawBody);
|
|
1355
|
+
} else if (request.parsedPayload !== void 0) {
|
|
1356
|
+
payload = request.parsedPayload;
|
|
1357
|
+
} else {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
"Binary mode callback with receipt handle is missing payload"
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const message = {
|
|
1363
|
+
messageId,
|
|
1364
|
+
payload,
|
|
1365
|
+
deliveryCount: request.deliveryCount ?? 1,
|
|
1366
|
+
createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
|
|
1367
|
+
contentType: request.contentType ?? transport.contentType,
|
|
1368
|
+
receiptHandle: request.receiptHandle
|
|
1369
|
+
};
|
|
1370
|
+
const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
|
|
1371
|
+
await cg.consumeMessage(handler, message, { visibilityDeadline });
|
|
1427
1372
|
} else {
|
|
1428
|
-
|
|
1429
|
-
res.send(text);
|
|
1373
|
+
await cg.consume(handler, { messageId });
|
|
1430
1374
|
}
|
|
1431
1375
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1376
|
+
|
|
1377
|
+
// src/nextjs-pages.ts
|
|
1378
|
+
function handleCallback2(handler, options) {
|
|
1434
1379
|
return async (req, res) => {
|
|
1435
1380
|
if (req.method !== "POST") {
|
|
1436
1381
|
res.status(200).end();
|
|
1437
1382
|
return;
|
|
1438
1383
|
}
|
|
1439
1384
|
try {
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1385
|
+
const parsed = parseRawCallback(req.body, req.headers);
|
|
1386
|
+
await handleCallback(handler, parsed, options);
|
|
1387
|
+
res.status(200).json({ status: "success" });
|
|
1443
1388
|
} catch (error) {
|
|
1444
|
-
console.error("
|
|
1445
|
-
|
|
1389
|
+
console.error("Queue callback error:", error);
|
|
1390
|
+
if (error instanceof Error && (error.message.includes("Invalid content type") || error.message.includes("Invalid CloudEvent") || error.message.includes("Missing required CloudEvent") || error.message.includes("Failed to parse CloudEvent") || error.message.includes("Binary mode callback"))) {
|
|
1391
|
+
res.status(400).json({ error: error.message });
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
res.status(500).json({ error: "Failed to process queue message" });
|
|
1446
1395
|
}
|
|
1447
1396
|
};
|
|
1448
1397
|
}
|