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