@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.js
CHANGED
|
@@ -41,46 +41,6 @@ var import_mixpart = require("mixpart");
|
|
|
41
41
|
var fs = __toESM(require("fs"));
|
|
42
42
|
var path = __toESM(require("path"));
|
|
43
43
|
|
|
44
|
-
// src/transports.ts
|
|
45
|
-
async function streamToBuffer(stream) {
|
|
46
|
-
let totalLength = 0;
|
|
47
|
-
const reader = stream.getReader();
|
|
48
|
-
const chunks = [];
|
|
49
|
-
try {
|
|
50
|
-
while (true) {
|
|
51
|
-
const { done, value } = await reader.read();
|
|
52
|
-
if (done) break;
|
|
53
|
-
chunks.push(value);
|
|
54
|
-
totalLength += value.length;
|
|
55
|
-
}
|
|
56
|
-
} finally {
|
|
57
|
-
reader.releaseLock();
|
|
58
|
-
}
|
|
59
|
-
return Buffer.concat(chunks, totalLength);
|
|
60
|
-
}
|
|
61
|
-
var JsonTransport = class {
|
|
62
|
-
contentType = "application/json";
|
|
63
|
-
replacer;
|
|
64
|
-
reviver;
|
|
65
|
-
/**
|
|
66
|
-
* Create a new JsonTransport.
|
|
67
|
-
* @param options - Optional JSON serialization options
|
|
68
|
-
* @param options.replacer - Custom replacer for JSON.stringify
|
|
69
|
-
* @param options.reviver - Custom reviver for JSON.parse
|
|
70
|
-
*/
|
|
71
|
-
constructor(options = {}) {
|
|
72
|
-
this.replacer = options.replacer;
|
|
73
|
-
this.reviver = options.reviver;
|
|
74
|
-
}
|
|
75
|
-
serialize(value) {
|
|
76
|
-
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
77
|
-
}
|
|
78
|
-
async deserialize(stream) {
|
|
79
|
-
const buffer = await streamToBuffer(stream);
|
|
80
|
-
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
44
|
// src/types.ts
|
|
85
45
|
var MessageNotFoundError = class extends Error {
|
|
86
46
|
constructor(messageId) {
|
|
@@ -102,14 +62,6 @@ var MessageCorruptedError = class extends Error {
|
|
|
102
62
|
this.name = "MessageCorruptedError";
|
|
103
63
|
}
|
|
104
64
|
};
|
|
105
|
-
var QueueEmptyError = class extends Error {
|
|
106
|
-
constructor(queueName, consumerGroup) {
|
|
107
|
-
super(
|
|
108
|
-
`No messages available in queue "${queueName}" for consumer group "${consumerGroup}"`
|
|
109
|
-
);
|
|
110
|
-
this.name = "QueueEmptyError";
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
65
|
var UnauthorizedError = class extends Error {
|
|
114
66
|
constructor(message = "Missing or invalid authentication token") {
|
|
115
67
|
super(message);
|
|
@@ -146,18 +98,6 @@ var MessageAlreadyProcessedError = class extends Error {
|
|
|
146
98
|
this.name = "MessageAlreadyProcessedError";
|
|
147
99
|
}
|
|
148
100
|
};
|
|
149
|
-
var ConcurrencyLimitError = class extends Error {
|
|
150
|
-
/** Current number of in-flight messages for this consumer group. */
|
|
151
|
-
currentInflight;
|
|
152
|
-
/** Maximum allowed concurrent messages (as configured). */
|
|
153
|
-
maxConcurrency;
|
|
154
|
-
constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
|
|
155
|
-
super(message);
|
|
156
|
-
this.name = "ConcurrencyLimitError";
|
|
157
|
-
this.currentInflight = currentInflight;
|
|
158
|
-
this.maxConcurrency = maxConcurrency;
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
101
|
var DuplicateMessageError = class extends Error {
|
|
162
102
|
idempotencyKey;
|
|
163
103
|
constructor(message, idempotencyKey) {
|
|
@@ -184,12 +124,15 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
|
|
|
184
124
|
// src/dev.ts
|
|
185
125
|
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
186
126
|
function filePathToUrlPath(filePath) {
|
|
187
|
-
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
127
|
+
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
|
|
188
128
|
if (!urlPath.startsWith("/")) {
|
|
189
129
|
urlPath = "/" + urlPath;
|
|
190
130
|
}
|
|
191
131
|
return urlPath;
|
|
192
132
|
}
|
|
133
|
+
function filePathToConsumerGroup(filePath) {
|
|
134
|
+
return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
|
|
135
|
+
}
|
|
193
136
|
function getDevRouteMappings() {
|
|
194
137
|
const g = globalThis;
|
|
195
138
|
if (ROUTE_MAPPINGS_KEY in g) {
|
|
@@ -210,11 +153,11 @@ function getDevRouteMappings() {
|
|
|
210
153
|
for (const [filePath, config] of Object.entries(vercelJson.functions)) {
|
|
211
154
|
if (!config.experimentalTriggers) continue;
|
|
212
155
|
for (const trigger of config.experimentalTriggers) {
|
|
213
|
-
if (trigger.type?.startsWith("queue/") && trigger.topic
|
|
156
|
+
if (trigger.type?.startsWith("queue/") && trigger.topic) {
|
|
214
157
|
mappings.push({
|
|
215
158
|
urlPath: filePathToUrlPath(filePath),
|
|
216
159
|
topic: trigger.topic,
|
|
217
|
-
consumer:
|
|
160
|
+
consumer: filePathToConsumerGroup(filePath)
|
|
218
161
|
});
|
|
219
162
|
}
|
|
220
163
|
}
|
|
@@ -247,20 +190,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
|
|
|
247
190
|
var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
|
|
248
191
|
async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
|
|
249
192
|
const client = new QueueClient();
|
|
250
|
-
const transport = new JsonTransport();
|
|
251
193
|
let elapsed = 0;
|
|
252
194
|
let interval = DEV_VISIBILITY_POLL_INTERVAL;
|
|
253
195
|
while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
|
|
254
196
|
try {
|
|
255
|
-
await client.receiveMessageById(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
transport
|
|
263
|
-
);
|
|
197
|
+
await client.receiveMessageById({
|
|
198
|
+
queueName: topicName,
|
|
199
|
+
consumerGroup,
|
|
200
|
+
messageId,
|
|
201
|
+
visibilityTimeoutSeconds: 0
|
|
202
|
+
});
|
|
264
203
|
return true;
|
|
265
204
|
} catch (error) {
|
|
266
205
|
if (error instanceof MessageNotFoundError) {
|
|
@@ -334,26 +273,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
|
|
|
334
273
|
console.log(
|
|
335
274
|
`[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
|
|
336
275
|
);
|
|
337
|
-
const cloudEvent = {
|
|
338
|
-
type: "com.vercel.queue.v1beta",
|
|
339
|
-
source: `/topic/${topicName}/consumer/${route.consumer}`,
|
|
340
|
-
id: messageId,
|
|
341
|
-
datacontenttype: "application/json",
|
|
342
|
-
data: {
|
|
343
|
-
messageId,
|
|
344
|
-
queueName: topicName,
|
|
345
|
-
consumerGroup: route.consumer
|
|
346
|
-
},
|
|
347
|
-
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
348
|
-
specversion: "1.0"
|
|
349
|
-
};
|
|
350
276
|
try {
|
|
351
277
|
const response = await fetch(url, {
|
|
352
278
|
method: "POST",
|
|
353
279
|
headers: {
|
|
354
|
-
"
|
|
355
|
-
|
|
356
|
-
|
|
280
|
+
"ce-type": CLOUD_EVENT_TYPE_V2BETA,
|
|
281
|
+
"ce-vqsqueuename": topicName,
|
|
282
|
+
"ce-vqsconsumergroup": route.consumer,
|
|
283
|
+
"ce-vqsmessageid": messageId
|
|
284
|
+
}
|
|
357
285
|
});
|
|
358
286
|
if (response.ok) {
|
|
359
287
|
try {
|
|
@@ -400,6 +328,46 @@ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
|
400
328
|
// src/oidc.ts
|
|
401
329
|
var import_oidc = require("@vercel/oidc");
|
|
402
330
|
|
|
331
|
+
// src/transports.ts
|
|
332
|
+
async function streamToBuffer(stream) {
|
|
333
|
+
let totalLength = 0;
|
|
334
|
+
const reader = stream.getReader();
|
|
335
|
+
const chunks = [];
|
|
336
|
+
try {
|
|
337
|
+
while (true) {
|
|
338
|
+
const { done, value } = await reader.read();
|
|
339
|
+
if (done) break;
|
|
340
|
+
chunks.push(value);
|
|
341
|
+
totalLength += value.length;
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
reader.releaseLock();
|
|
345
|
+
}
|
|
346
|
+
return Buffer.concat(chunks, totalLength);
|
|
347
|
+
}
|
|
348
|
+
var JsonTransport = class {
|
|
349
|
+
contentType = "application/json";
|
|
350
|
+
replacer;
|
|
351
|
+
reviver;
|
|
352
|
+
/**
|
|
353
|
+
* Create a new JsonTransport.
|
|
354
|
+
* @param options - Optional JSON serialization options
|
|
355
|
+
* @param options.replacer - Custom replacer for JSON.stringify
|
|
356
|
+
* @param options.reviver - Custom reviver for JSON.parse
|
|
357
|
+
*/
|
|
358
|
+
constructor(options = {}) {
|
|
359
|
+
this.replacer = options.replacer;
|
|
360
|
+
this.reviver = options.reviver;
|
|
361
|
+
}
|
|
362
|
+
serialize(value) {
|
|
363
|
+
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
364
|
+
}
|
|
365
|
+
async deserialize(stream) {
|
|
366
|
+
const buffer = await streamToBuffer(stream);
|
|
367
|
+
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
403
371
|
// src/client.ts
|
|
404
372
|
function isDebugEnabled() {
|
|
405
373
|
return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
|
|
@@ -460,6 +428,7 @@ var QueueClient = class {
|
|
|
460
428
|
providedToken;
|
|
461
429
|
defaultDeploymentId;
|
|
462
430
|
pinToDeployment;
|
|
431
|
+
transport;
|
|
463
432
|
constructor(options = {}) {
|
|
464
433
|
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
465
434
|
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
|
|
@@ -467,6 +436,10 @@ var QueueClient = class {
|
|
|
467
436
|
this.providedToken = options.token;
|
|
468
437
|
this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
469
438
|
this.pinToDeployment = options.pinToDeployment ?? true;
|
|
439
|
+
this.transport = options.transport || new JsonTransport();
|
|
440
|
+
}
|
|
441
|
+
getTransport() {
|
|
442
|
+
return this.transport;
|
|
470
443
|
}
|
|
471
444
|
getSendDeploymentId() {
|
|
472
445
|
if (isDevMode()) {
|
|
@@ -523,6 +496,8 @@ var QueueClient = class {
|
|
|
523
496
|
}
|
|
524
497
|
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
525
498
|
}
|
|
499
|
+
init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.38"}`);
|
|
500
|
+
init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
|
|
526
501
|
const response = await fetch(url, init);
|
|
527
502
|
if (isDebugEnabled()) {
|
|
528
503
|
const logData = {
|
|
@@ -545,7 +520,6 @@ var QueueClient = class {
|
|
|
545
520
|
* @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
|
|
546
521
|
* @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
|
|
547
522
|
* @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
|
|
548
|
-
* @param transport - Serializer for the payload
|
|
549
523
|
* @returns Promise with the generated messageId
|
|
550
524
|
* @throws {DuplicateMessageError} When idempotency key was already used
|
|
551
525
|
* @throws {ConsumerDiscoveryError} When consumer discovery fails
|
|
@@ -555,7 +529,8 @@ var QueueClient = class {
|
|
|
555
529
|
* @throws {ForbiddenError} When access is denied
|
|
556
530
|
* @throws {InternalServerError} When server encounters an error
|
|
557
531
|
*/
|
|
558
|
-
async sendMessage(options
|
|
532
|
+
async sendMessage(options) {
|
|
533
|
+
const transport = this.transport;
|
|
559
534
|
const {
|
|
560
535
|
queueName,
|
|
561
536
|
payload,
|
|
@@ -637,30 +612,25 @@ var QueueClient = class {
|
|
|
637
612
|
/**
|
|
638
613
|
* Receive messages from a topic as an async generator.
|
|
639
614
|
*
|
|
615
|
+
* When the queue is empty, the generator completes without yielding any
|
|
616
|
+
* messages. Callers should handle the case where no messages are yielded.
|
|
617
|
+
*
|
|
640
618
|
* @param options - Receive options
|
|
641
619
|
* @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
|
|
642
620
|
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
643
621
|
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
644
622
|
* @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
|
|
645
|
-
* @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
|
|
646
|
-
* @param transport - Deserializer for message payloads
|
|
647
623
|
* @yields Message objects with payload, messageId, receiptHandle, etc.
|
|
648
|
-
*
|
|
624
|
+
* Yields nothing if queue is empty.
|
|
649
625
|
* @throws {InvalidLimitError} When limit is outside 1-10 range
|
|
650
|
-
* @throws {ConcurrencyLimitError} When maxConcurrency exceeded
|
|
651
626
|
* @throws {BadRequestError} When parameters are invalid
|
|
652
627
|
* @throws {UnauthorizedError} When authentication fails
|
|
653
628
|
* @throws {ForbiddenError} When access is denied
|
|
654
629
|
* @throws {InternalServerError} When server encounters an error
|
|
655
630
|
*/
|
|
656
|
-
async *receiveMessages(options
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
consumerGroup,
|
|
660
|
-
visibilityTimeoutSeconds,
|
|
661
|
-
limit,
|
|
662
|
-
maxConcurrency
|
|
663
|
-
} = options;
|
|
631
|
+
async *receiveMessages(options) {
|
|
632
|
+
const transport = this.transport;
|
|
633
|
+
const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
|
|
664
634
|
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
665
635
|
throw new InvalidLimitError(limit);
|
|
666
636
|
}
|
|
@@ -678,9 +648,6 @@ var QueueClient = class {
|
|
|
678
648
|
if (limit !== void 0) {
|
|
679
649
|
headers.set("Vqs-Max-Messages", limit.toString());
|
|
680
650
|
}
|
|
681
|
-
if (maxConcurrency !== void 0) {
|
|
682
|
-
headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
|
|
683
|
-
}
|
|
684
651
|
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
685
652
|
if (effectiveDeploymentId) {
|
|
686
653
|
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
@@ -693,22 +660,10 @@ var QueueClient = class {
|
|
|
693
660
|
}
|
|
694
661
|
);
|
|
695
662
|
if (response.status === 204) {
|
|
696
|
-
|
|
663
|
+
return;
|
|
697
664
|
}
|
|
698
665
|
if (!response.ok) {
|
|
699
666
|
const errorText = await response.text();
|
|
700
|
-
if (response.status === 429) {
|
|
701
|
-
let errorData = {};
|
|
702
|
-
try {
|
|
703
|
-
errorData = JSON.parse(errorText);
|
|
704
|
-
} catch {
|
|
705
|
-
}
|
|
706
|
-
throw new ConcurrencyLimitError(
|
|
707
|
-
errorData.error || "Concurrency limit exceeded or throttled",
|
|
708
|
-
errorData.currentInflight,
|
|
709
|
-
errorData.maxConcurrency
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
667
|
throwCommonHttpError(
|
|
713
668
|
response.status,
|
|
714
669
|
response.statusText,
|
|
@@ -746,26 +701,18 @@ var QueueClient = class {
|
|
|
746
701
|
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
747
702
|
* @param options.messageId - Message ID to retrieve
|
|
748
703
|
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
749
|
-
* @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
|
|
750
|
-
* @param transport - Deserializer for the message payload
|
|
751
704
|
* @returns Promise with the message
|
|
752
705
|
* @throws {MessageNotFoundError} When message doesn't exist
|
|
753
706
|
* @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
|
|
754
707
|
* @throws {MessageAlreadyProcessedError} When message was already processed
|
|
755
|
-
* @throws {ConcurrencyLimitError} When maxConcurrency exceeded
|
|
756
708
|
* @throws {BadRequestError} When parameters are invalid
|
|
757
709
|
* @throws {UnauthorizedError} When authentication fails
|
|
758
710
|
* @throws {ForbiddenError} When access is denied
|
|
759
711
|
* @throws {InternalServerError} When server encounters an error
|
|
760
712
|
*/
|
|
761
|
-
async receiveMessageById(options
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
consumerGroup,
|
|
765
|
-
messageId,
|
|
766
|
-
visibilityTimeoutSeconds,
|
|
767
|
-
maxConcurrency
|
|
768
|
-
} = options;
|
|
713
|
+
async receiveMessageById(options) {
|
|
714
|
+
const transport = this.transport;
|
|
715
|
+
const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
|
|
769
716
|
const headers = new Headers({
|
|
770
717
|
Authorization: `Bearer ${await this.getToken()}`,
|
|
771
718
|
Accept: "multipart/mixed",
|
|
@@ -777,9 +724,6 @@ var QueueClient = class {
|
|
|
777
724
|
visibilityTimeoutSeconds.toString()
|
|
778
725
|
);
|
|
779
726
|
}
|
|
780
|
-
if (maxConcurrency !== void 0) {
|
|
781
|
-
headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
|
|
782
|
-
}
|
|
783
727
|
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
784
728
|
if (effectiveDeploymentId) {
|
|
785
729
|
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
@@ -813,18 +757,6 @@ var QueueClient = class {
|
|
|
813
757
|
if (response.status === 410) {
|
|
814
758
|
throw new MessageAlreadyProcessedError(messageId);
|
|
815
759
|
}
|
|
816
|
-
if (response.status === 429) {
|
|
817
|
-
let errorData = {};
|
|
818
|
-
try {
|
|
819
|
-
errorData = JSON.parse(errorText);
|
|
820
|
-
} catch {
|
|
821
|
-
}
|
|
822
|
-
throw new ConcurrencyLimitError(
|
|
823
|
-
errorData.error || "Concurrency limit exceeded or throttled",
|
|
824
|
-
errorData.currentInflight,
|
|
825
|
-
errorData.maxConcurrency
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
760
|
throwCommonHttpError(
|
|
829
761
|
response.status,
|
|
830
762
|
response.statusText,
|
|
@@ -1042,40 +974,90 @@ var QueueClient = class {
|
|
|
1042
974
|
};
|
|
1043
975
|
|
|
1044
976
|
// src/consumer-group.ts
|
|
977
|
+
var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
|
|
978
|
+
var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
|
|
979
|
+
var MAX_RENEWAL_INTERVAL_SECONDS = 60;
|
|
980
|
+
var MIN_RENEWAL_INTERVAL_SECONDS = 10;
|
|
981
|
+
var RETRY_INTERVAL_MS = 3e3;
|
|
982
|
+
function calculateRenewalInterval(visibilityTimeoutSeconds) {
|
|
983
|
+
return Math.min(
|
|
984
|
+
MAX_RENEWAL_INTERVAL_SECONDS,
|
|
985
|
+
Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
|
|
986
|
+
);
|
|
987
|
+
}
|
|
1045
988
|
var ConsumerGroup = class {
|
|
1046
989
|
client;
|
|
1047
990
|
topicName;
|
|
1048
991
|
consumerGroupName;
|
|
1049
992
|
visibilityTimeout;
|
|
1050
|
-
refreshInterval;
|
|
1051
|
-
transport;
|
|
1052
993
|
/**
|
|
1053
994
|
* Create a new ConsumerGroup instance.
|
|
1054
995
|
*
|
|
1055
|
-
* @param client - QueueClient instance to use for API calls
|
|
996
|
+
* @param client - QueueClient instance to use for API calls (transport is configured on the client)
|
|
1056
997
|
* @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
|
|
1057
998
|
* @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
|
|
1058
999
|
* @param options - Optional configuration
|
|
1059
|
-
* @param options.
|
|
1060
|
-
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
|
|
1061
|
-
* @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
|
|
1000
|
+
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
|
|
1062
1001
|
*/
|
|
1063
1002
|
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
1064
1003
|
this.client = client;
|
|
1065
1004
|
this.topicName = topicName;
|
|
1066
1005
|
this.consumerGroupName = consumerGroupName;
|
|
1067
|
-
this.visibilityTimeout =
|
|
1068
|
-
|
|
1069
|
-
|
|
1006
|
+
this.visibilityTimeout = Math.max(
|
|
1007
|
+
MIN_VISIBILITY_TIMEOUT_SECONDS,
|
|
1008
|
+
options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if an error is a 4xx client error that should stop retries.
|
|
1013
|
+
* 4xx errors indicate the request is fundamentally invalid and retrying won't help.
|
|
1014
|
+
* - 409: Ticket mismatch (lost ownership to another consumer)
|
|
1015
|
+
* - 404: Message/receipt handle not found
|
|
1016
|
+
* - 400, 401, 403: Other client errors
|
|
1017
|
+
*/
|
|
1018
|
+
isClientError(error) {
|
|
1019
|
+
return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
|
|
1020
|
+
error instanceof MessageNotFoundError || // 404 - receipt handle not found
|
|
1021
|
+
error instanceof BadRequestError || // 400 - invalid parameters
|
|
1022
|
+
error instanceof UnauthorizedError || // 401 - auth failed
|
|
1023
|
+
error instanceof ForbiddenError;
|
|
1070
1024
|
}
|
|
1071
1025
|
/**
|
|
1072
1026
|
* Starts a background loop that periodically extends the visibility timeout for a message.
|
|
1027
|
+
*
|
|
1028
|
+
* Timing strategy:
|
|
1029
|
+
* - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
|
|
1030
|
+
* - Extensions request the same duration as the initial visibility timeout
|
|
1031
|
+
* - When `visibilityDeadline` is provided (binary mode small body), the first
|
|
1032
|
+
* extension delay is calculated from the time remaining until the deadline
|
|
1033
|
+
* using the same renewal formula, ensuring the first extension fires before
|
|
1034
|
+
* the server-assigned lease expires. Subsequent renewals use the standard interval.
|
|
1035
|
+
*
|
|
1036
|
+
* Retry strategy:
|
|
1037
|
+
* - On transient failures (5xx, network errors): retry every 3 seconds
|
|
1038
|
+
* - On 4xx client errors: stop retrying (the lease is lost or invalid)
|
|
1039
|
+
*
|
|
1040
|
+
* @param receiptHandle - The receipt handle to extend visibility for
|
|
1041
|
+
* @param options - Optional configuration
|
|
1042
|
+
* @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
|
|
1043
|
+
* when the current visibility timeout expires. Used to calculate the first extension delay.
|
|
1073
1044
|
*/
|
|
1074
|
-
startVisibilityExtension(receiptHandle) {
|
|
1045
|
+
startVisibilityExtension(receiptHandle, options) {
|
|
1075
1046
|
let isRunning = true;
|
|
1076
1047
|
let isResolved = false;
|
|
1077
1048
|
let resolveLifecycle;
|
|
1078
1049
|
let timeoutId = null;
|
|
1050
|
+
const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
|
|
1051
|
+
let firstDelayMs = renewalIntervalMs;
|
|
1052
|
+
if (options?.visibilityDeadline) {
|
|
1053
|
+
const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
|
|
1054
|
+
if (timeRemainingMs > 0) {
|
|
1055
|
+
const timeRemainingSeconds = timeRemainingMs / 1e3;
|
|
1056
|
+
firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
|
|
1057
|
+
} else {
|
|
1058
|
+
firstDelayMs = 0;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1079
1061
|
const lifecyclePromise = new Promise((resolve) => {
|
|
1080
1062
|
resolveLifecycle = resolve;
|
|
1081
1063
|
});
|
|
@@ -1098,19 +1080,31 @@ var ConsumerGroup = class {
|
|
|
1098
1080
|
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1099
1081
|
});
|
|
1100
1082
|
if (isRunning) {
|
|
1101
|
-
timeoutId = setTimeout(() => extend(),
|
|
1083
|
+
timeoutId = setTimeout(() => extend(), renewalIntervalMs);
|
|
1102
1084
|
} else {
|
|
1103
1085
|
safeResolve();
|
|
1104
1086
|
}
|
|
1105
1087
|
} catch (error) {
|
|
1088
|
+
if (this.isClientError(error)) {
|
|
1089
|
+
console.error(
|
|
1090
|
+
`Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
|
|
1091
|
+
error
|
|
1092
|
+
);
|
|
1093
|
+
safeResolve();
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1106
1096
|
console.error(
|
|
1107
|
-
`Failed to extend visibility for receipt handle ${receiptHandle}:`,
|
|
1097
|
+
`Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
|
|
1108
1098
|
error
|
|
1109
1099
|
);
|
|
1110
|
-
|
|
1100
|
+
if (isRunning) {
|
|
1101
|
+
timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
|
|
1102
|
+
} else {
|
|
1103
|
+
safeResolve();
|
|
1104
|
+
}
|
|
1111
1105
|
}
|
|
1112
1106
|
};
|
|
1113
|
-
timeoutId = setTimeout(() => extend(),
|
|
1107
|
+
timeoutId = setTimeout(() => extend(), firstDelayMs);
|
|
1114
1108
|
return async (waitForCompletion = false) => {
|
|
1115
1109
|
isRunning = false;
|
|
1116
1110
|
if (timeoutId) {
|
|
@@ -1124,8 +1118,11 @@ var ConsumerGroup = class {
|
|
|
1124
1118
|
}
|
|
1125
1119
|
};
|
|
1126
1120
|
}
|
|
1127
|
-
async processMessage(message, handler) {
|
|
1128
|
-
const stopExtension = this.startVisibilityExtension(
|
|
1121
|
+
async processMessage(message, handler, options) {
|
|
1122
|
+
const stopExtension = this.startVisibilityExtension(
|
|
1123
|
+
message.receiptHandle,
|
|
1124
|
+
options
|
|
1125
|
+
);
|
|
1129
1126
|
try {
|
|
1130
1127
|
await handler(message.payload, {
|
|
1131
1128
|
messageId: message.messageId,
|
|
@@ -1142,9 +1139,10 @@ var ConsumerGroup = class {
|
|
|
1142
1139
|
});
|
|
1143
1140
|
} catch (error) {
|
|
1144
1141
|
await stopExtension();
|
|
1145
|
-
|
|
1142
|
+
const transport = this.client.getTransport();
|
|
1143
|
+
if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
1146
1144
|
try {
|
|
1147
|
-
await
|
|
1145
|
+
await transport.finalize(message.payload);
|
|
1148
1146
|
} catch (finalizeError) {
|
|
1149
1147
|
console.warn("Failed to finalize message payload:", finalizeError);
|
|
1150
1148
|
}
|
|
@@ -1152,35 +1150,49 @@ var ConsumerGroup = class {
|
|
|
1152
1150
|
throw error;
|
|
1153
1151
|
}
|
|
1154
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Process a pre-fetched message directly, without calling `receiveMessageById`.
|
|
1155
|
+
*
|
|
1156
|
+
* Used by the binary mode (v2beta) small body fast path, where the server
|
|
1157
|
+
* pushes the full message payload in the callback request. The message is
|
|
1158
|
+
* processed with the same lifecycle guarantees as `consume()`:
|
|
1159
|
+
* - Visibility timeout is extended periodically during processing
|
|
1160
|
+
* - Message is deleted on successful handler completion
|
|
1161
|
+
* - Payload is finalized on error if the transport supports it
|
|
1162
|
+
*
|
|
1163
|
+
* @param handler - Function to process the message payload and metadata
|
|
1164
|
+
* @param message - The complete message including payload and receipt handle
|
|
1165
|
+
* @param options - Optional configuration
|
|
1166
|
+
* @param options.visibilityDeadline - Absolute deadline when the server-assigned
|
|
1167
|
+
* visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
|
|
1168
|
+
* schedule the first visibility extension before the lease expires.
|
|
1169
|
+
*/
|
|
1170
|
+
async consumeMessage(handler, message, options) {
|
|
1171
|
+
await this.processMessage(message, handler, options);
|
|
1172
|
+
}
|
|
1155
1173
|
async consume(handler, options) {
|
|
1156
|
-
if (options
|
|
1157
|
-
const response = await this.client.receiveMessageById(
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
},
|
|
1164
|
-
this.transport
|
|
1165
|
-
);
|
|
1174
|
+
if (options && "messageId" in options) {
|
|
1175
|
+
const response = await this.client.receiveMessageById({
|
|
1176
|
+
queueName: this.topicName,
|
|
1177
|
+
consumerGroup: this.consumerGroupName,
|
|
1178
|
+
messageId: options.messageId,
|
|
1179
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1180
|
+
});
|
|
1166
1181
|
await this.processMessage(response.message, handler);
|
|
1167
1182
|
} else {
|
|
1183
|
+
const limit = options && "limit" in options ? options.limit : 1;
|
|
1168
1184
|
let messageFound = false;
|
|
1169
|
-
for await (const message of this.client.receiveMessages(
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
},
|
|
1176
|
-
this.transport
|
|
1177
|
-
)) {
|
|
1185
|
+
for await (const message of this.client.receiveMessages({
|
|
1186
|
+
queueName: this.topicName,
|
|
1187
|
+
consumerGroup: this.consumerGroupName,
|
|
1188
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1189
|
+
limit
|
|
1190
|
+
})) {
|
|
1178
1191
|
messageFound = true;
|
|
1179
1192
|
await this.processMessage(message, handler);
|
|
1180
|
-
break;
|
|
1181
1193
|
}
|
|
1182
1194
|
if (!messageFound) {
|
|
1183
|
-
|
|
1195
|
+
await handler(null, null);
|
|
1184
1196
|
}
|
|
1185
1197
|
}
|
|
1186
1198
|
}
|
|
@@ -1202,17 +1214,14 @@ var ConsumerGroup = class {
|
|
|
1202
1214
|
var Topic = class {
|
|
1203
1215
|
client;
|
|
1204
1216
|
topicName;
|
|
1205
|
-
transport;
|
|
1206
1217
|
/**
|
|
1207
1218
|
* Create a new Topic instance
|
|
1208
|
-
* @param client QueueClient instance to use for API calls
|
|
1219
|
+
* @param client QueueClient instance to use for API calls (transport is configured on the client)
|
|
1209
1220
|
* @param topicName Name of the topic to work with
|
|
1210
|
-
* @param transport Optional serializer/deserializer for the payload (defaults to JSON)
|
|
1211
1221
|
*/
|
|
1212
|
-
constructor(client, topicName
|
|
1222
|
+
constructor(client, topicName) {
|
|
1213
1223
|
this.client = client;
|
|
1214
1224
|
this.topicName = topicName;
|
|
1215
|
-
this.transport = transport || new JsonTransport();
|
|
1216
1225
|
}
|
|
1217
1226
|
/**
|
|
1218
1227
|
* Publish a message to the topic
|
|
@@ -1225,17 +1234,14 @@ var Topic = class {
|
|
|
1225
1234
|
* @throws {InternalServerError} When server encounters an error
|
|
1226
1235
|
*/
|
|
1227
1236
|
async publish(payload, options) {
|
|
1228
|
-
const result = await this.client.sendMessage(
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
},
|
|
1237
|
-
this.transport
|
|
1238
|
-
);
|
|
1237
|
+
const result = await this.client.sendMessage({
|
|
1238
|
+
queueName: this.topicName,
|
|
1239
|
+
payload,
|
|
1240
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1241
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1242
|
+
delaySeconds: options?.delaySeconds,
|
|
1243
|
+
headers: options?.headers
|
|
1244
|
+
});
|
|
1239
1245
|
if (isDevMode()) {
|
|
1240
1246
|
triggerDevCallbacks(this.topicName, result.messageId);
|
|
1241
1247
|
}
|
|
@@ -1248,15 +1254,11 @@ var Topic = class {
|
|
|
1248
1254
|
* @returns A ConsumerGroup instance
|
|
1249
1255
|
*/
|
|
1250
1256
|
consumerGroup(consumerGroupName, options) {
|
|
1251
|
-
const consumerOptions = {
|
|
1252
|
-
...options,
|
|
1253
|
-
transport: options?.transport || this.transport
|
|
1254
|
-
};
|
|
1255
1257
|
return new ConsumerGroup(
|
|
1256
1258
|
this.client,
|
|
1257
1259
|
this.topicName,
|
|
1258
1260
|
consumerGroupName,
|
|
1259
|
-
|
|
1261
|
+
options
|
|
1260
1262
|
);
|
|
1261
1263
|
}
|
|
1262
1264
|
/**
|
|
@@ -1265,220 +1267,167 @@ var Topic = class {
|
|
|
1265
1267
|
get name() {
|
|
1266
1268
|
return this.topicName;
|
|
1267
1269
|
}
|
|
1268
|
-
/**
|
|
1269
|
-
* Get the transport used by this topic
|
|
1270
|
-
*/
|
|
1271
|
-
get serializer() {
|
|
1272
|
-
return this.transport;
|
|
1273
|
-
}
|
|
1274
1270
|
};
|
|
1275
1271
|
|
|
1276
1272
|
// src/callback.ts
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
const lastIndex = pattern.lastIndexOf("*");
|
|
1280
|
-
if (firstIndex !== lastIndex) {
|
|
1281
|
-
return false;
|
|
1282
|
-
}
|
|
1283
|
-
if (firstIndex === -1) {
|
|
1284
|
-
return false;
|
|
1285
|
-
}
|
|
1286
|
-
if (firstIndex !== pattern.length - 1) {
|
|
1287
|
-
return false;
|
|
1288
|
-
}
|
|
1289
|
-
return true;
|
|
1290
|
-
}
|
|
1273
|
+
var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
|
|
1274
|
+
var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
|
|
1291
1275
|
function matchesWildcardPattern(topicName, pattern) {
|
|
1292
1276
|
const prefix = pattern.slice(0, -1);
|
|
1293
1277
|
return topicName.startsWith(prefix);
|
|
1294
1278
|
}
|
|
1295
|
-
function
|
|
1296
|
-
|
|
1297
|
-
if (exactHandler) {
|
|
1298
|
-
return exactHandler;
|
|
1299
|
-
}
|
|
1300
|
-
for (const pattern in handlers) {
|
|
1301
|
-
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
1302
|
-
return handlers[pattern];
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
return null;
|
|
1279
|
+
function isRecord(value) {
|
|
1280
|
+
return typeof value === "object" && value !== null;
|
|
1306
1281
|
}
|
|
1307
|
-
|
|
1308
|
-
const contentType = request.headers.get("content-type");
|
|
1282
|
+
function parseV1StructuredBody(body, contentType) {
|
|
1309
1283
|
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
1310
1284
|
throw new Error(
|
|
1311
1285
|
"Invalid content type: expected 'application/cloudevents+json'"
|
|
1312
1286
|
);
|
|
1313
1287
|
}
|
|
1314
|
-
|
|
1315
|
-
try {
|
|
1316
|
-
cloudEvent = await request.json();
|
|
1317
|
-
} catch (error) {
|
|
1318
|
-
throw new Error("Failed to parse CloudEvent from request body");
|
|
1319
|
-
}
|
|
1320
|
-
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
1288
|
+
if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
|
|
1321
1289
|
throw new Error("Invalid CloudEvent: missing required fields");
|
|
1322
1290
|
}
|
|
1323
|
-
if (
|
|
1291
|
+
if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
|
|
1324
1292
|
throw new Error(
|
|
1325
|
-
`Invalid CloudEvent type: expected '
|
|
1293
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
|
|
1326
1294
|
);
|
|
1327
1295
|
}
|
|
1296
|
+
const { data } = body;
|
|
1328
1297
|
const missingFields = [];
|
|
1329
|
-
if (!("queueName" in
|
|
1330
|
-
if (!("consumerGroup" in
|
|
1331
|
-
|
|
1332
|
-
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
1298
|
+
if (!("queueName" in data)) missingFields.push("queueName");
|
|
1299
|
+
if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
|
|
1300
|
+
if (!("messageId" in data)) missingFields.push("messageId");
|
|
1333
1301
|
if (missingFields.length > 0) {
|
|
1334
1302
|
throw new Error(
|
|
1335
1303
|
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1336
1304
|
);
|
|
1337
1305
|
}
|
|
1338
|
-
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
1339
1306
|
return {
|
|
1340
|
-
queueName,
|
|
1341
|
-
consumerGroup,
|
|
1342
|
-
messageId
|
|
1307
|
+
queueName: String(data.queueName),
|
|
1308
|
+
consumerGroup: String(data.consumerGroup),
|
|
1309
|
+
messageId: String(data.messageId)
|
|
1343
1310
|
};
|
|
1344
1311
|
}
|
|
1345
|
-
function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
|
|
1346
|
-
for (const topicPattern in handlers) {
|
|
1347
|
-
if (topicPattern.includes("*")) {
|
|
1348
|
-
if (!validateWildcardPattern(topicPattern)) {
|
|
1349
|
-
throw new Error(
|
|
1350
|
-
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
1351
|
-
);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
const routeHandler = async (request) => {
|
|
1356
|
-
try {
|
|
1357
|
-
const { queueName, consumerGroup, messageId } = await parseCallback(request);
|
|
1358
|
-
const topicHandler = findTopicHandler(queueName, handlers);
|
|
1359
|
-
if (!topicHandler) {
|
|
1360
|
-
const availableTopics = Object.keys(handlers).join(", ");
|
|
1361
|
-
return Response.json(
|
|
1362
|
-
{
|
|
1363
|
-
error: `No handler found for topic: ${queueName}`,
|
|
1364
|
-
availableTopics
|
|
1365
|
-
},
|
|
1366
|
-
{ status: 404 }
|
|
1367
|
-
);
|
|
1368
|
-
}
|
|
1369
|
-
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1370
|
-
if (!consumerGroupHandler) {
|
|
1371
|
-
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1372
|
-
return Response.json(
|
|
1373
|
-
{
|
|
1374
|
-
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
1375
|
-
availableGroups
|
|
1376
|
-
},
|
|
1377
|
-
{ status: 404 }
|
|
1378
|
-
);
|
|
1379
|
-
}
|
|
1380
|
-
const topic = new Topic(client, queueName);
|
|
1381
|
-
const cg = topic.consumerGroup(
|
|
1382
|
-
consumerGroup,
|
|
1383
|
-
visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
|
|
1384
|
-
);
|
|
1385
|
-
await cg.consume(consumerGroupHandler, { messageId });
|
|
1386
|
-
return Response.json({ status: "success" });
|
|
1387
|
-
} catch (error) {
|
|
1388
|
-
console.error("Queue callback error:", error);
|
|
1389
|
-
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"))) {
|
|
1390
|
-
return Response.json({ error: error.message }, { status: 400 });
|
|
1391
|
-
}
|
|
1392
|
-
return Response.json(
|
|
1393
|
-
{ error: "Failed to process queue message" },
|
|
1394
|
-
{ status: 500 }
|
|
1395
|
-
);
|
|
1396
|
-
}
|
|
1397
|
-
};
|
|
1398
|
-
return routeHandler;
|
|
1399
|
-
}
|
|
1400
|
-
function handleCallback(handlers, options) {
|
|
1401
|
-
return createCallbackHandler(
|
|
1402
|
-
handlers,
|
|
1403
|
-
options?.client || new QueueClient(),
|
|
1404
|
-
options?.visibilityTimeoutSeconds
|
|
1405
|
-
);
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
// src/nextjs-pages.ts
|
|
1409
1312
|
function getHeader(headers, name) {
|
|
1313
|
+
if (headers instanceof Headers) {
|
|
1314
|
+
return headers.get(name);
|
|
1315
|
+
}
|
|
1410
1316
|
const value = headers[name];
|
|
1411
|
-
|
|
1317
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
1318
|
+
return value ?? null;
|
|
1412
1319
|
}
|
|
1413
|
-
function
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
});
|
|
1420
|
-
}
|
|
1421
|
-
async function getBody(req) {
|
|
1422
|
-
if (req.body === void 0) {
|
|
1423
|
-
return readBody(req);
|
|
1320
|
+
function parseBinaryHeaders(headers) {
|
|
1321
|
+
const ceType = getHeader(headers, "ce-type");
|
|
1322
|
+
if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
|
|
1325
|
+
);
|
|
1424
1326
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1327
|
+
const queueName = getHeader(headers, "ce-vqsqueuename");
|
|
1328
|
+
const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
|
|
1329
|
+
const messageId = getHeader(headers, "ce-vqsmessageid");
|
|
1330
|
+
const missingFields = [];
|
|
1331
|
+
if (!queueName) missingFields.push("ce-vqsqueuename");
|
|
1332
|
+
if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
|
|
1333
|
+
if (!messageId) missingFields.push("ce-vqsmessageid");
|
|
1334
|
+
if (missingFields.length > 0) {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
`Missing required CloudEvent headers: ${missingFields.join(", ")}`
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
const base = {
|
|
1340
|
+
queueName,
|
|
1341
|
+
consumerGroup,
|
|
1342
|
+
messageId
|
|
1343
|
+
};
|
|
1344
|
+
const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
|
|
1345
|
+
if (!receiptHandle) {
|
|
1346
|
+
return base;
|
|
1347
|
+
}
|
|
1348
|
+
const result = { ...base, receiptHandle };
|
|
1349
|
+
const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
|
|
1350
|
+
if (deliveryCount) {
|
|
1351
|
+
result.deliveryCount = parseInt(deliveryCount, 10);
|
|
1352
|
+
}
|
|
1353
|
+
const createdAt = getHeader(headers, "ce-vqscreatedat");
|
|
1354
|
+
if (createdAt) {
|
|
1355
|
+
result.createdAt = createdAt;
|
|
1356
|
+
}
|
|
1357
|
+
const contentType = getHeader(headers, "content-type");
|
|
1358
|
+
if (contentType) {
|
|
1359
|
+
result.contentType = contentType;
|
|
1427
1360
|
}
|
|
1428
|
-
|
|
1361
|
+
const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
|
|
1362
|
+
if (visibilityDeadline) {
|
|
1363
|
+
result.visibilityDeadline = visibilityDeadline;
|
|
1364
|
+
}
|
|
1365
|
+
return result;
|
|
1429
1366
|
}
|
|
1430
|
-
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
const url = `${protocol}://${host}${req.url}`;
|
|
1437
|
-
const headers = new Headers();
|
|
1438
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
1439
|
-
if (value) {
|
|
1440
|
-
if (Array.isArray(value)) {
|
|
1441
|
-
value.forEach((v) => headers.append(key, v));
|
|
1442
|
-
} else {
|
|
1443
|
-
headers.set(key, value);
|
|
1444
|
-
}
|
|
1367
|
+
function parseRawCallback(body, headers) {
|
|
1368
|
+
const ceType = getHeader(headers, "ce-type");
|
|
1369
|
+
if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
|
|
1370
|
+
const result = parseBinaryHeaders(headers);
|
|
1371
|
+
if ("receiptHandle" in result) {
|
|
1372
|
+
result.parsedPayload = body;
|
|
1445
1373
|
}
|
|
1374
|
+
return result;
|
|
1446
1375
|
}
|
|
1447
|
-
|
|
1448
|
-
return new Request(url, {
|
|
1449
|
-
method: req.method || "POST",
|
|
1450
|
-
headers,
|
|
1451
|
-
body
|
|
1452
|
-
});
|
|
1376
|
+
return parseV1StructuredBody(body, getHeader(headers, "content-type"));
|
|
1453
1377
|
}
|
|
1454
|
-
async function
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1378
|
+
async function handleCallback(handler, request, options) {
|
|
1379
|
+
const { queueName, consumerGroup, messageId } = request;
|
|
1380
|
+
const client = options?.client || new QueueClient();
|
|
1381
|
+
const topic = new Topic(client, queueName);
|
|
1382
|
+
const cg = topic.consumerGroup(
|
|
1383
|
+
consumerGroup,
|
|
1384
|
+
options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
|
|
1385
|
+
);
|
|
1386
|
+
if ("receiptHandle" in request) {
|
|
1387
|
+
const transport = client.getTransport();
|
|
1388
|
+
let payload;
|
|
1389
|
+
if (request.rawBody) {
|
|
1390
|
+
payload = await transport.deserialize(request.rawBody);
|
|
1391
|
+
} else if (request.parsedPayload !== void 0) {
|
|
1392
|
+
payload = request.parsedPayload;
|
|
1393
|
+
} else {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
"Binary mode callback with receipt handle is missing payload"
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const message = {
|
|
1399
|
+
messageId,
|
|
1400
|
+
payload,
|
|
1401
|
+
deliveryCount: request.deliveryCount ?? 1,
|
|
1402
|
+
createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
|
|
1403
|
+
contentType: request.contentType ?? transport.contentType,
|
|
1404
|
+
receiptHandle: request.receiptHandle
|
|
1405
|
+
};
|
|
1406
|
+
const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
|
|
1407
|
+
await cg.consumeMessage(handler, message, { visibilityDeadline });
|
|
1463
1408
|
} else {
|
|
1464
|
-
|
|
1465
|
-
res.send(text);
|
|
1409
|
+
await cg.consume(handler, { messageId });
|
|
1466
1410
|
}
|
|
1467
1411
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1412
|
+
|
|
1413
|
+
// src/nextjs-pages.ts
|
|
1414
|
+
function handleCallback2(handler, options) {
|
|
1470
1415
|
return async (req, res) => {
|
|
1471
1416
|
if (req.method !== "POST") {
|
|
1472
1417
|
res.status(200).end();
|
|
1473
1418
|
return;
|
|
1474
1419
|
}
|
|
1475
1420
|
try {
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
|
|
1421
|
+
const parsed = parseRawCallback(req.body, req.headers);
|
|
1422
|
+
await handleCallback(handler, parsed, options);
|
|
1423
|
+
res.status(200).json({ status: "success" });
|
|
1479
1424
|
} catch (error) {
|
|
1480
|
-
console.error("
|
|
1481
|
-
|
|
1425
|
+
console.error("Queue callback error:", error);
|
|
1426
|
+
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"))) {
|
|
1427
|
+
res.status(400).json({ error: error.message });
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
res.status(500).json({ error: "Failed to process queue message" });
|
|
1482
1431
|
}
|
|
1483
1432
|
};
|
|
1484
1433
|
}
|