@vercel/queue 0.0.0-alpha.2 → 0.0.0-alpha.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,128 +1,79 @@
1
- // src/oidc.ts
2
- async function getVercelOidcToken() {
3
- const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
4
- const fromSymbol = globalThis;
5
- const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
6
- const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
7
- if (!token) {
8
- throw new Error(
9
- `The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`
10
- );
11
- }
12
- return token;
13
- }
14
-
15
- // src/client.ts
16
- import { parseMultipartStream } from "mixpart";
17
-
18
- // src/local.ts
19
- import { spawn } from "child_process";
20
- function isLocalhostWithPort(url) {
1
+ // src/transports.ts
2
+ async function streamToBuffer(stream) {
3
+ let totalLength = 0;
4
+ const reader = stream.getReader();
5
+ const chunks = [];
21
6
  try {
22
- const parsedUrl = new URL(url);
23
- const isLocalhost = parsedUrl.hostname === "localhost";
24
- const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 0;
25
- return { isLocalhost, port };
26
- } catch {
27
- return { isLocalhost: false };
7
+ while (true) {
8
+ const { done, value } = await reader.read();
9
+ if (done) break;
10
+ chunks.push(value);
11
+ totalLength += value.length;
12
+ }
13
+ } finally {
14
+ reader.releaseLock();
28
15
  }
16
+ return Buffer.concat(chunks, totalLength);
29
17
  }
30
- function isSupportedPlatform() {
31
- const platform = process.platform;
32
- return platform === "darwin" || platform === "linux";
33
- }
34
- function processDevelopmentCallbacks(callbacks) {
35
- const isDevelopment = process.env.NODE_ENV === "development";
36
- if (!isDevelopment) {
37
- return [];
18
+ var JsonTransport = class {
19
+ contentType = "application/json";
20
+ replacer;
21
+ reviver;
22
+ constructor(options = {}) {
23
+ this.replacer = options.replacer;
24
+ this.reviver = options.reviver;
38
25
  }
39
- if (!isSupportedPlatform()) {
40
- const hasLocalhostCallbacks = Object.values(callbacks).some((config) => {
41
- const { isLocalhost } = isLocalhostWithPort(config.url);
42
- return isLocalhost;
43
- });
44
- if (hasLocalhostCallbacks) {
45
- console.warn(
46
- `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.`
47
- );
48
- }
49
- return [];
26
+ serialize(value) {
27
+ return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
50
28
  }
51
- const localhostCallbacks = [];
52
- Object.entries(callbacks).forEach(([group, config]) => {
53
- const { isLocalhost, port } = isLocalhostWithPort(config.url);
54
- if (isLocalhost && port && port > 0) {
55
- localhostCallbacks.push({ group, config, port });
56
- } else {
57
- console.warn(
58
- `Queue Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
59
- );
60
- }
61
- });
62
- return localhostCallbacks;
63
- }
64
- function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
65
- localhostCallbacks.forEach(({ group, config, port }) => {
66
- const callbackHeaders = new Headers();
67
- callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
68
- callbackHeaders.set("Vqs-Queue-Name", queueName);
69
- callbackHeaders.set("Vqs-Consumer-Group", group);
70
- fireAndForgetWaitForHttpReady(
71
- config.url,
72
- port,
73
- config.delay || 0,
74
- 3,
75
- // Default retry frequency
76
- callbackHeaders
77
- );
78
- });
79
- }
80
- function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
81
- if (!isSupportedPlatform()) {
82
- console.warn(
83
- `Queue: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
84
- );
85
- return;
29
+ async deserialize(stream) {
30
+ const buffer = await streamToBuffer(stream);
31
+ return JSON.parse(buffer.toString("utf8"), this.reviver);
86
32
  }
87
- let headerArgs = "";
88
- if (headers) {
89
- const headerArray = [];
90
- headers.forEach((value, key) => {
91
- headerArray.push(`-H '${key}: ${value}'`);
92
- });
93
- headerArgs = headerArray.join(" ");
33
+ };
34
+ var BufferTransport = class {
35
+ contentType = "application/octet-stream";
36
+ serialize(value) {
37
+ return value;
38
+ }
39
+ async deserialize(stream) {
40
+ return await streamToBuffer(stream);
41
+ }
42
+ };
43
+ var StreamTransport = class {
44
+ contentType = "application/octet-stream";
45
+ serialize(value) {
46
+ return value;
47
+ }
48
+ async deserialize(stream) {
49
+ return stream;
94
50
  }
95
- const bashScript = `
96
- # Wait for any initial boot time
97
- sleep ${initialDelaySeconds}
51
+ async finalize(payload) {
52
+ const reader = payload.getReader();
53
+ try {
54
+ while (true) {
55
+ const { done } = await reader.read();
56
+ if (done) break;
57
+ }
58
+ } finally {
59
+ reader.releaseLock();
60
+ }
61
+ }
62
+ };
98
63
 
99
- missed=0
100
- while true; do
101
- # 1) Check if TCP port is listening
102
- if nc -z localhost ${port} 2>/dev/null; then
103
- missed=0
104
- # 2) If port is open, try HTTP POST check
105
- if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
106
- # Success: port is up AND HTTP returned 2xx (following redirects)
107
- exit 0
108
- fi
109
- else
110
- # Port was closed\u2014increment miss counter
111
- ((missed+=1))
112
- # If closed twice in a row, give up immediately
113
- if [ "$missed" -ge 2 ]; then
114
- exit 1
115
- fi
116
- fi
117
- # Wait before next cycle
118
- sleep ${retryFrequencySeconds}
119
- done
120
- `;
121
- const childProcess = spawn("bash", ["-c", bashScript], {
122
- stdio: "ignore",
123
- detached: true
124
- });
125
- childProcess.unref();
64
+ // src/client.ts
65
+ import { parseMultipartStream } from "mixpart";
66
+
67
+ // src/oidc.ts
68
+ function getVercelOidcToken() {
69
+ const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
70
+ const fromSymbol = globalThis;
71
+ const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
72
+ const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
73
+ if (!token) {
74
+ return null;
75
+ }
76
+ return token;
126
77
  }
127
78
 
128
79
  // src/types.ts
@@ -140,16 +91,6 @@ var MessageNotAvailableError = class extends Error {
140
91
  this.name = "MessageNotAvailableError";
141
92
  }
142
93
  };
143
- var FifoOrderingViolationError = class extends Error {
144
- nextMessageId;
145
- constructor(messageId, nextMessageId, reason) {
146
- super(
147
- `FIFO ordering violation for message ${messageId}: ${reason}. Process message ${nextMessageId} first.`
148
- );
149
- this.name = "FifoOrderingViolationError";
150
- this.nextMessageId = nextMessageId;
151
- }
152
- };
153
94
  var MessageCorruptedError = class extends Error {
154
95
  constructor(messageId, reason) {
155
96
  super(`Message ${messageId} is corrupted: ${reason}`);
@@ -191,16 +132,6 @@ var BadRequestError = class extends Error {
191
132
  this.name = "BadRequestError";
192
133
  }
193
134
  };
194
- var FailedDependencyError = class extends Error {
195
- nextMessageId;
196
- constructor(messageId, nextMessageId) {
197
- super(
198
- `Failed dependency: FIFO ordering violation for message ${messageId}. Must process message ${nextMessageId} first.`
199
- );
200
- this.name = "FailedDependencyError";
201
- this.nextMessageId = nextMessageId;
202
- }
203
- };
204
135
  var InternalServerError = class extends Error {
205
136
  constructor(message = "Unexpected server error") {
206
137
  super(message);
@@ -213,12 +144,6 @@ var InvalidLimitError = class extends Error {
213
144
  this.name = "InvalidLimitError";
214
145
  }
215
146
  };
216
- var InvalidCallbackError = class extends Error {
217
- constructor(message) {
218
- super(message);
219
- this.name = "InvalidCallbackError";
220
- }
221
- };
222
147
 
223
148
  // src/client.ts
224
149
  async function consumeStream(stream) {
@@ -248,40 +173,33 @@ function parseQueueHeaders(headers) {
248
173
  return {
249
174
  messageId,
250
175
  deliveryCount,
251
- timestamp,
176
+ createdAt: new Date(timestamp),
252
177
  contentType,
253
178
  ticket
254
179
  };
255
180
  }
256
- var QueueClient = class _QueueClient {
181
+ var QueueClient = class {
257
182
  baseUrl;
183
+ basePath;
258
184
  token;
259
185
  /**
260
186
  * Create a new Vercel Queue Service client
261
- * @param options Client configuration options
187
+ * @param options Client configuration options (optional - will auto-detect Vercel Function environment)
262
188
  */
263
- constructor(options) {
264
- this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
265
- this.token = options.token;
266
- }
267
- /**
268
- * Create a QueueClient automatically configured for Vercel Functions
269
- * This method automatically retrieves the OIDC token from the Vercel Function environment
270
- * Always creates a fresh instance since OIDC tokens expire after 15 minutes
271
- * @param baseUrl Optional base URL override
272
- * @returns Promise resolving to a new QueueClient instance
273
- */
274
- static async fromVercelFunction(baseUrl) {
275
- const token = await getVercelOidcToken();
276
- if (!token) {
277
- throw new Error(
278
- "Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment."
279
- );
189
+ constructor(options = {}) {
190
+ this.baseUrl = options.baseUrl || "https://vercel-queue.com";
191
+ this.basePath = options.basePath || "/api/v2/messages";
192
+ if (options.token) {
193
+ this.token = options.token;
194
+ } else {
195
+ const token = getVercelOidcToken();
196
+ if (!token) {
197
+ throw new Error(
198
+ "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'"
199
+ );
200
+ }
201
+ this.token = token;
280
202
  }
281
- return new _QueueClient({
282
- token,
283
- baseUrl
284
- });
285
203
  }
286
204
  /**
287
205
  * Send a message to a queue
@@ -294,40 +212,23 @@ var QueueClient = class _QueueClient {
294
212
  * @throws {InternalServerError} When server encounters an error
295
213
  */
296
214
  async sendMessage(options, transport) {
297
- const { queueName, payload, idempotencyKey, retentionSeconds, callbacks } = options;
215
+ const { queueName, payload, idempotencyKey, retentionSeconds } = options;
298
216
  const headers = new Headers({
299
217
  Authorization: `Bearer ${this.token}`,
300
218
  "Vqs-Queue-Name": queueName,
301
219
  "Content-Type": transport.contentType
302
220
  });
221
+ if (process.env.VERCEL_DEPLOYMENT_ID) {
222
+ headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
223
+ }
303
224
  if (idempotencyKey) {
304
225
  headers.set("Vqs-Idempotency-Key", idempotencyKey);
305
226
  }
306
227
  if (retentionSeconds !== void 0) {
307
228
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
308
229
  }
309
- let localhostCallbacks = [];
310
- if (callbacks) {
311
- const isDevelopment = process.env.NODE_ENV === "development";
312
- if (isDevelopment) {
313
- localhostCallbacks = processDevelopmentCallbacks(callbacks);
314
- } else {
315
- const endpoints = Object.entries(callbacks).map(
316
- ([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
317
- ).join(",");
318
- headers.set("Vqs-Callback-Url", endpoints);
319
- const delays = Object.entries(callbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
320
- if (delays) {
321
- headers.set("Vqs-Callback-Delay", delays);
322
- }
323
- const frequencies = Object.entries(callbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
324
- if (frequencies) {
325
- headers.set("Vqs-Callback-Frequency", frequencies);
326
- }
327
- }
328
- }
329
230
  const body = transport.serialize(payload);
330
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
231
+ const response = await fetch(`${this.baseUrl}${this.basePath}`, {
331
232
  method: "POST",
332
233
  headers,
333
234
  body
@@ -356,9 +257,6 @@ var QueueClient = class _QueueClient {
356
257
  );
357
258
  }
358
259
  const responseData = await response.json();
359
- if (localhostCallbacks.length > 0) {
360
- fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
361
- }
362
260
  return responseData;
363
261
  }
364
262
  /**
@@ -368,7 +266,7 @@ var QueueClient = class _QueueClient {
368
266
  * @returns AsyncGenerator that yields messages as they arrive
369
267
  * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
370
268
  * @throws {QueueEmptyError} When no messages are available (204)
371
- * @throws {MessageLockedError} When FIFO queue has locked messages (423)
269
+ * @throws {MessageLockedError} When messages are temporarily locked (423)
372
270
  * @throws {BadRequestError} When request parameters are invalid
373
271
  * @throws {UnauthorizedError} When authentication fails
374
272
  * @throws {ForbiddenError} When access is denied (environment mismatch)
@@ -394,7 +292,7 @@ var QueueClient = class _QueueClient {
394
292
  if (limit !== void 0) {
395
293
  headers.set("Vqs-Limit", limit.toString());
396
294
  }
397
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
295
+ const response = await fetch(`${this.baseUrl}${this.basePath}`, {
398
296
  method: "GET",
399
297
  headers
400
298
  });
@@ -419,7 +317,7 @@ var QueueClient = class _QueueClient {
419
317
  const parsed = parseInt(retryAfterHeader, 10);
420
318
  retryAfter = isNaN(parsed) ? void 0 : parsed;
421
319
  }
422
- throw new MessageLockedError("next message in FIFO queue", retryAfter);
320
+ throw new MessageLockedError("next message", retryAfter);
423
321
  }
424
322
  if (response.status >= 500) {
425
323
  throw new InternalServerError(
@@ -476,7 +374,7 @@ var QueueClient = class _QueueClient {
476
374
  headers.set("Vqs-Skip-Payload", "1");
477
375
  }
478
376
  const response = await fetch(
479
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
377
+ `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
480
378
  {
481
379
  method: "GET",
482
380
  headers
@@ -505,37 +403,7 @@ var QueueClient = class _QueueClient {
505
403
  }
506
404
  throw new MessageLockedError(messageId, retryAfter);
507
405
  }
508
- if (response.status === 424) {
509
- try {
510
- const errorData = await response.json();
511
- if (errorData.meta?.nextMessageId) {
512
- throw new FailedDependencyError(
513
- messageId,
514
- errorData.meta.nextMessageId
515
- );
516
- }
517
- } catch (parseError) {
518
- if (parseError instanceof FailedDependencyError) {
519
- throw parseError;
520
- }
521
- }
522
- throw new MessageNotAvailableError(
523
- messageId,
524
- "FIFO ordering violation"
525
- );
526
- }
527
406
  if (response.status === 409) {
528
- try {
529
- const errorData = await response.json();
530
- if (errorData.nextMessageId) {
531
- throw new FifoOrderingViolationError(
532
- messageId,
533
- errorData.nextMessageId,
534
- errorData.error
535
- );
536
- }
537
- } catch (parseError) {
538
- }
539
407
  throw new MessageNotAvailableError(messageId);
540
408
  }
541
409
  if (response.status >= 500) {
@@ -615,7 +483,7 @@ var QueueClient = class _QueueClient {
615
483
  async deleteMessage(options) {
616
484
  const { queueName, consumerGroup, messageId, ticket } = options;
617
485
  const response = await fetch(
618
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
486
+ `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
619
487
  {
620
488
  method: "DELETE",
621
489
  headers: new Headers({
@@ -676,7 +544,7 @@ var QueueClient = class _QueueClient {
676
544
  visibilityTimeoutSeconds
677
545
  } = options;
678
546
  const response = await fetch(
679
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
547
+ `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
680
548
  {
681
549
  method: "PATCH",
682
550
  headers: new Headers({
@@ -722,81 +590,336 @@ var QueueClient = class _QueueClient {
722
590
  }
723
591
  };
724
592
 
725
- // src/transports.ts
726
- var JsonTransport = class {
727
- contentType = "application/json";
728
- serialize(value) {
729
- return Buffer.from(JSON.stringify(value), "utf8");
593
+ // src/callback.ts
594
+ function validateWildcardPattern(pattern) {
595
+ const firstIndex = pattern.indexOf("*");
596
+ const lastIndex = pattern.lastIndexOf("*");
597
+ if (firstIndex !== lastIndex) {
598
+ return false;
730
599
  }
731
- async deserialize(stream) {
732
- const reader = stream.getReader();
733
- const chunks = [];
734
- try {
735
- while (true) {
736
- const { done, value } = await reader.read();
737
- if (done) break;
738
- chunks.push(value);
600
+ if (firstIndex === -1) {
601
+ return false;
602
+ }
603
+ if (firstIndex !== pattern.length - 1) {
604
+ return false;
605
+ }
606
+ return true;
607
+ }
608
+ function matchesWildcardPattern(topicName, pattern) {
609
+ const prefix = pattern.slice(0, -1);
610
+ return topicName.startsWith(prefix);
611
+ }
612
+ function findTopicHandler(queueName, handlers) {
613
+ const exactHandler = handlers[queueName];
614
+ if (exactHandler) {
615
+ return exactHandler;
616
+ }
617
+ for (const pattern in handlers) {
618
+ if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
619
+ return handlers[pattern];
620
+ }
621
+ }
622
+ return null;
623
+ }
624
+ async function parseCallback(request) {
625
+ const contentType = request.headers.get("content-type");
626
+ if (!contentType || !contentType.includes("application/cloudevents+json")) {
627
+ throw new Error(
628
+ "Invalid content type: expected 'application/cloudevents+json'"
629
+ );
630
+ }
631
+ let cloudEvent;
632
+ try {
633
+ cloudEvent = await request.json();
634
+ } catch (error) {
635
+ throw new Error("Failed to parse CloudEvent from request body");
636
+ }
637
+ if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
638
+ throw new Error("Invalid CloudEvent: missing required fields");
639
+ }
640
+ if (cloudEvent.type !== "com.vercel.queue.v1beta") {
641
+ throw new Error(
642
+ `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
643
+ );
644
+ }
645
+ const missingFields = [];
646
+ if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
647
+ if (!("consumerGroup" in cloudEvent.data))
648
+ missingFields.push("consumerGroup");
649
+ if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
650
+ if (missingFields.length > 0) {
651
+ throw new Error(
652
+ `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
653
+ );
654
+ }
655
+ const { messageId, queueName, consumerGroup } = cloudEvent.data;
656
+ return {
657
+ queueName,
658
+ consumerGroup,
659
+ messageId
660
+ };
661
+ }
662
+ function handleCallback(handlers) {
663
+ for (const topicPattern in handlers) {
664
+ if (topicPattern.includes("*")) {
665
+ if (!validateWildcardPattern(topicPattern)) {
666
+ throw new Error(
667
+ `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
668
+ );
739
669
  }
740
- } finally {
741
- reader.releaseLock();
742
670
  }
743
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
744
- const buffer = new Uint8Array(totalLength);
745
- let offset = 0;
746
- for (const chunk of chunks) {
747
- buffer.set(chunk, offset);
748
- offset += chunk.length;
671
+ }
672
+ const routeHandler = async (request) => {
673
+ try {
674
+ const { queueName, consumerGroup, messageId } = await parseCallback(request);
675
+ const topicHandler = findTopicHandler(queueName, handlers);
676
+ if (!topicHandler) {
677
+ const availableTopics = Object.keys(handlers).join(", ");
678
+ return Response.json(
679
+ {
680
+ error: `No handler found for topic: ${queueName}`,
681
+ availableTopics
682
+ },
683
+ { status: 404 }
684
+ );
685
+ }
686
+ const consumerGroupHandler = topicHandler[consumerGroup];
687
+ if (!consumerGroupHandler) {
688
+ const availableGroups = Object.keys(topicHandler).join(", ");
689
+ return Response.json(
690
+ {
691
+ error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
692
+ availableGroups
693
+ },
694
+ { status: 404 }
695
+ );
696
+ }
697
+ const client = new QueueClient();
698
+ const topic = new Topic(client, queueName);
699
+ const cg = topic.consumerGroup(consumerGroup);
700
+ await cg.consume(consumerGroupHandler, { messageId });
701
+ return Response.json({ status: "success" });
702
+ } catch (error) {
703
+ console.error("Queue callback error:", error);
704
+ if (error instanceof Error && (error.message.includes("Missing required CloudEvent data fields") || error.message.includes("Invalid CloudEvent") || error.message.includes("Invalid CloudEvent type") || error.message.includes("Invalid content type") || error.message.includes("Failed to parse CloudEvent"))) {
705
+ return Response.json({ error: error.message }, { status: 400 });
706
+ }
707
+ return Response.json(
708
+ { error: "Failed to process queue message" },
709
+ { status: 500 }
710
+ );
749
711
  }
750
- return JSON.parse(Buffer.from(buffer).toString("utf8"));
712
+ };
713
+ if (isDevMode()) {
714
+ registerDevRouteHandler(routeHandler, handlers);
751
715
  }
752
- };
753
- var BufferTransport = class {
754
- contentType = "application/octet-stream";
755
- serialize(value) {
756
- return value;
716
+ return routeHandler;
717
+ }
718
+
719
+ // src/dev.ts
720
+ var devRouteHandlers = /* @__PURE__ */ new Map();
721
+ var wildcardRouteHandlers = /* @__PURE__ */ new Map();
722
+ var routeHandlerKeys = /* @__PURE__ */ new WeakMap();
723
+ function cleanupDeadRefs(key, refs) {
724
+ const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
725
+ if (aliveRefs.length === 0) {
726
+ wildcardRouteHandlers.delete(key);
727
+ } else if (aliveRefs.length < refs.length) {
728
+ wildcardRouteHandlers.set(key, aliveRefs);
757
729
  }
758
- async deserialize(stream) {
759
- const reader = stream.getReader();
760
- const chunks = [];
761
- try {
762
- while (true) {
763
- const { done, value } = await reader.read();
764
- if (done) break;
765
- chunks.push(value);
730
+ }
731
+ function isDevMode() {
732
+ return process.env.NODE_ENV === "development";
733
+ }
734
+ function registerDevRouteHandler(routeHandler, handlers) {
735
+ const existingKeys = routeHandlerKeys.get(routeHandler);
736
+ if (existingKeys) {
737
+ const newKeys = /* @__PURE__ */ new Set();
738
+ for (const topicName in handlers) {
739
+ for (const consumerGroup in handlers[topicName]) {
740
+ newKeys.add(`${topicName}:${consumerGroup}`);
766
741
  }
767
- } finally {
768
- reader.releaseLock();
769
742
  }
770
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
771
- const buffer = new Uint8Array(totalLength);
772
- let offset = 0;
773
- for (const chunk of chunks) {
774
- buffer.set(chunk, offset);
775
- offset += chunk.length;
743
+ for (const key of existingKeys) {
744
+ if (!newKeys.has(key)) {
745
+ const [topicPattern] = key.split(":");
746
+ if (topicPattern.includes("*")) {
747
+ const refs = wildcardRouteHandlers.get(key);
748
+ if (refs) {
749
+ const filteredRefs = refs.filter(
750
+ (ref) => ref.deref() !== routeHandler
751
+ );
752
+ if (filteredRefs.length === 0) {
753
+ wildcardRouteHandlers.delete(key);
754
+ } else {
755
+ wildcardRouteHandlers.set(key, filteredRefs);
756
+ }
757
+ }
758
+ } else {
759
+ devRouteHandlers.delete(key);
760
+ }
761
+ }
776
762
  }
777
- return Buffer.from(buffer);
778
763
  }
779
- };
780
- var StreamTransport = class {
781
- contentType = "application/octet-stream";
782
- serialize(value) {
783
- return value;
764
+ const keys = /* @__PURE__ */ new Set();
765
+ for (const topicName in handlers) {
766
+ for (const consumerGroup in handlers[topicName]) {
767
+ const key = `${topicName}:${consumerGroup}`;
768
+ keys.add(key);
769
+ if (topicName.includes("*")) {
770
+ const weakRef = new WeakRef(routeHandler);
771
+ const existing = wildcardRouteHandlers.get(key) || [];
772
+ cleanupDeadRefs(key, existing);
773
+ const cleanedRefs = wildcardRouteHandlers.get(key) || [];
774
+ cleanedRefs.push(weakRef);
775
+ wildcardRouteHandlers.set(key, cleanedRefs);
776
+ } else {
777
+ devRouteHandlers.set(key, {
778
+ routeHandler,
779
+ topicPattern: topicName
780
+ });
781
+ }
782
+ }
784
783
  }
785
- async deserialize(stream) {
786
- return stream;
784
+ routeHandlerKeys.set(routeHandler, keys);
785
+ }
786
+ function findRouteHandlersForTopic(topicName) {
787
+ const handlersMap = /* @__PURE__ */ new Map();
788
+ for (const [
789
+ key,
790
+ { routeHandler, topicPattern }
791
+ ] of devRouteHandlers.entries()) {
792
+ const [_, consumerGroup] = key.split(":");
793
+ if (topicPattern === topicName) {
794
+ if (!handlersMap.has(routeHandler)) {
795
+ handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
796
+ }
797
+ handlersMap.get(routeHandler).add(consumerGroup);
798
+ }
787
799
  }
788
- async finalize(payload) {
789
- const reader = payload.getReader();
790
- try {
791
- while (true) {
792
- const { done } = await reader.read();
793
- if (done) break;
800
+ for (const [key, refs] of wildcardRouteHandlers.entries()) {
801
+ const [pattern, consumerGroup] = key.split(":");
802
+ if (matchesWildcardPattern(topicName, pattern)) {
803
+ cleanupDeadRefs(key, refs);
804
+ const cleanedRefs = wildcardRouteHandlers.get(key) || [];
805
+ for (const ref of cleanedRefs) {
806
+ const routeHandler = ref.deref();
807
+ if (routeHandler) {
808
+ if (!handlersMap.has(routeHandler)) {
809
+ handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
810
+ }
811
+ handlersMap.get(routeHandler).add(consumerGroup);
812
+ }
794
813
  }
795
- } finally {
796
- reader.releaseLock();
797
814
  }
798
815
  }
799
- };
816
+ return handlersMap;
817
+ }
818
+ function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
819
+ const cloudEvent = {
820
+ type: "com.vercel.queue.v1beta",
821
+ source: `/topic/${topicName}/consumer/${consumerGroup}`,
822
+ id: messageId,
823
+ datacontenttype: "application/json",
824
+ data: {
825
+ messageId,
826
+ queueName: topicName,
827
+ consumerGroup
828
+ },
829
+ time: (/* @__PURE__ */ new Date()).toISOString(),
830
+ specversion: "1.0"
831
+ };
832
+ return new Request("https://localhost/api/queue/callback", {
833
+ method: "POST",
834
+ headers: {
835
+ "Content-Type": "application/cloudevents+json"
836
+ },
837
+ body: JSON.stringify(cloudEvent)
838
+ });
839
+ }
840
+ var DEV_CALLBACK_DELAY = 1e3;
841
+ function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
842
+ console.log(
843
+ `[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
844
+ );
845
+ setTimeout(
846
+ () => {
847
+ console.log(
848
+ `[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
849
+ );
850
+ triggerDevCallbacks(topicName, messageId);
851
+ },
852
+ timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
853
+ );
854
+ }
855
+ function triggerDevCallbacks(topicName, messageId) {
856
+ const handlersMap = findRouteHandlersForTopic(topicName);
857
+ if (handlersMap.size === 0) {
858
+ return;
859
+ }
860
+ const consumerGroups = Array.from(
861
+ new Set(
862
+ Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
863
+ )
864
+ );
865
+ console.log(
866
+ `[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
867
+ );
868
+ setTimeout(async () => {
869
+ for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
870
+ for (const consumerGroup of consumerGroups2) {
871
+ try {
872
+ const request = createMockCloudEventRequest(
873
+ topicName,
874
+ consumerGroup,
875
+ messageId
876
+ );
877
+ const response = await routeHandler(request);
878
+ if (response.ok) {
879
+ try {
880
+ const responseData = await response.json();
881
+ if (responseData.status === "success") {
882
+ console.log(
883
+ `[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
884
+ );
885
+ }
886
+ } catch (jsonError) {
887
+ console.error(
888
+ `[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
889
+ jsonError
890
+ );
891
+ }
892
+ } else {
893
+ try {
894
+ const errorData = await response.json();
895
+ console.error(
896
+ `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
897
+ errorData.error || response.statusText
898
+ );
899
+ } catch (jsonError) {
900
+ console.error(
901
+ `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
902
+ response.statusText
903
+ );
904
+ }
905
+ }
906
+ } catch (error) {
907
+ console.error(
908
+ `[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
909
+ error
910
+ );
911
+ }
912
+ }
913
+ }
914
+ }, DEV_CALLBACK_DELAY);
915
+ }
916
+ function clearDevHandlers() {
917
+ devRouteHandlers.clear();
918
+ wildcardRouteHandlers.clear();
919
+ }
920
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
921
+ globalThis.__clearDevHandlers = clearDevHandlers;
922
+ }
800
923
 
801
924
  // src/consumer-group.ts
802
925
  var ConsumerGroup = class {
@@ -897,7 +1020,13 @@ var ConsumerGroup = class {
897
1020
  message.ticket
898
1021
  );
899
1022
  try {
900
- const result = await handler(message);
1023
+ const result = await handler(message.payload, {
1024
+ messageId: message.messageId,
1025
+ deliveryCount: message.deliveryCount,
1026
+ createdAt: message.createdAt,
1027
+ topicName: this.topicName,
1028
+ consumerGroup: this.consumerGroupName
1029
+ });
901
1030
  await stopExtension();
902
1031
  if (result && "timeoutSeconds" in result) {
903
1032
  await this.client.changeVisibility({
@@ -907,6 +1036,13 @@ var ConsumerGroup = class {
907
1036
  ticket: message.ticket,
908
1037
  visibilityTimeoutSeconds: result.timeoutSeconds
909
1038
  });
1039
+ if (isDevMode()) {
1040
+ scheduleDevTimeout(
1041
+ this.topicName,
1042
+ message.messageId,
1043
+ result.timeoutSeconds
1044
+ );
1045
+ }
910
1046
  } else {
911
1047
  await this.client.deleteMessage({
912
1048
  queueName: this.topicName,
@@ -927,219 +1063,58 @@ var ConsumerGroup = class {
927
1063
  throw error;
928
1064
  }
929
1065
  }
930
- /**
931
- * Start continuous processing of messages from the topic
932
- * @param signal AbortSignal to control when to stop processing
933
- * @param handler Function to process each message
934
- * @param options Processing options
935
- * @returns Promise that resolves when processing stops (due to signal or error)
936
- */
937
- async subscribe(signal, handler, options = {}) {
938
- const pollingInterval = options.pollingInterval || 1e3;
939
- while (!signal.aborted) {
940
- try {
941
- for await (const message of this.client.receiveMessages(
1066
+ async consume(handler, options) {
1067
+ if (options?.messageId) {
1068
+ if (options.skipPayload) {
1069
+ const response = await this.client.receiveMessageById(
942
1070
  {
943
1071
  queueName: this.topicName,
944
1072
  consumerGroup: this.consumerGroupName,
1073
+ messageId: options.messageId,
945
1074
  visibilityTimeoutSeconds: this.visibilityTimeout,
946
- limit: 1
947
- // Always process one message at a time
1075
+ skipPayload: true
948
1076
  },
949
1077
  this.transport
950
- )) {
951
- if (signal.aborted) {
952
- break;
953
- }
954
- try {
955
- await this.processMessage(message, handler);
956
- } catch (error) {
957
- console.error("Error processing message:", error);
958
- }
959
- }
960
- if (!signal.aborted) {
961
- await new Promise((resolve) => {
962
- const timeoutId = setTimeout(resolve, pollingInterval);
963
- signal.addEventListener(
964
- "abort",
965
- () => {
966
- clearTimeout(timeoutId);
967
- resolve();
968
- },
969
- { once: true }
970
- );
971
- });
972
- }
973
- } catch (error) {
974
- if (error instanceof QueueEmptyError) {
975
- if (!signal.aborted) {
976
- await new Promise((resolve) => {
977
- const timeoutId = setTimeout(resolve, pollingInterval);
978
- signal.addEventListener(
979
- "abort",
980
- () => {
981
- clearTimeout(timeoutId);
982
- resolve();
983
- },
984
- { once: true }
985
- );
986
- });
987
- }
988
- continue;
989
- }
990
- if (error instanceof MessageLockedError) {
991
- const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
992
- if (!signal.aborted) {
993
- await new Promise((resolve) => {
994
- const timeoutId = setTimeout(resolve, waitTime);
995
- signal.addEventListener(
996
- "abort",
997
- () => {
998
- clearTimeout(timeoutId);
999
- resolve();
1000
- },
1001
- { once: true }
1002
- );
1003
- });
1004
- }
1005
- continue;
1006
- }
1007
- console.error("Error polling topic:", error);
1008
- throw error;
1078
+ );
1079
+ await this.processMessage(
1080
+ response.message,
1081
+ handler
1082
+ );
1083
+ } else {
1084
+ const response = await this.client.receiveMessageById(
1085
+ {
1086
+ queueName: this.topicName,
1087
+ consumerGroup: this.consumerGroupName,
1088
+ messageId: options.messageId,
1089
+ visibilityTimeoutSeconds: this.visibilityTimeout
1090
+ },
1091
+ this.transport
1092
+ );
1093
+ await this.processMessage(
1094
+ response.message,
1095
+ handler
1096
+ );
1097
+ }
1098
+ } else {
1099
+ let messageFound = false;
1100
+ for await (const message of this.client.receiveMessages(
1101
+ {
1102
+ queueName: this.topicName,
1103
+ consumerGroup: this.consumerGroupName,
1104
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1105
+ limit: 1
1106
+ },
1107
+ this.transport
1108
+ )) {
1109
+ messageFound = true;
1110
+ await this.processMessage(message, handler);
1111
+ break;
1112
+ }
1113
+ if (!messageFound) {
1114
+ throw new Error("No messages available");
1009
1115
  }
1010
1116
  }
1011
1117
  }
1012
- /**
1013
- * Receive and process a specific message by its ID with full payload
1014
- * @param messageId The ID of the message to receive and process
1015
- * @param handler Function to process the message with full payload
1016
- * @returns Promise that resolves when the message is processed or rejects with specific errors
1017
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
1018
- * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1019
- * @throws {MessageLockedError} When the message is temporarily locked (423)
1020
- * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1021
- * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1022
- * @throws {MessageCorruptedError} When the message data is corrupted
1023
- * @throws {BadRequestError} When request parameters are invalid
1024
- * @throws {UnauthorizedError} When authentication fails
1025
- * @throws {ForbiddenError} When access is denied
1026
- * @throws {InternalServerError} When server encounters an error
1027
- */
1028
- async receiveMessage(messageId, handler) {
1029
- const response = await this.client.receiveMessageById(
1030
- {
1031
- queueName: this.topicName,
1032
- consumerGroup: this.consumerGroupName,
1033
- messageId,
1034
- visibilityTimeoutSeconds: this.visibilityTimeout
1035
- },
1036
- this.transport
1037
- );
1038
- await this.processMessage(response.message, handler);
1039
- }
1040
- /**
1041
- * Receive and process the next available message from the queue
1042
- * @param handler Function to process the message
1043
- * @returns Promise that resolves when the message is processed or rejects with specific errors
1044
- * @throws {QueueEmptyError} When no messages are available in the queue (204)
1045
- * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
1046
- * @throws {BadRequestError} When request parameters are invalid
1047
- * @throws {UnauthorizedError} When authentication fails
1048
- * @throws {ForbiddenError} When access is denied
1049
- * @throws {InternalServerError} When server encounters an error
1050
- */
1051
- async receiveNextMessage(handler) {
1052
- let messageFound = false;
1053
- for await (const message of this.client.receiveMessages(
1054
- {
1055
- queueName: this.topicName,
1056
- consumerGroup: this.consumerGroupName,
1057
- visibilityTimeoutSeconds: this.visibilityTimeout,
1058
- limit: 1
1059
- },
1060
- this.transport
1061
- )) {
1062
- messageFound = true;
1063
- await this.processMessage(message, handler);
1064
- break;
1065
- }
1066
- if (!messageFound) {
1067
- throw new Error("No messages available");
1068
- }
1069
- }
1070
- /**
1071
- * Receive and process multiple next available messages from the queue
1072
- * @param limit Number of messages to process (1-10)
1073
- * @param handler Function to process each message
1074
- * @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
1075
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
1076
- * @throws {QueueEmptyError} When no messages are available in the queue (204)
1077
- * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
1078
- * @throws {BadRequestError} When request parameters are invalid
1079
- * @throws {UnauthorizedError} When authentication fails
1080
- * @throws {ForbiddenError} When access is denied
1081
- * @throws {InternalServerError} When server encounters an error
1082
- */
1083
- async receiveNextMessages(limit, handler) {
1084
- if (limit < 1 || limit > 10) {
1085
- throw new InvalidLimitError(limit);
1086
- }
1087
- const processingPromises = [];
1088
- let messageCount = 0;
1089
- for await (const message of this.client.receiveMessages(
1090
- {
1091
- queueName: this.topicName,
1092
- consumerGroup: this.consumerGroupName,
1093
- visibilityTimeoutSeconds: this.visibilityTimeout,
1094
- limit
1095
- },
1096
- this.transport
1097
- )) {
1098
- messageCount++;
1099
- const wrappedPromise = this.processMessage(message, handler).then(
1100
- (value) => ({
1101
- status: "fulfilled",
1102
- value
1103
- }),
1104
- (reason) => ({ status: "rejected", reason })
1105
- );
1106
- processingPromises.push(wrappedPromise);
1107
- }
1108
- if (messageCount === 0) {
1109
- throw new Error("No messages available");
1110
- }
1111
- const results = await Promise.all(processingPromises);
1112
- return results;
1113
- }
1114
- /**
1115
- * Handle a specific message by its ID without downloading the payload (metadata only)
1116
- * @param messageId The ID of the message to handle
1117
- * @param handler Function to process the message metadata (payload will be void)
1118
- * @returns Promise that resolves when the message is handled or rejects with specific errors
1119
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
1120
- * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1121
- * @throws {MessageLockedError} When the message is temporarily locked (423)
1122
- * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1123
- * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1124
- * @throws {MessageCorruptedError} When the message data is corrupted
1125
- * @throws {BadRequestError} When request parameters are invalid
1126
- * @throws {UnauthorizedError} When authentication fails
1127
- * @throws {ForbiddenError} When access is denied
1128
- * @throws {InternalServerError} When server encounters an error
1129
- */
1130
- async handleMessage(messageId, handler) {
1131
- const response = await this.client.receiveMessageById(
1132
- {
1133
- queueName: this.topicName,
1134
- consumerGroup: this.consumerGroupName,
1135
- messageId,
1136
- visibilityTimeoutSeconds: this.visibilityTimeout,
1137
- skipPayload: true
1138
- },
1139
- this.transport
1140
- );
1141
- await this.processMessage(response.message, handler);
1142
- }
1143
1118
  /**
1144
1119
  * Get the consumer group name
1145
1120
  */
@@ -1186,11 +1161,13 @@ var Topic = class {
1186
1161
  queueName: this.topicName,
1187
1162
  payload,
1188
1163
  idempotencyKey: options?.idempotencyKey,
1189
- retentionSeconds: options?.retentionSeconds,
1190
- callbacks: options?.callbacks
1164
+ retentionSeconds: options?.retentionSeconds
1191
1165
  },
1192
1166
  this.transport
1193
1167
  );
1168
+ if (isDevMode()) {
1169
+ triggerDevCallbacks(this.topicName, result.messageId);
1170
+ }
1194
1171
  return { messageId: result.messageId };
1195
1172
  }
1196
1173
  /**
@@ -1226,53 +1203,59 @@ var Topic = class {
1226
1203
  };
1227
1204
 
1228
1205
  // src/factory.ts
1229
- function createTopic(client, topicName, transport) {
1230
- return new Topic(client, topicName, transport);
1206
+ async function send(topicName, payload, options) {
1207
+ const transport = options?.transport || new JsonTransport();
1208
+ const client = new QueueClient();
1209
+ const result = await client.sendMessage(
1210
+ {
1211
+ queueName: topicName,
1212
+ payload,
1213
+ idempotencyKey: options?.idempotencyKey,
1214
+ retentionSeconds: options?.retentionSeconds
1215
+ },
1216
+ transport
1217
+ );
1218
+ if (isDevMode()) {
1219
+ triggerDevCallbacks(topicName, result.messageId);
1220
+ }
1221
+ return { messageId: result.messageId };
1231
1222
  }
1232
-
1233
- // src/callback.ts
1234
- function parseCallbackRequest(request) {
1235
- const headers = request.headers;
1236
- const messageId = headers.get("Vqs-Message-Id");
1237
- const queueName = headers.get("Vqs-Queue-Name");
1238
- const consumerGroup = headers.get("Vqs-Consumer-Group");
1239
- const missingHeaders = [];
1240
- if (!messageId) missingHeaders.push("Vqs-Message-Id");
1241
- if (!queueName) missingHeaders.push("Vqs-Queue-Name");
1242
- if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
1243
- if (missingHeaders.length > 0) {
1244
- throw new InvalidCallbackError(
1245
- `Missing required queue callback headers: ${missingHeaders.join(", ")}`
1246
- );
1223
+ async function receive(topicName, consumerGroup, handler, options) {
1224
+ const transport = options?.transport || new JsonTransport();
1225
+ const client = new QueueClient();
1226
+ const topic = new Topic(client, topicName, transport);
1227
+ const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
1228
+ const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1229
+ if (messageId) {
1230
+ if (skipPayload) {
1231
+ return consumer.consume(handler, {
1232
+ messageId,
1233
+ skipPayload: true
1234
+ });
1235
+ } else {
1236
+ return consumer.consume(handler, { messageId });
1237
+ }
1238
+ } else {
1239
+ return consumer.consume(handler);
1247
1240
  }
1248
- return {
1249
- messageId,
1250
- queueName,
1251
- consumerGroup
1252
- };
1253
1241
  }
1254
1242
  export {
1255
1243
  BadRequestError,
1256
1244
  BufferTransport,
1257
- ConsumerGroup,
1258
- FailedDependencyError,
1259
- FifoOrderingViolationError,
1260
1245
  ForbiddenError,
1261
1246
  InternalServerError,
1262
- InvalidCallbackError,
1263
1247
  InvalidLimitError,
1264
1248
  JsonTransport,
1265
1249
  MessageCorruptedError,
1266
1250
  MessageLockedError,
1267
1251
  MessageNotAvailableError,
1268
1252
  MessageNotFoundError,
1269
- QueueClient,
1270
1253
  QueueEmptyError,
1271
1254
  StreamTransport,
1272
- Topic,
1273
1255
  UnauthorizedError,
1274
- createTopic,
1275
- getVercelOidcToken,
1276
- parseCallbackRequest
1256
+ handleCallback,
1257
+ parseCallback,
1258
+ receive,
1259
+ send
1277
1260
  };
1278
1261
  //# sourceMappingURL=index.mjs.map