@vercel/queue 0.0.0-alpha.13 → 0.0.0-alpha.15
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 +3 -3
- package/dist/index.js +307 -106
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -106
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
|
|
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,312 @@ 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
|
+
if (isDevMode()) {
|
|
1240
|
+
triggerDevCallbacks(topicName, result.messageId);
|
|
1241
|
+
}
|
|
1242
|
+
return { messageId: result.messageId };
|
|
1243
|
+
}
|
|
1244
|
+
async function receive(topicName, consumerGroup, handler, options) {
|
|
1245
|
+
const transport = options?.transport || new JsonTransport();
|
|
1246
|
+
const client = new QueueClient();
|
|
1247
|
+
const topic = new Topic(client, topicName, transport);
|
|
1248
|
+
const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
|
|
1249
|
+
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
1250
|
+
if (messageId) {
|
|
1251
|
+
if (skipPayload) {
|
|
1252
|
+
return consumer.consume(handler, {
|
|
1253
|
+
messageId,
|
|
1254
|
+
skipPayload: true
|
|
1255
|
+
});
|
|
1256
|
+
} else {
|
|
1257
|
+
return consumer.consume(handler, { messageId });
|
|
1258
|
+
}
|
|
1259
|
+
} else {
|
|
1260
|
+
return consumer.consume(handler);
|
|
1261
|
+
}
|
|
1061
1262
|
}
|
|
1062
1263
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1063
1264
|
0 && (module.exports = {
|