@vercel/queue 0.0.0-alpha.37 → 0.0.0-alpha.39
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 +210 -174
- package/dist/{types-CAA8nT8x.d.mts → callback-lq_sorrn.d.mts} +249 -16
- package/dist/{types-CAA8nT8x.d.ts → callback-lq_sorrn.d.ts} +249 -16
- package/dist/index.d.mts +74 -333
- package/dist/index.d.ts +74 -333
- package/dist/index.js +713 -679
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +709 -678
- package/dist/index.mjs.map +1 -1
- package/dist/nextjs-pages.d.mts +47 -27
- package/dist/nextjs-pages.d.ts +47 -27
- package/dist/nextjs-pages.js +320 -313
- package/dist/nextjs-pages.js.map +1 -1
- package/dist/nextjs-pages.mjs +320 -313
- package/dist/nextjs-pages.mjs.map +1 -1
- package/dist/web.d.mts +60 -0
- package/dist/web.d.ts +60 -0
- package/dist/web.js +1457 -0
- package/dist/web.js.map +1 -0
- package/dist/web.mjs +1420 -0
- package/dist/web.mjs.map +1 -0
- package/package.json +11 -1
package/dist/index.js
CHANGED
|
@@ -32,7 +32,8 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BadRequestError: () => BadRequestError,
|
|
34
34
|
BufferTransport: () => BufferTransport,
|
|
35
|
-
|
|
35
|
+
CLOUD_EVENT_TYPE_V1BETA: () => CLOUD_EVENT_TYPE_V1BETA,
|
|
36
|
+
CLOUD_EVENT_TYPE_V2BETA: () => CLOUD_EVENT_TYPE_V2BETA,
|
|
36
37
|
ConsumerDiscoveryError: () => ConsumerDiscoveryError,
|
|
37
38
|
ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
|
|
38
39
|
DuplicateMessageError: () => DuplicateMessageError,
|
|
@@ -45,11 +46,13 @@ __export(index_exports, {
|
|
|
45
46
|
MessageLockedError: () => MessageLockedError,
|
|
46
47
|
MessageNotAvailableError: () => MessageNotAvailableError,
|
|
47
48
|
MessageNotFoundError: () => MessageNotFoundError,
|
|
49
|
+
QueueClient: () => QueueClient,
|
|
48
50
|
QueueEmptyError: () => QueueEmptyError,
|
|
49
51
|
StreamTransport: () => StreamTransport,
|
|
50
52
|
UnauthorizedError: () => UnauthorizedError,
|
|
51
53
|
handleCallback: () => handleCallback,
|
|
52
54
|
parseCallback: () => parseCallback,
|
|
55
|
+
parseRawCallback: () => parseRawCallback,
|
|
53
56
|
receive: () => receive,
|
|
54
57
|
send: () => send
|
|
55
58
|
});
|
|
@@ -233,15 +236,476 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
|
|
|
233
236
|
}
|
|
234
237
|
};
|
|
235
238
|
|
|
239
|
+
// src/consumer-group.ts
|
|
240
|
+
var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
|
|
241
|
+
var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
|
|
242
|
+
var MAX_RENEWAL_INTERVAL_SECONDS = 60;
|
|
243
|
+
var MIN_RENEWAL_INTERVAL_SECONDS = 10;
|
|
244
|
+
var RETRY_INTERVAL_MS = 3e3;
|
|
245
|
+
function calculateRenewalInterval(visibilityTimeoutSeconds) {
|
|
246
|
+
return Math.min(
|
|
247
|
+
MAX_RENEWAL_INTERVAL_SECONDS,
|
|
248
|
+
Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
var ConsumerGroup = class {
|
|
252
|
+
client;
|
|
253
|
+
topicName;
|
|
254
|
+
consumerGroupName;
|
|
255
|
+
visibilityTimeout;
|
|
256
|
+
/**
|
|
257
|
+
* Create a new ConsumerGroup instance.
|
|
258
|
+
*
|
|
259
|
+
* @param client - QueueClient instance to use for API calls (transport is configured on the client)
|
|
260
|
+
* @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
|
|
261
|
+
* @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
|
|
262
|
+
* @param options - Optional configuration
|
|
263
|
+
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
|
|
264
|
+
*/
|
|
265
|
+
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
266
|
+
this.client = client;
|
|
267
|
+
this.topicName = topicName;
|
|
268
|
+
this.consumerGroupName = consumerGroupName;
|
|
269
|
+
this.visibilityTimeout = Math.max(
|
|
270
|
+
MIN_VISIBILITY_TIMEOUT_SECONDS,
|
|
271
|
+
options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Check if an error is a 4xx client error that should stop retries.
|
|
276
|
+
* 4xx errors indicate the request is fundamentally invalid and retrying won't help.
|
|
277
|
+
* - 409: Ticket mismatch (lost ownership to another consumer)
|
|
278
|
+
* - 404: Message/receipt handle not found
|
|
279
|
+
* - 400, 401, 403: Other client errors
|
|
280
|
+
*/
|
|
281
|
+
isClientError(error) {
|
|
282
|
+
return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
|
|
283
|
+
error instanceof MessageNotFoundError || // 404 - receipt handle not found
|
|
284
|
+
error instanceof BadRequestError || // 400 - invalid parameters
|
|
285
|
+
error instanceof UnauthorizedError || // 401 - auth failed
|
|
286
|
+
error instanceof ForbiddenError;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Starts a background loop that periodically extends the visibility timeout for a message.
|
|
290
|
+
*
|
|
291
|
+
* Timing strategy:
|
|
292
|
+
* - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
|
|
293
|
+
* - Extensions request the same duration as the initial visibility timeout
|
|
294
|
+
* - When `visibilityDeadline` is provided (binary mode small body), the first
|
|
295
|
+
* extension delay is calculated from the time remaining until the deadline
|
|
296
|
+
* using the same renewal formula, ensuring the first extension fires before
|
|
297
|
+
* the server-assigned lease expires. Subsequent renewals use the standard interval.
|
|
298
|
+
*
|
|
299
|
+
* Retry strategy:
|
|
300
|
+
* - On transient failures (5xx, network errors): retry every 3 seconds
|
|
301
|
+
* - On 4xx client errors: stop retrying (the lease is lost or invalid)
|
|
302
|
+
*
|
|
303
|
+
* @param receiptHandle - The receipt handle to extend visibility for
|
|
304
|
+
* @param options - Optional configuration
|
|
305
|
+
* @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
|
|
306
|
+
* when the current visibility timeout expires. Used to calculate the first extension delay.
|
|
307
|
+
*/
|
|
308
|
+
startVisibilityExtension(receiptHandle, options) {
|
|
309
|
+
let isRunning = true;
|
|
310
|
+
let isResolved = false;
|
|
311
|
+
let resolveLifecycle;
|
|
312
|
+
let timeoutId = null;
|
|
313
|
+
const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
|
|
314
|
+
let firstDelayMs = renewalIntervalMs;
|
|
315
|
+
if (options?.visibilityDeadline) {
|
|
316
|
+
const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
|
|
317
|
+
if (timeRemainingMs > 0) {
|
|
318
|
+
const timeRemainingSeconds = timeRemainingMs / 1e3;
|
|
319
|
+
firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
|
|
320
|
+
} else {
|
|
321
|
+
firstDelayMs = 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const lifecyclePromise = new Promise((resolve) => {
|
|
325
|
+
resolveLifecycle = resolve;
|
|
326
|
+
});
|
|
327
|
+
const safeResolve = () => {
|
|
328
|
+
if (!isResolved) {
|
|
329
|
+
isResolved = true;
|
|
330
|
+
resolveLifecycle();
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const extend = async () => {
|
|
334
|
+
if (!isRunning) {
|
|
335
|
+
safeResolve();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
await this.client.changeVisibility({
|
|
340
|
+
queueName: this.topicName,
|
|
341
|
+
consumerGroup: this.consumerGroupName,
|
|
342
|
+
receiptHandle,
|
|
343
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
344
|
+
});
|
|
345
|
+
if (isRunning) {
|
|
346
|
+
timeoutId = setTimeout(() => extend(), renewalIntervalMs);
|
|
347
|
+
} else {
|
|
348
|
+
safeResolve();
|
|
349
|
+
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (this.isClientError(error)) {
|
|
352
|
+
console.error(
|
|
353
|
+
`Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
|
|
354
|
+
error
|
|
355
|
+
);
|
|
356
|
+
safeResolve();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
console.error(
|
|
360
|
+
`Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
|
|
361
|
+
error
|
|
362
|
+
);
|
|
363
|
+
if (isRunning) {
|
|
364
|
+
timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
|
|
365
|
+
} else {
|
|
366
|
+
safeResolve();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
timeoutId = setTimeout(() => extend(), firstDelayMs);
|
|
371
|
+
return async (waitForCompletion = false) => {
|
|
372
|
+
isRunning = false;
|
|
373
|
+
if (timeoutId) {
|
|
374
|
+
clearTimeout(timeoutId);
|
|
375
|
+
timeoutId = null;
|
|
376
|
+
}
|
|
377
|
+
if (waitForCompletion) {
|
|
378
|
+
await lifecyclePromise;
|
|
379
|
+
} else {
|
|
380
|
+
safeResolve();
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async processMessage(message, handler, options) {
|
|
385
|
+
const stopExtension = this.startVisibilityExtension(
|
|
386
|
+
message.receiptHandle,
|
|
387
|
+
options
|
|
388
|
+
);
|
|
389
|
+
try {
|
|
390
|
+
await handler(message.payload, {
|
|
391
|
+
messageId: message.messageId,
|
|
392
|
+
deliveryCount: message.deliveryCount,
|
|
393
|
+
createdAt: message.createdAt,
|
|
394
|
+
topicName: this.topicName,
|
|
395
|
+
consumerGroup: this.consumerGroupName
|
|
396
|
+
});
|
|
397
|
+
await stopExtension();
|
|
398
|
+
await this.client.deleteMessage({
|
|
399
|
+
queueName: this.topicName,
|
|
400
|
+
consumerGroup: this.consumerGroupName,
|
|
401
|
+
receiptHandle: message.receiptHandle
|
|
402
|
+
});
|
|
403
|
+
} catch (error) {
|
|
404
|
+
await stopExtension();
|
|
405
|
+
const transport = this.client.getTransport();
|
|
406
|
+
if (transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
407
|
+
try {
|
|
408
|
+
await transport.finalize(message.payload);
|
|
409
|
+
} catch (finalizeError) {
|
|
410
|
+
console.warn("Failed to finalize message payload:", finalizeError);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Process a pre-fetched message directly, without calling `receiveMessageById`.
|
|
418
|
+
*
|
|
419
|
+
* Used by the binary mode (v2beta) small body fast path, where the server
|
|
420
|
+
* pushes the full message payload in the callback request. The message is
|
|
421
|
+
* processed with the same lifecycle guarantees as `consume()`:
|
|
422
|
+
* - Visibility timeout is extended periodically during processing
|
|
423
|
+
* - Message is deleted on successful handler completion
|
|
424
|
+
* - Payload is finalized on error if the transport supports it
|
|
425
|
+
*
|
|
426
|
+
* @param handler - Function to process the message payload and metadata
|
|
427
|
+
* @param message - The complete message including payload and receipt handle
|
|
428
|
+
* @param options - Optional configuration
|
|
429
|
+
* @param options.visibilityDeadline - Absolute deadline when the server-assigned
|
|
430
|
+
* visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
|
|
431
|
+
* schedule the first visibility extension before the lease expires.
|
|
432
|
+
*/
|
|
433
|
+
async consumeMessage(handler, message, options) {
|
|
434
|
+
await this.processMessage(message, handler, options);
|
|
435
|
+
}
|
|
436
|
+
async consume(handler, options) {
|
|
437
|
+
if (options && "messageId" in options) {
|
|
438
|
+
const response = await this.client.receiveMessageById({
|
|
439
|
+
queueName: this.topicName,
|
|
440
|
+
consumerGroup: this.consumerGroupName,
|
|
441
|
+
messageId: options.messageId,
|
|
442
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
443
|
+
});
|
|
444
|
+
await this.processMessage(response.message, handler);
|
|
445
|
+
} else {
|
|
446
|
+
const limit = options && "limit" in options ? options.limit : 1;
|
|
447
|
+
let messageFound = false;
|
|
448
|
+
for await (const message of this.client.receiveMessages({
|
|
449
|
+
queueName: this.topicName,
|
|
450
|
+
consumerGroup: this.consumerGroupName,
|
|
451
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
452
|
+
limit
|
|
453
|
+
})) {
|
|
454
|
+
messageFound = true;
|
|
455
|
+
await this.processMessage(message, handler);
|
|
456
|
+
}
|
|
457
|
+
if (!messageFound) {
|
|
458
|
+
await handler(null, null);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Get the consumer group name
|
|
464
|
+
*/
|
|
465
|
+
get name() {
|
|
466
|
+
return this.consumerGroupName;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Get the topic name this consumer group is subscribed to
|
|
470
|
+
*/
|
|
471
|
+
get topic() {
|
|
472
|
+
return this.topicName;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/topic.ts
|
|
477
|
+
var Topic = class {
|
|
478
|
+
client;
|
|
479
|
+
topicName;
|
|
480
|
+
/**
|
|
481
|
+
* Create a new Topic instance
|
|
482
|
+
* @param client QueueClient instance to use for API calls (transport is configured on the client)
|
|
483
|
+
* @param topicName Name of the topic to work with
|
|
484
|
+
*/
|
|
485
|
+
constructor(client, topicName) {
|
|
486
|
+
this.client = client;
|
|
487
|
+
this.topicName = topicName;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Publish a message to the topic
|
|
491
|
+
* @param payload The data to publish
|
|
492
|
+
* @param options Optional publish options
|
|
493
|
+
* @returns An object containing the message ID
|
|
494
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
495
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
496
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
497
|
+
* @throws {InternalServerError} When server encounters an error
|
|
498
|
+
*/
|
|
499
|
+
async publish(payload, options) {
|
|
500
|
+
const result = await this.client.sendMessage({
|
|
501
|
+
queueName: this.topicName,
|
|
502
|
+
payload,
|
|
503
|
+
idempotencyKey: options?.idempotencyKey,
|
|
504
|
+
retentionSeconds: options?.retentionSeconds,
|
|
505
|
+
delaySeconds: options?.delaySeconds,
|
|
506
|
+
headers: options?.headers
|
|
507
|
+
});
|
|
508
|
+
if (isDevMode()) {
|
|
509
|
+
triggerDevCallbacks(this.topicName, result.messageId);
|
|
510
|
+
}
|
|
511
|
+
return { messageId: result.messageId };
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Create a consumer group for this topic
|
|
515
|
+
* @param consumerGroupName Name of the consumer group
|
|
516
|
+
* @param options Optional configuration for the consumer group
|
|
517
|
+
* @returns A ConsumerGroup instance
|
|
518
|
+
*/
|
|
519
|
+
consumerGroup(consumerGroupName, options) {
|
|
520
|
+
return new ConsumerGroup(
|
|
521
|
+
this.client,
|
|
522
|
+
this.topicName,
|
|
523
|
+
consumerGroupName,
|
|
524
|
+
options
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Get the topic name
|
|
529
|
+
*/
|
|
530
|
+
get name() {
|
|
531
|
+
return this.topicName;
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// src/callback.ts
|
|
536
|
+
var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
|
|
537
|
+
var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
|
|
538
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
539
|
+
const prefix = pattern.slice(0, -1);
|
|
540
|
+
return topicName.startsWith(prefix);
|
|
541
|
+
}
|
|
542
|
+
function isRecord(value) {
|
|
543
|
+
return typeof value === "object" && value !== null;
|
|
544
|
+
}
|
|
545
|
+
function parseV1StructuredBody(body, contentType) {
|
|
546
|
+
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
"Invalid content type: expected 'application/cloudevents+json'"
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
|
|
552
|
+
throw new Error("Invalid CloudEvent: missing required fields");
|
|
553
|
+
}
|
|
554
|
+
if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const { data } = body;
|
|
560
|
+
const missingFields = [];
|
|
561
|
+
if (!("queueName" in data)) missingFields.push("queueName");
|
|
562
|
+
if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
|
|
563
|
+
if (!("messageId" in data)) missingFields.push("messageId");
|
|
564
|
+
if (missingFields.length > 0) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
queueName: String(data.queueName),
|
|
571
|
+
consumerGroup: String(data.consumerGroup),
|
|
572
|
+
messageId: String(data.messageId)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function getHeader(headers, name) {
|
|
576
|
+
if (headers instanceof Headers) {
|
|
577
|
+
return headers.get(name);
|
|
578
|
+
}
|
|
579
|
+
const value = headers[name];
|
|
580
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
581
|
+
return value ?? null;
|
|
582
|
+
}
|
|
583
|
+
function parseBinaryHeaders(headers) {
|
|
584
|
+
const ceType = getHeader(headers, "ce-type");
|
|
585
|
+
if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
const queueName = getHeader(headers, "ce-vqsqueuename");
|
|
591
|
+
const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
|
|
592
|
+
const messageId = getHeader(headers, "ce-vqsmessageid");
|
|
593
|
+
const missingFields = [];
|
|
594
|
+
if (!queueName) missingFields.push("ce-vqsqueuename");
|
|
595
|
+
if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
|
|
596
|
+
if (!messageId) missingFields.push("ce-vqsmessageid");
|
|
597
|
+
if (missingFields.length > 0) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`Missing required CloudEvent headers: ${missingFields.join(", ")}`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
const base = {
|
|
603
|
+
queueName,
|
|
604
|
+
consumerGroup,
|
|
605
|
+
messageId
|
|
606
|
+
};
|
|
607
|
+
const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
|
|
608
|
+
if (!receiptHandle) {
|
|
609
|
+
return base;
|
|
610
|
+
}
|
|
611
|
+
const result = { ...base, receiptHandle };
|
|
612
|
+
const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
|
|
613
|
+
if (deliveryCount) {
|
|
614
|
+
result.deliveryCount = parseInt(deliveryCount, 10);
|
|
615
|
+
}
|
|
616
|
+
const createdAt = getHeader(headers, "ce-vqscreatedat");
|
|
617
|
+
if (createdAt) {
|
|
618
|
+
result.createdAt = createdAt;
|
|
619
|
+
}
|
|
620
|
+
const contentType = getHeader(headers, "content-type");
|
|
621
|
+
if (contentType) {
|
|
622
|
+
result.contentType = contentType;
|
|
623
|
+
}
|
|
624
|
+
const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
|
|
625
|
+
if (visibilityDeadline) {
|
|
626
|
+
result.visibilityDeadline = visibilityDeadline;
|
|
627
|
+
}
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
function parseRawCallback(body, headers) {
|
|
631
|
+
const ceType = getHeader(headers, "ce-type");
|
|
632
|
+
if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
|
|
633
|
+
const result = parseBinaryHeaders(headers);
|
|
634
|
+
if ("receiptHandle" in result) {
|
|
635
|
+
result.parsedPayload = body;
|
|
636
|
+
}
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
639
|
+
return parseV1StructuredBody(body, getHeader(headers, "content-type"));
|
|
640
|
+
}
|
|
641
|
+
async function parseCallback(request) {
|
|
642
|
+
const ceType = request.headers.get("ce-type");
|
|
643
|
+
if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
|
|
644
|
+
const result = parseBinaryHeaders(request.headers);
|
|
645
|
+
if ("receiptHandle" in result && request.body) {
|
|
646
|
+
result.rawBody = request.body;
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
let body;
|
|
651
|
+
try {
|
|
652
|
+
body = await request.json();
|
|
653
|
+
} catch {
|
|
654
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
655
|
+
}
|
|
656
|
+
const headers = {};
|
|
657
|
+
request.headers.forEach((value, key) => {
|
|
658
|
+
headers[key] = value;
|
|
659
|
+
});
|
|
660
|
+
return parseRawCallback(body, headers);
|
|
661
|
+
}
|
|
662
|
+
async function handleCallback(handler, request, options) {
|
|
663
|
+
const { queueName, consumerGroup, messageId } = request;
|
|
664
|
+
const client = options?.client || new QueueClient();
|
|
665
|
+
const topic = new Topic(client, queueName);
|
|
666
|
+
const cg = topic.consumerGroup(
|
|
667
|
+
consumerGroup,
|
|
668
|
+
options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
|
|
669
|
+
);
|
|
670
|
+
if ("receiptHandle" in request) {
|
|
671
|
+
const transport = client.getTransport();
|
|
672
|
+
let payload;
|
|
673
|
+
if (request.rawBody) {
|
|
674
|
+
payload = await transport.deserialize(request.rawBody);
|
|
675
|
+
} else if (request.parsedPayload !== void 0) {
|
|
676
|
+
payload = request.parsedPayload;
|
|
677
|
+
} else {
|
|
678
|
+
throw new Error(
|
|
679
|
+
"Binary mode callback with receipt handle is missing payload"
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
const message = {
|
|
683
|
+
messageId,
|
|
684
|
+
payload,
|
|
685
|
+
deliveryCount: request.deliveryCount ?? 1,
|
|
686
|
+
createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
|
|
687
|
+
contentType: request.contentType ?? transport.contentType,
|
|
688
|
+
receiptHandle: request.receiptHandle
|
|
689
|
+
};
|
|
690
|
+
const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
|
|
691
|
+
await cg.consumeMessage(handler, message, { visibilityDeadline });
|
|
692
|
+
} else {
|
|
693
|
+
await cg.consume(handler, { messageId });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
236
697
|
// src/dev.ts
|
|
237
698
|
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
238
699
|
function filePathToUrlPath(filePath) {
|
|
239
|
-
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
|
|
700
|
+
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
|
|
240
701
|
if (!urlPath.startsWith("/")) {
|
|
241
702
|
urlPath = "/" + urlPath;
|
|
242
703
|
}
|
|
243
704
|
return urlPath;
|
|
244
705
|
}
|
|
706
|
+
function filePathToConsumerGroup(filePath) {
|
|
707
|
+
return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
|
|
708
|
+
}
|
|
245
709
|
function getDevRouteMappings() {
|
|
246
710
|
const g = globalThis;
|
|
247
711
|
if (ROUTE_MAPPINGS_KEY in g) {
|
|
@@ -262,11 +726,11 @@ function getDevRouteMappings() {
|
|
|
262
726
|
for (const [filePath, config] of Object.entries(vercelJson.functions)) {
|
|
263
727
|
if (!config.experimentalTriggers) continue;
|
|
264
728
|
for (const trigger of config.experimentalTriggers) {
|
|
265
|
-
if (trigger.type?.startsWith("queue/") && trigger.topic
|
|
729
|
+
if (trigger.type?.startsWith("queue/") && trigger.topic) {
|
|
266
730
|
mappings.push({
|
|
267
731
|
urlPath: filePathToUrlPath(filePath),
|
|
268
732
|
topic: trigger.topic,
|
|
269
|
-
consumer:
|
|
733
|
+
consumer: filePathToConsumerGroup(filePath)
|
|
270
734
|
});
|
|
271
735
|
}
|
|
272
736
|
}
|
|
@@ -299,20 +763,16 @@ var DEV_VISIBILITY_MAX_WAIT = 5e3;
|
|
|
299
763
|
var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
|
|
300
764
|
async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
|
|
301
765
|
const client = new QueueClient();
|
|
302
|
-
const transport = new JsonTransport();
|
|
303
766
|
let elapsed = 0;
|
|
304
767
|
let interval = DEV_VISIBILITY_POLL_INTERVAL;
|
|
305
768
|
while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
|
|
306
769
|
try {
|
|
307
|
-
await client.receiveMessageById(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
},
|
|
314
|
-
transport
|
|
315
|
-
);
|
|
770
|
+
await client.receiveMessageById({
|
|
771
|
+
queueName: topicName,
|
|
772
|
+
consumerGroup,
|
|
773
|
+
messageId,
|
|
774
|
+
visibilityTimeoutSeconds: 0
|
|
775
|
+
});
|
|
316
776
|
return true;
|
|
317
777
|
} catch (error) {
|
|
318
778
|
if (error instanceof MessageNotFoundError) {
|
|
@@ -386,26 +846,15 @@ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
|
|
|
386
846
|
console.log(
|
|
387
847
|
`[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
|
|
388
848
|
);
|
|
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
849
|
try {
|
|
403
850
|
const response = await fetch(url, {
|
|
404
851
|
method: "POST",
|
|
405
852
|
headers: {
|
|
406
|
-
"
|
|
407
|
-
|
|
408
|
-
|
|
853
|
+
"ce-type": CLOUD_EVENT_TYPE_V2BETA,
|
|
854
|
+
"ce-vqsqueuename": topicName,
|
|
855
|
+
"ce-vqsconsumergroup": route.consumer,
|
|
856
|
+
"ce-vqsmessageid": messageId
|
|
857
|
+
}
|
|
409
858
|
});
|
|
410
859
|
if (response.ok) {
|
|
411
860
|
try {
|
|
@@ -512,6 +961,7 @@ var QueueClient = class {
|
|
|
512
961
|
providedToken;
|
|
513
962
|
defaultDeploymentId;
|
|
514
963
|
pinToDeployment;
|
|
964
|
+
transport;
|
|
515
965
|
constructor(options = {}) {
|
|
516
966
|
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
517
967
|
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
|
|
@@ -519,6 +969,10 @@ var QueueClient = class {
|
|
|
519
969
|
this.providedToken = options.token;
|
|
520
970
|
this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
521
971
|
this.pinToDeployment = options.pinToDeployment ?? true;
|
|
972
|
+
this.transport = options.transport || new JsonTransport();
|
|
973
|
+
}
|
|
974
|
+
getTransport() {
|
|
975
|
+
return this.transport;
|
|
522
976
|
}
|
|
523
977
|
getSendDeploymentId() {
|
|
524
978
|
if (isDevMode()) {
|
|
@@ -575,6 +1029,8 @@ var QueueClient = class {
|
|
|
575
1029
|
}
|
|
576
1030
|
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
577
1031
|
}
|
|
1032
|
+
init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.39"}`);
|
|
1033
|
+
init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
|
|
578
1034
|
const response = await fetch(url, init);
|
|
579
1035
|
if (isDebugEnabled()) {
|
|
580
1036
|
const logData = {
|
|
@@ -597,7 +1053,6 @@ var QueueClient = class {
|
|
|
597
1053
|
* @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
|
|
598
1054
|
* @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
|
|
599
1055
|
* @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
|
|
600
|
-
* @param transport - Serializer for the payload
|
|
601
1056
|
* @returns Promise with the generated messageId
|
|
602
1057
|
* @throws {DuplicateMessageError} When idempotency key was already used
|
|
603
1058
|
* @throws {ConsumerDiscoveryError} When consumer discovery fails
|
|
@@ -607,7 +1062,8 @@ var QueueClient = class {
|
|
|
607
1062
|
* @throws {ForbiddenError} When access is denied
|
|
608
1063
|
* @throws {InternalServerError} When server encounters an error
|
|
609
1064
|
*/
|
|
610
|
-
async sendMessage(options
|
|
1065
|
+
async sendMessage(options) {
|
|
1066
|
+
const transport = this.transport;
|
|
611
1067
|
const {
|
|
612
1068
|
queueName,
|
|
613
1069
|
payload,
|
|
@@ -689,21 +1145,24 @@ var QueueClient = class {
|
|
|
689
1145
|
/**
|
|
690
1146
|
* Receive messages from a topic as an async generator.
|
|
691
1147
|
*
|
|
1148
|
+
* When the queue is empty, the generator completes without yielding any
|
|
1149
|
+
* messages. Callers should handle the case where no messages are yielded.
|
|
1150
|
+
*
|
|
692
1151
|
* @param options - Receive options
|
|
693
1152
|
* @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
|
|
694
1153
|
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
695
1154
|
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
696
1155
|
* @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
|
|
697
|
-
* @param transport - Deserializer for message payloads
|
|
698
1156
|
* @yields Message objects with payload, messageId, receiptHandle, etc.
|
|
699
|
-
*
|
|
1157
|
+
* Yields nothing if queue is empty.
|
|
700
1158
|
* @throws {InvalidLimitError} When limit is outside 1-10 range
|
|
701
1159
|
* @throws {BadRequestError} When parameters are invalid
|
|
702
1160
|
* @throws {UnauthorizedError} When authentication fails
|
|
703
1161
|
* @throws {ForbiddenError} When access is denied
|
|
704
1162
|
* @throws {InternalServerError} When server encounters an error
|
|
705
1163
|
*/
|
|
706
|
-
async *receiveMessages(options
|
|
1164
|
+
async *receiveMessages(options) {
|
|
1165
|
+
const transport = this.transport;
|
|
707
1166
|
const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
|
|
708
1167
|
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
709
1168
|
throw new InvalidLimitError(limit);
|
|
@@ -734,7 +1193,7 @@ var QueueClient = class {
|
|
|
734
1193
|
}
|
|
735
1194
|
);
|
|
736
1195
|
if (response.status === 204) {
|
|
737
|
-
|
|
1196
|
+
return;
|
|
738
1197
|
}
|
|
739
1198
|
if (!response.ok) {
|
|
740
1199
|
const errorText = await response.text();
|
|
@@ -775,7 +1234,6 @@ var QueueClient = class {
|
|
|
775
1234
|
* @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
|
|
776
1235
|
* @param options.messageId - Message ID to retrieve
|
|
777
1236
|
* @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
|
|
778
|
-
* @param transport - Deserializer for the message payload
|
|
779
1237
|
* @returns Promise with the message
|
|
780
1238
|
* @throws {MessageNotFoundError} When message doesn't exist
|
|
781
1239
|
* @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
|
|
@@ -785,7 +1243,8 @@ var QueueClient = class {
|
|
|
785
1243
|
* @throws {ForbiddenError} When access is denied
|
|
786
1244
|
* @throws {InternalServerError} When server encounters an error
|
|
787
1245
|
*/
|
|
788
|
-
async receiveMessageById(options
|
|
1246
|
+
async receiveMessageById(options) {
|
|
1247
|
+
const transport = this.transport;
|
|
789
1248
|
const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
|
|
790
1249
|
const headers = new Headers({
|
|
791
1250
|
Authorization: `Bearer ${await this.getToken()}`,
|
|
@@ -797,212 +1256,86 @@ var QueueClient = class {
|
|
|
797
1256
|
"Vqs-Visibility-Timeout-Seconds",
|
|
798
1257
|
visibilityTimeoutSeconds.toString()
|
|
799
1258
|
);
|
|
800
|
-
}
|
|
801
|
-
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
802
|
-
if (effectiveDeploymentId) {
|
|
803
|
-
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
804
|
-
}
|
|
805
|
-
const response = await this.fetch(
|
|
806
|
-
this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
|
|
807
|
-
{
|
|
808
|
-
method: "POST",
|
|
809
|
-
headers
|
|
810
|
-
}
|
|
811
|
-
);
|
|
812
|
-
if (!response.ok) {
|
|
813
|
-
const errorText = await response.text();
|
|
814
|
-
if (response.status === 404) {
|
|
815
|
-
throw new MessageNotFoundError(messageId);
|
|
816
|
-
}
|
|
817
|
-
if (response.status === 409) {
|
|
818
|
-
let errorData = {};
|
|
819
|
-
try {
|
|
820
|
-
errorData = JSON.parse(errorText);
|
|
821
|
-
} catch {
|
|
822
|
-
}
|
|
823
|
-
if (errorData.originalMessageId) {
|
|
824
|
-
throw new MessageNotAvailableError(
|
|
825
|
-
messageId,
|
|
826
|
-
`This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
|
|
827
|
-
);
|
|
828
|
-
}
|
|
829
|
-
throw new MessageNotAvailableError(messageId);
|
|
830
|
-
}
|
|
831
|
-
if (response.status === 410) {
|
|
832
|
-
throw new MessageAlreadyProcessedError(messageId);
|
|
833
|
-
}
|
|
834
|
-
throwCommonHttpError(
|
|
835
|
-
response.status,
|
|
836
|
-
response.statusText,
|
|
837
|
-
errorText,
|
|
838
|
-
"receive message by ID"
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
842
|
-
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
843
|
-
if (!parsedHeaders) {
|
|
844
|
-
await consumeStream(multipartMessage.payload);
|
|
845
|
-
throw new MessageCorruptedError(
|
|
846
|
-
messageId,
|
|
847
|
-
"Missing required queue headers in response"
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
const deserializedPayload = await transport.deserialize(
|
|
851
|
-
multipartMessage.payload
|
|
852
|
-
);
|
|
853
|
-
const message = {
|
|
854
|
-
...parsedHeaders,
|
|
855
|
-
payload: deserializedPayload
|
|
856
|
-
};
|
|
857
|
-
return { message };
|
|
858
|
-
}
|
|
859
|
-
throw new MessageNotFoundError(messageId);
|
|
860
|
-
}
|
|
861
|
-
/**
|
|
862
|
-
* Delete (acknowledge) a message after successful processing.
|
|
863
|
-
*
|
|
864
|
-
* @param options - Delete options
|
|
865
|
-
* @param options.queueName - Topic name
|
|
866
|
-
* @param options.consumerGroup - Consumer group name
|
|
867
|
-
* @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
|
|
868
|
-
* @returns Promise indicating deletion success
|
|
869
|
-
* @throws {MessageNotFoundError} When receipt handle not found
|
|
870
|
-
* @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
|
|
871
|
-
* @throws {BadRequestError} When parameters are invalid
|
|
872
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
873
|
-
* @throws {ForbiddenError} When access is denied
|
|
874
|
-
* @throws {InternalServerError} When server encounters an error
|
|
875
|
-
*/
|
|
876
|
-
async deleteMessage(options) {
|
|
877
|
-
const { queueName, consumerGroup, receiptHandle } = options;
|
|
878
|
-
const headers = new Headers({
|
|
879
|
-
Authorization: `Bearer ${await this.getToken()}`,
|
|
880
|
-
...this.customHeaders
|
|
881
|
-
});
|
|
882
|
-
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
883
|
-
if (effectiveDeploymentId) {
|
|
884
|
-
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
885
|
-
}
|
|
886
|
-
const response = await this.fetch(
|
|
887
|
-
this.buildUrl(
|
|
888
|
-
queueName,
|
|
889
|
-
"consumer",
|
|
890
|
-
consumerGroup,
|
|
891
|
-
"lease",
|
|
892
|
-
receiptHandle
|
|
893
|
-
),
|
|
894
|
-
{
|
|
895
|
-
method: "DELETE",
|
|
896
|
-
headers
|
|
897
|
-
}
|
|
898
|
-
);
|
|
899
|
-
if (!response.ok) {
|
|
900
|
-
const errorText = await response.text();
|
|
901
|
-
if (response.status === 404) {
|
|
902
|
-
throw new MessageNotFoundError(receiptHandle);
|
|
903
|
-
}
|
|
904
|
-
if (response.status === 409) {
|
|
905
|
-
throw new MessageNotAvailableError(
|
|
906
|
-
receiptHandle,
|
|
907
|
-
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
throwCommonHttpError(
|
|
911
|
-
response.status,
|
|
912
|
-
response.statusText,
|
|
913
|
-
errorText,
|
|
914
|
-
"delete message",
|
|
915
|
-
"Missing or invalid receipt handle"
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
return { deleted: true };
|
|
919
|
-
}
|
|
920
|
-
/**
|
|
921
|
-
* Extend or change the visibility timeout of a message.
|
|
922
|
-
* Used to prevent message redelivery while still processing.
|
|
923
|
-
*
|
|
924
|
-
* @param options - Visibility options
|
|
925
|
-
* @param options.queueName - Topic name
|
|
926
|
-
* @param options.consumerGroup - Consumer group name
|
|
927
|
-
* @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
|
|
928
|
-
* @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
|
|
929
|
-
* @returns Promise indicating success
|
|
930
|
-
* @throws {MessageNotFoundError} When receipt handle not found
|
|
931
|
-
* @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
|
|
932
|
-
* @throws {BadRequestError} When parameters are invalid
|
|
933
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
934
|
-
* @throws {ForbiddenError} When access is denied
|
|
935
|
-
* @throws {InternalServerError} When server encounters an error
|
|
936
|
-
*/
|
|
937
|
-
async changeVisibility(options) {
|
|
938
|
-
const {
|
|
939
|
-
queueName,
|
|
940
|
-
consumerGroup,
|
|
941
|
-
receiptHandle,
|
|
942
|
-
visibilityTimeoutSeconds
|
|
943
|
-
} = options;
|
|
944
|
-
const headers = new Headers({
|
|
945
|
-
Authorization: `Bearer ${await this.getToken()}`,
|
|
946
|
-
"Content-Type": "application/json",
|
|
947
|
-
...this.customHeaders
|
|
948
|
-
});
|
|
1259
|
+
}
|
|
949
1260
|
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
950
1261
|
if (effectiveDeploymentId) {
|
|
951
1262
|
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
952
1263
|
}
|
|
953
1264
|
const response = await this.fetch(
|
|
954
|
-
this.buildUrl(
|
|
955
|
-
queueName,
|
|
956
|
-
"consumer",
|
|
957
|
-
consumerGroup,
|
|
958
|
-
"lease",
|
|
959
|
-
receiptHandle
|
|
960
|
-
),
|
|
1265
|
+
this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
|
|
961
1266
|
{
|
|
962
|
-
method: "
|
|
963
|
-
headers
|
|
964
|
-
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
1267
|
+
method: "POST",
|
|
1268
|
+
headers
|
|
965
1269
|
}
|
|
966
1270
|
);
|
|
967
1271
|
if (!response.ok) {
|
|
968
1272
|
const errorText = await response.text();
|
|
969
1273
|
if (response.status === 404) {
|
|
970
|
-
throw new MessageNotFoundError(
|
|
1274
|
+
throw new MessageNotFoundError(messageId);
|
|
971
1275
|
}
|
|
972
1276
|
if (response.status === 409) {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1277
|
+
let errorData = {};
|
|
1278
|
+
try {
|
|
1279
|
+
errorData = JSON.parse(errorText);
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
if (errorData.originalMessageId) {
|
|
1283
|
+
throw new MessageNotAvailableError(
|
|
1284
|
+
messageId,
|
|
1285
|
+
`This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
throw new MessageNotAvailableError(messageId);
|
|
1289
|
+
}
|
|
1290
|
+
if (response.status === 410) {
|
|
1291
|
+
throw new MessageAlreadyProcessedError(messageId);
|
|
977
1292
|
}
|
|
978
1293
|
throwCommonHttpError(
|
|
979
1294
|
response.status,
|
|
980
1295
|
response.statusText,
|
|
981
1296
|
errorText,
|
|
982
|
-
"
|
|
983
|
-
"Missing receipt handle or invalid visibility timeout"
|
|
1297
|
+
"receive message by ID"
|
|
984
1298
|
);
|
|
985
1299
|
}
|
|
986
|
-
|
|
1300
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
1301
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
1302
|
+
if (!parsedHeaders) {
|
|
1303
|
+
await consumeStream(multipartMessage.payload);
|
|
1304
|
+
throw new MessageCorruptedError(
|
|
1305
|
+
messageId,
|
|
1306
|
+
"Missing required queue headers in response"
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
const deserializedPayload = await transport.deserialize(
|
|
1310
|
+
multipartMessage.payload
|
|
1311
|
+
);
|
|
1312
|
+
const message = {
|
|
1313
|
+
...parsedHeaders,
|
|
1314
|
+
payload: deserializedPayload
|
|
1315
|
+
};
|
|
1316
|
+
return { message };
|
|
1317
|
+
}
|
|
1318
|
+
throw new MessageNotFoundError(messageId);
|
|
987
1319
|
}
|
|
988
1320
|
/**
|
|
989
|
-
*
|
|
990
|
-
* Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
|
|
991
|
-
* Functionally equivalent to changeVisibility but follows an alternative API pattern.
|
|
1321
|
+
* Delete (acknowledge) a message after successful processing.
|
|
992
1322
|
*
|
|
993
|
-
* @param options -
|
|
994
|
-
* @
|
|
1323
|
+
* @param options - Delete options
|
|
1324
|
+
* @param options.queueName - Topic name
|
|
1325
|
+
* @param options.consumerGroup - Consumer group name
|
|
1326
|
+
* @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
|
|
1327
|
+
* @returns Promise indicating deletion success
|
|
1328
|
+
* @throws {MessageNotFoundError} When receipt handle not found
|
|
1329
|
+
* @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
|
|
1330
|
+
* @throws {BadRequestError} When parameters are invalid
|
|
1331
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1332
|
+
* @throws {ForbiddenError} When access is denied
|
|
1333
|
+
* @throws {InternalServerError} When server encounters an error
|
|
995
1334
|
*/
|
|
996
|
-
async
|
|
997
|
-
const {
|
|
998
|
-
queueName,
|
|
999
|
-
consumerGroup,
|
|
1000
|
-
receiptHandle,
|
|
1001
|
-
visibilityTimeoutSeconds
|
|
1002
|
-
} = options;
|
|
1335
|
+
async deleteMessage(options) {
|
|
1336
|
+
const { queueName, consumerGroup, receiptHandle } = options;
|
|
1003
1337
|
const headers = new Headers({
|
|
1004
1338
|
Authorization: `Bearer ${await this.getToken()}`,
|
|
1005
|
-
"Content-Type": "application/json",
|
|
1006
1339
|
...this.customHeaders
|
|
1007
1340
|
});
|
|
1008
1341
|
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
@@ -1015,503 +1348,202 @@ var QueueClient = class {
|
|
|
1015
1348
|
"consumer",
|
|
1016
1349
|
consumerGroup,
|
|
1017
1350
|
"lease",
|
|
1018
|
-
receiptHandle
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
return { success: true };
|
|
1047
|
-
}
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
// src/consumer-group.ts
|
|
1051
|
-
var ConsumerGroup = class {
|
|
1052
|
-
client;
|
|
1053
|
-
topicName;
|
|
1054
|
-
consumerGroupName;
|
|
1055
|
-
visibilityTimeout;
|
|
1056
|
-
refreshInterval;
|
|
1057
|
-
transport;
|
|
1058
|
-
/**
|
|
1059
|
-
* Create a new ConsumerGroup instance.
|
|
1060
|
-
*
|
|
1061
|
-
* @param client - QueueClient instance to use for API calls
|
|
1062
|
-
* @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
|
|
1063
|
-
* @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
|
|
1064
|
-
* @param options - Optional configuration
|
|
1065
|
-
* @param options.transport - Payload serializer (default: JsonTransport)
|
|
1066
|
-
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
|
|
1067
|
-
* @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
|
|
1068
|
-
*/
|
|
1069
|
-
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
1070
|
-
this.client = client;
|
|
1071
|
-
this.topicName = topicName;
|
|
1072
|
-
this.consumerGroupName = consumerGroupName;
|
|
1073
|
-
this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
|
|
1074
|
-
this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
|
|
1075
|
-
this.transport = options.transport || new JsonTransport();
|
|
1076
|
-
}
|
|
1077
|
-
/**
|
|
1078
|
-
* Starts a background loop that periodically extends the visibility timeout for a message.
|
|
1079
|
-
*/
|
|
1080
|
-
startVisibilityExtension(receiptHandle) {
|
|
1081
|
-
let isRunning = true;
|
|
1082
|
-
let isResolved = false;
|
|
1083
|
-
let resolveLifecycle;
|
|
1084
|
-
let timeoutId = null;
|
|
1085
|
-
const lifecyclePromise = new Promise((resolve) => {
|
|
1086
|
-
resolveLifecycle = resolve;
|
|
1087
|
-
});
|
|
1088
|
-
const safeResolve = () => {
|
|
1089
|
-
if (!isResolved) {
|
|
1090
|
-
isResolved = true;
|
|
1091
|
-
resolveLifecycle();
|
|
1092
|
-
}
|
|
1093
|
-
};
|
|
1094
|
-
const extend = async () => {
|
|
1095
|
-
if (!isRunning) {
|
|
1096
|
-
safeResolve();
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
try {
|
|
1100
|
-
await this.client.changeVisibility({
|
|
1101
|
-
queueName: this.topicName,
|
|
1102
|
-
consumerGroup: this.consumerGroupName,
|
|
1103
|
-
receiptHandle,
|
|
1104
|
-
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1105
|
-
});
|
|
1106
|
-
if (isRunning) {
|
|
1107
|
-
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
1108
|
-
} else {
|
|
1109
|
-
safeResolve();
|
|
1110
|
-
}
|
|
1111
|
-
} catch (error) {
|
|
1112
|
-
console.error(
|
|
1113
|
-
`Failed to extend visibility for receipt handle ${receiptHandle}:`,
|
|
1114
|
-
error
|
|
1115
|
-
);
|
|
1116
|
-
safeResolve();
|
|
1117
|
-
}
|
|
1118
|
-
};
|
|
1119
|
-
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
1120
|
-
return async (waitForCompletion = false) => {
|
|
1121
|
-
isRunning = false;
|
|
1122
|
-
if (timeoutId) {
|
|
1123
|
-
clearTimeout(timeoutId);
|
|
1124
|
-
timeoutId = null;
|
|
1125
|
-
}
|
|
1126
|
-
if (waitForCompletion) {
|
|
1127
|
-
await lifecyclePromise;
|
|
1128
|
-
} else {
|
|
1129
|
-
safeResolve();
|
|
1130
|
-
}
|
|
1131
|
-
};
|
|
1132
|
-
}
|
|
1133
|
-
async processMessage(message, handler) {
|
|
1134
|
-
const stopExtension = this.startVisibilityExtension(message.receiptHandle);
|
|
1135
|
-
try {
|
|
1136
|
-
await handler(message.payload, {
|
|
1137
|
-
messageId: message.messageId,
|
|
1138
|
-
deliveryCount: message.deliveryCount,
|
|
1139
|
-
createdAt: message.createdAt,
|
|
1140
|
-
topicName: this.topicName,
|
|
1141
|
-
consumerGroup: this.consumerGroupName
|
|
1142
|
-
});
|
|
1143
|
-
await stopExtension();
|
|
1144
|
-
await this.client.deleteMessage({
|
|
1145
|
-
queueName: this.topicName,
|
|
1146
|
-
consumerGroup: this.consumerGroupName,
|
|
1147
|
-
receiptHandle: message.receiptHandle
|
|
1148
|
-
});
|
|
1149
|
-
} catch (error) {
|
|
1150
|
-
await stopExtension();
|
|
1151
|
-
if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
1152
|
-
try {
|
|
1153
|
-
await this.transport.finalize(message.payload);
|
|
1154
|
-
} catch (finalizeError) {
|
|
1155
|
-
console.warn("Failed to finalize message payload:", finalizeError);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
throw error;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
async consume(handler, options) {
|
|
1162
|
-
if (options?.messageId) {
|
|
1163
|
-
const response = await this.client.receiveMessageById(
|
|
1164
|
-
{
|
|
1165
|
-
queueName: this.topicName,
|
|
1166
|
-
consumerGroup: this.consumerGroupName,
|
|
1167
|
-
messageId: options.messageId,
|
|
1168
|
-
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1169
|
-
},
|
|
1170
|
-
this.transport
|
|
1171
|
-
);
|
|
1172
|
-
await this.processMessage(response.message, handler);
|
|
1173
|
-
} else {
|
|
1174
|
-
let messageFound = false;
|
|
1175
|
-
for await (const message of this.client.receiveMessages(
|
|
1176
|
-
{
|
|
1177
|
-
queueName: this.topicName,
|
|
1178
|
-
consumerGroup: this.consumerGroupName,
|
|
1179
|
-
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1180
|
-
limit: 1
|
|
1181
|
-
},
|
|
1182
|
-
this.transport
|
|
1183
|
-
)) {
|
|
1184
|
-
messageFound = true;
|
|
1185
|
-
await this.processMessage(message, handler);
|
|
1186
|
-
break;
|
|
1187
|
-
}
|
|
1188
|
-
if (!messageFound) {
|
|
1189
|
-
throw new Error("No messages available");
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
/**
|
|
1194
|
-
* Get the consumer group name
|
|
1195
|
-
*/
|
|
1196
|
-
get name() {
|
|
1197
|
-
return this.consumerGroupName;
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Get the topic name this consumer group is subscribed to
|
|
1201
|
-
*/
|
|
1202
|
-
get topic() {
|
|
1203
|
-
return this.topicName;
|
|
1204
|
-
}
|
|
1205
|
-
};
|
|
1206
|
-
|
|
1207
|
-
// src/topic.ts
|
|
1208
|
-
var Topic = class {
|
|
1209
|
-
client;
|
|
1210
|
-
topicName;
|
|
1211
|
-
transport;
|
|
1212
|
-
/**
|
|
1213
|
-
* Create a new Topic instance
|
|
1214
|
-
* @param client QueueClient instance to use for API calls
|
|
1215
|
-
* @param topicName Name of the topic to work with
|
|
1216
|
-
* @param transport Optional serializer/deserializer for the payload (defaults to JSON)
|
|
1217
|
-
*/
|
|
1218
|
-
constructor(client, topicName, transport) {
|
|
1219
|
-
this.client = client;
|
|
1220
|
-
this.topicName = topicName;
|
|
1221
|
-
this.transport = transport || new JsonTransport();
|
|
1351
|
+
receiptHandle
|
|
1352
|
+
),
|
|
1353
|
+
{
|
|
1354
|
+
method: "DELETE",
|
|
1355
|
+
headers
|
|
1356
|
+
}
|
|
1357
|
+
);
|
|
1358
|
+
if (!response.ok) {
|
|
1359
|
+
const errorText = await response.text();
|
|
1360
|
+
if (response.status === 404) {
|
|
1361
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
1362
|
+
}
|
|
1363
|
+
if (response.status === 409) {
|
|
1364
|
+
throw new MessageNotAvailableError(
|
|
1365
|
+
receiptHandle,
|
|
1366
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
throwCommonHttpError(
|
|
1370
|
+
response.status,
|
|
1371
|
+
response.statusText,
|
|
1372
|
+
errorText,
|
|
1373
|
+
"delete message",
|
|
1374
|
+
"Missing or invalid receipt handle"
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
return { deleted: true };
|
|
1222
1378
|
}
|
|
1223
1379
|
/**
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
* @
|
|
1228
|
-
* @
|
|
1380
|
+
* Extend or change the visibility timeout of a message.
|
|
1381
|
+
* Used to prevent message redelivery while still processing.
|
|
1382
|
+
*
|
|
1383
|
+
* @param options - Visibility options
|
|
1384
|
+
* @param options.queueName - Topic name
|
|
1385
|
+
* @param options.consumerGroup - Consumer group name
|
|
1386
|
+
* @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
|
|
1387
|
+
* @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
|
|
1388
|
+
* @returns Promise indicating success
|
|
1389
|
+
* @throws {MessageNotFoundError} When receipt handle not found
|
|
1390
|
+
* @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
|
|
1391
|
+
* @throws {BadRequestError} When parameters are invalid
|
|
1229
1392
|
* @throws {UnauthorizedError} When authentication fails
|
|
1230
|
-
* @throws {ForbiddenError} When access is denied
|
|
1393
|
+
* @throws {ForbiddenError} When access is denied
|
|
1231
1394
|
* @throws {InternalServerError} When server encounters an error
|
|
1232
1395
|
*/
|
|
1233
|
-
async
|
|
1234
|
-
const
|
|
1396
|
+
async changeVisibility(options) {
|
|
1397
|
+
const {
|
|
1398
|
+
queueName,
|
|
1399
|
+
consumerGroup,
|
|
1400
|
+
receiptHandle,
|
|
1401
|
+
visibilityTimeoutSeconds
|
|
1402
|
+
} = options;
|
|
1403
|
+
const headers = new Headers({
|
|
1404
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
1405
|
+
"Content-Type": "application/json",
|
|
1406
|
+
...this.customHeaders
|
|
1407
|
+
});
|
|
1408
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
1409
|
+
if (effectiveDeploymentId) {
|
|
1410
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
1411
|
+
}
|
|
1412
|
+
const response = await this.fetch(
|
|
1413
|
+
this.buildUrl(
|
|
1414
|
+
queueName,
|
|
1415
|
+
"consumer",
|
|
1416
|
+
consumerGroup,
|
|
1417
|
+
"lease",
|
|
1418
|
+
receiptHandle
|
|
1419
|
+
),
|
|
1235
1420
|
{
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
delaySeconds: options?.delaySeconds,
|
|
1241
|
-
headers: options?.headers
|
|
1242
|
-
},
|
|
1243
|
-
this.transport
|
|
1421
|
+
method: "PATCH",
|
|
1422
|
+
headers,
|
|
1423
|
+
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
1424
|
+
}
|
|
1244
1425
|
);
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1426
|
+
if (!response.ok) {
|
|
1427
|
+
const errorText = await response.text();
|
|
1428
|
+
if (response.status === 404) {
|
|
1429
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
1430
|
+
}
|
|
1431
|
+
if (response.status === 409) {
|
|
1432
|
+
throw new MessageNotAvailableError(
|
|
1433
|
+
receiptHandle,
|
|
1434
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
throwCommonHttpError(
|
|
1438
|
+
response.status,
|
|
1439
|
+
response.statusText,
|
|
1440
|
+
errorText,
|
|
1441
|
+
"change visibility",
|
|
1442
|
+
"Missing receipt handle or invalid visibility timeout"
|
|
1443
|
+
);
|
|
1247
1444
|
}
|
|
1248
|
-
return {
|
|
1249
|
-
}
|
|
1250
|
-
/**
|
|
1251
|
-
* Create a consumer group for this topic
|
|
1252
|
-
* @param consumerGroupName Name of the consumer group
|
|
1253
|
-
* @param options Optional configuration for the consumer group
|
|
1254
|
-
* @returns A ConsumerGroup instance
|
|
1255
|
-
*/
|
|
1256
|
-
consumerGroup(consumerGroupName, options) {
|
|
1257
|
-
const consumerOptions = {
|
|
1258
|
-
...options,
|
|
1259
|
-
transport: options?.transport || this.transport
|
|
1260
|
-
};
|
|
1261
|
-
return new ConsumerGroup(
|
|
1262
|
-
this.client,
|
|
1263
|
-
this.topicName,
|
|
1264
|
-
consumerGroupName,
|
|
1265
|
-
consumerOptions
|
|
1266
|
-
);
|
|
1267
|
-
}
|
|
1268
|
-
/**
|
|
1269
|
-
* Get the topic name
|
|
1270
|
-
*/
|
|
1271
|
-
get name() {
|
|
1272
|
-
return this.topicName;
|
|
1445
|
+
return { success: true };
|
|
1273
1446
|
}
|
|
1274
1447
|
/**
|
|
1275
|
-
*
|
|
1448
|
+
* Alternative endpoint for changing message visibility timeout.
|
|
1449
|
+
* Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
|
|
1450
|
+
* Functionally equivalent to changeVisibility but follows an alternative API pattern.
|
|
1451
|
+
*
|
|
1452
|
+
* @param options - Options for changing visibility
|
|
1453
|
+
* @returns Promise resolving to change visibility response
|
|
1276
1454
|
*/
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
if (firstIndex !== pattern.length - 1) {
|
|
1293
|
-
return false;
|
|
1294
|
-
}
|
|
1295
|
-
return true;
|
|
1296
|
-
}
|
|
1297
|
-
function matchesWildcardPattern(topicName, pattern) {
|
|
1298
|
-
const prefix = pattern.slice(0, -1);
|
|
1299
|
-
return topicName.startsWith(prefix);
|
|
1300
|
-
}
|
|
1301
|
-
function findTopicHandler(queueName, handlers) {
|
|
1302
|
-
const exactHandler = handlers[queueName];
|
|
1303
|
-
if (exactHandler) {
|
|
1304
|
-
return exactHandler;
|
|
1305
|
-
}
|
|
1306
|
-
for (const pattern in handlers) {
|
|
1307
|
-
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
1308
|
-
return handlers[pattern];
|
|
1455
|
+
async changeVisibilityAlt(options) {
|
|
1456
|
+
const {
|
|
1457
|
+
queueName,
|
|
1458
|
+
consumerGroup,
|
|
1459
|
+
receiptHandle,
|
|
1460
|
+
visibilityTimeoutSeconds
|
|
1461
|
+
} = options;
|
|
1462
|
+
const headers = new Headers({
|
|
1463
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
1464
|
+
"Content-Type": "application/json",
|
|
1465
|
+
...this.customHeaders
|
|
1466
|
+
});
|
|
1467
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
1468
|
+
if (effectiveDeploymentId) {
|
|
1469
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
1309
1470
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
} catch (error) {
|
|
1324
|
-
throw new Error("Failed to parse CloudEvent from request body");
|
|
1325
|
-
}
|
|
1326
|
-
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
1327
|
-
throw new Error("Invalid CloudEvent: missing required fields");
|
|
1328
|
-
}
|
|
1329
|
-
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
1330
|
-
throw new Error(
|
|
1331
|
-
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
1332
|
-
);
|
|
1333
|
-
}
|
|
1334
|
-
const missingFields = [];
|
|
1335
|
-
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
1336
|
-
if (!("consumerGroup" in cloudEvent.data))
|
|
1337
|
-
missingFields.push("consumerGroup");
|
|
1338
|
-
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
1339
|
-
if (missingFields.length > 0) {
|
|
1340
|
-
throw new Error(
|
|
1341
|
-
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1342
|
-
);
|
|
1343
|
-
}
|
|
1344
|
-
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
1345
|
-
return {
|
|
1346
|
-
queueName,
|
|
1347
|
-
consumerGroup,
|
|
1348
|
-
messageId
|
|
1349
|
-
};
|
|
1350
|
-
}
|
|
1351
|
-
function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
|
|
1352
|
-
for (const topicPattern in handlers) {
|
|
1353
|
-
if (topicPattern.includes("*")) {
|
|
1354
|
-
if (!validateWildcardPattern(topicPattern)) {
|
|
1355
|
-
throw new Error(
|
|
1356
|
-
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
1357
|
-
);
|
|
1471
|
+
const response = await this.fetch(
|
|
1472
|
+
this.buildUrl(
|
|
1473
|
+
queueName,
|
|
1474
|
+
"consumer",
|
|
1475
|
+
consumerGroup,
|
|
1476
|
+
"lease",
|
|
1477
|
+
receiptHandle,
|
|
1478
|
+
"visibility"
|
|
1479
|
+
),
|
|
1480
|
+
{
|
|
1481
|
+
method: "PATCH",
|
|
1482
|
+
headers,
|
|
1483
|
+
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
1358
1484
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
const topicHandler = findTopicHandler(queueName, handlers);
|
|
1365
|
-
if (!topicHandler) {
|
|
1366
|
-
const availableTopics = Object.keys(handlers).join(", ");
|
|
1367
|
-
return Response.json(
|
|
1368
|
-
{
|
|
1369
|
-
error: `No handler found for topic: ${queueName}`,
|
|
1370
|
-
availableTopics
|
|
1371
|
-
},
|
|
1372
|
-
{ status: 404 }
|
|
1373
|
-
);
|
|
1485
|
+
);
|
|
1486
|
+
if (!response.ok) {
|
|
1487
|
+
const errorText = await response.text();
|
|
1488
|
+
if (response.status === 404) {
|
|
1489
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
1374
1490
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
{
|
|
1380
|
-
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
1381
|
-
availableGroups
|
|
1382
|
-
},
|
|
1383
|
-
{ status: 404 }
|
|
1491
|
+
if (response.status === 409) {
|
|
1492
|
+
throw new MessageNotAvailableError(
|
|
1493
|
+
receiptHandle,
|
|
1494
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
1384
1495
|
);
|
|
1385
1496
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
return Response.json({ status: "success" });
|
|
1393
|
-
} catch (error) {
|
|
1394
|
-
console.error("Queue callback error:", error);
|
|
1395
|
-
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"))) {
|
|
1396
|
-
return Response.json({ error: error.message }, { status: 400 });
|
|
1397
|
-
}
|
|
1398
|
-
return Response.json(
|
|
1399
|
-
{ error: "Failed to process queue message" },
|
|
1400
|
-
{ status: 500 }
|
|
1497
|
+
throwCommonHttpError(
|
|
1498
|
+
response.status,
|
|
1499
|
+
response.statusText,
|
|
1500
|
+
errorText,
|
|
1501
|
+
"change visibility (alt)",
|
|
1502
|
+
"Missing receipt handle or invalid visibility timeout"
|
|
1401
1503
|
);
|
|
1402
1504
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
}
|
|
1406
|
-
function handleCallback(handlers, options) {
|
|
1407
|
-
return createCallbackHandler(
|
|
1408
|
-
handlers,
|
|
1409
|
-
options?.client || new QueueClient(),
|
|
1410
|
-
options?.visibilityTimeoutSeconds
|
|
1411
|
-
);
|
|
1412
|
-
}
|
|
1505
|
+
return { success: true };
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1413
1508
|
|
|
1414
1509
|
// src/factory.ts
|
|
1415
1510
|
async function send(topicName, payload, options) {
|
|
1416
|
-
const transport = options?.transport || new JsonTransport();
|
|
1417
1511
|
const client = options?.client || new QueueClient();
|
|
1418
|
-
const result = await client.sendMessage(
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
},
|
|
1427
|
-
transport
|
|
1428
|
-
);
|
|
1512
|
+
const result = await client.sendMessage({
|
|
1513
|
+
queueName: topicName,
|
|
1514
|
+
payload,
|
|
1515
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1516
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1517
|
+
delaySeconds: options?.delaySeconds,
|
|
1518
|
+
headers: options?.headers
|
|
1519
|
+
});
|
|
1429
1520
|
if (isDevMode()) {
|
|
1430
1521
|
triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
|
|
1431
1522
|
}
|
|
1432
1523
|
return { messageId: result.messageId };
|
|
1433
1524
|
}
|
|
1434
1525
|
async function receive(topicName, consumerGroup, handler, options) {
|
|
1435
|
-
const transport = options?.transport || new JsonTransport();
|
|
1436
1526
|
const client = options?.client || new QueueClient();
|
|
1437
|
-
const topic = new Topic(client, topicName
|
|
1438
|
-
const {
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1527
|
+
const topic = new Topic(client, topicName);
|
|
1528
|
+
const { client: _, ...rest } = options || {};
|
|
1529
|
+
const { visibilityTimeoutSeconds } = rest;
|
|
1530
|
+
const consumer = topic.consumerGroup(
|
|
1531
|
+
consumerGroup,
|
|
1532
|
+
visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
|
|
1533
|
+
);
|
|
1534
|
+
if (options && "messageId" in options) {
|
|
1535
|
+
return consumer.consume(handler, { messageId: options.messageId });
|
|
1442
1536
|
} else {
|
|
1443
|
-
|
|
1537
|
+
const limit = options && "limit" in options ? options.limit : void 0;
|
|
1538
|
+
return consumer.consume(handler, limit !== void 0 ? { limit } : {});
|
|
1444
1539
|
}
|
|
1445
1540
|
}
|
|
1446
|
-
|
|
1447
|
-
// src/queue-client.ts
|
|
1448
|
-
var Client = class {
|
|
1449
|
-
client;
|
|
1450
|
-
/**
|
|
1451
|
-
* Create a new Client
|
|
1452
|
-
* @param options QueueClient configuration options
|
|
1453
|
-
*/
|
|
1454
|
-
constructor(options = {}) {
|
|
1455
|
-
this.client = new QueueClient(options);
|
|
1456
|
-
}
|
|
1457
|
-
/**
|
|
1458
|
-
* Send a message to a topic
|
|
1459
|
-
* @param topicName Name of the topic to send to
|
|
1460
|
-
* @param payload The data to send
|
|
1461
|
-
* @param options Optional publish options and transport
|
|
1462
|
-
* @returns Promise with the message ID
|
|
1463
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1464
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1465
|
-
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
1466
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1467
|
-
*/
|
|
1468
|
-
async send(topicName, payload, options) {
|
|
1469
|
-
return send(topicName, payload, {
|
|
1470
|
-
...options,
|
|
1471
|
-
client: this.client
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
/**
|
|
1475
|
-
* Create a callback handler for processing queue messages.
|
|
1476
|
-
* Returns a Next.js route handler function that routes messages to appropriate handlers.
|
|
1477
|
-
*
|
|
1478
|
-
* @param handlers - Object with topic-specific handlers organized by consumer groups
|
|
1479
|
-
* @param options - Optional configuration
|
|
1480
|
-
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
|
|
1481
|
-
* @returns A Next.js route handler function
|
|
1482
|
-
*
|
|
1483
|
-
* @example
|
|
1484
|
-
* ```typescript
|
|
1485
|
-
* // Basic usage
|
|
1486
|
-
* export const POST = client.handleCallback({
|
|
1487
|
-
* "user-events": {
|
|
1488
|
-
* "welcome": (user, metadata) => console.log("Welcoming user", user),
|
|
1489
|
-
* "analytics": (user, metadata) => console.log("Tracking user", user),
|
|
1490
|
-
* },
|
|
1491
|
-
* });
|
|
1492
|
-
*
|
|
1493
|
-
* // With custom visibility timeout
|
|
1494
|
-
* export const POST = client.handleCallback({
|
|
1495
|
-
* "video-processing": {
|
|
1496
|
-
* "transcode": async (video) => await transcodeVideo(video),
|
|
1497
|
-
* },
|
|
1498
|
-
* }, {
|
|
1499
|
-
* visibilityTimeoutSeconds: 300, // 5 minutes for long operations
|
|
1500
|
-
* });
|
|
1501
|
-
* ```
|
|
1502
|
-
*/
|
|
1503
|
-
handleCallback(handlers, options) {
|
|
1504
|
-
return handleCallback(handlers, {
|
|
1505
|
-
...options,
|
|
1506
|
-
client: this.client
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
};
|
|
1510
1541
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1511
1542
|
0 && (module.exports = {
|
|
1512
1543
|
BadRequestError,
|
|
1513
1544
|
BufferTransport,
|
|
1514
|
-
|
|
1545
|
+
CLOUD_EVENT_TYPE_V1BETA,
|
|
1546
|
+
CLOUD_EVENT_TYPE_V2BETA,
|
|
1515
1547
|
ConsumerDiscoveryError,
|
|
1516
1548
|
ConsumerRegistryNotConfiguredError,
|
|
1517
1549
|
DuplicateMessageError,
|
|
@@ -1524,11 +1556,13 @@ var Client = class {
|
|
|
1524
1556
|
MessageLockedError,
|
|
1525
1557
|
MessageNotAvailableError,
|
|
1526
1558
|
MessageNotFoundError,
|
|
1559
|
+
QueueClient,
|
|
1527
1560
|
QueueEmptyError,
|
|
1528
1561
|
StreamTransport,
|
|
1529
1562
|
UnauthorizedError,
|
|
1530
1563
|
handleCallback,
|
|
1531
1564
|
parseCallback,
|
|
1565
|
+
parseRawCallback,
|
|
1532
1566
|
receive,
|
|
1533
1567
|
send
|
|
1534
1568
|
});
|