@vercel/queue 0.0.0-alpha.12 → 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.d.mts CHANGED
@@ -27,6 +27,12 @@ interface Transport<T = unknown> {
27
27
  */
28
28
  declare class JsonTransport<T = unknown> implements Transport<T> {
29
29
  readonly contentType = "application/json";
30
+ readonly replacer?: Parameters<typeof JSON.parse>[1];
31
+ readonly reviver?: Parameters<typeof JSON.parse>[1];
32
+ constructor(options?: {
33
+ replacer?: Parameters<typeof JSON.parse>[1];
34
+ reviver?: Parameters<typeof JSON.parse>[1];
35
+ });
30
36
  serialize(value: T): Buffer;
31
37
  deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
32
38
  }
package/dist/index.d.ts CHANGED
@@ -27,6 +27,12 @@ interface Transport<T = unknown> {
27
27
  */
28
28
  declare class JsonTransport<T = unknown> implements Transport<T> {
29
29
  readonly contentType = "application/json";
30
+ readonly replacer?: Parameters<typeof JSON.parse>[1];
31
+ readonly reviver?: Parameters<typeof JSON.parse>[1];
32
+ constructor(options?: {
33
+ replacer?: Parameters<typeof JSON.parse>[1];
34
+ reviver?: Parameters<typeof JSON.parse>[1];
35
+ });
30
36
  serialize(value: T): Buffer;
31
37
  deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
32
38
  }
package/dist/index.js CHANGED
@@ -41,27 +41,36 @@ __export(index_exports, {
41
41
  module.exports = __toCommonJS(index_exports);
42
42
 
43
43
  // src/transports.ts
44
+ async function streamToBuffer(stream) {
45
+ let totalLength = 0;
46
+ const reader = stream.getReader();
47
+ const chunks = [];
48
+ try {
49
+ while (true) {
50
+ const { done, value } = await reader.read();
51
+ if (done) break;
52
+ chunks.push(value);
53
+ totalLength += value.length;
54
+ }
55
+ } finally {
56
+ reader.releaseLock();
57
+ }
58
+ return Buffer.concat(chunks, totalLength);
59
+ }
44
60
  var JsonTransport = class {
45
61
  contentType = "application/json";
62
+ replacer;
63
+ reviver;
64
+ constructor(options = {}) {
65
+ this.replacer = options.replacer;
66
+ this.reviver = options.reviver;
67
+ }
46
68
  serialize(value) {
47
- return Buffer.from(JSON.stringify(value), "utf8");
69
+ return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
48
70
  }
49
71
  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"));
72
+ const buffer = await streamToBuffer(stream);
73
+ return JSON.parse(buffer.toString("utf8"), this.reviver);
65
74
  }
66
75
  };
67
76
  var BufferTransport = class {
@@ -70,25 +79,7 @@ var BufferTransport = class {
70
79
  return value;
71
80
  }
72
81
  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);
82
+ return await streamToBuffer(stream);
92
83
  }
93
84
  };
94
85
  var StreamTransport = class {
@@ -842,111 +833,6 @@ var ConsumerGroup = class {
842
833
  }
843
834
  };
844
835
 
845
- // src/topic.ts
846
- var Topic = class {
847
- client;
848
- topicName;
849
- transport;
850
- /**
851
- * Create a new Topic instance
852
- * @param client QueueClient instance to use for API calls
853
- * @param topicName Name of the topic to work with
854
- * @param transport Optional serializer/deserializer for the payload (defaults to JSON)
855
- */
856
- constructor(client, topicName, transport) {
857
- this.client = client;
858
- this.topicName = topicName;
859
- this.transport = transport || new JsonTransport();
860
- }
861
- /**
862
- * Publish a message to the topic
863
- * @param payload The data to publish
864
- * @param options Optional publish options
865
- * @returns An object containing the message ID
866
- * @throws {BadRequestError} When request parameters are invalid
867
- * @throws {UnauthorizedError} When authentication fails
868
- * @throws {ForbiddenError} When access is denied (environment mismatch)
869
- * @throws {InternalServerError} When server encounters an error
870
- */
871
- async publish(payload, options) {
872
- const result = await this.client.sendMessage(
873
- {
874
- queueName: this.topicName,
875
- payload,
876
- idempotencyKey: options?.idempotencyKey,
877
- retentionSeconds: options?.retentionSeconds
878
- },
879
- this.transport
880
- );
881
- return { messageId: result.messageId };
882
- }
883
- /**
884
- * Create a consumer group for this topic
885
- * @param consumerGroupName Name of the consumer group
886
- * @param options Optional configuration for the consumer group
887
- * @returns A ConsumerGroup instance
888
- */
889
- consumerGroup(consumerGroupName, options) {
890
- const consumerOptions = {
891
- ...options,
892
- transport: options?.transport || this.transport
893
- };
894
- return new ConsumerGroup(
895
- this.client,
896
- this.topicName,
897
- consumerGroupName,
898
- consumerOptions
899
- );
900
- }
901
- /**
902
- * Get the topic name
903
- */
904
- get name() {
905
- return this.topicName;
906
- }
907
- /**
908
- * Get the transport used by this topic
909
- */
910
- get serializer() {
911
- return this.transport;
912
- }
913
- };
914
-
915
- // 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
- }
948
- }
949
-
950
836
  // src/callback.ts
951
837
  function validateWildcardPattern(pattern) {
952
838
  const firstIndex = pattern.indexOf("*");
@@ -1026,7 +912,7 @@ function handleCallback(handlers) {
1026
912
  }
1027
913
  }
1028
914
  }
1029
- return async (request) => {
915
+ const routeHandler = async (request) => {
1030
916
  try {
1031
917
  const { queueName, consumerGroup, messageId } = await parseCallback(request);
1032
918
  const topicHandler = findTopicHandler(queueName, handlers);
@@ -1067,6 +953,309 @@ function handleCallback(handlers) {
1067
953
  );
1068
954
  }
1069
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
+ }
1070
1259
  }
1071
1260
  // Annotate the CommonJS export names for ESM import in node:
1072
1261
  0 && (module.exports = {