@vercel/queue 0.0.0-alpha.12 → 0.0.0-alpha.3

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,90 +1,130 @@
1
- // src/transports.ts
2
- var JsonTransport = class {
3
- contentType = "application/json";
4
- serialize(value) {
5
- return Buffer.from(JSON.stringify(value), "utf8");
6
- }
7
- async deserialize(stream) {
8
- const reader = stream.getReader();
9
- let totalLength = 0;
10
- const chunks = [];
11
- try {
12
- while (true) {
13
- const { done, value } = await reader.read();
14
- if (done) break;
15
- chunks.push(value);
16
- totalLength += value.length;
17
- }
18
- } finally {
19
- reader.releaseLock();
20
- }
21
- const buffer = Buffer.concat(chunks, totalLength);
22
- return JSON.parse(buffer.toString("utf8"));
23
- }
24
- };
25
- var BufferTransport = class {
26
- contentType = "application/octet-stream";
27
- serialize(value) {
28
- return value;
29
- }
30
- async deserialize(stream) {
31
- const reader = stream.getReader();
32
- const chunks = [];
33
- try {
34
- while (true) {
35
- const { done, value } = await reader.read();
36
- if (done) break;
37
- chunks.push(value);
38
- }
39
- } finally {
40
- reader.releaseLock();
41
- }
42
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
43
- const buffer = new Uint8Array(totalLength);
44
- let offset = 0;
45
- for (const chunk of chunks) {
46
- buffer.set(chunk, offset);
47
- offset += chunk.length;
48
- }
49
- return Buffer.from(buffer);
50
- }
51
- };
52
- var StreamTransport = class {
53
- contentType = "application/octet-stream";
54
- serialize(value) {
55
- return value;
56
- }
57
- async deserialize(stream) {
58
- return stream;
59
- }
60
- async finalize(payload) {
61
- const reader = payload.getReader();
62
- try {
63
- while (true) {
64
- const { done } = await reader.read();
65
- if (done) break;
66
- }
67
- } finally {
68
- reader.releaseLock();
69
- }
70
- }
71
- };
72
-
73
- // src/client.ts
74
- import { parseMultipartStream } from "mixpart";
75
-
76
1
  // src/oidc.ts
77
- function getVercelOidcToken() {
2
+ async function getVercelOidcToken() {
78
3
  const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
79
4
  const fromSymbol = globalThis;
80
5
  const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
81
6
  const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
82
7
  if (!token) {
83
- return null;
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
+ );
84
11
  }
85
12
  return token;
86
13
  }
