@vercel/queue 0.0.0-alpha.13 → 0.0.0-alpha.14

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/README.md CHANGED
@@ -34,7 +34,7 @@ vc env pull
34
34
 
35
35
  Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
36
36
 
37
- ```json
37
+ ```json5
38
38
  {
39
39
  "compilerOptions": {
40
40
  "moduleResolution": "bundler"
@@ -140,7 +140,7 @@ While you can split handlers into separate routes if needed (e.g., for code orga
140
140
 
141
141
  Configure which topics and consumers your API route handles:
142
142
 
143
- ```json
143
+ ```json5
144
144
  {
145
145
  "functions": {
146
146
  "app/api/queue/route.ts": {
@@ -305,7 +305,7 @@ interface MessageTimeoutResult {
305
305
 
306
306
  If you need more than 1,000 messages per second, you can create multiple topics (e.g., user-specific or shard-based topics) and handle them with a single consumer using wildcards in your `vercel.json`:
307
307
 
308
- ```json
308
+ ```json5
309
309
  {
310
310
  "functions": {
311
311
  "app/api/queue/route.ts": {
package/dist/index.js CHANGED
@@ -833,111 +833,6 @@ var ConsumerGroup = class {
833
833
  }
834
834
  };
835
835
 
836
- // src/topic.ts
837
- var Topic = class {
838
- client;
839
- topicName;
840
- transport;
841
- /**
842
- * Create a new Topic instance
843
- * @param client QueueClient instance to use for API calls
844
- * @param topicName Name of the topic to work with
845
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
846
- */
847
- constructor(client, topicName, transport) {
848
- this.client = client;
849
- this.topicName = topicName;
850
- this.transport = transport || new JsonTransport();
851
- }
852
- /**
853
- * Publish a message to the topic
854
- * @param payload The data to publish
855
- * @param options Optional publish options
856
- * @returns An object containing the message ID
857
- * @throws {BadRequestError} When request parameters are invalid
858
- * @throws {UnauthorizedError} When authentication fails
859
- * @throws {ForbiddenError} When access is denied (environment mismatch)
860
- * @throws {InternalServerError} When server encounters an error
861
- */
862
- async publish(payload, options) {
863
- const result = await this.client.sendMessage(
864
- {
865
- queueName: this.topicName,
866
- payload,
867
- idempotencyKey: options?.idempotencyKey,
868
- retentionSeconds: options?.retentionSeconds
869
- },
870
- this.transport
871
- );
872
- return { messageId: result.messageId };
873
- }
874
- /**
875
- * Create a consumer group for this topic
876
- * @param consumerGroupName Name of the consumer group
877
- * @param options Optional configuration for the consumer group
878
- * @returns A ConsumerGroup instance
879
- */
880
- consumerGroup(consumerGroupName, options) {
881
- const consumerOptions = {
882
- ...options,
883
- transport: options?.transport || this.transport
884
- };
885
- return new ConsumerGroup(
886
- this.client,
887
- this.topicName,
888
- consumerGroupName,
889
- consumerOptions
890
- );
891
- }
892
- /**
893
- * Get the topic name
894
- */
895
- get name() {
896
- return this.topicName;
897
- }
898
- /**
899
- * Get the transport used by this topic
900
- */
901
- get serializer() {
902
- return this.transport;
903
- }
904
- };
905
-
906
- // src/factory.ts
907
- async function send(topicName, payload, options) {
908
- const transport = options?.transport || new JsonTransport();
909
- const client = new QueueClient();
910
- const result = await client.sendMessage(
911
- {
912
- queueName: topicName,
913
- payload,
914
- idempotencyKey: options?.idempotencyKey,
915
- retentionSeconds: options?.retentionSeconds
916
- },
917
- transport
918
- );
919
- return { messageId: result.messageId };
920
- }
921
- async function receive(topicName, consumerGroup, handler, options) {
922
- const transport = options?.transport || new JsonTransport();
923
- const client = new QueueClient();
924
- const topic = new Topic(client, topicName, transport);
925
- const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
926
- const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
927
- if (messageId) {
928
- if (skipPayload) {
929
- return consumer.consume(handler, {
930
- messageId,
931
- skipPayload: true
932
- });
933
- } else {
934
- return consumer.consume(handler, { messageId });
935
- }
936
- } else {
937
- return consumer.consume(handler);
938
- }
939
- }
940
-
941
836
  // src/callback.ts
942
837
  function validateWildcardPattern(pattern) {
943
838
  const firstIndex = pattern.indexOf("*");
@@ -1017,7 +912,7 @@ function handleCallback(handlers) {
1017
912
  }
1018
913
  }
1019
914
  }
1020
- return async (request) => {
915
+ const routeHandler = async (request) => {
1021
916
  try {
1022
917
  const { queueName, consumerGroup, messageId } = await parseCallback(request);
1023
918
  const topicHandler = findTopicHandler(queueName, handlers);
@@ -1058,6 +953,309 @@ function handleCallback(handlers) {
1058
953
  );
1059
954
  }
1060
955
  };
956
+ if (isDevMode()) {
957
+ registerDevRouteHandler(routeHandler, handlers);
958
+ }
959
+ return routeHandler;
960
+ }
961
+
962
+ // src/dev.ts
963
+ var devRouteHandlers = /* @__PURE__ */ new Map();
964
+ var wildcardRouteHandlers = /* @__PURE__ */ new Map();
965
+ var routeHandlerKeys = /* @__PURE__ */ new WeakMap();
966
+ function cleanupDeadRefs(key, refs) {
967
+ const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
968
+ if (aliveRefs.length === 0) {
969
+ wildcardRouteHandlers.delete(key);
970
+ } else if (aliveRefs.length < refs.length) {
971
+ wildcardRouteHandlers.set(key, aliveRefs);
972
+ }
973
+ }
974
+ function isDevMode() {
975
+ return process.env.NODE_ENV === "development";
976
+ }
977
+ function registerDevRouteHandler(routeHandler, handlers) {
978
+ const existingKeys = routeHandlerKeys.get(routeHandler);
979
+ if (existingKeys) {
980
+ const newKeys = /* @__PURE__ */ new Set();
981
+ for (const topicName in handlers) {
982
+ for (const consumerGroup in handlers[topicName]) {
983
+ newKeys.add(`${topicName}:${consumerGroup}`);
984
+ }
985
+ }
986
+ for (const key of existingKeys) {
987
+ if (!newKeys.has(key)) {
988
+ const [topicPattern] = key.split(":");
989
+ if (topicPattern.includes("*")) {
990
+ const refs = wildcardRouteHandlers.get(key);
991
+ if (refs) {
992
+ const filteredRefs = refs.filter(
993
+ (ref) => ref.deref() !== routeHandler
994
+ );
995
+ if (filteredRefs.length === 0) {
996
+ wildcardRouteHandlers.delete(key);
997
+ } else {
998
+ wildcardRouteHandlers.set(key, filteredRefs);
999
+ }
1000
+ }
1001
+ } else {
1002
+ devRouteHandlers.delete(key);
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+ const keys = /* @__PURE__ */ new Set();
1008
+ for (const topicName in handlers) {
1009
+ for (const consumerGroup in handlers[topicName]) {
1010
+ const key = `${topicName}:${consumerGroup}`;
1011
+ keys.add(key);
1012
+ if (topicName.includes("*")) {
1013
+ const weakRef = new WeakRef(routeHandler);
1014
+ const existing = wildcardRouteHandlers.get(key) || [];
1015
+ cleanupDeadRefs(key, existing);
1016
+ const cleanedRefs = wildcardRouteHandlers.get(key) || [];
1017
+ cleanedRefs.push(weakRef);
1018
+ wildcardRouteHandlers.set(key, cleanedRefs);
1019
+ } else {
1020
+ devRouteHandlers.set(key, {
1021
+ routeHandler,
1022
+ topicPattern: topicName
1023
+ });
1024
+ }
1025
+ }
1026
+ }
1027
+ routeHandlerKeys.set(routeHandler, keys);
1028
+ }
1029
+ function findRouteHandlersForTopic(topicName) {
1030
+ const handlersMap = /* @__PURE__ */ new Map();
1031
+ for (const [
1032
+ key,
1033
+ { routeHandler, topicPattern }
1034
+ ] of devRouteHandlers.entries()) {
1035
+ const [_, consumerGroup] = key.split(":");
1036
+ if (topicPattern === topicName) {
1037
+ if (!handlersMap.has(routeHandler)) {
1038
+ handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
1039
+ }
1040
+ handlersMap.get(routeHandler).add(consumerGroup);
1041
+ }
1042
+ }
1043
+ for (const [key, refs] of wildcardRouteHandlers.entries()) {
1044
+ const [pattern, consumerGroup] = key.split(":");
1045
+ if (matchesWildcardPattern(topicName, pattern)) {
1046
+ cleanupDeadRefs(key, refs);
1047
+ const cleanedRefs = wildcardRouteHandlers.get(key) || [];
1048
+ for (const ref of cleanedRefs) {
1049
+ const routeHandler = ref.deref();
1050
+ if (routeHandler) {
1051
+ if (!handlersMap.has(routeHandler)) {
1052
+ handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
1053
+ }
1054
+ handlersMap.get(routeHandler).add(consumerGroup);
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+ return handlersMap;
1060
+ }
1061
+ function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
1062
+ const cloudEvent = {
1063
+ type: "com.vercel.queue.v1beta",
1064
+ source: `/topic/${topicName}/consumer/${consumerGroup}`,
1065
+ id: messageId,
1066
+ datacontenttype: "application/json",
1067
+ data: {
1068
+ messageId,
1069
+ queueName: topicName,
1070
+ consumerGroup
1071
+ },
1072
+ time: (/* @__PURE__ */ new Date()).toISOString(),
1073
+ specversion: "1.0"
1074
+ };
1075
+ return new Request("https://localhost/api/queue/callback", {
1076
+ method: "POST",
1077
+ headers: {
1078
+ "Content-Type": "application/cloudevents+json"
1079
+ },
1080
+ body: JSON.stringify(cloudEvent)
1081
+ });
1082
+ }
1083
+ var DEV_CALLBACK_DELAY = 1e3;
1084
+ function triggerDevCallbacks(topicName, messageId) {
1085
+ const handlersMap = findRouteHandlersForTopic(topicName);
1086
+ if (handlersMap.size === 0) {
1087
+ return;
1088
+ }
1089
+ const consumerGroups = Array.from(
1090
+ new Set(
1091
+ Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
1092
+ )
1093
+ );
1094
+ console.log(
1095
+ `[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
1096
+ );
1097
+ setTimeout(async () => {
1098
+ for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
1099
+ for (const consumerGroup of consumerGroups2) {
1100
+ try {
1101
+ const request = createMockCloudEventRequest(
1102
+ topicName,
1103
+ consumerGroup,
1104
+ messageId
1105
+ );
1106
+ const response = await routeHandler(request);
1107
+ if (response.ok) {
1108
+ try {
1109
+ const responseData = await response.json();
1110
+ if (responseData.status === "success") {
1111
+ console.log(
1112
+ `[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
1113
+ );
1114
+ }
1115
+ } catch (jsonError) {
1116
+ console.error(
1117
+ `[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
1118
+ jsonError
1119
+ );
1120
+ }
1121
+ } else {
1122
+ try {
1123
+ const errorData = await response.json();
1124
+ console.error(
1125
+ `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
1126
+ errorData.error || response.statusText
1127
+ );
1128
+ } catch (jsonError) {
1129
+ console.error(
1130
+ `[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
1131
+ response.statusText
1132
+ );
1133
+ }
1134
+ }
1135
+ } catch (error) {
1136
+ console.error(
1137
+ `[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
1138
+ error
1139
+ );
1140
+ }
1141
+ }
1142
+ }
1143
+ }, DEV_CALLBACK_DELAY);
1144
+ }
1145
+ function clearDevHandlers() {
1146
+ devRouteHandlers.clear();
1147
+ wildcardRouteHandlers.clear();
1148
+ }
1149
+ if (process.env.NODE_ENV === "test" || process.env.VITEST) {
1150
+ globalThis.__clearDevHandlers = clearDevHandlers;
1151
+ }
1152
+
1153
+ // src/topic.ts
1154
+ var Topic = class {
1155
+ client;
1156
+ topicName;
1157
+ transport;
1158
+ /**
1159
+ * Create a new Topic instance
1160
+ * @param client QueueClient instance to use for API calls
1161
+ * @param topicName Name of the topic to work with
1162
+ * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
1163
+ */
1164
+ constructor(client, topicName, transport) {
1165
+ this.client = client;
1166
+ this.topicName = topicName;
1167
+ this.transport = transport || new JsonTransport();
1168
+ }
1169
+ /**
1170
+ * Publish a message to the topic
1171
+ * @param payload The data to publish
1172
+ * @param options Optional publish options
1173
+ * @returns An object containing the message ID
1174
+ * @throws {BadRequestError} When request parameters are invalid
1175
+ * @throws {UnauthorizedError} When authentication fails
1176
+ * @throws {ForbiddenError} When access is denied (environment mismatch)
1177
+ * @throws {InternalServerError} When server encounters an error
1178
+ */
1179
+ async publish(payload, options) {
1180
+ const result = await this.client.sendMessage(
1181
+ {
1182
+ queueName: this.topicName,
1183
+ payload,
1184
+ idempotencyKey: options?.idempotencyKey,
1185
+ retentionSeconds: options?.retentionSeconds
1186
+ },
1187
+ this.transport
1188
+ );
1189
+ if (isDevMode()) {
1190
+ triggerDevCallbacks(this.topicName, result.messageId);
1191
+ }
1192
+ return { messageId: result.messageId };
1193
+ }
1194
+ /**
1195
+ * Create a consumer group for this topic
1196
+ * @param consumerGroupName Name of the consumer group
1197
+ * @param options Optional configuration for the consumer group
1198
+ * @returns A ConsumerGroup instance
1199
+ */
1200
+ consumerGroup(consumerGroupName, options) {
1201
+ const consumerOptions = {
1202
+ ...options,
1203
+ transport: options?.transport || this.transport
1204
+ };
1205
+ return new ConsumerGroup(
1206
+ this.client,
1207
+ this.topicName,
1208
+ consumerGroupName,
1209
+ consumerOptions
1210
+ );
1211
+ }
1212
+ /**
1213
+ * Get the topic name
1214
+ */
1215
+ get name() {
1216
+ return this.topicName;
1217
+ }
1218
+ /**
1219
+ * Get the transport used by this topic
1220
+ */
1221
+ get serializer() {
1222
+ return this.transport;
1223
+ }
1224
+ };
1225
+
1226
+ // src/factory.ts
1227
+ async function send(topicName, payload, options) {
1228
+ const transport = options?.transport || new JsonTransport();
1229
+ const client = new QueueClient();
1230
+ const result = await client.sendMessage(
1231
+ {
1232
+ queueName: topicName,
1233
+ payload,
1234
+ idempotencyKey: options?.idempotencyKey,
1235
+ retentionSeconds: options?.retentionSeconds
1236
+ },
1237
+ transport
1238
+ );
1239
+ return { messageId: result.messageId };
1240
+ }
1241
+ async function receive(topicName, consumerGroup, handler, options) {
1242
+ const transport = options?.transport || new JsonTransport();
1243
+ const client = new QueueClient();
1244
+ const topic = new Topic(client, topicName, transport);
1245
+ const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
1246
+ const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
1247
+ if (messageId) {
1248
+ if (skipPayload) {
1249
+ return consumer.consume(handler, {
1250
+ messageId,
1251
+ skipPayload: true
1252
+ });
1253
+ } else {
1254
+ return consumer.consume(handler, { messageId });
1255
+ }
1256
+ } else {
1257
+ return consumer.consume(handler);
1258
+ }
1061
1259
  }
1062
1260
  // Annotate the CommonJS export names for ESM import in node:
1063
1261
  0 && (module.exports = {