@vercel/queue 0.0.0-alpha.32 → 0.0.0-alpha.34

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.
@@ -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/nextjs-pages.ts
@@ -27,8 +37,43 @@ module.exports = __toCommonJS(nextjs_pages_exports);
27
37
  // src/client.ts
28
38
  var import_mixpart = require("mixpart");
29
39
 
30
- // src/oidc.ts
31
- var import_oidc = require("@vercel/oidc");
40
+ // src/dev.ts
41
+ var fs = __toESM(require("fs"));
42
+ var path = __toESM(require("path"));
43
+
44
+ // src/transports.ts
45
+ async function streamToBuffer(stream) {
46
+ let totalLength = 0;
47
+ const reader = stream.getReader();
48
+ const chunks = [];
49
+ try {
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done) break;
53
+ chunks.push(value);
54
+ totalLength += value.length;
55
+ }
56
+ } finally {
57
+ reader.releaseLock();
58
+ }
59
+ return Buffer.concat(chunks, totalLength);
60
+ }
61
+ var JsonTransport = class {
62
+ contentType = "application/json";
63
+ replacer;
64
+ reviver;
65
+ constructor(options = {}) {
66
+ this.replacer = options.replacer;
67
+ this.reviver = options.reviver;
68
+ }
69
+ serialize(value) {
70
+ return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
71
+ }
72
+ async deserialize(stream) {
73
+ const buffer = await streamToBuffer(stream);
74
+ return JSON.parse(buffer.toString("utf8"), this.reviver);
75
+ }
76
+ };
32
77
 
33
78
  // src/types.ts
34
79
  var MessageNotFoundError = class extends Error {
@@ -59,15 +104,6 @@ var QueueEmptyError = class extends Error {
59
104
  this.name = "QueueEmptyError";
60
105
  }
61
106
  };