87
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) {
21
+ 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 };
28
+ }
29
+ }
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 [];
38
+ }
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 [];
50
+ }
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;
86
+ }
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(" ");
94
+ }
95
+ const bashScript = `
96
+ # Wait for any initial boot time
97
+ sleep ${initialDelaySeconds}
98
+
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();
126
+ }
127
+
88
128
  // src/types.ts
89
129
  var MessageNotFoundError = class extends Error {
90
130
  constructor(messageId) {
@@ -100,6 +140,16 @@ var MessageNotAvailableError = class extends Error {
100
140
  this.name = "MessageNotAvailableError";
101
141
  }
102
142
  };
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
+ };
103
153
  var MessageCorruptedError = class extends Error {
104
154
  constructor(messageId, reason) {
105
155
  super(`Message ${messageId} is corrupted: ${reason}`);
@@ -141,6 +191,16 @@ var BadRequestError = class extends Error {
141
191
  this.name = "BadRequestError";
142
192
  }
143
193
  };
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
+ };
144
204
  var InternalServerError = class extends Error {
145
205
  constructor(message = "Unexpected server error") {
146
206
  super(message);
@@ -153,6 +213,12 @@ var InvalidLimitError = class extends Error {
153
213
  this.name = "InvalidLimitError";
154
214
  }
155
215
  };
216
+ var InvalidCallbackError = class extends Error {
217
+ constructor(message) {
218
+ super(message);
219
+ this.name = "InvalidCallbackError";
220
+ }
221
+ };
156
222
 
157
223
  // src/client.ts
158
224
  async function consumeStream(stream) {
@@ -182,33 +248,40 @@ function parseQueueHeaders(headers) {
182
248
  return {
183
249
  messageId,
184
250
  deliveryCount,
185
- createdAt: new Date(timestamp),
251
+ timestamp,
186
252
  contentType,
187
253
  ticket
188
254
  };
189
255
  }
190
- var QueueClient = class {
256
+ var QueueClient = class _QueueClient {
191
257
  baseUrl;
192
- basePath;
193
258
  token;
194
259
  /**
195
260
  * Create a new Vercel Queue Service client
196
- * @param options Client configuration options (optional - will auto-detect Vercel Function environment)
261
+ * @param options Client configuration options
197
262
  */
198
- constructor(options = {}) {
199
- this.baseUrl = options.baseUrl || "https://vercel-queue.com";
200
- this.basePath = options.basePath || "/api/v2/messages";
201
- if (options.token) {
202
- this.token = options.token;
203
- } else {
204
- const token = getVercelOidcToken();
205
- if (!token) {
206
- throw new Error(
207
- "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'"
208
- );
209
- }
210
- this.token = token;
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
+ );
211
280
  }
281
+ return new _QueueClient({
282
+ token,
283
+ baseUrl
284
+ });
212
285
  }
213
286
  /**
214
287
  * Send a message to a queue
@@ -221,23 +294,48 @@ var QueueClient = class {
221
294
  * @throws {InternalServerError} When server encounters an error
222
295
  */
223
296
  async sendMessage(options, transport) {
224
- const { queueName, payload, idempotencyKey, retentionSeconds } = options;
297
+ const { queueName, payload, idempotencyKey, retentionSeconds, callback } = options;
225
298
  const headers = new Headers({
226
299
  Authorization: `Bearer ${this.token}`,
227
300
  "Vqs-Queue-Name": queueName,
228
301
  "Content-Type": transport.contentType
229
302
  });
230
- if (process.env.VERCEL_DEPLOYMENT_ID) {
231
- headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
232
- }
233
303
  if (idempotencyKey) {
234
304
  headers.set("Vqs-Idempotency-Key", idempotencyKey);
235
305
  }
236
306
  if (retentionSeconds !== void 0) {
237
307
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
238
308
  }
309
+ let normalizedCallbacks;
310
+ if (callback) {
311
+ if ("url" in callback && typeof callback.url === "string") {
312
+ normalizedCallbacks = { default: callback };
313
+ } else {
314
+ normalizedCallbacks = callback;
315
+ }
316
+ }
317
+ let localhostCallbacks = [];
318
+ if (normalizedCallbacks) {
319
+ const isDevelopment = process.env.NODE_ENV === "development";
320
+ if (isDevelopment) {
321
+ localhostCallbacks = processDevelopmentCallbacks(normalizedCallbacks);
322
+ } else {
323
+ const endpoints = Object.entries(normalizedCallbacks).map(
324
+ ([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
325
+ ).join(",");
326
+ headers.set("Vqs-Callback-Url", endpoints);
327
+ const delays = Object.entries(normalizedCallbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
328
+ if (delays) {
329
+ headers.set("Vqs-Callback-Delay", delays);
330
+ }
331
+ const frequencies = Object.entries(normalizedCallbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
332
+ if (frequencies) {
333
+ headers.set("Vqs-Callback-Frequency", frequencies);
334
+ }
335
+ }
336
+ }
239
337
  const body = transport.serialize(payload);
240
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
338
+ const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
241
339
  method: "POST",
242
340
  headers,
243
341
  body
@@ -266,6 +364,9 @@ var QueueClient = class {
266
364
  );
267
365
  }
268
366
  const responseData = await response.json();
367
+ if (localhostCallbacks.length > 0) {
368
+ fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
369
+ }
269
370
  return responseData;
270
371
  }
271
372
  /**
@@ -275,7 +376,7 @@ var QueueClient = class {
275
376
  * @returns AsyncGenerator that yields messages as they arrive
276
377
  * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
277
378
  * @throws {QueueEmptyError} When no messages are available (204)
278
- * @throws {MessageLockedError} When messages are temporarily locked (423)
379
+ * @throws {MessageLockedError} When FIFO queue has locked messages (423)
279
380
  * @throws {BadRequestError} When request parameters are invalid
280
381
  * @throws {UnauthorizedError} When authentication fails
281
382
  * @throws {ForbiddenError} When access is denied (environment mismatch)
@@ -301,7 +402,7 @@ var QueueClient = class {
301
402
  if (limit !== void 0) {
302
403
  headers.set("Vqs-Limit", limit.toString());
303
404
  }
304
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
405
+ const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
305
406
  method: "GET",
306
407
  headers
307
408
  });
@@ -326,7 +427,7 @@ var QueueClient = class {
326
427
  const parsed = parseInt(retryAfterHeader, 10);
327
428
  retryAfter = isNaN(parsed) ? void 0 : parsed;
328
429
  }
329
- throw new MessageLockedError("next message", retryAfter);
430
+ throw new MessageLockedError("next message in FIFO queue", retryAfter);
330
431
  }
331
432
  if (response.status >= 500) {
332
433
  throw new InternalServerError(
@@ -383,7 +484,7 @@ var QueueClient = class {
383
484
  headers.set("Vqs-Skip-Payload", "1");
384
485
  }
385
486
  const response = await fetch(
386
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
487
+ `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
387
488
  {
388
489
  method: "GET",
389
490
  headers
@@ -412,7 +513,37 @@ var QueueClient = class {
412
513
  }
413
514
  throw new MessageLockedError(messageId, retryAfter);
414
515
  }
