@vercel/queue 0.1.7 → 0.2.1

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.d.mts CHANGED
@@ -755,6 +755,73 @@ declare function handleCallback<T = unknown>(handler: MessageHandler<T>, options
755
755
  retry?: RetryHandler;
756
756
  }): (requestOrEvent: CallbackRequestInput) => Promise<Response>;
757
757
 
758
+ /**
759
+ * Options for {@link registerDevConsumer}.
760
+ */
761
+ interface RegisterDevConsumerOptions {
762
+ /**
763
+ * Topic name to subscribe to. Supports wildcard patterns (e.g. `"user-*"`).
764
+ */
765
+ topic: string;
766
+ /**
767
+ * The QueueClient instance used for callback processing (visibility
768
+ * extensions, acks, etc.). Messages delivered to the handler are fetched
769
+ * via this client.
770
+ */
771
+ client: QueueClient;
772
+ /**
773
+ * Function invoked with each message delivered to the topic.
774
+ */
775
+ handler: MessageHandler;
776
+ /**
777
+ * Logical identifier for this consumer. Multiple consumers for the same
778
+ * topic must use distinct groups; re-registering with the same group
779
+ * replaces the previous handler (useful for HMR).
780
+ *
781
+ * @default "dev-consumer"
782
+ */
783
+ consumerGroup?: string;
784
+ /**
785
+ * Lock duration for in-flight messages. Forwarded to `coreHandleCallback`.
786
+ */
787
+ visibilityTimeoutSeconds?: number;
788
+ /**
789
+ * Called when the handler throws. Return `{ afterSeconds: N }` to schedule
790
+ * local re-delivery, or `{ acknowledge: true }` to drop the message.
791
+ */
792
+ retry?: RetryHandler;
793
+ }
794
+ /**
795
+ * Register a consumer for a topic without relying on `vercel.json` route
796
+ * discovery or stack-trace-based caller detection.
797
+ *
798
+ * Intended for framework integrations (e.g. Nitro) where queue handlers are
799
+ * wired through a central dispatcher rather than per-file `handleCallback()`
800
+ * calls. The caller supplies the topic and a handler; `send()` in dev mode
801
+ * will route messages through it via the same retry/re-delivery machinery
802
+ * used for file-discovered handlers.
803
+ *
804
+ * Call only when `isDevMode()` is true. In production this has no effect on
805
+ * routing — messages are delivered by the Vercel Queue Service directly.
806
+ *
807
+ * @returns An unregister function. Calling it removes the handler that this
808
+ * call registered; if the entry was replaced (via re-registration with the
809
+ * same `consumerGroup`), the unregister is a no-op.
810
+ *
811
+ * @example
812
+ * ```ts
813
+ * const unregister = registerDevConsumer({
814
+ * topic: "orders",
815
+ * client,
816
+ * handler: async (message, metadata) => {
817
+ * await dispatch(message, metadata);
818
+ * },
819
+ * consumerGroup: "nitro-vercel-queue",
820
+ * });
821
+ * ```
822
+ */
823
+ declare function registerDevConsumer(options: RegisterDevConsumerOptions): () => void;
824
+
758
825
  /**
759
826
  * Core queue callback utilities for handling incoming webhook payloads
760
827
  * from Vercel triggers using the CloudEvent specification.
@@ -828,4 +895,4 @@ declare function parseRawCallback(body: unknown, headers: Record<string, string
828
895
  */
829
896
  declare function parseCallback(request: Request): Promise<ParsedCallbackRequest>;
830
897
 