62
- var MessageLockedError = class extends Error {
63
- retryAfter;
64
- constructor(messageId, retryAfter) {
65
- const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
66
- super(`Message ${messageId} is temporarily locked.${retryMessage}`);
67
- this.name = "MessageLockedError";
68
- this.retryAfter = retryAfter;
69
- }
70
- };
71
107
  var UnauthorizedError = class extends Error {
72
108
  constructor(message = "Missing or invalid authentication token") {
73
109
  super(message);
@@ -98,8 +134,268 @@ var InvalidLimitError = class extends Error {
98
134
  this.name = "InvalidLimitError";
99
135
  }
100
136
  };
137
+ var MessageAlreadyProcessedError = class extends Error {
138
+ constructor(messageId) {
139
+ super(`Message ${messageId} has already been processed`);
140
+ this.name = "MessageAlreadyProcessedError";
141
+ }
142
+ };
143
+ var ConcurrencyLimitError = class extends Error {
144
+ currentInflight;
145
+ maxConcurrency;
146
+ constructor(message = "Concurrency limit exceeded", currentInflight, maxConcurrency) {
147
+ super(message);
148
+ this.name = "ConcurrencyLimitError";
149
+ this.currentInflight = currentInflight;
150
+ this.maxConcurrency = maxConcurrency;
151
+ }
152
+ };
153
+ var DuplicateMessageError = class extends Error {
154
+ idempotencyKey;
155
+ constructor(message, idempotencyKey) {
156
+ super(message);
157
+ this.name = "DuplicateMessageError";
158
+ this.idempotencyKey = idempotencyKey;
159
+ }
160
+ };
161
+ var ConsumerDiscoveryError = class extends Error {
162
+ deploymentId;
163
+ constructor(message, deploymentId) {
164
+ super(message);
165
+ this.name = "ConsumerDiscoveryError";
166
+ this.deploymentId = deploymentId;
167
+ }
168
+ };
169
+ var ConsumerRegistryNotConfiguredError = class extends Error {
170
+ constructor(message = "Consumer registry not configured") {
171
+ super(message);
172
+ this.name = "ConsumerRegistryNotConfiguredError";
173
+ }
174
+ };
175
+
176
+ // src/dev.ts
177
+ var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
178
+ function filePathToUrlPath(filePath) {
179
+ let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|js|tsx|jsx)$/, "").replace(/\.(ts|js|tsx|jsx)$/, "");
180
+ if (!urlPath.startsWith("/")) {
181
+ urlPath = "/" + urlPath;
182
+ }
183
+ return urlPath;
184
+ }
185
+ function getDevRouteMappings() {
186
+ const g = globalThis;
187
+ if (ROUTE_MAPPINGS_KEY in g) {
188
+ return g[ROUTE_MAPPINGS_KEY] ?? null;
189
+ }
190
+ try {
191
+ const vercelJsonPath = path.join(process.cwd(), "vercel.json");
192
+ if (!fs.existsSync(vercelJsonPath)) {
193
+ g[ROUTE_MAPPINGS_KEY] = null;
194
+ return null;
195
+ }
196
+ const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
197
+ if (!vercelJson.functions) {
198
+ g[ROUTE_MAPPINGS_KEY] = null;
199
+ return null;
200
+ }
201
+ const mappings = [];
202
+ for (const [filePath, config] of Object.entries(vercelJson.functions)) {
203
+ if (!config.experimentalTriggers) continue;
204
+ for (const trigger of config.experimentalTriggers) {
205
+ if (trigger.type?.startsWith("queue/") && trigger.topic && trigger.consumer) {
206
+ mappings.push({
207
+ urlPath: filePathToUrlPath(filePath),
208
+ topic: trigger.topic,
209
+ consumer: trigger.consumer
210
+ });
211
+ }
212
+ }
213
+ }
214
+ g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
215
+ return g[ROUTE_MAPPINGS_KEY];
216
+ } catch (error) {
217
+ console.warn("[Dev Mode] Failed to read vercel.json:", error);
218
+ g[ROUTE_MAPPINGS_KEY] = null;
219
+ return null;
220
+ }
221
+ }
222
+ function findMatchingRoutes(topicName) {
223
+ const mappings = getDevRouteMappings();
224
+ if (!mappings) {
225
+ return [];
226
+ }
227
+ return mappings.filter((mapping) => {
228
+ if (mapping.topic.includes("*")) {
229
+ return matchesWildcardPattern(topicName, mapping.topic);
230
+ }
231
+ return mapping.topic === topicName;
232
+ });
233
+ }
234
+ function isDevMode() {
235
+ return process.env.NODE_ENV === "development";
236
+ }
237
+ var DEV_VISIBILITY_POLL_INTERVAL = 50;
238
+ var DEV_VISIBILITY_MAX_WAIT = 5e3;
239
+ var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
240
+ async function waitForMessageVisibility(topicName, consumerGroup, messageId) {
241
+ const client = new QueueClient();
242
+ const transport = new JsonTransport();
243
+ let elapsed = 0;
244
+ let interval = DEV_VISIBILITY_POLL_INTERVAL;
245
+ while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
246
+ try {
247
+ await client.receiveMessageById(
248
+ {
249
+ queueName: topicName,
250
+ consumerGroup,
251
+ messageId,
252
+ visibilityTimeoutSeconds: 0
253
+ },
254
+ transport
255
+ );
256
+ return true;
257
+ } catch (error) {
258
+ if (error instanceof MessageNotFoundError) {
259
+ await new Promise((resolve) => setTimeout(resolve, interval));
260
+ elapsed += interval;
261
+ interval = Math.min(
262
+ interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
263
+ DEV_VISIBILITY_MAX_WAIT - elapsed
264
+ );
265
+ continue;
266
+ }
267
+ if (error instanceof MessageAlreadyProcessedError) {
268
+ console.log(
269
+ `[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
270
+ );
271
+ return false;
272
+ }
273
+ console.error(
274
+ `[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
275
+ error
276
+ );
277
+ return false;
278
+ }
279
+ }
280
+ console.warn(
281
+ `[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
282
+ );
283
+ return false;
284
+ }
285
+ function triggerDevCallbacks(topicName, messageId, delaySeconds) {
286
+ if (delaySeconds && delaySeconds > 0) {
287
+ console.log(
288
+ `[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
289
+ );
290
+ setTimeout(() => {
291
+ triggerDevCallbacks(topicName, messageId);
292
+ }, delaySeconds * 1e3);
293
+ return;
294
+ }
295
+ console.log(
296
+ `[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
297
+ );
298
+ const matchingRoutes = findMatchingRoutes(topicName);
299
+ if (matchingRoutes.length === 0) {
300
+ console.log(
301
+ `[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
302
+ );
303
+ return;
304
+ }
305
+ const consumerGroups = matchingRoutes.map((r) => r.consumer);
306
+ console.log(
307
+ `[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
308
+ );
309
+ (async () => {
310
+ const firstRoute = matchingRoutes[0];
311
+ const isVisible = await waitForMessageVisibility(
312
+ topicName,
313
+ firstRoute.consumer,
314
+ messageId
315
+ );
316
+ if (!isVisible) {
317
+ console.warn(
318
+ `[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
319
+ );
320
+ return;
321
+ }
322
+ const port = process.env.PORT || 3e3;
323
+ const baseUrl = `http://localhost:${port}`;
324
+ for (const route of matchingRoutes) {
325
+ const url = `${baseUrl}${route.urlPath}`;
326
+ console.log(
327
+ `[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
328
+ );
329
+ const cloudEvent = {
330
+ type: "com.vercel.queue.v1beta",
331
+ source: `/topic/${topicName}/consumer/${route.consumer}`,
332
+ id: messageId,
333
+ datacontenttype: "application/json",
334
+ data: {
335
+ messageId,
336
+ queueName: topicName,
337
+ consumerGroup: route.consumer
338
+ },
339
+ time: (/* @__PURE__ */ new Date()).toISOString(),
340
+ specversion: "1.0"
341
+ };
342
+ try {
343
+ const response = await fetch(url, {
344
+ method: "POST",
345
+ headers: {
346
+ "Content-Type": "application/cloudevents+json"
347
+ },
348
+ body: JSON.stringify(cloudEvent)
349
+ });
350
+ if (response.ok) {
351
+ try {
352
+ const responseData = await response.json();
353
+ if (responseData.status === "success") {
354
+ console.log(
355
+ `[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
356
+ );
357
+ }
358
+ } catch {
359
+ console.warn(
360
+ `[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
361
+ );
362
+ }
363
+ } else {
364
+ try {
365
+ const errorData = await response.json();
366
+ console.error(
367
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
368
+ );
369
+ } catch {
370
+ console.error(
371
+ `[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
372
+ );
373
+ }
374
+ }
375
+ } catch (error) {
376
+ console.error(
377
+ `[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
378
+ error
379
+ );
380
+ }
381
+ }
382
+ })();
383
+ }
384
+ function clearDevRouteMappings() {
385
+ const g = globalThis;
386
+ delete g[ROUTE_MAPPINGS_KEY];
387
+ }
388
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
389
+ globalThis.__clearDevRouteMappings = clearDevRouteMappings;
390
+ }
391
+
392
+ // src/oidc.ts
393
+ var import_oidc = require("@vercel/oidc");
101
394
 
102
395
  // src/client.ts
396
+ function isDebugEnabled() {
397
+ return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
398
+ }
103
399
  async function consumeStream(stream) {
104
400
  const reader = stream.getReader();
105
401
  try {
@@ -111,17 +407,34 @@ async function consumeStream(stream) {
111
407
  reader.releaseLock();
112
408
  }
113
409
  }
410
+ function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
411
+ if (status === 400) {
412
+ throw new BadRequestError(errorText || badRequestDefault);
413
+ }
414
+ if (status === 401) {
415
+ throw new UnauthorizedError(errorText || void 0);
416
+ }
417
+ if (status === 403) {
418
+ throw new ForbiddenError(errorText || void 0);
419
+ }
420
+ if (status >= 500) {
421
+ throw new InternalServerError(
422
+ errorText || `Server error: ${status} ${statusText}`
423
+ );
424
+ }
425
+ throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
426
+ }
114
427
  function parseQueueHeaders(headers) {
115
428
  const messageId = headers.get("Vqs-Message-Id");
116
429
  const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
117
430
  const timestamp = headers.get("Vqs-Timestamp");
118
431
  const contentType = headers.get("Content-Type") || "application/octet-stream";
119
- const ticket = headers.get("Vqs-Ticket");
120
- if (!messageId || !timestamp || !ticket) {
432
+ const receiptHandle = headers.get("Vqs-Receipt-Handle");
433
+ if (!messageId || !timestamp || !receiptHandle) {
121
434
  return null;
122
435
  }
123
436
  const deliveryCount = parseInt(deliveryCountStr, 10);
124
- if (isNaN(deliveryCount)) {
437
+ if (Number.isNaN(deliveryCount)) {
125
438
  return null;
126
439
  }
127
440
  return {
@@ -129,30 +442,43 @@ function parseQueueHeaders(headers) {
129
442
  deliveryCount,
130
443
  createdAt: new Date(timestamp),
131
444
  contentType,
132
- ticket
445
+ receiptHandle
133
446
  };
134
447
  }
135
448
  var QueueClient = class {
136
449
  baseUrl;
137
450
  basePath;
138
- customHeaders = {};
139
- /**
140
- * Create a new Vercel Queue Service client
141
- * @param options Client configuration options
142
- */
451
+ customHeaders;
452
+ providedToken;
453
+ defaultDeploymentId;
454
+ pinToDeployment;
143
455
  constructor(options = {}) {
144
456
  this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
145
- this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
146
- const VERCEL_QUEUE_HEADER_PREFIX = "VERCEL_QUEUE_HEADER_";
147
- this.customHeaders = Object.fromEntries(
148
- Object.entries(process.env).filter(([key]) => key.startsWith(VERCEL_QUEUE_HEADER_PREFIX)).map(([key, value]) => [
149
- // This allows headers to use dashes independent of shell used
150
- key.replace(VERCEL_QUEUE_HEADER_PREFIX, "").replaceAll("__", "-"),
151
- value || ""
152
- ])
153
- );
457
+ this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v3/topic";
458
+ this.customHeaders = options.headers || {};
459
+ this.providedToken = options.token;
460
+ this.defaultDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
461
+ this.pinToDeployment = options.pinToDeployment ?? true;
462
+ }
463
+ getSendDeploymentId() {
464
+ if (isDevMode()) {
465
+ return void 0;
466
+ }
467
+ if (this.pinToDeployment) {
468
+ return this.defaultDeploymentId;
469
+ }
470
+ return void 0;
471
+ }
472
+ getConsumeDeploymentId() {
473
+ if (isDevMode()) {
474
+ return void 0;
475
+ }
476
+ return this.defaultDeploymentId;
154
477
  }
155
478
  async getToken() {
479
+ if (this.providedToken) {
480
+ return this.providedToken;
481
+ }
156
482
  const token = await (0, import_oidc.getVercelOidcToken)();
157
483
  if (!token) {
158
484
  throw new Error(
@@ -161,25 +487,61 @@ var QueueClient = class {
161
487
  }
162
488
  return token;
163
489
  }
164
- /**
165
- * Send a message to a queue
166
- * @param options Send message options
167
- * @param transport Serializer/deserializer for the payload
168
- * @returns Promise with the message ID
169
- * @throws {BadRequestError} When request parameters are invalid
170
- * @throws {UnauthorizedError} When authentication fails
171
- * @throws {ForbiddenError} When access is denied (environment mismatch)
172
- * @throws {InternalServerError} When server encounters an error
173
- */
490
+ buildUrl(queueName, ...pathSegments) {
491
+ const encodedQueue = encodeURIComponent(queueName);
492
+ const segments = pathSegments.map((s) => encodeURIComponent(s));
493
+ const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
494
+ return `${this.baseUrl}${this.basePath}/${encodedQueue}${path2}`;
495
+ }
496
+ async fetch(url, init) {
497
+ const method = init.method || "GET";
498
+ if (isDebugEnabled()) {
499
+ const logData = {
500
+ method,
501
+ url,
502
+ headers: init.headers
503
+ };
504
+ const body = init.body;
505
+ if (body !== void 0 && body !== null) {
506
+ if (body instanceof ArrayBuffer) {
507
+ logData.bodySize = body.byteLength;
508
+ } else if (body instanceof Uint8Array) {
509
+ logData.bodySize = body.byteLength;
510
+ } else if (typeof body === "string") {
511
+ logData.bodySize = body.length;
512
+ } else {
513
+ logData.bodyType = typeof body;
514
+ }
515
+ }
516
+ console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
517
+ }
518
+ const response = await fetch(url, init);
519
+ if (isDebugEnabled()) {
520
+ const logData = {
521
+ method,
522
+ url,
523
+ status: response.status,
524
+ statusText: response.statusText,
525
+ headers: response.headers
526
+ };
527
+ console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
528
+ }
529
+ return response;
530
+ }
174
531
  async sendMessage(options, transport) {
175
- const { queueName, payload, idempotencyKey, retentionSeconds } = options;
532
+ const {
533
+ queueName,
534
+ payload,
535
+ idempotencyKey,
536
+ retentionSeconds,
537
+ delaySeconds
538
+ } = options;
176
539
  const headers = new Headers({
177
540
  Authorization: `Bearer ${await this.getToken()}`,
178
- "Vqs-Queue-Name": queueName,
179
541
  "Content-Type": transport.contentType,
180
542
  ...this.customHeaders
181
543
  });
182
- const deploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
544
+ const deploymentId = this.getSendDeploymentId();
183
545
  if (deploymentId) {
184
546
  headers.set("Vqs-Deployment-Id", deploymentId);
185
547
  }
@@ -189,106 +551,106 @@ var QueueClient = class {
189
551
  if (retentionSeconds !== void 0) {
190
552
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
191
553
  }
192
- const body = transport.serialize(payload);
193
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
554
+ if (delaySeconds !== void 0) {
555
+ headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
556
+ }
557
+ const serialized = transport.serialize(payload);
558
+ const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
559
+ const response = await this.fetch(this.buildUrl(queueName), {
194
560
  method: "POST",
195
561
  body,
196
562
  headers
197
563
  });
198
564
  if (!response.ok) {
199
- if (response.status === 400) {
200
- const errorText = await response.text();
201
- throw new BadRequestError(errorText || "Invalid parameters");
202
- }
203
- if (response.status === 401) {
204
- throw new UnauthorizedError();
205
- }
206
- if (response.status === 403) {
207
- throw new ForbiddenError();
208
- }
565
+ const errorText = await response.text();
209
566
  if (response.status === 409) {
210
- throw new Error("Duplicate idempotency key detected");
567
+ throw new DuplicateMessageError(
568
+ errorText || "Duplicate idempotency key detected",
569
+ idempotencyKey
570
+ );
211
571
  }
212
- if (response.status >= 500) {
213
- throw new InternalServerError(
214
- `Server error: ${response.status} ${response.statusText}`
572
+ if (response.status === 502) {
573
+ throw new ConsumerDiscoveryError(
574
+ errorText || "Consumer discovery failed",
575
+ deploymentId
215
576
  );
216
577
  }
217
- throw new Error(
218
- `Failed to send message: ${response.status} ${response.statusText}`
578
+ if (response.status === 503) {
579
+ throw new ConsumerRegistryNotConfiguredError(
580
+ errorText || "Consumer registry not configured"
581
+ );
582
+ }
583
+ throwCommonHttpError(
584
+ response.status,
585
+ response.statusText,
586
+ errorText,
587
+ "send message"
219
588
  );
220
589
  }
221
590
  const responseData = await response.json();
222
591
  return responseData;
223
592
  }
224
- /**
225
- * Receive messages from a queue
226
- * @param options Receive messages options
227
- * @param transport Serializer/deserializer for the payload
228
- * @returns AsyncGenerator that yields messages as they arrive
229
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
230
- * @throws {QueueEmptyError} When no messages are available (204)
231
- * @throws {MessageLockedError} When messages are temporarily locked (423)
232
- * @throws {BadRequestError} When request parameters are invalid
233
- * @throws {UnauthorizedError} When authentication fails
234
- * @throws {ForbiddenError} When access is denied (environment mismatch)
235
- * @throws {InternalServerError} When server encounters an error
236
- */
237
593
  async *receiveMessages(options, transport) {
238
- const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
594
+ const {
595
+ queueName,
596
+ consumerGroup,
597
+ visibilityTimeoutSeconds,
598
+ limit,
599
+ maxConcurrency
600
+ } = options;
239
601
  if (limit !== void 0 && (limit < 1 || limit > 10)) {
240
602
  throw new InvalidLimitError(limit);
241
603
  }
242
604
  const headers = new Headers({
243
605
  Authorization: `Bearer ${await this.getToken()}`,
244
- "Vqs-Queue-Name": queueName,
245
- "Vqs-Consumer-Group": consumerGroup,
246
606
  Accept: "multipart/mixed",
247
607
  ...this.customHeaders
248
608
  });
249
609
  if (visibilityTimeoutSeconds !== void 0) {
250
610
  headers.set(
251
- "Vqs-Visibility-Timeout",
611
+ "Vqs-Visibility-Timeout-Seconds",
252
612
  visibilityTimeoutSeconds.toString()
253
613
  );
254
614
  }
255
615
  if (limit !== void 0) {
256
- headers.set("Vqs-Limit", limit.toString());
616
+ headers.set("Vqs-Max-Messages", limit.toString());
257
617
  }
258
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
259
- method: "GET",
260
- headers
261
- });
618
+ if (maxConcurrency !== void 0) {
619
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
620
+ }
621
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
622
+ if (effectiveDeploymentId) {
623
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
624
+ }
625
+ const response = await this.fetch(
626
+ this.buildUrl(queueName, "consumer", consumerGroup),
627
+ {
628
+ method: "POST",
629
+ headers
630
+ }
631
+ );
262
632
  if (response.status === 204) {
263
633
  throw new QueueEmptyError(queueName, consumerGroup);
264
634
  }
265
635
  if (!response.ok) {
266
- if (response.status === 400) {
267
- const errorText = await response.text();
268
- throw new BadRequestError(errorText || "Invalid parameters");
269
- }
270
- if (response.status === 401) {
271
- throw new UnauthorizedError();
272
- }
273
- if (response.status === 403) {
274
- throw new ForbiddenError();
275
- }
276
- if (response.status === 423) {
277
- const retryAfterHeader = response.headers.get("Retry-After");
278
- let retryAfter;
279
- if (retryAfterHeader) {
280
- const parsed = parseInt(retryAfterHeader, 10);
281
- retryAfter = isNaN(parsed) ? void 0 : parsed;
636
+ const errorText = await response.text();
637
+ if (response.status === 429) {
638
+ let errorData = {};
639
+ try {
640
+ errorData = JSON.parse(errorText);
641
+ } catch {
282
642
  }
283
- throw new MessageLockedError("next message", retryAfter);
284
- }
285
- if (response.status >= 500) {
286
- throw new InternalServerError(
287
- `Server error: ${response.status} ${response.statusText}`
643
+ throw new ConcurrencyLimitError(
644
+ errorData.error || "Concurrency limit exceeded or throttled",
645
+ errorData.currentInflight,
646
+ errorData.maxConcurrency
288
647
  );
289
648
  }
290
- throw new Error(
291
- `Failed to receive messages: ${response.status} ${response.statusText}`
649
+ throwCommonHttpError(
650
+ response.status,
651
+ response.statusText,
652
+ errorText,
653
+ "receive messages"
292
654
  );
293
655
  }
294
656
  for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
@@ -319,458 +681,250 @@ var QueueClient = class {
319
681
  consumerGroup,
320
682
  messageId,
321
683
  visibilityTimeoutSeconds,
322
- skipPayload
684
+ maxConcurrency
323
685
  } = options;
324
686
  const headers = new Headers({
325
687
  Authorization: `Bearer ${await this.getToken()}`,
326
- "Vqs-Queue-Name": queueName,
327
- "Vqs-Consumer-Group": consumerGroup,
328
688
  Accept: "multipart/mixed",
329
689
  ...this.customHeaders
330
690
  });
331
691
  if (visibilityTimeoutSeconds !== void 0) {
332
692
  headers.set(
333
- "Vqs-Visibility-Timeout",
693
+ "Vqs-Visibility-Timeout-Seconds",
334
694
  visibilityTimeoutSeconds.toString()
335
695
  );
336
696
  }
337
- if (skipPayload) {
338
- headers.set("Vqs-Skip-Payload", "1");
697
+ if (maxConcurrency !== void 0) {
698
+ headers.set("Vqs-Max-Concurrency", maxConcurrency.toString());
339
699
  }
340
- const response = await fetch(
341
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
700
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
701
+ if (effectiveDeploymentId) {
702
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
703
+ }
704
+ const response = await this.fetch(
705
+ this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
342
706
  {
343
- method: "GET",
707
+ method: "POST",
344
708
  headers
345
709
  }
346
710
  );
347
711
  if (!response.ok) {
348
- if (response.status === 400) {
349
- const errorText = await response.text();
350
- throw new BadRequestError(errorText || "Invalid parameters");
351
- }
352
- if (response.status === 401) {
353
- throw new UnauthorizedError();
354
- }
355
- if (response.status === 403) {
356
- throw new ForbiddenError();
357
- }
712
+ const errorText = await response.text();
358
713
  if (response.status === 404) {
359
714
  throw new MessageNotFoundError(messageId);
360
715
  }
361
- if (response.status === 423) {
362
- const retryAfterHeader = response.headers.get("Retry-After");
363
- let retryAfter;
364
- if (retryAfterHeader) {
365
- const parsed = parseInt(retryAfterHeader, 10);
366
- retryAfter = isNaN(parsed) ? void 0 : parsed;
367
- }
368
- throw new MessageLockedError(messageId, retryAfter);
369
- }
370
716
  if (response.status === 409) {
717
+ let errorData = {};
718
+ try {
719
+ errorData = JSON.parse(errorText);
720
+ } catch {
721
+ }
722
+ if (errorData.originalMessageId) {
723
+ throw new MessageNotAvailableError(
724
+ messageId,
725
+ `This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
726
+ );
727
+ }
371
728
  throw new MessageNotAvailableError(messageId);
372
729
  }