516
+ if (response.status === 424) {
517
+ try {
518
+ const errorData = await response.json();
519
+ if (errorData.meta?.nextMessageId) {
520
+ throw new FailedDependencyError(
521
+ messageId,
522
+ errorData.meta.nextMessageId
523
+ );
524
+ }
525
+ } catch (parseError) {
526
+ if (parseError instanceof FailedDependencyError) {
527
+ throw parseError;
528
+ }
529
+ }
530
+ throw new MessageNotAvailableError(
531
+ messageId,
532
+ "FIFO ordering violation"
533
+ );
534
+ }
415
535
  if (response.status === 409) {
536
+ try {
537
+ const errorData = await response.json();
538
+ if (errorData.nextMessageId) {
539
+ throw new FifoOrderingViolationError(
540
+ messageId,
541
+ errorData.nextMessageId,
542
+ errorData.error
543
+ );
544
+ }
545
+ } catch (parseError) {
546
+ }
416
547
  throw new MessageNotAvailableError(messageId);
417
548
  }
418
549
  if (response.status >= 500) {
@@ -492,7 +623,7 @@ var QueueClient = class {
492
623
  async deleteMessage(options) {
493
624
  const { queueName, consumerGroup, messageId, ticket } = options;
494
625
  const response = await fetch(
495
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
626
+ `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
496
627
  {
497
628
  method: "DELETE",
498
629
  headers: new Headers({
@@ -553,7 +684,7 @@ var QueueClient = class {
553
684
  visibilityTimeoutSeconds
554
685
  } = options;
555
686
  const response = await fetch(
556
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
687
+ `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
557
688
  {
558
689
  method: "PATCH",
559
690
  headers: new Headers({
@@ -599,6 +730,82 @@ var QueueClient = class {
599
730
  }
600
731
  };
601
732
 
733
+ // src/transports.ts
734
+ var JsonTransport = class {
735
+ contentType = "application/json";
736
+ serialize(value) {
737
+ return Buffer.from(JSON.stringify(value), "utf8");
738
+ }
739
+ async deserialize(stream) {
740
+ const reader = stream.getReader();
741
+ const chunks = [];
742
+ try {
743
+ while (true) {
744
+ const { done, value } = await reader.read();
745
+ if (done) break;
746
+ chunks.push(value);
747
+ }
748
+ } finally {
749
+ reader.releaseLock();
750
+ }
751
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
752
+ const buffer = new Uint8Array(totalLength);
753
+ let offset = 0;
754
+ for (const chunk of chunks) {
755
+ buffer.set(chunk, offset);
756
+ offset += chunk.length;
757
+ }
758
+ return JSON.parse(Buffer.from(buffer).toString("utf8"));
759
+ }
760
+ };
761
+ var BufferTransport = class {
762
+ contentType = "application/octet-stream";
763
+ serialize(value) {
764
+ return value;
765
+ }
766
+ async deserialize(stream) {
767
+ const reader = stream.getReader();
768
+ const chunks = [];
769
+ try {
770
+ while (true) {
771
+ const { done, value } = await reader.read();
772
+ if (done) break;
773
+ chunks.push(value);
774
+ }
775
+ } finally {
776
+ reader.releaseLock();
777
+ }
778
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
779
+ const buffer = new Uint8Array(totalLength);
780
+ let offset = 0;
781
+ for (const chunk of chunks) {
782
+ buffer.set(chunk, offset);
783
+ offset += chunk.length;
784
+ }
785
+ return Buffer.from(buffer);
786
+ }
787
+ };
788
+ var StreamTransport = class {
789
+ contentType = "application/octet-stream";
790
+ serialize(value) {
791
+ return value;
792
+ }
793
+ async deserialize(stream) {
794
+ return stream;
795
+ }
796
+ async finalize(payload) {
797
+ const reader = payload.getReader();
798
+ try {
799
+ while (true) {
800
+ const { done } = await reader.read();
801
+ if (done) break;
802
+ }
803
+ } finally {
804
+ reader.releaseLock();
805
+ }
806
+ }
807
+ };
808
+
602
809
  // src/consumer-group.ts
603
810
  var ConsumerGroup = class {
604
811
  client;
@@ -698,13 +905,7 @@ var ConsumerGroup = class {
698
905
  message.ticket
699
906
  );
700
907
  try {
701
- const result = await handler(message.payload, {
702
- messageId: message.messageId,
703
- deliveryCount: message.deliveryCount,
704
- createdAt: message.createdAt,
705
- topicName: this.topicName,
706
- consumerGroup: this.consumerGroupName
707
- });
908
+ const result = await handler(message);
708
909
  await stopExtension();
709
910
  if (result && "timeoutSeconds" in result) {
710
911
  await this.client.changeVisibility({
@@ -734,58 +935,219 @@ var ConsumerGroup = class {
734
935
  throw error;
735
936
  }
736
937
  }
737
- async consume(handler, options) {
738
- if (options?.messageId) {
739
- if (options.skipPayload) {
740
- const response = await this.client.receiveMessageById(
938
+ /**
939
+ * Start continuous processing of messages from the topic
940
+ * @param signal AbortSignal to control when to stop processing
941
+ * @param handler Function to process each message
942
+ * @param options Processing options
943
+ * @returns Promise that resolves when processing stops (due to signal or error)
944
+ */
945
+ async subscribe(signal, handler, options = {}) {
946
+ const pollingInterval = options.pollingInterval || 1e3;
947
+ while (!signal.aborted) {
948
+ try {
949
+ for await (const message of this.client.receiveMessages(
741
950
  {
742
951
  queueName: this.topicName,
743
952
  consumerGroup: this.consumerGroupName,
744
- messageId: options.messageId,
745
953
  visibilityTimeoutSeconds: this.visibilityTimeout,
746
- skipPayload: true
954
+ limit: 1
955
+ // Always process one message at a time
747
956
  },
748
957
  this.transport
749
- );
750
- await this.processMessage(
751
- response.message,
752
- handler
753
- );
754
- } else {
755
- const response = await this.client.receiveMessageById(
756
- {
757
- queueName: this.topicName,
758
- consumerGroup: this.consumerGroupName,
759
- messageId: options.messageId,
760
- visibilityTimeoutSeconds: this.visibilityTimeout
761
- },
762
- this.transport
763
- );
764
- await this.processMessage(
765
- response.message,
766
- handler
767
- );
768
- }
769
- } else {
770
- let messageFound = false;
771
- for await (const message of this.client.receiveMessages(
772
- {
773
- queueName: this.topicName,
774
- consumerGroup: this.consumerGroupName,
775
- visibilityTimeoutSeconds: this.visibilityTimeout,
776
- limit: 1
777
- },
778
- this.transport
779
- )) {
780
- messageFound = true;
781
- await this.processMessage(message, handler);
782
- break;
783
- }
784
- if (!messageFound) {
785
- throw new Error("No messages available");
958
+ )) {
959
+ if (signal.aborted) {
960
+ break;
961
+ }
962
+ try {
963
+ await this.processMessage(message, handler);
964
+ } catch (error) {
965
+ console.error("Error processing message:", error);
966
+ }
967
+ }
968
+ if (!signal.aborted) {
969
+ await new Promise((resolve) => {
970
+ const timeoutId = setTimeout(resolve, pollingInterval);
971
+ signal.addEventListener(
972
+ "abort",
973
+ () => {
974
+ clearTimeout(timeoutId);
975
+ resolve();
976
+ },
977
+ { once: true }
978
+ );
979
+ });
980
+ }
981
+ } catch (error) {
982
+ if (error instanceof QueueEmptyError) {
983
+ if (!signal.aborted) {
984
+ await new Promise((resolve) => {
985
+ const timeoutId = setTimeout(resolve, pollingInterval);
986
+ signal.addEventListener(
987
+ "abort",
988
+ () => {
989
+ clearTimeout(timeoutId);
990
+ resolve();
991
+ },
992
+ { once: true }
993
+ );
994
+ });
995
+ }
996
+ continue;
997
+ }
998
+ if (error instanceof MessageLockedError) {
999
+ const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
1000
+ if (!signal.aborted) {
1001
+ await new Promise((resolve) => {
1002
+ const timeoutId = setTimeout(resolve, waitTime);
1003
+ signal.addEventListener(
1004
+ "abort",
1005
+ () => {
1006
+ clearTimeout(timeoutId);
1007
+ resolve();
1008
+ },
1009
+ { once: true }
1010
+ );
1011
+ });
1012
+ }
1013
+ continue;
1014
+ }
1015
+ console.error("Error polling topic:", error);
1016
+ throw error;
786
1017
  }
787
1018
  }
788
1019
  }
1020
+ /**
1021
+ * Receive and process a specific message by its ID with full payload
1022
+ * @param messageId The ID of the message to receive and process
1023
+ * @param handler Function to process the message with full payload
1024
+ * @returns Promise that resolves when the message is processed or rejects with specific errors
1025
+ * @throws {MessageNotFoundError} When the message doesn't exist (404)
1026
+ * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1027
+ * @throws {MessageLockedError} When the message is temporarily locked (423)
1028
+ * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1029
+ * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1030
+ * @throws {MessageCorruptedError} When the message data is corrupted
1031
+ * @throws {BadRequestError} When request parameters are invalid
1032
+ * @throws {UnauthorizedError} When authentication fails
1033
+ * @throws {ForbiddenError} When access is denied
1034
+ * @throws {InternalServerError} When server encounters an error
1035
+ */
1036
+ async receiveMessage(messageId, handler) {
1037
+ const response = await this.client.receiveMessageById(
1038
+ {
1039
+ queueName: this.topicName,
1040
+ consumerGroup: this.consumerGroupName,
1041
+ messageId,
1042
+ visibilityTimeoutSeconds: this.visibilityTimeout
1043
+ },
1044
+ this.transport
1045
+ );
1046
+ await this.processMessage(response.message, handler);
1047
+ }
1048
+ /**
1049
+ * Receive and process the next available message from the queue
1050
+ * @param handler Function to process the message
1051
+ * @returns Promise that resolves when the message is processed or rejects with specific errors
1052
+ * @throws {QueueEmptyError} When no messages are available in the queue (204)
1053
+ * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
1054
+ * @throws {BadRequestError} When request parameters are invalid
1055
+ * @throws {UnauthorizedError} When authentication fails
1056
+ * @throws {ForbiddenError} When access is denied
1057
+ * @throws {InternalServerError} When server encounters an error
1058
+ */
1059
+ async receiveNextMessage(handler) {
1060
+ let messageFound = false;
1061
+ for await (const message of this.client.receiveMessages(
1062
+ {
1063
+ queueName: this.topicName,
1064
+ consumerGroup: this.consumerGroupName,
1065
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1066
+ limit: 1
1067
+ },
1068
+ this.transport
1069
+ )) {
1070
+ messageFound = true;
1071
+ await this.processMessage(message, handler);
1072
+ break;
1073
+ }
1074
+ if (!messageFound) {
1075
+ throw new Error("No messages available");
1076
+ }
1077
+ }
1078
+ /**
1079
+ * Receive and process multiple next available messages from the queue
1080
+ * @param limit Number of messages to process (1-10)
1081
+ * @param handler Function to process each message
1082
+ * @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
1083
+ * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
1084
+ * @throws {QueueEmptyError} When no messages are available in the queue (204)
1085
+ * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
1086
+ * @throws {BadRequestError} When request parameters are invalid
1087
+ * @throws {UnauthorizedError} When authentication fails
1088
+ * @throws {ForbiddenError} When access is denied
1089
+ * @throws {InternalServerError} When server encounters an error
1090
+ */
1091
+ async receiveNextMessages(limit, handler) {
1092
+ if (limit < 1 || limit > 10) {
1093
+ throw new InvalidLimitError(limit);
1094
+ }
1095
+ const processingPromises = [];
1096
+ let messageCount = 0;
1097
+ for await (const message of this.client.receiveMessages(
1098
+ {
1099
+ queueName: this.topicName,
1100
+ consumerGroup: this.consumerGroupName,
1101
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1102
+ limit
1103
+ },
1104
+ this.transport
1105
+ )) {
1106
+ messageCount++;
1107
+ const wrappedPromise = this.processMessage(message, handler).then(
1108
+ (value) => ({
1109
+ status: "fulfilled",
1110
+ value
1111
+ }),
1112
+ (reason) => ({ status: "rejected", reason })
1113
+ );
1114
+ processingPromises.push(wrappedPromise);
1115
+ }
1116
+ if (messageCount === 0) {
1117
+ throw new Error("No messages available");
1118
+ }
1119
+ const results = await Promise.all(processingPromises);
1120
+ return results;
1121
+ }
1122
+ /**
1123
+ * Handle a specific message by its ID without downloading the payload (metadata only)
1124
+ * @param messageId The ID of the message to handle
1125
+ * @param handler Function to process the message metadata (payload will be void)
1126
+ * @returns Promise that resolves when the message is handled or rejects with specific errors
1127
+ * @throws {MessageNotFoundError} When the message doesn't exist (404)
1128
+ * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1129
+ * @throws {MessageLockedError} When the message is temporarily locked (423)
1130
+ * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1131
+ * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1132
+ * @throws {MessageCorruptedError} When the message data is corrupted
1133
+ * @throws {BadRequestError} When request parameters are invalid
1134
+ * @throws {UnauthorizedError} When authentication fails
1135
+ * @throws {ForbiddenError} When access is denied
1136
+ * @throws {InternalServerError} When server encounters an error
1137
+ */
1138
+ async handleMessage(messageId, handler) {
1139
+ const response = await this.client.receiveMessageById(
1140
+ {
1141
+ queueName: this.topicName,
1142
+ consumerGroup: this.consumerGroupName,
1143
+ messageId,
1144
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1145
+ skipPayload: true
1146
+ },
1147
+ this.transport
1148
+ );
1149
+ await this.processMessage(response.message, handler);
1150
+ }
789
1151
  /**
790
1152
  * Get the consumer group name
791
1153
  */
@@ -832,7 +1194,8 @@ var Topic = class {
832
1194
  queueName: this.topicName,
833
1195
  payload,
834
1196
  idempotencyKey: options?.idempotencyKey,
835
- retentionSeconds: options?.retentionSeconds
1197
+ retentionSeconds: options?.retentionSeconds,
1198
+ callback: options?.callback
836
1199
  },
837
1200
  this.transport
838
1201
  );
@@ -871,156 +1234,79 @@ var Topic = class {
871
1234
  };
872
1235
 
873
1236
  // src/factory.ts
874
- async function send(topicName, payload, options) {
875
- const transport = options?.transport || new JsonTransport();
876
- const client = new QueueClient();
877
- const result = await client.sendMessage(
878
- {
879
- queueName: topicName,
880
- payload,
881
- idempotencyKey: options?.idempotencyKey,
882
- retentionSeconds: options?.retentionSeconds
883
- },
884
- transport
885
- );
886
- return { messageId: result.messageId };
887
- }
888
- async function receive(topicName, consumerGroup, handler, options) {
889
- const transport = options?.transport || new JsonTransport();
890
- const client = new QueueClient();
891
- const topic = new Topic(client, topicName, transport);
892
- const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
893
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
894
- if (messageId) {
895
- if (skipPayload) {
896
- return consumer.consume(handler, {
897
- messageId,
898
- skipPayload: true
899
- });
900
- } else {
901
- return consumer.consume(handler, { messageId });
902
- }
903
- } else {
904
- return consumer.consume(handler);
905
- }
1237
+ function createTopic(client, topicName, transport) {
1238
+ return new Topic(client, topicName, transport);
906
1239
  }
907
1240
 
908
1241
  // src/callback.ts
909
- function validateWildcardPattern(pattern) {
910
- const firstIndex = pattern.indexOf("*");
911
- const lastIndex = pattern.lastIndexOf("*");
912
- if (firstIndex !== lastIndex) {
913
- return false;
914
- }
915
- if (firstIndex === -1) {
916
- return false;
917
- }
918
- if (firstIndex !== pattern.length - 1) {
919
- return false;
920
- }
921
- return true;
922
- }
923
- function matchesWildcardPattern(topicName, pattern) {
924
- const prefix = pattern.slice(0, -1);
925
- return topicName.startsWith(prefix);
926
- }
927
- function findTopicHandler(queueName, handlers) {
928
- const exactHandler = handlers[queueName];
929
- if (exactHandler) {
930
- return exactHandler;
931
- }
932
- for (const pattern in handlers) {
933
- if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
934
- return handlers[pattern];
935
- }
936
- }
937
- return null;
938
- }
939
- async function parseCallback(request) {
940
- const contentType = request.headers.get("content-type");
941
- if (!contentType || !contentType.includes("application/cloudevents+json")) {
942
- throw new Error(
943
- "Invalid content type: expected 'application/cloudevents+json'"
944
- );
945
- }
946
- let cloudEvent;
947
- try {
948
- cloudEvent = await request.json();
949
- } catch (error) {
950
- throw new Error("Failed to parse CloudEvent from request body");
951
- }
952
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
953
- throw new Error("Invalid CloudEvent: missing required fields");
954
- }
955
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
956
- throw new Error(
957
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
958
- );
959
- }
960
- const missingFields = [];
961
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
962
- if (!("consumerGroup" in cloudEvent.data))
963
- missingFields.push("consumerGroup");
964
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
965
- if (missingFields.length > 0) {
966
- throw new Error(
967
- `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1242
+ function parseCallbackRequest(request) {
1243
+ const headers = request.headers;
1244
+ const messageId = headers.get("Vqs-Message-Id");
1245
+ const queueName = headers.get("Vqs-Queue-Name");
1246
+ const consumerGroup = headers.get("Vqs-Consumer-Group");
1247
+ const missingHeaders = [];
1248
+ if (!messageId) missingHeaders.push("Vqs-Message-Id");
1249
+ if (!queueName) missingHeaders.push("Vqs-Queue-Name");
1250
+ if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
1251
+ if (missingHeaders.length > 0) {
1252
+ throw new InvalidCallbackError(
1253
+ `Missing required queue callback headers: ${missingHeaders.join(", ")}`
968
1254
  );
969
1255
  }
970
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
971
1256
  return {
1257
+ messageId,
972
1258
  queueName,
973
- consumerGroup,
974
- messageId
1259
+ consumerGroup
975
1260
  };
976
1261
  }
977
1262
  function handleCallback(handlers) {
978
- for (const topicPattern in handlers) {
979
- if (topicPattern.includes("*")) {
980
- if (!validateWildcardPattern(topicPattern)) {
981
- throw new Error(
982
- `Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
983
- );
984
- }
985
- }
986
- }
987
1263
  return async (request) => {
988
1264
  try {
989
- const { queueName, consumerGroup, messageId } = await parseCallback(request);
990
- const topicHandler = findTopicHandler(queueName, handlers);
1265
+ const { queueName, consumerGroup, messageId } = parseCallbackRequest(request);
1266
+ const topicHandler = handlers[queueName];
991
1267
  if (!topicHandler) {
992
- const availableTopics = Object.keys(handlers).join(", ");
993
- return Response.json(
994
- {
995
- error: `No handler found for topic: ${queueName}`,
996
- availableTopics
997
- },
998
- { status: 404 }
999
- );
1268
+ throw new Error(`No handler found for topic: ${queueName}`);
1000
1269
  }
1001
- const consumerGroupHandler = topicHandler[consumerGroup];
1002
- if (!consumerGroupHandler) {
1003
- const availableGroups = Object.keys(topicHandler).join(", ");
1004
- return Response.json(
1005
- {
1006
- error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
1007
- availableGroups
1008
- },
1009
- { status: 404 }
1010
- );
1270
+ let actualHandler;
1271
+ if (typeof topicHandler === "function") {
1272
+ if (consumerGroup !== "default") {
1273
+ throw new Error(
1274
+ `Topic "${queueName}" has a single handler but received consumer group "${consumerGroup}". Expected "default".`
1275
+ );
1276
+ }
1277
+ actualHandler = topicHandler;
1278
+ } else {
1279
+ const consumerGroupHandler = topicHandler[consumerGroup];
1280
+ if (!consumerGroupHandler) {
1281
+ const availableGroups = Object.keys(topicHandler).join(", ");
1282
+ throw new Error(
1283
+ `No handler found for consumer group "${consumerGroup}" in topic "${queueName}". Available groups: ${availableGroups}`
1284
+ );
1285
+ }
1286
+ actualHandler = consumerGroupHandler;
1011
1287
  }
1012
- const client = new QueueClient();
1288
+ const client = await QueueClient.fromVercelFunction();
1013
1289
  const topic = new Topic(client, queueName);
1014
1290
  const cg = topic.consumerGroup(consumerGroup);
1015
- await cg.consume(consumerGroupHandler, { messageId });
1291
+ await cg.receiveMessage(messageId, async (message) => {
1292
+ const metadata = {
1293
+ messageId: message.messageId,
1294
+ deliveryCount: message.deliveryCount,
1295
+ timestamp: message.timestamp
1296
+ };
1297
+ return await actualHandler(message.payload, metadata);
1298
+ });
1016
1299
  return Response.json({ status: "success" });
1017
1300
  } catch (error) {
1018
- console.error("Queue callback error:", error);
1019
- 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"))) {
1020
- return Response.json({ error: error.message }, { status: 400 });
1301
+ console.error("Callback error:", error);
1302
+ if (error instanceof InvalidCallbackError) {
1303
+ return Response.json(
1304
+ { error: "Invalid callback request" },
1305
+ { status: 400 }
1306
+ );
1021
1307
  }
1022
1308
  return Response.json(
1023
- { error: "Failed to process queue message" },
1309
+ { error: "Failed to process callback" },
1024
1310
  { status: 500 }
1025
1311
  );
1026
1312
  }
@@ -1029,20 +1315,26 @@ function handleCallback(handlers) {
1029
1315
  export {
1030
1316
  BadRequestError,
1031
1317
  BufferTransport,
1318
+ ConsumerGroup,
1319
+ FailedDependencyError,
1320
+ FifoOrderingViolationError,
1032
1321
  ForbiddenError,
1033
1322
  InternalServerError,
1323
+ InvalidCallbackError,
1034
1324
  InvalidLimitError,
1035
1325
  JsonTransport,
1036
1326
  MessageCorruptedError,
1037
1327
  MessageLockedError,
1038
1328
  MessageNotAvailableError,
1039
1329
  MessageNotFoundError,
1330
+ QueueClient,
1040
1331
  QueueEmptyError,
1041
1332
  StreamTransport,
1333
+ Topic,
1042
1334
  UnauthorizedError,
1335
+ createTopic,
1336
+ getVercelOidcToken,
1043
1337
  handleCallback,
1044
- parseCallback,
1045
- receive,
1046
- send
1338
+ parseCallbackRequest
1047
1339
  };
1048
1340
  //# sourceMappingURL=index.mjs.map