@vercel/queue 0.0.0-alpha.4 → 0.0.0-alpha.40

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,14 +32,16 @@ var index_exports = {};
22
32
  __export(index_exports, {
23
33
  BadRequestError: () => BadRequestError,
24
34
  BufferTransport: () => BufferTransport,
25
- ConsumerGroup: () => ConsumerGroup,
26
- FailedDependencyError: () => FailedDependencyError,
27
- FifoOrderingViolationError: () => FifoOrderingViolationError,
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,
28
40
  ForbiddenError: () => ForbiddenError,
29
41
  InternalServerError: () => InternalServerError,
30
- InvalidCallbackError: () => InvalidCallbackError,
31
42
  InvalidLimitError: () => InvalidLimitError,
32
43
  JsonTransport: () => JsonTransport,
44
+ MessageAlreadyProcessedError: () => MessageAlreadyProcessedError,
33
45
  MessageCorruptedError: () => MessageCorruptedError,
34
46
  MessageLockedError: () => MessageLockedError,
35
47
  MessageNotAvailableError: () => MessageNotAvailableError,
@@ -37,128 +49,91 @@ __export(index_exports, {
37
49
  QueueClient: () => QueueClient,
38
50
  QueueEmptyError: () => QueueEmptyError,
39
51
  StreamTransport: () => StreamTransport,
40
- Topic: () => Topic,
41
52
  UnauthorizedError: () => UnauthorizedError,
42
- createTopic: () => createTopic,
43
- handleCallback: () => handleCallback,
44
- parseCallbackRequest: () => parseCallbackRequest,
45
- receive: () => receive,
46
- send: () => send
53
+ parseCallback: () => parseCallback,
54
+ parseRawCallback: () => parseRawCallback
47
55
  });
48
56
  module.exports = __toCommonJS(index_exports);
49
57
 
50
- // src/client.ts
51
- var import_mixpart = require("mixpart");
52
-
53
- // src/local.ts
54
- var import_node_child_process = require("child_process");
55
- function isLocalhostWithPort(url) {
58
+ // src/transports.ts
59
+ async function streamToBuffer(stream) {
60
+ let totalLength = 0;
61
+ const reader = stream.getReader();
62
+ const chunks = [];
56
63
  try {
57
- const parsedUrl = new URL(url);
58
- const isLocalhost = parsedUrl.hostname === "localhost";
59
- const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 0;
60
- return { isLocalhost, port };
61
- } catch {
62
- return { isLocalhost: false };
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();
63
72
  }
73
+ return Buffer.concat(chunks, totalLength);
64
74
  }
65
- function isSupportedPlatform() {
66
- const platform = process.platform;
67
- return platform === "darwin" || platform === "linux";
68
- }
69
- function processDevelopmentCallbacks(callbacks) {
70
- const isDevelopment = process.env.NODE_ENV === "development";
71
- if (!isDevelopment) {
72
- return [];
75
+ var JsonTransport = class {
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;
73
88
  }
74
- if (!isSupportedPlatform()) {
75
- const hasLocalhostCallbacks = Object.values(callbacks).some((config) => {
76
- const { isLocalhost } = isLocalhostWithPort(config.url);
77
- return isLocalhost;
78
- });
79
- if (hasLocalhostCallbacks) {
80
- console.warn(
81
- `Queue Development Mode: Localhost callbacks are not supported on ${process.platform}. Localhost callback handling requires bash, nc, and curl which are available on macOS and Linux only. Consider using a production callback URL or developing on a supported platform.`
82
- );
83
- }
84
- return [];
89
+ serialize(value) {
90
+ return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
85
91
  }
86
- const localhostCallbacks = [];
87
- Object.entries(callbacks).forEach(([group, config]) => {
88
- const { isLocalhost, port } = isLocalhostWithPort(config.url);
89
- if (isLocalhost && port && port > 0) {
90
- localhostCallbacks.push({ group, config, port });
91
- } else {
92
- console.warn(
93
- `Queue Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
94
- );
95
- }
96
- });
97
- return localhostCallbacks;
98
- }
99
- function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
100
- localhostCallbacks.forEach(({ group, config, port }) => {
101
- const callbackHeaders = new Headers();
102
- callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
103
- callbackHeaders.set("Vqs-Queue-Name", queueName);
104
- callbackHeaders.set("Vqs-Consumer-Group", group);
105
- fireAndForgetWaitForHttpReady(
106
- config.url,
107
- port,
108
- config.delay || 0,
109
- 3,
110
- // Default retry frequency
111
- callbackHeaders
112
- );
113
- });
114
- }
115
- function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
116
- if (!isSupportedPlatform()) {
117
- console.warn(
118
- `Queue: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
119
- );
120
- return;
92
+ async deserialize(stream) {
93
+ const buffer = await streamToBuffer(stream);
94
+ return JSON.parse(buffer.toString("utf8"), this.reviver);
121
95
  }
122
- let headerArgs = "";
123
- if (headers) {
124
- const headerArray = [];
125
- headers.forEach((value, key) => {
126
- headerArray.push(`-H '${key}: ${value}'`);
127
- });
128
- headerArgs = headerArray.join(" ");
96
+ };
97
+ var BufferTransport = class {
98
+ contentType = "application/octet-stream";
99
+ serialize(value) {
100
+ return value;
101
+ }
102
+ async deserialize(stream) {
103
+ return await streamToBuffer(stream);
104
+ }
105
+ };
106
+ var StreamTransport = class {
107
+ contentType = "application/octet-stream";
108
+ serialize(value) {
109
+ return value;
110
+ }
111
+ async deserialize(stream) {
112
+ return stream;
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
+ */
118
+ async finalize(payload) {
119
+ const reader = payload.getReader();
120
+ try {
121
+ while (true) {
122
+ const { done } = await reader.read();
123
+ if (done) break;
124
+ }
125
+ } finally {
126
+ reader.releaseLock();
127
+ }
129
128
  }
130
- const bashScript = `
131
- # Wait for any initial boot time
132
- sleep ${initialDelaySeconds}
129
+ };
133
130
 
134
- missed=0
135
- while true; do
136
- # 1) Check if TCP port is listening
137
- if nc -z localhost ${port} 2>/dev/null; then
138
- missed=0
139
- # 2) If port is open, try HTTP POST check
140
- if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
141
- # Success: port is up AND HTTP returned 2xx (following redirects)
142
- exit 0
143
- fi
144
- else
145
- # Port was closed\u2014increment miss counter
146
- ((missed+=1))
147
- # If closed twice in a row, give up immediately
148
- if [ "$missed" -ge 2 ]; then
149
- exit 1
150
- fi
151
- fi
152
- # Wait before next cycle
153
- sleep ${retryFrequencySeconds}
154
- done
155
- `;
156
- const childProcess = (0, import_node_child_process.spawn)("bash", ["-c", bashScript], {
157
- stdio: "ignore",
158
- detached: true
159
- });
160
- childProcess.unref();
161
- }
131
+ // src/api-client.ts
132
+ var import_mixpart = require("mixpart");
133
+
134
+ // src/dev.ts
135
+ var fs = __toESM(require("fs"));
136
+ var path = __toESM(require("path"));
162
137
 
163
138
  // src/types.ts
164
139
  var MessageNotFoundError = class extends Error {
@@ -175,12 +150,6 @@ var MessageNotAvailableError = class extends Error {
175
150
  this.name = "MessageNotAvailableError";
176
151
  }
177
152
  };
178
- var FifoOrderingViolationError = class extends Error {
179
- constructor(messageId, reason) {
180
- super(`FIFO ordering violation for message ${messageId}: ${reason}`);
181
- this.name = "FifoOrderingViolationError";
182
- }
183
- };
184
153
  var MessageCorruptedError = class extends Error {
185
154
  constructor(messageId, reason) {
186
155
  super(`Message ${messageId} is corrupted: ${reason}`);
@@ -196,6 +165,7 @@ var QueueEmptyError = class extends Error {
196
165
  }
197
166
  };
198
167
  var MessageLockedError = class extends Error {
168
+ /** Suggested retry delay in seconds, if provided by the server. */
199
169
  retryAfter;
200
170
  constructor(messageId, retryAfter) {
201
171
  const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
@@ -222,14 +192,6 @@ var BadRequestError = class extends Error {
222
192
  this.name = "BadRequestError";
223
193
  }
224
194
  };
225
- var FailedDependencyError = class extends Error {
226
- constructor(messageId) {
227
- super(
228
- `Failed dependency: FIFO ordering violation for message ${messageId}`
229
- );
230
- this.name = "FailedDependencyError";
231
- }
232
- };
233
195
  var InternalServerError = class extends Error {
234
196
  constructor(message = "Unexpected server error") {
235
197
  super(message);
@@ -242,981 +204,1489 @@ var InvalidLimitError = class extends Error {
242
204
  this.name = "InvalidLimitError";
243
205
  }
244
206
  };
245
- var InvalidCallbackError = class extends Error {
246
- constructor(message) {
247
- super(message);
248
- this.name = "InvalidCallbackError";
207
+ var MessageAlreadyProcessedError = class extends Error {
208
+ constructor(messageId) {
209
+ super(`Message ${messageId} has already been processed`);
210
+ this.name = "MessageAlreadyProcessedError";
249
211
  }
250
212
  };
251
-
252
- // src/client.ts
253
- async function consumeStream(stream) {
254
- const reader = stream.getReader();
255
- try {
256
- while (true) {
257
- const { done } = await reader.read();
258
- if (done) break;
259
- }
260
- } finally {
261
- reader.releaseLock();
213
+ var DuplicateMessageError = class extends Error {
214
+ idempotencyKey;
215
+ constructor(message, idempotencyKey) {
216
+ super(message);
217
+ this.name = "DuplicateMessageError";
218
+ this.idempotencyKey = idempotencyKey;
262
219
  }
263
- }
264
- function parseQueueHeaders(headers) {
265
- const messageId = headers.get("Vqs-Message-Id");
266
- const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
267
- const timestamp = headers.get("Vqs-Timestamp");
268
- const contentType = headers.get("Content-Type") || "application/octet-stream";
269
- const ticket = headers.get("Vqs-Ticket");
270
- if (!messageId || !timestamp || !ticket) {
271
- 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;
272
227
  }
273
- const deliveryCount = parseInt(deliveryCountStr, 10);
274
- if (isNaN(deliveryCount)) {
275
- return null;
228
+ };
229
+ var ConsumerRegistryNotConfiguredError = class extends Error {
230
+ constructor(message = "Consumer registry not configured") {
231
+ super(message);
232
+ this.name = "ConsumerRegistryNotConfiguredError";
276
233
  }
277
- return {
278
- messageId,
279
- deliveryCount,
280
- timestamp,
281
- contentType,
282
- ticket
283
- };
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
+ );
284
247
  }
285
- var QueueClient = class _QueueClient {
286
- baseUrl;
287
- token;
288
- /**
289
- * Internal default instance for use by createTopic and other convenience functions
290
- * @internal
291
- */
292
- static _defaultInstance = null;
293
- /**
294
- * Create a new Vercel Queue Service client
295
- * @param options Client configuration options (optional - will auto-detect Vercel Function environment)
296
- */
297
- constructor(options = {}) {
298
- this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
299
- if (options.token) {
300
- this.token = options.token;
301
- } else {
302
- const token = this.getVercelOidcTokenSync();
303
- if (!token) {
304
- throw new Error(
305
- "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'"
306
- );
307
- }
308
- this.token = token;
309
- }
310
- }
248
+ var ConsumerGroup = class {
249
+ client;
250
+ topicName;
251
+ consumerGroupName;
252
+ visibilityTimeout;
311
253
  /**
312
- * Get the default client instance for internal use by convenience functions
313
- * @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)
314
261
  */
315
- static _getDefaultInstance() {
316
- if (!this._defaultInstance) {
317
- this._defaultInstance = new _QueueClient();
318
- }
319
- return this._defaultInstance;
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
+ );
320
270
  }
321
271
  /**
322
- * Synchronously get OIDC token from environment
323
- * Used internally by constructor - mirrors the logic from getVercelOidcToken but synchronously
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
324
277
  */
325
- getVercelOidcTokenSync() {
326
- try {
327
- const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
328
- const fromSymbol = globalThis;
329
- const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
330
- const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
331
- return token || null;
332
- } catch {
333
- return null;
334
- }
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;
335
284
  }
336
285
  /**
337
- * Send a message to a queue
338
- * @param options Send message options
339
- * @param transport Serializer/deserializer for the payload
340
- * @returns Promise with the message ID
341
- * @throws {BadRequestError} When request parameters are invalid
342
- * @throws {UnauthorizedError} When authentication fails
343
- * @throws {ForbiddenError} When access is denied (environment mismatch)
344
- * @throws {InternalServerError} When server encounters an error
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.
345
304
  */
346
- async sendMessage(options, transport) {
347
- const { queueName, payload, idempotencyKey, retentionSeconds, callback } = options;
348
- const headers = new Headers({
349
- Authorization: `Bearer ${this.token}`,
350
- "Vqs-Queue-Name": queueName,
351
- "Content-Type": transport.contentType
352
- });
353
- if (idempotencyKey) {
354
- headers.set("Vqs-Idempotency-Key", idempotencyKey);
355
- }
356
- if (retentionSeconds !== void 0) {
357
- headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
358
- }
359
- let normalizedCallbacks;
360
- if (callback) {
361
- if ("url" in callback && typeof callback.url === "string") {
362
- normalizedCallbacks = { default: callback };
363
- } else {
364
- normalizedCallbacks = callback;
365
- }
366
- }
367
- let localhostCallbacks = [];
368
- if (normalizedCallbacks) {
369
- const isDevelopment = process.env.NODE_ENV === "development";
370
- if (isDevelopment) {
371
- localhostCallbacks = processDevelopmentCallbacks(normalizedCallbacks);
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;
372
317
  } else {
373
- const endpoints = Object.entries(normalizedCallbacks).map(
374
- ([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
375
- ).join(",");
376
- headers.set("Vqs-Callback-Url", endpoints);
377
- const delays = Object.entries(normalizedCallbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
378
- if (delays) {
379
- headers.set("Vqs-Callback-Delay", delays);
380
- }
381
- const frequencies = Object.entries(normalizedCallbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
382
- if (frequencies) {
383
- headers.set("Vqs-Callback-Frequency", frequencies);
384
- }
318
+ firstDelayMs = 0;
385
319
  }
386
320
  }
387
- const body = transport.serialize(payload);
388
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
389
- method: "POST",
390
- headers,
391
- body
321
+ const lifecyclePromise = new Promise((resolve) => {
322
+ resolveLifecycle = resolve;
392
323
  });
393
- if (!response.ok) {
394
- if (response.status === 400) {
395
- const errorText = await response.text();
396
- throw new BadRequestError(errorText || "Invalid parameters");
324
+ const safeResolve = () => {
325
+ if (!isResolved) {
326
+ isResolved = true;
327
+ resolveLifecycle();
397
328
  }
398
- if (response.status === 401) {
399
- throw new UnauthorizedError();
329
+ };
330
+ const extend = async () => {
331
+ if (!isRunning) {
332
+ safeResolve();
333
+ return;
400
334
  }
401
- if (response.status === 403) {
402
- throw new ForbiddenError();
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
+ }
403
365
  }
404
- if (response.status === 409) {
405
- throw new Error("Duplicate idempotency key detected");
366
+ };
367
+ timeoutId = setTimeout(() => extend(), firstDelayMs);
368
+ return async (waitForCompletion = false) => {
369
+ isRunning = false;
370
+ if (timeoutId) {
371
+ clearTimeout(timeoutId);
372
+ timeoutId = null;
406
373
  }
407
- if (response.status >= 500) {
408
- throw new InternalServerError(
409
- `Server error: ${response.status} ${response.statusText}`
410
- );
374
+ if (waitForCompletion) {
375
+ await lifecyclePromise;
376
+ } else {
377
+ safeResolve();
411
378
  }
412
- throw new Error(
413
- `Failed to send message: ${response.status} ${response.statusText}`
414
- );
415
- }
416
- const responseData = await response.json();
417
- if (localhostCallbacks.length > 0) {
418
- fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
419
- }
420
- return responseData;
379
+ };
421
380
  }
422
381
  /**
423
- * Receive messages from a queue
424
- * @param options Receive messages options
425
- * @param transport Serializer/deserializer for the payload
426
- * @returns AsyncGenerator that yields messages as they arrive
427
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
428
- * @throws {QueueEmptyError} When no messages are available (204)
429
- * @throws {MessageLockedError} When FIFO queue has locked messages (423)
430
- * @throws {BadRequestError} When request parameters are invalid
431
- * @throws {UnauthorizedError} When authentication fails
432
- * @throws {ForbiddenError} When access is denied (environment mismatch)
433
- * @throws {InternalServerError} When server encounters an error
382
+ * Clean up the message payload if the transport supports it and payload exists.
434
383
  */
435
- async *receiveMessages(options, transport) {
436
- const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
437
- if (limit !== void 0 && (limit < 1 || limit > 10)) {
438
- throw new InvalidLimitError(limit);
439
- }
440
- const headers = new Headers({
441
- Authorization: `Bearer ${this.token}`,
442
- "Vqs-Queue-Name": queueName,
443
- "Vqs-Consumer-Group": consumerGroup,
444
- Accept: "multipart/mixed"
445
- });
446
- if (visibilityTimeoutSeconds !== void 0) {
447
- headers.set(
448
- "Vqs-Visibility-Timeout",
449
- visibilityTimeoutSeconds.toString()
450
- );
451
- }
452
- if (limit !== void 0) {
453
- headers.set("Vqs-Limit", limit.toString());
454
- }
455
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
456
- method: "GET",
457
- headers
458
- });
459
- if (response.status === 204) {
460
- throw new QueueEmptyError(queueName, consumerGroup);
461
- }
462
- if (!response.ok) {
463
- if (response.status === 400) {
464
- const errorText = await response.text();
465
- throw new BadRequestError(errorText || "Invalid parameters");
466
- }
467
- if (response.status === 401) {
468
- throw new UnauthorizedError();
469
- }
470
- if (response.status === 403) {
471
- throw new ForbiddenError();
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);
472
391
  }
473
- if (response.status === 423) {
474
- const retryAfterHeader = response.headers.get("Retry-After");
475
- let retryAfter;
476
- if (retryAfterHeader) {
477
- const parsed = parseInt(retryAfterHeader, 10);
478
- retryAfter = isNaN(parsed) ? void 0 : parsed;
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
+ };
408
+ try {
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
+ }
479
456
  }
480
- throw new MessageLockedError("next message in FIFO queue", retryAfter);
481
- }
482
- if (response.status >= 500) {
483
- throw new InternalServerError(
484
- `Server error: ${response.status} ${response.statusText}`
485
- );
486
457
  }
487
- throw new Error(
488
- `Failed to receive messages: ${response.status} ${response.statusText}`
489
- );
458
+ await this.finalizePayload(message.payload);
459
+ throw error;
490
460
  }
491
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
492
- try {
493
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
494
- if (!parsedHeaders) {
495
- console.warn("Missing required queue headers in multipart part");
496
- await consumeStream(multipartMessage.payload);
497
- continue;
498
- }
499
- const deserializedPayload = await transport.deserialize(
500
- multipartMessage.payload
501
- );
502
- const message = {
503
- ...parsedHeaders,
504
- payload: deserializedPayload
505
- };
506
- yield message;
507
- } catch (error) {
508
- console.warn("Failed to process multipart message:", error);
509
- await consumeStream(multipartMessage.payload);
461
+ }
462
+ /**
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.
478
+ */
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 });
510
504
  }
505
+ return messagesProcessed;
511
506
  }
512
507
  }
513
- async receiveMessageById(options, transport) {
514
- const {
515
- queueName,
516
- consumerGroup,
517
- messageId,
518
- visibilityTimeoutSeconds,
519
- skipPayload
520
- } = options;
521
- const headers = new Headers({
522
- Authorization: `Bearer ${this.token}`,
523
- "Vqs-Queue-Name": queueName,
524
- "Vqs-Consumer-Group": consumerGroup,
525
- Accept: "multipart/mixed"
508
+ /**
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
539
+ * @throws {BadRequestError} When request parameters are invalid
540
+ * @throws {UnauthorizedError} When authentication fails
541
+ * @throws {ForbiddenError} When access is denied (environment mismatch)
542
+ * @throws {InternalServerError} When server encounters an error
543
+ */
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
526
552
  });
527
- if (visibilityTimeoutSeconds !== void 0) {
528
- headers.set(
529
- "Vqs-Visibility-Timeout",
530
- visibilityTimeoutSeconds.toString()
553
+ if (result.messageId && isDevMode()) {
554
+ triggerDevCallbacks(
555
+ this.topicName,
556
+ result.messageId,
557
+ this.client.getRegion()
531
558
  );
532
559
  }
533
- if (skipPayload) {
534
- headers.set("Vqs-Skip-Payload", "1");
535
- }
536
- const response = await fetch(
537
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
538
- {
539
- method: "GET",
540
- headers
541
- }
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
542
574
  );
543
- if (!response.ok) {
544
- if (response.status === 400) {
545
- const errorText = await response.text();
546
- throw new BadRequestError(errorText || "Invalid parameters");
547
- }
548
- if (response.status === 401) {
549
- throw new UnauthorizedError();
550
- }
551
- if (response.status === 403) {
552
- throw new ForbiddenError();
553
- }
554
- if (response.status === 404) {
555
- throw new MessageNotFoundError(messageId);
556
- }
557
- if (response.status === 423) {
558
- const retryAfterHeader = response.headers.get("Retry-After");
559
- let retryAfter;
560
- if (retryAfterHeader) {
561
- const parsed = parseInt(retryAfterHeader, 10);
562
- retryAfter = isNaN(parsed) ? void 0 : parsed;
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
+ });
563
800
  }
564
- throw new MessageLockedError(messageId, retryAfter);
565
801
  }
566
- if (response.status === 424) {
567
- throw new FailedDependencyError(messageId);
568
- }
569
- if (response.status === 409) {
570
- throw new MessageNotAvailableError(messageId);
571
- }
572
- if (response.status >= 500) {
573
- throw new InternalServerError(
574
- `Server error: ${response.status} ${response.statusText}`
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
575
849
  );
850
+ continue;
576
851
  }
577
- throw new Error(
578
- `Failed to receive message by ID: ${response.status} ${response.statusText}`
579
- );
580
- }
581
- if (skipPayload && response.status === 204) {
582
- const parsedHeaders = parseQueueHeaders(response.headers);
583
- if (!parsedHeaders) {
584
- throw new MessageCorruptedError(
585
- messageId,
586
- "Missing required queue headers in 204 response"
852
+ if (error instanceof MessageAlreadyProcessedError) {
853
+ console.log(
854
+ `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
587
855
  );
856
+ return false;
588
857
  }
589
- const message = {
590
- ...parsedHeaders,
591
- payload: void 0
592
- };
593
- return { message };
858
+ console.error(
859
+ `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
860
+ error
861
+ );
862
+ return false;
594
863
  }
595
- if (!transport) {
596
- throw new Error("Transport is required when skipPayload is not true");
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;
597
907
  }
598
- try {
599
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
600
- try {
601
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
602
- if (!parsedHeaders) {
603
- console.warn("Missing required queue headers in multipart part");
604
- await consumeStream(multipartMessage.payload);
605
- continue;
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
+ );
606
949
  }
607
- const deserializedPayload = await transport.deserialize(
608
- multipartMessage.payload
609
- );
610
- const message = {
611
- ...parsedHeaders,
612
- payload: deserializedPayload
613
- };
614
- return { message };
615
- } catch (error) {
616
- console.warn("Failed to deserialize message by ID:", error);
617
- await consumeStream(multipartMessage.payload);
618
- throw new MessageCorruptedError(
619
- messageId,
620
- `Failed to deserialize payload: ${error}`
621
- );
622
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
+ );
623
956
  }
624
- } catch (error) {
625
- if (error instanceof MessageCorruptedError) {
626
- throw error;
627
- }
628
- throw new MessageCorruptedError(
629
- messageId,
630
- `Failed to parse multipart response: ${error}`
631
- );
632
957
  }
633
- throw new MessageNotFoundError(messageId);
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
+ }
634
1054
  }
635
1055
  /**
636
- * Delete a message (acknowledge processing)
637
- * @param options Delete message options
638
- * @returns Promise with delete status
639
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
640
- * @throws {MessageNotAvailableError} When message can't be deleted (409)
641
- * @throws {BadRequestError} When ticket is missing or invalid (400)
642
- * @throws {UnauthorizedError} When authentication fails
643
- * @throws {ForbiddenError} When access is denied (environment mismatch)
644
- * @throws {InternalServerError} When server encounters an error
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.
645
1060
  */
646
- async deleteMessage(options) {
647
- const { queueName, consumerGroup, messageId, ticket } = options;
648
- const response = await fetch(
649
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
650
- {
651
- method: "DELETE",
652
- headers: new Headers({
653
- Authorization: `Bearer ${this.token}`,
654
- "Vqs-Queue-Name": queueName,
655
- "Vqs-Consumer-Group": consumerGroup,
656
- "Vqs-Ticket": ticket
657
- })
658
- }
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 })'
659
1083
  );
660
- if (!response.ok) {
661
- if (response.status === 400) {
662
- throw new BadRequestError("Missing or invalid ticket");
663
- }
664
- if (response.status === 401) {
665
- throw new UnauthorizedError();
666
- }
667
- if (response.status === 403) {
668
- throw new ForbiddenError();
669
- }
670
- if (response.status === 404) {
671
- throw new MessageNotFoundError(messageId);
672
- }
673
- if (response.status === 409) {
674
- throw new MessageNotAvailableError(
675
- messageId,
676
- "Invalid ticket, message not in correct state, or already processed"
677
- );
678
- }
679
- if (response.status >= 500) {
680
- throw new InternalServerError(
681
- `Server error: ${response.status} ${response.statusText}`
682
- );
683
- }
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) {
684
1105
  throw new Error(
685
- `Failed to delete message: ${response.status} ${response.statusText}`
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'"
686
1107
  );
687
1108
  }
688
- return { deleted: true };
1109
+ return token;
689
1110
  }
690
- /**
691
- * Change the visibility timeout of a message
692
- * @param options Change visibility options
693
- * @returns Promise with update status
694
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
695
- * @throws {MessageNotAvailableError} When message can't be updated (409)
696
- * @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
697
- * @throws {UnauthorizedError} When authentication fails
698
- * @throws {ForbiddenError} When access is denied (environment mismatch)
699
- * @throws {InternalServerError} When server encounters an error
700
- */
701
- async changeVisibility(options) {
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.0-alpha.40"}`);
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;
702
1156
  const {
703
1157
  queueName,
704
- consumerGroup,
705
- messageId,
706
- ticket,
707
- visibilityTimeoutSeconds
1158
+ payload,
1159
+ idempotencyKey,
1160
+ retentionSeconds,
1161
+ delaySeconds,
1162
+ headers: optionHeaders
708
1163
  } = options;
709
- const response = await fetch(
710
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
711
- {
712
- method: "PATCH",
713
- headers: new Headers({
714
- Authorization: `Bearer ${this.token}`,
715
- "Vqs-Queue-Name": queueName,
716
- "Vqs-Consumer-Group": consumerGroup,
717
- "Vqs-Ticket": ticket,
718
- "Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString()
719
- })
1164
+ const headers = new Headers();
1165
+ if (this.customHeaders) {
1166
+ for (const [name, value] of Object.entries(this.customHeaders)) {
1167
+ headers.append(name, value);
720
1168
  }
721
- );
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
+ });
722
1205
  if (!response.ok) {
723
- if (response.status === 400) {
724
- throw new BadRequestError(
725
- "Missing ticket or invalid visibility timeout"
1206
+ const errorText = await response.text();
1207
+ if (response.status === 409) {
1208
+ throw new DuplicateMessageError(
1209
+ errorText || "Duplicate idempotency key detected",
1210
+ idempotencyKey
726
1211
  );
727
1212
  }
728
- if (response.status === 401) {
729
- throw new UnauthorizedError();
730
- }
731
- if (response.status === 403) {
732
- throw new ForbiddenError();
733
- }
734
- if (response.status === 404) {
735
- throw new MessageNotFoundError(messageId);
736
- }
737
- if (response.status === 409) {
738
- throw new MessageNotAvailableError(
739
- messageId,
740
- "Invalid ticket, message not in correct state, or already processed"
1213
+ if (response.status === 502) {
1214
+ throw new ConsumerDiscoveryError(
1215
+ errorText || "Consumer discovery failed",
1216
+ deploymentId
741
1217
  );
742
1218
  }
743
- if (response.status >= 500) {
744
- throw new InternalServerError(
745
- `Server error: ${response.status} ${response.statusText}`
1219
+ if (response.status === 503) {
1220
+ throw new ConsumerRegistryNotConfiguredError(
1221
+ errorText || "Consumer registry not configured"
746
1222
  );
747
1223
  }
748
- throw new Error(
749
- `Failed to change visibility: ${response.status} ${response.statusText}`
1224
+ throwCommonHttpError(
1225
+ response.status,
1226
+ response.statusText,
1227
+ errorText,
1228
+ "send message"
750
1229
  );
751
1230
  }
752
- return { updated: true };
753
- }
754
- };
755
-
756
- // src/transports.ts
757
- var JsonTransport = class {
758
- contentType = "application/json";
759
- serialize(value) {
760
- return Buffer.from(JSON.stringify(value), "utf8");
1231
+ if (response.status === 202) {
1232
+ return { messageId: null };
1233
+ }
1234
+ const responseData = await response.json();
1235
+ return responseData;
761
1236
  }
762
- async deserialize(stream) {
763
- const reader = stream.getReader();
764
- const chunks = [];
765
- try {
766
- while (true) {
767
- const { done, value } = await reader.read();
768
- if (done) break;
769
- chunks.push(value);
770
- }
771
- } finally {
772
- reader.releaseLock();
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);
773
1242
  }
774
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
775
- const buffer = new Uint8Array(totalLength);
776
- let offset = 0;
777
- for (const chunk of chunks) {
778
- buffer.set(chunk, offset);
779
- offset += chunk.length;
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
+ );
780
1253
  }
781
- return JSON.parse(Buffer.from(buffer).toString("utf8"));
782
- }
783
- };
784
- var BufferTransport = class {
785
- contentType = "application/octet-stream";
786
- serialize(value) {
787
- return value;
788
- }
789
- async deserialize(stream) {
790
- const reader = stream.getReader();
791
- const chunks = [];
792
- try {
793
- while (true) {
794
- const { done, value } = await reader.read();
795
- if (done) break;
796
- chunks.push(value);
797
- }
798
- } finally {
799
- reader.releaseLock();
1254
+ if (limit !== void 0) {
1255
+ headers.set("Vqs-Max-Messages", limit.toString());
800
1256
  }
801
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
802
- const buffer = new Uint8Array(totalLength);
803
- let offset = 0;
804
- for (const chunk of chunks) {
805
- buffer.set(chunk, offset);
806
- offset += chunk.length;
1257
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1258
+ if (effectiveDeploymentId) {
1259
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
807
1260
  }
808
- return Buffer.from(buffer);
809
- }
810
- };
811
- var StreamTransport = class {
812
- contentType = "application/octet-stream";
813
- serialize(value) {
814
- return value;
815
- }
816
- async deserialize(stream) {
817
- return stream;
818
- }
819
- async finalize(payload) {
820
- const reader = payload.getReader();
821
- try {
822
- while (true) {
823
- const { done } = await reader.read();
824
- if (done) break;
1261
+ const response = await this.fetch(
1262
+ this.buildUrl(queueName, "consumer", consumerGroup),
1263
+ {
1264
+ method: "POST",
1265
+ headers
825
1266
  }
826
- } finally {
827
- reader.releaseLock();
1267
+ );
1268
+ if (response.status === 204) {
1269
+ return;
828
1270
  }
829
- }
830
- };
831
-
832
- // src/consumer-group.ts
833
- var ConsumerGroup = class {
834
- client;
835
- topicName;
836
- consumerGroupName;
837
- visibilityTimeout;
838
- refreshInterval;
839
- transport;
840
- /**
841
- * Create a new ConsumerGroup instance
842
- * @param client QueueClient instance to use for API calls
843
- * @param topicName Name of the topic to consume from
844
- * @param consumerGroupName Name of the consumer group
845
- * @param options Optional configuration
846
- */
847
- constructor(client, topicName, consumerGroupName, options = {}) {
848
- this.client = client;
849
- this.topicName = topicName;
850
- this.consumerGroupName = consumerGroupName;
851
- this.visibilityTimeout = options.visibilityTimeoutSeconds || 30;
852
- this.refreshInterval = options.refreshInterval || 10;
853
- this.transport = options.transport || new JsonTransport();
854
- }
855
- /**
856
- * Starts a background loop that periodically extends the visibility timeout for a message.
857
- * This prevents the message from becoming visible to other consumers while it's being processed.
858
- *
859
- * The extension loop runs every `refreshInterval` seconds and updates the message's
860
- * visibility timeout to `visibilityTimeout` seconds from the current time.
861
- *
862
- * @param messageId - The unique identifier of the message to extend visibility for
863
- * @param ticket - The receipt ticket that proves ownership of the message
864
- * @returns A function that when called will stop the extension loop
865
- *
866
- * @remarks
867
- * - The first extension attempt occurs after `refreshInterval` seconds, not immediately
868
- * - If an extension fails, the loop terminates with an error logged to console
869
- * - The returned stop function is idempotent - calling it multiple times is safe
870
- * - By default, the stop function returns immediately without waiting for in-flight
871
- * - Pass `true` to the stop function to wait for any in-flight extension to complete
872
- */
873
- startVisibilityExtension(messageId, ticket) {
874
- let isRunning = true;
875
- let resolveLifecycle;
876
- let timeoutId = null;
877
- const lifecyclePromise = new Promise((resolve) => {
878
- resolveLifecycle = resolve;
879
- });
880
- const extend = async () => {
881
- if (!isRunning) {
882
- resolveLifecycle();
883
- return;
884
- }
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)) {
885
1281
  try {
886
- await this.client.changeVisibility({
887
- queueName: this.topicName,
888
- consumerGroup: this.consumerGroupName,
889
- messageId,
890
- ticket,
891
- visibilityTimeoutSeconds: this.visibilityTimeout
892
- });
893
- if (isRunning) {
894
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
895
- } else {
896
- resolveLifecycle();
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;
897
1287
  }
898
- } catch (error) {
899
- console.error(
900
- `Failed to extend visibility for message ${messageId}:`,
901
- error
1288
+ const deserializedPayload = await transport.deserialize(
1289
+ multipartMessage.payload
902
1290
  );
903
- resolveLifecycle();
904
- }
905
- };
906
- timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
907
- return async (waitForCompletion = false) => {
908
- isRunning = false;
909
- if (timeoutId) {
910
- clearTimeout(timeoutId);
911
- timeoutId = null;
912
- }
913
- if (waitForCompletion) {
914
- await lifecyclePromise;
915
- } else {
916
- resolveLifecycle();
1291
+ const message = {
1292
+ ...parsedHeaders,
1293
+ payload: deserializedPayload
1294
+ };
1295
+ yield message;
1296
+ } catch (error) {
1297
+ console.warn("Failed to process multipart message:", error);
1298
+ await consumeStream(multipartMessage.payload);
917
1299
  }
918
- };
1300
+ }
919
1301
  }
920
- /**
921
- * Process a single message with the given handler
922
- * @param message The message to process
923
- * @param handler Function to process the message
924
- */
925
- async processMessage(message, handler) {
926
- const stopExtension = this.startVisibilityExtension(
927
- message.messageId,
928
- message.ticket
1302
+ async receiveMessageById(options) {
1303
+ const transport = this.transport;
1304
+ const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
1305
+ const headers = new Headers({
1306
+ Authorization: `Bearer ${await this.getToken()}`,
1307
+ Accept: "multipart/mixed",
1308
+ ...this.customHeaders
1309
+ });
1310
+ if (visibilityTimeoutSeconds !== void 0) {
1311
+ headers.set(
1312
+ "Vqs-Visibility-Timeout-Seconds",
1313
+ visibilityTimeoutSeconds.toString()
1314
+ );
1315
+ }
1316
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
1317
+ if (effectiveDeploymentId) {
1318
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
1319
+ }
1320
+ const response = await this.fetch(
1321
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
1322
+ {
1323
+ method: "POST",
1324
+ headers
1325
+ }
929
1326
  );
930
- try {
931
- const result = await handler(message.payload, {
932
- messageId: message.messageId,
933
- deliveryCount: message.deliveryCount,
934
- timestamp: message.timestamp
935
- });
936
- await stopExtension();
937
- if (result && "timeoutSeconds" in result) {
938
- await this.client.changeVisibility({
939
- queueName: this.topicName,
940
- consumerGroup: this.consumerGroupName,
941
- messageId: message.messageId,
942
- ticket: message.ticket,
943
- visibilityTimeoutSeconds: result.timeoutSeconds
944
- });
945
- } else {
946
- await this.client.deleteMessage({
947
- queueName: this.topicName,
948
- consumerGroup: this.consumerGroupName,
949
- messageId: message.messageId,
950
- ticket: message.ticket
951
- });
1327
+ if (!response.ok) {
1328
+ const errorText = await response.text();
1329
+ if (response.status === 404) {
1330
+ throw new MessageNotFoundError(messageId);
952
1331
  }
953
- } catch (error) {
954
- await stopExtension();
955
- if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
1332
+ if (response.status === 409) {
1333
+ let errorData = {};
956
1334
  try {
957
- await this.transport.finalize(message.payload);
958
- } catch (finalizeError) {
959
- console.warn("Failed to finalize message payload:", finalizeError);
1335
+ errorData = JSON.parse(errorText);
1336
+ } catch {
960
1337
  }
1338
+ if (errorData.originalMessageId) {
1339
+ throw new MessageNotAvailableError(
1340
+ messageId,
1341
+ `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
1342
+ );
1343
+ }
1344
+ throw new MessageNotAvailableError(messageId);
961
1345
  }
962
- throw error;
1346
+ if (response.status === 410) {
1347
+ throw new MessageAlreadyProcessedError(messageId);
1348
+ }
1349
+ throwCommonHttpError(
1350
+ response.status,
1351
+ response.statusText,
1352
+ errorText,
1353
+ "receive message by ID"
1354
+ );
963
1355
  }
964
- }
965
- async consume(handler, options) {
966
- if (options?.messageId) {
967
- if (options.skipPayload) {
968
- const response = await this.client.receiveMessageById(
969
- {
970
- queueName: this.topicName,
971
- consumerGroup: this.consumerGroupName,
972
- messageId: options.messageId,
973
- visibilityTimeoutSeconds: this.visibilityTimeout,
974
- skipPayload: true
975
- },
976
- this.transport
977
- );
978
- await this.processMessage(
979
- response.message,
980
- handler
981
- );
982
- } else {
983
- const response = await this.client.receiveMessageById(
984
- {
985
- queueName: this.topicName,
986
- consumerGroup: this.consumerGroupName,
987
- messageId: options.messageId,
988
- visibilityTimeoutSeconds: this.visibilityTimeout
989
- },
990
- this.transport
991
- );
992
- await this.processMessage(
993
- response.message,
994
- handler
1356
+ for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
1357
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
1358
+ if (!parsedHeaders) {
1359
+ await consumeStream(multipartMessage.payload);
1360
+ throw new MessageCorruptedError(
1361
+ messageId,
1362
+ "Missing required queue headers in response"
995
1363
  );
996
1364
  }
997
- } else {
998
- let messageFound = false;
999
- for await (const message of this.client.receiveMessages(
1000
- {
1001
- queueName: this.topicName,
1002
- consumerGroup: this.consumerGroupName,
1003
- visibilityTimeoutSeconds: this.visibilityTimeout,
1004
- limit: 1
1005
- },
1006
- this.transport
1007
- )) {
1008
- messageFound = true;
1009
- await this.processMessage(message, handler);
1010
- break;
1365
+ const deserializedPayload = await transport.deserialize(
1366
+ multipartMessage.payload
1367
+ );
1368
+ const message = {
1369
+ ...parsedHeaders,
1370
+ payload: deserializedPayload
1371
+ };
1372
+ return { message };
1373
+ }
1374
+ throw new MessageNotFoundError(messageId);
1375
+ }
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
+ ),
1394
+ {
1395
+ method: "DELETE",
1396
+ headers
1397
+ }
1398
+ );
1399
+ if (!response.ok) {
1400
+ const errorText = await response.text();
1401
+ if (response.status === 404) {
1402
+ throw new MessageNotFoundError(receiptHandle);
1011
1403
  }
1012
- if (!messageFound) {
1013
- throw new Error("No messages available");
1404
+ if (response.status === 409) {
1405
+ throw new MessageNotAvailableError(
1406
+ receiptHandle,
1407
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
1408
+ );
1014
1409
  }
1410
+ throwCommonHttpError(
1411
+ response.status,
1412
+ response.statusText,
1413
+ errorText,
1414
+ "acknowledge message",
1415
+ "Missing or invalid receipt handle"
1416
+ );
1015
1417
  }
1418
+ return { acknowledged: true };
1016
1419
  }
1017
- /**
1018
- * Get the consumer group name
1019
- */
1020
- get name() {
1021
- return this.consumerGroupName;
1022
- }
1023
- /**
1024
- * Get the topic name this consumer group is subscribed to
1025
- */
1026
- get topic() {
1027
- return this.topicName;
1028
- }
1029
- };
1030
-
1031
- // src/topic.ts
1032
- var Topic = class {
1033
- client;
1034
- topicName;
1035
- transport;
1036
- /**
1037
- * Create a new Topic instance
1038
- * @param client QueueClient instance to use for API calls
1039
- * @param topicName Name of the topic to work with
1040
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1041
- */
1042
- constructor(client, topicName, transport) {
1043
- this.client = client;
1044
- this.topicName = topicName;
1045
- this.transport = transport || new JsonTransport();
1046
- }
1047
- /**
1048
- * Publish a message to the topic
1049
- * @param payload The data to publish
1050
- * @param options Optional publish options
1051
- * @returns An object containing the message ID
1052
- * @throws {BadRequestError} When request parameters are invalid
1053
- * @throws {UnauthorizedError} When authentication fails
1054
- * @throws {ForbiddenError} When access is denied (environment mismatch)
1055
- * @throws {InternalServerError} When server encounters an error
1056
- */
1057
- async publish(payload, options) {
1058
- const result = await this.client.sendMessage(
1420
+ async changeVisibility(options) {
1421
+ const {
1422
+ queueName,
1423
+ consumerGroup,
1424
+ receiptHandle,
1425
+ visibilityTimeoutSeconds
1426
+ } = options;
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
+ ),
1059
1444
  {
1060
- queueName: this.topicName,
1061
- payload,
1062
- idempotencyKey: options?.idempotencyKey,
1063
- retentionSeconds: options?.retentionSeconds,
1064
- callback: options?.callback
1065
- },
1066
- this.transport
1445
+ method: "PATCH",
1446
+ headers,
1447
+ body: JSON.stringify({ visibilityTimeoutSeconds })
1448
+ }
1067
1449
  );
1068
- return { messageId: result.messageId };
1450
+ if (!response.ok) {
1451
+ const errorText = await response.text();
1452
+ if (response.status === 404) {
1453
+ throw new MessageNotFoundError(receiptHandle);
1454
+ }
1455
+ if (response.status === 409) {
1456
+ throw new MessageNotAvailableError(
1457
+ receiptHandle,
1458
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
1459
+ );
1460
+ }
1461
+ throwCommonHttpError(
1462
+ response.status,
1463
+ response.statusText,
1464
+ errorText,
1465
+ "change visibility",
1466
+ "Missing receipt handle or invalid visibility timeout"
1467
+ );
1468
+ }
1469
+ return { success: true };
1069
1470
  }
1070
- /**
1071
- * Create a consumer group for this topic
1072
- * @param consumerGroupName Name of the consumer group
1073
- * @param options Optional configuration for the consumer group
1074
- * @returns A ConsumerGroup instance
1075
- */
1076
- consumerGroup(consumerGroupName, options) {
1077
- const consumerOptions = {
1078
- ...options,
1079
- transport: options?.transport || this.transport
1080
- };
1081
- return new ConsumerGroup(
1082
- this.client,
1083
- this.topicName,
1084
- consumerGroupName,
1085
- consumerOptions
1086
- );
1471
+ };
1472
+
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");
1087
1479
  }
1088
- /**
1089
- * Get the topic name
1090
- */
1091
- get name() {
1092
- return this.topicName;
1480
+ return api;
1481
+ }
1482
+ var QueueClient = class {
1483
+ constructor(options) {
1484
+ apiClients.set(this, new ApiClient(options));
1093
1485
  }
1094
1486
  /**
1095
- * Get the transport used by this topic
1487
+ * Send a message to a topic.
1488
+ *
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
+ * ```
1494
+ *
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)
1096
1500
  */
1097
- get serializer() {
1098
- return this.transport;
1099
- }
1100
- };
1101
-
1102
- // src/factory.ts
1103
- function createTopic(topicName, transport) {
1104
- const client = QueueClient._getDefaultInstance();
1105
- return new Topic(client, topicName, transport);
1106
- }
1107
- async function send(topicName, payload, options) {
1108
- const transport = options?.transport || new JsonTransport();
1109
- const client = QueueClient._getDefaultInstance();
1110
- const result = await client.sendMessage(
1111
- {
1501
+ send = async (topicName, payload, options) => {
1502
+ const api = getApiClient(this);
1503
+ const result = await api.sendMessage({
1112
1504
  queueName: topicName,
1113
1505
  payload,
1114
1506
  idempotencyKey: options?.idempotencyKey,
1115
1507
  retentionSeconds: options?.retentionSeconds,
1116
- callback: options?.callback
1117
- },
1118
- transport
1119
- );
1120
- return { messageId: result.messageId };
1121
- }
1122
- async function receive(topicName, consumerGroup, handler, options) {
1123
- const transport = options?.transport || new JsonTransport();
1124
- const topic = createTopic(topicName, transport);
1125
- const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
1126
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1127
- if (messageId) {
1128
- if (skipPayload) {
1129
- return consumer.consume(handler, {
1130
- messageId,
1131
- skipPayload: true
1132
- });
1133
- } else {
1134
- return consumer.consume(handler, { messageId });
1508
+ delaySeconds: options?.delaySeconds,
1509
+ headers: options?.headers
1510
+ });
1511
+ if (result.messageId && isDevMode()) {
1512
+ triggerDevCallbacks(
1513
+ topicName,
1514
+ result.messageId,
1515
+ api.getRegion(),
1516
+ options?.delaySeconds
1517
+ );
1135
1518
  }
1136
- } else {
1137
- return consumer.consume(handler);
1138
- }
1139
- }
1140
-
1141
- // src/callback.ts
1142
- function parseCallbackRequest(request) {
1143
- const headers = request.headers;
1144
- const messageId = headers.get("Vqs-Message-Id");
1145
- const queueName = headers.get("Vqs-Queue-Name");
1146
- const consumerGroup = headers.get("Vqs-Consumer-Group");
1147
- const missingHeaders = [];
1148
- if (!messageId) missingHeaders.push("Vqs-Message-Id");
1149
- if (!queueName) missingHeaders.push("Vqs-Queue-Name");
1150
- if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
1151
- if (missingHeaders.length > 0) {
1152
- throw new InvalidCallbackError(
1153
- `Missing required queue callback headers: ${missingHeaders.join(", ")}`
1154
- );
1155
- }
1156
- return {
1157
- messageId,
1158
- queueName,
1159
- consumerGroup
1519
+ return { messageId: result.messageId };
1160
1520
  };
1161
- }
1162
- function handleCallback(handlers) {
1163
- return async (request) => {
1521
+ /**
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
1541
+ */
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 } : {}
1549
+ );
1164
1550
  try {
1165
- const { queueName, consumerGroup, messageId } = parseCallbackRequest(request);
1166
- const topicHandler = handlers[queueName];
1167
- if (!topicHandler) {
1168
- throw new Error(`No handler found for topic: ${queueName}`);
1169
- }
1170
- let actualHandler;
1171
- if (typeof topicHandler === "function") {
1172
- if (consumerGroup !== "default") {
1173
- throw new Error(
1174
- `Topic "${queueName}" has a single handler but received consumer group "${consumerGroup}". Expected "default".`
1175
- );
1176
- }
1177
- actualHandler = topicHandler;
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
1557
+ });
1178
1558
  } else {
1179
- const consumerGroupHandler = topicHandler[consumerGroup];
1180
- if (!consumerGroupHandler) {
1181
- const availableGroups = Object.keys(topicHandler).join(", ");
1182
- throw new Error(
1183
- `No handler found for consumer group "${consumerGroup}" in topic "${queueName}". Available groups: ${availableGroups}`
1184
- );
1185
- }
1186
- actualHandler = consumerGroupHandler;
1559
+ const limit = options && "limit" in options ? options.limit : void 0;
1560
+ count = await consumer.consume(handler, {
1561
+ ...limit !== void 0 ? { limit } : {},
1562
+ retry
1563
+ });
1564
+ }
1565
+ if (count === 0) {
1566
+ return { ok: false, reason: "empty" };
1187
1567
  }
1188
- const client = new QueueClient();
1189
- const topic = new Topic(client, queueName);
1190
- const cg = topic.consumerGroup(consumerGroup);
1191
- await cg.consume(actualHandler, { messageId });
1192
- return Response.json({ status: "success" });
1568
+ return { ok: true };
1193
1569
  } catch (error) {
1194
- console.error("Callback error:", error);
1195
- if (error instanceof InvalidCallbackError) {
1570
+ if (options && "messageId" in options && error instanceof MessageNotFoundError) {
1571
+ return { ok: false, reason: "not_found", messageId: options.messageId };
1572
+ }
1573
+ if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
1574
+ return {
1575
+ ok: false,
1576
+ reason: "not_available",
1577
+ messageId: options.messageId
1578
+ };
1579
+ }
1580
+ if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
1581
+ return {
1582
+ ok: false,
1583
+ reason: "already_processed",
1584
+ messageId: options.messageId
1585
+ };
1586
+ }
1587
+ throw error;
1588
+ }
1589
+ };
1590
+ /**
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
1608
+ */
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
+ }
1196
1624
  return Response.json(
1197
- { error: "Invalid callback request" },
1198
- { status: 400 }
1625
+ { error: "Failed to process queue message" },
1626
+ { status: 500 }
1199
1627
  );
1200
1628
  }
1201
- return Response.json(
1202
- { error: "Failed to process callback" },
1203
- { status: 500 }
1204
- );
1205
- }
1629
+ };
1206
1630
  };
1207
- }
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;
1656
+ }
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" });
1672
+ }
1673
+ };
1674
+ };
1675
+ };
1208
1676
  // Annotate the CommonJS export names for ESM import in node:
1209
1677
  0 && (module.exports = {
1210
1678
  BadRequestError,
1211
1679
  BufferTransport,
1212
- ConsumerGroup,
1213
- FailedDependencyError,
1214
- FifoOrderingViolationError,
1680
+ CLOUD_EVENT_TYPE_V1BETA,
1681
+ CLOUD_EVENT_TYPE_V2BETA,
1682
+ ConsumerDiscoveryError,
1683
+ ConsumerRegistryNotConfiguredError,
1684
+ DuplicateMessageError,
1215
1685
  ForbiddenError,
1216
1686
  InternalServerError,
1217
- InvalidCallbackError,
1218
1687
  InvalidLimitError,
1219
1688
  JsonTransport,
1689
+ MessageAlreadyProcessedError,
1220
1690
  MessageCorruptedError,
1221
1691
  MessageLockedError,
1222
1692
  MessageNotAvailableError,
@@ -1224,12 +1694,8 @@ function handleCallback(handlers) {
1224
1694
  QueueClient,
1225
1695
  QueueEmptyError,
1226
1696
  StreamTransport,
1227
- Topic,
1228
1697
  UnauthorizedError,
1229
- createTopic,
1230
- handleCallback,
1231
- parseCallbackRequest,
1232
- receive,
1233
- send
1698
+ parseCallback,
1699
+ parseRawCallback
1234
1700
  });
1235
1701
  //# sourceMappingURL=index.js.map