@vercel/queue 0.0.0-alpha.8 → 0.0.1

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.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -22,45 +32,66 @@ var index_exports = {};
22
32
  __export(index_exports, {
23
33
  BadRequestError: () => BadRequestError,
24
34
  BufferTransport: () => BufferTransport,
35
+ CLOUD_EVENT_TYPE_V1BETA: () => CLOUD_EVENT_TYPE_V1BETA,
36
+ CLOUD_EVENT_TYPE_V2BETA: () => CLOUD_EVENT_TYPE_V2BETA,
37
+ ConsumerDiscoveryError: () => ConsumerDiscoveryError,
38
+ ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
39
+ DuplicateMessageError: () => DuplicateMessageError,
25
40
  ForbiddenError: () => ForbiddenError,
26
41
  InternalServerError: () => InternalServerError,
27
42
  InvalidLimitError: () => InvalidLimitError,
28
43
  JsonTransport: () => JsonTransport,
44
+ MessageAlreadyProcessedError: () => MessageAlreadyProcessedError,
29
45
  MessageCorruptedError: () => MessageCorruptedError,
30
46
  MessageLockedError: () => MessageLockedError,
31
47
  MessageNotAvailableError: () => MessageNotAvailableError,
32
48
  MessageNotFoundError: () => MessageNotFoundError,
49
+ QueueClient: () => QueueClient,
33
50
  QueueEmptyError: () => QueueEmptyError,
34
51
  StreamTransport: () => StreamTransport,
35
52
  UnauthorizedError: () => UnauthorizedError,
36
- handleCallback: () => handleCallback,
37
- receive: () => receive,
38
- send: () => send
53
+ parseCallback: () => parseCallback,
54
+ parseRawCallback: () => parseRawCallback
39
55
  });
40
56
  module.exports = __toCommonJS(index_exports);
41
57
 
42
58
  // src/transports.ts
59
+ async function streamToBuffer(stream) {
60
+ let totalLength = 0;
61
+ const reader = stream.getReader();
62
+ const chunks = [];
63
+ try {
64
+ while (true) {
65
+ const { done, value } = await reader.read();
66
+ if (done) break;
67
+ chunks.push(value);
68
+ totalLength += value.length;
69
+ }
70
+ } finally {
71
+ reader.releaseLock();
72
+ }
73
+ return Buffer.concat(chunks, totalLength);
74
+ }
43
75
  var JsonTransport = class {
44
76
  contentType = "application/json";
77
+ replacer;
78
+ reviver;
79
+ /**
80
+ * Create a new JsonTransport.
81
+ * @param options - Optional JSON serialization options
82
+ * @param options.replacer - Custom replacer for JSON.stringify
83
+ * @param options.reviver - Custom reviver for JSON.parse
84
+ */
85
+ constructor(options = {}) {
86
+ this.replacer = options.replacer;
87
+ this.reviver = options.reviver;
88
+ }
45
89
  serialize(value) {
46
- return Buffer.from(JSON.stringify(value), "utf8");
90
+ return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
47
91
  }
48
92
  async deserialize(stream) {
49
- const reader = stream.getReader();
50
- let totalLength = 0;
51
- const chunks = [];
52
- try {
53
- while (true) {
54
- const { done, value } = await reader.read();
55
- if (done) break;
56
- chunks.push(value);
57
- totalLength += value.length;
58
- }
59
- } finally {
60
- reader.releaseLock();
61
- }
62
- const buffer = Buffer.concat(chunks, totalLength);
63
- return JSON.parse(buffer.toString("utf8"));
93
+ const buffer = await streamToBuffer(stream);
94
+ return JSON.parse(buffer.toString("utf8"), this.reviver);
64
95
  }
65
96
  };
66
97
  var BufferTransport = class {
@@ -69,25 +100,7 @@ var BufferTransport = class {
69
100
  return value;
70
101
  }
71
102
  async deserialize(stream) {
72
- const reader = stream.getReader();
73
- const chunks = [];
74
- try {
75
- while (true) {
76
- const { done, value } = await reader.read();
77
- if (done) break;
78
- chunks.push(value);
79
- }
80
- } finally {
81
- reader.releaseLock();
82
- }
83
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
84
- const buffer = new Uint8Array(totalLength);
85
- let offset = 0;
86
- for (const chunk of chunks) {
87
- buffer.set(chunk, offset);
88
- offset += chunk.length;
89
- }
90
- return Buffer.from(buffer);
103
+ return await streamToBuffer(stream);
91
104
  }
92
105
  };