831
- export { BadRequestError, type BaseUrlResolver, BufferTransport, CLOUD_EVENT_TYPE_V1BETA, CLOUD_EVENT_TYPE_V2BETA, ConsumerDiscoveryError, ConsumerRegistryNotConfiguredError, DuplicateMessageError, ForbiddenError, InternalServerError, InvalidLimitError, JsonTransport, type Message, MessageAlreadyProcessedError, MessageCorruptedError, type MessageHandler, MessageLockedError, type MessageMetadata, MessageNotAvailableError, MessageNotFoundError, type ParsedCallbackRequest, type ParsedCallbackV1, type ParsedCallbackV2, PollingQueueClient, type PollingQueueClientOptions, QueueClient, type QueueClientOptions, QueueEmptyError, type ReceiveBatchOptions, type ReceiveByIdOptions, type ReceiveOptions, type ReceiveResult, type RetryDirective, type RetryHandler, type SendOptions, type SendResult, StreamTransport, type Transport, UnauthorizedError, type VercelRegion, handleCallback, parseCallback, parseRawCallback, send };
898
+ export { BadRequestError, type BaseUrlResolver, BufferTransport, CLOUD_EVENT_TYPE_V1BETA, CLOUD_EVENT_TYPE_V2BETA, ConsumerDiscoveryError, ConsumerRegistryNotConfiguredError, DuplicateMessageError, ForbiddenError, InternalServerError, InvalidLimitError, JsonTransport, type Message, MessageAlreadyProcessedError, MessageCorruptedError, type MessageHandler, MessageLockedError, type MessageMetadata, MessageNotAvailableError, MessageNotFoundError, type ParsedCallbackRequest, type ParsedCallbackV1, type ParsedCallbackV2, PollingQueueClient, type PollingQueueClientOptions, QueueClient, type QueueClientOptions, QueueEmptyError, type ReceiveBatchOptions, type ReceiveByIdOptions, type ReceiveOptions, type ReceiveResult, type RegisterDevConsumerOptions, type RetryDirective, type RetryHandler, type SendOptions, type SendResult, StreamTransport, type Transport, UnauthorizedError, type VercelRegion, handleCallback, parseCallback, parseRawCallback, registerDevConsumer, send };
package/dist/index.d.ts CHANGED
@@ -755,6 +755,73 @@ declare function handleCallback<T = unknown>(handler: MessageHandler<T>, options
755
755
  retry?: RetryHandler;
756
756
  }): (requestOrEvent: CallbackRequestInput) => Promise<Response>;
757
757
 
758
+ /**
759
+ * Options for {@link registerDevConsumer}.
760
+ */
761
+ interface RegisterDevConsumerOptions {
762
+ /**
763
+ * Topic name to subscribe to. Supports wildcard patterns (e.g. `"user-*"`).
764
+ */
765
+ topic: string;
766
+ /**
767
+ * The QueueClient instance used for callback processing (visibility
768
+ * extensions, acks, etc.). Messages delivered to the handler are fetched
769
+ * via this client.
770
+ */
771
+ client: QueueClient;
772
+ /**
773
+ * Function invoked with each message delivered to the topic.
774
+ */
775
+ handler: MessageHandler;
776
+ /**
777
+ * Logical identifier for this consumer. Multiple consumers for the same
778
+ * topic must use distinct groups; re-registering with the same group
779
+ * replaces the previous handler (useful for HMR).
780
+ *
781
+ * @default "dev-consumer"
782
+ */
783
+ consumerGroup?: string;
784
+ /**
785
+ * Lock duration for in-flight messages. Forwarded to `coreHandleCallback`.
786
+ */
787
+ visibilityTimeoutSeconds?: number;
788
+ /**
789
+ * Called when the handler throws. Return `{ afterSeconds: N }` to schedule
790
+ * local re-delivery, or `{ acknowledge: true }` to drop the message.
791
+ */
792
+ retry?: RetryHandler;
793
+ }
794
+ /**
795
+ * Register a consumer for a topic without relying on `vercel.json` route
796
+ * discovery or stack-trace-based caller detection.
797
+ *
798
+ * Intended for framework integrations (e.g. Nitro) where queue handlers are
799
+ * wired through a central dispatcher rather than per-file `handleCallback()`
800
+ * calls. The caller supplies the topic and a handler; `send()` in dev mode
801
+ * will route messages through it via the same retry/re-delivery machinery
802
+ * used for file-discovered handlers.
803
+ *
804
+ * Call only when `isDevMode()` is true. In production this has no effect on
805
+ * routing — messages are delivered by the Vercel Queue Service directly.
806
+ *
807
+ * @returns An unregister function. Calling it removes the handler that this
808
+ * call registered; if the entry was replaced (via re-registration with the
809
+ * same `consumerGroup`), the unregister is a no-op.
810
+ *
811
+ * @example
812
+ * ```ts
813
+ * const unregister = registerDevConsumer({
814
+ * topic: "orders",
815
+ * client,
816
+ * handler: async (message, metadata) => {
817
+ * await dispatch(message, metadata);
818
+ * },
819
+ * consumerGroup: "nitro-vercel-queue",
820
+ * });
821
+ * ```
822
+ */
823
+ declare function registerDevConsumer(options: RegisterDevConsumerOptions): () => void;
824
+
758
825
  /**
759
826
  * Core queue callback utilities for handling incoming webhook payloads
760
827
  * from Vercel triggers using the CloudEvent specification.
@@ -828,4 +895,4 @@ declare function parseRawCallback(body: unknown, headers: Record<string, string
828
895
  */
