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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,128 +1,88 @@
1
- // src/oidc.ts
2
- async function getVercelOidcToken() {
3
- const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
4
- const fromSymbol = globalThis;
5
- const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
6
- const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
7
- if (!token) {
8
- throw new Error(
9
- `The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`
10
- );
1
+ // src/transports.ts
2
+ var JsonTransport = class {
3
+ contentType = "application/json";
4
+ serialize(value) {
5
+ return Buffer.from(JSON.stringify(value), "utf8");
11
6
  }
12
- return token;
13
- }
14
-
15
- // src/client.ts
16
- import { parseMultipartStream } from "mixpart";
17
-
18
- // src/local.ts
19
- import { spawn } from "child_process";
20
- function isLocalhostWithPort(url) {
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 };
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"));
28
23
  }
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 [];
24
+ };
25
+ var BufferTransport = class {
26
+ contentType = "application/octet-stream";
27
+ serialize(value) {
28
+ return value;
38
29
  }
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
- `VQS 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
- );
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();
48
41
  }
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
- `VQS Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
59
- );
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;
60
48
  }
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
- `VQS: 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;
49
+ return Buffer.from(buffer);
86
50
  }
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(" ");
51
+ };
52
+ var StreamTransport = class {
53
+ contentType = "application/octet-stream";
54
+ serialize(value) {
55
+ return value;
94
56
  }
95
- const bashScript = `
96
- # Wait for any initial boot time
97
- sleep ${initialDelaySeconds}
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";
98
75
 
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();
76
+ // src/oidc.ts
77
+ function getVercelOidcToken() {
78
+ const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
79
+ const fromSymbol = globalThis;
80
+ const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
81
+ const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
82
+ if (!token) {
83
+ return null;
84
+ }
85
+ return token;
126
86
  }
127
87
 
128
88
  // src/types.ts
@@ -140,16 +100,6 @@ var MessageNotAvailableError = class extends Error {
140
100
  this.name = "MessageNotAvailableError";
141
101
  }
142
102
  };
