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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,110 +22,156 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  BadRequestError: () => BadRequestError,
24
24
  BufferTransport: () => BufferTransport,
25
+ ConsumerGroup: () => ConsumerGroup,
26
+ FailedDependencyError: () => FailedDependencyError,
27
+ FifoOrderingViolationError: () => FifoOrderingViolationError,
25
28
  ForbiddenError: () => ForbiddenError,
26
29
  InternalServerError: () => InternalServerError,
30
+ InvalidCallbackError: () => InvalidCallbackError,
27
31
  InvalidLimitError: () => InvalidLimitError,
28
32
  JsonTransport: () => JsonTransport,
29
33
  MessageCorruptedError: () => MessageCorruptedError,
30
34
  MessageLockedError: () => MessageLockedError,
31
35
  MessageNotAvailableError: () => MessageNotAvailableError,
32
36
  MessageNotFoundError: () => MessageNotFoundError,
37
+ QueueClient: () => QueueClient,
33
38
  QueueEmptyError: () => QueueEmptyError,
34
39
  StreamTransport: () => StreamTransport,
40
+ Topic: () => Topic,
35
41
  UnauthorizedError: () => UnauthorizedError,
36
- handleCallback: () => handleCallback,
37
- receive: () => receive,
38
- send: () => send
42
+ createTopic: () => createTopic,
43
+ getVercelOidcToken: () => getVercelOidcToken,
44
+ parseCallbackRequest: () => parseCallbackRequest
39
45
  });
40
46
  module.exports = __toCommonJS(index_exports);
41
47
 
42
- // src/transports.ts
43
- var JsonTransport = class {
44
- contentType = "application/json";
45
- serialize(value) {
46
- return Buffer.from(JSON.stringify(value), "utf8");
47
- }
48
- async deserialize(stream) {
49
- const reader = stream.getReader();
50
- let totalLength = 0;
51
- const chunks = [];
52
- try {
53
- while (true) {
54
- const { done, value } = await reader.read();
55
- if (done) break;
56
- chunks.push(value);
57
- totalLength += value.length;
58
- }
59
- } finally {
60
- reader.releaseLock();
61
- }
62
- const buffer = Buffer.concat(chunks, totalLength);
63
- return JSON.parse(buffer.toString("utf8"));
64
- }
65
- };
66
- var BufferTransport = class {
67
- contentType = "application/octet-stream";
68
- serialize(value) {
69
- return value;
70
- }
71
- async deserialize(stream) {
72
- const reader = stream.getReader();
73
- const chunks = [];
74
- try {
75
- while (true) {
76
- const { done, value } = await reader.read();
77
- if (done) break;
78
- chunks.push(value);
79
- }
80
- } finally {
81
- reader.releaseLock();
82
- }
83
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
84
- const buffer = new Uint8Array(totalLength);
85
- let offset = 0;
86
- for (const chunk of chunks) {
87
- buffer.set(chunk, offset);
88
- offset += chunk.length;
89
- }
90
- return Buffer.from(buffer);
91
- }
92
- };
93
- var StreamTransport = class {
94
- contentType = "application/octet-stream";
95
- serialize(value) {
96
- return value;
97
- }
98
- async deserialize(stream) {
99
- return stream;
100
- }
101
- async finalize(payload) {
102
- const reader = payload.getReader();
103
- try {
104
- while (true) {
105
- const { done } = await reader.read();
106
- if (done) break;
107
- }
108
- } finally {
109
- reader.releaseLock();
110
- }
111
- }
112
- };
113
-
114
- // src/client.ts
115
- var import_mixpart = require("mixpart");
116
-
117
48
  // src/oidc.ts