373
- if (response.status >= 500) {
374
- throw new InternalServerError(
375
- `Server error: ${response.status} ${response.statusText}`
730
+ if (response.status === 410) {
731
+ throw new MessageAlreadyProcessedError(messageId);
732
+ }
733
+ if (response.status === 429) {
734
+ let errorData = {};
735
+ try {
736
+ errorData = JSON.parse(errorText);
737
+ } catch {
738
+ }
739
+ throw new ConcurrencyLimitError(
740
+ errorData.error || "Concurrency limit exceeded or throttled",
741
+ errorData.currentInflight,
742
+ errorData.maxConcurrency
376
743
  );
377
744
  }
378
- throw new Error(
379
- `Failed to receive message by ID: ${response.status} ${response.statusText}`
745
+ throwCommonHttpError(
746
+ response.status,
747
+ response.statusText,
748
+ errorText,
749
+ "receive message by ID"
380
750
  );
381
751
  }
382
- if (skipPayload && response.status === 204) {
383
- const parsedHeaders = parseQueueHeaders(response.headers);
752
+ for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
753
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
384
754
  if (!parsedHeaders) {
755
+ await consumeStream(multipartMessage.payload);
385
756
  throw new MessageCorruptedError(
386
757
  messageId,
387
- "Missing required queue headers in 204 response"
758
+ "Missing required queue headers in response"
388
759
  );
389
760
  }
761
+ const deserializedPayload = await transport.deserialize(
762
+ multipartMessage.payload
763
+ );
390
764
  const message = {
391
765
  ...parsedHeaders,
392
- payload: void 0
766
+ payload: deserializedPayload
393
767
  };
394
768
  return { message };
395
769
  }
396
- if (!transport) {
397
- throw new Error("Transport is required when skipPayload is not true");
398
- }
399
- try {
400
- for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
401
- try {
402
- const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
403
- if (!parsedHeaders) {
404
- console.warn("Missing required queue headers in multipart part");
405
- await consumeStream(multipartMessage.payload);
406
- continue;
407
- }
408
- const deserializedPayload = await transport.deserialize(
409
- multipartMessage.payload
410
- );
411
- const message = {
412
- ...parsedHeaders,
413
- payload: deserializedPayload
414
- };
415
- return { message };
416
- } catch (error) {
417
- console.warn("Failed to deserialize message by ID:", error);
418
- await consumeStream(multipartMessage.payload);
419
- throw new MessageCorruptedError(
420
- messageId,
421
- `Failed to deserialize payload: ${error}`
422
- );
423
- }
424
- }
425
- } catch (error) {
426
- if (error instanceof MessageCorruptedError) {
427
- throw error;
428
- }
429
- throw new MessageCorruptedError(
430
- messageId,
431
- `Failed to parse multipart response: ${error}`
432
- );
433
- }
434
770
  throw new MessageNotFoundError(messageId);
435
771
  }
