@vercel/queue 0.0.0-alpha.36 → 0.0.0-alpha.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -153,18 +153,6 @@ var MessageAlreadyProcessedError = class extends Error {
153
153
  this.name = "MessageAlreadyProcessedError";
154
154
  }
155
155
  };
156
- var ConcurrencyLimitError = class extends Error {
157
- /** Current number of in-flight messages for this consumer group. */
158
- currentInflight;
159
- /** Maximum allowed concurrent messages (as configured). */
160
- maxConcurrency;
161
- constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
162
- super(message);
163
- this.name = "ConcurrencyLimitError";
164
- this.currentInflight = currentInflight;
165
- this.maxConcurrency = maxConcurrency;
166
- }
167
- };
168
156
  var DuplicateMessageError = class extends Error {
169
157
  idempotencyKey;
170
158
  constructor(message, idempotencyKey) {
@@ -188,239 +176,685 @@ var ConsumerRegistryNotConfiguredError = class extends Error {
188
176
  }
189
177
  };
190
178
 
191
- // src/dev.ts
192
- var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
193
- function filePathToUrlPath(filePath) {
194
- let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
195
- if (!urlPath.startsWith("/")) {
196
- urlPath = "/" + urlPath;
197
- }
198
- return urlPath;
199
- }
200
- function getDevRouteMappings() {
201
- const g = globalThis;
202
- if (ROUTE_MAPPINGS_KEY in g) {
203
- return g[ROUTE_MAPPINGS_KEY] ?? null;
204
- }
205
- try {
206
- const vercelJsonPath = path.join(process.cwd(), "vercel.json");
207
- if (!fs.existsSync(vercelJsonPath)) {
208
- g[ROUTE_MAPPINGS_KEY] = null;
209
- return null;
210
- }
211
- const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
212
- if (!vercelJson.functions) {
213
- g[ROUTE_MAPPINGS_KEY] = null;
214
- return null;
215
- }
216
- const mappings = [];
217
- for (const [filePath, config] of Object.entries(vercelJson.functions)) {
218
- if (!config.experimentalTriggers) continue;
219
- for (const trigger of config.experimentalTriggers) {
220
- if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
221
- mappings.push({
222
- urlPath: filePathToUrlPath(filePath),
223
- topic: trigger.topic,
224
- consumer: trigger.consumer
225
- });
226
- }
227
- }
228
- }
229
- g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
230
- return g[ROUTE_MAPPINGS_KEY];
231
- } catch (error) {
232
- console.warn("[Dev Mode] Failed to read vercel.json:", error);
233
- g[ROUTE_MAPPINGS_KEY] = null;
234
- return null;
235
- }
236
- }
237
- function findMatchingRoutes(topicName) {
238
- const mappings = getDevRouteMappings();
239
- if (!mappings) {
240
- return [];
241
- }
242
- return mappings.filter((mapping) => {
243
- if (mapping.topic.includes("*")) {
244
- return matchesWildcardPattern(topicName, mapping.topic);
245
- }
246
- return mapping.topic === topicName;
247
- });
248
- }
249
- function isDevMode() {
250
- return process.env.NODE_ENV === "development";
251
- }
252
- var DEV_VISIBILITY_POLL_INTERVAL = 50;
253
- var DEV_VISIBILITY_MAX_WAIT = 5e3;
254
- var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
255
- async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
256
- const client = new QueueClient();
257
- const transport = new JsonTransport();
258
- let elapsed = 0;
259
- let interval = DEV_VISIBILITY_POLL_INTERVAL;
260
- while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
261
- try {
262
- await client.receiveMessageById(
263
- {
264
- queueName: topicName,
265
- consumerGroup,
266
- messageId,
267
- visibilityTimeoutSeconds: 0
268
- },
269
- transport
270
- );
271
- return true;
272
- } catch (error) {
273
- if (error instanceof MessageNotFoundError) {
274
- await new Promise((resolve) => setTimeout(resolve, interval));
275
- elapsed += interval;
276
- interval = Math.min(
277
- interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
278
- DEV_VISIBILITY_MAX_WAIT - elapsed
279
- );
280
- continue;
281
- }
282
- if (error instanceof MessageAlreadyProcessedError) {
283
- console.log(
284
- `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
285
- );
286
- return false;
287
- }
288
- console.error(
289
- `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
290
- error
291
- );
292
- return false;
293
- }
294
- }
295
- console.warn(
296
- `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
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)
297
189
  );
298
- return false;
299
190
  }
300
- function triggerDevCallbacks(topicName, messageId, delaySeconds) {
301
- if (delaySeconds && delaySeconds > 0) {
302
- console.log(
303
- `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
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
304
212
  );
305
- setTimeout(() => {
306
- triggerDevCallbacks(topicName, messageId);
307
- }, delaySeconds * 1e3);
308
- return;
309
213
  }
310
- console.log(
311
- `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
312
- );
313
- const matchingRoutes = findMatchingRoutes(topicName);
314
- if (matchingRoutes.length === 0) {
315
- console.log(
316
- `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
317
- );
318
- return;
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;
319
227
  }
320
- const consumerGroups = matchingRoutes.map((r) => r.consumer);
321
- console.log(
322
- `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
323
- );
324
- (async () => {
325
- const firstRoute = matchingRoutes[0];
326
- const isVisible = await waitForMessageVisibility(
327
- topicName,
328
- firstRoute.consumer,
329
- messageId
330
- );
331
- if (!isVisible) {
332
- console.warn(
333
- `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
334
- );
335
- return;
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
+ }
336
263
  }