118
- function getVercelOidcToken() {
49
+ async function getVercelOidcToken() {
119
50
  const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
120
51
  const fromSymbol = globalThis;
121
52
  const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
122
53
  const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
123
54
  if (!token) {
124
- return null;
55
+ throw new Error(
56
+ `The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`
57
+ );
125
58
  }
126
59
  return token;
127
60
  }
128
61
 
62
+ // src/client.ts
63
+ var import_mixpart = require("mixpart");
64
+
65
+ // src/local.ts
66
+ var import_node_child_process = require("child_process");
67
+ function isLocalhostWithPort(url) {
68
+ try {
69
+ const parsedUrl = new URL(url);
70
+ const isLocalhost = parsedUrl.hostname === "localhost";
71
+ const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 0;
72
+ return { isLocalhost, port };
73
+ } catch {
74
+ return { isLocalhost: false };
75
+ }
76
+ }
77
+ function isSupportedPlatform() {
78
+ const platform = process.platform;
79
+ return platform === "darwin" || platform === "linux";
80
+ }
81
+ function processDevelopmentCallbacks(callbacks) {
82
+ const isDevelopment = process.env.NODE_ENV === "development";
83
+ if (!isDevelopment) {
84
+ return [];
85
+ }
86
+ if (!isSupportedPlatform()) {
87
+ const hasLocalhostCallbacks = Object.values(callbacks).some((config) => {
88
+ const { isLocalhost } = isLocalhostWithPort(config.url);
89
+ return isLocalhost;
90
+ });
91
+ if (hasLocalhostCallbacks) {
92
+ console.warn(
93
+ `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.`
94
+ );
95
+ }
96
+ return [];
97
+ }
98
+ const localhostCallbacks = [];
99
+ Object.entries(callbacks).forEach(([group, config]) => {
100
+ const { isLocalhost, port } = isLocalhostWithPort(config.url);
101
+ if (isLocalhost && port && port > 0) {
102
+ localhostCallbacks.push({ group, config, port });
103
+ } else {
104
+ console.warn(
105
+ `Queue Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
106
+ );
107
+ }
108
+ });
109
+ return localhostCallbacks;
110
+ }
111
+ function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
112
+ localhostCallbacks.forEach(({ group, config, port }) => {
113
+ const callbackHeaders = new Headers();
114
+ callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
115
+ callbackHeaders.set("Vqs-Queue-Name", queueName);
116
+ callbackHeaders.set("Vqs-Consumer-Group", group);
117
+ fireAndForgetWaitForHttpReady(
118
+ config.url,
119
+ port,
120
+ config.delay || 0,
121
+ 3,
122
+ // Default retry frequency
123
+ callbackHeaders
124
+ );
125
+ });
126
+ }
127
+ function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
128
+ if (!isSupportedPlatform()) {
129
+ console.warn(
130
+ `Queue: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
131
+ );
132
+ return;
133
+ }
134
+ let headerArgs = "";
135
+ if (headers) {
136
+ const headerArray = [];
137
+ headers.forEach((value, key) => {
138
+ headerArray.push(`-H '${key}: ${value}'`);
139
+ });
140
+ headerArgs = headerArray.join(" ");
141
+ }
142
+ const bashScript = `
143
+ # Wait for any initial boot time
144
+ sleep ${initialDelaySeconds}
145
+
146
+ missed=0
147
+ while true; do
148
+ # 1) Check if TCP port is listening
149
+ if nc -z localhost ${port} 2>/dev/null; then
150
+ missed=0
151
+ # 2) If port is open, try HTTP POST check
152
+ if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
153
+ # Success: port is up AND HTTP returned 2xx (following redirects)
154
+ exit 0
155
+ fi
156
+ else
157
+ # Port was closed\u2014increment miss counter
158
+ ((missed+=1))
159
+ # If closed twice in a row, give up immediately
160
+ if [ "$missed" -ge 2 ]; then
161
+ exit 1
162
+ fi
163
+ fi
164
+ # Wait before next cycle
165
+ sleep ${retryFrequencySeconds}
166
+ done
167
+ `;
168
+ const childProcess = (0, import_node_child_process.spawn)("bash", ["-c", bashScript], {
169
+ stdio: "ignore",
170
+ detached: true
171
+ });
172
+ childProcess.unref();
173
+ }
174
+
129
175
  // src/types.ts
130
176
  var MessageNotFoundError = class extends Error {
131
177
  constructor(messageId) {
@@ -141,6 +187,16 @@ var MessageNotAvailableError = class extends Error {
141
187
  this.name = "MessageNotAvailableError";
142
188
  }
143
189
  };
190
+ var FifoOrderingViolationError = class extends Error {
191
+ nextMessageId;
192
+ constructor(messageId, nextMessageId, reason) {
193
+ super(
194
+ `FIFO ordering violation for message ${messageId}: ${reason}. Process message ${nextMessageId} first.`
195
+ );
196
+ this.name = "FifoOrderingViolationError";
197
+ this.nextMessageId = nextMessageId;
198
+ }
199
+ };
144
200
  var MessageCorruptedError = class extends Error {
145
201
  constructor(messageId, reason) {
146
202
  super(`Message ${messageId} is corrupted: ${reason}`);
@@ -182,6 +238,16 @@ var BadRequestError = class extends Error {
182
238
  this.name = "BadRequestError";
183
239
  }
184
240
  };
241
+ var FailedDependencyError = class extends Error {
242
+ nextMessageId;
243
+ constructor(messageId, nextMessageId) {
244
+ super(
245
+ `Failed dependency: FIFO ordering violation for message ${messageId}. Must process message ${nextMessageId} first.`
246
+ );
247
+ this.name = "FailedDependencyError";
248
+ this.nextMessageId = nextMessageId;
249
+ }
250
+ };
185
251
  var InternalServerError = class extends Error {
186
252
  constructor(message = "Unexpected server error") {
187
253
  super(message);
@@ -194,6 +260,12 @@ var InvalidLimitError = class extends Error {
194
260
  this.name = "InvalidLimitError";
195
261
  }
196
262
  };
263
+ var InvalidCallbackError = class extends Error {
264
+ constructor(message) {
265
+ super(message);
266
+ this.name = "InvalidCallbackError";
267
+ }
268
+ };
197
269
 
198
270
  // src/client.ts
199
271
  async function consumeStream(stream) {
@@ -223,33 +295,40 @@ function parseQueueHeaders(headers) {
223
295
  return {
224
296
  messageId,
225
297
  deliveryCount,
226
- createdAt: new Date(timestamp),
298
+ timestamp,
227
299
  contentType,
228
300
  ticket
229
301
  };
230
302
  }
231
- var QueueClient = class {
303
+ var QueueClient = class _QueueClient {
232
304
  baseUrl;
233
- basePath;
234
305
  token;
235
306
  /**
236
307
  * Create a new Vercel Queue Service client
237
- * @param options Client configuration options (optional - will auto-detect Vercel Function environment)
308
+ * @param options Client configuration options
238
309
  */
239
- constructor(options = {}) {
240
- this.baseUrl = options.baseUrl || "https://api.vercel.com";
241
- this.basePath = options.basePath || "/v1/queues/messages";
242
- if (options.token) {
243
- this.token = options.token;
244
- } else {
245
- const token = getVercelOidcToken();
246
- if (!token) {
247
- throw new Error(
248
- "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'"
249
- );
250
- }
251
- this.token = token;
310
+ constructor(options) {
311
+ this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
312
+ this.token = options.token;
313
+ }
314
+ /**
315
+ * Create a QueueClient automatically configured for Vercel Functions
316
+ * This method automatically retrieves the OIDC token from the Vercel Function environment
317
+ * Always creates a fresh instance since OIDC tokens expire after 15 minutes
318
+ * @param baseUrl Optional base URL override
319
+ * @returns Promise resolving to a new QueueClient instance
320
+ */
321
+ static async fromVercelFunction(baseUrl) {
322
+ const token = await getVercelOidcToken();
323
+ if (!token) {
324
+ throw new Error(
325
+ "Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment."
326
+ );
252
327
  }
328
+ return new _QueueClient({
329
+ token,
330
+ baseUrl
331
+ });
253
332
  }
254
333
  /**
255
334
  * Send a message to a queue
@@ -262,23 +341,40 @@ var QueueClient = class {
262
341
  * @throws {InternalServerError} When server encounters an error
263
342
  */
264
343
  async sendMessage(options, transport) {
265
- const { queueName, payload, idempotencyKey, retentionSeconds } = options;
344
+ const { queueName, payload, idempotencyKey, retentionSeconds, callbacks } = options;
266
345
  const headers = new Headers({
267
346
  Authorization: `Bearer ${this.token}`,
268
347
  "Vqs-Queue-Name": queueName,
269
348
  "Content-Type": transport.contentType
270
349
  });
271
- if (process.env.VERCEL_DEPLOYMENT_ID) {
272
- headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
273
- }
274
350
  if (idempotencyKey) {
275
351
  headers.set("Vqs-Idempotency-Key", idempotencyKey);
276
352
  }
277
353
  if (retentionSeconds !== void 0) {
278
354
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
279
355
  }
356
+ let localhostCallbacks = [];
357
+ if (callbacks) {
358
+ const isDevelopment = process.env.NODE_ENV === "development";
359
+ if (isDevelopment) {
360
+ localhostCallbacks = processDevelopmentCallbacks(callbacks);
361
+ } else {
362
+ const endpoints = Object.entries(callbacks).map(
363
+ ([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
364
+ ).join(",");
365
+ headers.set("Vqs-Callback-Url", endpoints);
366
+ const delays = Object.entries(callbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
367
+ if (delays) {
368
+ headers.set("Vqs-Callback-Delay", delays);
369
+ }
370
+ const frequencies = Object.entries(callbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
371
+ if (frequencies) {
372
+ headers.set("Vqs-Callback-Frequency", frequencies);
373
+ }
374
+ }
375
+ }
280
376
  const body = transport.serialize(payload);
281
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
377
+ const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
282
378
  method: "POST",
283
379
  headers,
284
380
  body
@@ -307,6 +403,9 @@ var QueueClient = class {
307
403
  );
308
404
  }
309
405
  const responseData = await response.json();
406
+ if (localhostCallbacks.length > 0) {
407
+ fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
408
+ }
310
409
  return responseData;
311
410
  }
312
411
  /**
@@ -316,7 +415,7 @@ var QueueClient = class {
316
415
  * @returns AsyncGenerator that yields messages as they arrive
317
416
  * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
318
417
  * @throws {QueueEmptyError} When no messages are available (204)
319
- * @throws {MessageLockedError} When messages are temporarily locked (423)
418
+ * @throws {MessageLockedError} When FIFO queue has locked messages (423)
320
419
  * @throws {BadRequestError} When request parameters are invalid
321
420
  * @throws {UnauthorizedError} When authentication fails
322
421
  * @throws {ForbiddenError} When access is denied (environment mismatch)
@@ -342,7 +441,7 @@ var QueueClient = class {
342
441
  if (limit !== void 0) {
343
442
  headers.set("Vqs-Limit", limit.toString());
344
443
  }
345
- const response = await fetch(`${this.baseUrl}${this.basePath}`, {
444
+ const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
346
445
  method: "GET",
347
446
  headers
348
447
  });
@@ -367,7 +466,7 @@ var QueueClient = class {
367
466
  const parsed = parseInt(retryAfterHeader, 10);
368
467
  retryAfter = isNaN(parsed) ? void 0 : parsed;
369
468
  }
370
- throw new MessageLockedError("next message", retryAfter);
469
+ throw new MessageLockedError("next message in FIFO queue", retryAfter);
371
470
  }
372
471
  if (response.status >= 500) {
373
472
  throw new InternalServerError(
@@ -424,7 +523,7 @@ var QueueClient = class {
424
523
  headers.set("Vqs-Skip-Payload", "1");
425
524
  }
426
525
  const response = await fetch(
427
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
526
+ `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
428
527
  {
429
528
  method: "GET",
430
529
  headers
@@ -453,7 +552,37 @@ var QueueClient = class {
453
552
  }
454
553
  throw new MessageLockedError(messageId, retryAfter);
455
554
  }
555
+ if (response.status === 424) {
556
+ try {
557
+ const errorData = await response.json();
558
+ if (errorData.meta?.nextMessageId) {
559
+ throw new FailedDependencyError(
560
+ messageId,
561
+ errorData.meta.nextMessageId
562
+ );
563
+ }
564
+ } catch (parseError) {
565
+ if (parseError instanceof FailedDependencyError) {
566
+ throw parseError;
567
+ }
568
+ }
569
+ throw new MessageNotAvailableError(
570
+ messageId,
571
+ "FIFO ordering violation"
572
+ );
573
+ }
456
574
  if (response.status === 409) {
575
+ try {
576
+ const errorData = await response.json();
577
+ if (errorData.nextMessageId) {
578
+ throw new FifoOrderingViolationError(
579
+ messageId,
580
+ errorData.nextMessageId,
581
+ errorData.error
582
+ );
583
+ }
584
+ } catch (parseError) {
585
+ }
457
586
  throw new MessageNotAvailableError(messageId);
458
587
  }
459
588
  if (response.status >= 500) {
@@ -533,7 +662,7 @@ var QueueClient = class {
533
662
  async deleteMessage(options) {
534
663
  const { queueName, consumerGroup, messageId, ticket } = options;
535
664
  const response = await fetch(
536
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
665
+ `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
537
666
  {
538
667
  method: "DELETE",
539
668
  headers: new Headers({
@@ -594,7 +723,7 @@ var QueueClient = class {
594
723
  visibilityTimeoutSeconds
595
724
  } = options;
596
725
  const response = await fetch(
597
- `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
726
+ `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
598
727
  {
599
728
  method: "PATCH",
600
729
  headers: new Headers({
@@ -640,6 +769,82 @@ var QueueClient = class {
640
769
  }
641
770
  };
642
771
 
772
+ // src/transports.ts
773
+ var JsonTransport = class {
774
+ contentType = "application/json";
775
+ serialize(value) {
776
+ return Buffer.from(JSON.stringify(value), "utf8");
777
+ }
778
+ async deserialize(stream) {
779
+ const reader = stream.getReader();
780
+ const chunks = [];
781
+ try {
782
+ while (true) {
783
+ const { done, value } = await reader.read();
784
+ if (done) break;
785
+ chunks.push(value);
786
+ }
787
+ } finally {
788
+ reader.releaseLock();
789
+ }
790
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
791
+ const buffer = new Uint8Array(totalLength);
792
+ let offset = 0;
793
+ for (const chunk of chunks) {
794
+ buffer.set(chunk, offset);
795
+ offset += chunk.length;
796
+ }
797
+ return JSON.parse(Buffer.from(buffer).toString("utf8"));
798
+ }
799
+ };
800
+ var BufferTransport = class {
801
+ contentType = "application/octet-stream";
802
+ serialize(value) {
803
+ return value;
804
+ }
805
+ async deserialize(stream) {
806
+ const reader = stream.getReader();
807
+ const chunks = [];
808
+ try {
809
+ while (true) {
810
+ const { done, value } = await reader.read();
811
+ if (done) break;
812
+ chunks.push(value);
813
+ }
814
+ } finally {
815
+ reader.releaseLock();
816
+ }
817
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
818
+ const buffer = new Uint8Array(totalLength);
819
+ let offset = 0;
820
+ for (const chunk of chunks) {
821
+ buffer.set(chunk, offset);
822
+ offset += chunk.length;
823
+ }
824
+ return Buffer.from(buffer);
825
+ }
826
+ };
827
+ var StreamTransport = class {
828
+ contentType = "application/octet-stream";
829
+ serialize(value) {
830
+ return value;
831
+ }
832
+ async deserialize(stream) {
833
+ return stream;
834
+ }
835
+ async finalize(payload) {
836
+ const reader = payload.getReader();
837
+ try {
838
+ while (true) {
839
+ const { done } = await reader.read();
840
+ if (done) break;
841
+ }
842
+ } finally {
843
+ reader.releaseLock();
844
+ }
845
+ }
846
+ };
847
+
643
848
  // src/consumer-group.ts
644
849
  var ConsumerGroup = class {
645
850
  client;
@@ -739,11 +944,7 @@ var ConsumerGroup = class {
739
944
  message.ticket
740
945
  );
741
946
  try {
742
- const result = await handler(message.payload, {
743
- messageId: message.messageId,
744
- deliveryCount: message.deliveryCount,
745
- createdAt: message.createdAt
746
- });
947
+ const result = await handler(message);
747
948
  await stopExtension();
748
949
  if (result && "timeoutSeconds" in result) {
749
950
  await this.client.changeVisibility({
@@ -773,58 +974,219 @@ var ConsumerGroup = class {
773
974
  throw error;
774
975
  }
775
976
  }
776
- async consume(handler, options) {
777
- if (options?.messageId) {
778
- if (options.skipPayload) {
779
- const response = await this.client.receiveMessageById(
977
+ /**
978
+ * Start continuous processing of messages from the topic
979
+ * @param signal AbortSignal to control when to stop processing
980
+ * @param handler Function to process each message
981
+ * @param options Processing options
982
+ * @returns Promise that resolves when processing stops (due to signal or error)
983
+ */
984
+ async subscribe(signal, handler, options = {}) {
985
+ const pollingInterval = options.pollingInterval || 1e3;
986
+ while (!signal.aborted) {
987
+ try {
988
+ for await (const message of this.client.receiveMessages(
780
989
  {
781
990
  queueName: this.topicName,
782
991
  consumerGroup: this.consumerGroupName,
783
- messageId: options.messageId,
784
992
  visibilityTimeoutSeconds: this.visibilityTimeout,
785
- skipPayload: true
993
+ limit: 1
994
+ // Always process one message at a time
786
995
  },
787
996
  this.transport
788
- );
789
- await this.processMessage(
790
- response.message,
791
- handler
792
- );
793
- } else {
794
- const response = await this.client.receiveMessageById(
795
- {
796
- queueName: this.topicName,
797
- consumerGroup: this.consumerGroupName,
798
- messageId: options.messageId,
799
- visibilityTimeoutSeconds: this.visibilityTimeout
800
- },
801
- this.transport
802
- );
803
- await this.processMessage(
804
- response.message,
805
- handler
806
- );
807
- }
808
- } else {
809
- let messageFound = false;
810
- for await (const message of this.client.receiveMessages(
811
- {
812
- queueName: this.topicName,
813
- consumerGroup: this.consumerGroupName,
814
- visibilityTimeoutSeconds: this.visibilityTimeout,
815
- limit: 1
816
- },
817
- this.transport
818
- )) {
819
- messageFound = true;
820
- await this.processMessage(message, handler);
821
- break;
822
- }
823
- if (!messageFound) {
824
- throw new Error("No messages available");
997
+ )) {
998
+ if (signal.aborted) {
999
+ break;
1000
+ }
1001
+ try {
1002
+ await this.processMessage(message, handler);
1003
+ } catch (error) {
1004
+ console.error("Error processing message:", error);
1005
+ }
1006
+ }
1007
+ if (!signal.aborted) {
1008
+ await new Promise((resolve) => {
1009
+ const timeoutId = setTimeout(resolve, pollingInterval);
1010
+ signal.addEventListener(
1011
+ "abort",
1012
+ () => {
1013
+ clearTimeout(timeoutId);
1014
+ resolve();
1015
+ },
1016
+ { once: true }
1017
+ );
1018
+ });
1019
+ }
1020
+ } catch (error) {
1021
+ if (error instanceof QueueEmptyError) {
1022
+ if (!signal.aborted) {
1023
+ await new Promise((resolve) => {
1024
+ const timeoutId = setTimeout(resolve, pollingInterval);
1025
+ signal.addEventListener(
1026
+ "abort",
1027
+ () => {
1028
+ clearTimeout(timeoutId);
1029
+ resolve();
1030
+ },
1031
+ { once: true }
1032
+ );
1033
+ });
1034
+ }
1035
+ continue;
1036
+ }
1037
+ if (error instanceof MessageLockedError) {
1038
+ const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
1039
+ if (!signal.aborted) {
1040
+ await new Promise((resolve) => {
1041
+ const timeoutId = setTimeout(resolve, waitTime);
1042
+ signal.addEventListener(
1043
+ "abort",
1044
+ () => {
1045
+ clearTimeout(timeoutId);
1046
+ resolve();
1047
+ },
1048
+ { once: true }
1049
+ );
1050
+ });
1051
+ }
1052
+ continue;
1053
+ }
1054
+ console.error("Error polling topic:", error);
1055
+ throw error;
825
1056
  }
826
1057
  }
827
1058
  }
1059
+ /**
1060
+ * Receive and process a specific message by its ID with full payload
1061
+ * @param messageId The ID of the message to receive and process
1062
+ * @param handler Function to process the message with full payload
1063
+ * @returns Promise that resolves when the message is processed or rejects with specific errors
1064
+ * @throws {MessageNotFoundError} When the message doesn't exist (404)
1065
+ * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1066
+ * @throws {MessageLockedError} When the message is temporarily locked (423)
1067
+ * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1068
+ * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1069
+ * @throws {MessageCorruptedError} When the message data is corrupted
1070
+ * @throws {BadRequestError} When request parameters are invalid
1071
+ * @throws {UnauthorizedError} When authentication fails
1072
+ * @throws {ForbiddenError} When access is denied
1073
+ * @throws {InternalServerError} When server encounters an error
1074
+ */
1075
+ async receiveMessage(messageId, handler) {
1076
+ const response = await this.client.receiveMessageById(
1077
+ {
1078
+ queueName: this.topicName,
1079
+ consumerGroup: this.consumerGroupName,
1080
+ messageId,
1081
+ visibilityTimeoutSeconds: this.visibilityTimeout
1082
+ },
1083
+ this.transport
1084
+ );
1085
+ await this.processMessage(response.message, handler);
1086
+ }
1087
+ /**
1088
+ * Receive and process the next available message from the queue
1089
+ * @param handler Function to process the message
1090
+ * @returns Promise that resolves when the message is processed or rejects with specific errors
1091
+ * @throws {QueueEmptyError} When no messages are available in the queue (204)
1092
+ * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
1093
+ * @throws {BadRequestError} When request parameters are invalid
1094
+ * @throws {UnauthorizedError} When authentication fails
1095
+ * @throws {ForbiddenError} When access is denied
1096
+ * @throws {InternalServerError} When server encounters an error
1097
+ */
1098
+ async receiveNextMessage(handler) {
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");
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Receive and process multiple next available messages from the queue
1119
+ * @param limit Number of messages to process (1-10)
1120
+ * @param handler Function to process each message
1121
+ * @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
1122
+ * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
1123
+ * @throws {QueueEmptyError} When no messages are available in the queue (204)
1124
+ * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
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 receiveNextMessages(limit, handler) {
1131
+ if (limit < 1 || limit > 10) {
1132
+ throw new InvalidLimitError(limit);
1133
+ }
1134
+ const processingPromises = [];
1135
+ let messageCount = 0;
1136
+ for await (const message of this.client.receiveMessages(
1137
+ {
1138
+ queueName: this.topicName,
1139
+ consumerGroup: this.consumerGroupName,
1140
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1141
+ limit
1142
+ },
1143
+ this.transport
1144
+ )) {
1145
+ messageCount++;
1146
+ const wrappedPromise = this.processMessage(message, handler).then(
1147
+ (value) => ({
1148
+ status: "fulfilled",
1149
+ value
1150
+ }),
1151
+ (reason) => ({ status: "rejected", reason })
1152
+ );
1153
+ processingPromises.push(wrappedPromise);
1154
+ }
1155
+ if (messageCount === 0) {
1156
+ throw new Error("No messages available");
1157
+ }
1158
+ const results = await Promise.all(processingPromises);
1159
+ return results;
1160
+ }
1161
+ /**
1162
+ * Handle a specific message by its ID without downloading the payload (metadata only)
1163
+ * @param messageId The ID of the message to handle
1164
+ * @param handler Function to process the message metadata (payload will be void)
1165
+ * @returns Promise that resolves when the message is handled or rejects with specific errors
1166
+ * @throws {MessageNotFoundError} When the message doesn't exist (404)
1167
+ * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1168
+ * @throws {MessageLockedError} When the message is temporarily locked (423)
1169
+ * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1170
+ * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1171
+ * @throws {MessageCorruptedError} When the message data is corrupted
1172
+ * @throws {BadRequestError} When request parameters are invalid
1173
+ * @throws {UnauthorizedError} When authentication fails
1174
+ * @throws {ForbiddenError} When access is denied
1175
+ * @throws {InternalServerError} When server encounters an error
1176
+ */
1177
+ async handleMessage(messageId, handler) {
1178
+ const response = await this.client.receiveMessageById(
1179
+ {
1180
+ queueName: this.topicName,
1181
+ consumerGroup: this.consumerGroupName,
1182
+ messageId,
1183
+ visibilityTimeoutSeconds: this.visibilityTimeout,
1184
+ skipPayload: true
1185
+ },
1186
+ this.transport
1187
+ );
1188
+ await this.processMessage(response.message, handler);
1189
+ }
828
1190
  /**
829
1191
  * Get the consumer group name
830
1192
  */
@@ -871,7 +1233,8 @@ var Topic = class {
871
1233
  queueName: this.topicName,
872
1234
  payload,
873
1235
  idempotencyKey: options?.idempotencyKey,
874
- retentionSeconds: options?.retentionSeconds
1236
+ retentionSeconds: options?.retentionSeconds,
1237
+ callbacks: options?.callbacks
875
1238
  },
876
1239
  this.transport
877
1240
  );
@@ -910,139 +1273,54 @@ var Topic = class {
910
1273
  };
911
1274
 
912
1275
  // src/factory.ts
913
- async function send(topicName, payload, options) {
914
- const transport = options?.transport || new JsonTransport();
915
- const client = new QueueClient();
916
- const result = await client.sendMessage(
917
- {
918
- queueName: topicName,
919
- payload,
920
- idempotencyKey: options?.idempotencyKey,
921
- retentionSeconds: options?.retentionSeconds
922
- },
923
- transport
924
- );
925
- return { messageId: result.messageId };
926
- }
927
- async function receive(topicName, consumerGroup, handler, options) {
928
- const transport = options?.transport || new JsonTransport();
929
- const client = new QueueClient();
930
- const topic = new Topic(client, topicName, transport);
931
- const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
932
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
933
- if (messageId) {
934
- if (skipPayload) {
935
- return consumer.consume(handler, {
936
- messageId,
937
- skipPayload: true
938
- });
939
- } else {
940
- return consumer.consume(handler, { messageId });
941
- }
942
- } else {
943
- return consumer.consume(handler);
944
- }
1276
+ function createTopic(client, topicName, transport) {
1277
+ return new Topic(client, topicName, transport);
945
1278
  }
946
1279
 
947
1280
  // src/callback.ts
948
- async function parseCallbackRequest(request) {
949
- const contentType = request.headers.get("content-type");
950
- if (!contentType || !contentType.includes("application/cloudevents+json")) {
951
- throw new Error(
952
- "Invalid content type: expected 'application/cloudevents+json'"
953
- );
954
- }
955
- let cloudEvent;
956
- try {
957
- cloudEvent = await request.json();
958
- } catch (error) {
959
- throw new Error("Failed to parse CloudEvent from request body");
960
- }
961
- if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
962
- throw new Error("Invalid CloudEvent: missing required fields");
963
- }
964
- if (cloudEvent.type !== "com.vercel.queue.v1beta") {
965
- throw new Error(
966
- `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
967
- );
968
- }
969
- const missingFields = [];
970
- if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
971
- if (!("consumerGroup" in cloudEvent.data))
972
- missingFields.push("consumerGroup");
973
- if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
974
- if (missingFields.length > 0) {
975
- throw new Error(
976
- `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
1281
+ function parseCallbackRequest(request) {
1282
+ const headers = request.headers;
1283
+ const messageId = headers.get("Vqs-Message-Id");
1284
+ const queueName = headers.get("Vqs-Queue-Name");
1285
+ const consumerGroup = headers.get("Vqs-Consumer-Group");
1286
+ const missingHeaders = [];
1287
+ if (!messageId) missingHeaders.push("Vqs-Message-Id");
1288
+ if (!queueName) missingHeaders.push("Vqs-Queue-Name");
1289
+ if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
1290
+ if (missingHeaders.length > 0) {
1291
+ throw new InvalidCallbackError(
1292
+ `Missing required queue callback headers: ${missingHeaders.join(", ")}`
977
1293
  );
978
1294
  }
979
- const { messageId, queueName, consumerGroup } = cloudEvent.data;
980
1295
  return {
1296
+ messageId,
981
1297
  queueName,
982
- consumerGroup,
983
- messageId
984
- };
985
- }
986
- function handleCallback(handlers) {
987
- return async (request) => {
988
- try {
989
- const { queueName, consumerGroup, messageId } = await parseCallbackRequest(request);
990
- const topicHandler = handlers[queueName];
991
- 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
- );
1000
- }
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
- );
1011
- }
1012
- const client = new QueueClient();
1013
- const topic = new Topic(client, queueName);
1014
- const cg = topic.consumerGroup(consumerGroup);
1015
- await cg.consume(consumerGroupHandler, { messageId });
1016
- return Response.json({ status: "success" });
1017
- } 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 });
1021
- }
1022
- return Response.json(
1023
- { error: "Failed to process queue message" },
1024
- { status: 500 }
1025
- );
1026
- }
1298
+ consumerGroup
1027
1299
  };
1028
1300
  }
1029
1301
  // Annotate the CommonJS export names for ESM import in node:
1030
1302
  0 && (module.exports = {
1031
1303
  BadRequestError,
1032
1304
  BufferTransport,
1305
+ ConsumerGroup,
1306
+ FailedDependencyError,
1307
+ FifoOrderingViolationError,
1033
1308
  ForbiddenError,
1034
1309
  InternalServerError,
1310
+ InvalidCallbackError,
1035
1311
  InvalidLimitError,
1036
1312
  JsonTransport,
1037
1313
  MessageCorruptedError,
1038
1314
  MessageLockedError,
1039
1315
  MessageNotAvailableError,
1040
1316
  MessageNotFoundError,
1317
+ QueueClient,
1041
1318
  QueueEmptyError,
1042
1319
  StreamTransport,
1320
+ Topic,
1043
1321
  UnauthorizedError,
1044
- handleCallback,
1045
- receive,
1046
- send
1322
+ createTopic,
1323
+ getVercelOidcToken,
1324
+ parseCallbackRequest
1047
1325
  });
1048
1326
  //# sourceMappingURL=index.js.map