436
- /**
437
- * Delete a message (acknowledge processing)
438
- * @param options Delete message options
439
- * @returns Promise with delete status
440
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
441
- * @throws {MessageNotAvailableError} When message can't be deleted (409)
442
- * @throws {BadRequestError} When ticket is missing or invalid (400)
443
- * @throws {UnauthorizedError} When authentication fails
444
- * @throws {ForbiddenError} When access is denied (environment mismatch)
445
- * @throws {InternalServerError} When server encounters an error
446
- */
447
772
  async deleteMessage(options) {
448
- const { queueName, consumerGroup, messageId, ticket } = options;
449
- const response = await fetch(
450
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
773
+ const { queueName, consumerGroup, receiptHandle } = options;
774
+ const headers = new Headers({
775
+ Authorization: `Bearer ${await this.getToken()}`,
776
+ ...this.customHeaders
777
+ });
778
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
779
+ if (effectiveDeploymentId) {
780
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
781
+ }
782
+ const response = await this.fetch(
783
+ this.buildUrl(
784
+ queueName,
785
+ "consumer",
786
+ consumerGroup,
787
+ "lease",
788
+ receiptHandle
789
+ ),
451
790
  {
452
791
  method: "DELETE",
453
- headers: new Headers({
454
- Authorization: `Bearer ${await this.getToken()}`,
455
- "Vqs-Queue-Name": queueName,
456
- "Vqs-Consumer-Group": consumerGroup,
457
- "Vqs-Ticket": ticket,
458
- ...this.customHeaders
459
- })
792
+ headers
460
793
  }
461
794
  );
462
795
  if (!response.ok) {
463
- if (response.status === 400) {
464
- throw new BadRequestError("Missing or invalid ticket");
465
- }
466
- if (response.status === 401) {
467
- throw new UnauthorizedError();
468
- }
469
- if (response.status === 403) {
470
- throw new ForbiddenError();
471
- }
796
+ const errorText = await response.text();
472
797
  if (response.status === 404) {
473
- throw new MessageNotFoundError(messageId);
798
+ throw new MessageNotFoundError(receiptHandle);
474
799
  }
475
800
  if (response.status === 409) {
476
801
  throw new MessageNotAvailableError(
477
- messageId,
478
- "Invalid ticket, message not in correct state, or already processed"
479
- );
480
- }
481
- if (response.status >= 500) {
482
- throw new InternalServerError(
483
- `Server error: ${response.status} ${response.statusText}`
802
+ receiptHandle,
803
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
484
804
  );
485
805
  }
486
- throw new Error(
487
- `Failed to delete message: ${response.status} ${response.statusText}`
806
+ throwCommonHttpError(
807
+ response.status,
808
+ response.statusText,
809
+ errorText,
810
+ "delete message",
811
+ "Missing or invalid receipt handle"
488
812
  );
489
813
  }
490
814
  return { deleted: true };
491
815
  }
492
- /**
493
- * Change the visibility timeout of a message
494
- * @param options Change visibility options
495
- * @returns Promise with update status
496
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
497
- * @throws {MessageNotAvailableError} When message can't be updated (409)
498
- * @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
499
- * @throws {UnauthorizedError} When authentication fails
500
- * @throws {ForbiddenError} When access is denied (environment mismatch)
501
- * @throws {InternalServerError} When server encounters an error
502
- */
503
816
  async changeVisibility(options) {
504
817
  const {
505
818
  queueName,
506
819
  consumerGroup,
507
- messageId,
508
- ticket,
820
+ receiptHandle,
509
821
  visibilityTimeoutSeconds
510
822
  } = options;
511
- const response = await fetch(
512
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
823
+ const headers = new Headers({
824
+ Authorization: `Bearer ${await this.getToken()}`,
825
+ "Content-Type": "application/json",
826
+ ...this.customHeaders
827
+ });
828
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
829
+ if (effectiveDeploymentId) {
830
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
831
+ }
832
+ const response = await this.fetch(
833
+ this.buildUrl(
834
+ queueName,
835
+ "consumer",
836
+ consumerGroup,
837
+ "lease",
838
+ receiptHandle
839
+ ),
513
840
  {
514
841
  method: "PATCH",
515
- headers: new Headers({
516
- Authorization: `Bearer ${await this.getToken()}`,
517
- "Vqs-Queue-Name": queueName,
518
- "Vqs-Consumer-Group": consumerGroup,
519
- "Vqs-Ticket": ticket,
520
- "Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
521
- ...this.customHeaders
522
- })
842
+ headers,
843
+ body: JSON.stringify({ visibilityTimeoutSeconds })
523
844
  }
524
845
  );
525
846
  if (!response.ok) {
526
- if (response.status === 400) {
527
- throw new BadRequestError(
528
- "Missing ticket or invalid visibility timeout"
529
- );
530
- }
531
- if (response.status === 401) {
532
- throw new UnauthorizedError();
533
- }
534
- if (response.status === 403) {
535
- throw new ForbiddenError();
536
- }
847
+ const errorText = await response.text();
537
848
  if (response.status === 404) {
538
- throw new MessageNotFoundError(messageId);
849
+ throw new MessageNotFoundError(receiptHandle);
539
850
  }
540
851
  if (response.status === 409) {
541
852
  throw new MessageNotAvailableError(
542
- messageId,
543
- "Invalid ticket, message not in correct state, or already processed"
544
- );
545
- }
546
- if (response.status >= 500) {
547
- throw new InternalServerError(
548
- `Server error: ${response.status} ${response.statusText}`
853
+ receiptHandle,
854
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
549
855
  );
550
856
  }
551
- throw new Error(
552
- `Failed to change visibility: ${response.status} ${response.statusText}`
857
+ throwCommonHttpError(
858
+ response.status,
859
+ response.statusText,
860
+ errorText,
861
+ "change visibility",
862
+ "Missing receipt handle or invalid visibility timeout"
553
863
  );
554
864
  }
555
- return { updated: true };
865
+ return { success: true };
556
866
  }
557
- };
558
-
559
- // src/transports.ts
560
- async function streamToBuffer(stream) {
561
- let totalLength = 0;
562
- const reader = stream.getReader();
563
- const chunks = [];
564
- try {
565
- while (true) {
566
- const { done, value } = await reader.read();
567
- if (done) break;
568
- chunks.push(value);
569
- totalLength += value.length;
867
+ /**
868
+ * Alternative endpoint for changing message visibility timeout.
869
+ * Uses the /visibility path suffix and expects visibilityTimeoutSeconds in the body.
870
+ * Functionally equivalent to changeVisibility but follows an alternative API pattern.
871
+ *
872
+ * @param options - Options for changing visibility
873
+ * @returns Promise resolving to change visibility response
874
+ */
875
+ async changeVisibilityAlt(options) {
876
+ const {
877
+ queueName,
878
+ consumerGroup,
879
+ receiptHandle,
880
+ visibilityTimeoutSeconds
881
+ } = options;
882
+ const headers = new Headers({
883
+ Authorization: `Bearer ${await this.getToken()}`,
884
+ "Content-Type": "application/json",
885
+ ...this.customHeaders
886
+ });
887
+ const effectiveDeploymentId = this.getConsumeDeploymentId();
888
+ if (effectiveDeploymentId) {
889
+ headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
570
890
  }
571
- } finally {
572
- reader.releaseLock();
573
- }
574
- return Buffer.concat(chunks, totalLength);
575
- }
576
- var JsonTransport = class {
577
- contentType = "application/json";
578
- replacer;
579
- reviver;
580
- constructor(options = {}) {
581
- this.replacer = options.replacer;
582
- this.reviver = options.reviver;
583
- }
584
- serialize(value) {
585
- return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
586
- }
587
- async deserialize(stream) {
588
- const buffer = await streamToBuffer(stream);
589
- return JSON.parse(buffer.toString("utf8"), this.reviver);
590
- }
591
- };
592
-
593
- // src/dev.ts
594
- var GLOBAL_KEY = Symbol.for("@vercel/queue.devHandlers");
595
- function getDevHandlerState() {
596
- const g = globalThis;
597
- if (!g[GLOBAL_KEY]) {
598
- g[GLOBAL_KEY] = {
599
- devRouteHandlers: /* @__PURE__ */ new Map(),
600
- wildcardRouteHandlers: /* @__PURE__ */ new Map()
601
- };
602
- }
603
- return g[GLOBAL_KEY];
604
- }
605
- var { devRouteHandlers, wildcardRouteHandlers } = getDevHandlerState();
606
- function cleanupDeadRefs(key, refs) {
607
- const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
608
- if (aliveRefs.length === 0) {
609
- wildcardRouteHandlers.delete(key);
610
- } else if (aliveRefs.length < refs.length) {
611
- wildcardRouteHandlers.set(key, aliveRefs);
612
- }
613
- }
614
- function isDevMode() {
615
- return process.env.NODE_ENV === "development";
616
- }
617
- function registerDevRouteHandler(routeHandler, handlers) {
618
- for (const topicName in handlers) {
619
- for (const consumerGroup in handlers[topicName]) {
620
- const key = `${topicName}:${consumerGroup}`;
621
- if (topicName.includes("*")) {
622
- const existing = wildcardRouteHandlers.get(key) || [];
623
- cleanupDeadRefs(key, existing);
624
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
625
- const weakRef = new WeakRef(routeHandler);
626
- cleanedRefs.push(weakRef);
627
- wildcardRouteHandlers.set(key, cleanedRefs);
628
- } else {
629
- devRouteHandlers.set(key, {
630
- routeHandler,
631
- topicPattern: topicName
632
- });
891
+ const response = await this.fetch(
892
+ this.buildUrl(
893
+ queueName,
894
+ "consumer",
895
+ consumerGroup,
896
+ "lease",
897
+ receiptHandle,
898
+ "visibility"
899
+ ),
900
+ {
901
+ method: "PATCH",
902
+ headers,
903
+ body: JSON.stringify({ visibilityTimeoutSeconds })
633
904
  }
634
- }
635
- }
636
- }
637
- function findRouteHandlersForTopic(topicName) {
638
- const handlersMap = /* @__PURE__ */ new Map();
639
- for (const [
640
- key,
641
- { routeHandler, topicPattern }
642
- ] of devRouteHandlers.entries()) {
643
- const [_, consumerGroup] = key.split(":");
644
- if (topicPattern === topicName) {
645
- if (!handlersMap.has(routeHandler)) {
646
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
905
+ );
906
+ if (!response.ok) {
907
+ const errorText = await response.text();
908
+ if (response.status === 404) {
909
+ throw new MessageNotFoundError(receiptHandle);
647
910
  }
648
- handlersMap.get(routeHandler).add(consumerGroup);
649
- }
650
- }
651
- for (const [key, refs] of wildcardRouteHandlers.entries()) {
652
- const [pattern, consumerGroup] = key.split(":");
653
- if (matchesWildcardPattern(topicName, pattern)) {
654
- cleanupDeadRefs(key, refs);
655
- const cleanedRefs = wildcardRouteHandlers.get(key) || [];
656
- for (const ref of cleanedRefs) {
657
- const routeHandler = ref.deref();
658
- if (routeHandler) {
659
- if (!handlersMap.has(routeHandler)) {
660
- handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
661
- }
662
- handlersMap.get(routeHandler).add(consumerGroup);
663
- }
911
+ if (response.status === 409) {
912
+ throw new MessageNotAvailableError(
913
+ receiptHandle,
914
+ errorText || "Invalid receipt handle, message not in correct state, or already processed"
915
+ );
664
916
  }
665
- }
666
- }
667
- return handlersMap;
668
- }
669
- function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
670
- const cloudEvent = {
671
- type: "com.vercel.queue.v1beta",
672
- source: `/topic/${topicName}/consumer/${consumerGroup}`,
673
- id: messageId,
674
- datacontenttype: "application/json",
675
- data: {
676
- messageId,
677
- queueName: topicName,
678
- consumerGroup
679
- },
680
- time: (/* @__PURE__ */ new Date()).toISOString(),
681
- specversion: "1.0"
682
- };
683
- return new Request("https://localhost/api/queue/callback", {
684
- method: "POST",
685
- headers: {
686
- "Content-Type": "application/cloudevents+json"
687
- },
688
- body: JSON.stringify(cloudEvent)
689
- });
690
- }
691
- var DEV_CALLBACK_DELAY = 1e3;
692
- function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
693
- console.log(
694
- `[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
695
- );
696
- setTimeout(
697
- () => {
698
- console.log(
699
- `[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
917
+ throwCommonHttpError(
918
+ response.status,
919
+ response.statusText,
920
+ errorText,
921
+ "change visibility (alt)",
922
+ "Missing receipt handle or invalid visibility timeout"
700
923
  );
701
- triggerDevCallbacks(topicName, messageId);
702
- },
703
- timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
704
- );
705
- }
706
- function triggerDevCallbacks(topicName, messageId) {
707
- const handlersMap = findRouteHandlersForTopic(topicName);
708
- if (handlersMap.size === 0) {
709
- return;
710
- }
711
- const consumerGroups = Array.from(
712
- new Set(
713
- Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
714
- )
715
- );
716
- console.log(
717
- `[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
718
- );
719
- setTimeout(async () => {
720
- for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
721
- for (const consumerGroup of consumerGroups2) {
722
- try {
723
- const request = createMockCloudEventRequest(
724
- topicName,
725
- consumerGroup,
726
- messageId
727
- );
728
- const response = await routeHandler(request);
729
- if (response.ok) {
730
- try {
731
- const responseData = await response.json();
732
- if (responseData.status === "success") {
733
- console.log(
734
- `[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
735
- );
736
- }
737
- } catch (jsonError) {
738
- console.error(
739
- `[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
740
- jsonError
741
- );
742
- }
743
- } else {
744
- try {
745
- const errorData = await response.json();
746
- console.error(
747
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
748
- errorData.error || response.statusText
749
- );
750
- } catch (jsonError) {
751
- console.error(
752
- `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
753
- response.statusText
754
- );
755
- }
756
- }
757
- } catch (error) {
758
- console.error(
759
- `[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
760
- error
761
- );
762
- }
763
- }
764
924
  }
765
- }, DEV_CALLBACK_DELAY);
766
- }
767
- function clearDevHandlers() {
768
- devRouteHandlers.clear();
769
- wildcardRouteHandlers.clear();
770
- }
771
- if (process.env.NODE_ENV === "test" || process.env.VITEST) {
772
- globalThis.__clearDevHandlers = clearDevHandlers;
773
- }
925
+ return { success: true };
926
+ }
927
+ };
774
928
 
775
929
  // src/consumer-group.ts
776
930
  var ConsumerGroup = class {
@@ -802,8 +956,7 @@ var ConsumerGroup = class {
802
956
  * The extension loop runs every `refreshInterval` seconds and updates the message's
803
957
  * visibility timeout to `visibilityTimeout` seconds from the current time.
804
958
  *
805
- * @param messageId - The unique identifier of the message to extend visibility for
806
- * @param ticket - The receipt ticket that proves ownership of the message
959
+ * @param receiptHandle - The receipt handle that proves ownership of the message
807
960
  * @returns A function that when called will stop the extension loop
808
961
  *
809
962
  * @remarks
@@ -813,37 +966,43 @@ var ConsumerGroup = class {
813
966
  * - By default, the stop function returns immediately without waiting for in-flight
814
967
  * - Pass `true` to the stop function to wait for any in-flight extension to complete
815
968
  */
816
- startVisibilityExtension(messageId, ticket) {
969
+ startVisibilityExtension(receiptHandle) {
817
970
  let isRunning = true;
971
+ let isResolved = false;
818
972
  let resolveLifecycle;
819
973
  let timeoutId = null;
820
974
  const lifecyclePromise = new Promise((resolve) => {
821
975
  resolveLifecycle = resolve;
822
976
  });
977
+ const safeResolve = () => {
978
+ if (!isResolved) {
979
+ isResolved = true;
980
+ resolveLifecycle();
981
+ }
982
+ };
823
983
  const extend = async () => {
824
984
  if (!isRunning) {
825
- resolveLifecycle();
985
+ safeResolve();
826
986
  return;
827
987
  }
828
988
  try {
829
989
  await this.client.changeVisibility({
830
990
  queueName: this.topicName,
831
991
  consumerGroup: this.consumerGroupName,
832
- messageId,
833
- ticket,
992
+ receiptHandle,
834
993
  visibilityTimeoutSeconds: this.visibilityTimeout
835
994
  });
836
995
  if (isRunning) {
837
996
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
838
997
  } else {
839
- resolveLifecycle();
998
+ safeResolve();
840
999
  }
841
1000
  } catch (error) {
842
1001
  console.error(
843
- `Failed to extend visibility for message ${messageId}:`,
1002
+ `Failed to extend visibility for receipt handle ${receiptHandle}:`,
844
1003
  error
845
1004
  );
846
- resolveLifecycle();
1005
+ safeResolve();
847
1006
  }
848
1007
  };
849
1008
  timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
@@ -856,22 +1015,14 @@ var ConsumerGroup = class {
856
1015
  if (waitForCompletion) {
857
1016
  await lifecyclePromise;
858
1017
  } else {
859
- resolveLifecycle();
1018
+ safeResolve();
860
1019
  }
861
1020
  };
862
1021
  }
863
- /**
864
- * Process a single message with the given handler
865
- * @param message The message to process
866
- * @param handler Function to process the message
867
- */
868
1022
  async processMessage(message, handler) {
869
- const stopExtension = this.startVisibilityExtension(
870
- message.messageId,
871
- message.ticket
872
- );
1023
+ const stopExtension = this.startVisibilityExtension(message.receiptHandle);
873
1024
  try {
874
- const result = await handler(message.payload, {
1025
+ await handler(message.payload, {
875
1026
  messageId: message.messageId,
876
1027
  deliveryCount: message.deliveryCount,
877
1028
  createdAt: message.createdAt,
@@ -879,29 +1030,11 @@ var ConsumerGroup = class {
879
1030
  consumerGroup: this.consumerGroupName
880
1031
  });
881
1032
  await stopExtension();
882
- if (result && "timeoutSeconds" in result) {
883
- await this.client.changeVisibility({
884
- queueName: this.topicName,
885
- consumerGroup: this.consumerGroupName,
886
- messageId: message.messageId,
887
- ticket: message.ticket,
888
- visibilityTimeoutSeconds: result.timeoutSeconds
889
- });
890
- if (isDevMode()) {
891
- scheduleDevTimeout(
892
- this.topicName,
893
- message.messageId,
894
- result.timeoutSeconds
895
- );
896
- }
897
- } else {
898
- await this.client.deleteMessage({
899
- queueName: this.topicName,
900
- consumerGroup: this.consumerGroupName,
901
- messageId: message.messageId,
902
- ticket: message.ticket
903
- });
904
- }
1033
+ await this.client.deleteMessage({
1034
+ queueName: this.topicName,
1035
+ consumerGroup: this.consumerGroupName,
1036
+ receiptHandle: message.receiptHandle
1037
+ });
905
1038
  } catch (error) {
906
1039
  await stopExtension();
907
1040
  if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
@@ -916,36 +1049,16 @@ var ConsumerGroup = class {
916
1049
  }
917
1050
  async consume(handler, options) {
918
1051
  if (options?.messageId) {
919
- if (options.skipPayload) {
920
- const response = await this.client.receiveMessageById(
921
- {
922
- queueName: this.topicName,
923
- consumerGroup: this.consumerGroupName,
924
- messageId: options.messageId,
925
- visibilityTimeoutSeconds: this.visibilityTimeout,
926
- skipPayload: true
927
- },
928
- this.transport
929
- );
930
- await this.processMessage(
931
- response.message,
932
- handler
933
- );
934
- } else {
935
- const response = await this.client.receiveMessageById(
936
- {
937
- queueName: this.topicName,
938
- consumerGroup: this.consumerGroupName,
939
- messageId: options.messageId,
940
- visibilityTimeoutSeconds: this.visibilityTimeout
941
- },
942
- this.transport
943
- );
944
- await this.processMessage(
945
- response.message,
946
- handler
947
- );
948
- }
1052
+ const response = await this.client.receiveMessageById(
1053
+ {
1054
+ queueName: this.topicName,
1055
+ consumerGroup: this.consumerGroupName,
1056
+ messageId: options.messageId,
1057
+ visibilityTimeoutSeconds: this.visibilityTimeout
1058
+ },
1059
+ this.transport
1060
+ );
1061
+ await this.processMessage(response.message, handler);
949
1062
  } else {
950
1063
  let messageFound = false;
951
1064
  for await (const message of this.client.receiveMessages(
@@ -1013,7 +1126,7 @@ var Topic = class {
1013
1126
  payload,
1014
1127
  idempotencyKey: options?.idempotencyKey,
1015
1128
  retentionSeconds: options?.retentionSeconds,
1016
- deploymentId: options?.deploymentId
1129
+ delaySeconds: options?.delaySeconds
1017
1130
  },
1018
1131
  this.transport
1019
1132
  );
@@ -1123,7 +1236,7 @@ async function parseCallback(request) {
1123
1236
  messageId
1124
1237
  };
1125
1238
  }
1126
- function handleCallback(handlers) {
1239
+ function createCallbackHandler(handlers, client) {
1127
1240
  for (const topicPattern in handlers) {
1128
1241
  if (topicPattern.includes("*")) {
1129
1242
  if (!validateWildcardPattern(topicPattern)) {
@@ -1158,7 +1271,6 @@ function handleCallback(handlers) {
1158
1271
  { status: 404 }
1159
1272
  );
1160
1273
  }
1161
- const client = new QueueClient();
1162
1274
  const topic = new Topic(client, queueName);
1163
1275
  const cg = topic.consumerGroup(consumerGroup);
1164
1276
  await cg.consume(consumerGroupHandler, { messageId });
@@ -1174,11 +1286,11 @@ function handleCallback(handlers) {
1174
1286
  );
1175
1287
  }
1176
1288
  };
1177
- if (isDevMode()) {
1178
- registerDevRouteHandler(routeHandler, handlers);
1179
- }
1180
1289
  return routeHandler;
1181
1290
  }
1291
+ function handleCallback(handlers, client) {
1292
+ return createCallbackHandler(handlers, client || new QueueClient());
1293
+ }
1182
1294
 
1183
1295
  // src/nextjs-pages.ts
1184
1296
  function getHeader(headers, name) {