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