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