143
- var FifoOrderingViolationError = class extends Error {
144
- nextMessageId;
145
- constructor(messageId, nextMessageId, reason) {
146
- super(
147
- `FIFO ordering violation for message ${messageId}: ${reason}. Process message ${nextMessageId} first.`
148
- );
149
- this.name = "FifoOrderingViolationError";
150
- this.nextMessageId = nextMessageId;
151
- }
152
- };
153
103
  var MessageCorruptedError = class extends Error {
154
104
  constructor(messageId, reason) {
155
105
  super(`Message ${messageId} is corrupted: ${reason}`);
@@ -191,16 +141,6 @@ var BadRequestError = class extends Error {
191
141
  this.name = "BadRequestError";
192
142
  }
193
143
  };
194
- var FailedDependencyError = class extends Error {
195
- nextMessageId;
196
- constructor(messageId, nextMessageId) {
197
- super(
198
- `Failed dependency: FIFO ordering violation for message ${messageId}. Must process message ${nextMessageId} first.`
199
- );
200
- this.name = "FailedDependencyError";
201
- this.nextMessageId = nextMessageId;
202
- }
203
- };
204
144
  var InternalServerError = class extends Error {
205
145
  constructor(message = "Unexpected server error") {
206
146
  super(message);
@@ -213,12 +153,6 @@ var InvalidLimitError = class extends Error {
213
153
  this.name = "InvalidLimitError";
214
154
  }
215
155
  };
216
- var InvalidCallbackError = class extends Error {
217
- constructor(message) {
218
- super(message);
219
- this.name = "InvalidCallbackError";
220
- }
221
- };
222
156
 
223
157
  // src/client.ts
224
158
  async function consumeStream(stream) {
@@ -232,7 +166,7 @@ async function consumeStream(stream) {
232
166
  reader.releaseLock();
233
167
  }
234
168
  }
235
- function parseVQSHeaders(headers) {
169
+ function parseQueueHeaders(headers) {
236
170
  const messageId = headers.get("Vqs-Message-Id");
237
171
  const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
238
172
  const timestamp = headers.get("Vqs-Timestamp");
@@ -248,40 +182,33 @@ function parseVQSHeaders(headers) {
248
182
  return {
249
183
  messageId,
250
184
  deliveryCount,
251
- timestamp,
185
+ createdAt: new Date(timestamp),
252
186
  contentType,
253
187
  ticket
254
188
  };
255
189
  }
256
- var VQSClient = class _VQSClient {
190
+ var QueueClient = class {
257
191
  baseUrl;
192
+ basePath;
258
193
  token;
259
194
  /**
260
195
  * Create a new Vercel Queue Service client
261
- * @param options Client configuration options
196
+ * @param options Client configuration options (optional - will auto-detect Vercel Function environment)
262
197
  */
263
- constructor(options) {
264
- this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
265
- this.token = options.token;
266
- }
267
- /**
268
- * Create a VQSClient 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 VQSClient 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
- );
198
+ constructor(options = {}) {
199
+ this.baseUrl = options.baseUrl || "https://api.vercel.com";
200
+ this.basePath = options.basePath || "/v1/queues/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;
280
211
  }
281
- return new _VQSClient({
282
- token,
283
- baseUrl
284
- });
285
212
  }
286
213
  /**
287
214
  * Send a message to a queue
@@ -294,40 +221,23 @@ var VQSClient = class _VQSClient {
294
221
  * @throws {InternalServerError} When server encounters an error
295
222
  */
296
223
  async sendMessage(options, transport) {
297
- const { queueName, payload, idempotencyKey, retentionSeconds, callbacks } = options;
224
+ const { queueName, payload, idempotencyKey, retentionSeconds } = options;
298
225
  const headers = new Headers({
299
226
  Authorization: `Bearer ${this.token}`,
300
227
  "Vqs-Queue-Name": queueName,
301
228
  "Content-Type": transport.contentType
302
229
  });
230
+ if (process.env.VERCEL_DEPLOYMENT_ID) {
231
+ headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
232
+ }
303
233
  if (idempotencyKey) {
304
234
  headers.set("Vqs-Idempotency-Key", idempotencyKey);
305
235
  }
306
236
  if (retentionSeconds !== void 0) {
307
237
  headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
308
238
  }
309
- let localhostCallbacks = [];
310
- if (callbacks) {
311
- const isDevelopment = process.env.NODE_ENV === "development";
312
- if (isDevelopment) {
313
- localhostCallbacks = processDevelopmentCallbacks(callbacks);
314
- } else {
315
- const endpoints = Object.entries(callbacks).map(
316
- ([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
317
- ).join(",");
318
- headers.set("Vqs-Callback-Url", endpoints);
319
- const delays = Object.entries(callbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
320
- if (delays) {
321
- headers.set("Vqs-Callback-Delay", delays);
322
- }
323
- const frequencies = Object.entries(callbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
324
- if (frequencies) {
325
- headers.set("Vqs-Callback-Frequency", frequencies);
326
- }
327
- }
328
- }
329
239
  const body = transport.serialize(payload);
330
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
240
+ const response = await fetch(`${this.baseUrl}${this.basePath}`, {
331
241
  method: "POST",
332
242
  headers,
333
243
  body
@@ -356,9 +266,6 @@ var VQSClient = class _VQSClient {
356
266
  );
357
267
  }
358
268
  const responseData = await response.json();
359
- if (localhostCallbacks.length > 0) {
360
- fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
361
- }
362
269
  return responseData;
363
270
  }
364
271
  /**
@@ -368,7 +275,7 @@ var VQSClient = class _VQSClient {
368
275
  * @returns AsyncGenerator that yields messages as they arrive
369
276
  * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
370
277
  * @throws {QueueEmptyError} When no messages are available (204)
371
- * @throws {MessageLockedError} When FIFO queue has locked messages (423)
278
+ * @throws {MessageLockedError} When messages are temporarily locked (423)
372
279
  * @throws {BadRequestError} When request parameters are invalid
373
280
  * @throws {UnauthorizedError} When authentication fails
374
281
  * @throws {ForbiddenError} When access is denied (environment mismatch)
@@ -394,7 +301,7 @@ var VQSClient = class _VQSClient {
394
301
  if (limit !== void 0) {
395
302
  headers.set("Vqs-Limit", limit.toString());
396
303
  }
397
- const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
304
+ const response = await fetch(`${this.baseUrl}${this.basePath}`, {
398
305
  method: "GET",
399
306
  headers
400
307
  });
@@ -419,7 +326,7 @@ var VQSClient = class _VQSClient {
419
326
  const parsed = parseInt(retryAfterHeader, 10);
420
327
  retryAfter = isNaN(parsed) ? void 0 : parsed;
421
328
  }
422
- throw new MessageLockedError("next message in FIFO queue", retryAfter);
329
+ throw new MessageLockedError("next message", retryAfter);
423
330
  }
424
331
  if (response.status >= 500) {
425
332
  throw new InternalServerError(
@@ -432,9 +339,9 @@ var VQSClient = class _VQSClient {
432
339
  }
433
340
  for await (const multipartMessage of parseMultipartStream(response)) {
434
341
  try {
435
- const parsedHeaders = parseVQSHeaders(multipartMessage.headers);
342
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
436
343
  if (!parsedHeaders) {
437
- console.warn("Missing required VQS headers in multipart part");
344
+ console.warn("Missing required queue headers in multipart part");
438
345
  await consumeStream(multipartMessage.payload);
439
346
  continue;
440
347
  }
@@ -476,7 +383,7 @@ var VQSClient = class _VQSClient {
476
383
  headers.set("Vqs-Skip-Payload", "1");
477
384
  }
478
385
  const response = await fetch(
479
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
386
+ `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
480
387
  {
481
388
  method: "GET",
482
389
  headers
@@ -505,37 +412,7 @@ var VQSClient = class _VQSClient {
505
412
  }
506
413
  throw new MessageLockedError(messageId, retryAfter);
507
414
  }
508
- if (response.status === 424) {
509
- try {
510
- const errorData = await response.json();
511
- if (errorData.meta?.nextMessageId) {
512
- throw new FailedDependencyError(
513
- messageId,
514
- errorData.meta.nextMessageId
515
- );
516
- }
517
- } catch (parseError) {
518
- if (parseError instanceof FailedDependencyError) {
519
- throw parseError;
520
- }
521
- }
522
- throw new MessageNotAvailableError(
523
- messageId,
524
- "FIFO ordering violation"
525
- );
526
- }
527
415
  if (response.status === 409) {
528
- try {
529
- const errorData = await response.json();
530
- if (errorData.nextMessageId) {
531
- throw new FifoOrderingViolationError(
532
- messageId,
533
- errorData.nextMessageId,
534
- errorData.error
535
- );
536
- }
537
- } catch (parseError) {
538
- }
539
416
  throw new MessageNotAvailableError(messageId);
540
417
  }
541
418
  if (response.status >= 500) {
@@ -548,11 +425,11 @@ var VQSClient = class _VQSClient {
548
425
  );
549
426
  }
550
427
  if (skipPayload && response.status === 204) {
551
- const parsedHeaders = parseVQSHeaders(response.headers);
428
+ const parsedHeaders = parseQueueHeaders(response.headers);
552
429
  if (!parsedHeaders) {
553
430
  throw new MessageCorruptedError(
554
431
  messageId,
555
- "Missing required VQS headers in 204 response"
432
+ "Missing required queue headers in 204 response"
556
433
  );
557
434
  }
558
435
  const message = {
@@ -567,9 +444,9 @@ var VQSClient = class _VQSClient {
567
444
  try {
568
445
  for await (const multipartMessage of parseMultipartStream(response)) {
569
446
  try {
570
- const parsedHeaders = parseVQSHeaders(multipartMessage.headers);
447
+ const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
571
448
  if (!parsedHeaders) {
572
- console.warn("Missing required VQS headers in multipart part");
449
+ console.warn("Missing required queue headers in multipart part");
573
450
  await consumeStream(multipartMessage.payload);
574
451
  continue;
575
452
  }
@@ -615,7 +492,7 @@ var VQSClient = class _VQSClient {
615
492
  async deleteMessage(options) {
616
493
  const { queueName, consumerGroup, messageId, ticket } = options;
617
494
  const response = await fetch(
618
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
495
+ `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
619
496
  {
620
497
  method: "DELETE",
621
498
  headers: new Headers({
@@ -676,7 +553,7 @@ var VQSClient = class _VQSClient {
676
553
  visibilityTimeoutSeconds
677
554
  } = options;
678
555
  const response = await fetch(
679
- `${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
556
+ `${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
680
557
  {
681
558
  method: "PATCH",
682
559
  headers: new Headers({
@@ -722,82 +599,6 @@ var VQSClient = class _VQSClient {
722
599
  }
723
600
  };
724
601
 
725
- // src/transports.ts
726
- var JsonTransport = class {
727
- contentType = "application/json";
728
- serialize(value) {
729
- return Buffer.from(JSON.stringify(value), "utf8");
730
- }
731
- async deserialize(stream) {
732
- const reader = stream.getReader();
733
- const chunks = [];
734
- try {
735
- while (true) {
736
- const { done, value } = await reader.read();
737
- if (done) break;
738
- chunks.push(value);
739
- }
740
- } finally {
741
- reader.releaseLock();
742
- }
743
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
744
- const buffer = new Uint8Array(totalLength);
745
- let offset = 0;
746
- for (const chunk of chunks) {
747
- buffer.set(chunk, offset);
748
- offset += chunk.length;
749
- }
750
- return JSON.parse(Buffer.from(buffer).toString("utf8"));
751
- }
752
- };
753
- var BufferTransport = class {
754
- contentType = "application/octet-stream";
755
- serialize(value) {
756
- return value;
757
- }
758
- async deserialize(stream) {
759
- const reader = stream.getReader();
760
- const chunks = [];
761
- try {
762
- while (true) {
763
- const { done, value } = await reader.read();
764
- if (done) break;
765
- chunks.push(value);
766
- }
767
- } finally {
768
- reader.releaseLock();
769
- }
770
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
771
- const buffer = new Uint8Array(totalLength);
772
- let offset = 0;
773
- for (const chunk of chunks) {
774
- buffer.set(chunk, offset);
775
- offset += chunk.length;
776
- }
777
- return Buffer.from(buffer);
778
- }
779
- };
780
- var StreamTransport = class {
781
- contentType = "application/octet-stream";
782
- serialize(value) {
783
- return value;
784
- }
785
- async deserialize(stream) {
786
- return stream;
787
- }
788
- async finalize(payload) {
789
- const reader = payload.getReader();
790
- try {
791
- while (true) {
792
- const { done } = await reader.read();
793
- if (done) break;
794
- }
795
- } finally {
796
- reader.releaseLock();
797
- }
798
- }
799
- };
800
-
801
602
  // src/consumer-group.ts
802
603
  var ConsumerGroup = class {
803
604
  client;
@@ -808,7 +609,7 @@ var ConsumerGroup = class {
808
609
  transport;
809
610
  /**
810
611
  * Create a new ConsumerGroup instance
811
- * @param client VQSClient instance to use for API calls
612
+ * @param client QueueClient instance to use for API calls
812
613
  * @param topicName Name of the topic to consume from
813
614
  * @param consumerGroupName Name of the consumer group
814
615
  * @param options Optional configuration
@@ -897,7 +698,11 @@ var ConsumerGroup = class {
897
698
  message.ticket
898
699
  );
899
700
  try {
900
- const result = await handler(message);
701
+ const result = await handler(message.payload, {
702
+ messageId: message.messageId,
703
+ deliveryCount: message.deliveryCount,
704
+ createdAt: message.createdAt
705
+ });
901
706
  await stopExtension();
902
707
  if (result && "timeoutSeconds" in result) {
903
708
  await this.client.changeVisibility({
@@ -927,219 +732,58 @@ var ConsumerGroup = class {
927
732
  throw error;
928
733
  }
929
734
  }
930
- /**
931
- * Start continuous processing of messages from the topic
932
- * @param signal AbortSignal to control when to stop processing
933
- * @param handler Function to process each message
934
- * @param options Processing options
935
- * @returns Promise that resolves when processing stops (due to signal or error)
936
- */
937
- async subscribe(signal, handler, options = {}) {
938
- const pollingInterval = options.pollingInterval || 1e3;
939
- while (!signal.aborted) {
940
- try {
941
- for await (const message of this.client.receiveMessages(
735
+ async consume(handler, options) {
736
+ if (options?.messageId) {
737
+ if (options.skipPayload) {
738
+ const response = await this.client.receiveMessageById(
942
739
  {
943
740
  queueName: this.topicName,
944
741
  consumerGroup: this.consumerGroupName,
742
+ messageId: options.messageId,
945
743
  visibilityTimeoutSeconds: this.visibilityTimeout,
946
- limit: 1
947
- // Always process one message at a time
744
+ skipPayload: true
948
745
  },
949
746
  this.transport
950
- )) {
951
- if (signal.aborted) {
952
- break;
953
- }
954
- try {
955
- await this.processMessage(message, handler);
956
- } catch (error) {
957
- console.error("Error processing message:", error);
958
- }
959
- }
960
- if (!signal.aborted) {
961
- await new Promise((resolve) => {
962
- const timeoutId = setTimeout(resolve, pollingInterval);
963
- signal.addEventListener(
964
- "abort",
965
- () => {
966
- clearTimeout(timeoutId);
967
- resolve();
968
- },
969
- { once: true }
970
- );
971
- });
972
- }
973
- } catch (error) {
974
- if (error instanceof QueueEmptyError) {
975
- if (!signal.aborted) {
976
- await new Promise((resolve) => {
977
- const timeoutId = setTimeout(resolve, pollingInterval);
978
- signal.addEventListener(
979
- "abort",
980
- () => {
981
- clearTimeout(timeoutId);
982
- resolve();
983
- },
984
- { once: true }
985
- );
986
- });
987
- }
988
- continue;
989
- }
990
- if (error instanceof MessageLockedError) {
991
- const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
992
- if (!signal.aborted) {
993
- await new Promise((resolve) => {
994
- const timeoutId = setTimeout(resolve, waitTime);
995
- signal.addEventListener(
996
- "abort",
997
- () => {
998
- clearTimeout(timeoutId);
999
- resolve();
1000
- },
1001
- { once: true }
1002
- );
1003
- });
1004
- }
1005
- continue;
1006
- }
1007
- console.error("Error polling topic:", error);
1008
- throw error;
747
+ );
748
+ await this.processMessage(
749
+ response.message,
750
+ handler
751
+ );
752
+ } else {
753
+ const response = await this.client.receiveMessageById(
754
+ {
755
+ queueName: this.topicName,
756
+ consumerGroup: this.consumerGroupName,
757
+ messageId: options.messageId,
758
+ visibilityTimeoutSeconds: this.visibilityTimeout
759
+ },
760
+ this.transport
761
+ );
762
+ await this.processMessage(
763
+ response.message,
764
+ handler
765
+ );
766
+ }
767
+ } else {
768
+ let messageFound = false;
769
+ for await (const message of this.client.receiveMessages(
770
+ {
771
+ queueName: this.topicName,
772
+ consumerGroup: this.consumerGroupName,
773
+ visibilityTimeoutSeconds: this.visibilityTimeout,
774
+ limit: 1
775
+ },
776
+ this.transport
777
+ )) {
778
+ messageFound = true;
779
+ await this.processMessage(message, handler);
780
+ break;
781
+ }
782
+ if (!messageFound) {
783
+ throw new Error("No messages available");
1009
784
  }
1010
785
  }
1011
786
  }
1012
- /**
1013
- * Receive and process a specific message by its ID with full payload
1014
- * @param messageId The ID of the message to receive and process
1015
- * @param handler Function to process the message with full payload
1016
- * @returns Promise that resolves when the message is processed or rejects with specific errors
1017
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
1018
- * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1019
- * @throws {MessageLockedError} When the message is temporarily locked (423)
1020
- * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1021
- * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1022
- * @throws {MessageCorruptedError} When the message data is corrupted
1023
- * @throws {BadRequestError} When request parameters are invalid
1024
- * @throws {UnauthorizedError} When authentication fails
1025
- * @throws {ForbiddenError} When access is denied
1026
- * @throws {InternalServerError} When server encounters an error
1027
- */
1028
- async receiveMessage(messageId, handler) {
1029
- const response = await this.client.receiveMessageById(
1030
- {
1031
- queueName: this.topicName,
1032
- consumerGroup: this.consumerGroupName,
1033
- messageId,
1034
- visibilityTimeoutSeconds: this.visibilityTimeout
1035
- },
1036
- this.transport
1037
- );
1038
- await this.processMessage(response.message, handler);
1039
- }
1040
- /**
1041
- * Receive and process the next available message from the queue
1042
- * @param handler Function to process the message
1043
- * @returns Promise that resolves when the message is processed or rejects with specific errors
1044
- * @throws {QueueEmptyError} When no messages are available in the queue (204)
1045
- * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
1046
- * @throws {BadRequestError} When request parameters are invalid
1047
- * @throws {UnauthorizedError} When authentication fails
1048
- * @throws {ForbiddenError} When access is denied
1049
- * @throws {InternalServerError} When server encounters an error
1050
- */
1051
- async receiveNextMessage(handler) {
1052
- let messageFound = false;
1053
- for await (const message of this.client.receiveMessages(
1054
- {
1055
- queueName: this.topicName,
1056
- consumerGroup: this.consumerGroupName,
1057
- visibilityTimeoutSeconds: this.visibilityTimeout,
1058
- limit: 1
1059
- },
1060
- this.transport
1061
- )) {
1062
- messageFound = true;
1063
- await this.processMessage(message, handler);
1064
- break;
1065
- }
1066
- if (!messageFound) {
1067
- throw new Error("No messages available");
1068
- }
1069
- }
1070
- /**
1071
- * Receive and process multiple next available messages from the queue
1072
- * @param limit Number of messages to process (1-10)
1073
- * @param handler Function to process each message
1074
- * @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
1075
- * @throws {InvalidLimitError} When limit parameter is not between 1 and 10
1076
- * @throws {QueueEmptyError} When no messages are available in the queue (204)
1077
- * @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
1078
- * @throws {BadRequestError} When request parameters are invalid
1079
- * @throws {UnauthorizedError} When authentication fails
1080
- * @throws {ForbiddenError} When access is denied
1081
- * @throws {InternalServerError} When server encounters an error
1082
- */
1083
- async receiveNextMessages(limit, handler) {
1084
- if (limit < 1 || limit > 10) {
1085
- throw new InvalidLimitError(limit);
1086
- }
1087
- const processingPromises = [];
1088
- let messageCount = 0;
1089
- for await (const message of this.client.receiveMessages(
1090
- {
1091
- queueName: this.topicName,
1092
- consumerGroup: this.consumerGroupName,
1093
- visibilityTimeoutSeconds: this.visibilityTimeout,
1094
- limit
1095
- },
1096
- this.transport
1097
- )) {
1098
- messageCount++;
1099
- const wrappedPromise = this.processMessage(message, handler).then(
1100
- (value) => ({
1101
- status: "fulfilled",
1102
- value
1103
- }),
1104
- (reason) => ({ status: "rejected", reason })
1105
- );
1106
- processingPromises.push(wrappedPromise);
1107
- }
1108
- if (messageCount === 0) {
1109
- throw new Error("No messages available");
1110
- }
1111
- const results = await Promise.all(processingPromises);
1112
- return results;
1113
- }
1114
- /**
1115
- * Handle a specific message by its ID without downloading the payload (metadata only)
1116
- * @param messageId The ID of the message to handle
1117
- * @param handler Function to process the message metadata (payload will be void)
1118
- * @returns Promise that resolves when the message is handled or rejects with specific errors
1119
- * @throws {MessageNotFoundError} When the message doesn't exist (404)
1120
- * @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
1121
- * @throws {MessageLockedError} When the message is temporarily locked (423)
1122
- * @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
1123
- * @throws {FailedDependencyError} When FIFO ordering is violated (424)
1124
- * @throws {MessageCorruptedError} When the message data is corrupted
1125
- * @throws {BadRequestError} When request parameters are invalid
1126
- * @throws {UnauthorizedError} When authentication fails
1127
- * @throws {ForbiddenError} When access is denied
1128
- * @throws {InternalServerError} When server encounters an error
1129
- */
1130
- async handleMessage(messageId, handler) {
1131
- const response = await this.client.receiveMessageById(
1132
- {
1133
- queueName: this.topicName,
1134
- consumerGroup: this.consumerGroupName,
1135
- messageId,
1136
- visibilityTimeoutSeconds: this.visibilityTimeout,
1137
- skipPayload: true
1138
- },
1139
- this.transport
1140
- );
1141
- await this.processMessage(response.message, handler);
1142
- }
1143
787
  /**
1144
788
  * Get the consumer group name
1145
789
  */
@@ -1161,7 +805,7 @@ var Topic = class {
1161
805
  transport;
1162
806
  /**
1163
807
  * Create a new Topic instance
1164
- * @param client VQSClient instance to use for API calls
808
+ * @param client QueueClient instance to use for API calls
1165
809
  * @param topicName Name of the topic to work with
1166
810
  * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1167
811
  */
@@ -1186,8 +830,7 @@ var Topic = class {
1186
830
  queueName: this.topicName,
1187
831
  payload,
1188
832
  idempotencyKey: options?.idempotencyKey,
1189
- retentionSeconds: options?.retentionSeconds,
1190
- callbacks: options?.callbacks
833
+ retentionSeconds: options?.retentionSeconds
1191
834
  },
1192
835
  this.transport
1193
836
  );
@@ -1226,40 +869,127 @@ var Topic = class {
1226
869
  };
1227
870
 
1228
871
  // src/factory.ts
1229
- function createTopic(client, topicName, transport) {
1230
- return new Topic(client, topicName, transport);
872
+ async function send(topicName, payload, options) {
873
+ const transport = options?.transport || new JsonTransport();
874
+ const client = new QueueClient();
875
+ const result = await client.sendMessage(
876
+ {
877
+ queueName: topicName,
878
+ payload,
879
+ idempotencyKey: options?.idempotencyKey,
880
+ retentionSeconds: options?.retentionSeconds
881
+ },
882
+ transport
883
+ );
884
+ return { messageId: result.messageId };
885
+ }
886
+ async function receive(topicName, consumerGroup, handler, options) {
887
+ const transport = options?.transport || new JsonTransport();
888
+ const client = new QueueClient();
889
+ const topic = new Topic(client, topicName, transport);
890
+ const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
891
+ const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
892
+ if (messageId) {
893
+ if (skipPayload) {
894
+ return consumer.consume(handler, {
895
+ messageId,
896
+ skipPayload: true
897
+ });
898
+ } else {
899
+ return consumer.consume(handler, { messageId });
900
+ }
901
+ } else {
902
+ return consumer.consume(handler);
903
+ }
1231
904
  }
1232
905
 
1233
906
  // src/callback.ts
1234
- function parseCallbackRequest(request) {
1235
- const headers = request.headers;
1236
- const messageId = headers.get("Vqs-Message-Id");
1237
- const queueName = headers.get("Vqs-Queue-Name");
1238
- const consumerGroup = headers.get("Vqs-Consumer-Group");
1239
- const missingHeaders = [];
1240
- if (!messageId) missingHeaders.push("Vqs-Message-Id");
1241
- if (!queueName) missingHeaders.push("Vqs-Queue-Name");
1242
- if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
1243
- if (missingHeaders.length > 0) {
1244
- throw new InvalidCallbackError(
1245
- `Missing required VQS callback headers: ${missingHeaders.join(", ")}`
907
+ async function parseCallbackRequest(request) {
908
+ const contentType = request.headers.get("content-type");
909
+ if (!contentType || !contentType.includes("application/cloudevents+json")) {
910
+ throw new Error(
911
+ "Invalid content type: expected 'application/cloudevents+json'"
1246
912
  );
1247
913
  }
914
+ let cloudEvent;
915
+ try {
916
+ cloudEvent = await request.json();
917
+ } catch (error) {
918
+ throw new Error("Failed to parse CloudEvent from request body");
919
+ }
920
+ if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
921
+ throw new Error("Invalid CloudEvent: missing required fields");
922
+ }
923
+ if (cloudEvent.type !== "com.vercel.queue.v1beta") {
924
+ throw new Error(
925
+ `Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
926
+ );
927
+ }
928
+ const missingFields = [];
929
+ if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
930
+ if (!("consumerGroup" in cloudEvent.data))
931
+ missingFields.push("consumerGroup");
932
+ if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
933
+ if (missingFields.length > 0) {
934
+ throw new Error(
935
+ `Missing required CloudEvent data fields: ${missingFields.join(", ")}`
936
+ );
937
+ }
938
+ const { messageId, queueName, consumerGroup } = cloudEvent.data;
1248
939
  return {
1249
- messageId,
1250
940
  queueName,
1251
- consumerGroup
941
+ consumerGroup,
942
+ messageId
943
+ };
944
+ }
945
+ function handleCallback(handlers) {
946
+ return async (request) => {
947
+ try {
948
+ const { queueName, consumerGroup, messageId } = await parseCallbackRequest(request);
949
+ const topicHandler = handlers[queueName];
950
+ if (!topicHandler) {
951
+ const availableTopics = Object.keys(handlers).join(", ");
952
+ return Response.json(
953
+ {
954
+ error: `No handler found for topic: ${queueName}`,
955
+ availableTopics
956
+ },
957
+ { status: 404 }
958
+ );
959
+ }
960
+ const consumerGroupHandler = topicHandler[consumerGroup];
961
+ if (!consumerGroupHandler) {
962
+ const availableGroups = Object.keys(topicHandler).join(", ");
963
+ return Response.json(
964
+ {
965
+ error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
966
+ availableGroups
967
+ },
968
+ { status: 404 }
969
+ );
970
+ }
971
+ const client = new QueueClient();
972
+ const topic = new Topic(client, queueName);
973
+ const cg = topic.consumerGroup(consumerGroup);
974
+ await cg.consume(consumerGroupHandler, { messageId });
975
+ return Response.json({ status: "success" });
976
+ } catch (error) {
977
+ console.error("Queue callback error:", error);
978
+ 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"))) {
979
+ return Response.json({ error: error.message }, { status: 400 });
980
+ }
981
+ return Response.json(
982
+ { error: "Failed to process queue message" },
983
+ { status: 500 }
984
+ );
985
+ }
1252
986
  };
1253
987
  }
1254
988
  export {
1255
989
  BadRequestError,
1256
990
  BufferTransport,
1257
- ConsumerGroup,
1258
- FailedDependencyError,
1259
- FifoOrderingViolationError,
1260
991
  ForbiddenError,
1261
992
  InternalServerError,
1262
- InvalidCallbackError,
1263
993
  InvalidLimitError,
1264
994
  JsonTransport,
1265
995
  MessageCorruptedError,
@@ -1268,11 +998,9 @@ export {
1268
998
  MessageNotFoundError,
1269
999
  QueueEmptyError,
1270
1000
  StreamTransport,
1271
- Topic,
1272
1001
  UnauthorizedError,
1273
- VQSClient,
1274
- createTopic,
1275
- getVercelOidcToken,
1276
- parseCallbackRequest
1002
+ handleCallback,
1003
+ receive,
1004
+ send
1277
1005
  };
1278
1006
  //# sourceMappingURL=index.mjs.map