829
896
  declare function parseCallback(request: Request): Promise<ParsedCallbackRequest>;
830
897
 
831
- export { BadRequestError, type BaseUrlResolver, BufferTransport, CLOUD_EVENT_TYPE_V1BETA, CLOUD_EVENT_TYPE_V2BETA, ConsumerDiscoveryError, ConsumerRegistryNotConfiguredError, DuplicateMessageError, ForbiddenError, InternalServerError, InvalidLimitError, JsonTransport, type Message, MessageAlreadyProcessedError, MessageCorruptedError, type MessageHandler, MessageLockedError, type MessageMetadata, MessageNotAvailableError, MessageNotFoundError, type ParsedCallbackRequest, type ParsedCallbackV1, type ParsedCallbackV2, PollingQueueClient, type PollingQueueClientOptions, QueueClient, type QueueClientOptions, QueueEmptyError, type ReceiveBatchOptions, type ReceiveByIdOptions, type ReceiveOptions, type ReceiveResult, type RetryDirective, type RetryHandler, type SendOptions, type SendResult, StreamTransport, type Transport, UnauthorizedError, type VercelRegion, handleCallback, parseCallback, parseRawCallback, send };
898
+ export { BadRequestError, type BaseUrlResolver, BufferTransport, CLOUD_EVENT_TYPE_V1BETA, CLOUD_EVENT_TYPE_V2BETA, ConsumerDiscoveryError, ConsumerRegistryNotConfiguredError, DuplicateMessageError, ForbiddenError, InternalServerError, InvalidLimitError, JsonTransport, type Message, MessageAlreadyProcessedError, MessageCorruptedError, type MessageHandler, MessageLockedError, type MessageMetadata, MessageNotAvailableError, MessageNotFoundError, type ParsedCallbackRequest, type ParsedCallbackV1, type ParsedCallbackV2, PollingQueueClient, type PollingQueueClientOptions, QueueClient, type QueueClientOptions, QueueEmptyError, type ReceiveBatchOptions, type ReceiveByIdOptions, type ReceiveOptions, type ReceiveResult, type RegisterDevConsumerOptions, type RetryDirective, type RetryHandler, type SendOptions, type SendResult, StreamTransport, type Transport, UnauthorizedError, type VercelRegion, handleCallback, parseCallback, parseRawCallback, registerDevConsumer, send };
package/dist/index.js CHANGED
@@ -54,6 +54,7 @@ __export(index_exports, {
54
54
  handleCallback: () => handleCallback2,
55
55
  parseCallback: () => parseCallback,
56
56
  parseRawCallback: () => parseRawCallback,
57
+ registerDevConsumer: () => registerDevConsumer,
57
58
  send: () => send
58
59
  });
59
60
  module.exports = __toCommonJS(index_exports);
@@ -657,7 +658,13 @@ function parseBinaryHeaders(headers) {
657
658
  `Missing required CloudEvent headers: ${missingFields.join(", ")}`
658
659
  );
659
660
  }
660
- const region = getHeader(headers, "ce-vqsregion") ?? void 0;
661
+ const rawRegion = getHeader(headers, "ce-vqsregion") ?? void 0;
662
+ if (rawRegion !== void 0 && !/^[a-z]{2,5}[0-9]{1,2}$/.test(rawRegion)) {
663
+ throw new Error(
664
+ `Invalid ce-vqsregion header: ${JSON.stringify(rawRegion)}. Region must match /^[a-z]{2,5}[0-9]{1,2}$/ (e.g. "iad1", "lhr1").`
665
+ );
666
+ }
667
+ const region = rawRegion;
661
668
  const base = {
662
669
  queueName,
663
670
  consumerGroup,
@@ -1002,6 +1009,44 @@ Add a trigger to vercel.json:
1002
1009
  );