93
106
  var StreamTransport = class {
@@ -98,6 +111,10 @@ var StreamTransport = class {
98
111
  async deserialize(stream) {
99
112
  return stream;
100
113
  }
114
+ /**
115
+ * Consume any remaining stream data to prevent resource leaks.
116
+ * Called automatically by ConsumerGroup; manual call required for direct client usage.
117
+ */
101
118
  async finalize(payload) {
102
119
  const reader = payload.getReader();
103
120
  try {
@@ -111,9 +128,13 @@ var StreamTransport = class {
111
128
  }
112
129
  };
113
130
 
114
- // src/client.ts
131
+ // src/api-client.ts
115
132
  var import_mixpart = require("mixpart");
116
133
 
134
+ // src/dev.ts
135
+ var fs = __toESM(require("fs"));
136
+ var path = __toESM(require("path"));
137
+
117
138
  // src/types.ts
118
139
  var MessageNotFoundError = class extends Error {
119
140
  constructor(messageId) {
@@ -144,6 +165,7 @@ var QueueEmptyError = class extends Error {
144
165
  }
145
166
  };
146
167
  var MessageLockedError = class extends Error {
168
+ /** Suggested retry delay in seconds, if provided by the server. */
147
169
  retryAfter;
148
170
  constructor(messageId, retryAfter) {
149
171
  const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
@@ -182,228 +204,1089 @@ var InvalidLimitError = class extends Error {
182
204
  this.name = "InvalidLimitError";
183
205
  }
184
206
  };
185
-
186
- // src/client.ts
187
- async function consumeStream(stream) {
188
- const reader = stream.getReader();
189
- try {
190
- while (true) {
191
- const { done } = await reader.read();
192
- if (done) break;
193
- }
194
- } finally {
195
- reader.releaseLock();
207
+ var MessageAlreadyProcessedError = class extends Error {
208
+ constructor(messageId) {
209
+ super(`Message ${messageId} has already been processed`);
210
+ this.name = "MessageAlreadyProcessedError";
196
211
  }
197
- }
198
- function parseQueueHeaders(headers) {
199
- const messageId = headers.get("Vqs-Message-Id");
200
- const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
201
- const timestamp = headers.get("Vqs-Timestamp");
202
- const contentType = headers.get("Content-Type") || "application/octet-stream";
203
- const ticket = headers.get("Vqs-Ticket");
204
- if (!messageId || !timestamp || !ticket) {
205
- return null;
212
+ };
213
+ var DuplicateMessageError = class extends Error {
214
+ idempotencyKey;
215
+ constructor(message, idempotencyKey) {
216
+ super(message);
217
+ this.name = "DuplicateMessageError";
218
+ this.idempotencyKey = idempotencyKey;
206
219
  }
207
- const deliveryCount = parseInt(deliveryCountStr, 10);
208
- if (isNaN(deliveryCount)) {
209
- return null;
220
+ };
221
+ var ConsumerDiscoveryError = class extends Error {
222
+ deploymentId;
223
+ constructor(message, deploymentId) {
224
+ super(message);
225
+ this.name = "ConsumerDiscoveryError";
226
+ this.deploymentId = deploymentId;
210
227
  }
211
- return {
212
- messageId,
213
- deliveryCount,
214
- timestamp,
215
- contentType,
216
- ticket
217
- };
228
+ };
229
+ var ConsumerRegistryNotConfiguredError = class extends Error {
230
+ constructor(message = "Consumer registry not configured") {
231
+ super(message);
232
+ this.name = "ConsumerRegistryNotConfiguredError";
233
+ }
234
+ };
235
+
236
+ // src/consumer-group.ts
237
+ var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
238
+ var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
239
+ var MAX_RENEWAL_INTERVAL_SECONDS = 60;
240
+ var MIN_RENEWAL_INTERVAL_SECONDS = 10;
241
+ var RETRY_INTERVAL_MS = 3e3;
242
+ function calculateRenewalInterval(visibilityTimeoutSeconds) {
243
+ return Math.min(
244
+ MAX_RENEWAL_INTERVAL_SECONDS,
245
+ Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
246
+ );
218
247
  }
219
- var QueueClient = class _QueueClient {
220
- baseUrl;
221
- token;
248
+ var ConsumerGroup = class {
249
+ client;
250
+ topicName;
251
+ consumerGroupName;
252
+ visibilityTimeout;
222
253
  /**
223
- * Internal default instance for use by convenience functions
224
- * @internal
254
+ * Create a new ConsumerGroup instance.
255
+ *
256
+ * @param client - ApiClient instance to use for API calls (transport is configured on the client)
257
+ * @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
258
+ * @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
259
+ * @param options - Optional configuration
260
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
225
261
  */
226
- static _defaultInstance = null;
262
+ constructor(client, topicName, consumerGroupName, options = {}) {
263
+ this.client = client;
264
+ this.topicName = topicName;
265
+ this.consumerGroupName = consumerGroupName;
266
+ this.visibilityTimeout = Math.max(
267
+ MIN_VISIBILITY_TIMEOUT_SECONDS,
268
+ options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
269
+ );
270
+ }
227
271
  /**
228
- * Create a new Vercel Queue Service client
229
- * @param options Client configuration options (optional - will auto-detect Vercel Function environment)
272
+ * Check if an error is a 4xx client error that should stop retries.
273
+ * 4xx errors indicate the request is fundamentally invalid and retrying won't help.
274
+ * - 409: Ticket mismatch (lost ownership to another consumer)
275
+ * - 404: Message/receipt handle not found
276
+ * - 400, 401, 403: Other client errors
230
277
  */
231
- constructor(options = {}) {
232
- this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
233
- if (options.token) {
234
- this.token = options.token;
235
- } else {
236
- const token = this.getVercelOidcTokenSync();
237
- if (!token) {
238
- throw new Error(
239
- "Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment, or provide a token explicitly.\n\nTo set up your environment:\n1. Link your project: 'vercel link'\n2. Pull environment variables: 'vercel env pull'\n3. Run with environment: 'dotenv -e .env.local -- your-command'"
240
- );
241
- }
242
- this.token = token;
243
- }
278
+ isClientError(error) {
279
+ return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
280
+ error instanceof MessageNotFoundError || // 404 - receipt handle not found
281
+ error instanceof BadRequestError || // 400 - invalid parameters
282
+ error instanceof UnauthorizedError || // 401 - auth failed
283
+ error instanceof ForbiddenError;
244
284
  }
245
285
  /**
246
- * Get the default client instance for internal use by convenience functions
247
- * @internal
286
+ * Starts a background loop that periodically extends the visibility timeout for a message.
287
+ *
288
+ * Timing strategy:
289
+ * - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
290
+ * - Extensions request the same duration as the initial visibility timeout
291
+ * - When `visibilityDeadline` is provided (binary mode small body), the first
292
+ * extension delay is calculated from the time remaining until the deadline
293
+ * using the same renewal formula, ensuring the first extension fires before
294
+ * the server-assigned lease expires. Subsequent renewals use the standard interval.
295
+ *
296
+ * Retry strategy:
297
+ * - On transient failures (5xx, network errors): retry every 3 seconds
298
+ * - On 4xx client errors: stop retrying (the lease is lost or invalid)
299
+ *
300
+ * @param receiptHandle - The receipt handle to extend visibility for
301
+ * @param options - Optional configuration
302
+ * @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
303
+ * when the current visibility timeout expires. Used to calculate the first extension delay.
248
304
  */
249
- static _getDefaultInstance() {
250
- if (!this._defaultInstance) {
251
- this._defaultInstance = new _QueueClient();
305
+ startVisibilityExtension(receiptHandle, options) {
306
+ let isRunning = true;
307
+ let isResolved = false;
308
+ let resolveLifecycle;
309
+ let timeoutId = null;
310
+ const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
311
+ let firstDelayMs = renewalIntervalMs;
312
+ if (options?.visibilityDeadline) {
313
+ const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
314
+ if (timeRemainingMs > 0) {
315
+ const timeRemainingSeconds = timeRemainingMs / 1e3;
316
+ firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
317
+ } else {
318
+ firstDelayMs = 0;
319
+ }
252
320
  }
253
- return this._defaultInstance;
321
+ const lifecyclePromise = new Promise((resolve) => {
322
+ resolveLifecycle = resolve;
323
+ });
324
+ const safeResolve = () => {
325
+ if (!isResolved) {
326
+ isResolved = true;
327
+ resolveLifecycle();
328
+ }
329
+ };
330
+ const extend = async () => {
331
+ if (!isRunning) {
332
+ safeResolve();
333
+ return;
334
+ }
335
+ try {
336
+ await this.client.changeVisibility({
337
+ queueName: this.topicName,
338
+ consumerGroup: this.consumerGroupName,
339
+ receiptHandle,
340
+ visibilityTimeoutSeconds: this.visibilityTimeout
341
+ });
342
+ if (isRunning) {
343
+ timeoutId = setTimeout(() => extend(), renewalIntervalMs);
344
+ } else {
345
+ safeResolve();
346
+ }
347
+ } catch (error) {
348
+ if (this.isClientError(error)) {
349
+ console.error(
350
+ `Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
351
+ error
352
+ );
353
+ safeResolve();
354
+ return;
355
+ }
356
+ console.error(
357
+ `Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
358
+ error
359
+ );
360
+ if (isRunning) {
361
+ timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
362
+ } else {
363
+ safeResolve();
364
+ }
365
+ }
366
+ };
367
+ timeoutId = setTimeout(() => extend(), firstDelayMs);
368
+ return async (waitForCompletion = false) => {
369
+ isRunning = false;
370
+ if (timeoutId) {
371
+ clearTimeout(timeoutId);
372
+ timeoutId = null;
373
+ }
374
+ if (waitForCompletion) {
375
+ await lifecyclePromise;
376
+ } else {
377
+ safeResolve();
378
+ }
379
+ };
254
380
  }
255
381
  /**
256
- * Synchronously get OIDC token from environment
257
- * Used internally by constructor - mirrors the logic from getVercelOidcToken but synchronously
382
+ * Clean up the message payload if the transport supports it and payload exists.
258
383
  */
259
- getVercelOidcTokenSync() {
384
+ async finalizePayload(payload) {
385
+ const transport = this.client.getTransport();
386
+ if (transport.finalize && payload !== void 0 && payload !== null) {
387
+ try {
388
+ await transport.finalize(payload);
389
+ } catch (finalizeError) {
390
+ console.warn("Failed to finalize message payload:", finalizeError);
391
+ }
392
+ }
393
+ }
394
+ async processMessage(message, handler, options) {
395
+ const stopExtension = this.startVisibilityExtension(
396
+ message.receiptHandle,
397
+ options
398
+ );
399
+ const metadata = {
400
+ messageId: message.messageId,
401
+ deliveryCount: message.deliveryCount,
402
+ createdAt: message.createdAt,
403
+ expiresAt: message.expiresAt,
404
+ topicName: this.topicName,
405
+ consumerGroup: this.consumerGroupName,
406
+ region: this.client.getRegion()
407
+ };
260
408
  try {
261
- const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
262
- const fromSymbol = globalThis;
263
- const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
264
- const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
265
- return token || null;
266
- } catch {
267
- return null;
409
+ await handler(message.payload, metadata);
410
+ await stopExtension();
411
+ await this.client.acknowledgeMessage({
412
+ queueName: this.topicName,
413
+ consumerGroup: this.consumerGroupName,
414
+ receiptHandle: message.receiptHandle
415
+ });
416
+ } catch (error) {
417
+ await stopExtension();
418
+ if (options?.retry) {
419
+ let directive;
420
+ try {
421
+ directive = options.retry(error, metadata);
422
+ } catch (retryError) {
423
+ console.warn("retry handler threw:", retryError);
424
+ }
425
+ if (directive) {
426
+ if ("acknowledge" in directive && directive.acknowledge) {
427
+ try {
428
+ await this.client.acknowledgeMessage({
429
+ queueName: this.topicName,
430
+ consumerGroup: this.consumerGroupName,
431
+ receiptHandle: message.receiptHandle
432
+ });
433
+ } catch (ackError) {
434
+ console.warn("Failed to acknowledge message:", ackError);
435
+ }
436
+ await this.finalizePayload(message.payload);
437
+ return;
438
+ }
439
+ if ("afterSeconds" in directive && typeof directive.afterSeconds === "number") {
440
+ try {
441
+ await this.client.changeVisibility({
442
+ queueName: this.topicName,
443
+ consumerGroup: this.consumerGroupName,
444
+ receiptHandle: message.receiptHandle,
445
+ visibilityTimeoutSeconds: directive.afterSeconds
446
+ });
447
+ } catch (changeError) {
448
+ console.warn(
449
+ "Failed to reschedule message for retry:",
450
+ changeError
451
+ );
452
+ }
453
+ await this.finalizePayload(message.payload);
454
+ return;
455
+ }
456
+ }
457
+ }
458
+ await this.finalizePayload(message.payload);
459
+ throw error;
268
460
  }
269
461
  }
270
462
  /**
271
- * Send a message to a queue
272
- * @param options Send message options
273
- * @param transport Serializer/deserializer for the payload
274
- * @returns Promise with the message ID
275
- * @throws {BadRequestError} When request parameters are invalid
276
- * @throws {UnauthorizedError} When authentication fails
277
- * @throws {ForbiddenError} When access is denied (environment mismatch)
278
- * @throws {InternalServerError} When server encounters an error
463
+ * Process a pre-fetched message directly, without calling `receiveMessageById`.
464
+ *
465
+ * Used by the binary mode (v2beta) small body fast path, where the server
466
+ * pushes the full message payload in the callback request. The message is
467
+ * processed with the same lifecycle guarantees as `consume()`:
468
+ * - Visibility timeout is extended periodically during processing
469
+ * - Message is acknowledged on successful handler completion
470
+ * - Payload is finalized on error if the transport supports it
471
+ *
472
+ * @param handler - Function to process the message payload and metadata
473
+ * @param message - The complete message including payload and receipt handle
474
+ * @param options - Optional configuration
475
+ * @param options.visibilityDeadline - Absolute deadline when the server-assigned
476
+ * visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
477
+ * schedule the first visibility extension before the lease expires.
279
478
  */
280
- async sendMessage(options, transport) {
281
- const { queueName, payload, idempotencyKey, retentionSeconds } = options;
282
- const headers = new Headers({
283
- Authorization: `Bearer ${this.token}`,
284
- "Vqs-Queue-Name": queueName,
285
- "Content-Type": transport.contentType
286
- });
287
- if (process.env.VERCEL_DEPLOYMENT_ID) {
288
- headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
289
- }
290
- if (idempotencyKey) {
291
- headers.set("Vqs-Idempotency-Key", idempotencyKey);
292
- }
293
- if (retentionSeconds !== void 0) {
294
- headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
295
- }
296
- const body = transport.serialize(payload);
297
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
298
- method: "POST",
299
- headers,
300
- body
301
- });
302
- if (!response.ok) {
303
- if (response.status === 400) {
304
- const errorText = await response.text();
305
- throw new BadRequestError(errorText || "Invalid parameters");
306
- }
307
- if (response.status === 401) {
308
- throw new UnauthorizedError();
309
- }
310
- if (response.status === 403) {
311
- throw new ForbiddenError();
312
- }
313
- if (response.status === 409) {
314
- throw new Error("Duplicate idempotency key detected");
315
- }
316
- if (response.status >= 500) {
317
- throw new InternalServerError(
318
- `Server error: ${response.status} ${response.statusText}`
319
- );
479
+ async consumeMessage(handler, message, options) {
480
+ await this.processMessage(message, handler, options);
481
+ }
482
+ async consume(handler, options) {
483
+ const retry = options?.retry;
484
+ if (options && "messageId" in options) {
485
+ const response = await this.client.receiveMessageById({
486
+ queueName: this.topicName,
487
+ consumerGroup: this.consumerGroupName,
488
+ messageId: options.messageId,
489
+ visibilityTimeoutSeconds: this.visibilityTimeout
490
+ });
491
+ await this.processMessage(response.message, handler, { retry });
492
+ return 1;
493
+ } else {
494
+ const limit = options && "limit" in options ? options.limit : 1;
495
+ let messagesProcessed = 0;
496
+ for await (const message of this.client.receiveMessages({
497
+ queueName: this.topicName,
498
+ consumerGroup: this.consumerGroupName,
499
+ visibilityTimeoutSeconds: this.visibilityTimeout,
500
+ limit
501
+ })) {
502
+ messagesProcessed++;
503
+ await this.processMessage(message, handler, { retry });
320
504
  }
321
- throw new Error(
322
- `Failed to send message: ${response.status} ${response.statusText}`
323
- );
505
+ return messagesProcessed;
324
506
  }
325
- const responseData = await response.json();
326
- return responseData;
327
507
  }
328
508
  /**
329
- * Receive messages from a queue
330
- * @param options Receive messages options
331
- * @param transport Serializer/deserializer for the payload
332
- * @returns AsyncGenerator that yields messages as they arrive
333
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
334
- * @throws {QueueEmptyError} When no messages are available (204)
335
- * @throws {MessageLockedError} When messages are temporarily locked (423)
509
+ * Get the consumer group name
510
+ */
511
+ get name() {
512
+ return this.consumerGroupName;
513
+ }
514
+ /**
515
+ * Get the topic name this consumer group is subscribed to
516
+ */
517
+ get topic() {
518
+ return this.topicName;
519
+ }
520
+ };
521
+
522
+ // src/topic.ts
523
+ var Topic = class {
524
+ client;
525
+ topicName;
526
+ /**
527
+ * @param client ApiClient instance to use for API calls
528
+ * @param topicName Name of the topic to work with
529
+ */
530
+ constructor(client, topicName) {
531
+ this.client = client;
532
+ this.topicName = topicName;
533
+ }
534
+ /**
535
+ * Publish a message to the topic
536
+ * @param payload The data to publish
537
+ * @param options Optional publish options
538
+ * @returns `{ messageId }` — `messageId` is `null` when deferred
336
539
  * @throws {BadRequestError} When request parameters are invalid
337
540
  * @throws {UnauthorizedError} When authentication fails
338
541
  * @throws {ForbiddenError} When access is denied (environment mismatch)
339
542
  * @throws {InternalServerError} When server encounters an error
340
543
  */
341
- async *receiveMessages(options, transport) {
342
- const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
343
- if (limit !== void 0 && (limit < 1 || limit > 10)) {
344
- throw new InvalidLimitError(limit);
345
- }
346
- const headers = new Headers({
347
- Authorization: `Bearer ${this.token}`,
348
- "Vqs-Queue-Name": queueName,
349
- "Vqs-Consumer-Group": consumerGroup,
350
- Accept: "multipart/mixed"
351
- });
352
- if (visibilityTimeoutSeconds !== void 0) {
353
- headers.set(
354
- "Vqs-Visibility-Timeout",
355
- visibilityTimeoutSeconds.toString()
356
- );
357
- }
358
- if (limit !== void 0) {
359
- headers.set("Vqs-Limit", limit.toString());
360
- }
361
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
362
- method: "GET",
363
- headers
544
+ async publish(payload, options) {
545
+ const result = await this.client.sendMessage({
546
+ queueName: this.topicName,
547
+ payload,
548
+ idempotencyKey: options?.idempotencyKey,
549
+ retentionSeconds: options?.retentionSeconds,
550
+ delaySeconds: options?.delaySeconds,
551
+ headers: options?.headers
364
552
  });
365
- if (response.status === 204) {
366
- throw new QueueEmptyError(queueName, consumerGroup);
367
- }
368
- if (!response.ok) {
369
- if (response.status === 400) {
370
- const errorText = await response.text();
371
- throw new BadRequestError(errorText || "Invalid parameters");
372
- }
373
- if (response.status === 401) {
374
- throw new UnauthorizedError();
375
- }
376
- if (response.status === 403) {
377
- throw new ForbiddenError();
378
- }
379
- if (response.status === 423) {
380
- const retryAfterHeader = response.headers.get("Retry-After");
381
- let retryAfter;
382
- if (retryAfterHeader) {
383
- const parsed = parseInt(retryAfterHeader, 10);
384
- retryAfter = isNaN(parsed) ? void 0 : parsed;
385
- }
386
- throw new MessageLockedError("next message", retryAfter);
387
- }
388
- if (response.status >= 500) {
389
- throw new InternalServerError(
390
- `Server error: ${response.status} ${response.statusText}`
391
- );
392
- }
393
- throw new Error(
394
- `Failed to receive messages: ${response.status} ${response.statusText}`
553
+ if (result.messageId && isDevMode()) {
554
+ triggerDevCallbacks(
555
+ this.topicName,
556
+ result.messageId,
557
+ this.client.getRegion()
395
558
  );
396
559
  }
397
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
398
- try {
399
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
400
- if (!parsedHeaders) {
401
- console.warn("Missing required queue headers in multipart part");
402
- await consumeStream(multipartMessage.payload);
403
- continue;
404
- }
405
- const deserializedPayload = await transport.deserialize(
406
- multipartMessage.payload
560
+ return { messageId: result.messageId };
561
+ }
562
+ /**
563
+ * Create a consumer group for this topic
564
+ * @param consumerGroupName Name of the consumer group
565
+ * @param options Optional configuration for the consumer group
566
+ * @returns A ConsumerGroup instance
567
+ */
568
+ consumerGroup(consumerGroupName, options) {
569
+ return new ConsumerGroup(
570
+ this.client,
571
+ this.topicName,
572
+ consumerGroupName,
573
+ options
574
+ );
575
+ }
576
+ /**
577
+ * Get the topic name
578
+ */
579
+ get name() {
580
+ return this.topicName;
581
+ }
582
+ };
583
+
584
+ // src/callback.ts
585
+ var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
586
+ var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
587
+ function matchesWildcardPattern(topicName, pattern) {
588
+ const prefix = pattern.slice(0, -1);
589
+ return topicName.startsWith(prefix);
590
+ }
591
+ function isRecord(value) {
592
+ return typeof value === "object" && value !== null;
593
+ }
594
+ function parseV1StructuredBody(body, contentType) {
595
+ if (!contentType || !contentType.includes("application/cloudevents+json")) {
596
+ throw new Error(
597
+ "Invalid content type: expected 'application/cloudevents+json'"
598
+ );
599
+ }
600
+ if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
601
+ throw new Error("Invalid CloudEvent: missing required fields");
602
+ }
603
+ if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
604
+ throw new Error(
605
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
606
+ );
607
+ }
608
+ const { data } = body;
609
+ const missingFields = [];
610
+ if (!("queueName" in data)) missingFields.push("queueName");
611
+ if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
612
+ if (!("messageId" in data)) missingFields.push("messageId");
613
+ if (missingFields.length > 0) {
614
+ throw new Error(
615
+ `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
616
+ );
617
+ }
618
+ return {
619
+ queueName: String(data.queueName),
620
+ consumerGroup: String(data.consumerGroup),
621
+ messageId: String(data.messageId)
622
+ };
623
+ }
624
+ function getHeader(headers, name) {
625
+ if (headers instanceof Headers) {
626
+ return headers.get(name);
627
+ }
628
+ const value = headers[name];
629
+ if (Array.isArray(value)) return value[0] ?? null;
630
+ return value ?? null;
631
+ }
632
+ function parseBinaryHeaders(headers) {
633
+ const ceType = getHeader(headers, "ce-type");
634
+ if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
635
+ throw new Error(
636
+ `Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
637
+ );
638
+ }
639
+ const queueName = getHeader(headers, "ce-vqsqueuename");
640
+ const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
641
+ const messageId = getHeader(headers, "ce-vqsmessageid");
642
+ const missingFields = [];
643
+ if (!queueName) missingFields.push("ce-vqsqueuename");
644
+ if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
645
+ if (!messageId) missingFields.push("ce-vqsmessageid");
646
+ if (missingFields.length > 0) {
647
+ throw new Error(
648
+ `Missing required CloudEvent headers: ${missingFields.join(", ")}`
649
+ );
650
+ }
651
+ const region = getHeader(headers, "ce-vqsregion") ?? void 0;
652
+ const base = {
653
+ queueName,
654
+ consumerGroup,
655
+ messageId,
656
+ region
657
+ };
658
+ const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
659
+ if (!receiptHandle) {
660
+ return base;
661
+ }
662
+ const result = { ...base, receiptHandle };
663
+ const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
664
+ if (deliveryCount) {
665
+ result.deliveryCount = parseInt(deliveryCount, 10);
666
+ }
667
+ const createdAt = getHeader(headers, "ce-vqscreatedat");
668
+ if (createdAt) {
669
+ result.createdAt = createdAt;
670
+ }
671
+ const expiresAt = getHeader(headers, "ce-vqsexpiresat");
672
+ if (expiresAt) {
673
+ result.expiresAt = expiresAt;
674
+ }
675
+ const contentType = getHeader(headers, "content-type");
676
+ if (contentType) {
677
+ result.contentType = contentType;
678
+ }
679
+ const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
680
+ if (visibilityDeadline) {
681
+ result.visibilityDeadline = visibilityDeadline;
682
+ }
683
+ return result;
684
+ }
685
+ function parseRawCallback(body, headers) {
686
+ const ceType = getHeader(headers, "ce-type");
687
+ if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
688
+ const result = parseBinaryHeaders(headers);
689
+ if ("receiptHandle" in result) {
690
+ result.parsedPayload = body;
691
+ }
692
+ return result;
693
+ }
694
+ return parseV1StructuredBody(body, getHeader(headers, "content-type"));
695
+ }
696
+ async function parseCallback(request) {
697
+ const ceType = request.headers.get("ce-type");
698
+ if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
699
+ const result = parseBinaryHeaders(request.headers);
700
+ if ("receiptHandle" in result && request.body) {
701
+ result.rawBody = request.body;
702
+ }
703
+ return result;
704
+ }
705
+ let body;
706
+ try {
707
+ body = await request.json();
708
+ } catch {
709
+ throw new Error("Failed to parse CloudEvent from request body");
710
+ }
711
+ const headers = {};
712
+ request.headers.forEach((value, key) => {
713
+ headers[key] = value;
714
+ });
715
+ return parseRawCallback(body, headers);
716
+ }
717
+ async function handleCallback(handler, request, options) {
718
+ const { queueName, consumerGroup, messageId } = request;
719
+ if (!options?.client) {
720
+ throw new Error("HandleCallbackOptions.client is required");
721
+ }
722
+ let api = getApiClient(options.client);
723
+ if (request.region) {
724
+ api = api.withRegion(request.region);
725
+ }
726
+ const topic = new Topic(api, queueName);
727
+ const cg = topic.consumerGroup(
728
+ consumerGroup,
729
+ options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
730
+ );
731
+ if ("receiptHandle" in request) {
732
+ const transport = api.getTransport();
733
+ let payload;
734
+ if (request.rawBody) {
735
+ payload = await transport.deserialize(request.rawBody);
736
+ } else if (request.parsedPayload !== void 0) {
737
+ payload = request.parsedPayload;
738
+ } else {
739
+ throw new Error(
740
+ "Binary mode callback with receipt handle is missing payload"
741
+ );
742
+ }
743
+ const message = {
744
+ messageId,
745
+ payload,
746
+ deliveryCount: request.deliveryCount ?? 1,
747
+ createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
748
+ expiresAt: request.expiresAt ? new Date(request.expiresAt) : void 0,
749
+ contentType: request.contentType ?? transport.contentType,
750
+ receiptHandle: request.receiptHandle
751
+ };
752
+ const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
753
+ await cg.consumeMessage(handler, message, {
754
+ visibilityDeadline,
755
+ retry: options?.retry
756
+ });
757
+ } else {
758
+ await cg.consume(handler, { messageId, retry: options?.retry });
759
+ }
760
+ }
761
+
762
+ // src/dev.ts
763
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
764
+ function filePathToUrlPath(filePath) {
765
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
766
+ if (!urlPath.startsWith("/")) {
767
+ urlPath = "/" + urlPath;
768
+ }
769
+ return urlPath;
770
+ }
771
+ function filePathToConsumerGroup(filePath) {
772
+ return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
773
+ }
774
+ function getDevRouteMappings() {
775
+ const g = globalThis;
776
+ if (ROUTE_MAPPINGS_KEY in g) {
777
+ return g[ROUTE_MAPPINGS_KEY] ?? null;
778
+ }
779
+ try {
780
+ const vercelJsonPath = path.join(process.cwd(), "vercel.json");
781
+ if (!fs.existsSync(vercelJsonPath)) {
782
+ g[ROUTE_MAPPINGS_KEY] = null;
783
+ return null;
784
+ }
785
+ const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
786
+ if (!vercelJson.functions) {
787
+ g[ROUTE_MAPPINGS_KEY] = null;
788
+ return null;
789
+ }
790
+ const mappings = [];
791
+ for (const [filePath, config] of Object.entries(vercelJson.functions)) {
792
+ if (!config.experimentalTriggers) continue;
793
+ for (const trigger of config.experimentalTriggers) {
794
+ if (trigger.type?.startsWith("queue/") && trigger.topic) {
795
+ mappings.push({
796
+ urlPath: filePathToUrlPath(filePath),
797
+ topic: trigger.topic,
798
+ consumer: filePathToConsumerGroup(filePath)
799
+ });
800
+ }
801
+ }
802
+ }
803
+ g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
804
+ return g[ROUTE_MAPPINGS_KEY];
805
+ } catch (error) {
806
+ console.warn("[Dev Mode] Failed to read vercel.json:", error);
807
+ g[ROUTE_MAPPINGS_KEY] = null;
808
+ return null;
809
+ }
810
+ }
811
+ function findMatchingRoutes(topicName) {
812
+ const mappings = getDevRouteMappings();
813
+ if (!mappings) {
814
+ return [];
815
+ }
816
+ return mappings.filter((mapping) => {
817
+ if (mapping.topic.includes("*")) {
818
+ return matchesWildcardPattern(topicName, mapping.topic);
819
+ }
820
+ return mapping.topic === topicName;
821
+ });
822
+ }
823
+ function isDevMode() {
824
+ return process.env.NODE_ENV === "development";
825
+ }
826
+ var DEV_VISIBILITY_POLL_INTERVAL = 50;
827
+ var DEV_VISIBILITY_MAX_WAIT = 5e3;
828
+ var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
829
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId, region) {
830
+ const client = new ApiClient({ region });
831
+ let elapsed = 0;
832
+ let interval = DEV_VISIBILITY_POLL_INTERVAL;
833
+ while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
834
+ try {
835
+ await client.receiveMessageById({
836
+ queueName: topicName,
837
+ consumerGroup,
838
+ messageId,
839
+ visibilityTimeoutSeconds: 0
840
+ });
841
+ return true;
842
+ } catch (error) {
843
+ if (error instanceof MessageNotFoundError) {
844
+ await new Promise((resolve) => setTimeout(resolve, interval));
845
+ elapsed += interval;
846
+ interval = Math.min(
847
+ interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
848
+ DEV_VISIBILITY_MAX_WAIT - elapsed
849
+ );
850
+ continue;
851
+ }
852
+ if (error instanceof MessageAlreadyProcessedError) {
853
+ console.log(
854
+ `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
855
+ );
856
+ return false;
857
+ }
858
+ console.error(
859
+ `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
860
+ error
861
+ );
862
+ return false;
863
+ }
864
+ }
865
+ console.warn(
866
+ `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
867
+ );
868
+ return false;
869
+ }
870
+ function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
871
+ if (delaySeconds && delaySeconds > 0) {
872
+ console.log(
873
+ `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
874
+ );
875
+ setTimeout(() => {
876
+ triggerDevCallbacks(topicName, messageId, region);
877
+ }, delaySeconds * 1e3);
878
+ return;
879
+ }
880
+ console.log(
881
+ `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
882
+ );
883
+ const matchingRoutes = findMatchingRoutes(topicName);
884
+ if (matchingRoutes.length === 0) {
885
+ console.log(
886
+ `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
887
+ );
888
+ return;
889
+ }
890
+ const consumerGroups = matchingRoutes.map((r) => r.consumer);
891
+ console.log(
892
+ `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
893
+ );
894
+ (async () => {
895
+ const firstRoute = matchingRoutes[0];
896
+ const isVisible = await waitForMessageVisibility(
897
+ topicName,
898
+ firstRoute.consumer,
899
+ messageId,
900
+ region
901
+ );
902
+ if (!isVisible) {
903
+ console.warn(
904
+ `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
905
+ );
906
+ return;
907
+ }
908
+ const port = process.env.PORT || 3e3;
909
+ const baseUrl = `http://localhost:${port}`;
910
+ for (const route of matchingRoutes) {
911
+ const url = `${baseUrl}${route.urlPath}`;
912
+ console.log(
913
+ `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
914
+ );
915
+ try {
916
+ const response = await fetch(url, {
917
+ method: "POST",
918
+ headers: {
919
+ "ce-type": CLOUD_EVENT_TYPE_V2BETA,
920
+ "ce-vqsqueuename": topicName,
921
+ "ce-vqsconsumergroup": route.consumer,
922
+ "ce-vqsmessageid": messageId,
923
+ "ce-vqsregion": region
924
+ }
925
+ });
926
+ if (response.ok) {
927
+ try {
928
+ const responseData = await response.json();
929
+ if (responseData.status === "success") {
930
+ console.log(
931
+ `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
932
+ );
933
+ }
934
+ } catch {
935
+ console.warn(
936
+ `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
937
+ );
938
+ }
939
+ } else {
940
+ try {
941
+ const errorData = await response.json();
942
+ console.error(
943
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
944
+ );
945
+ } catch {
946
+ console.error(
947
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
948
+ );
949
+ }
950
+ }
951
+ } catch (error) {
952
+ console.error(
953
+ `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
954
+ error
955
+ );
956
+ }
957
+ }
958
+ })();
959
+ }
960
+ function clearDevRouteMappings() {
961
+ const g = globalThis;
962
+ delete g[ROUTE_MAPPINGS_KEY];
963
+ }
964
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
965
+ globalThis.__clearDevRouteMappings = clearDevRouteMappings;
966
+ }
967
+
968
+ // src/oidc.ts
969
+ var import_oidc = require("@vercel/oidc");
970
+
971
+ // src/api-client.ts
972
+ function isDebugEnabled() {
973
+ return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
974
+ }
975
+ async function consumeStream(stream) {
976
+ const reader = stream.getReader();
977
+ try {
978
+ while (true) {
979
+ const { done } = await reader.read();
980
+ if (done) break;
981
+ }
982
+ } finally {
983
+ reader.releaseLock();
984
+ }
985
+ }
986
+ function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
987
+ if (status === 400) {
988
+ throw new BadRequestError(errorText || badRequestDefault);
989
+ }
990
+ if (status === 401) {
991
+ throw new UnauthorizedError(errorText || void 0);
992
+ }
993
+ if (status === 403) {
994
+ throw new ForbiddenError(errorText || void 0);
995
+ }
996
+ if (status >= 500) {
997
+ throw new InternalServerError(
998
+ errorText || `Server error: ${status} ${statusText}`
999
+ );
1000
+ }
1001
+ throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
1002
+ }
1003
+ function parseQueueHeaders(headers) {
1004
+ const messageId = headers.get("Vqs-Message-Id");
1005
+ const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
1006
+ const timestamp = headers.get("Vqs-Timestamp");
1007
+ const contentType = headers.get("Content-Type") || "application/octet-stream";
1008
+ const receiptHandle = headers.get("Vqs-Receipt-Handle");
1009
+ if (!messageId || !timestamp || !receiptHandle) {
1010
+ return null;
1011
+ }
1012
+ const deliveryCount = parseInt(deliveryCountStr, 10);
1013
+ if (Number.isNaN(deliveryCount)) {
1014
+ return null;
1015
+ }
1016
+ return {
1017
+ messageId,
1018
+ deliveryCount,
1019
+ createdAt: new Date(timestamp),
1020
+ contentType,
1021
+ receiptHandle
1022
+ };
1023
+ }
1024
+ var DEFAULT_BASE_URL_RESOLVER = (region) => `https://${region}.vercel-queue.com`;
1025
+ function resolveBaseUrl(region, resolver) {
1026
+ return (resolver ?? DEFAULT_BASE_URL_RESOLVER)(region);
1027
+ }
1028
+ var BASE_PATH = "/api/v3/topic";
1029
+ var ApiClient = class _ApiClient {
1030
+ baseUrl;
1031
+ customHeaders;
1032
+ providedToken;
1033
+ resolvedDeploymentId;
1034
+ pinSends;
1035
+ explicitlyUnpinned;
1036
+ transport;
1037
+ region;
1038
+ baseUrlResolver;
1039
+ constructor(options) {
1040
+ this.region = options.region;
1041
+ this.baseUrlResolver = options.resolveBaseUrl;
1042
+ this.baseUrl = resolveBaseUrl(this.region, this.baseUrlResolver);
1043
+ this.customHeaders = options.headers || {};
1044
+ this.providedToken = options.token;
1045
+ this.transport = options.transport || new JsonTransport();
1046
+ if (options.deploymentId === null) {
1047
+ this.pinSends = false;
1048
+ this.explicitlyUnpinned = true;
1049
+ } else {
1050
+ this.resolvedDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
1051
+ this.pinSends = true;
1052
+ this.explicitlyUnpinned = false;
1053
+ }
1054
+ }
1055
+ /**
1056
+ * Return a new ApiClient targeting the given region, sharing all other
1057
+ * configuration (token, transport, headers, deployment ID, resolver).
1058
+ * Used internally by handleCallback to route follow-up API calls to the
1059
+ * region indicated by the incoming `ce-vqsregion` header.
1060
+ */
1061
+ withRegion(region) {
1062
+ return new _ApiClient({
1063
+ region,
1064
+ resolveBaseUrl: this.baseUrlResolver,
1065
+ token: this.providedToken,
1066
+ headers: { ...this.customHeaders },
1067
+ deploymentId: this.explicitlyUnpinned ? null : this.resolvedDeploymentId,
1068
+ transport: this.transport
1069
+ });
1070
+ }
1071
+ getRegion() {
1072
+ return this.region;
1073
+ }
1074
+ getTransport() {
1075
+ return this.transport;
1076
+ }
1077
+ requireDeploymentId() {
1078
+ if (isDevMode() || this.explicitlyUnpinned || this.resolvedDeploymentId) {
1079
+ return;
1080
+ }
1081
+ throw new Error(
1082
+ 'No deployment ID available. VERCEL_DEPLOYMENT_ID is not set.\n\nThis usually means the code is running outside a Vercel deployment (e.g. during build or in a non-Vercel environment).\n\nTo fix this, create a new QueueClient with an explicit deploymentId:\n new QueueClient({ region: "iad1", deploymentId: "dpl_xxx" })\nOr explicitly opt out of deployment pinning:\n new QueueClient({ region: "iad1", deploymentId: null })'
1083
+ );
1084
+ }
1085
+ getSendDeploymentId() {
1086
+ if (isDevMode()) {
1087
+ return void 0;
1088
+ }
1089
+ this.requireDeploymentId();
1090
+ return this.pinSends ? this.resolvedDeploymentId : void 0;
1091
+ }
1092
+ getConsumeDeploymentId() {
1093
+ if (isDevMode()) {
1094
+ return void 0;
1095
+ }
1096
+ this.requireDeploymentId();
1097
+ return this.resolvedDeploymentId;
1098
+ }
1099
+ async getToken() {
1100
+ if (this.providedToken) {
1101
+ return this.providedToken;
1102
+ }
1103
+ const token = await (0, import_oidc.getVercelOidcToken)();
1104
+ if (!token) {
1105
+ throw new Error(
1106
+ "Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment, or provide a token explicitly.\n\nTo set up your environment:\n1. Link your project: 'vercel link'\n2. Pull environment variables: 'vercel env pull'\n3. Run with environment: 'dotenv -e .env.local -- your-command'"
1107
+ );
1108
+ }
1109
+ return token;
1110
+ }
1111
+ buildUrl(queueName, ...pathSegments) {
1112
+ const encodedQueue = encodeURIComponent(queueName);
1113
+ const segments = pathSegments.map((s) => encodeURIComponent(s));
1114
+ const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
1115
+ return `${this.baseUrl}${BASE_PATH}/${encodedQueue}${path2}`;
1116
+ }
1117
+ async fetch(url, init) {
1118
+ const method = init.method || "GET";
1119
+ if (isDebugEnabled()) {
1120
+ const logData = {
1121
+ method,
1122
+ url,
1123
+ headers: init.headers
1124
+ };
1125
+ const body = init.body;
1126
+ if (body !== void 0 && body !== null) {
1127
+ if (body instanceof ArrayBuffer) {
1128
+ logData.bodySize = body.byteLength;
1129
+ } else if (body instanceof Uint8Array) {
1130
+ logData.bodySize = body.byteLength;
1131
+ } else if (typeof body === "string") {
1132
+ logData.bodySize = body.length;
1133
+ } else {
1134
+ logData.bodyType = typeof body;
1135
+ }
1136
+ }
1137
+ console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1138
+ }
1139
+ init.headers.set("User-Agent", `@vercel/queue/${"0.0.1"}`);
1140
+ init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1141
+ const response = await fetch(url, init);
1142
+ if (isDebugEnabled()) {
1143
+ const logData = {
1144
+ method,
1145
+ url,
1146
+ status: response.status,
1147
+ statusText: response.statusText,
1148
+ headers: response.headers
1149
+ };
1150
+ console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
1151
+ }
1152
+ return response;
1153
+ }
1154
+ async sendMessage(options) {
1155
+ const transport = this.transport;
1156
+ const {
1157
+ queueName,
1158
+ payload,
1159
+ idempotencyKey,
1160
+ retentionSeconds,
1161
+ delaySeconds,
1162
+ headers: optionHeaders
1163
+ } = options;
1164
+ const headers = new Headers();
1165
+ if (this.customHeaders) {
1166
+ for (const [name, value] of Object.entries(this.customHeaders)) {
1167
+ headers.append(name, value);
1168
+ }
1169
+ }
1170
+ if (optionHeaders) {
1171
+ const protectedHeaderNames = /* @__PURE__ */ new Set(["authorization", "content-type"]);
1172
+ const isProtectedHeader = (name) => {
1173
+ const lower = name.toLowerCase();
1174
+ if (protectedHeaderNames.has(lower)) return true;
1175
+ return lower.startsWith("vqs-");
1176
+ };
1177
+ for (const [name, value] of Object.entries(optionHeaders)) {
1178
+ if (!isProtectedHeader(name) && value !== void 0) {
1179
+ headers.append(name, value);
1180
+ }
1181
+ }
1182
+ }
1183
+ headers.set("Authorization", `Bearer ${await this.getToken()}`);
1184
+ headers.set("Content-Type", transport.contentType);
1185
+ const deploymentId = this.getSendDeploymentId();
1186
+ if (deploymentId) {
1187
+ headers.set("Vqs-Deployment-Id", deploymentId);
1188
+ }
1189
+ if (idempotencyKey) {
1190
+ headers.set("Vqs-Idempotency-Key", idempotencyKey);
1191
+ }
1192
+ if (retentionSeconds !== void 0) {
1193
+ headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
1194
+ }
1195
+ if (delaySeconds !== void 0) {
1196
+ headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
1197
+ }
1198
+ const serialized = transport.serialize(payload);
1199
+ const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
1200
+ const response = await this.fetch(this.buildUrl(queueName), {
1201
+ method: "POST",
1202
+ body,
1203
+ headers
1204
+ });
1205
+ if (!response.ok) {
1206
+ const errorText = await response.text();
1207
+ if (response.status === 409) {
1208
+ throw new DuplicateMessageError(
1209
+ errorText || "Duplicate idempotency key detected",
1210
+ idempotencyKey
1211
+ );
1212
+ }
1213
+ if (response.status === 502) {
1214
+ throw new ConsumerDiscoveryError(
1215
+ errorText || "Consumer discovery failed",
1216
+ deploymentId
1217
+ );
1218
+ }
1219
+ if (response.status === 503) {
1220
+ throw new ConsumerRegistryNotConfiguredError(
1221
+ errorText || "Consumer registry not configured"
1222
+ );
1223
+ }
1224
+ throwCommonHttpError(
1225
+ response.status,
1226
+ response.statusText,
1227
+ errorText,
1228
+ "send message"
1229
+ );
1230
+ }
1231
+ if (response.status === 202) {
1232
+ return { messageId: null };
1233
+ }
1234
+ const responseData = await response.json();
1235
+ return responseData;
1236
+ }
1237
+ async *receiveMessages(options) {
1238
+ const transport = this.transport;
1239
+ const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
1240
+ if (limit !== void 0 && (limit < 1 || limit > 10)) {
1241
+ throw new InvalidLimitError(limit);
1242
+ }
1243
+ const headers = new Headers({
1244
+ Authorization: `Bearer ${await this.getToken()}`,
1245
+ Accept: "multipart/mixed",
1246
+ ...this.customHeaders
1247
+ });
1248
+ if (visibilityTimeoutSeconds !== void 0) {
1249
+ headers.set(
1250
+ "Vqs-Visibility-Timeout-Seconds",
1251
+ visibilityTimeoutSeconds.toString()
1252
+ );
1253
+ }
1254
+ if (limit !== void 0) {
1255
+ headers.set("Vqs-Max-Messages", limit.toString());
1256
+ }
1257
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1258
+ if (effectiveDeploymentId) {
1259
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1260
+ }
1261
+ const response = await this.fetch(
1262
+ this.buildUrl(queueName, "consumer", consumerGroup),
1263
+ {
1264
+ method: "POST",
1265
+ headers
1266
+ }
1267
+ );
1268
+ if (response.status === 204) {
1269
+ return;
1270
+ }
1271
+ if (!response.ok) {
1272
+ const errorText = await response.text();
1273
+ throwCommonHttpError(
1274
+ response.status,
1275
+ response.statusText,
1276
+ errorText,
1277
+ "receive messages"
1278
+ );
1279
+ }
1280
+ for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
1281
+ try {
1282
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
1283
+ if (!parsedHeaders) {
1284
+ console.warn("Missing required queue headers in multipart part");
1285
+ await consumeStream(multipartMessage.payload);
1286
+ continue;
1287
+ }
1288
+ const deserializedPayload = await transport.deserialize(
1289
+ multipartMessage.payload
407
1290
  );
408
1291
  const message = {
409
1292
  ...parsedHeaders,
@@ -416,649 +1299,403 @@ var QueueClient = class _QueueClient {
416
1299
  }
417
1300
  }
418
1301
  }
419
- async receiveMessageById(options, transport) {
420
- const {
421
- queueName,
422
- consumerGroup,
423
- messageId,
424
- visibilityTimeoutSeconds,
425
- skipPayload
426
- } = options;
1302
+ async receiveMessageById(options) {
1303
+ const transport = this.transport;
1304
+ const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
427
1305
  const headers = new Headers({
428
- Authorization: `Bearer ${this.token}`,
429
- "Vqs-Queue-Name": queueName,
430
- "Vqs-Consumer-Group": consumerGroup,
431
- Accept: "multipart/mixed"
1306
+ Authorization: `Bearer ${await this.getToken()}`,
1307
+ Accept: "multipart/mixed",
1308
+ ...this.customHeaders
432
1309
  });
433
1310
  if (visibilityTimeoutSeconds !== void 0) {
434
1311
  headers.set(
435
- "Vqs-Visibility-Timeout",
1312
+ "Vqs-Visibility-Timeout-Seconds",
436
1313
  visibilityTimeoutSeconds.toString()
437
1314
  );
438
1315
  }
439
- if (skipPayload) {
440
- headers.set("Vqs-Skip-Payload", "1");
1316
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1317
+ if (effectiveDeploymentId) {
1318
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
441
1319
  }
442
- const response = await fetch(
443
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
1320
+ const response = await this.fetch(
1321
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
444
1322
  {
445
- method: "GET",
1323
+ method: "POST",
446
1324
  headers
447
1325
  }
448
1326
  );
449
1327
  if (!response.ok) {
450
- if (response.status === 400) {
451
- const errorText = await response.text();
452
- throw new BadRequestError(errorText || "Invalid parameters");
453
- }
454
- if (response.status === 401) {
455
- throw new UnauthorizedError();
456
- }
457
- if (response.status === 403) {
458
- throw new ForbiddenError();
459
- }
1328
+ const errorText = await response.text();
460
1329
  if (response.status === 404) {
461
1330
  throw new MessageNotFoundError(messageId);
462
1331
  }
463
- if (response.status === 423) {
464
- const retryAfterHeader = response.headers.get("Retry-After");
465
- let retryAfter;
466
- if (retryAfterHeader) {
467
- const parsed = parseInt(retryAfterHeader, 10);
468
- retryAfter = isNaN(parsed) ? void 0 : parsed;
469
- }
470
- throw new MessageLockedError(messageId, retryAfter);
471
- }
472
1332
  if (response.status === 409) {
1333
+ let errorData = {};
1334
+ try {
1335
+ errorData = JSON.parse(errorText);
1336
+ } catch {
1337
+ }
1338
+ if (errorData.originalMessageId) {
1339
+ throw new MessageNotAvailableError(
1340
+ messageId,
1341
+ `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
1342
+ );
1343
+ }
473
1344
  throw new MessageNotAvailableError(messageId);
474
1345
  }
475
- if (response.status >= 500) {
476
- throw new InternalServerError(
477
- `Server error: ${response.status} ${response.statusText}`
478
- );
1346
+ if (response.status === 410) {
1347
+ throw new MessageAlreadyProcessedError(messageId);
479
1348
  }
480
- throw new Error(
481
- `Failed to receive message by ID: ${response.status} ${response.statusText}`
1349
+ throwCommonHttpError(
1350
+ response.status,
1351
+ response.statusText,
1352
+ errorText,
1353
+ "receive message by ID"
482
1354
  );
483
1355
  }
484
- if (skipPayload && response.status === 204) {
485
- const parsedHeaders = parseQueueHeaders(response.headers);
1356
+ for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
1357
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
486
1358
  if (!parsedHeaders) {
1359
+ await consumeStream(multipartMessage.payload);
487
1360
  throw new MessageCorruptedError(
488
1361
  messageId,
489
- "Missing required queue headers in 204 response"
1362
+ "Missing required queue headers in response"
490
1363
  );
491
1364
  }
1365
+ const deserializedPayload = await transport.deserialize(
1366
+ multipartMessage.payload
1367
+ );
492
1368
  const message = {
493
1369
  ...parsedHeaders,
494
- payload: void 0
1370
+ payload: deserializedPayload
495
1371
  };
496
1372
  return { message };
497
1373
  }
498
- if (!transport) {
499
- throw new Error("Transport is required when skipPayload is not true");
500
- }
501
- try {
502
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
503
- try {
504
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
505
- if (!parsedHeaders) {
506
- console.warn("Missing required queue headers in multipart part");
507
- await consumeStream(multipartMessage.payload);
508
- continue;
509
- }
510
- const deserializedPayload = await transport.deserialize(
511
- multipartMessage.payload
512
- );
513
- const message = {
514
- ...parsedHeaders,
515
- payload: deserializedPayload
516
- };
517
- return { message };
518
- } catch (error) {
519
- console.warn("Failed to deserialize message by ID:", error);
520
- await consumeStream(multipartMessage.payload);
521
- throw new MessageCorruptedError(
522
- messageId,
523
- `Failed to deserialize payload: ${error}`
524
- );
525
- }
526
- }
527
- } catch (error) {
528
- if (error instanceof MessageCorruptedError) {
529
- throw error;
530
- }
531
- throw new MessageCorruptedError(
532
- messageId,
533
- `Failed to parse multipart response: ${error}`
534
- );
535
- }
536
1374
  throw new MessageNotFoundError(messageId);
537
1375
  }
538
- /**
539
- * Delete a message (acknowledge processing)
540
- * @param options Delete message options
541
- * @returns Promise with delete status
542
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
543
- * @throws {MessageNotAvailableError} When message can't be deleted (409)
544
- * @throws {BadRequestError} When ticket is missing or invalid (400)
545
- * @throws {UnauthorizedError} When authentication fails
546
- * @throws {ForbiddenError} When access is denied (environment mismatch)
547
- * @throws {InternalServerError} When server encounters an error
548
- */
549
- async deleteMessage(options) {
550
- const { queueName, consumerGroup, messageId, ticket } = options;
551
- const response = await fetch(
552
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
1376
+ async acknowledgeMessage(options) {
1377
+ const { queueName, consumerGroup, receiptHandle } = options;
1378
+ const headers = new Headers({
1379
+ Authorization: `Bearer ${await this.getToken()}`,
1380
+ ...this.customHeaders
1381
+ });
1382
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1383
+ if (effectiveDeploymentId) {
1384
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1385
+ }
1386
+ const response = await this.fetch(
1387
+ this.buildUrl(
1388
+ queueName,
1389
+ "consumer",
1390
+ consumerGroup,
1391
+ "lease",
1392
+ receiptHandle
1393
+ ),
553
1394
  {
554
1395
  method: "DELETE",
555
- headers: new Headers({
556
- Authorization: `Bearer ${this.token}`,
557
- "Vqs-Queue-Name": queueName,
558
- "Vqs-Consumer-Group": consumerGroup,
559
- "Vqs-Ticket": ticket
560
- })
1396
+ headers
561
1397
  }
562
1398
  );
563
- if (!response.ok) {
564
- if (response.status === 400) {
565
- throw new BadRequestError("Missing or invalid ticket");
566
- }
567
- if (response.status === 401) {
568
- throw new UnauthorizedError();
569
- }
570
- if (response.status === 403) {
571
- throw new ForbiddenError();
572
- }
1399
+ if (!response.ok) {
1400
+ const errorText = await response.text();
573
1401
  if (response.status === 404) {
574
- throw new MessageNotFoundError(messageId);
1402
+ throw new MessageNotFoundError(receiptHandle);
575
1403
  }
576
1404
  if (response.status === 409) {
577
1405
  throw new MessageNotAvailableError(
578
- messageId,
579
- "Invalid ticket, message not in correct state, or already processed"
580
- );
581
- }
582
- if (response.status >= 500) {
583
- throw new InternalServerError(
584
- `Server error: ${response.status} ${response.statusText}`
1406
+ receiptHandle,
1407
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
585
1408
  );
586
1409
  }
587
- throw new Error(
588
- `Failed to delete message: ${response.status} ${response.statusText}`
1410
+ throwCommonHttpError(
1411
+ response.status,
1412
+ response.statusText,
1413
+ errorText,
1414
+ "acknowledge message",
1415
+ "Missing or invalid receipt handle"
589
1416
  );
590
1417
  }
591
- return { deleted: true };
1418
+ return { acknowledged: true };
592
1419
  }
593
- /**
594
- * Change the visibility timeout of a message
595
- * @param options Change visibility options
596
- * @returns Promise with update status
597
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
598
- * @throws {MessageNotAvailableError} When message can't be updated (409)
599
- * @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
600
- * @throws {UnauthorizedError} When authentication fails
601
- * @throws {ForbiddenError} When access is denied (environment mismatch)
602
- * @throws {InternalServerError} When server encounters an error
603
- */
604
1420
  async changeVisibility(options) {
605
1421
  const {
606
1422
  queueName,
607
1423
  consumerGroup,
608
- messageId,
609
- ticket,
1424
+ receiptHandle,
610
1425
  visibilityTimeoutSeconds
611
1426
  } = options;
612
- const response = await fetch(
613
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
1427
+ const headers = new Headers({
1428
+ Authorization: `Bearer ${await this.getToken()}`,
1429
+ "Content-Type": "application/json",
1430
+ ...this.customHeaders
1431
+ });
1432
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1433
+ if (effectiveDeploymentId) {
1434
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1435
+ }
1436
+ const response = await this.fetch(
1437
+ this.buildUrl(
1438
+ queueName,
1439
+ "consumer",
1440
+ consumerGroup,
1441
+ "lease",
1442
+ receiptHandle
1443
+ ),
614
1444
  {
615
1445
  method: "PATCH",
616
- headers: new Headers({
617
- Authorization: `Bearer ${this.token}`,
618
- "Vqs-Queue-Name": queueName,
619
- "Vqs-Consumer-Group": consumerGroup,
620
- "Vqs-Ticket": ticket,
621
- "Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString()
622
- })
1446
+ headers,
1447
+ body: JSON.stringify({ visibilityTimeoutSeconds })
623
1448
  }
624
1449
  );
625
1450
  if (!response.ok) {
626
- if (response.status === 400) {
627
- throw new BadRequestError(
628
- "Missing ticket or invalid visibility timeout"
629
- );
630
- }
631
- if (response.status === 401) {
632
- throw new UnauthorizedError();
633
- }
634
- if (response.status === 403) {
635
- throw new ForbiddenError();
636
- }
1451
+ const errorText = await response.text();
637
1452
  if (response.status === 404) {
638
- throw new MessageNotFoundError(messageId);
1453
+ throw new MessageNotFoundError(receiptHandle);
639
1454
  }
640
1455
  if (response.status === 409) {
641
1456
  throw new MessageNotAvailableError(
642
- messageId,
643
- "Invalid ticket, message not in correct state, or already processed"
644
- );
645
- }
646
- if (response.status >= 500) {
647
- throw new InternalServerError(
648
- `Server error: ${response.status} ${response.statusText}`
1457
+ receiptHandle,
1458
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
649
1459
  );
650
1460
  }
651
- throw new Error(
652
- `Failed to change visibility: ${response.status} ${response.statusText}`
1461
+ throwCommonHttpError(
1462
+ response.status,
1463
+ response.statusText,
1464
+ errorText,
1465
+ "change visibility",
1466
+ "Missing receipt handle or invalid visibility timeout"
653
1467
  );
654
1468
  }
655
- return { updated: true };
1469
+ return { success: true };
656
1470
  }
657
1471
  };
658
1472
 
659
- // src/consumer-group.ts
660
- var ConsumerGroup = class {
661
- client;
662
- topicName;
663
- consumerGroupName;
664
- visibilityTimeout;
665
- refreshInterval;
666
- transport;
667
- /**
668
- * Create a new ConsumerGroup instance
669
- * @param client QueueClient instance to use for API calls
670
- * @param topicName Name of the topic to consume from
671
- * @param consumerGroupName Name of the consumer group
672
- * @param options Optional configuration
673
- */
674
- constructor(client, topicName, consumerGroupName, options = {}) {
675
- this.client = client;
676
- this.topicName = topicName;
677
- this.consumerGroupName = consumerGroupName;
678
- this.visibilityTimeout = options.visibilityTimeoutSeconds || 30;
679
- this.refreshInterval = options.refreshInterval || 10;
680
- this.transport = options.transport || new JsonTransport();
1473
+ // src/client.ts
1474
+ var apiClients = /* @__PURE__ */ new WeakMap();
1475
+ function getApiClient(client) {
1476
+ const api = apiClients.get(client);
1477
+ if (!api) {
1478
+ throw new Error("QueueClient not initialized");
1479
+ }
1480
+ return api;
1481
+ }
1482
+ var QueueClient = class {
1483
+ constructor(options) {
1484
+ apiClients.set(this, new ApiClient(options));
681
1485
  }
682
1486
  /**
683
- * Starts a background loop that periodically extends the visibility timeout for a message.
684
- * This prevents the message from becoming visible to other consumers while it's being processed.
685
- *
686
- * The extension loop runs every `refreshInterval` seconds and updates the message's
687
- * visibility timeout to `visibilityTimeout` seconds from the current time.
1487
+ * Send a message to a topic.
688
1488
  *
689
- * @param messageId - The unique identifier of the message to extend visibility for
690
- * @param ticket - The receipt ticket that proves ownership of the message
691
- * @returns A function that when called will stop the extension loop
1489
+ * This is an arrow function property so it can be destructured:
1490
+ * ```typescript
1491
+ * const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
1492
+ * await send("my-topic", payload);
1493
+ * ```
692
1494
  *
693
- * @remarks
694
- * - The first extension attempt occurs after `refreshInterval` seconds, not immediately
695
- * - If an extension fails, the loop terminates with an error logged to console
696
- * - The returned stop function is idempotent - calling it multiple times is safe
697
- * - By default, the stop function returns immediately without waiting for in-flight
698
- * - Pass `true` to the stop function to wait for any in-flight extension to complete
1495
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1496
+ * @param payload - The data to send (serialized via the configured transport)
1497
+ * @param options - Optional send options (idempotencyKey, retentionSeconds, delaySeconds, headers)
1498
+ * @returns `{ messageId }` `messageId` is `null` when the server accepted
1499
+ * the message for deferred processing (no ID available yet)
699
1500
  */
700
- startVisibilityExtension(messageId, ticket) {
701
- let isRunning = true;
702
- let resolveLifecycle;
703
- let timeoutId = null;
704
- const lifecyclePromise = new Promise((resolve) => {
705
- resolveLifecycle = resolve;
1501
+ send = async (topicName, payload, options) => {
1502
+ const api = getApiClient(this);
1503
+ const result = await api.sendMessage({
1504
+ queueName: topicName,
1505
+ payload,
1506
+ idempotencyKey: options?.idempotencyKey,
1507
+ retentionSeconds: options?.retentionSeconds,
1508
+ delaySeconds: options?.delaySeconds,
1509
+ headers: options?.headers
706
1510
  });
707
- const extend = async () => {
708
- if (!isRunning) {
709
- resolveLifecycle();
710
- return;
711
- }
712
- try {
713
- await this.client.changeVisibility({
714
- queueName: this.topicName,
715
- consumerGroup: this.consumerGroupName,
716
- messageId,
717
- ticket,
718
- visibilityTimeoutSeconds: this.visibilityTimeout
719
- });
720
- if (isRunning) {
721
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
722
- } else {
723
- resolveLifecycle();
724
- }
725
- } catch (error) {
726
- console.error(
727
- `Failed to extend visibility for message ${messageId}:`,
728
- error
729
- );
730
- resolveLifecycle();
731
- }
732
- };
733
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
734
- return async (waitForCompletion = false) => {
735
- isRunning = false;
736
- if (timeoutId) {
737
- clearTimeout(timeoutId);
738
- timeoutId = null;
739
- }
740
- if (waitForCompletion) {
741
- await lifecyclePromise;
742
- } else {
743
- resolveLifecycle();
744
- }
745
- };
746
- }
1511
+ if (result.messageId && isDevMode()) {
1512
+ triggerDevCallbacks(
1513
+ topicName,
1514
+ result.messageId,
1515
+ api.getRegion(),
1516
+ options?.delaySeconds
1517
+ );
1518
+ }
1519
+ return { messageId: result.messageId };
1520
+ };
747
1521
  /**
748
- * Process a single message with the given handler
749
- * @param message The message to process
750
- * @param handler Function to process the message
1522
+ * Receive and process messages from a topic.
1523
+ *
1524
+ * Each message is automatically locked, kept alive via periodic visibility
1525
+ * extensions during processing, and acknowledged upon successful handler completion.
1526
+ * The handler is not called when the queue is empty — check `result.ok` instead.
1527
+ *
1528
+ * This is an arrow function property so it can be destructured:
1529
+ * ```typescript
1530
+ * const { receive } = new QueueClient({ region: process.env.QUEUE_REGION! });
1531
+ * const result = await receive("my-topic", "my-group", handler);
1532
+ * if (!result.ok) console.log(result.reason);
1533
+ * ```
1534
+ *
1535
+ * @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
1536
+ * @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
1537
+ * @param handler - Function to process each message payload and metadata.
1538
+ * Not called when the queue is empty.
1539
+ * @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
1540
+ * @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
751
1541
  */
752
- async processMessage(message, handler) {
753
- const stopExtension = this.startVisibilityExtension(
754
- message.messageId,
755
- message.ticket
1542
+ receive = async (topicName, consumerGroup, handler, options) => {
1543
+ const api = getApiClient(this);
1544
+ const topic = new Topic(api, topicName);
1545
+ const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
1546
+ const consumer = topic.consumerGroup(
1547
+ consumerGroup,
1548
+ visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
756
1549
  );
757
1550
  try {
758
- const result = await handler(message.payload, {
759
- messageId: message.messageId,
760
- deliveryCount: message.deliveryCount,
761
- timestamp: message.timestamp
762
- });
763
- await stopExtension();
764
- if (result && "timeoutSeconds" in result) {
765
- await this.client.changeVisibility({
766
- queueName: this.topicName,
767
- consumerGroup: this.consumerGroupName,
768
- messageId: message.messageId,
769
- ticket: message.ticket,
770
- visibilityTimeoutSeconds: result.timeoutSeconds
1551
+ let count;
1552
+ const retry = options?.retry;
1553
+ if (options && "messageId" in options) {
1554
+ count = await consumer.consume(handler, {
1555
+ messageId: options.messageId,
1556
+ retry
771
1557
  });
772
1558
  } else {
773
- await this.client.deleteMessage({
774
- queueName: this.topicName,
775
- consumerGroup: this.consumerGroupName,
776
- messageId: message.messageId,
777
- ticket: message.ticket
1559
+ const limit = options && "limit" in options ? options.limit : void 0;
1560
+ count = await consumer.consume(handler, {
1561
+ ...limit !== void 0 ? { limit } : {},
1562
+ retry
778
1563
  });
779
1564
  }
780
- } catch (error) {
781
- await stopExtension();
782
- if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
783
- try {
784
- await this.transport.finalize(message.payload);
785
- } catch (finalizeError) {
786
- console.warn("Failed to finalize message payload:", finalizeError);
787
- }
1565
+ if (count === 0) {
1566
+ return { ok: false, reason: "empty" };
788
1567
  }
789
- throw error;
790
- }
791
- }
792
- async consume(handler, options) {
793
- if (options?.messageId) {
794
- if (options.skipPayload) {
795
- const response = await this.client.receiveMessageById(
796
- {
797
- queueName: this.topicName,
798
- consumerGroup: this.consumerGroupName,
799
- messageId: options.messageId,
800
- visibilityTimeoutSeconds: this.visibilityTimeout,
801
- skipPayload: true
802
- },
803
- this.transport
804
- );
805
- await this.processMessage(
806
- response.message,
807
- handler
808
- );
809
- } else {
810
- const response = await this.client.receiveMessageById(
811
- {
812
- queueName: this.topicName,
813
- consumerGroup: this.consumerGroupName,
814
- messageId: options.messageId,
815
- visibilityTimeoutSeconds: this.visibilityTimeout
816
- },
817
- this.transport
818
- );
819
- await this.processMessage(
820
- response.message,
821
- handler
822
- );
1568
+ return { ok: true };
1569
+ } catch (error) {
1570
+ if (options && "messageId" in options && error instanceof MessageNotFoundError) {
1571
+ return { ok: false, reason: "not_found", messageId: options.messageId };
823
1572
  }
824
- } else {
825
- let messageFound = false;
826
- for await (const message of this.client.receiveMessages(
827
- {
828
- queueName: this.topicName,
829
- consumerGroup: this.consumerGroupName,
830
- visibilityTimeoutSeconds: this.visibilityTimeout,
831
- limit: 1
832
- },
833
- this.transport
834
- )) {
835
- messageFound = true;
836
- await this.processMessage(message, handler);
837
- break;
1573
+ if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
1574
+ return {
1575
+ ok: false,
1576
+ reason: "not_available",
1577
+ messageId: options.messageId
1578
+ };
838
1579
  }
839
- if (!messageFound) {
840
- throw new Error("No messages available");
1580
+ if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
1581
+ return {
1582
+ ok: false,
1583
+ reason: "already_processed",
1584
+ messageId: options.messageId
1585
+ };
841
1586
  }
1587
+ throw error;
842
1588
  }
843
- }
844
- /**
845
- * Get the consumer group name
846
- */
847
- get name() {
848
- return this.consumerGroupName;
849
- }
850
- /**
851
- * Get the topic name this consumer group is subscribed to
852
- */
853
- get topic() {
854
- return this.topicName;
855
- }
856
- };
857
-
858
- // src/topic.ts
859
- var Topic = class {
860
- client;
861
- topicName;
862
- transport;
863
- /**
864
- * Create a new Topic instance
865
- * @param client QueueClient instance to use for API calls
866
- * @param topicName Name of the topic to work with
867
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
868
- */
869
- constructor(client, topicName, transport) {
870
- this.client = client;
871
- this.topicName = topicName;
872
- this.transport = transport || new JsonTransport();
873
- }
874
- /**
875
- * Publish a message to the topic
876
- * @param payload The data to publish
877
- * @param options Optional publish options
878
- * @returns An object containing the message ID
879
- * @throws {BadRequestError} When request parameters are invalid
880
- * @throws {UnauthorizedError} When authentication fails
881
- * @throws {ForbiddenError} When access is denied (environment mismatch)
882
- * @throws {InternalServerError} When server encounters an error
883
- */
884
- async publish(payload, options) {
885
- const result = await this.client.sendMessage(
886
- {
887
- queueName: this.topicName,
888
- payload,
889
- idempotencyKey: options?.idempotencyKey,
890
- retentionSeconds: options?.retentionSeconds
891
- },
892
- this.transport
893
- );
894
- return { messageId: result.messageId };
895
- }
896
- /**
897
- * Create a consumer group for this topic
898
- * @param consumerGroupName Name of the consumer group
899
- * @param options Optional configuration for the consumer group
900
- * @returns A ConsumerGroup instance
901
- */
902
- consumerGroup(consumerGroupName, options) {
903
- const consumerOptions = {
904
- ...options,
905
- transport: options?.transport || this.transport
906
- };
907
- return new ConsumerGroup(
908
- this.client,
909
- this.topicName,
910
- consumerGroupName,
911
- consumerOptions
912
- );
913
- }
914
- /**
915
- * Get the topic name
916
- */
917
- get name() {
918
- return this.topicName;
919
- }
1589
+ };
920
1590
  /**
921
- * Get the transport used by this topic
1591
+ * Create a Web API route handler for processing queue callback messages.
1592
+ *
1593
+ * Parses incoming `Request` as a CloudEvent and invokes the handler.
1594
+ * For use on Vercel — Vercel invokes this route when messages are available.
1595
+ *
1596
+ * This is an arrow function property so it can be destructured:
1597
+ * ```typescript
1598
+ * const { handleCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1599
+ * export const POST = handleCallback(handler);
1600
+ * ```
1601
+ *
1602
+ * @param handler - Function to process the message payload and metadata
1603
+ * @param options - Optional configuration
1604
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1605
+ * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1606
+ * reschedule the message for redelivery after N seconds.
1607
+ * @returns A `(request: Request) => Promise<Response>` route handler
922
1608
  */
923
- get serializer() {
924
- return this.transport;
925
- }
926
- };
927
-
928
- // src/factory.ts
929
- async function send(topicName, payload, options) {
930
- const transport = options?.transport || new JsonTransport();
931
- const client = QueueClient._getDefaultInstance();
932
- const result = await client.sendMessage(
933
- {
934
- queueName: topicName,
935
- payload,
936
- idempotencyKey: options?.idempotencyKey,
937
- retentionSeconds: options?.retentionSeconds
938
- },
939
- transport
940
- );
941
- return { messageId: result.messageId };
942
- }
943
- async function receive(topicName, consumerGroup, handler, options) {
944
- const transport = options?.transport || new JsonTransport();
945
- const client = QueueClient._getDefaultInstance();
946
- const topic = new Topic(client, topicName, transport);
947
- const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
948
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
949
- if (messageId) {
950
- if (skipPayload) {
951
- return consumer.consume(handler, {
952
- messageId,
953
- skipPayload: true
954
- });
955
- } else {
956
- return consumer.consume(handler, { messageId });
957
- }
958
- } else {
959
- return consumer.consume(handler);
960
- }
961
- }
962
-
963
- // src/callback.ts
964
- async function parseCallbackRequest(request) {
965
- const contentType = request.headers.get("content-type");
966
- if (!contentType || !contentType.includes("application/cloudevents+json")) {
967
- throw new Error(
968
- "Invalid content type: expected 'application/cloudevents+json'"
969
- );
970
- }
971
- let cloudEvent;
972
- try {
973
- cloudEvent = await request.json();
974
- } catch (error) {
975
- throw new Error("Failed to parse CloudEvent from request body");
976
- }
977
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
978
- throw new Error("Invalid CloudEvent: missing required fields");
979
- }
980
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
981
- throw new Error(
982
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
983
- );
984
- }
985
- const missingFields = [];
986
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
987
- if (!("consumerGroup" in cloudEvent.data))
988
- missingFields.push("consumerGroup");
989
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
990
- if (missingFields.length > 0) {
991
- throw new Error(
992
- `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
993
- );
994
- }
995
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
996
- return {
997
- queueName,
998
- consumerGroup,
999
- messageId
1000
- };
1001
- }
1002
- function handleCallback(handlers) {
1003
- return async (request) => {
1004
- try {
1005
- const { queueName, consumerGroup, messageId } = await parseCallbackRequest(request);
1006
- const topicHandler = handlers[queueName];
1007
- if (!topicHandler) {
1008
- const availableTopics = Object.keys(handlers).join(", ");
1609
+ handleCallback = (handler, options) => {
1610
+ return async (request) => {
1611
+ try {
1612
+ const parsed = await parseCallback(request);
1613
+ await handleCallback(handler, parsed, {
1614
+ client: this,
1615
+ visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
1616
+ retry: options?.retry
1617
+ });
1618
+ return Response.json({ status: "success" });
1619
+ } catch (error) {
1620
+ console.error("Queue callback error:", error);
1621
+ if (error instanceof Error && (error.message.includes("Invalid content type") || error.message.includes("Invalid CloudEvent") || error.message.includes("Missing required CloudEvent") || error.message.includes("Failed to parse CloudEvent") || error.message.includes("Binary mode callback"))) {
1622
+ return Response.json({ error: error.message }, { status: 400 });
1623
+ }
1009
1624
  return Response.json(
1010
- {
1011
- error: `No handler found for topic: ${queueName}`,
1012
- availableTopics
1013
- },
1014
- { status: 404 }
1625
+ { error: "Failed to process queue message" },
1626
+ { status: 500 }
1015
1627
  );
1016
1628
  }
1017
- const consumerGroupHandler = topicHandler[consumerGroup];
1018
- if (!consumerGroupHandler) {
1019
- const availableGroups = Object.keys(topicHandler).join(", ");
1020
- return Response.json(
1021
- {
1022
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1023
- availableGroups
1024
- },
1025
- { status: 404 }
1026
- );
1629
+ };
1630
+ };
1631
+ /**
1632
+ * Create a Connect-style route handler for processing queue callback messages.
1633
+ * For use on Vercel — Vercel invokes this route when messages are available.
1634
+ *
1635
+ * For frameworks using the `(req, res)` middleware pattern where `req.body`
1636
+ * is pre-parsed (Next.js Pages Router, etc.).
1637
+ *
1638
+ * This is an arrow function property so it can be destructured:
1639
+ * ```typescript
1640
+ * const { handleNodeCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
1641
+ * app.post("/api/queue", handleNodeCallback(handler));
1642
+ * ```
1643
+ *
1644
+ * @param handler - Function to process the message payload and metadata
1645
+ * @param options - Optional configuration
1646
+ * @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
1647
+ * @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
1648
+ * reschedule the message for redelivery after N seconds.
1649
+ * @returns A `(req, res) => Promise<void>` route handler
1650
+ */
1651
+ handleNodeCallback = (handler, options) => {
1652
+ return async (req, res) => {
1653
+ if (req.method !== "POST") {
1654
+ res.status(200).end();
1655
+ return;
1027
1656
  }
1028
- const client = new QueueClient();
1029
- const topic = new Topic(client, queueName);
1030
- const cg = topic.consumerGroup(consumerGroup);
1031
- await cg.consume(consumerGroupHandler, { messageId });
1032
- return Response.json({ status: "success" });
1033
- } catch (error) {
1034
- console.error("Queue callback error:", error);
1035
- 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"))) {
1036
- return Response.json({ error: error.message }, { status: 400 });
1657
+ try {
1658
+ const parsed = parseRawCallback(req.body, req.headers);
1659
+ await handleCallback(handler, parsed, {
1660
+ client: this,
1661
+ visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
1662
+ retry: options?.retry
1663
+ });
1664
+ res.status(200).json({ status: "success" });
1665
+ } catch (error) {
1666
+ console.error("Queue callback error:", error);
1667
+ if (error instanceof Error && (error.message.includes("Invalid content type") || error.message.includes("Invalid CloudEvent") || error.message.includes("Missing required CloudEvent") || error.message.includes("Failed to parse CloudEvent") || error.message.includes("Binary mode callback"))) {
1668
+ res.status(400).json({ error: error.message });
1669
+ return;
1670
+ }
1671
+ res.status(500).json({ error: "Failed to process queue message" });
1037
1672
  }
1038
- return Response.json(
1039
- { error: "Failed to process queue message" },
1040
- { status: 500 }
1041
- );
1042
- }
1673
+ };
1043
1674
  };
1044
- }
1675
+ };
1045
1676
  // Annotate the CommonJS export names for ESM import in node:
1046
1677
  0 && (module.exports = {
1047
1678
  BadRequestError,
1048
1679
  BufferTransport,
1680
+ CLOUD_EVENT_TYPE_V1BETA,
1681
+ CLOUD_EVENT_TYPE_V2BETA,
1682
+ ConsumerDiscoveryError,
1683
+ ConsumerRegistryNotConfiguredError,
1684
+ DuplicateMessageError,
1049
1685
  ForbiddenError,
1050
1686
  InternalServerError,
1051
1687
  InvalidLimitError,
1052
1688
  JsonTransport,
1689
+ MessageAlreadyProcessedError,
1053
1690
  MessageCorruptedError,
1054
1691
  MessageLockedError,
1055
1692
  MessageNotAvailableError,
1056
1693
  MessageNotFoundError,
1694
+ QueueClient,
1057
1695
  QueueEmptyError,
1058
1696
  StreamTransport,
1059
1697
  UnauthorizedError,
1060
- handleCallback,
1061
- receive,
1062
- send
1698
+ parseCallback,
1699
+ parseRawCallback
1063
1700
  });
1064
1701
  //# sourceMappingURL=index.js.map