337
- const port = process.env.PORT || 3e3;
338
- const baseUrl = `http://localhost:${port}`;
339
- for (const route of matchingRoutes) {
340
- const url = `${baseUrl}${route.urlPath}`;
341
- console.log(
342
- `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
343
- );
344
- const cloudEvent = {
345
- type: "com.vercel.queue.v1beta",
346
- source: `/topic/${topicName}/consumer/${route.consumer}`,
347
- id: messageId,
348
- datacontenttype: "application/json",
349
- data: {
350
- messageId,
351
- queueName: topicName,
352
- consumerGroup: route.consumer
353
- },
354
- time: (/* @__PURE__ */ new Date()).toISOString(),
355
- specversion: "1.0"
356
- };
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
+ }
357
278
  try {
358
- const response = await fetch(url, {
359
- method: "POST",
360
- headers: {
361
- "Content-Type": "application/cloudevents+json"
362
- },
363
- body: JSON.stringify(cloudEvent)
279
+ await this.client.changeVisibility({
280
+ queueName: this.topicName,
281
+ consumerGroup: this.consumerGroupName,
282
+ receiptHandle,
283
+ visibilityTimeoutSeconds: this.visibilityTimeout
364
284
  });
365
- if (response.ok) {
366
- try {
367
- const responseData = await response.json();
368
- if (responseData.status === "success") {
369
- console.log(
370
- `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
371
- );
372
- }
373
- } catch {
374
- console.warn(
375
- `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
376
- );
377
- }
285
+ if (isRunning) {
286
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
378
287
  } else {
379
- try {
380
- const errorData = await response.json();
381
- console.error(
382
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
383
- );
384
- } catch {
385
- console.error(
386
- `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
387
- );
388
- }
288
+ safeResolve();
389
289
  }
390
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
+ }
391
299
  console.error(
392
- `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
300
+ `Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
393
301
  error
394
302
  );
303
+ if (isRunning) {
304
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
305
+ } else {
306
+ safeResolve();
307
+ }
395
308
  }
396
- }
397
- })();
398
- }
399
- function clearDevRouteMappings() {
400
- const g = globalThis;
401
- delete g[ROUTE_MAPPINGS_KEY];
402
- }
403
- if (process.env.NODE_ENV === "test" || process.env.VITEST) {
404
- globalThis.__clearDevRouteMappings = clearDevRouteMappings;
405
- }
406
-
407
- // src/oidc.ts
408
- import { getVercelOidcToken } from "@vercel/oidc";
409
-
410
- // src/client.ts
411
- function isDebugEnabled() {
412
- return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
413
- }
414
- async function consumeStream(stream) {
415
- const reader = stream.getReader();
416
- try {
417
- while (true) {
418
- const { done } = await reader.read();
419
- if (done) break;
420
- }
421
- } finally {
422
- reader.releaseLock();
423
- }
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
+
637
+ // src/dev.ts
638
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
639
+ function filePathToUrlPath(filePath) {
640
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
641
+ if (!urlPath.startsWith("/")) {
642
+ urlPath = "/" + urlPath;
643
+ }
644
+ return urlPath;
645
+ }
646
+ function filePathToConsumerGroup(filePath) {
647
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
648
+ }
649
+ function getDevRouteMappings() {
650
+ const g = globalThis;
651
+ if (ROUTE_MAPPINGS_KEY in g) {
652
+ return g[ROUTE_MAPPINGS_KEY] ?? null;
653
+ }
654
+ try {
655
+ const vercelJsonPath = path.join(process.cwd(), "vercel.json");
656
+ if (!fs.existsSync(vercelJsonPath)) {
657
+ g[ROUTE_MAPPINGS_KEY] = null;
658
+ return null;
659
+ }
660
+ const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
661
+ if (!vercelJson.functions) {
662
+ g[ROUTE_MAPPINGS_KEY] = null;
663
+ return null;
664
+ }
665
+ const mappings = [];
666
+ for (const [filePath, config] of Object.entries(vercelJson.functions)) {
667
+ if (!config.experimentalTriggers) continue;
668
+ for (const trigger of config.experimentalTriggers) {
669
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
670
+ mappings.push({
671
+ urlPath: filePathToUrlPath(filePath),
672
+ topic: trigger.topic,
673
+ consumer: filePathToConsumerGroup(filePath)
674
+ });
675
+ }
676
+ }
677
+ }
678
+ g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
679
+ return g[ROUTE_MAPPINGS_KEY];
680
+ } catch (error) {
681
+ console.warn("[Dev Mode] Failed to read vercel.json:", error);
682
+ g[ROUTE_MAPPINGS_KEY] = null;
683
+ return null;
684
+ }
685
+ }
686
+ function findMatchingRoutes(topicName) {
687
+ const mappings = getDevRouteMappings();
688
+ if (!mappings) {
689
+ return [];
690
+ }
691
+ return mappings.filter((mapping) => {
692
+ if (mapping.topic.includes("*")) {
693
+ return matchesWildcardPattern(topicName, mapping.topic);
694
+ }
695
+ return mapping.topic === topicName;
696
+ });
697
+ }
698
+ function isDevMode() {
699
+ return process.env.NODE_ENV === "development";
700
+ }
701
+ var DEV_VISIBILITY_POLL_INTERVAL = 50;
702
+ var DEV_VISIBILITY_MAX_WAIT = 5e3;
703
+ var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
704
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
705
+ const client = new QueueClient();
706
+ let elapsed = 0;
707
+ let interval = DEV_VISIBILITY_POLL_INTERVAL;
708
+ while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
709
+ try {
710
+ await client.receiveMessageById({
711
+ queueName: topicName,
712
+ consumerGroup,
713
+ messageId,
714
+ visibilityTimeoutSeconds: 0
715
+ });
716
+ return true;
717
+ } catch (error) {
718
+ if (error instanceof MessageNotFoundError) {
719
+ await new Promise((resolve) => setTimeout(resolve, interval));
720
+ elapsed += interval;
721
+ interval = Math.min(
722
+ interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
723
+ DEV_VISIBILITY_MAX_WAIT - elapsed
724
+ );
725
+ continue;
726
+ }
727
+ if (error instanceof MessageAlreadyProcessedError) {
728
+ console.log(
729
+ `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
730
+ );
731
+ return false;
732
+ }
733
+ console.error(
734
+ `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
735
+ error
736
+ );
737
+ return false;
738
+ }
739
+ }
740
+ console.warn(
741
+ `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
742
+ );
743
+ return false;
744
+ }
745
+ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
746
+ if (delaySeconds && delaySeconds > 0) {
747
+ console.log(
748
+ `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
749
+ );
750
+ setTimeout(() => {
751
+ triggerDevCallbacks(topicName, messageId);
752
+ }, delaySeconds * 1e3);
753
+ return;
754
+ }
755
+ console.log(
756
+ `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
757
+ );
758
+ const matchingRoutes = findMatchingRoutes(topicName);
759
+ if (matchingRoutes.length === 0) {
760
+ console.log(
761
+ `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
762
+ );
763
+ return;
764
+ }
765
+ const consumerGroups = matchingRoutes.map((r) => r.consumer);
766
+ console.log(
767
+ `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
768
+ );
769
+ (async () => {
770
+ const firstRoute = matchingRoutes[0];
771
+ const isVisible = await waitForMessageVisibility(
772
+ topicName,
773
+ firstRoute.consumer,
774
+ messageId
775
+ );
776
+ if (!isVisible) {
777
+ console.warn(
778
+ `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
779
+ );
780
+ return;
781
+ }
782
+ const port = process.env.PORT || 3e3;
783
+ const baseUrl = `http://localhost:${port}`;
784
+ for (const route of matchingRoutes) {
785
+ const url = `${baseUrl}${route.urlPath}`;
786
+ console.log(
787
+ `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
788
+ );
789
+ try {
790
+ const response = await fetch(url, {
791
+ method: "POST",
792
+ headers: {
793
+ "ce-type": CLOUD_EVENT_TYPE_V2BETA,
794
+ "ce-vqsqueuename": topicName,
795
+ "ce-vqsconsumergroup": route.consumer,
796
+ "ce-vqsmessageid": messageId
797
+ }
798
+ });
799
+ if (response.ok) {
800
+ try {
801
+ const responseData = await response.json();
802
+ if (responseData.status === "success") {
803
+ console.log(
804
+ `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
805
+ );
806
+ }
807
+ } catch {
808
+ console.warn(
809
+ `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
810
+ );
811
+ }
812
+ } else {
813
+ try {
814
+ const errorData = await response.json();
815
+ console.error(
816
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
817
+ );
818
+ } catch {
819
+ console.error(
820
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
821
+ );
822
+ }
823
+ }
824
+ } catch (error) {
825
+ console.error(
826
+ `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
827
+ error
828
+ );
829
+ }
830
+ }
831
+ })();
832
+ }
833
+ function clearDevRouteMappings() {
834
+ const g = globalThis;
835
+ delete g[ROUTE_MAPPINGS_KEY];
836
+ }
837
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
838
+ globalThis.__clearDevRouteMappings = clearDevRouteMappings;
839
+ }
840
+
841
+ // src/oidc.ts
842
+ import { getVercelOidcToken } from "@vercel/oidc";
843
+
844
+ // src/client.ts
845
+ function isDebugEnabled() {
846
+ return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
847
+ }
848
+ async function consumeStream(stream) {
849
+ const reader = stream.getReader();
850
+ try {
851
+ while (true) {
852
+ const { done } = await reader.read();
853
+ if (done) break;
854
+ }
855
+ } finally {
856
+ reader.releaseLock();
857
+ }
424
858
  }
