@vercel/queue 0.0.0-alpha.4 → 0.0.0-alpha.6
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 +151 -313
- package/dist/index.d.mts +52 -104
- package/dist/index.d.ts +52 -104
- package/dist/index.js +47 -224
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +47 -221
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1,116 +1,6 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
import { parseMultipartStream } from "mixpart";
|
|
3
3
|
|
|
4
|
-
// src/local.ts
|
|
5
|
-
import { spawn } from "child_process";
|
|
6
|
-
function isLocalhostWithPort(url) {
|
|
7
|
-
try {
|
|
8
|
-
const parsedUrl = new URL(url);
|
|
9
|
-
const isLocalhost = parsedUrl.hostname === "localhost";
|
|
10
|
-
const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 0;
|
|
11
|
-
return { isLocalhost, port };
|
|
12
|
-
} catch {
|
|
13
|
-
return { isLocalhost: false };
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function isSupportedPlatform() {
|
|
17
|
-
const platform = process.platform;
|
|
18
|
-
return platform === "darwin" || platform === "linux";
|
|
19
|
-
}
|
|
20
|
-
function processDevelopmentCallbacks(callbacks) {
|
|
21
|
-
const isDevelopment = process.env.NODE_ENV === "development";
|
|
22
|
-
if (!isDevelopment) {
|
|
23
|
-
return [];
|
|
24
|
-
}
|
|
25
|
-
if (!isSupportedPlatform()) {
|
|
26
|
-
const hasLocalhostCallbacks = Object.values(callbacks).some((config) => {
|
|
27
|
-
const { isLocalhost } = isLocalhostWithPort(config.url);
|
|
28
|
-
return isLocalhost;
|
|
29
|
-
});
|
|
30
|
-
if (hasLocalhostCallbacks) {
|
|
31
|
-
console.warn(
|
|
32
|
-
`Queue 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.`
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
const localhostCallbacks = [];
|
|
38
|
-
Object.entries(callbacks).forEach(([group, config]) => {
|
|
39
|
-
const { isLocalhost, port } = isLocalhostWithPort(config.url);
|
|
40
|
-
if (isLocalhost && port && port > 0) {
|
|
41
|
-
localhostCallbacks.push({ group, config, port });
|
|
42
|
-
} else {
|
|
43
|
-
console.warn(
|
|
44
|
-
`Queue Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
return localhostCallbacks;
|
|
49
|
-
}
|
|
50
|
-
function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
|
|
51
|
-
localhostCallbacks.forEach(({ group, config, port }) => {
|
|
52
|
-
const callbackHeaders = new Headers();
|
|
53
|
-
callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
|
|
54
|
-
callbackHeaders.set("Vqs-Queue-Name", queueName);
|
|
55
|
-
callbackHeaders.set("Vqs-Consumer-Group", group);
|
|
56
|
-
fireAndForgetWaitForHttpReady(
|
|
57
|
-
config.url,
|
|
58
|
-
port,
|
|
59
|
-
config.delay || 0,
|
|
60
|
-
3,
|
|
61
|
-
// Default retry frequency
|
|
62
|
-
callbackHeaders
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
|
|
67
|
-
if (!isSupportedPlatform()) {
|
|
68
|
-
console.warn(
|
|
69
|
-
`Queue: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
|
|
70
|
-
);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
let headerArgs = "";
|
|
74
|
-
if (headers) {
|
|
75
|
-
const headerArray = [];
|
|
76
|
-
headers.forEach((value, key) => {
|
|
77
|
-
headerArray.push(`-H '${key}: ${value}'`);
|
|
78
|
-
});
|
|
79
|
-
headerArgs = headerArray.join(" ");
|
|
80
|
-
}
|
|
81
|
-
const bashScript = `
|
|
82
|
-
# Wait for any initial boot time
|
|
83
|
-
sleep ${initialDelaySeconds}
|
|
84
|
-
|
|
85
|
-
missed=0
|
|
86
|
-
while true; do
|
|
87
|
-
# 1) Check if TCP port is listening
|
|
88
|
-
if nc -z localhost ${port} 2>/dev/null; then
|
|
89
|
-
missed=0
|
|
90
|
-
# 2) If port is open, try HTTP POST check
|
|
91
|
-
if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
|
|
92
|
-
# Success: port is up AND HTTP returned 2xx (following redirects)
|
|
93
|
-
exit 0
|
|
94
|
-
fi
|
|
95
|
-
else
|
|
96
|
-
# Port was closed\u2014increment miss counter
|
|
97
|
-
((missed+=1))
|
|
98
|
-
# If closed twice in a row, give up immediately
|
|
99
|
-
if [ "$missed" -ge 2 ]; then
|
|
100
|
-
exit 1
|
|
101
|
-
fi
|
|
102
|
-
fi
|
|
103
|
-
# Wait before next cycle
|
|
104
|
-
sleep ${retryFrequencySeconds}
|
|
105
|
-
done
|
|
106
|
-
`;
|
|
107
|
-
const childProcess = spawn("bash", ["-c", bashScript], {
|
|
108
|
-
stdio: "ignore",
|
|
109
|
-
detached: true
|
|
110
|
-
});
|
|
111
|
-
childProcess.unref();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
4
|
// src/types.ts
|
|
115
5
|
var MessageNotFoundError = class extends Error {
|
|
116
6
|
constructor(messageId) {
|
|
@@ -126,12 +16,6 @@ var MessageNotAvailableError = class extends Error {
|
|
|
126
16
|
this.name = "MessageNotAvailableError";
|
|
127
17
|
}
|
|
128
18
|
};
|
|
129
|
-
var FifoOrderingViolationError = class extends Error {
|
|
130
|
-
constructor(messageId, reason) {
|
|
131
|
-
super(`FIFO ordering violation for message ${messageId}: ${reason}`);
|
|
132
|
-
this.name = "FifoOrderingViolationError";
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
19
|
var MessageCorruptedError = class extends Error {
|
|
136
20
|
constructor(messageId, reason) {
|
|
137
21
|
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
@@ -173,14 +57,6 @@ var BadRequestError = class extends Error {
|
|
|
173
57
|
this.name = "BadRequestError";
|
|
174
58
|
}
|
|
175
59
|
};
|
|
176
|
-
var FailedDependencyError = class extends Error {
|
|
177
|
-
constructor(messageId) {
|
|
178
|
-
super(
|
|
179
|
-
`Failed dependency: FIFO ordering violation for message ${messageId}`
|
|
180
|
-
);
|
|
181
|
-
this.name = "FailedDependencyError";
|
|
182
|
-
}
|
|
183
|
-
};
|
|
184
60
|
var InternalServerError = class extends Error {
|
|
185
61
|
constructor(message = "Unexpected server error") {
|
|
186
62
|
super(message);
|
|
@@ -193,12 +69,6 @@ var InvalidLimitError = class extends Error {
|
|
|
193
69
|
this.name = "InvalidLimitError";
|
|
194
70
|
}
|
|
195
71
|
};
|
|
196
|
-
var InvalidCallbackError = class extends Error {
|
|
197
|
-
constructor(message) {
|
|
198
|
-
super(message);
|
|
199
|
-
this.name = "InvalidCallbackError";
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
72
|
|
|
203
73
|
// src/client.ts
|
|
204
74
|
async function consumeStream(stream) {
|
|
@@ -295,46 +165,21 @@ var QueueClient = class _QueueClient {
|
|
|
295
165
|
* @throws {InternalServerError} When server encounters an error
|
|
296
166
|
*/
|
|
297
167
|
async sendMessage(options, transport) {
|
|
298
|
-
const { queueName, payload, idempotencyKey, retentionSeconds
|
|
168
|
+
const { queueName, payload, idempotencyKey, retentionSeconds } = options;
|
|
299
169
|
const headers = new Headers({
|
|
300
170
|
Authorization: `Bearer ${this.token}`,
|
|
301
171
|
"Vqs-Queue-Name": queueName,
|
|
302
172
|
"Content-Type": transport.contentType
|
|
303
173
|
});
|
|
174
|
+
if (process.env.VERCEL_DEPLOYMENT_ID) {
|
|
175
|
+
headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
|
|
176
|
+
}
|
|
304
177
|
if (idempotencyKey) {
|
|
305
178
|
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
306
179
|
}
|
|
307
180
|
if (retentionSeconds !== void 0) {
|
|
308
181
|
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
309
182
|
}
|
|
310
|
-
let normalizedCallbacks;
|
|
311
|
-
if (callback) {
|
|
312
|
-
if ("url" in callback && typeof callback.url === "string") {
|
|
313
|
-
normalizedCallbacks = { default: callback };
|
|
314
|
-
} else {
|
|
315
|
-
normalizedCallbacks = callback;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
let localhostCallbacks = [];
|
|
319
|
-
if (normalizedCallbacks) {
|
|
320
|
-
const isDevelopment = process.env.NODE_ENV === "development";
|
|
321
|
-
if (isDevelopment) {
|
|
322
|
-
localhostCallbacks = processDevelopmentCallbacks(normalizedCallbacks);
|
|
323
|
-
} else {
|
|
324
|
-
const endpoints = Object.entries(normalizedCallbacks).map(
|
|
325
|
-
([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
|
|
326
|
-
).join(",");
|
|
327
|
-
headers.set("Vqs-Callback-Url", endpoints);
|
|
328
|
-
const delays = Object.entries(normalizedCallbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
|
|
329
|
-
if (delays) {
|
|
330
|
-
headers.set("Vqs-Callback-Delay", delays);
|
|
331
|
-
}
|
|
332
|
-
const frequencies = Object.entries(normalizedCallbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
|
|
333
|
-
if (frequencies) {
|
|
334
|
-
headers.set("Vqs-Callback-Frequency", frequencies);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
183
|
const body = transport.serialize(payload);
|
|
339
184
|
const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
|
|
340
185
|
method: "POST",
|
|
@@ -365,9 +210,6 @@ var QueueClient = class _QueueClient {
|
|
|
365
210
|
);
|
|
366
211
|
}
|
|
367
212
|
const responseData = await response.json();
|
|
368
|
-
if (localhostCallbacks.length > 0) {
|
|
369
|
-
fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
|
|
370
|
-
}
|
|
371
213
|
return responseData;
|
|
372
214
|
}
|
|
373
215
|
/**
|
|
@@ -377,7 +219,7 @@ var QueueClient = class _QueueClient {
|
|
|
377
219
|
* @returns AsyncGenerator that yields messages as they arrive
|
|
378
220
|
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
379
221
|
* @throws {QueueEmptyError} When no messages are available (204)
|
|
380
|
-
* @throws {MessageLockedError} When
|
|
222
|
+
* @throws {MessageLockedError} When messages are temporarily locked (423)
|
|
381
223
|
* @throws {BadRequestError} When request parameters are invalid
|
|
382
224
|
* @throws {UnauthorizedError} When authentication fails
|
|
383
225
|
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
@@ -428,7 +270,7 @@ var QueueClient = class _QueueClient {
|
|
|
428
270
|
const parsed = parseInt(retryAfterHeader, 10);
|
|
429
271
|
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
430
272
|
}
|
|
431
|
-
throw new MessageLockedError("next message
|
|
273
|
+
throw new MessageLockedError("next message", retryAfter);
|
|
432
274
|
}
|
|
433
275
|
if (response.status >= 500) {
|
|
434
276
|
throw new InternalServerError(
|
|
@@ -514,9 +356,6 @@ var QueueClient = class _QueueClient {
|
|
|
514
356
|
}
|
|
515
357
|
throw new MessageLockedError(messageId, retryAfter);
|
|
516
358
|
}
|
|
517
|
-
if (response.status === 424) {
|
|
518
|
-
throw new FailedDependencyError(messageId);
|
|
519
|
-
}
|
|
520
359
|
if (response.status === 409) {
|
|
521
360
|
throw new MessageNotAvailableError(messageId);
|
|
522
361
|
}
|
|
@@ -712,24 +551,20 @@ var JsonTransport = class {
|
|
|
712
551
|
}
|
|
713
552
|
async deserialize(stream) {
|
|
714
553
|
const reader = stream.getReader();
|
|
554
|
+
let totalLength = 0;
|
|
715
555
|
const chunks = [];
|
|
716
556
|
try {
|
|
717
557
|
while (true) {
|
|
718
558
|
const { done, value } = await reader.read();
|
|
719
559
|
if (done) break;
|
|
720
560
|
chunks.push(value);
|
|
561
|
+
totalLength += value.length;
|
|
721
562
|
}
|
|
722
563
|
} finally {
|
|
723
564
|
reader.releaseLock();
|
|
724
565
|
}
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
let offset = 0;
|
|
728
|
-
for (const chunk of chunks) {
|
|
729
|
-
buffer.set(chunk, offset);
|
|
730
|
-
offset += chunk.length;
|
|
731
|
-
}
|
|
732
|
-
return JSON.parse(Buffer.from(buffer).toString("utf8"));
|
|
566
|
+
const buffer = Buffer.concat(chunks, totalLength);
|
|
567
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
733
568
|
}
|
|
734
569
|
};
|
|
735
570
|
var BufferTransport = class {
|
|
@@ -1011,8 +846,7 @@ var Topic = class {
|
|
|
1011
846
|
queueName: this.topicName,
|
|
1012
847
|
payload,
|
|
1013
848
|
idempotencyKey: options?.idempotencyKey,
|
|
1014
|
-
retentionSeconds: options?.retentionSeconds
|
|
1015
|
-
callback: options?.callback
|
|
849
|
+
retentionSeconds: options?.retentionSeconds
|
|
1016
850
|
},
|
|
1017
851
|
this.transport
|
|
1018
852
|
);
|
|
@@ -1063,8 +897,7 @@ async function send(topicName, payload, options) {
|
|
|
1063
897
|
queueName: topicName,
|
|
1064
898
|
payload,
|
|
1065
899
|
idempotencyKey: options?.idempotencyKey,
|
|
1066
|
-
retentionSeconds: options?.retentionSeconds
|
|
1067
|
-
callback: options?.callback
|
|
900
|
+
retentionSeconds: options?.retentionSeconds
|
|
1068
901
|
},
|
|
1069
902
|
transport
|
|
1070
903
|
);
|
|
@@ -1091,23 +924,22 @@ async function receive(topicName, consumerGroup, handler, options) {
|
|
|
1091
924
|
|
|
1092
925
|
// src/callback.ts
|
|
1093
926
|
function parseCallbackRequest(request) {
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
`Missing required queue callback headers: ${missingHeaders.join(", ")}`
|
|
927
|
+
const queueName = request.headers.get("Vqs-Queue-Name");
|
|
928
|
+
const consumerGroup = request.headers.get("Vqs-Consumer-Group");
|
|
929
|
+
const messageId = request.headers.get("Vqs-Message-Id");
|
|
930
|
+
if (!queueName || !consumerGroup || !messageId) {
|
|
931
|
+
const missingHeaders = [];
|
|
932
|
+
if (!queueName) missingHeaders.push("Vqs-Queue-Name");
|
|
933
|
+
if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
|
|
934
|
+
if (!messageId) missingHeaders.push("Vqs-Message-Id");
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Missing required queue headers: ${missingHeaders.join(", ")}`
|
|
1105
937
|
);
|
|
1106
938
|
}
|
|
1107
939
|
return {
|
|
1108
|
-
messageId,
|
|
1109
940
|
queueName,
|
|
1110
|
-
consumerGroup
|
|
941
|
+
consumerGroup,
|
|
942
|
+
messageId
|
|
1111
943
|
};
|
|
1112
944
|
}
|
|
1113
945
|
function handleCallback(handlers) {
|
|
@@ -1116,41 +948,38 @@ function handleCallback(handlers) {
|
|
|
1116
948
|
const { queueName, consumerGroup, messageId } = parseCallbackRequest(request);
|
|
1117
949
|
const topicHandler = handlers[queueName];
|
|
1118
950
|
if (!topicHandler) {
|
|
1119
|
-
|
|
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
|
+
);
|
|
1120
959
|
}
|
|
1121
|
-
|
|
1122
|
-
if (
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (!consumerGroupHandler) {
|
|
1132
|
-
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1133
|
-
throw new Error(
|
|
1134
|
-
`No handler found for consumer group "${consumerGroup}" in topic "${queueName}". Available groups: ${availableGroups}`
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1137
|
-
actualHandler = consumerGroupHandler;
|
|
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
|
+
);
|
|
1138
970
|
}
|
|
1139
971
|
const client = new QueueClient();
|
|
1140
972
|
const topic = new Topic(client, queueName);
|
|
1141
973
|
const cg = topic.consumerGroup(consumerGroup);
|
|
1142
|
-
await cg.consume(
|
|
974
|
+
await cg.consume(consumerGroupHandler, { messageId });
|
|
1143
975
|
return Response.json({ status: "success" });
|
|
1144
976
|
} catch (error) {
|
|
1145
|
-
console.error("
|
|
1146
|
-
if (error instanceof
|
|
1147
|
-
return Response.json(
|
|
1148
|
-
{ error: "Invalid callback request" },
|
|
1149
|
-
{ status: 400 }
|
|
1150
|
-
);
|
|
977
|
+
console.error("Queue callback error:", error);
|
|
978
|
+
if (error instanceof Error && error.message.includes("Missing required queue headers")) {
|
|
979
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
1151
980
|
}
|
|
1152
981
|
return Response.json(
|
|
1153
|
-
{ error: "Failed to process
|
|
982
|
+
{ error: "Failed to process queue message" },
|
|
1154
983
|
{ status: 500 }
|
|
1155
984
|
);
|
|
1156
985
|
}
|
|
@@ -1160,11 +989,8 @@ export {
|
|
|
1160
989
|
BadRequestError,
|
|
1161
990
|
BufferTransport,
|
|
1162
991
|
ConsumerGroup,
|
|
1163
|
-
FailedDependencyError,
|
|
1164
|
-
FifoOrderingViolationError,
|
|
1165
992
|
ForbiddenError,
|
|
1166
993
|
InternalServerError,
|
|
1167
|
-
InvalidCallbackError,
|
|
1168
994
|
InvalidLimitError,
|
|
1169
995
|
JsonTransport,
|
|
1170
996
|
MessageCorruptedError,
|