1003
1010
  }
1004
1011
  }
1012
+ function registerDevConsumer(options) {
1013
+ const {
1014
+ topic,
1015
+ client,
1016
+ handler,
1017
+ consumerGroup = "dev-consumer",
1018
+ visibilityTimeoutSeconds,
1019
+ retry
1020
+ } = options;
1021
+ const entry = {
1022
+ consumerGroup,
1023
+ handler,
1024
+ client,
1025
+ options: visibilityTimeoutSeconds !== void 0 || retry !== void 0 ? { visibilityTimeoutSeconds, retry } : void 0
1026
+ };
1027
+ const registry = getHandlerRegistry();
1028
+ const existing = registry.get(topic) ?? [];
1029
+ const existingIndex = existing.findIndex(
1030
+ (e) => e.consumerGroup === consumerGroup
1031
+ );
1032
+ if (existingIndex >= 0) {
1033
+ existing[existingIndex] = entry;
1034
+ } else {
1035
+ existing.push(entry);
1036
+ }
1037
+ registry.set(topic, existing);
1038
+ return () => {
1039
+ const current = registry.get(topic);
1040
+ if (!current) return;
1041
+ const filtered = current.filter((e) => e !== entry);
1042
+ if (filtered.length === current.length) return;
1043
+ if (filtered.length === 0) {
1044
+ registry.delete(topic);
1045
+ } else {
1046
+ registry.set(topic, filtered);
1047
+ }
1048
+ };
1049
+ }
1005
1050
  function lookupHandlers(topicName) {
1006
1051
  const registry = getHandlerRegistry();
1007
1052
  const result = [];
@@ -1509,6 +1554,14 @@ function parseQueueHeaders(headers) {
1509
1554
  receiptHandle
1510
1555
  };
1511
1556
  }
1557
+ var REGION_PATTERN = /^[a-z]{2,5}[0-9]{1,2}$/;
1558
+ function validateRegion(region) {
1559
+ if (!REGION_PATTERN.test(region)) {
1560
+ throw new Error(
1561
+ `Invalid region code: ${JSON.stringify(region)}. Region must match the pattern /^[a-z]{2,5}[0-9]{1,2}$/ (e.g. "iad1", "lhr1").`
1562
+ );
1563
+ }
1564
+ }
1512
1565
  var DEFAULT_BASE_URL_RESOLVER = (region) => new URL(`https://${region}.vercel-queue.com`);
1513
1566
  function resolveBaseUrl(region, resolver) {
1514
1567
  return (resolver ?? DEFAULT_BASE_URL_RESOLVER)(region);
@@ -1526,6 +1579,7 @@ var ApiClient = class _ApiClient {
1526
1579
  baseUrlResolver;
1527
1580
  dispatcher;
1528
1581
  constructor(options) {
1582
+ validateRegion(options.region);
1529
1583
  this.region = options.region;
1530
1584
  this.baseUrlResolver = options.resolveBaseUrl;
1531
1585
  this.baseUrl = resolveBaseUrl(this.region, this.baseUrlResolver);
@@ -1640,7 +1694,7 @@ Cause: ${cause}`
1640
1694
  }
1641
1695
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1642
1696
  }
1643
- init.headers.set("User-Agent", `@vercel/queue/${"0.1.7"}`);
1697
+ init.headers.set("User-Agent", `@vercel/queue/${"0.2.1"}`);
1644
1698
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1645
1699
  const fetchInit = this.dispatcher ? { ...init, dispatcher: this.dispatcher } : init;
1646
1700
  const response = await fetch(url, fetchInit);
@@ -2330,6 +2384,7 @@ function handleCallback2(handler, options) {
2330
2384
  handleCallback,
2331
2385
  parseCallback,
2332
2386
  parseRawCallback,
2387
+ registerDevConsumer,
2333
2388
  send
2334
2389
  });
2335
2390
  //# sourceMappingURL=index.js.map