425
859
  function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
426
860
  if (status === 400) {
@@ -467,6 +901,7 @@ var QueueClient = class {
467
901
  providedToken;
468
902
  defaultDeploymentId;
469
903
  pinToDeployment;
904
+ transport;
470
905
  constructor(options = {}) {
471
906
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
472
907
  this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
@@ -474,6 +909,10 @@ var QueueClient = class {
474
909
  this.providedToken = options.token;
475
910
  this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
476
911
  this.pinToDeployment = options.pinToDeployment ?? true;
912
+ this.transport = options.transport || new JsonTransport();
913
+ }
914
+ getTransport() {
915
+ return this.transport;
477
916
  }
478
917
  getSendDeploymentId() {
479
918
  if (isDevMode()) {
@@ -530,6 +969,8 @@ var QueueClient = class {
530
969
  }
531
970
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
532
971
  }
972
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.0-alpha.38"}`);
973
+ init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
533
974
  const response = await fetch(url, init);
534
975
  if (isDebugEnabled()) {
535
976
  const logData = {
@@ -552,7 +993,6 @@ var QueueClient = class {
552
993
  * @param options.idempotencyKey - Optional deduplication key (dedup window: min(retention, 24h))
553
994
  * @param options.retentionSeconds - Message TTL (default: 86400, min: 60, max: 86400)
554
995
  * @param options.delaySeconds - Delivery delay (default: 0, max: retentionSeconds)
555
- * @param transport - Serializer for the payload
556
996
  * @returns Promise with the generated messageId
557
997
  * @throws {DuplicateMessageError} When idempotency key was already used
558
998
  * @throws {ConsumerDiscoveryError} When consumer discovery fails
@@ -562,7 +1002,8 @@ var QueueClient = class {
562
1002
  * @throws {ForbiddenError} When access is denied
563
1003
  * @throws {InternalServerError} When server encounters an error
564
1004
  */
565
- async sendMessage(options, transport) {
1005
+ async sendMessage(options) {
1006
+ const transport = this.transport;
566
1007
  const {
567
1008
  queueName,
568
1009
  payload,
@@ -644,30 +1085,25 @@ var QueueClient = class {
644
1085
  /**
645
1086
  * Receive messages from a topic as an async generator.
646
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
+ *
647
1091
  * @param options - Receive options
648
1092
  * @param options.queueName - Topic name (pattern: `[A-Za-z0-9_-]+`)
649
1093
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
650
1094
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
651
1095
  * @param options.limit - Max messages to retrieve (default: 1, min: 1, max: 10)
652
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
653
- * @param transport - Deserializer for message payloads
654
1096
  * @yields Message objects with payload, messageId, receiptHandle, etc.
655
- * @throws {QueueEmptyError} When no messages available
1097
+ * Yields nothing if queue is empty.
656
1098
  * @throws {InvalidLimitError} When limit is outside 1-10 range
657
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
658
1099
  * @throws {BadRequestError} When parameters are invalid
659
1100
  * @throws {UnauthorizedError} When authentication fails
660
1101
  * @throws {ForbiddenError} When access is denied
661
1102
  * @throws {InternalServerError} When server encounters an error
662
1103
  */
663
- async *receiveMessages(options, transport) {
664
- const {
665
- queueName,
666
- consumerGroup,
667
- visibilityTimeoutSeconds,
668
- limit,
669
- maxConcurrency
670
- } = options;
1104
+ async *receiveMessages(options) {
1105
+ const transport = this.transport;
1106
+ const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
671
1107
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
672
1108
  throw new InvalidLimitError(limit);
673
1109
  }
@@ -685,9 +1121,6 @@ var QueueClient = class {
685
1121
  if (limit !== void 0) {
686
1122
  headers.set("Vqs-Max-Messages", limit.toString());
687
1123
  }
688
- if (maxConcurrency !== void 0) {
689
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
690
- }
691
1124
  const effectiveDeploymentId = this.getConsumeDeploymentId();
692
1125
  if (effectiveDeploymentId) {
693
1126
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
@@ -700,22 +1133,10 @@ var QueueClient = class {
700
1133
  }
701
1134
  );
702
1135
  if (response.status === 204) {
703
- throw new QueueEmptyError(queueName, consumerGroup);
1136
+ return;
704
1137
  }
705
1138
  if (!response.ok) {
706
1139
  const errorText = await response.text();
707
- if (response.status === 429) {
708
- let errorData = {};
709
- try {
710
- errorData = JSON.parse(errorText);
711
- } catch {
712
- }
713
- throw new ConcurrencyLimitError(
714
- errorData.error || "Concurrency limit exceeded or throttled",
715
- errorData.currentInflight,
716
- errorData.maxConcurrency
717
- );
718
- }
719
1140
  throwCommonHttpError(
720
1141
  response.status,
721
1142
  response.statusText,
@@ -753,26 +1174,18 @@ var QueueClient = class {
753
1174
  * @param options.consumerGroup - Consumer group name (pattern: `[A-Za-z0-9_-]+`)
754
1175
  * @param options.messageId - Message ID to retrieve
755
1176
  * @param options.visibilityTimeoutSeconds - Lock duration (default: 30, min: 0, max: 3600)
756
- * @param options.maxConcurrency - Max in-flight messages (default: unlimited, min: 1)
757
- * @param transport - Deserializer for the message payload
758
1177
  * @returns Promise with the message
759
1178
  * @throws {MessageNotFoundError} When message doesn't exist
760
1179
  * @throws {MessageNotAvailableError} When message is in wrong state or was a duplicate
761
1180
  * @throws {MessageAlreadyProcessedError} When message was already processed
762
- * @throws {ConcurrencyLimitError} When maxConcurrency exceeded
763
1181
  * @throws {BadRequestError} When parameters are invalid
764
1182
  * @throws {UnauthorizedError} When authentication fails
765
1183
  * @throws {ForbiddenError} When access is denied
766
1184
  * @throws {InternalServerError} When server encounters an error
767
1185
  */
768
- async receiveMessageById(options, transport) {
769
- const {
770
- queueName,
771
- consumerGroup,
772
- messageId,
773
- visibilityTimeoutSeconds,
774
- maxConcurrency
775
- } = options;
1186
+ async receiveMessageById(options) {
1187
+ const transport = this.transport;
1188
+ const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
776
1189
  const headers = new Headers({
777
1190
  Authorization: `Bearer ${await this.getToken()}`,
778
1191
  Accept: "multipart/mixed",
@@ -783,227 +1196,86 @@ var QueueClient = class {
783
1196
  "Vqs-Visibility-Timeout-Seconds",
784
1197
  visibilityTimeoutSeconds.toString()
785
1198
  );
786
- }
787
- if (maxConcurrency !== void 0) {
788
- headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
789
- }
790
- const effectiveDeploymentId = this.getConsumeDeploymentId();
791
- if (effectiveDeploymentId) {
792
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
793
- }
794
- const response = await this.fetch(
795
- this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
796
- {
797
- method: "POST",
798
- headers
799
- }
800
- );
801
- if (!response.ok) {
802
- const errorText = await response.text();
803
- if (response.status === 404) {
804
- throw new MessageNotFoundError(messageId);
805
- }
806
- if (response.status === 409) {
807
- let errorData = {};
808
- try {
809
- errorData = JSON.parse(errorText);
810
- } catch {
811
- }
812
- if (errorData.originalMessageId) {
813
- throw new MessageNotAvailableError(
814
- messageId,
815
- `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
816
- );
817
- }
818
- throw new MessageNotAvailableError(messageId);
819
- }
820
- if (response.status === 410) {
821
- throw new MessageAlreadyProcessedError(messageId);
822
- }
823
- if (response.status === 429) {
824
- let errorData = {};
825
- try {
826
- errorData = JSON.parse(errorText);
827
- } catch {
828
- }
829
- throw new ConcurrencyLimitError(
830
- errorData.error || "Concurrency limit exceeded or throttled",
831
- errorData.currentInflight,
832
- errorData.maxConcurrency
833
- );
834
- }
835
- throwCommonHttpError(
836
- response.status,
837
- response.statusText,
838
- errorText,
839
- "receive message by ID"
840
- );
841
- }
842
- for await (const multipartMessage of parseMultipartStream(response)) {
843
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
844
- if (!parsedHeaders) {
845
- await consumeStream(multipartMessage.payload);
846
- throw new MessageCorruptedError(
847
- messageId,
848
- "Missing required queue headers in response"
849
- );
850
- }
851
- const deserializedPayload = await transport.deserialize(
852
- multipartMessage.payload
853
- );
854
- const message = {
855
- ...parsedHeaders,
856
- payload: deserializedPayload
857
- };
858
- return { message };
859
- }
860
- throw new MessageNotFoundError(messageId);
861
- }
862
- /**
863
- * Delete (acknowledge) a message after successful processing.
864
- *
865
- * @param options - Delete options
866
- * @param options.queueName - Topic name
867
- * @param options.consumerGroup - Consumer group name
868
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
869
- * @returns Promise indicating deletion success
870
- * @throws {MessageNotFoundError} When receipt handle not found
871
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
872
- * @throws {BadRequestError} When parameters are invalid
873
- * @throws {UnauthorizedError} When authentication fails
874
- * @throws {ForbiddenError} When access is denied
875
- * @throws {InternalServerError} When server encounters an error
876
- */
877
- async deleteMessage(options) {
878
- const { queueName, consumerGroup, receiptHandle } = options;
879
- const headers = new Headers({
880
- Authorization: `Bearer ${await this.getToken()}`,
881
- ...this.customHeaders
882
- });
883
- const effectiveDeploymentId = this.getConsumeDeploymentId();
884
- if (effectiveDeploymentId) {
885
- headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
886
- }
887
- const response = await this.fetch(
888
- this.buildUrl(
889
- queueName,
890
- "consumer",
891
- consumerGroup,
892
- "lease",
893
- receiptHandle
894
- ),
895
- {
896
- method: "DELETE",
897
- headers
898
- }
899
- );
900
- if (!response.ok) {
901
- const errorText = await response.text();
902
- if (response.status === 404) {
903
- throw new MessageNotFoundError(receiptHandle);
904
- }
905
- if (response.status === 409) {
906
- throw new MessageNotAvailableError(
907
- receiptHandle,
908
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
909
- );
910
- }
911
- throwCommonHttpError(
912
- response.status,
913
- response.statusText,
914
- errorText,
915
- "delete message",
916
- "Missing or invalid receipt handle"
917
- );
918
- }
919
- return { deleted: true };
920
- }
921
- /**
922
- * Extend or change the visibility timeout of a message.
923
- * Used to prevent message redelivery while still processing.
924
- *
925
- * @param options - Visibility options
926
- * @param options.queueName - Topic name
927
- * @param options.consumerGroup - Consumer group name
928
- * @param options.receiptHandle - Receipt handle from the received message (must use same deployment ID as receive)
929
- * @param options.visibilityTimeoutSeconds - New timeout (min: 0, max: 3600, cannot exceed message expiration)
930
- * @returns Promise indicating success
931
- * @throws {MessageNotFoundError} When receipt handle not found
932
- * @throws {MessageNotAvailableError} When receipt handle invalid or message already processed
933
- * @throws {BadRequestError} When parameters are invalid
934
- * @throws {UnauthorizedError} When authentication fails
935
- * @throws {ForbiddenError} When access is denied
936
- * @throws {InternalServerError} When server encounters an error
937
- */
938
- async changeVisibility(options) {
939
- const {
940
- queueName,
941
- consumerGroup,
942
- receiptHandle,
943
- visibilityTimeoutSeconds
944
- } = options;
945
- const headers = new Headers({
946
- Authorization: `Bearer ${await this.getToken()}`,
947
- "Content-Type": "application/json",
948
- ...this.customHeaders
949
- });
1199
+ }
950
1200
  const effectiveDeploymentId = this.getConsumeDeploymentId();
951
1201
  if (effectiveDeploymentId) {
952
1202
  headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
953
1203
  }
954
1204
  const response = await this.fetch(
955
- this.buildUrl(
956
- queueName,
957
- "consumer",
958
- consumerGroup,
959
- "lease",
960
- receiptHandle
961
- ),
1205
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
962
1206
  {
963
- method: "PATCH",
964
- headers,
965
- body: JSON.stringify({ visibilityTimeoutSeconds })
1207
+ method: "POST",
1208
+ headers
966
1209
  }
967
1210
  );
968
1211
  if (!response.ok) {
969
1212
  const errorText = await response.text();
970
1213
  if (response.status === 404) {
971
- throw new MessageNotFoundError(receiptHandle);
1214
+ throw new MessageNotFoundError(messageId);
972
1215
  }
973
1216
  if (response.status === 409) {
974
- throw new MessageNotAvailableError(
975
- receiptHandle,
976
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
977
- );
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);
978
1232
  }
979
1233
  throwCommonHttpError(
980
1234
  response.status,
981
1235
  response.statusText,
982
1236
  errorText,
983
- "change visibility",
984
- "Missing receipt handle or invalid visibility timeout"
1237
+ "receive message by ID"
985
1238
  );
986
1239
  }
987
- return { success: true };
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);
988
1259
  }
989
1260
  /**
990
- * Alternative endpoint for changing message visibility timeout.
991
- * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
992
- * Functionally equivalent to changeVisibility but follows an alternative API pattern.
1261
+ * Delete (acknowledge) a message after successful processing.
993
1262
  *
994
- * @param options - Options for changing visibility
995
- * @returns Promise resolving to change visibility response
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
996
1274
  */
997
- async changeVisibilityAlt(options) {
998
- const {
999
- queueName,
1000
- consumerGroup,
1001
- receiptHandle,
1002
- visibilityTimeoutSeconds
1003
- } = options;
1275
+ async deleteMessage(options) {
1276
+ const { queueName, consumerGroup, receiptHandle } = options;
1004
1277
  const headers = new Headers({
1005
1278
  Authorization: `Bearer ${await this.getToken()}`,
1006
- "Content-Type": "application/json",
1007
1279
  ...this.customHeaders
1008
1280
  });
1009
1281
  const effectiveDeploymentId = this.getConsumeDeploymentId();
@@ -1016,503 +1288,201 @@ var QueueClient = class {
1016
1288
  "consumer",
1017
1289
  consumerGroup,
1018
1290
  "lease",
1019
- receiptHandle,
1020
- "visibility"
1021
- ),
1022
- {
1023
- method: "PATCH",
1024
- headers,
1025
- body: JSON.stringify({ visibilityTimeoutSeconds })
1026
- }
1027
- );
1028
- if (!response.ok) {
1029
- const errorText = await response.text();
1030
- if (response.status === 404) {
1031
- throw new MessageNotFoundError(receiptHandle);
1032
- }
1033
- if (response.status === 409) {
1034
- throw new MessageNotAvailableError(
1035
- receiptHandle,
1036
- errorText || "Invalid receipt handle, message not in correct state, or already processed"
1037
- );
1038
- }
1039
- throwCommonHttpError(
1040
- response.status,
1041
- response.statusText,
1042
- errorText,
1043
- "change visibility (alt)",
1044
- "Missing receipt handle or invalid visibility timeout"
1045
- );
1046
- }
1047
- return { success: true };
1048
- }
1049
- };
1050
-
1051
- // src/consumer-group.ts
1052
- var ConsumerGroup = class {
1053
- client;
1054
- topicName;
1055
- consumerGroupName;
1056
- visibilityTimeout;
1057
- refreshInterval;
1058
- transport;
1059
- /**
1060
- * Create a new ConsumerGroup instance.
1061
- *
1062
- * @param client - QueueClient instance to use for API calls
1063
- * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
1064
- * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1065
- * @param options - Optional configuration
1066
- * @param options.transport - Payload serializer (default: JsonTransport)
1067
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1068
- * @param options.visibilityRefreshInterval - Lock refresh interval in seconds (default: visibilityTimeout / 3)
1069
- */
1070
- constructor(client, topicName, consumerGroupName, options = {}) {
1071
- this.client = client;
1072
- this.topicName = topicName;
1073
- this.consumerGroupName = consumerGroupName;
1074
- this.visibilityTimeout = options.visibilityTimeoutSeconds ?? 30;
1075
- this.refreshInterval = options.visibilityRefreshInterval ?? Math.floor(this.visibilityTimeout / 3);
1076
- this.transport = options.transport || new JsonTransport();
1077
- }
1078
- /**
1079
- * Starts a background loop that periodically extends the visibility timeout for a message.
1080
- */
1081
- startVisibilityExtension(receiptHandle) {
1082
- let isRunning = true;
1083
- let isResolved = false;
1084
- let resolveLifecycle;
1085
- let timeoutId = null;
1086
- const lifecyclePromise = new Promise((resolve) => {
1087
- resolveLifecycle = resolve;
1088
- });
1089
- const safeResolve = () => {
1090
- if (!isResolved) {
1091
- isResolved = true;
1092
- resolveLifecycle();
1093
- }
1094
- };
1095
- const extend = async () => {
1096
- if (!isRunning) {
1097
- safeResolve();
1098
- return;
1099
- }
1100
- try {
1101
- await this.client.changeVisibility({
1102
- queueName: this.topicName,
1103
- consumerGroup: this.consumerGroupName,
1104
- receiptHandle,
1105
- visibilityTimeoutSeconds: this.visibilityTimeout
1106
- });
1107
- if (isRunning) {
1108
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1109
- } else {
1110
- safeResolve();
1111
- }
1112
- } catch (error) {
1113
- console.error(
1114
- `Failed to extend visibility for receipt handle ${receiptHandle}:`,
1115
- error
1116
- );
1117
- safeResolve();
1118
- }
1119
- };
1120
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
1121
- return async (waitForCompletion = false) => {
1122
- isRunning = false;
1123
- if (timeoutId) {
1124
- clearTimeout(timeoutId);
1125
- timeoutId = null;
1126
- }
1127
- if (waitForCompletion) {
1128
- await lifecyclePromise;
1129
- } else {
1130
- safeResolve();
1131
- }
1132
- };
1133
- }
1134
- async processMessage(message, handler) {
1135
- const stopExtension = this.startVisibilityExtension(message.receiptHandle);
1136
- try {
1137
- await handler(message.payload, {
1138
- messageId: message.messageId,
1139
- deliveryCount: message.deliveryCount,
1140
- createdAt: message.createdAt,
1141
- topicName: this.topicName,
1142
- consumerGroup: this.consumerGroupName
1143
- });
1144
- await stopExtension();
1145
- await this.client.deleteMessage({
1146
- queueName: this.topicName,
1147
- consumerGroup: this.consumerGroupName,
1148
- receiptHandle: message.receiptHandle
1149
- });
1150
- } catch (error) {
1151
- await stopExtension();
1152
- if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
1153
- try {
1154
- await this.transport.finalize(message.payload);
1155
- } catch (finalizeError) {
1156
- console.warn("Failed to finalize message payload:", finalizeError);
1157
- }
1158
- }
1159
- throw error;
1160
- }
1161
- }
1162
- async consume(handler, options) {
1163
- if (options?.messageId) {
1164
- const response = await this.client.receiveMessageById(
1165
- {
1166
- queueName: this.topicName,
1167
- consumerGroup: this.consumerGroupName,
1168
- messageId: options.messageId,
1169
- visibilityTimeoutSeconds: this.visibilityTimeout
1170
- },
1171
- this.transport
1172
- );
1173
- await this.processMessage(response.message, handler);
1174
- } else {
1175
- let messageFound = false;
1176
- for await (const message of this.client.receiveMessages(
1177
- {
1178
- queueName: this.topicName,
1179
- consumerGroup: this.consumerGroupName,
1180
- visibilityTimeoutSeconds: this.visibilityTimeout,
1181
- limit: 1
1182
- },
1183
- this.transport
1184
- )) {
1185
- messageFound = true;
1186
- await this.processMessage(message, handler);
1187
- break;
1188
- }
1189
- if (!messageFound) {
1190
- throw new Error("No messages available");
1191
- }
1192
- }
1193
- }
1194
- /**
1195
- * Get the consumer group name
1196
- */
1197
- get name() {
1198
- return this.consumerGroupName;
1199
- }
1200
- /**
1201
- * Get the topic name this consumer group is subscribed to
1202
- */
1203
- get topic() {
1204
- return this.topicName;
1205
- }
1206
- };
1207
-
1208
- // src/topic.ts
1209
- var Topic = class {
1210
- client;
1211
- topicName;
1212
- transport;
1213
- /**
1214
- * Create a new Topic instance
1215
- * @param client QueueClient instance to use for API calls
1216
- * @param topicName Name of the topic to work with
1217
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1218
- */
1219
- constructor(client, topicName, transport) {
1220
- this.client = client;
1221
- this.topicName = topicName;
1222
- 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 };
1223
1318
  }
1224
1319
  /**
1225
- * Publish a message to the topic
1226
- * @param payload The data to publish
1227
- * @param options Optional publish options
1228
- * @returns An object containing the message ID
1229
- * @throws {BadRequestError} When request parameters are invalid
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
1230
1332
  * @throws {UnauthorizedError} When authentication fails
1231
- * @throws {ForbiddenError} When access is denied (environment mismatch)
1333
+ * @throws {ForbiddenError} When access is denied
1232
1334
  * @throws {InternalServerError} When server encounters an error
1233
1335
  */
1234
- async publish(payload, options) {
1235
- const result = await this.client.sendMessage(
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
+ ),
1236
1360
  {
1237
- queueName: this.topicName,
1238
- payload,
1239
- idempotencyKey: options?.idempotencyKey,
1240
- retentionSeconds: options?.retentionSeconds,
1241
- delaySeconds: options?.delaySeconds,
1242
- headers: options?.headers
1243
- },
1244
- this.transport
1361
+ method: "PATCH",
1362
+ headers,
1363
+ body: JSON.stringify({ visibilityTimeoutSeconds })
1364
+ }
1245
1365
  );
1246
- if (isDevMode()) {
1247
- triggerDevCallbacks(this.topicName, result.messageId);
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
+ );
1248
1384
  }
1249
- return { messageId: result.messageId };
1250
- }
1251
- /**
1252
- * Create a consumer group for this topic
1253
- * @param consumerGroupName Name of the consumer group
1254
- * @param options Optional configuration for the consumer group
1255
- * @returns A ConsumerGroup instance
1256
- */
1257
- consumerGroup(consumerGroupName, options) {
1258
- const consumerOptions = {
1259
- ...options,
1260
- transport: options?.transport || this.transport
1261
- };
1262
- return new ConsumerGroup(
1263
- this.client,
1264
- this.topicName,
1265
- consumerGroupName,
1266
- consumerOptions
1267
- );
1268
- }
1269
- /**
1270
- * Get the topic name
1271
- */
1272
- get name() {
1273
- return this.topicName;
1385
+ return { success: true };
1274
1386
  }
1275
1387
  /**
1276
- * Get the transport used by this topic
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
1277
1394
  */
1278
- get serializer() {
1279
- return this.transport;
1280
- }
1281
- };
1282
-
1283
- // src/callback.ts
1284
- function validateWildcardPattern(pattern) {
1285
- const firstIndex = pattern.indexOf("*");
1286
- const lastIndex = pattern.lastIndexOf("*");
1287
- if (firstIndex !== lastIndex) {
1288
- return false;
1289
- }
1290
- if (firstIndex === -1) {
1291
- return false;
1292
- }
1293
- if (firstIndex !== pattern.length - 1) {
1294
- return false;
1295
- }
1296
- return true;
1297
- }
1298
- function matchesWildcardPattern(topicName, pattern) {
1299
- const prefix = pattern.slice(0, -1);
1300
- return topicName.startsWith(prefix);
1301
- }
1302
- function findTopicHandler(queueName, handlers) {
1303
- const exactHandler = handlers[queueName];
1304
- if (exactHandler) {
1305
- return exactHandler;
1306
- }
1307
- for (const pattern in handlers) {
1308
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
1309
- 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);
1310
1410
  }
1311
- }
1312
- return null;
1313
- }
1314
- async function parseCallback(request) {
1315
- const contentType = request.headers.get("content-type");
1316
- if (!contentType || !contentType.includes("application/cloudevents+json")) {
1317
- throw new Error(
1318
- "Invalid content type: expected 'application/cloudevents+json'"
1319
- );
1320
- }
1321
- let cloudEvent;
1322
- try {
1323
- cloudEvent = await request.json();
1324
- } catch (error) {
1325
- throw new Error("Failed to parse CloudEvent from request body");
1326
- }
1327
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
1328
- throw new Error("Invalid CloudEvent: missing required fields");
1329
- }
1330
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
1331
- throw new Error(
1332
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
1333
- );
1334
- }
1335
- const missingFields = [];
1336
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
1337
- if (!("consumerGroup" in cloudEvent.data))
1338
- missingFields.push("consumerGroup");
1339
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
1340
- if (missingFields.length > 0) {
1341
- throw new Error(
1342
- `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1343
- );
1344
- }
1345
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
1346
- return {
1347
- queueName,
1348
- consumerGroup,
1349
- messageId
1350
- };
1351
- }
1352
- function createCallbackHandler(handlers, client, visibilityTimeoutSeconds) {
1353
- for (const topicPattern in handlers) {
1354
- if (topicPattern.includes("*")) {
1355
- if (!validateWildcardPattern(topicPattern)) {
1356
- throw new Error(
1357
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
1358
- );
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 })
1359
1424
  }
1360
- }
1361
- }
1362
- const routeHandler = async (request) => {
1363
- try {
1364
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
1365
- const topicHandler = findTopicHandler(queueName, handlers);
1366
- if (!topicHandler) {
1367
- const availableTopics = Object.keys(handlers).join(", ");
1368
- return Response.json(
1369
- {
1370
- error: `No handler found for topic: ${queueName}`,
1371
- availableTopics
1372
- },
1373
- { status: 404 }
1374
- );
1425
+ );
1426
+ if (!response.ok) {
1427
+ const errorText = await response.text();
1428
+ if (response.status === 404) {
1429
+ throw new MessageNotFoundError(receiptHandle);
1375
1430
  }
1376
- const consumerGroupHandler = topicHandler[consumerGroup];
1377
- if (!consumerGroupHandler) {
1378
- const availableGroups = Object.keys(topicHandler).join(", ");
1379
- return Response.json(
1380
- {
1381
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1382
- availableGroups
1383
- },
1384
- { 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"
1385
1435
  );
1386
1436
  }
1387
- const topic = new Topic(client, queueName);
1388
- const cg = topic.consumerGroup(
1389
- consumerGroup,
1390
- visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : void 0
1391
- );
1392
- await cg.consume(consumerGroupHandler, { messageId });
1393
- return Response.json({ status: "success" });
1394
- } catch (error) {
1395
- console.error("Queue callback error:", error);
1396
- 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"))) {
1397
- return Response.json({ error: error.message }, { status: 400 });
1398
- }
1399
- return Response.json(
1400
- { error: "Failed to process queue message" },
1401
- { status: 500 }
1437
+ throwCommonHttpError(
1438
+ response.status,
1439
+ response.statusText,
1440
+ errorText,
1441
+ "change visibility (alt)",
1442
+ "Missing receipt handle or invalid visibility timeout"
1402
1443
  );
1403
1444
  }
1404
- };
1405
- return routeHandler;
1406
- }
1407
- function handleCallback(handlers, options) {
1408
- return createCallbackHandler(
1409
- handlers,
1410
- options?.client || new QueueClient(),
1411
- options?.visibilityTimeoutSeconds
1412
- );
1413
- }
1445
+ return { success: true };
1446
+ }
1447
+ };
1414
1448
 
1415
1449
  // src/factory.ts
1416
1450
  async function send(topicName, payload, options) {
1417
- const transport = options?.transport || new JsonTransport();
1418
1451
  const client = options?.client || new QueueClient();
1419
- const result = await client.sendMessage(
1420
- {
1421
- queueName: topicName,
1422
- payload,
1423
- idempotencyKey: options?.idempotencyKey,
1424
- retentionSeconds: options?.retentionSeconds,
1425
- delaySeconds: options?.delaySeconds,
1426
- headers: options?.headers
1427
- },
1428
- transport
1429
- );
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
+ });
1430
1460
  if (isDevMode()) {
1431
1461
  triggerDevCallbacks(topicName, result.messageId, options?.delaySeconds);
1432
1462
  }
1433
1463
  return { messageId: result.messageId };
1434
1464
  }
1435
1465
  async function receive(topicName, consumerGroup, handler, options) {
1436
- const transport = options?.transport || new JsonTransport();
1437
1466
  const client = options?.client || new QueueClient();
1438
- const topic = new Topic(client, topicName, transport);
1439
- const { messageId, client: _, ...consumerGroupOptions } = options || {};
1440
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1441
- if (messageId) {
1442
- return consumer.consume(handler, { messageId });
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 });
1443
1476
  } else {
1444
- return consumer.consume(handler);
1477
+ const limit = options && "limit" in options ? options.limit : void 0;
1478
+ return consumer.consume(handler, limit !== void 0 ? { limit } : {});
1445
1479
  }
1446
1480
  }
1447
-
1448
- // src/queue-client.ts
1449
- var Client = class {
1450
- client;
1451
- /**
1452
- * Create a new Client
1453
- * @param options QueueClient configuration options
1454
- */
1455
- constructor(options = {}) {
1456
- this.client = new QueueClient(options);
1457
- }
1458
- /**
1459
- * Send a message to a topic
1460
- * @param topicName Name of the topic to send to
1461
- * @param payload The data to send
1462
- * @param options Optional publish options and transport
1463
- * @returns Promise with the message ID
1464
- * @throws {BadRequestError} When request parameters are invalid
1465
- * @throws {UnauthorizedError} When authentication fails
1466
- * @throws {ForbiddenError} When access is denied (environment mismatch)
1467
- * @throws {InternalServerError} When server encounters an error
1468
- */
1469
- async send(topicName, payload, options) {
1470
- return send(topicName, payload, {
1471
- ...options,
1472
- client: this.client
1473
- });
1474
- }
1475
- /**
1476
- * Create a callback handler for processing queue messages.
1477
- * Returns a Next.js route handler function that routes messages to appropriate handlers.
1478
- *
1479
- * @param handlers - Object with topic-specific handlers organized by consumer groups
1480
- * @param options - Optional configuration
1481
- * @param options.visibilityTimeoutSeconds - Message lock duration (default: 30, max: 3600)
1482
- * @returns A Next.js route handler function
1483
- *
1484
- * @example
1485
- * ```typescript
1486
- * // Basic usage
1487
- * export const POST = client.handleCallback({
1488
- * "user-events": {
1489
- * "welcome": (user, metadata) => console.log("Welcoming user", user),
1490
- * "analytics": (user, metadata) => console.log("Tracking user", user),
1491
- * },
1492
- * });
1493
- *
1494
- * // With custom visibility timeout
1495
- * export const POST = client.handleCallback({
1496
- * "video-processing": {
1497
- * "transcode": async (video) => await transcodeVideo(video),
1498
- * },
1499
- * }, {
1500
- * visibilityTimeoutSeconds: 300, // 5 minutes for long operations
1501
- * });
1502
- * ```
1503
- */
1504
- handleCallback(handlers, options) {
1505
- return handleCallback(handlers, {
1506
- ...options,
1507
- client: this.client
1508
- });
1509
- }
1510
- };
1511
1481
  export {
1512
1482
  BadRequestError,
1513
1483
  BufferTransport,
1514
- Client,
1515
- ConcurrencyLimitError,
1484
+ CLOUD_EVENT_TYPE_V1BETA,
1485
+ CLOUD_EVENT_TYPE_V2BETA,
1516
1486
  ConsumerDiscoveryError,
1517
1487
  ConsumerRegistryNotConfiguredError,
1518
1488
  DuplicateMessageError,
@@ -1525,11 +1495,13 @@ export {
1525
1495
  MessageLockedError,
1526
1496
  MessageNotAvailableError,
1527
1497
  MessageNotFoundError,
1498
+ QueueClient,
1528
1499
  QueueEmptyError,
1529
1500
  StreamTransport,
1530
1501
  UnauthorizedError,
1531
1502
  handleCallback,
1532
1503
  parseCallback,
1504
+ parseRawCallback,
1533
1505
  receive,
1534
1506
  send
1535
1507
  };