@vercel/queue 0.0.0-alpha.32 → 0.0.0-alpha.34
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 +3 -52
- package/dist/index.d.mts +184 -75
- package/dist/index.d.ts +184 -75
- package/dist/index.js +818 -644
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +802 -644
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs-pages.d.mts +1 -1
- package/dist/nextjs-pages.d.ts +1 -1
- package/dist/nextjs-pages.js +667 -555
- package/dist/nextjs-pages.js.map +1 -1
- package/dist/nextjs-pages.mjs +657 -555
- package/dist/nextjs-pages.mjs.map +1 -1
- package/dist/{types-JvOenjfT.d.mts → types-BHtRP_i_.d.mts} +173 -18
- package/dist/{types-JvOenjfT.d.ts → types-BHtRP_i_.d.ts} +173 -18
- package/package.json +2 -6
- package/bin/local-discover.js +0 -196
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -22,10 +32,16 @@ var index_exports = {};
|
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
BadRequestError: () => BadRequestError,
|
|
24
34
|
BufferTransport: () => BufferTransport,
|
|
35
|
+
Client: () => Client,
|
|
36
|
+
ConcurrencyLimitError: () => ConcurrencyLimitError,
|
|
37
|
+
ConsumerDiscoveryError: () => ConsumerDiscoveryError,
|
|
38
|
+
ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
|
|
39
|
+
DuplicateMessageError: () => DuplicateMessageError,
|
|
25
40
|
ForbiddenError: () => ForbiddenError,
|
|
26
41
|
InternalServerError: () => InternalServerError,
|
|
27
42
|
InvalidLimitError: () => InvalidLimitError,
|
|
28
43
|
JsonTransport: () => JsonTransport,
|
|
44
|
+
MessageAlreadyProcessedError: () => MessageAlreadyProcessedError,
|
|
29
45
|
MessageCorruptedError: () => MessageCorruptedError,
|
|
30
46
|
MessageLockedError: () => MessageLockedError,
|
|
31
47
|
MessageNotAvailableError: () => MessageNotAvailableError,
|
|
@@ -106,8 +122,9 @@ var StreamTransport = class {
|
|
|
106
122
|
// src/client.ts
|
|
107
123
|
var import_mixpart = require("mixpart");
|
|
108
124
|
|
|
109
|
-
// src/
|
|
110
|
-
var
|
|
125
|
+
// src/dev.ts
|
|
126
|
+
var fs = __toESM(require("fs"));
|
|
127
|
+
var path = __toESM(require("path"));
|
|
111
128
|
|
|
112
129
|
// src/types.ts
|
|
113
130
|
var MessageNotFoundError = class extends Error {
|
|
@@ -177,8 +194,268 @@ var InvalidLimitError = class extends Error {
|
|
|
177
194
|
this.name = "InvalidLimitError";
|
|
178
195
|
}
|
|
179
196
|
};
|
|
197
|
+
var MessageAlreadyProcessedError = class extends Error {
|
|
198
|
+
constructor(messageId) {
|
|
199
|
+
super(`Message ${messageId} has already been processed`);
|
|
200
|
+
this.name = "MessageAlreadyProcessedError";
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
var ConcurrencyLimitError = class extends Error {
|
|
204
|
+
currentInflight;
|
|
205
|
+
maxConcurrency;
|
|
206
|
+
constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
|
|
207
|
+
super(message);
|
|
208
|
+
this.name = "ConcurrencyLimitError";
|
|
209
|
+
this.currentInflight = currentInflight;
|
|
210
|
+
this.maxConcurrency = maxConcurrency;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var DuplicateMessageError = class extends Error {
|
|
214
|
+
idempotencyKey;
|
|
215
|
+
constructor(message, idempotencyKey) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.name = "DuplicateMessageError";
|
|
218
|
+
this.idempotencyKey = idempotencyKey;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var ConsumerDiscoveryError = class extends Error {
|
|
222
|
+
deploymentId;
|
|
223
|
+
constructor(message, deploymentId) {
|
|
224
|
+
super(message);
|
|
225
|
+
this.name = "ConsumerDiscoveryError";
|
|
226
|
+
this.deploymentId = deploymentId;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var ConsumerRegistryNotConfiguredError = class extends Error {
|
|
230
|
+
constructor(message = "Consumer registry not configured") {
|
|
231
|
+
super(message);
|
|
232
|
+
this.name = "ConsumerRegistryNotConfiguredError";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/dev.ts
|
|
237
|
+
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
238
|
+
function filePathToUrlPath(filePath) {
|
|
239
|
+
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
240
|
+
if (!urlPath.startsWith("/")) {
|
|
241
|
+
urlPath = "/" + urlPath;
|
|
242
|
+
}
|
|
243
|
+
return urlPath;
|
|
244
|
+
}
|
|
245
|
+
function getDevRouteMappings() {
|
|
246
|
+
const g = globalThis;
|
|
247
|
+
if (ROUTE_MAPPINGS_KEY in g) {
|
|
248
|
+
return g[ROUTE_MAPPINGS_KEY] ?? null;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const vercelJsonPath = path.join(process.cwd(), "vercel.json");
|
|
252
|
+
if (!fs.existsSync(vercelJsonPath)) {
|
|
253
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
|
|
257
|
+
if (!vercelJson.functions) {
|
|
258
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const mappings = [];
|
|
262
|
+
for (const [filePath, config] of Object.entries(vercelJson.functions)) {
|
|
263
|
+
if (!config.experimentalTriggers) continue;
|
|
264
|
+
for (const trigger of config.experimentalTriggers) {
|
|
265
|
+
if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
|
|
266
|
+
mappings.push({
|
|
267
|
+
urlPath: filePathToUrlPath(filePath),
|
|
268
|
+
topic: trigger.topic,
|
|
269
|
+
consumer: trigger.consumer
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
|
|
275
|
+
return g[ROUTE_MAPPINGS_KEY];
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.warn("[Dev Mode] Failed to read vercel.json:", error);
|
|
278
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function findMatchingRoutes(topicName) {
|
|
283
|
+
const mappings = getDevRouteMappings();
|
|
284
|
+
if (!mappings) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
return mappings.filter((mapping) => {
|
|
288
|
+
if (mapping.topic.includes("*")) {
|
|
289
|
+
return matchesWildcardPattern(topicName, mapping.topic);
|
|
290
|
+
}
|
|
291
|
+
return mapping.topic === topicName;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function isDevMode() {
|
|
295
|
+
return process.env.NODE_ENV === "development";
|
|
296
|
+
}
|
|
297
|
+
var DEV_VISIBILITY_POLL_INTERVAL = 50;
|
|
298
|
+
var DEV_VISIBILITY_MAX_WAIT = 5e3;
|
|
299
|
+
var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
|
|
300
|
+
async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
|
|
301
|
+
const client = new QueueClient();
|
|
302
|
+
const transport = new JsonTransport();
|
|
303
|
+
let elapsed = 0;
|
|
304
|
+
let interval = DEV_VISIBILITY_POLL_INTERVAL;
|
|
305
|
+
while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
|
|
306
|
+
try {
|
|
307
|
+
await client.receiveMessageById(
|
|
308
|
+
{
|
|
309
|
+
queueName: topicName,
|
|
310
|
+
consumerGroup,
|
|
311
|
+
messageId,
|
|
312
|
+
visibilityTimeoutSeconds: 0
|
|
313
|
+
},
|
|
314
|
+
transport
|
|
315
|
+
);
|
|
316
|
+
return true;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error instanceof MessageNotFoundError) {
|
|
319
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
320
|
+
elapsed += interval;
|
|
321
|
+
interval = Math.min(
|
|
322
|
+
interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
|
|
323
|
+
DEV_VISIBILITY_MAX_WAIT - elapsed
|
|
324
|
+
);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (error instanceof MessageAlreadyProcessedError) {
|
|
328
|
+
console.log(
|
|
329
|
+
`[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
|
|
330
|
+
);
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
console.error(
|
|
334
|
+
`[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
|
|
335
|
+
error
|
|
336
|
+
);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
console.warn(
|
|
341
|
+
`[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
|
|
342
|
+
);
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
function triggerDevCallbacks(topicName, messageId, delaySeconds) {
|
|
346
|
+
if (delaySeconds && delaySeconds > 0) {
|
|
347
|
+
console.log(
|
|
348
|
+
`[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
|
|
349
|
+
);
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
triggerDevCallbacks(topicName, messageId);
|
|
352
|
+
}, delaySeconds * 1e3);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
console.log(
|
|
356
|
+
`[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
|
|
357
|
+
);
|
|
358
|
+
const matchingRoutes = findMatchingRoutes(topicName);
|
|
359
|
+
if (matchingRoutes.length === 0) {
|
|
360
|
+
console.log(
|
|
361
|
+
`[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
|
|
362
|
+
);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const consumerGroups = matchingRoutes.map((r) => r.consumer);
|
|
366
|
+
console.log(
|
|
367
|
+
`[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
|
|
368
|
+
);
|
|
369
|
+
(async () => {
|
|
370
|
+
const firstRoute = matchingRoutes[0];
|
|
371
|
+
const isVisible = await waitForMessageVisibility(
|
|
372
|
+
topicName,
|
|
373
|
+
firstRoute.consumer,
|
|
374
|
+
messageId
|
|
375
|
+
);
|
|
376
|
+
if (!isVisible) {
|
|
377
|
+
console.warn(
|
|
378
|
+
`[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
|
|
379
|
+
);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const port = process.env.PORT || 3e3;
|
|
383
|
+
const baseUrl = `http://localhost:${port}`;
|
|
384
|
+
for (const route of matchingRoutes) {
|
|
385
|
+
const url = `${baseUrl}${route.urlPath}`;
|
|
386
|
+
console.log(
|
|
387
|
+
`[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
|
|
388
|
+
);
|
|
389
|
+
const cloudEvent = {
|
|
390
|
+
type: "com.vercel.queue.v1beta",
|
|
391
|
+
source: `/topic/${topicName}/consumer/${route.consumer}`,
|
|
392
|
+
id: messageId,
|
|
393
|
+
datacontenttype: "application/json",
|
|
394
|
+
data: {
|
|
395
|
+
messageId,
|
|
396
|
+
queueName: topicName,
|
|
397
|
+
consumerGroup: route.consumer
|
|
398
|
+
},
|
|
399
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
400
|
+
specversion: "1.0"
|
|
401
|
+
};
|
|
402
|
+
try {
|
|
403
|
+
const response = await fetch(url, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: {
|
|
406
|
+
"Content-Type": "application/cloudevents+json"
|
|
407
|
+
},
|
|
408
|
+
body: JSON.stringify(cloudEvent)
|
|
409
|
+
});
|
|
410
|
+
if (response.ok) {
|
|
411
|
+
try {
|
|
412
|
+
const responseData = await response.json();
|
|
413
|
+
if (responseData.status === "success") {
|
|
414
|
+
console.log(
|
|
415
|
+
`[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
console.warn(
|
|
420
|
+
`[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
try {
|
|
425
|
+
const errorData = await response.json();
|
|
426
|
+
console.error(
|
|
427
|
+
`[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
|
|
428
|
+
);
|
|
429
|
+
} catch {
|
|
430
|
+
console.error(
|
|
431
|
+
`[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error(
|
|
437
|
+
`[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
|
|
438
|
+
error
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
})();
|
|
443
|
+
}
|
|
444
|
+
function clearDevRouteMappings() {
|
|
445
|
+
const g = globalThis;
|
|
446
|
+
delete g[ROUTE_MAPPINGS_KEY];
|
|
447
|
+
}
|
|
448
|
+
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
449
|
+
globalThis.__clearDevRouteMappings = clearDevRouteMappings;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/oidc.ts
|
|
453
|
+
var import_oidc = require("@vercel/oidc");
|
|
180
454
|
|
|
181
455
|
// src/client.ts
|
|
456
|
+
function isDebugEnabled() {
|
|
457
|
+
return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
|
|
458
|
+
}
|
|
182
459
|
async function consumeStream(stream) {
|
|
183
460
|
const reader = stream.getReader();
|
|
184
461
|
try {
|
|
@@ -190,17 +467,34 @@ async function consumeStream(stream) {
|
|
|
190
467
|
reader.releaseLock();
|
|
191
468
|
}
|
|
192
469
|
}
|
|
470
|
+
function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
|
|
471
|
+
if (status === 400) {
|
|
472
|
+
throw new BadRequestError(errorText || badRequestDefault);
|
|
473
|
+
}
|
|
474
|
+
if (status === 401) {
|
|
475
|
+
throw new UnauthorizedError(errorText || void 0);
|
|
476
|
+
}
|
|
477
|
+
if (status === 403) {
|
|
478
|
+
throw new ForbiddenError(errorText || void 0);
|
|
479
|
+
}
|
|
480
|
+
if (status >= 500) {
|
|
481
|
+
throw new InternalServerError(
|
|
482
|
+
errorText || `Server error: ${status} ${statusText}`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
|
|
486
|
+
}
|
|
193
487
|
function parseQueueHeaders(headers) {
|
|
194
488
|
const messageId = headers.get("Vqs-Message-Id");
|
|
195
489
|
const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
|
|
196
490
|
const timestamp = headers.get("Vqs-Timestamp");
|
|
197
491
|
const contentType = headers.get("Content-Type") || "application/octet-stream";
|
|
198
|
-
const
|
|
199
|
-
if (!messageId || !timestamp || !
|
|
492
|
+
const receiptHandle = headers.get("Vqs-Receipt-Handle");
|
|
493
|
+
if (!messageId || !timestamp || !receiptHandle) {
|
|
200
494
|
return null;
|
|
201
495
|
}
|
|
202
496
|
const deliveryCount = parseInt(deliveryCountStr, 10);
|
|
203
|
-
if (isNaN(deliveryCount)) {
|
|
497
|
+
if (Number.isNaN(deliveryCount)) {
|
|
204
498
|
return null;
|
|
205
499
|
}
|
|
206
500
|
return {
|
|
@@ -208,30 +502,43 @@ function parseQueueHeaders(headers) {
|
|
|
208
502
|
deliveryCount,
|
|
209
503
|
createdAt: new Date(timestamp),
|
|
210
504
|
contentType,
|
|
211
|
-
|
|
505
|
+
receiptHandle
|
|
212
506
|
};
|
|
213
507
|
}
|
|
214
508
|
var QueueClient = class {
|
|
215
509
|
baseUrl;
|
|
216
510
|
basePath;
|
|
217
|
-
customHeaders
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
*/
|
|
511
|
+
customHeaders;
|
|
512
|
+
providedToken;
|
|
513
|
+
defaultDeploymentId;
|
|
514
|
+
pinToDeployment;
|
|
222
515
|
constructor(options = {}) {
|
|
223
516
|
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
224
|
-
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/
|
|
225
|
-
|
|
226
|
-
this.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
517
|
+
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
|
|
518
|
+
this.customHeaders = options.headers || {};
|
|
519
|
+
this.providedToken = options.token;
|
|
520
|
+
this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
521
|
+
this.pinToDeployment = options.pinToDeployment ?? true;
|
|
522
|
+
}
|
|
523
|
+
getSendDeploymentId() {
|
|
524
|
+
if (isDevMode()) {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
if (this.pinToDeployment) {
|
|
528
|
+
return this.defaultDeploymentId;
|
|
529
|
+
}
|
|
530
|
+
return void 0;
|
|
531
|
+
}
|
|
532
|
+
getConsumeDeploymentId() {
|
|
533
|
+
if (isDevMode()) {
|
|
534
|
+
return void 0;
|
|
535
|
+
}
|
|
536
|
+
return this.defaultDeploymentId;
|
|
233
537
|
}
|
|
234
538
|
async getToken() {
|
|
539
|
+
if (this.providedToken) {
|
|
540
|
+
return this.providedToken;
|
|
541
|
+
}
|
|
235
542
|
const token = await (0, import_oidc.getVercelOidcToken)();
|
|
236
543
|
if (!token) {
|
|
237
544
|
throw new Error(
|
|
@@ -240,25 +547,61 @@ var QueueClient = class {
|
|
|
240
547
|
}
|
|
241
548
|
return token;
|
|
242
549
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
550
|
+
buildUrl(queueName, ...pathSegments) {
|
|
551
|
+
const encodedQueue = encodeURIComponent(queueName);
|
|
552
|
+
const segments = pathSegments.map((s) => encodeURIComponent(s));
|
|
553
|
+
const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
|
|
554
|
+
return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
|
|
555
|
+
}
|
|
556
|
+
async fetch(url, init) {
|
|
557
|
+
const method = init.method || "GET";
|
|
558
|
+
if (isDebugEnabled()) {
|
|
559
|
+
const logData = {
|
|
560
|
+
method,
|
|
561
|
+
url,
|
|
562
|
+
headers: init.headers
|
|
563
|
+
};
|
|
564
|
+
const body = init.body;
|
|
565
|
+
if (body !== void 0 && body !== null) {
|
|
566
|
+
if (body instanceof ArrayBuffer) {
|
|
567
|
+
logData.bodySize = body.byteLength;
|
|
568
|
+
} else if (body instanceof Uint8Array) {
|
|
569
|
+
logData.bodySize = body.byteLength;
|
|
570
|
+
} else if (typeof body === "string") {
|
|
571
|
+
logData.bodySize = body.length;
|
|
572
|
+
} else {
|
|
573
|
+
logData.bodyType = typeof body;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
577
|
+
}
|
|
578
|
+
const response = await fetch(url, init);
|
|
579
|
+
if (isDebugEnabled()) {
|
|
580
|
+
const logData = {
|
|
581
|
+
method,
|
|
582
|
+
url,
|
|
583
|
+
status: response.status,
|
|
584
|
+
statusText: response.statusText,
|
|
585
|
+
headers: response.headers
|
|
586
|
+
};
|
|
587
|
+
console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
|
|
588
|
+
}
|
|
589
|
+
return response;
|
|
590
|
+
}
|
|
253
591
|
async sendMessage(options, transport) {
|
|
254
|
-
const {
|
|
592
|
+
const {
|
|
593
|
+
queueName,
|
|
594
|
+
payload,
|
|
595
|
+
idempotencyKey,
|
|
596
|
+
retentionSeconds,
|
|
597
|
+
delaySeconds
|
|
598
|
+
} = options;
|
|
255
599
|
const headers = new Headers({
|
|
256
600
|
Authorization: `Bearer ${await this.getToken()}`,
|
|
257
|
-
"Vqs-Queue-Name": queueName,
|
|
258
601
|
"Content-Type": transport.contentType,
|
|
259
602
|
...this.customHeaders
|
|
260
603
|
});
|
|
261
|
-
const deploymentId =
|
|
604
|
+
const deploymentId = this.getSendDeploymentId();
|
|
262
605
|
if (deploymentId) {
|
|
263
606
|
headers.set("Vqs-Deployment-Id", deploymentId);
|
|
264
607
|
}
|
|
@@ -268,106 +611,106 @@ var QueueClient = class {
|
|
|
268
611
|
if (retentionSeconds !== void 0) {
|
|
269
612
|
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
270
613
|
}
|
|
271
|
-
|
|
272
|
-
|
|
614
|
+
if (delaySeconds !== void 0) {
|
|
615
|
+
headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
|
|
616
|
+
}
|
|
617
|
+
const serialized = transport.serialize(payload);
|
|
618
|
+
const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
|
|
619
|
+
const response = await this.fetch(this.buildUrl(queueName), {
|
|
273
620
|
method: "POST",
|
|
274
621
|
body,
|
|
275
622
|
headers
|
|
276
623
|
});
|
|
277
624
|
if (!response.ok) {
|
|
278
|
-
|
|
279
|
-
const errorText = await response.text();
|
|
280
|
-
throw new BadRequestError(errorText || "Invalid parameters");
|
|
281
|
-
}
|
|
282
|
-
if (response.status === 401) {
|
|
283
|
-
throw new UnauthorizedError();
|
|
284
|
-
}
|
|
285
|
-
if (response.status === 403) {
|
|
286
|
-
throw new ForbiddenError();
|
|
287
|
-
}
|
|
625
|
+
const errorText = await response.text();
|
|
288
626
|
if (response.status === 409) {
|
|
289
|
-
throw new
|
|
627
|
+
throw new DuplicateMessageError(
|
|
628
|
+
errorText || "Duplicate idempotency key detected",
|
|
629
|
+
idempotencyKey
|
|
630
|
+
);
|
|
290
631
|
}
|
|
291
|
-
if (response.status
|
|
292
|
-
throw new
|
|
293
|
-
|
|
632
|
+
if (response.status === 502) {
|
|
633
|
+
throw new ConsumerDiscoveryError(
|
|
634
|
+
errorText || "Consumer discovery failed",
|
|
635
|
+
deploymentId
|
|
294
636
|
);
|
|
295
637
|
}
|
|
296
|
-
|
|
297
|
-
|
|
638
|
+
if (response.status === 503) {
|
|
639
|
+
throw new ConsumerRegistryNotConfiguredError(
|
|
640
|
+
errorText || "Consumer registry not configured"
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
throwCommonHttpError(
|
|
644
|
+
response.status,
|
|
645
|
+
response.statusText,
|
|
646
|
+
errorText,
|
|
647
|
+
"send message"
|
|
298
648
|
);
|
|
299
649
|
}
|
|
300
650
|
const responseData = await response.json();
|
|
301
651
|
return responseData;
|
|
302
652
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Receive messages from a queue
|
|
305
|
-
* @param options Receive messages options
|
|
306
|
-
* @param transport Serializer/deserializer for the payload
|
|
307
|
-
* @returns AsyncGenerator that yields messages as they arrive
|
|
308
|
-
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
309
|
-
* @throws {QueueEmptyError} When no messages are available (204)
|
|
310
|
-
* @throws {MessageLockedError} When messages are temporarily locked (423)
|
|
311
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
312
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
313
|
-
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
314
|
-
* @throws {InternalServerError} When server encounters an error
|
|
315
|
-
*/
|
|
316
653
|
async *receiveMessages(options, transport) {
|
|
317
|
-
const {
|
|
654
|
+
const {
|
|
655
|
+
queueName,
|
|
656
|
+
consumerGroup,
|
|
657
|
+
visibilityTimeoutSeconds,
|
|
658
|
+
limit,
|
|
659
|
+
maxConcurrency
|
|
660
|
+
} = options;
|
|
318
661
|
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
319
662
|
throw new InvalidLimitError(limit);
|
|
320
663
|
}
|
|
321
664
|
const headers = new Headers({
|
|
322
665
|
Authorization: `Bearer ${await this.getToken()}`,
|
|
323
|
-
"Vqs-Queue-Name": queueName,
|
|
324
|
-
"Vqs-Consumer-Group": consumerGroup,
|
|
325
666
|
Accept: "multipart/mixed",
|
|
326
667
|
...this.customHeaders
|
|
327
668
|
});
|
|
328
669
|
if (visibilityTimeoutSeconds !== void 0) {
|
|
329
670
|
headers.set(
|
|
330
|
-
"Vqs-Visibility-Timeout",
|
|
671
|
+
"Vqs-Visibility-Timeout-Seconds",
|
|
331
672
|
visibilityTimeoutSeconds.toString()
|
|
332
673
|
);
|
|
333
674
|
}
|
|
334
675
|
if (limit !== void 0) {
|
|
335
|
-
headers.set("Vqs-
|
|
676
|
+
headers.set("Vqs-Max-Messages", limit.toString());
|
|
336
677
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
678
|
+
if (maxConcurrency !== void 0) {
|
|
679
|
+
headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
|
|
680
|
+
}
|
|
681
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
682
|
+
if (effectiveDeploymentId) {
|
|
683
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
684
|
+
}
|
|
685
|
+
const response = await this.fetch(
|
|
686
|
+
this.buildUrl(queueName, "consumer", consumerGroup),
|
|
687
|
+
{
|
|
688
|
+
method: "POST",
|
|
689
|
+
headers
|
|
690
|
+
}
|
|
691
|
+
);
|
|
341
692
|
if (response.status === 204) {
|
|
342
693
|
throw new QueueEmptyError(queueName, consumerGroup);
|
|
343
694
|
}
|
|
344
695
|
if (!response.ok) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
if (response.status === 403) {
|
|
353
|
-
throw new ForbiddenError();
|
|
354
|
-
}
|
|
355
|
-
if (response.status === 423) {
|
|
356
|
-
const retryAfterHeader = response.headers.get("Retry-After");
|
|
357
|
-
let retryAfter;
|
|
358
|
-
if (retryAfterHeader) {
|
|
359
|
-
const parsed = parseInt(retryAfterHeader, 10);
|
|
360
|
-
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
696
|
+
const errorText = await response.text();
|
|
697
|
+
if (response.status === 429) {
|
|
698
|
+
let errorData = {};
|
|
699
|
+
try {
|
|
700
|
+
errorData = JSON.parse(errorText);
|
|
701
|
+
} catch {
|
|
361
702
|
}
|
|
362
|
-
throw new
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
703
|
+
throw new ConcurrencyLimitError(
|
|
704
|
+
errorData.error || "Concurrency limit exceeded or throttled",
|
|
705
|
+
errorData.currentInflight,
|
|
706
|
+
errorData.maxConcurrency
|
|
367
707
|
);
|
|
368
708
|
}
|
|
369
|
-
|
|
370
|
-
|
|
709
|
+
throwCommonHttpError(
|
|
710
|
+
response.status,
|
|
711
|
+
response.statusText,
|
|
712
|
+
errorText,
|
|
713
|
+
"receive messages"
|
|
371
714
|
);
|
|
372
715
|
}
|
|
373
716
|
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
@@ -398,550 +741,250 @@ var QueueClient = class {
|
|
|
398
741
|
consumerGroup,
|
|
399
742
|
messageId,
|
|
400
743
|
visibilityTimeoutSeconds,
|
|
401
|
-
|
|
744
|
+
maxConcurrency
|
|
402
745
|
} = options;
|
|
403
746
|
const headers = new Headers({
|
|
404
747
|
Authorization: `Bearer ${await this.getToken()}`,
|
|
405
|
-
"Vqs-Queue-Name": queueName,
|
|
406
|
-
"Vqs-Consumer-Group": consumerGroup,
|
|
407
748
|
Accept: "multipart/mixed",
|
|
408
749
|
...this.customHeaders
|
|
409
750
|
});
|
|
410
751
|
if (visibilityTimeoutSeconds !== void 0) {
|
|
411
752
|
headers.set(
|
|
412
|
-
"Vqs-Visibility-Timeout",
|
|
753
|
+
"Vqs-Visibility-Timeout-Seconds",
|
|
413
754
|
visibilityTimeoutSeconds.toString()
|
|
414
755
|
);
|
|
415
756
|
}
|
|
416
|
-
if (
|
|
417
|
-
headers.set("Vqs-
|
|
757
|
+
if (maxConcurrency !== void 0) {
|
|
758
|
+
headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
|
|
418
759
|
}
|
|
419
|
-
const
|
|
420
|
-
|
|
760
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
761
|
+
if (effectiveDeploymentId) {
|
|
762
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
763
|
+
}
|
|
764
|
+
const response = await this.fetch(
|
|
765
|
+
this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
|
|
421
766
|
{
|
|
422
|
-
method: "
|
|
767
|
+
method: "POST",
|
|
423
768
|
headers
|
|
424
769
|
}
|
|
425
770
|
);
|
|
426
771
|
if (!response.ok) {
|
|
427
|
-
|
|
428
|
-
const errorText = await response.text();
|
|
429
|
-
throw new BadRequestError(errorText || "Invalid parameters");
|
|
430
|
-
}
|
|
431
|
-
if (response.status === 401) {
|
|
432
|
-
throw new UnauthorizedError();
|
|
433
|
-
}
|
|
434
|
-
if (response.status === 403) {
|
|
435
|
-
throw new ForbiddenError();
|
|
436
|
-
}
|
|
772
|
+
const errorText = await response.text();
|
|
437
773
|
if (response.status === 404) {
|
|
438
774
|
throw new MessageNotFoundError(messageId);
|
|
439
775
|
}
|
|
440
|
-
if (response.status === 423) {
|
|
441
|
-
const retryAfterHeader = response.headers.get("Retry-After");
|
|
442
|
-
let retryAfter;
|
|
443
|
-
if (retryAfterHeader) {
|
|
444
|
-
const parsed = parseInt(retryAfterHeader, 10);
|
|
445
|
-
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
446
|
-
}
|
|
447
|
-
throw new MessageLockedError(messageId, retryAfter);
|
|
448
|
-
}
|
|
449
776
|
if (response.status === 409) {
|
|
777
|
+
let errorData = {};
|
|
778
|
+
try {
|
|
779
|
+
errorData = JSON.parse(errorText);
|
|
780
|
+
} catch {
|
|
781
|
+
}
|
|
782
|
+
if (errorData.originalMessageId) {
|
|
783
|
+
throw new MessageNotAvailableError(
|
|
784
|
+
messageId,
|
|
785
|
+
`This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
450
788
|
throw new MessageNotAvailableError(messageId);
|
|
451
789
|
}
|
|
452
|
-
if (response.status
|
|
453
|
-
throw new
|
|
454
|
-
|
|
790
|
+
if (response.status === 410) {
|
|
791
|
+
throw new MessageAlreadyProcessedError(messageId);
|
|
792
|
+
}
|
|
793
|
+
if (response.status === 429) {
|
|
794
|
+
let errorData = {};
|
|
795
|
+
try {
|
|
796
|
+
errorData = JSON.parse(errorText);
|
|
797
|
+
} catch {
|
|
798
|
+
}
|
|
799
|
+
throw new ConcurrencyLimitError(
|
|
800
|
+
errorData.error || "Concurrency limit exceeded or throttled",
|
|
801
|
+
errorData.currentInflight,
|
|
802
|
+
errorData.maxConcurrency
|
|
455
803
|
);
|
|
456
804
|
}
|
|
457
|
-
|
|
458
|
-
|
|
805
|
+
throwCommonHttpError(
|
|
806
|
+
response.status,
|
|
807
|
+
response.statusText,
|
|
808
|
+
errorText,
|
|
809
|
+
"receive message by ID"
|
|
459
810
|
);
|
|
460
811
|
}
|
|
461
|
-
|
|
462
|
-
const parsedHeaders = parseQueueHeaders(
|
|
812
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
813
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
463
814
|
if (!parsedHeaders) {
|
|
815
|
+
await consumeStream(multipartMessage.payload);
|
|
464
816
|
throw new MessageCorruptedError(
|
|
465
817
|
messageId,
|
|
466
|
-
"Missing required queue headers in
|
|
818
|
+
"Missing required queue headers in response"
|
|
467
819
|
);
|
|
468
820
|
}
|
|
821
|
+
const deserializedPayload = await transport.deserialize(
|
|
822
|
+
multipartMessage.payload
|
|
823
|
+
);
|
|
469
824
|
const message = {
|
|
470
825
|
...parsedHeaders,
|
|
471
|
-
payload:
|
|
826
|
+
payload: deserializedPayload
|
|
472
827
|
};
|
|
473
828
|
return { message };
|
|
474
829
|
}
|
|
475
|
-
if (!transport) {
|
|
476
|
-
throw new Error("Transport is required when skipPayload is not true");
|
|
477
|
-
}
|
|
478
|
-
try {
|
|
479
|
-
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
480
|
-
try {
|
|
481
|
-
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
482
|
-
if (!parsedHeaders) {
|
|
483
|
-
console.warn("Missing required queue headers in multipart part");
|
|
484
|
-
await consumeStream(multipartMessage.payload);
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
const deserializedPayload = await transport.deserialize(
|
|
488
|
-
multipartMessage.payload
|
|
489
|
-
);
|
|
490
|
-
const message = {
|
|
491
|
-
...parsedHeaders,
|
|
492
|
-
payload: deserializedPayload
|
|
493
|
-
};
|
|
494
|
-
return { message };
|
|
495
|
-
} catch (error) {
|
|
496
|
-
console.warn("Failed to deserialize message by ID:", error);
|
|
497
|
-
await consumeStream(multipartMessage.payload);
|
|
498
|
-
throw new MessageCorruptedError(
|
|
499
|
-
messageId,
|
|
500
|
-
`Failed to deserialize payload: ${error}`
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
} catch (error) {
|
|
505
|
-
if (error instanceof MessageCorruptedError) {
|
|
506
|
-
throw error;
|
|
507
|
-
}
|
|
508
|
-
throw new MessageCorruptedError(
|
|
509
|
-
messageId,
|
|
510
|
-
`Failed to parse multipart response: ${error}`
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
830
|
throw new MessageNotFoundError(messageId);
|
|
514
831
|
}
|
|
515
|
-
/**
|
|
516
|
-
* Delete a message (acknowledge processing)
|
|
517
|
-
* @param options Delete message options
|
|
518
|
-
* @returns Promise with delete status
|
|
519
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
520
|
-
* @throws {MessageNotAvailableError} When message can't be deleted (409)
|
|
521
|
-
* @throws {BadRequestError} When ticket is missing or invalid (400)
|
|
522
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
523
|
-
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
524
|
-
* @throws {InternalServerError} When server encounters an error
|
|
525
|
-
*/
|
|
526
832
|
async deleteMessage(options) {
|
|
527
|
-
const { queueName, consumerGroup,
|
|
528
|
-
const
|
|
529
|
-
|
|
833
|
+
const { queueName, consumerGroup, receiptHandle } = options;
|
|
834
|
+
const headers = new Headers({
|
|
835
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
836
|
+
...this.customHeaders
|
|
837
|
+
});
|
|
838
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
839
|
+
if (effectiveDeploymentId) {
|
|
840
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
841
|
+
}
|
|
842
|
+
const response = await this.fetch(
|
|
843
|
+
this.buildUrl(
|
|
844
|
+
queueName,
|
|
845
|
+
"consumer",
|
|
846
|
+
consumerGroup,
|
|
847
|
+
"lease",
|
|
848
|
+
receiptHandle
|
|
849
|
+
),
|
|
530
850
|
{
|
|
531
851
|
method: "DELETE",
|
|
532
|
-
headers
|
|
533
|
-
Authorization: `Bearer ${await this.getToken()}`,
|
|
534
|
-
"Vqs-Queue-Name": queueName,
|
|
535
|
-
"Vqs-Consumer-Group": consumerGroup,
|
|
536
|
-
"Vqs-Ticket": ticket,
|
|
537
|
-
...this.customHeaders
|
|
538
|
-
})
|
|
852
|
+
headers
|
|
539
853
|
}
|
|
540
854
|
);
|
|
541
855
|
if (!response.ok) {
|
|
542
|
-
|
|
543
|
-
throw new BadRequestError("Missing or invalid ticket");
|
|
544
|
-
}
|
|
545
|
-
if (response.status === 401) {
|
|
546
|
-
throw new UnauthorizedError();
|
|
547
|
-
}
|
|
548
|
-
if (response.status === 403) {
|
|
549
|
-
throw new ForbiddenError();
|
|
550
|
-
}
|
|
856
|
+
const errorText = await response.text();
|
|
551
857
|
if (response.status === 404) {
|
|
552
|
-
throw new MessageNotFoundError(
|
|
858
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
553
859
|
}
|
|
554
860
|
if (response.status === 409) {
|
|
555
861
|
throw new MessageNotAvailableError(
|
|
556
|
-
|
|
557
|
-
"Invalid
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
if (response.status >= 500) {
|
|
561
|
-
throw new InternalServerError(
|
|
562
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
862
|
+
receiptHandle,
|
|
863
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
563
864
|
);
|
|
564
865
|
}
|
|
565
|
-
|
|
566
|
-
|
|
866
|
+
throwCommonHttpError(
|
|
867
|
+
response.status,
|
|
868
|
+
response.statusText,
|
|
869
|
+
errorText,
|
|
870
|
+
"delete message",
|
|
871
|
+
"Missing or invalid receipt handle"
|
|
567
872
|
);
|
|
568
873
|
}
|
|
569
874
|
return { deleted: true };
|
|
570
875
|
}
|
|
571
|
-
/**
|
|
572
|
-
* Change the visibility timeout of a message
|
|
573
|
-
* @param options Change visibility options
|
|
574
|
-
* @returns Promise with update status
|
|
575
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
576
|
-
* @throws {MessageNotAvailableError} When message can't be updated (409)
|
|
577
|
-
* @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
|
|
578
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
579
|
-
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
580
|
-
* @throws {InternalServerError} When server encounters an error
|
|
581
|
-
*/
|
|
582
876
|
async changeVisibility(options) {
|
|
583
877
|
const {
|
|
584
878
|
queueName,
|
|
585
879
|
consumerGroup,
|
|
586
|
-
|
|
587
|
-
ticket,
|
|
880
|
+
receiptHandle,
|
|
588
881
|
visibilityTimeoutSeconds
|
|
589
882
|
} = options;
|
|
590
|
-
const
|
|
591
|
-
|
|
883
|
+
const headers = new Headers({
|
|
884
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
885
|
+
"Content-Type": "application/json",
|
|
886
|
+
...this.customHeaders
|
|
887
|
+
});
|
|
888
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
889
|
+
if (effectiveDeploymentId) {
|
|
890
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
891
|
+
}
|
|
892
|
+
const response = await this.fetch(
|
|
893
|
+
this.buildUrl(
|
|
894
|
+
queueName,
|
|
895
|
+
"consumer",
|
|
896
|
+
consumerGroup,
|
|
897
|
+
"lease",
|
|
898
|
+
receiptHandle
|
|
899
|
+
),
|
|
592
900
|
{
|
|
593
901
|
method: "PATCH",
|
|
594
|
-
headers
|
|
595
|
-
|
|
596
|
-
"Vqs-Queue-Name": queueName,
|
|
597
|
-
"Vqs-Consumer-Group": consumerGroup,
|
|
598
|
-
"Vqs-Ticket": ticket,
|
|
599
|
-
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
|
|
600
|
-
...this.customHeaders
|
|
601
|
-
})
|
|
902
|
+
headers,
|
|
903
|
+
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
602
904
|
}
|
|
603
905
|
);
|
|
604
906
|
if (!response.ok) {
|
|
605
|
-
|
|
606
|
-
throw new BadRequestError(
|
|
607
|
-
"Missing ticket or invalid visibility timeout"
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
if (response.status === 401) {
|
|
611
|
-
throw new UnauthorizedError();
|
|
612
|
-
}
|
|
613
|
-
if (response.status === 403) {
|
|
614
|
-
throw new ForbiddenError();
|
|
615
|
-
}
|
|
907
|
+
const errorText = await response.text();
|
|
616
908
|
if (response.status === 404) {
|
|
617
|
-
throw new MessageNotFoundError(
|
|
909
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
618
910
|
}
|
|
619
911
|
if (response.status === 409) {
|
|
620
912
|
throw new MessageNotAvailableError(
|
|
621
|
-
|
|
622
|
-
"Invalid
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
if (response.status >= 500) {
|
|
626
|
-
throw new InternalServerError(
|
|
627
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
913
|
+
receiptHandle,
|
|
914
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
628
915
|
);
|
|
629
916
|
}
|
|
630
|
-
|
|
631
|
-
|
|
917
|
+
throwCommonHttpError(
|
|
918
|
+
response.status,
|
|
919
|
+
response.statusText,
|
|
920
|
+
errorText,
|
|
921
|
+
"change visibility",
|
|
922
|
+
"Missing receipt handle or invalid visibility timeout"
|
|
632
923
|
);
|
|
633
924
|
}
|
|
634
|
-
return {
|
|
635
|
-
}
|
|
636
|
-
};
|
|
637
|
-
|
|
638
|
-
// src/callback.ts
|
|
639
|
-
function validateWildcardPattern(pattern) {
|
|
640
|
-
const firstIndex = pattern.indexOf("*");
|
|
641
|
-
const lastIndex = pattern.lastIndexOf("*");
|
|
642
|
-
if (firstIndex !== lastIndex) {
|
|
643
|
-
return false;
|
|
644
|
-
}
|
|
645
|
-
if (firstIndex === -1) {
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
if (firstIndex !== pattern.length - 1) {
|
|
649
|
-
return false;
|
|
925
|
+
return { success: true };
|
|
650
926
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
927
|
+
/**
|
|
928
|
+
* Alternative endpoint for changing message visibility timeout.
|
|
929
|
+
* Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
|
|
930
|
+
* Functionally equivalent to changeVisibility but follows an alternative API pattern.
|
|
931
|
+
*
|
|
932
|
+
* @param options - Options for changing visibility
|
|
933
|
+
* @returns Promise resolving to change visibility response
|
|
934
|
+
*/
|
|
935
|
+
async changeVisibilityAlt(options) {
|
|
936
|
+
const {
|
|
937
|
+
queueName,
|
|
938
|
+
consumerGroup,
|
|
939
|
+
receiptHandle,
|
|
940
|
+
visibilityTimeoutSeconds
|
|
941
|
+
} = options;
|
|
942
|
+
const headers = new Headers({
|
|
943
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
944
|
+
"Content-Type": "application/json",
|
|
945
|
+
...this.customHeaders
|
|
946
|
+
});
|
|
947
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
948
|
+
if (effectiveDeploymentId) {
|
|
949
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
665
950
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
} catch (error) {
|
|
680
|
-
throw new Error("Failed to parse CloudEvent from request body");
|
|
681
|
-
}
|
|
682
|
-
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
683
|
-
throw new Error("Invalid CloudEvent: missing required fields");
|
|
684
|
-
}
|
|
685
|
-
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
686
|
-
throw new Error(
|
|
687
|
-
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
const missingFields = [];
|
|
691
|
-
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
692
|
-
if (!("consumerGroup" in cloudEvent.data))
|
|
693
|
-
missingFields.push("consumerGroup");
|
|
694
|
-
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
695
|
-
if (missingFields.length > 0) {
|
|
696
|
-
throw new Error(
|
|
697
|
-
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
698
|
-
);
|
|
699
|
-
}
|
|
700
|
-
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
701
|
-
return {
|
|
702
|
-
queueName,
|
|
703
|
-
consumerGroup,
|
|
704
|
-
messageId
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
function handleCallback(handlers) {
|
|
708
|
-
for (const topicPattern in handlers) {
|
|
709
|
-
if (topicPattern.includes("*")) {
|
|
710
|
-
if (!validateWildcardPattern(topicPattern)) {
|
|
711
|
-
throw new Error(
|
|
712
|
-
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
713
|
-
);
|
|
951
|
+
const response = await this.fetch(
|
|
952
|
+
this.buildUrl(
|
|
953
|
+
queueName,
|
|
954
|
+
"consumer",
|
|
955
|
+
consumerGroup,
|
|
956
|
+
"lease",
|
|
957
|
+
receiptHandle,
|
|
958
|
+
"visibility"
|
|
959
|
+
),
|
|
960
|
+
{
|
|
961
|
+
method: "PATCH",
|
|
962
|
+
headers,
|
|
963
|
+
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
714
964
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const topicHandler = findTopicHandler(queueName, handlers);
|
|
721
|
-
if (!topicHandler) {
|
|
722
|
-
const availableTopics = Object.keys(handlers).join(", ");
|
|
723
|
-
return Response.json(
|
|
724
|
-
{
|
|
725
|
-
error: `No handler found for topic: ${queueName}`,
|
|
726
|
-
availableTopics
|
|
727
|
-
},
|
|
728
|
-
{ status: 404 }
|
|
729
|
-
);
|
|
965
|
+
);
|
|
966
|
+
if (!response.ok) {
|
|
967
|
+
const errorText = await response.text();
|
|
968
|
+
if (response.status === 404) {
|
|
969
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
730
970
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
{
|
|
736
|
-
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
737
|
-
availableGroups
|
|
738
|
-
},
|
|
739
|
-
{ status: 404 }
|
|
971
|
+
if (response.status === 409) {
|
|
972
|
+
throw new MessageNotAvailableError(
|
|
973
|
+
receiptHandle,
|
|
974
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
740
975
|
);
|
|
741
976
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
console.error("Queue callback error:", error);
|
|
749
|
-
if (error instanceof Error && (error.message.includes("Missing required CloudEvent data fields") || error.message.includes("Invalid CloudEvent") || error.message.includes("Invalid CloudEvent type") || error.message.includes("Invalid content type") || error.message.includes("Failed to parse CloudEvent"))) {
|
|
750
|
-
return Response.json({ error: error.message }, { status: 400 });
|
|
751
|
-
}
|
|
752
|
-
return Response.json(
|
|
753
|
-
{ error: "Failed to process queue message" },
|
|
754
|
-
{ status: 500 }
|
|
977
|
+
throwCommonHttpError(
|
|
978
|
+
response.status,
|
|
979
|
+
response.statusText,
|
|
980
|
+
errorText,
|
|
981
|
+
"change visibility (alt)",
|
|
982
|
+
"Missing receipt handle or invalid visibility timeout"
|
|
755
983
|
);
|
|
756
984
|
}
|
|
757
|
-
|
|
758
|
-
if (isDevMode()) {
|
|
759
|
-
registerDevRouteHandler(routeHandler, handlers);
|
|
760
|
-
}
|
|
761
|
-
return routeHandler;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// src/dev.ts
|
|
765
|
-
var GLOBAL_KEY = Symbol.for("@vercel/queue.devHandlers");
|
|
766
|
-
function getDevHandlerState() {
|
|
767
|
-
const g = globalThis;
|
|
768
|
-
if (!g[GLOBAL_KEY]) {
|
|
769
|
-
g[GLOBAL_KEY] = {
|
|
770
|
-
devRouteHandlers: /* @__PURE__ */ new Map(),
|
|
771
|
-
wildcardRouteHandlers: /* @__PURE__ */ new Map()
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
return g[GLOBAL_KEY];
|
|
775
|
-
}
|
|
776
|
-
var { devRouteHandlers, wildcardRouteHandlers } = getDevHandlerState();
|
|
777
|
-
function cleanupDeadRefs(key, refs) {
|
|
778
|
-
const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
|
|
779
|
-
if (aliveRefs.length === 0) {
|
|
780
|
-
wildcardRouteHandlers.delete(key);
|
|
781
|
-
} else if (aliveRefs.length < refs.length) {
|
|
782
|
-
wildcardRouteHandlers.set(key, aliveRefs);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
function isDevMode() {
|
|
786
|
-
return process.env.NODE_ENV === "development";
|
|
787
|
-
}
|
|
788
|
-
function registerDevRouteHandler(routeHandler, handlers) {
|
|
789
|
-
for (const topicName in handlers) {
|
|
790
|
-
for (const consumerGroup in handlers[topicName]) {
|
|
791
|
-
const key = `${topicName}:${consumerGroup}`;
|
|
792
|
-
if (topicName.includes("*")) {
|
|
793
|
-
const existing = wildcardRouteHandlers.get(key) || [];
|
|
794
|
-
cleanupDeadRefs(key, existing);
|
|
795
|
-
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
796
|
-
const weakRef = new WeakRef(routeHandler);
|
|
797
|
-
cleanedRefs.push(weakRef);
|
|
798
|
-
wildcardRouteHandlers.set(key, cleanedRefs);
|
|
799
|
-
} else {
|
|
800
|
-
devRouteHandlers.set(key, {
|
|
801
|
-
routeHandler,
|
|
802
|
-
topicPattern: topicName
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
function findRouteHandlersForTopic(topicName) {
|
|
809
|
-
const handlersMap = /* @__PURE__ */ new Map();
|
|
810
|
-
for (const [
|
|
811
|
-
key,
|
|
812
|
-
{ routeHandler, topicPattern }
|
|
813
|
-
] of devRouteHandlers.entries()) {
|
|
814
|
-
const [_, consumerGroup] = key.split(":");
|
|
815
|
-
if (topicPattern === topicName) {
|
|
816
|
-
if (!handlersMap.has(routeHandler)) {
|
|
817
|
-
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
818
|
-
}
|
|
819
|
-
handlersMap.get(routeHandler).add(consumerGroup);
|
|
820
|
-
}
|
|
985
|
+
return { success: true };
|
|
821
986
|
}
|
|
822
|
-
|
|
823
|
-
const [pattern, consumerGroup] = key.split(":");
|
|
824
|
-
if (matchesWildcardPattern(topicName, pattern)) {
|
|
825
|
-
cleanupDeadRefs(key, refs);
|
|
826
|
-
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
827
|
-
for (const ref of cleanedRefs) {
|
|
828
|
-
const routeHandler = ref.deref();
|
|
829
|
-
if (routeHandler) {
|
|
830
|
-
if (!handlersMap.has(routeHandler)) {
|
|
831
|
-
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
832
|
-
}
|
|
833
|
-
handlersMap.get(routeHandler).add(consumerGroup);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
return handlersMap;
|
|
839
|
-
}
|
|
840
|
-
function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
|
|
841
|
-
const cloudEvent = {
|
|
842
|
-
type: "com.vercel.queue.v1beta",
|
|
843
|
-
source: `/topic/${topicName}/consumer/${consumerGroup}`,
|
|
844
|
-
id: messageId,
|
|
845
|
-
datacontenttype: "application/json",
|
|
846
|
-
data: {
|
|
847
|
-
messageId,
|
|
848
|
-
queueName: topicName,
|
|
849
|
-
consumerGroup
|
|
850
|
-
},
|
|
851
|
-
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
852
|
-
specversion: "1.0"
|
|
853
|
-
};
|
|
854
|
-
return new Request("https://localhost/api/queue/callback", {
|
|
855
|
-
method: "POST",
|
|
856
|
-
headers: {
|
|
857
|
-
"Content-Type": "application/cloudevents+json"
|
|
858
|
-
},
|
|
859
|
-
body: JSON.stringify(cloudEvent)
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
var DEV_CALLBACK_DELAY = 1e3;
|
|
863
|
-
function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
|
|
864
|
-
console.log(
|
|
865
|
-
`[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
|
|
866
|
-
);
|
|
867
|
-
setTimeout(
|
|
868
|
-
() => {
|
|
869
|
-
console.log(
|
|
870
|
-
`[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
|
|
871
|
-
);
|
|
872
|
-
triggerDevCallbacks(topicName, messageId);
|
|
873
|
-
},
|
|
874
|
-
timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
function triggerDevCallbacks(topicName, messageId) {
|
|
878
|
-
const handlersMap = findRouteHandlersForTopic(topicName);
|
|
879
|
-
if (handlersMap.size === 0) {
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
const consumerGroups = Array.from(
|
|
883
|
-
new Set(
|
|
884
|
-
Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
|
|
885
|
-
)
|
|
886
|
-
);
|
|
887
|
-
console.log(
|
|
888
|
-
`[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
|
|
889
|
-
);
|
|
890
|
-
setTimeout(async () => {
|
|
891
|
-
for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
|
|
892
|
-
for (const consumerGroup of consumerGroups2) {
|
|
893
|
-
try {
|
|
894
|
-
const request = createMockCloudEventRequest(
|
|
895
|
-
topicName,
|
|
896
|
-
consumerGroup,
|
|
897
|
-
messageId
|
|
898
|
-
);
|
|
899
|
-
const response = await routeHandler(request);
|
|
900
|
-
if (response.ok) {
|
|
901
|
-
try {
|
|
902
|
-
const responseData = await response.json();
|
|
903
|
-
if (responseData.status === "success") {
|
|
904
|
-
console.log(
|
|
905
|
-
`[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
|
|
906
|
-
);
|
|
907
|
-
}
|
|
908
|
-
} catch (jsonError) {
|
|
909
|
-
console.error(
|
|
910
|
-
`[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
|
|
911
|
-
jsonError
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
} else {
|
|
915
|
-
try {
|
|
916
|
-
const errorData = await response.json();
|
|
917
|
-
console.error(
|
|
918
|
-
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
919
|
-
errorData.error || response.statusText
|
|
920
|
-
);
|
|
921
|
-
} catch (jsonError) {
|
|
922
|
-
console.error(
|
|
923
|
-
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
924
|
-
response.statusText
|
|
925
|
-
);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
} catch (error) {
|
|
929
|
-
console.error(
|
|
930
|
-
`[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
|
|
931
|
-
error
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
}, DEV_CALLBACK_DELAY);
|
|
937
|
-
}
|
|
938
|
-
function clearDevHandlers() {
|
|
939
|
-
devRouteHandlers.clear();
|
|
940
|
-
wildcardRouteHandlers.clear();
|
|
941
|
-
}
|
|
942
|
-
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
943
|
-
globalThis.__clearDevHandlers = clearDevHandlers;
|
|
944
|
-
}
|
|
987
|
+
};
|
|
945
988
|
|
|
946
989
|
// src/consumer-group.ts
|
|
947
990
|
var ConsumerGroup = class {
|
|
@@ -973,8 +1016,7 @@ var ConsumerGroup = class {
|
|
|
973
1016
|
* The extension loop runs every `refreshInterval` seconds and updates the message's
|
|
974
1017
|
* visibility timeout to `visibilityTimeout` seconds from the current time.
|
|
975
1018
|
*
|
|
976
|
-
* @param
|
|
977
|
-
* @param ticket - The receipt ticket that proves ownership of the message
|
|
1019
|
+
* @param receiptHandle - The receipt handle that proves ownership of the message
|
|
978
1020
|
* @returns A function that when called will stop the extension loop
|
|
979
1021
|
*
|
|
980
1022
|
* @remarks
|
|
@@ -984,37 +1026,43 @@ var ConsumerGroup = class {
|
|
|
984
1026
|
* - By default, the stop function returns immediately without waiting for in-flight
|
|
985
1027
|
* - Pass `true` to the stop function to wait for any in-flight extension to complete
|
|
986
1028
|
*/
|
|
987
|
-
startVisibilityExtension(
|
|
1029
|
+
startVisibilityExtension(receiptHandle) {
|
|
988
1030
|
let isRunning = true;
|
|
1031
|
+
let isResolved = false;
|
|
989
1032
|
let resolveLifecycle;
|
|
990
1033
|
let timeoutId = null;
|
|
991
1034
|
const lifecyclePromise = new Promise((resolve) => {
|
|
992
1035
|
resolveLifecycle = resolve;
|
|
993
1036
|
});
|
|
1037
|
+
const safeResolve = () => {
|
|
1038
|
+
if (!isResolved) {
|
|
1039
|
+
isResolved = true;
|
|
1040
|
+
resolveLifecycle();
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
994
1043
|
const extend = async () => {
|
|
995
1044
|
if (!isRunning) {
|
|
996
|
-
|
|
1045
|
+
safeResolve();
|
|
997
1046
|
return;
|
|
998
1047
|
}
|
|
999
1048
|
try {
|
|
1000
1049
|
await this.client.changeVisibility({
|
|
1001
1050
|
queueName: this.topicName,
|
|
1002
1051
|
consumerGroup: this.consumerGroupName,
|
|
1003
|
-
|
|
1004
|
-
ticket,
|
|
1052
|
+
receiptHandle,
|
|
1005
1053
|
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1006
1054
|
});
|
|
1007
1055
|
if (isRunning) {
|
|
1008
1056
|
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
1009
1057
|
} else {
|
|
1010
|
-
|
|
1058
|
+
safeResolve();
|
|
1011
1059
|
}
|
|
1012
1060
|
} catch (error) {
|
|
1013
1061
|
console.error(
|
|
1014
|
-
`Failed to extend visibility for
|
|
1062
|
+
`Failed to extend visibility for receipt handle ${receiptHandle}:`,
|
|
1015
1063
|
error
|
|
1016
1064
|
);
|
|
1017
|
-
|
|
1065
|
+
safeResolve();
|
|
1018
1066
|
}
|
|
1019
1067
|
};
|
|
1020
1068
|
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
@@ -1027,22 +1075,14 @@ var ConsumerGroup = class {
|
|
|
1027
1075
|
if (waitForCompletion) {
|
|
1028
1076
|
await lifecyclePromise;
|
|
1029
1077
|
} else {
|
|
1030
|
-
|
|
1078
|
+
safeResolve();
|
|
1031
1079
|
}
|
|
1032
1080
|
};
|
|
1033
1081
|
}
|
|
1034
|
-
/**
|
|
1035
|
-
* Process a single message with the given handler
|
|
1036
|
-
* @param message The message to process
|
|
1037
|
-
* @param handler Function to process the message
|
|
1038
|
-
*/
|
|
1039
1082
|
async processMessage(message, handler) {
|
|
1040
|
-
const stopExtension = this.startVisibilityExtension(
|
|
1041
|
-
message.messageId,
|
|
1042
|
-
message.ticket
|
|
1043
|
-
);
|
|
1083
|
+
const stopExtension = this.startVisibilityExtension(message.receiptHandle);
|
|
1044
1084
|
try {
|
|
1045
|
-
|
|
1085
|
+
await handler(message.payload, {
|
|
1046
1086
|
messageId: message.messageId,
|
|
1047
1087
|
deliveryCount: message.deliveryCount,
|
|
1048
1088
|
createdAt: message.createdAt,
|
|
@@ -1050,29 +1090,11 @@ var ConsumerGroup = class {
|
|
|
1050
1090
|
consumerGroup: this.consumerGroupName
|
|
1051
1091
|
});
|
|
1052
1092
|
await stopExtension();
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
ticket: message.ticket,
|
|
1059
|
-
visibilityTimeoutSeconds: result.timeoutSeconds
|
|
1060
|
-
});
|
|
1061
|
-
if (isDevMode()) {
|
|
1062
|
-
scheduleDevTimeout(
|
|
1063
|
-
this.topicName,
|
|
1064
|
-
message.messageId,
|
|
1065
|
-
result.timeoutSeconds
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
} else {
|
|
1069
|
-
await this.client.deleteMessage({
|
|
1070
|
-
queueName: this.topicName,
|
|
1071
|
-
consumerGroup: this.consumerGroupName,
|
|
1072
|
-
messageId: message.messageId,
|
|
1073
|
-
ticket: message.ticket
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1093
|
+
await this.client.deleteMessage({
|
|
1094
|
+
queueName: this.topicName,
|
|
1095
|
+
consumerGroup: this.consumerGroupName,
|
|
1096
|
+
receiptHandle: message.receiptHandle
|
|
1097
|
+
});
|
|
1076
1098
|
} catch (error) {
|
|
1077
1099
|
await stopExtension();
|
|
1078
1100
|
if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
@@ -1087,36 +1109,16 @@ var ConsumerGroup = class {
|
|
|
1087
1109
|
}
|
|
1088
1110
|
async consume(handler, options) {
|
|
1089
1111
|
if (options?.messageId) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
);
|
|
1101
|
-
await this.processMessage(
|
|
1102
|
-
response.message,
|
|
1103
|
-
handler
|
|
1104
|
-
);
|
|
1105
|
-
} else {
|
|
1106
|
-
const response = await this.client.receiveMessageById(
|
|
1107
|
-
{
|
|
1108
|
-
queueName: this.topicName,
|
|
1109
|
-
consumerGroup: this.consumerGroupName,
|
|
1110
|
-
messageId: options.messageId,
|
|
1111
|
-
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1112
|
-
},
|
|
1113
|
-
this.transport
|
|
1114
|
-
);
|
|
1115
|
-
await this.processMessage(
|
|
1116
|
-
response.message,
|
|
1117
|
-
handler
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1112
|
+
const response = await this.client.receiveMessageById(
|
|
1113
|
+
{
|
|
1114
|
+
queueName: this.topicName,
|
|
1115
|
+
consumerGroup: this.consumerGroupName,
|
|
1116
|
+
messageId: options.messageId,
|
|
1117
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1118
|
+
},
|
|
1119
|
+
this.transport
|
|
1120
|
+
);
|
|
1121
|
+
await this.processMessage(response.message, handler);
|
|
1120
1122
|
} else {
|
|
1121
1123
|
let messageFound = false;
|
|
1122
1124
|
for await (const message of this.client.receiveMessages(
|
|
@@ -1184,7 +1186,7 @@ var Topic = class {
|
|
|
1184
1186
|
payload,
|
|
1185
1187
|
idempotencyKey: options?.idempotencyKey,
|
|
1186
1188
|
retentionSeconds: options?.retentionSeconds,
|
|
1187
|
-
|
|
1189
|
+
delaySeconds: options?.delaySeconds
|
|
1188
1190
|
},
|
|
1189
1191
|
this.transport
|
|
1190
1192
|
);
|
|
@@ -1225,52 +1227,224 @@ var Topic = class {
|
|
|
1225
1227
|
}
|
|
1226
1228
|
};
|
|
1227
1229
|
|
|
1230
|
+
// src/callback.ts
|
|
1231
|
+
function validateWildcardPattern(pattern) {
|
|
1232
|
+
const firstIndex = pattern.indexOf("*");
|
|
1233
|
+
const lastIndex = pattern.lastIndexOf("*");
|
|
1234
|
+
if (firstIndex !== lastIndex) {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
if (firstIndex === -1) {
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
if (firstIndex !== pattern.length - 1) {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
1246
|
+
const prefix = pattern.slice(0, -1);
|
|
1247
|
+
return topicName.startsWith(prefix);
|
|
1248
|
+
}
|
|
1249
|
+
function findTopicHandler(queueName, handlers) {
|
|
1250
|
+
const exactHandler = handlers[queueName];
|
|
1251
|
+
if (exactHandler) {
|
|
1252
|
+
return exactHandler;
|
|
1253
|
+
}
|
|
1254
|
+
for (const pattern in handlers) {
|
|
1255
|
+
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
1256
|
+
return handlers[pattern];
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
async function parseCallback(request) {
|
|
1262
|
+
const contentType = request.headers.get("content-type");
|
|
1263
|
+
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
"Invalid content type: expected 'application/cloudevents+json'"
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
let cloudEvent;
|
|
1269
|
+
try {
|
|
1270
|
+
cloudEvent = await request.json();
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
1273
|
+
}
|
|
1274
|
+
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
1275
|
+
throw new Error("Invalid CloudEvent: missing required fields");
|
|
1276
|
+
}
|
|
1277
|
+
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const missingFields = [];
|
|
1283
|
+
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
1284
|
+
if (!("consumerGroup" in cloudEvent.data))
|
|
1285
|
+
missingFields.push("consumerGroup");
|
|
1286
|
+
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
1287
|
+
if (missingFields.length > 0) {
|
|
1288
|
+
throw new Error(
|
|
1289
|
+
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
1293
|
+
return {
|
|
1294
|
+
queueName,
|
|
1295
|
+
consumerGroup,
|
|
1296
|
+
messageId
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
function createCallbackHandler(handlers, client) {
|
|
1300
|
+
for (const topicPattern in handlers) {
|
|
1301
|
+
if (topicPattern.includes("*")) {
|
|
1302
|
+
if (!validateWildcardPattern(topicPattern)) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
const routeHandler = async (request) => {
|
|
1310
|
+
try {
|
|
1311
|
+
const { queueName, consumerGroup, messageId } = await parseCallback(request);
|
|
1312
|
+
const topicHandler = findTopicHandler(queueName, handlers);
|
|
1313
|
+
if (!topicHandler) {
|
|
1314
|
+
const availableTopics = Object.keys(handlers).join(", ");
|
|
1315
|
+
return Response.json(
|
|
1316
|
+
{
|
|
1317
|
+
error: `No handler found for topic: ${queueName}`,
|
|
1318
|
+
availableTopics
|
|
1319
|
+
},
|
|
1320
|
+
{ status: 404 }
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1324
|
+
if (!consumerGroupHandler) {
|
|
1325
|
+
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1326
|
+
return Response.json(
|
|
1327
|
+
{
|
|
1328
|
+
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
1329
|
+
availableGroups
|
|
1330
|
+
},
|
|
1331
|
+
{ status: 404 }
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
const topic = new Topic(client, queueName);
|
|
1335
|
+
const cg = topic.consumerGroup(consumerGroup);
|
|
1336
|
+
await cg.consume(consumerGroupHandler, { messageId });
|
|
1337
|
+
return Response.json({ status: "success" });
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
console.error("Queue callback error:", error);
|
|
1340
|
+
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"))) {
|
|
1341
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
1342
|
+
}
|
|
1343
|
+
return Response.json(
|
|
1344
|
+
{ error: "Failed to process queue message" },
|
|
1345
|
+
{ status: 500 }
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
return routeHandler;
|
|
1350
|
+
}
|
|
1351
|
+
function handleCallback(handlers, client) {
|
|
1352
|
+
return createCallbackHandler(handlers, client || new QueueClient());
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1228
1355
|
// src/factory.ts
|
|
1229
1356
|
async function send(topicName, payload, options) {
|
|
1230
1357
|
const transport = options?.transport || new JsonTransport();
|
|
1231
|
-
const client = new QueueClient();
|
|
1358
|
+
const client = options?.client || new QueueClient();
|
|
1232
1359
|
const result = await client.sendMessage(
|
|
1233
1360
|
{
|
|
1234
1361
|
queueName: topicName,
|
|
1235
1362
|
payload,
|
|
1236
1363
|
idempotencyKey: options?.idempotencyKey,
|
|
1237
1364
|
retentionSeconds: options?.retentionSeconds,
|
|
1238
|
-
|
|
1365
|
+
delaySeconds: options?.delaySeconds
|
|
1239
1366
|
},
|
|
1240
1367
|
transport
|
|
1241
1368
|
);
|
|
1242
1369
|
if (isDevMode()) {
|
|
1243
|
-
triggerDevCallbacks(topicName, result.messageId);
|
|
1370
|
+
triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
|
|
1244
1371
|
}
|
|
1245
1372
|
return { messageId: result.messageId };
|
|
1246
1373
|
}
|
|
1247
1374
|
async function receive(topicName, consumerGroup, handler, options) {
|
|
1248
1375
|
const transport = options?.transport || new JsonTransport();
|
|
1249
|
-
const client = new QueueClient();
|
|
1376
|
+
const client = options?.client || new QueueClient();
|
|
1250
1377
|
const topic = new Topic(client, topicName, transport);
|
|
1251
|
-
const { messageId,
|
|
1378
|
+
const { messageId, client: _, ...consumerGroupOptions } = options || {};
|
|
1252
1379
|
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
1253
1380
|
if (messageId) {
|
|
1254
|
-
|
|
1255
|
-
return consumer.consume(handler, {
|
|
1256
|
-
messageId,
|
|
1257
|
-
skipPayload: true
|
|
1258
|
-
});
|
|
1259
|
-
} else {
|
|
1260
|
-
return consumer.consume(handler, { messageId });
|
|
1261
|
-
}
|
|
1381
|
+
return consumer.consume(handler, { messageId });
|
|
1262
1382
|
} else {
|
|
1263
1383
|
return consumer.consume(handler);
|
|
1264
1384
|
}
|
|
1265
1385
|
}
|
|
1386
|
+
|
|
1387
|
+
// src/queue-client.ts
|
|
1388
|
+
var Client = class {
|
|
1389
|
+
client;
|
|
1390
|
+
/**
|
|
1391
|
+
* Create a new Client
|
|
1392
|
+
* @param options QueueClient configuration options
|
|
1393
|
+
*/
|
|
1394
|
+
constructor(options = {}) {
|
|
1395
|
+
this.client = new QueueClient(options);
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Send a message to a topic
|
|
1399
|
+
* @param topicName Name of the topic to send to
|
|
1400
|
+
* @param payload The data to send
|
|
1401
|
+
* @param options Optional publish options and transport
|
|
1402
|
+
* @returns Promise with the message ID
|
|
1403
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1404
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1405
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
1406
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1407
|
+
*/
|
|
1408
|
+
async send(topicName, payload, options) {
|
|
1409
|
+
return send(topicName, payload, {
|
|
1410
|
+
...options,
|
|
1411
|
+
client: this.client
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Create a callback handler for processing queue messages
|
|
1416
|
+
* Returns a Next.js route handler function that routes messages to appropriate handlers
|
|
1417
|
+
* @param handlers Object with topic-specific handlers organized by consumer groups
|
|
1418
|
+
* @returns A Next.js route handler function
|
|
1419
|
+
*
|
|
1420
|
+
* @example
|
|
1421
|
+
* ```typescript
|
|
1422
|
+
* export const POST = client.handleCallback({
|
|
1423
|
+
* "user-events": {
|
|
1424
|
+
* "welcome": (user, metadata) => console.log("Welcoming user", user),
|
|
1425
|
+
* "analytics": (user, metadata) => console.log("Tracking user", user),
|
|
1426
|
+
* },
|
|
1427
|
+
* });
|
|
1428
|
+
* ```
|
|
1429
|
+
*/
|
|
1430
|
+
handleCallback(handlers) {
|
|
1431
|
+
return handleCallback(handlers, this.client);
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1266
1434
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1267
1435
|
0 && (module.exports = {
|
|
1268
1436
|
BadRequestError,
|
|
1269
1437
|
BufferTransport,
|
|
1438
|
+
Client,
|
|
1439
|
+
ConcurrencyLimitError,
|
|
1440
|
+
ConsumerDiscoveryError,
|
|
1441
|
+
ConsumerRegistryNotConfiguredError,
|
|
1442
|
+
DuplicateMessageError,
|
|
1270
1443
|
ForbiddenError,
|
|
1271
1444
|
InternalServerError,
|
|
1272
1445
|
InvalidLimitError,
|
|
1273
1446
|
JsonTransport,
|
|
1447
|
+
MessageAlreadyProcessedError,
|
|
1274
1448
|
MessageCorruptedError,
|
|
1275
1449
|
MessageLockedError,
|
|
1276
1450
|
MessageNotAvailableError,
|