@vercel/queue 0.0.0-alpha.37 → 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 +208 -168
- package/dist/{types-CAA8nT8x.d.mts → callback-lq_sorrn.d.mts} +249 -16
- package/dist/{types-CAA8nT8x.d.ts → callback-lq_sorrn.d.ts} +249 -16
- package/dist/index.d.mts +74 -333
- package/dist/index.d.ts +74 -333
- package/dist/index.js +713 -679
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +709 -678
- 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 +320 -313
- package/dist/nextjs-pages.js.map +1 -1
- package/dist/nextjs-pages.mjs +320 -313
- 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/web.js
ADDED
|
@@ -0,0 +1,1457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/web.ts
|
|
31
|
+
var web_exports = {};
|
|
32
|
+
__export(web_exports, {
|
|
33
|
+
handleCallback: () => handleCallback2
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(web_exports);
|
|
36
|
+
|
|
37
|
+
// src/client.ts
|
|
38
|
+
var import_mixpart = require("mixpart");
|
|
39
|
+
|
|
40
|
+
// src/dev.ts
|
|
41
|
+
var fs = __toESM(require("fs"));
|
|
42
|
+
var path = __toESM(require("path"));
|
|
43
|
+
|
|
44
|
+
// src/types.ts
|
|
45
|
+
var MessageNotFoundError = class extends Error {
|
|
46
|
+
constructor(messageId) {
|
|
47
|
+
super(`Message ${messageId} not found`);
|
|
48
|
+
this.name = "MessageNotFoundError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var MessageNotAvailableError = class extends Error {
|
|
52
|
+
constructor(messageId, reason) {
|
|
53
|
+
super(
|
|
54
|
+
`Message ${messageId} not available for processing${reason ? `: ${reason}` : ""}`
|
|
55
|
+
);
|
|
56
|
+
this.name = "MessageNotAvailableError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var MessageCorruptedError = class extends Error {
|
|
60
|
+
constructor(messageId, reason) {
|
|
61
|
+
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
62
|
+
this.name = "MessageCorruptedError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var UnauthorizedError = class extends Error {
|
|
66
|
+
constructor(message = "Missing or invalid authentication token") {
|
|
67
|
+
super(message);
|
|
68
|
+
this.name = "UnauthorizedError";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var ForbiddenError = class extends Error {
|
|
72
|
+
constructor(message = "Queue environment doesn't match token environment") {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "ForbiddenError";
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var BadRequestError = class extends Error {
|
|
78
|
+
constructor(message) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "BadRequestError";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var InternalServerError = class extends Error {
|
|
84
|
+
constructor(message = "Unexpected server error") {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "InternalServerError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var InvalidLimitError = class extends Error {
|
|
90
|
+
constructor(limit, min = 1, max = 10) {
|
|
91
|
+
super(`Invalid limit: ${limit}. Limit must be between ${min} and ${max}.`);
|
|
92
|
+
this.name = "InvalidLimitError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var MessageAlreadyProcessedError = class extends Error {
|
|
96
|
+
constructor(messageId) {
|
|
97
|
+
super(`Message ${messageId} has already been processed`);
|
|
98
|
+
this.name = "MessageAlreadyProcessedError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var DuplicateMessageError = class extends Error {
|
|
102
|
+
idempotencyKey;
|
|
103
|
+
constructor(message, idempotencyKey) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "DuplicateMessageError";
|
|
106
|
+
this.idempotencyKey = idempotencyKey;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var ConsumerDiscoveryError = class extends Error {
|
|
110
|
+
deploymentId;
|
|
111
|
+
constructor(message, deploymentId) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = "ConsumerDiscoveryError";
|
|
114
|
+
this.deploymentId = deploymentId;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var ConsumerRegistryNotConfiguredError = class extends Error {
|
|
118
|
+
constructor(message = "Consumer registry not configured") {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "ConsumerRegistryNotConfiguredError";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/dev.ts
|
|
125
|
+
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
126
|
+
function filePathToUrlPath(filePath) {
|
|
127
|
+
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
|
|
128
|
+
if (!urlPath.startsWith("/")) {
|
|
129
|
+
urlPath = "/" + urlPath;
|
|
130
|
+
}
|
|
131
|
+
return urlPath;
|
|
132
|
+
}
|
|
133
|
+
function filePathToConsumerGroup(filePath) {
|
|
134
|
+
return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
|
|
135
|
+
}
|
|
136
|
+
function getDevRouteMappings() {
|
|
137
|
+
const g = globalThis;
|
|
138
|
+
if (ROUTE_MAPPINGS_KEY in g) {
|
|
139
|
+
return g[ROUTE_MAPPINGS_KEY] ?? null;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const vercelJsonPath = path.join(process.cwd(), "vercel.json");
|
|
143
|
+
if (!fs.existsSync(vercelJsonPath)) {
|
|
144
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
|
|
148
|
+
if (!vercelJson.functions) {
|
|
149
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const mappings = [];
|
|
153
|
+
for (const [filePath, config] of Object.entries(vercelJson.functions)) {
|
|
154
|
+
if (!config.experimentalTriggers) continue;
|
|
155
|
+
for (const trigger of config.experimentalTriggers) {
|
|
156
|
+
if (trigger.type?.startsWith("queue/") && trigger.topic) {
|
|
157
|
+
mappings.push({
|
|
158
|
+
urlPath: filePathToUrlPath(filePath),
|
|
159
|
+
topic: trigger.topic,
|
|
160
|
+
consumer: filePathToConsumerGroup(filePath)
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
|
|
166
|
+
return g[ROUTE_MAPPINGS_KEY];
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn("[Dev Mode] Failed to read vercel.json:", error);
|
|
169
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function findMatchingRoutes(topicName) {
|
|
174
|
+
const mappings = getDevRouteMappings();
|
|
175
|
+
if (!mappings) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
return mappings.filter((mapping) => {
|
|
179
|
+
if (mapping.topic.includes("*")) {
|
|
180
|
+
return matchesWildcardPattern(topicName, mapping.topic);
|
|
181
|
+
}
|
|
182
|
+
return mapping.topic === topicName;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function isDevMode() {
|
|
186
|
+
return process.env.NODE_ENV === "development";
|
|
187
|
+
}
|
|
188
|
+
var DEV_VISIBILITY_POLL_INTERVAL = 50;
|
|
189
|
+
var DEV_VISIBILITY_MAX_WAIT = 5e3;
|
|
190
|
+
var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
|
|
191
|
+
async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
|
|
192
|
+
const client = new QueueClient();
|
|
193
|
+
let elapsed = 0;
|
|
194
|
+
let interval = DEV_VISIBILITY_POLL_INTERVAL;
|
|
195
|
+
while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
|
|
196
|
+
try {
|
|
197
|
+
await client.receiveMessageById({
|
|
198
|
+
queueName: topicName,
|
|
199
|
+
consumerGroup,
|
|
200
|
+
messageId,
|
|
201
|
+
visibilityTimeoutSeconds: 0
|
|
202
|
+
});
|
|
203
|
+
return true;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof MessageNotFoundError) {
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
207
|
+
elapsed += interval;
|
|
208
|
+
interval = Math.min(
|
|
209
|
+
interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
|
|
210
|
+
DEV_VISIBILITY_MAX_WAIT - elapsed
|
|
211
|
+
);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (error instanceof MessageAlreadyProcessedError) {
|
|
215
|
+
console.log(
|
|
216
|
+
`[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
|
|
217
|
+
);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
console.error(
|
|
221
|
+
`[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
|
|
222
|
+
error
|
|
223
|
+
);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
console.warn(
|
|
228
|
+
`[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
|
|
229
|
+
);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
function triggerDevCallbacks(topicName, messageId, delaySeconds) {
|
|
233
|
+
if (delaySeconds && delaySeconds > 0) {
|
|
234
|
+
console.log(
|
|
235
|
+
`[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
|
|
236
|
+
);
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
triggerDevCallbacks(topicName, messageId);
|
|
239
|
+
}, delaySeconds * 1e3);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
console.log(
|
|
243
|
+
`[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
|
|
244
|
+
);
|
|
245
|
+
const matchingRoutes = findMatchingRoutes(topicName);
|
|
246
|
+
if (matchingRoutes.length === 0) {
|
|
247
|
+
console.log(
|
|
248
|
+
`[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const consumerGroups = matchingRoutes.map((r) => r.consumer);
|
|
253
|
+
console.log(
|
|
254
|
+
`[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
|
|
255
|
+
);
|
|
256
|
+
(async () => {
|
|
257
|
+
const firstRoute = matchingRoutes[0];
|
|
258
|
+
const isVisible = await waitForMessageVisibility(
|
|
259
|
+
topicName,
|
|
260
|
+
firstRoute.consumer,
|
|
261
|
+
messageId
|
|
262
|
+
);
|
|
263
|
+
if (!isVisible) {
|
|
264
|
+
console.warn(
|
|
265
|
+
`[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
|
|
266
|
+
);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const port = process.env.PORT || 3e3;
|
|
270
|
+
const baseUrl = `http://localhost:${port}`;
|
|
271
|
+
for (const route of matchingRoutes) {
|
|
272
|
+
const url = `${baseUrl}${route.urlPath}`;
|
|
273
|
+
console.log(
|
|
274
|
+
`[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
|
|
275
|
+
);
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(url, {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers: {
|
|
280
|
+
"ce-type": CLOUD_EVENT_TYPE_V2BETA,
|
|
281
|
+
"ce-vqsqueuename": topicName,
|
|
282
|
+
"ce-vqsconsumergroup": route.consumer,
|
|
283
|
+
"ce-vqsmessageid": messageId
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
if (response.ok) {
|
|
287
|
+
try {
|
|
288
|
+
const responseData = await response.json();
|
|
289
|
+
if (responseData.status === "success") {
|
|
290
|
+
console.log(
|
|
291
|
+
`[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
console.warn(
|
|
296
|
+
`[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
try {
|
|
301
|
+
const errorData = await response.json();
|
|
302
|
+
console.error(
|
|
303
|
+
`[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
|
|
304
|
+
);
|
|
305
|
+
} catch {
|
|
306
|
+
console.error(
|
|
307
|
+
`[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error(
|
|
313
|
+
`[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
|
|
314
|
+
error
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
})();
|
|
319
|
+
}
|
|
320
|
+
function clearDevRouteMappings() {
|
|
321
|
+
const g = globalThis;
|
|
322
|
+
delete g[ROUTE_MAPPINGS_KEY];
|
|
323
|
+
}
|
|
324
|
+
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
325
|
+
globalThis.__clearDevRouteMappings = clearDevRouteMappings;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/oidc.ts
|
|
329
|
+
var import_oidc = require("@vercel/oidc");
|
|
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
|
+
|
|
371
|
+
// src/client.ts
|
|
372
|
+
function isDebugEnabled() {
|
|
373
|
+
return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
|
|
374
|
+
}
|
|
375
|
+
async function consumeStream(stream) {
|
|
376
|
+
const reader = stream.getReader();
|
|
377
|
+
try {
|
|
378
|
+
while (true) {
|
|
379
|
+
const { done } = await reader.read();
|
|
380
|
+
if (done) break;
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
reader.releaseLock();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
|
|
387
|
+
if (status === 400) {
|
|
388
|
+
throw new BadRequestError(errorText || badRequestDefault);
|
|
389
|
+
}
|
|
390
|
+
if (status === 401) {
|
|
391
|
+
throw new UnauthorizedError(errorText || void 0);
|
|
392
|
+
}
|
|
393
|
+
if (status === 403) {
|
|
394
|
+
throw new ForbiddenError(errorText || void 0);
|
|
395
|
+
}
|
|
396
|
+
if (status >= 500) {
|
|
397
|
+
throw new InternalServerError(
|
|
398
|
+
errorText || `Server error: ${status} ${statusText}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
|
|
402
|
+
}
|
|
403
|
+
function parseQueueHeaders(headers) {
|
|
404
|
+
const messageId = headers.get("Vqs-Message-Id");
|
|
405
|
+
const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
|
|
406
|
+
const timestamp = headers.get("Vqs-Timestamp");
|
|
407
|
+
const contentType = headers.get("Content-Type") || "application/octet-stream";
|
|
408
|
+
const receiptHandle = headers.get("Vqs-Receipt-Handle");
|
|
409
|
+
if (!messageId || !timestamp || !receiptHandle) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const deliveryCount = parseInt(deliveryCountStr, 10);
|
|
413
|
+
if (Number.isNaN(deliveryCount)) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
messageId,
|
|
418
|
+
deliveryCount,
|
|
419
|
+
createdAt: new Date(timestamp),
|
|
420
|
+
contentType,
|
|
421
|
+
receiptHandle
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
var QueueClient = class {
|
|
425
|
+
baseUrl;
|
|
426
|
+
basePath;
|
|
427
|
+
customHeaders;
|
|
428
|
+
providedToken;
|
|
429
|
+
defaultDeploymentId;
|
|
430
|
+
pinToDeployment;
|
|
431
|
+
transport;
|
|
432
|
+
constructor(options = {}) {
|
|
433
|
+
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
434
|
+
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
|
|
435
|
+
this.customHeaders = options.headers || {};
|
|
436
|
+
this.providedToken = options.token;
|
|
437
|
+
this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
438
|
+
this.pinToDeployment = options.pinToDeployment ?? true;
|
|
439
|
+
this.transport = options.transport || new JsonTransport();
|
|
440
|
+
}
|
|
441
|
+
getTransport() {
|
|
442
|
+
return this.transport;
|
|
443
|
+
}
|
|
444
|
+
getSendDeploymentId() {
|
|
445
|
+
if (isDevMode()) {
|
|
446
|
+
return void 0;
|
|
447
|
+
}
|
|
448
|
+
if (this.pinToDeployment) {
|
|
449
|
+
return this.defaultDeploymentId;
|
|
450
|
+
}
|
|
451
|
+
return void 0;
|
|
452
|
+
}
|
|
453
|
+
getConsumeDeploymentId() {
|
|
454
|
+
if (isDevMode()) {
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
return this.defaultDeploymentId;
|
|
458
|
+
}
|
|
459
|
+
async getToken() {
|
|
460
|
+
if (this.providedToken) {
|
|
461
|
+
return this.providedToken;
|
|
462
|
+
}
|
|
463
|
+
const token = await (0, import_oidc.getVercelOidcToken)();
|
|
464
|
+
if (!token) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
"Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment, or provide a token explicitly.\n\nTo set up your environment:\n1. Link your project: 'vercel link'\n2. Pull environment variables: 'vercel env pull'\n3. Run with environment: 'dotenv -e .env.local -- your-command'"
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return token;
|
|
470
|
+
}
|
|
471
|
+
buildUrl(queueName, ...pathSegments) {
|
|
472
|
+
const encodedQueue = encodeURIComponent(queueName);
|
|
473
|
+
const segments = pathSegments.map((s) => encodeURIComponent(s));
|
|
474
|
+
const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
|
|
475
|
+
return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
|
|
476
|
+
}
|
|
477
|
+
async fetch(url, init) {
|
|
478
|
+
const method = init.method || "GET";
|
|
479
|
+
if (isDebugEnabled()) {
|
|
480
|
+
const logData = {
|
|
481
|
+
method,
|
|
482
|
+
url,
|
|
483
|
+
headers: init.headers
|
|
484
|
+
};
|
|
485
|
+
const body = init.body;
|
|
486
|
+
if (body !== void 0 && body !== null) {
|
|
487
|
+
if (body instanceof ArrayBuffer) {
|
|
488
|
+
logData.bodySize = body.byteLength;
|
|
489
|
+
} else if (body instanceof Uint8Array) {
|
|
490
|
+
logData.bodySize = body.byteLength;
|
|
491
|
+
} else if (typeof body === "string") {
|
|
492
|
+
logData.bodySize = body.length;
|
|
493
|
+
} else {
|
|
494
|
+
logData.bodyType = typeof body;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
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());
|
|
501
|
+
const response = await fetch(url, init);
|
|
502
|
+
if (isDebugEnabled()) {
|
|
503
|
+
const logData = {
|
|
504
|
+
method,
|
|
505
|
+
url,
|
|
506
|
+
status: response.status,
|
|
507
|
+
statusText: response.statusText,
|
|
508
|
+
headers: response.headers
|
|
509
|
+
};
|
|
510
|
+
console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
|
|
511
|
+
}
|
|
512
|
+
return response;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Send a message to a topic.
|
|
516
|
+
*
|
|
517
|
+
* @param options - Message options including queue name, payload, and optional settings
|
|
518
|
+
* @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
|
|
519
|
+
* @param options.payload - Message payload
|
|
520
|
+
* @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
|
|
521
|
+
* @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
|
|
522
|
+
* @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
|
|
523
|
+
* @returns Promise with the generated messageId
|
|
524
|
+
* @throws {DuplicateMessageError} When idempotency key was already used
|
|
525
|
+
* @throws {ConsumerDiscoveryError} When consumer discovery fails
|
|
526
|
+
* @throws {ConsumerRegistryNotConfiguredError} When registry not configured
|
|
527
|
+
* @throws {BadRequestError} When parameters are invalid
|
|
528
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
529
|
+
* @throws {ForbiddenError} When access is denied
|
|
530
|
+
* @throws {InternalServerError} When server encounters an error
|
|
531
|
+
*/
|
|
532
|
+
async sendMessage(options) {
|
|
533
|
+
const transport = this.transport;
|
|
534
|
+
const {
|
|
535
|
+
queueName,
|
|
536
|
+
payload,
|
|
537
|
+
idempotencyKey,
|
|
538
|
+
retentionSeconds,
|
|
539
|
+
delaySeconds,
|
|
540
|
+
headers: optionHeaders
|
|
541
|
+
} = options;
|
|
542
|
+
const headers = new Headers();
|
|
543
|
+
if (this.customHeaders) {
|
|
544
|
+
for (const [name, value] of Object.entries(this.customHeaders)) {
|
|
545
|
+
headers.append(name, value);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (optionHeaders) {
|
|
549
|
+
const protectedHeaderNames = /* @__PURE__ */ new Set(["authorization", "content-type"]);
|
|
550
|
+
const isProtectedHeader = (name) => {
|
|
551
|
+
const lower = name.toLowerCase();
|
|
552
|
+
if (protectedHeaderNames.has(lower)) return true;
|
|
553
|
+
return lower.startsWith("vqs-");
|
|
554
|
+
};
|
|
555
|
+
for (const [name, value] of Object.entries(optionHeaders)) {
|
|
556
|
+
if (!isProtectedHeader(name) && value !== void 0) {
|
|
557
|
+
headers.append(name, value);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
headers.set("Authorization", `Bearer ${await this.getToken()}`);
|
|
562
|
+
headers.set("Content-Type", transport.contentType);
|
|
563
|
+
const deploymentId = this.getSendDeploymentId();
|
|
564
|
+
if (deploymentId) {
|
|
565
|
+
headers.set("Vqs-Deployment-Id", deploymentId);
|
|
566
|
+
}
|
|
567
|
+
if (idempotencyKey) {
|
|
568
|
+
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
569
|
+
}
|
|
570
|
+
if (retentionSeconds !== void 0) {
|
|
571
|
+
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
572
|
+
}
|
|
573
|
+
if (delaySeconds !== void 0) {
|
|
574
|
+
headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
|
|
575
|
+
}
|
|
576
|
+
const serialized = transport.serialize(payload);
|
|
577
|
+
const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
|
|
578
|
+
const response = await this.fetch(this.buildUrl(queueName), {
|
|
579
|
+
method: "POST",
|
|
580
|
+
body,
|
|
581
|
+
headers
|
|
582
|
+
});
|
|
583
|
+
if (!response.ok) {
|
|
584
|
+
const errorText = await response.text();
|
|
585
|
+
if (response.status === 409) {
|
|
586
|
+
throw new DuplicateMessageError(
|
|
587
|
+
errorText || "Duplicate idempotency key detected",
|
|
588
|
+
idempotencyKey
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
if (response.status === 502) {
|
|
592
|
+
throw new ConsumerDiscoveryError(
|
|
593
|
+
errorText || "Consumer discovery failed",
|
|
594
|
+
deploymentId
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (response.status === 503) {
|
|
598
|
+
throw new ConsumerRegistryNotConfiguredError(
|
|
599
|
+
errorText || "Consumer registry not configured"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
throwCommonHttpError(
|
|
603
|
+
response.status,
|
|
604
|
+
response.statusText,
|
|
605
|
+
errorText,
|
|
606
|
+
"send message"
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const responseData = await response.json();
|
|
610
|
+
return responseData;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Receive messages from a topic as an async generator.
|
|
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
|
+
*
|
|
618
|
+
* @param options - Receive options
|
|
619
|
+
* @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
|
|
620
|
+
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
621
|
+
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
622
|
+
* @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
|
|
623
|
+
* @yields Message objects with payload, messageId, receiptHandle, etc.
|
|
624
|
+
* Yields nothing if queue is empty.
|
|
625
|
+
* @throws {InvalidLimitError} When limit is outside 1-10 range
|
|
626
|
+
* @throws {BadRequestError} When parameters are invalid
|
|
627
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
628
|
+
* @throws {ForbiddenError} When access is denied
|
|
629
|
+
* @throws {InternalServerError} When server encounters an error
|
|
630
|
+
*/
|
|
631
|
+
async *receiveMessages(options) {
|
|
632
|
+
const transport = this.transport;
|
|
633
|
+
const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
|
|
634
|
+
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
635
|
+
throw new InvalidLimitError(limit);
|
|
636
|
+
}
|
|
637
|
+
const headers = new Headers({
|
|
638
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
639
|
+
Accept: "multipart/mixed",
|
|
640
|
+
...this.customHeaders
|
|
641
|
+
});
|
|
642
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
643
|
+
headers.set(
|
|
644
|
+
"Vqs-Visibility-Timeout-Seconds",
|
|
645
|
+
visibilityTimeoutSeconds.toString()
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
if (limit !== void 0) {
|
|
649
|
+
headers.set("Vqs-Max-Messages", limit.toString());
|
|
650
|
+
}
|
|
651
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
652
|
+
if (effectiveDeploymentId) {
|
|
653
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
654
|
+
}
|
|
655
|
+
const response = await this.fetch(
|
|
656
|
+
this.buildUrl(queueName, "consumer", consumerGroup),
|
|
657
|
+
{
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers
|
|
660
|
+
}
|
|
661
|
+
);
|
|
662
|
+
if (response.status === 204) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (!response.ok) {
|
|
666
|
+
const errorText = await response.text();
|
|
667
|
+
throwCommonHttpError(
|
|
668
|
+
response.status,
|
|
669
|
+
response.statusText,
|
|
670
|
+
errorText,
|
|
671
|
+
"receive messages"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
675
|
+
try {
|
|
676
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
677
|
+
if (!parsedHeaders) {
|
|
678
|
+
console.warn("Missing required queue headers in multipart part");
|
|
679
|
+
await consumeStream(multipartMessage.payload);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const deserializedPayload = await transport.deserialize(
|
|
683
|
+
multipartMessage.payload
|
|
684
|
+
);
|
|
685
|
+
const message = {
|
|
686
|
+
...parsedHeaders,
|
|
687
|
+
payload: deserializedPayload
|
|
688
|
+
};
|
|
689
|
+
yield message;
|
|
690
|
+
} catch (error) {
|
|
691
|
+
console.warn("Failed to process multipart message:", error);
|
|
692
|
+
await consumeStream(multipartMessage.payload);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Receive a specific message by its ID.
|
|
698
|
+
*
|
|
699
|
+
* @param options - Receive options
|
|
700
|
+
* @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
|
|
701
|
+
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
702
|
+
* @param options.messageId - Message ID to retrieve
|
|
703
|
+
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
704
|
+
* @returns Promise with the message
|
|
705
|
+
* @throws {MessageNotFoundError} When message doesn't exist
|
|
706
|
+
* @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
|
|
707
|
+
* @throws {MessageAlreadyProcessedError} When message was already processed
|
|
708
|
+
* @throws {BadRequestError} When parameters are invalid
|
|
709
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
710
|
+
* @throws {ForbiddenError} When access is denied
|
|
711
|
+
* @throws {InternalServerError} When server encounters an error
|
|
712
|
+
*/
|
|
713
|
+
async receiveMessageById(options) {
|
|
714
|
+
const transport = this.transport;
|
|
715
|
+
const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
|
|
716
|
+
const headers = new Headers({
|
|
717
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
718
|
+
Accept: "multipart/mixed",
|
|
719
|
+
...this.customHeaders
|
|
720
|
+
});
|
|
721
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
722
|
+
headers.set(
|
|
723
|
+
"Vqs-Visibility-Timeout-Seconds",
|
|
724
|
+
visibilityTimeoutSeconds.toString()
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
728
|
+
if (effectiveDeploymentId) {
|
|
729
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
730
|
+
}
|
|
731
|
+
const response = await this.fetch(
|
|
732
|
+
this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
|
|
733
|
+
{
|
|
734
|
+
method: "POST",
|
|
735
|
+
headers
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
const errorText = await response.text();
|
|
740
|
+
if (response.status === 404) {
|
|
741
|
+
throw new MessageNotFoundError(messageId);
|
|
742
|
+
}
|
|
743
|
+
if (response.status === 409) {
|
|
744
|
+
let errorData = {};
|
|
745
|
+
try {
|
|
746
|
+
errorData = JSON.parse(errorText);
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
if (errorData.originalMessageId) {
|
|
750
|
+
throw new MessageNotAvailableError(
|
|
751
|
+
messageId,
|
|
752
|
+
`This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
throw new MessageNotAvailableError(messageId);
|
|
756
|
+
}
|
|
757
|
+
if (response.status === 410) {
|
|
758
|
+
throw new MessageAlreadyProcessedError(messageId);
|
|
759
|
+
}
|
|
760
|
+
throwCommonHttpError(
|
|
761
|
+
response.status,
|
|
762
|
+
response.statusText,
|
|
763
|
+
errorText,
|
|
764
|
+
"receive message by ID"
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
768
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
769
|
+
if (!parsedHeaders) {
|
|
770
|
+
await consumeStream(multipartMessage.payload);
|
|
771
|
+
throw new MessageCorruptedError(
|
|
772
|
+
messageId,
|
|
773
|
+
"Missing required queue headers in response"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
const deserializedPayload = await transport.deserialize(
|
|
777
|
+
multipartMessage.payload
|
|
778
|
+
);
|
|
779
|
+
const message = {
|
|
780
|
+
...parsedHeaders,
|
|
781
|
+
payload: deserializedPayload
|
|
782
|
+
};
|
|
783
|
+
return { message };
|
|
784
|
+
}
|
|
785
|
+
throw new MessageNotFoundError(messageId);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Delete (acknowledge) a message after successful processing.
|
|
789
|
+
*
|
|
790
|
+
* @param options - Delete options
|
|
791
|
+
* @param options.queueName - Topic name
|
|
792
|
+
* @param options.consumerGroup - Consumer group name
|
|
793
|
+
* @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
|
|
794
|
+
* @returns Promise indicating deletion success
|
|
795
|
+
* @throws {MessageNotFoundError} When receipt handle not found
|
|
796
|
+
* @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
|
|
797
|
+
* @throws {BadRequestError} When parameters are invalid
|
|
798
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
799
|
+
* @throws {ForbiddenError} When access is denied
|
|
800
|
+
* @throws {InternalServerError} When server encounters an error
|
|
801
|
+
*/
|
|
802
|
+
async deleteMessage(options) {
|
|
803
|
+
const { queueName, consumerGroup, receiptHandle } = options;
|
|
804
|
+
const headers = new Headers({
|
|
805
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
806
|
+
...this.customHeaders
|
|
807
|
+
});
|
|
808
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
809
|
+
if (effectiveDeploymentId) {
|
|
810
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
811
|
+
}
|
|
812
|
+
const response = await this.fetch(
|
|
813
|
+
this.buildUrl(
|
|
814
|
+
queueName,
|
|
815
|
+
"consumer",
|
|
816
|
+
consumerGroup,
|
|
817
|
+
"lease",
|
|
818
|
+
receiptHandle
|
|
819
|
+
),
|
|
820
|
+
{
|
|
821
|
+
method: "DELETE",
|
|
822
|
+
headers
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
if (!response.ok) {
|
|
826
|
+
const errorText = await response.text();
|
|
827
|
+
if (response.status === 404) {
|
|
828
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
829
|
+
}
|
|
830
|
+
if (response.status === 409) {
|
|
831
|
+
throw new MessageNotAvailableError(
|
|
832
|
+
receiptHandle,
|
|
833
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
throwCommonHttpError(
|
|
837
|
+
response.status,
|
|
838
|
+
response.statusText,
|
|
839
|
+
errorText,
|
|
840
|
+
"delete message",
|
|
841
|
+
"Missing or invalid receipt handle"
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
return { deleted: true };
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Extend or change the visibility timeout of a message.
|
|
848
|
+
* Used to prevent message redelivery while still processing.
|
|
849
|
+
*
|
|
850
|
+
* @param options - Visibility options
|
|
851
|
+
* @param options.queueName - Topic name
|
|
852
|
+
* @param options.consumerGroup - Consumer group name
|
|
853
|
+
* @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
|
|
854
|
+
* @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
|
|
855
|
+
* @returns Promise indicating success
|
|
856
|
+
* @throws {MessageNotFoundError} When receipt handle not found
|
|
857
|
+
* @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
|
|
858
|
+
* @throws {BadRequestError} When parameters are invalid
|
|
859
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
860
|
+
* @throws {ForbiddenError} When access is denied
|
|
861
|
+
* @throws {InternalServerError} When server encounters an error
|
|
862
|
+
*/
|
|
863
|
+
async changeVisibility(options) {
|
|
864
|
+
const {
|
|
865
|
+
queueName,
|
|
866
|
+
consumerGroup,
|
|
867
|
+
receiptHandle,
|
|
868
|
+
visibilityTimeoutSeconds
|
|
869
|
+
} = options;
|
|
870
|
+
const headers = new Headers({
|
|
871
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
872
|
+
"Content-Type": "application/json",
|
|
873
|
+
...this.customHeaders
|
|
874
|
+
});
|
|
875
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
876
|
+
if (effectiveDeploymentId) {
|
|
877
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
878
|
+
}
|
|
879
|
+
const response = await this.fetch(
|
|
880
|
+
this.buildUrl(
|
|
881
|
+
queueName,
|
|
882
|
+
"consumer",
|
|
883
|
+
consumerGroup,
|
|
884
|
+
"lease",
|
|
885
|
+
receiptHandle
|
|
886
|
+
),
|
|
887
|
+
{
|
|
888
|
+
method: "PATCH",
|
|
889
|
+
headers,
|
|
890
|
+
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
891
|
+
}
|
|
892
|
+
);
|
|
893
|
+
if (!response.ok) {
|
|
894
|
+
const errorText = await response.text();
|
|
895
|
+
if (response.status === 404) {
|
|
896
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
897
|
+
}
|
|
898
|
+
if (response.status === 409) {
|
|
899
|
+
throw new MessageNotAvailableError(
|
|
900
|
+
receiptHandle,
|
|
901
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
throwCommonHttpError(
|
|
905
|
+
response.status,
|
|
906
|
+
response.statusText,
|
|
907
|
+
errorText,
|
|
908
|
+
"change visibility",
|
|
909
|
+
"Missing receipt handle or invalid visibility timeout"
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
return { success: true };
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Alternative endpoint for changing message visibility timeout.
|
|
916
|
+
* Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
|
|
917
|
+
* Functionally equivalent to changeVisibility but follows an alternative API pattern.
|
|
918
|
+
*
|
|
919
|
+
* @param options - Options for changing visibility
|
|
920
|
+
* @returns Promise resolving to change visibility response
|
|
921
|
+
*/
|
|
922
|
+
async changeVisibilityAlt(options) {
|
|
923
|
+
const {
|
|
924
|
+
queueName,
|
|
925
|
+
consumerGroup,
|
|
926
|
+
receiptHandle,
|
|
927
|
+
visibilityTimeoutSeconds
|
|
928
|
+
} = options;
|
|
929
|
+
const headers = new Headers({
|
|
930
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
931
|
+
"Content-Type": "application/json",
|
|
932
|
+
...this.customHeaders
|
|
933
|
+
});
|
|
934
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
935
|
+
if (effectiveDeploymentId) {
|
|
936
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
937
|
+
}
|
|
938
|
+
const response = await this.fetch(
|
|
939
|
+
this.buildUrl(
|
|
940
|
+
queueName,
|
|
941
|
+
"consumer",
|
|
942
|
+
consumerGroup,
|
|
943
|
+
"lease",
|
|
944
|
+
receiptHandle,
|
|
945
|
+
"visibility"
|
|
946
|
+
),
|
|
947
|
+
{
|
|
948
|
+
method: "PATCH",
|
|
949
|
+
headers,
|
|
950
|
+
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
951
|
+
}
|
|
952
|
+
);
|
|
953
|
+
if (!response.ok) {
|
|
954
|
+
const errorText = await response.text();
|
|
955
|
+
if (response.status === 404) {
|
|
956
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
957
|
+
}
|
|
958
|
+
if (response.status === 409) {
|
|
959
|
+
throw new MessageNotAvailableError(
|
|
960
|
+
receiptHandle,
|
|
961
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
throwCommonHttpError(
|
|
965
|
+
response.status,
|
|
966
|
+
response.statusText,
|
|
967
|
+
errorText,
|
|
968
|
+
"change visibility (alt)",
|
|
969
|
+
"Missing receipt handle or invalid visibility timeout"
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return { success: true };
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
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
|
+
}
|
|
988
|
+
var ConsumerGroup = class {
|
|
989
|
+
client;
|
|
990
|
+
topicName;
|
|
991
|
+
consumerGroupName;
|
|
992
|
+
visibilityTimeout;
|
|
993
|
+
/**
|
|
994
|
+
* Create a new ConsumerGroup instance.
|
|
995
|
+
*
|
|
996
|
+
* @param client - QueueClient instance to use for API calls (transport is configured on the client)
|
|
997
|
+
* @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
|
|
998
|
+
* @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
|
|
999
|
+
* @param options - Optional configuration
|
|
1000
|
+
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
|
|
1001
|
+
*/
|
|
1002
|
+
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
1003
|
+
this.client = client;
|
|
1004
|
+
this.topicName = topicName;
|
|
1005
|
+
this.consumerGroupName = consumerGroupName;
|
|
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;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
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.
|
|
1044
|
+
*/
|
|
1045
|
+
startVisibilityExtension(receiptHandle, options) {
|
|
1046
|
+
let isRunning = true;
|
|
1047
|
+
let isResolved = false;
|
|
1048
|
+
let resolveLifecycle;
|
|
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
|
+
}
|
|
1061
|
+
const lifecyclePromise = new Promise((resolve) => {
|
|
1062
|
+
resolveLifecycle = resolve;
|
|
1063
|
+
});
|
|
1064
|
+
const safeResolve = () => {
|
|
1065
|
+
if (!isResolved) {
|
|
1066
|
+
isResolved = true;
|
|
1067
|
+
resolveLifecycle();
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
const extend = async () => {
|
|
1071
|
+
if (!isRunning) {
|
|
1072
|
+
safeResolve();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
await this.client.changeVisibility({
|
|
1077
|
+
queueName: this.topicName,
|
|
1078
|
+
consumerGroup: this.consumerGroupName,
|
|
1079
|
+
receiptHandle,
|
|
1080
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1081
|
+
});
|
|
1082
|
+
if (isRunning) {
|
|
1083
|
+
timeoutId = setTimeout(() => extend(), renewalIntervalMs);
|
|
1084
|
+
} else {
|
|
1085
|
+
safeResolve();
|
|
1086
|
+
}
|
|
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
|
+
}
|
|
1096
|
+
console.error(
|
|
1097
|
+
`Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
|
|
1098
|
+
error
|
|
1099
|
+
);
|
|
1100
|
+
if (isRunning) {
|
|
1101
|
+
timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
|
|
1102
|
+
} else {
|
|
1103
|
+
safeResolve();
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
timeoutId = setTimeout(() => extend(), firstDelayMs);
|
|
1108
|
+
return async (waitForCompletion = false) => {
|
|
1109
|
+
isRunning = false;
|
|
1110
|
+
if (timeoutId) {
|
|
1111
|
+
clearTimeout(timeoutId);
|
|
1112
|
+
timeoutId = null;
|
|
1113
|
+
}
|
|
1114
|
+
if (waitForCompletion) {
|
|
1115
|
+
await lifecyclePromise;
|
|
1116
|
+
} else {
|
|
1117
|
+
safeResolve();
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
async processMessage(message, handler, options) {
|
|
1122
|
+
const stopExtension = this.startVisibilityExtension(
|
|
1123
|
+
message.receiptHandle,
|
|
1124
|
+
options
|
|
1125
|
+
);
|
|
1126
|
+
try {
|
|
1127
|
+
await handler(message.payload, {
|
|
1128
|
+
messageId: message.messageId,
|
|
1129
|
+
deliveryCount: message.deliveryCount,
|
|
1130
|
+
createdAt: message.createdAt,
|
|
1131
|
+
topicName: this.topicName,
|
|
1132
|
+
consumerGroup: this.consumerGroupName
|
|
1133
|
+
});
|
|
1134
|
+
await stopExtension();
|
|
1135
|
+
await this.client.deleteMessage({
|
|
1136
|
+
queueName: this.topicName,
|
|
1137
|
+
consumerGroup: this.consumerGroupName,
|
|
1138
|
+
receiptHandle: message.receiptHandle
|
|
1139
|
+
});
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
await stopExtension();
|
|
1142
|
+
const transport = this.client.getTransport();
|
|
1143
|
+
if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
1144
|
+
try {
|
|
1145
|
+
await transport.finalize(message.payload);
|
|
1146
|
+
} catch (finalizeError) {
|
|
1147
|
+
console.warn("Failed to finalize message payload:", finalizeError);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
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
|
+
}
|
|
1173
|
+
async consume(handler, options) {
|
|
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
|
+
});
|
|
1181
|
+
await this.processMessage(response.message, handler);
|
|
1182
|
+
} else {
|
|
1183
|
+
const limit = options && "limit" in options ? options.limit : 1;
|
|
1184
|
+
let messageFound = false;
|
|
1185
|
+
for await (const message of this.client.receiveMessages({
|
|
1186
|
+
queueName: this.topicName,
|
|
1187
|
+
consumerGroup: this.consumerGroupName,
|
|
1188
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1189
|
+
limit
|
|
1190
|
+
})) {
|
|
1191
|
+
messageFound = true;
|
|
1192
|
+
await this.processMessage(message, handler);
|
|
1193
|
+
}
|
|
1194
|
+
if (!messageFound) {
|
|
1195
|
+
await handler(null, null);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Get the consumer group name
|
|
1201
|
+
*/
|
|
1202
|
+
get name() {
|
|
1203
|
+
return this.consumerGroupName;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Get the topic name this consumer group is subscribed to
|
|
1207
|
+
*/
|
|
1208
|
+
get topic() {
|
|
1209
|
+
return this.topicName;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
// src/topic.ts
|
|
1214
|
+
var Topic = class {
|
|
1215
|
+
client;
|
|
1216
|
+
topicName;
|
|
1217
|
+
/**
|
|
1218
|
+
* Create a new Topic instance
|
|
1219
|
+
* @param client QueueClient instance to use for API calls (transport is configured on the client)
|
|
1220
|
+
* @param topicName Name of the topic to work with
|
|
1221
|
+
*/
|
|
1222
|
+
constructor(client, topicName) {
|
|
1223
|
+
this.client = client;
|
|
1224
|
+
this.topicName = topicName;
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Publish a message to the topic
|
|
1228
|
+
* @param payload The data to publish
|
|
1229
|
+
* @param options Optional publish options
|
|
1230
|
+
* @returns An object containing the message ID
|
|
1231
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1232
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1233
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
1234
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1235
|
+
*/
|
|
1236
|
+
async publish(payload, options) {
|
|
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
|
+
});
|
|
1245
|
+
if (isDevMode()) {
|
|
1246
|
+
triggerDevCallbacks(this.topicName, result.messageId);
|
|
1247
|
+
}
|
|
1248
|
+
return { messageId: result.messageId };
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Create a consumer group for this topic
|
|
1252
|
+
* @param consumerGroupName Name of the consumer group
|
|
1253
|
+
* @param options Optional configuration for the consumer group
|
|
1254
|
+
* @returns A ConsumerGroup instance
|
|
1255
|
+
*/
|
|
1256
|
+
consumerGroup(consumerGroupName, options) {
|
|
1257
|
+
return new ConsumerGroup(
|
|
1258
|
+
this.client,
|
|
1259
|
+
this.topicName,
|
|
1260
|
+
consumerGroupName,
|
|
1261
|
+
options
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Get the topic name
|
|
1266
|
+
*/
|
|
1267
|
+
get name() {
|
|
1268
|
+
return this.topicName;
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
// src/callback.ts
|
|
1273
|
+
var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
|
|
1274
|
+
var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
|
|
1275
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
1276
|
+
const prefix = pattern.slice(0, -1);
|
|
1277
|
+
return topicName.startsWith(prefix);
|
|
1278
|
+
}
|
|
1279
|
+
function isRecord(value) {
|
|
1280
|
+
return typeof value === "object" && value !== null;
|
|
1281
|
+
}
|
|
1282
|
+
function parseV1StructuredBody(body, contentType) {
|
|
1283
|
+
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
"Invalid content type: expected 'application/cloudevents+json'"
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
|
|
1289
|
+
throw new Error("Invalid CloudEvent: missing required fields");
|
|
1290
|
+
}
|
|
1291
|
+
if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
const { data } = body;
|
|
1297
|
+
const missingFields = [];
|
|
1298
|
+
if (!("queueName" in data)) missingFields.push("queueName");
|
|
1299
|
+
if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
|
|
1300
|
+
if (!("messageId" in data)) missingFields.push("messageId");
|
|
1301
|
+
if (missingFields.length > 0) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
return {
|
|
1307
|
+
queueName: String(data.queueName),
|
|
1308
|
+
consumerGroup: String(data.consumerGroup),
|
|
1309
|
+
messageId: String(data.messageId)
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
function getHeader(headers, name) {
|
|
1313
|
+
if (headers instanceof Headers) {
|
|
1314
|
+
return headers.get(name);
|
|
1315
|
+
}
|
|
1316
|
+
const value = headers[name];
|
|
1317
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
1318
|
+
return value ?? null;
|
|
1319
|
+
}
|
|
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
|
+
);
|
|
1326
|
+
}
|
|
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;
|
|
1360
|
+
}
|
|
1361
|
+
const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
|
|
1362
|
+
if (visibilityDeadline) {
|
|
1363
|
+
result.visibilityDeadline = visibilityDeadline;
|
|
1364
|
+
}
|
|
1365
|
+
return result;
|
|
1366
|
+
}
|
|
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;
|
|
1373
|
+
}
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1376
|
+
return parseV1StructuredBody(body, getHeader(headers, "content-type"));
|
|
1377
|
+
}
|
|
1378
|
+
async function parseCallback(request) {
|
|
1379
|
+
const ceType = request.headers.get("ce-type");
|
|
1380
|
+
if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
|
|
1381
|
+
const result = parseBinaryHeaders(request.headers);
|
|
1382
|
+
if ("receiptHandle" in result && request.body) {
|
|
1383
|
+
result.rawBody = request.body;
|
|
1384
|
+
}
|
|
1385
|
+
return result;
|
|
1386
|
+
}
|
|
1387
|
+
let body;
|
|
1388
|
+
try {
|
|
1389
|
+
body = await request.json();
|
|
1390
|
+
} catch {
|
|
1391
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
1392
|
+
}
|
|
1393
|
+
const headers = {};
|
|
1394
|
+
request.headers.forEach((value, key) => {
|
|
1395
|
+
headers[key] = value;
|
|
1396
|
+
});
|
|
1397
|
+
return parseRawCallback(body, headers);
|
|
1398
|
+
}
|
|
1399
|
+
async function handleCallback(handler, request, options) {
|
|
1400
|
+
const { queueName, consumerGroup, messageId } = request;
|
|
1401
|
+
const client = options?.client || new QueueClient();
|
|
1402
|
+
const topic = new Topic(client, queueName);
|
|
1403
|
+
const cg = topic.consumerGroup(
|
|
1404
|
+
consumerGroup,
|
|
1405
|
+
options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
|
|
1406
|
+
);
|
|
1407
|
+
if ("receiptHandle" in request) {
|
|
1408
|
+
const transport = client.getTransport();
|
|
1409
|
+
let payload;
|
|
1410
|
+
if (request.rawBody) {
|
|
1411
|
+
payload = await transport.deserialize(request.rawBody);
|
|
1412
|
+
} else if (request.parsedPayload !== void 0) {
|
|
1413
|
+
payload = request.parsedPayload;
|
|
1414
|
+
} else {
|
|
1415
|
+
throw new Error(
|
|
1416
|
+
"Binary mode callback with receipt handle is missing payload"
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
const message = {
|
|
1420
|
+
messageId,
|
|
1421
|
+
payload,
|
|
1422
|
+
deliveryCount: request.deliveryCount ?? 1,
|
|
1423
|
+
createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
|
|
1424
|
+
contentType: request.contentType ?? transport.contentType,
|
|
1425
|
+
receiptHandle: request.receiptHandle
|
|
1426
|
+
};
|
|
1427
|
+
const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
|
|
1428
|
+
await cg.consumeMessage(handler, message, { visibilityDeadline });
|
|
1429
|
+
} else {
|
|
1430
|
+
await cg.consume(handler, { messageId });
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/web.ts
|
|
1435
|
+
function handleCallback2(handler, options) {
|
|
1436
|
+
return async (request) => {
|
|
1437
|
+
try {
|
|
1438
|
+
const parsed = await parseCallback(request);
|
|
1439
|
+
await handleCallback(handler, parsed, options);
|
|
1440
|
+
return Response.json({ status: "success" });
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
console.error("Queue callback error:", error);
|
|
1443
|
+
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"))) {
|
|
1444
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
1445
|
+
}
|
|
1446
|
+
return Response.json(
|
|
1447
|
+
{ error: "Failed to process queue message" },
|
|
1448
|
+
{ status: 500 }
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1454
|
+
0 && (module.exports = {
|
|
1455
|
+
handleCallback
|
|
1456
|
+
});
|
|
1457
|
+
//# sourceMappingURL=web.js.map
|