@vercel/queue 0.0.0-alpha.3 → 0.0.0-alpha.30
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 +288 -800
- package/bin/local-discover.js +196 -0
- package/dist/index.d.mts +101 -717
- package/dist/index.d.ts +101 -717
- package/dist/index.js +490 -603
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +486 -593
- package/dist/index.mjs.map +1 -1
- package/dist/pages.d.mts +47 -0
- package/dist/pages.d.ts +47 -0
- package/dist/pages.js +1250 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +1223 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/types-JvOenjfT.d.mts +256 -0
- package/dist/types-JvOenjfT.d.ts +256 -0
- package/package.json +23 -6
package/dist/index.mjs
CHANGED
|
@@ -1,129 +1,71 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
async function
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
|
|
7
|
-
if (!token) {
|
|
8
|
-
throw new Error(
|
|
9
|
-
`The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`
|
|
10
|
-
);
|
|
11
|
-
}
|
|
12
|
-
return token;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// src/client.ts
|
|
16
|
-
import { parseMultipartStream } from "mixpart";
|
|
17
|
-
|
|
18
|
-
// src/local.ts
|
|
19
|
-
import { spawn } from "child_process";
|
|
20
|
-
function isLocalhostWithPort(url) {
|
|
1
|
+
// src/transports.ts
|
|
2
|
+
async function streamToBuffer(stream) {
|
|
3
|
+
let totalLength = 0;
|
|
4
|
+
const reader = stream.getReader();
|
|
5
|
+
const chunks = [];
|
|
21
6
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
7
|
+
while (true) {
|
|
8
|
+
const { done, value } = await reader.read();
|
|
9
|
+
if (done) break;
|
|
10
|
+
chunks.push(value);
|
|
11
|
+
totalLength += value.length;
|
|
12
|
+
}
|
|
13
|
+
} finally {
|
|
14
|
+
reader.releaseLock();
|
|
28
15
|
}
|
|
16
|
+
return Buffer.concat(chunks, totalLength);
|
|
29
17
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return [];
|
|
18
|
+
var JsonTransport = class {
|
|
19
|
+
contentType = "application/json";
|
|
20
|
+
replacer;
|
|
21
|
+
reviver;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.replacer = options.replacer;
|
|
24
|
+
this.reviver = options.reviver;
|
|
38
25
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const { isLocalhost } = isLocalhostWithPort(config.url);
|
|
42
|
-
return isLocalhost;
|
|
43
|
-
});
|
|
44
|
-
if (hasLocalhostCallbacks) {
|
|
45
|
-
console.warn(
|
|
46
|
-
`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.`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return [];
|
|
26
|
+
serialize(value) {
|
|
27
|
+
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
50
28
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (isLocalhost && port && port > 0) {
|
|
55
|
-
localhostCallbacks.push({ group, config, port });
|
|
56
|
-
} else {
|
|
57
|
-
console.warn(
|
|
58
|
-
`Queue Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
return localhostCallbacks;
|
|
63
|
-
}
|
|
64
|
-
function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
|
|
65
|
-
localhostCallbacks.forEach(({ group, config, port }) => {
|
|
66
|
-
const callbackHeaders = new Headers();
|
|
67
|
-
callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
|
|
68
|
-
callbackHeaders.set("Vqs-Queue-Name", queueName);
|
|
69
|
-
callbackHeaders.set("Vqs-Consumer-Group", group);
|
|
70
|
-
fireAndForgetWaitForHttpReady(
|
|
71
|
-
config.url,
|
|
72
|
-
port,
|
|
73
|
-
config.delay || 0,
|
|
74
|
-
3,
|
|
75
|
-
// Default retry frequency
|
|
76
|
-
callbackHeaders
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
|
|
81
|
-
if (!isSupportedPlatform()) {
|
|
82
|
-
console.warn(
|
|
83
|
-
`Queue: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
|
|
84
|
-
);
|
|
85
|
-
return;
|
|
29
|
+
async deserialize(stream) {
|
|
30
|
+
const buffer = await streamToBuffer(stream);
|
|
31
|
+
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
86
32
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
33
|
+
};
|
|
34
|
+
var BufferTransport = class {
|
|
35
|
+
contentType = "application/octet-stream";
|
|
36
|
+
serialize(value) {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
async deserialize(stream) {
|
|
40
|
+
return await streamToBuffer(stream);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var StreamTransport = class {
|
|
44
|
+
contentType = "application/octet-stream";
|
|
45
|
+
serialize(value) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
async deserialize(stream) {
|
|
49
|
+
return stream;
|
|
94
50
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
51
|
+
async finalize(payload) {
|
|
52
|
+
const reader = payload.getReader();
|
|
53
|
+
try {
|
|
54
|
+
while (true) {
|
|
55
|
+
const { done } = await reader.read();
|
|
56
|
+
if (done) break;
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
reader.releaseLock();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
98
63
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# 2) If port is open, try HTTP POST check
|
|
105
|
-
if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
|
|
106
|
-
# Success: port is up AND HTTP returned 2xx (following redirects)
|
|
107
|
-
exit 0
|
|
108
|
-
fi
|
|
109
|
-
else
|
|
110
|
-
# Port was closed\u2014increment miss counter
|
|
111
|
-
((missed+=1))
|
|
112
|
-
# If closed twice in a row, give up immediately
|
|
113
|
-
if [ "$missed" -ge 2 ]; then
|
|
114
|
-
exit 1
|
|
115
|
-
fi
|
|
116
|
-
fi
|
|
117
|
-
# Wait before next cycle
|
|
118
|
-
sleep ${retryFrequencySeconds}
|
|
119
|
-
done
|
|
120
|
-
`;
|
|
121
|
-
const childProcess = spawn("bash", ["-c", bashScript], {
|
|
122
|
-
stdio: "ignore",
|
|
123
|
-
detached: true
|
|
124
|
-
});
|
|
125
|
-
childProcess.unref();
|
|
126
|
-
}
|
|
64
|
+
// src/client.ts
|
|
65
|
+
import { parseMultipartStream } from "mixpart";
|
|
66
|
+
|
|
67
|
+
// src/oidc.ts
|
|
68
|
+
import { getVercelOidcToken } from "@vercel/oidc";
|
|
127
69
|
|
|
128
70
|
// src/types.ts
|
|
129
71
|
var MessageNotFoundError = class extends Error {
|
|
@@ -140,16 +82,6 @@ var MessageNotAvailableError = class extends Error {
|
|
|
140
82
|
this.name = "MessageNotAvailableError";
|
|
141
83
|
}
|
|
142
84
|
};
|
|
143
|
-
var FifoOrderingViolationError = class extends Error {
|
|
144
|
-
nextMessageId;
|
|
145
|
-
constructor(messageId, nextMessageId, reason) {
|
|
146
|
-
super(
|
|
147
|
-
`FIFO ordering violation for message ${messageId}: ${reason}. Process message ${nextMessageId} first.`
|
|
148
|
-
);
|
|
149
|
-
this.name = "FifoOrderingViolationError";
|
|
150
|
-
this.nextMessageId = nextMessageId;
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
85
|
var MessageCorruptedError = class extends Error {
|
|
154
86
|
constructor(messageId, reason) {
|
|
155
87
|
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
@@ -191,16 +123,6 @@ var BadRequestError = class extends Error {
|
|
|
191
123
|
this.name = "BadRequestError";
|
|
192
124
|
}
|
|
193
125
|
};
|
|
194
|
-
var FailedDependencyError = class extends Error {
|
|
195
|
-
nextMessageId;
|
|
196
|
-
constructor(messageId, nextMessageId) {
|
|
197
|
-
super(
|
|
198
|
-
`Failed dependency: FIFO ordering violation for message ${messageId}. Must process message ${nextMessageId} first.`
|
|
199
|
-
);
|
|
200
|
-
this.name = "FailedDependencyError";
|
|
201
|
-
this.nextMessageId = nextMessageId;
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
126
|
var InternalServerError = class extends Error {
|
|
205
127
|
constructor(message = "Unexpected server error") {
|
|
206
128
|
super(message);
|
|
@@ -213,12 +135,6 @@ var InvalidLimitError = class extends Error {
|
|
|
213
135
|
this.name = "InvalidLimitError";
|
|
214
136
|
}
|
|
215
137
|
};
|
|
216
|
-
var InvalidCallbackError = class extends Error {
|
|
217
|
-
constructor(message) {
|
|
218
|
-
super(message);
|
|
219
|
-
this.name = "InvalidCallbackError";
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
138
|
|
|
223
139
|
// src/client.ts
|
|
224
140
|
async function consumeStream(stream) {
|
|
@@ -248,40 +164,39 @@ function parseQueueHeaders(headers) {
|
|
|
248
164
|
return {
|
|
249
165
|
messageId,
|
|
250
166
|
deliveryCount,
|
|
251
|
-
timestamp,
|
|
167
|
+
createdAt: new Date(timestamp),
|
|
252
168
|
contentType,
|
|
253
169
|
ticket
|
|
254
170
|
};
|
|
255
171
|
}
|
|
256
|
-
var QueueClient = class
|
|
172
|
+
var QueueClient = class {
|
|
257
173
|
baseUrl;
|
|
258
|
-
|
|
174
|
+
basePath;
|
|
175
|
+
customHeaders = {};
|
|
259
176
|
/**
|
|
260
177
|
* Create a new Vercel Queue Service client
|
|
261
178
|
* @param options Client configuration options
|
|
262
179
|
*/
|
|
263
|
-
constructor(options) {
|
|
264
|
-
this.baseUrl = options.baseUrl || "https://
|
|
265
|
-
this.
|
|
180
|
+
constructor(options = {}) {
|
|
181
|
+
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
182
|
+
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
|
|
183
|
+
const VERCEL_QUEUE_HEADER_PREFIX = "VERCEL_QUEUE_HEADER_";
|
|
184
|
+
this.customHeaders = Object.fromEntries(
|
|
185
|
+
Object.entries(process.env).filter(([key]) => key.startsWith(VERCEL_QUEUE_HEADER_PREFIX)).map(([key, value]) => [
|
|
186
|
+
// This allows headers to use dashes independent of shell used
|
|
187
|
+
key.replace(VERCEL_QUEUE_HEADER_PREFIX, "").replaceAll("__", "-"),
|
|
188
|
+
value || ""
|
|
189
|
+
])
|
|
190
|
+
);
|
|
266
191
|
}
|
|
267
|
-
|
|
268
|
-
* Create a QueueClient automatically configured for Vercel Functions
|
|
269
|
-
* This method automatically retrieves the OIDC token from the Vercel Function environment
|
|
270
|
-
* Always creates a fresh instance since OIDC tokens expire after 15 minutes
|
|
271
|
-
* @param baseUrl Optional base URL override
|
|
272
|
-
* @returns Promise resolving to a new QueueClient instance
|
|
273
|
-
*/
|
|
274
|
-
static async fromVercelFunction(baseUrl) {
|
|
192
|
+
async getToken() {
|
|
275
193
|
const token = await getVercelOidcToken();
|
|
276
194
|
if (!token) {
|
|
277
195
|
throw new Error(
|
|
278
|
-
"Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment."
|
|
196
|
+
"Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment, or provide a token explicitly.\n\nTo set up your environment:\n1. Link your project: 'vercel link'\n2. Pull environment variables: 'vercel env pull'\n3. Run with environment: 'dotenv -e .env.local -- your-command'"
|
|
279
197
|
);
|
|
280
198
|
}
|
|
281
|
-
return
|
|
282
|
-
token,
|
|
283
|
-
baseUrl
|
|
284
|
-
});
|
|
199
|
+
return token;
|
|
285
200
|
}
|
|
286
201
|
/**
|
|
287
202
|
* Send a message to a queue
|
|
@@ -294,51 +209,28 @@ var QueueClient = class _QueueClient {
|
|
|
294
209
|
* @throws {InternalServerError} When server encounters an error
|
|
295
210
|
*/
|
|
296
211
|
async sendMessage(options, transport) {
|
|
297
|
-
const { queueName, payload, idempotencyKey, retentionSeconds
|
|
212
|
+
const { queueName, payload, idempotencyKey, retentionSeconds } = options;
|
|
298
213
|
const headers = new Headers({
|
|
299
|
-
Authorization: `Bearer ${this.
|
|
214
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
300
215
|
"Vqs-Queue-Name": queueName,
|
|
301
|
-
"Content-Type": transport.contentType
|
|
216
|
+
"Content-Type": transport.contentType,
|
|
217
|
+
...this.customHeaders
|
|
302
218
|
});
|
|
219
|
+
const deploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
220
|
+
if (deploymentId) {
|
|
221
|
+
headers.set("Vqs-Deployment-Id", deploymentId);
|
|
222
|
+
}
|
|
303
223
|
if (idempotencyKey) {
|
|
304
224
|
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
305
225
|
}
|
|
306
226
|
if (retentionSeconds !== void 0) {
|
|
307
227
|
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
308
228
|
}
|
|
309
|
-
let normalizedCallbacks;
|
|
310
|
-
if (callback) {
|
|
311
|
-
if ("url" in callback && typeof callback.url === "string") {
|
|
312
|
-
normalizedCallbacks = { default: callback };
|
|
313
|
-
} else {
|
|
314
|
-
normalizedCallbacks = callback;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
let localhostCallbacks = [];
|
|
318
|
-
if (normalizedCallbacks) {
|
|
319
|
-
const isDevelopment = process.env.NODE_ENV === "development";
|
|
320
|
-
if (isDevelopment) {
|
|
321
|
-
localhostCallbacks = processDevelopmentCallbacks(normalizedCallbacks);
|
|
322
|
-
} else {
|
|
323
|
-
const endpoints = Object.entries(normalizedCallbacks).map(
|
|
324
|
-
([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
|
|
325
|
-
).join(",");
|
|
326
|
-
headers.set("Vqs-Callback-Url", endpoints);
|
|
327
|
-
const delays = Object.entries(normalizedCallbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
|
|
328
|
-
if (delays) {
|
|
329
|
-
headers.set("Vqs-Callback-Delay", delays);
|
|
330
|
-
}
|
|
331
|
-
const frequencies = Object.entries(normalizedCallbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
|
|
332
|
-
if (frequencies) {
|
|
333
|
-
headers.set("Vqs-Callback-Frequency", frequencies);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
229
|
const body = transport.serialize(payload);
|
|
338
|
-
const response = await fetch(`${this.baseUrl}
|
|
230
|
+
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
339
231
|
method: "POST",
|
|
340
|
-
|
|
341
|
-
|
|
232
|
+
body,
|
|
233
|
+
headers
|
|
342
234
|
});
|
|
343
235
|
if (!response.ok) {
|
|
344
236
|
if (response.status === 400) {
|
|
@@ -364,9 +256,6 @@ var QueueClient = class _QueueClient {
|
|
|
364
256
|
);
|
|
365
257
|
}
|
|
366
258
|
const responseData = await response.json();
|
|
367
|
-
if (localhostCallbacks.length > 0) {
|
|
368
|
-
fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
|
|
369
|
-
}
|
|
370
259
|
return responseData;
|
|
371
260
|
}
|
|
372
261
|
/**
|
|
@@ -376,7 +265,7 @@ var QueueClient = class _QueueClient {
|
|
|
376
265
|
* @returns AsyncGenerator that yields messages as they arrive
|
|
377
266
|
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
378
267
|
* @throws {QueueEmptyError} When no messages are available (204)
|
|
379
|
-
* @throws {MessageLockedError} When
|
|
268
|
+
* @throws {MessageLockedError} When messages are temporarily locked (423)
|
|
380
269
|
* @throws {BadRequestError} When request parameters are invalid
|
|
381
270
|
* @throws {UnauthorizedError} When authentication fails
|
|
382
271
|
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
@@ -388,10 +277,11 @@ var QueueClient = class _QueueClient {
|
|
|
388
277
|
throw new InvalidLimitError(limit);
|
|
389
278
|
}
|
|
390
279
|
const headers = new Headers({
|
|
391
|
-
Authorization: `Bearer ${this.
|
|
280
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
392
281
|
"Vqs-Queue-Name": queueName,
|
|
393
282
|
"Vqs-Consumer-Group": consumerGroup,
|
|
394
|
-
Accept: "multipart/mixed"
|
|
283
|
+
Accept: "multipart/mixed",
|
|
284
|
+
...this.customHeaders
|
|
395
285
|
});
|
|
396
286
|
if (visibilityTimeoutSeconds !== void 0) {
|
|
397
287
|
headers.set(
|
|
@@ -402,7 +292,7 @@ var QueueClient = class _QueueClient {
|
|
|
402
292
|
if (limit !== void 0) {
|
|
403
293
|
headers.set("Vqs-Limit", limit.toString());
|
|
404
294
|
}
|
|
405
|
-
const response = await fetch(`${this.baseUrl}
|
|
295
|
+
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
406
296
|
method: "GET",
|
|
407
297
|
headers
|
|
408
298
|
});
|
|
@@ -427,7 +317,7 @@ var QueueClient = class _QueueClient {
|
|
|
427
317
|
const parsed = parseInt(retryAfterHeader, 10);
|
|
428
318
|
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
429
319
|
}
|
|
430
|
-
throw new MessageLockedError("next message
|
|
320
|
+
throw new MessageLockedError("next message", retryAfter);
|
|
431
321
|
}
|
|
432
322
|
if (response.status >= 500) {
|
|
433
323
|
throw new InternalServerError(
|
|
@@ -469,10 +359,11 @@ var QueueClient = class _QueueClient {
|
|
|
469
359
|
skipPayload
|
|
470
360
|
} = options;
|
|
471
361
|
const headers = new Headers({
|
|
472
|
-
Authorization: `Bearer ${this.
|
|
362
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
473
363
|
"Vqs-Queue-Name": queueName,
|
|
474
364
|
"Vqs-Consumer-Group": consumerGroup,
|
|
475
|
-
Accept: "multipart/mixed"
|
|
365
|
+
Accept: "multipart/mixed",
|
|
366
|
+
...this.customHeaders
|
|
476
367
|
});
|
|
477
368
|
if (visibilityTimeoutSeconds !== void 0) {
|
|
478
369
|
headers.set(
|
|
@@ -484,7 +375,7 @@ var QueueClient = class _QueueClient {
|
|
|
484
375
|
headers.set("Vqs-Skip-Payload", "1");
|
|
485
376
|
}
|
|
486
377
|
const response = await fetch(
|
|
487
|
-
`${this.baseUrl}
|
|
378
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
488
379
|
{
|
|
489
380
|
method: "GET",
|
|
490
381
|
headers
|
|
@@ -513,37 +404,7 @@ var QueueClient = class _QueueClient {
|
|
|
513
404
|
}
|
|
514
405
|
throw new MessageLockedError(messageId, retryAfter);
|
|
515
406
|
}
|
|
516
|
-
if (response.status === 424) {
|
|
517
|
-
try {
|
|
518
|
-
const errorData = await response.json();
|
|
519
|
-
if (errorData.meta?.nextMessageId) {
|
|
520
|
-
throw new FailedDependencyError(
|
|
521
|
-
messageId,
|
|
522
|
-
errorData.meta.nextMessageId
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
} catch (parseError) {
|
|
526
|
-
if (parseError instanceof FailedDependencyError) {
|
|
527
|
-
throw parseError;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
throw new MessageNotAvailableError(
|
|
531
|
-
messageId,
|
|
532
|
-
"FIFO ordering violation"
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
407
|
if (response.status === 409) {
|
|
536
|
-
try {
|
|
537
|
-
const errorData = await response.json();
|
|
538
|
-
if (errorData.nextMessageId) {
|
|
539
|
-
throw new FifoOrderingViolationError(
|
|
540
|
-
messageId,
|
|
541
|
-
errorData.nextMessageId,
|
|
542
|
-
errorData.error
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
} catch (parseError) {
|
|
546
|
-
}
|
|
547
408
|
throw new MessageNotAvailableError(messageId);
|
|
548
409
|
}
|
|
549
410
|
if (response.status >= 500) {
|
|
@@ -623,14 +484,15 @@ var QueueClient = class _QueueClient {
|
|
|
623
484
|
async deleteMessage(options) {
|
|
624
485
|
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
625
486
|
const response = await fetch(
|
|
626
|
-
`${this.baseUrl}
|
|
487
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
627
488
|
{
|
|
628
489
|
method: "DELETE",
|
|
629
490
|
headers: new Headers({
|
|
630
|
-
Authorization: `Bearer ${this.
|
|
491
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
631
492
|
"Vqs-Queue-Name": queueName,
|
|
632
493
|
"Vqs-Consumer-Group": consumerGroup,
|
|
633
|
-
"Vqs-Ticket": ticket
|
|
494
|
+
"Vqs-Ticket": ticket,
|
|
495
|
+
...this.customHeaders
|
|
634
496
|
})
|
|
635
497
|
}
|
|
636
498
|
);
|
|
@@ -684,15 +546,16 @@ var QueueClient = class _QueueClient {
|
|
|
684
546
|
visibilityTimeoutSeconds
|
|
685
547
|
} = options;
|
|
686
548
|
const response = await fetch(
|
|
687
|
-
`${this.baseUrl}
|
|
549
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
688
550
|
{
|
|
689
551
|
method: "PATCH",
|
|
690
552
|
headers: new Headers({
|
|
691
|
-
Authorization: `Bearer ${this.
|
|
553
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
692
554
|
"Vqs-Queue-Name": queueName,
|
|
693
555
|
"Vqs-Consumer-Group": consumerGroup,
|
|
694
556
|
"Vqs-Ticket": ticket,
|
|
695
|
-
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString()
|
|
557
|
+
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
|
|
558
|
+
...this.customHeaders
|
|
696
559
|
})
|
|
697
560
|
}
|
|
698
561
|
);
|
|
@@ -730,81 +593,303 @@ var QueueClient = class _QueueClient {
|
|
|
730
593
|
}
|
|
731
594
|
};
|
|
732
595
|
|
|
733
|
-
// src/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
596
|
+
// src/callback.ts
|
|
597
|
+
function validateWildcardPattern(pattern) {
|
|
598
|
+
const firstIndex = pattern.indexOf("*");
|
|
599
|
+
const lastIndex = pattern.lastIndexOf("*");
|
|
600
|
+
if (firstIndex !== lastIndex) {
|
|
601
|
+
return false;
|
|
738
602
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
603
|
+
if (firstIndex === -1) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
if (firstIndex !== pattern.length - 1) {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
612
|
+
const prefix = pattern.slice(0, -1);
|
|
613
|
+
return topicName.startsWith(prefix);
|
|
614
|
+
}
|
|
615
|
+
function findTopicHandler(queueName, handlers) {
|
|
616
|
+
const exactHandler = handlers[queueName];
|
|
617
|
+
if (exactHandler) {
|
|
618
|
+
return exactHandler;
|
|
619
|
+
}
|
|
620
|
+
for (const pattern in handlers) {
|
|
621
|
+
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
622
|
+
return handlers[pattern];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
async function parseCallback(request) {
|
|
628
|
+
const contentType = request.headers.get("content-type");
|
|
629
|
+
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
630
|
+
throw new Error(
|
|
631
|
+
"Invalid content type: expected 'application/cloudevents+json'"
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
let cloudEvent;
|
|
635
|
+
try {
|
|
636
|
+
cloudEvent = await request.json();
|
|
637
|
+
} catch (error) {
|
|
638
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
639
|
+
}
|
|
640
|
+
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
641
|
+
throw new Error("Invalid CloudEvent: missing required fields");
|
|
642
|
+
}
|
|
643
|
+
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
644
|
+
throw new Error(
|
|
645
|
+
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const missingFields = [];
|
|
649
|
+
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
650
|
+
if (!("consumerGroup" in cloudEvent.data))
|
|
651
|
+
missingFields.push("consumerGroup");
|
|
652
|
+
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
653
|
+
if (missingFields.length > 0) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
659
|
+
return {
|
|
660
|
+
queueName,
|
|
661
|
+
consumerGroup,
|
|
662
|
+
messageId
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function handleCallback(handlers) {
|
|
666
|
+
for (const topicPattern in handlers) {
|
|
667
|
+
if (topicPattern.includes("*")) {
|
|
668
|
+
if (!validateWildcardPattern(topicPattern)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
671
|
+
);
|
|
747
672
|
}
|
|
748
|
-
} finally {
|
|
749
|
-
reader.releaseLock();
|
|
750
673
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
674
|
+
}
|
|
675
|
+
const routeHandler = async (request) => {
|
|
676
|
+
try {
|
|
677
|
+
const { queueName, consumerGroup, messageId } = await parseCallback(request);
|
|
678
|
+
const topicHandler = findTopicHandler(queueName, handlers);
|
|
679
|
+
if (!topicHandler) {
|
|
680
|
+
const availableTopics = Object.keys(handlers).join(", ");
|
|
681
|
+
return Response.json(
|
|
682
|
+
{
|
|
683
|
+
error: `No handler found for topic: ${queueName}`,
|
|
684
|
+
availableTopics
|
|
685
|
+
},
|
|
686
|
+
{ status: 404 }
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
690
|
+
if (!consumerGroupHandler) {
|
|
691
|
+
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
692
|
+
return Response.json(
|
|
693
|
+
{
|
|
694
|
+
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
695
|
+
availableGroups
|
|
696
|
+
},
|
|
697
|
+
{ status: 404 }
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const client = new QueueClient();
|
|
701
|
+
const topic = new Topic(client, queueName);
|
|
702
|
+
const cg = topic.consumerGroup(consumerGroup);
|
|
703
|
+
await cg.consume(consumerGroupHandler, { messageId });
|
|
704
|
+
return Response.json({ status: "success" });
|
|
705
|
+
} catch (error) {
|
|
706
|
+
console.error("Queue callback error:", error);
|
|
707
|
+
if (error instanceof Error && (error.message.includes("Missing required CloudEvent data fields") || error.message.includes("Invalid CloudEvent") || error.message.includes("Invalid CloudEvent type") || error.message.includes("Invalid content type") || error.message.includes("Failed to parse CloudEvent"))) {
|
|
708
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
709
|
+
}
|
|
710
|
+
return Response.json(
|
|
711
|
+
{ error: "Failed to process queue message" },
|
|
712
|
+
{ status: 500 }
|
|
713
|
+
);
|
|
757
714
|
}
|
|
758
|
-
|
|
715
|
+
};
|
|
716
|
+
if (isDevMode()) {
|
|
717
|
+
registerDevRouteHandler(routeHandler, handlers);
|
|
759
718
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
719
|
+
return routeHandler;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/dev.ts
|
|
723
|
+
var devRouteHandlers = /* @__PURE__ */ new Map();
|
|
724
|
+
var wildcardRouteHandlers = /* @__PURE__ */ new Map();
|
|
725
|
+
function cleanupDeadRefs(key, refs) {
|
|
726
|
+
const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
|
|
727
|
+
if (aliveRefs.length === 0) {
|
|
728
|
+
wildcardRouteHandlers.delete(key);
|
|
729
|
+
} else if (aliveRefs.length < refs.length) {
|
|
730
|
+
wildcardRouteHandlers.set(key, aliveRefs);
|
|
765
731
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
732
|
+
}
|
|
733
|
+
function isDevMode() {
|
|
734
|
+
return process.env.NODE_ENV === "development";
|
|
735
|
+
}
|
|
736
|
+
function registerDevRouteHandler(routeHandler, handlers) {
|
|
737
|
+
for (const topicName in handlers) {
|
|
738
|
+
for (const consumerGroup in handlers[topicName]) {
|
|
739
|
+
const key = `${topicName}:${consumerGroup}`;
|
|
740
|
+
if (topicName.includes("*")) {
|
|
741
|
+
const existing = wildcardRouteHandlers.get(key) || [];
|
|
742
|
+
cleanupDeadRefs(key, existing);
|
|
743
|
+
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
744
|
+
const weakRef = new WeakRef(routeHandler);
|
|
745
|
+
cleanedRefs.push(weakRef);
|
|
746
|
+
wildcardRouteHandlers.set(key, cleanedRefs);
|
|
747
|
+
} else {
|
|
748
|
+
devRouteHandlers.set(key, {
|
|
749
|
+
routeHandler,
|
|
750
|
+
topicPattern: topicName
|
|
751
|
+
});
|
|
774
752
|
}
|
|
775
|
-
} finally {
|
|
776
|
-
reader.releaseLock();
|
|
777
753
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function findRouteHandlersForTopic(topicName) {
|
|
757
|
+
const handlersMap = /* @__PURE__ */ new Map();
|
|
758
|
+
for (const [
|
|
759
|
+
key,
|
|
760
|
+
{ routeHandler, topicPattern }
|
|
761
|
+
] of devRouteHandlers.entries()) {
|
|
762
|
+
const [_, consumerGroup] = key.split(":");
|
|
763
|
+
if (topicPattern === topicName) {
|
|
764
|
+
if (!handlersMap.has(routeHandler)) {
|
|
765
|
+
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
766
|
+
}
|
|
767
|
+
handlersMap.get(routeHandler).add(consumerGroup);
|
|
784
768
|
}
|
|
785
|
-
return Buffer.from(buffer);
|
|
786
769
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
770
|
+
for (const [key, refs] of wildcardRouteHandlers.entries()) {
|
|
771
|
+
const [pattern, consumerGroup] = key.split(":");
|
|
772
|
+
if (matchesWildcardPattern(topicName, pattern)) {
|
|
773
|
+
cleanupDeadRefs(key, refs);
|
|
774
|
+
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
775
|
+
for (const ref of cleanedRefs) {
|
|
776
|
+
const routeHandler = ref.deref();
|
|
777
|
+
if (routeHandler) {
|
|
778
|
+
if (!handlersMap.has(routeHandler)) {
|
|
779
|
+
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
780
|
+
}
|
|
781
|
+
handlersMap.get(routeHandler).add(consumerGroup);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
792
785
|
}
|
|
793
|
-
|
|
794
|
-
|
|
786
|
+
return handlersMap;
|
|
787
|
+
}
|
|
788
|
+
function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
|
|
789
|
+
const cloudEvent = {
|
|
790
|
+
type: "com.vercel.queue.v1beta",
|
|
791
|
+
source: `/topic/${topicName}/consumer/${consumerGroup}`,
|
|
792
|
+
id: messageId,
|
|
793
|
+
datacontenttype: "application/json",
|
|
794
|
+
data: {
|
|
795
|
+
messageId,
|
|
796
|
+
queueName: topicName,
|
|
797
|
+
consumerGroup
|
|
798
|
+
},
|
|
799
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
800
|
+
specversion: "1.0"
|
|
801
|
+
};
|
|
802
|
+
return new Request("https://localhost/api/queue/callback", {
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: {
|
|
805
|
+
"Content-Type": "application/cloudevents+json"
|
|
806
|
+
},
|
|
807
|
+
body: JSON.stringify(cloudEvent)
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
var DEV_CALLBACK_DELAY = 1e3;
|
|
811
|
+
function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
|
|
812
|
+
console.log(
|
|
813
|
+
`[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
|
|
814
|
+
);
|
|
815
|
+
setTimeout(
|
|
816
|
+
() => {
|
|
817
|
+
console.log(
|
|
818
|
+
`[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
|
|
819
|
+
);
|
|
820
|
+
triggerDevCallbacks(topicName, messageId);
|
|
821
|
+
},
|
|
822
|
+
timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
function triggerDevCallbacks(topicName, messageId) {
|
|
826
|
+
const handlersMap = findRouteHandlersForTopic(topicName);
|
|
827
|
+
if (handlersMap.size === 0) {
|
|
828
|
+
return;
|
|
795
829
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
830
|
+
const consumerGroups = Array.from(
|
|
831
|
+
new Set(
|
|
832
|
+
Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
|
|
833
|
+
)
|
|
834
|
+
);
|
|
835
|
+
console.log(
|
|
836
|
+
`[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
|
|
837
|
+
);
|
|
838
|
+
setTimeout(async () => {
|
|
839
|
+
for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
|
|
840
|
+
for (const consumerGroup of consumerGroups2) {
|
|
841
|
+
try {
|
|
842
|
+
const request = createMockCloudEventRequest(
|
|
843
|
+
topicName,
|
|
844
|
+
consumerGroup,
|
|
845
|
+
messageId
|
|
846
|
+
);
|
|
847
|
+
const response = await routeHandler(request);
|
|
848
|
+
if (response.ok) {
|
|
849
|
+
try {
|
|
850
|
+
const responseData = await response.json();
|
|
851
|
+
if (responseData.status === "success") {
|
|
852
|
+
console.log(
|
|
853
|
+
`[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
} catch (jsonError) {
|
|
857
|
+
console.error(
|
|
858
|
+
`[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
|
|
859
|
+
jsonError
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
} else {
|
|
863
|
+
try {
|
|
864
|
+
const errorData = await response.json();
|
|
865
|
+
console.error(
|
|
866
|
+
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
867
|
+
errorData.error || response.statusText
|
|
868
|
+
);
|
|
869
|
+
} catch (jsonError) {
|
|
870
|
+
console.error(
|
|
871
|
+
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
872
|
+
response.statusText
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
} catch (error) {
|
|
877
|
+
console.error(
|
|
878
|
+
`[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
|
|
879
|
+
error
|
|
880
|
+
);
|
|
881
|
+
}
|
|
802
882
|
}
|
|
803
|
-
} finally {
|
|
804
|
-
reader.releaseLock();
|
|
805
883
|
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
884
|
+
}, DEV_CALLBACK_DELAY);
|
|
885
|
+
}
|
|
886
|
+
function clearDevHandlers() {
|
|
887
|
+
devRouteHandlers.clear();
|
|
888
|
+
wildcardRouteHandlers.clear();
|
|
889
|
+
}
|
|
890
|
+
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
891
|
+
globalThis.__clearDevHandlers = clearDevHandlers;
|
|
892
|
+
}
|
|
808
893
|
|
|
809
894
|
// src/consumer-group.ts
|
|
810
895
|
var ConsumerGroup = class {
|
|
@@ -905,7 +990,13 @@ var ConsumerGroup = class {
|
|
|
905
990
|
message.ticket
|
|
906
991
|
);
|
|
907
992
|
try {
|
|
908
|
-
const result = await handler(message
|
|
993
|
+
const result = await handler(message.payload, {
|
|
994
|
+
messageId: message.messageId,
|
|
995
|
+
deliveryCount: message.deliveryCount,
|
|
996
|
+
createdAt: message.createdAt,
|
|
997
|
+
topicName: this.topicName,
|
|
998
|
+
consumerGroup: this.consumerGroupName
|
|
999
|
+
});
|
|
909
1000
|
await stopExtension();
|
|
910
1001
|
if (result && "timeoutSeconds" in result) {
|
|
911
1002
|
await this.client.changeVisibility({
|
|
@@ -915,6 +1006,13 @@ var ConsumerGroup = class {
|
|
|
915
1006
|
ticket: message.ticket,
|
|
916
1007
|
visibilityTimeoutSeconds: result.timeoutSeconds
|
|
917
1008
|
});
|
|
1009
|
+
if (isDevMode()) {
|
|
1010
|
+
scheduleDevTimeout(
|
|
1011
|
+
this.topicName,
|
|
1012
|
+
message.messageId,
|
|
1013
|
+
result.timeoutSeconds
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
918
1016
|
} else {
|
|
919
1017
|
await this.client.deleteMessage({
|
|
920
1018
|
queueName: this.topicName,
|
|
@@ -935,219 +1033,58 @@ var ConsumerGroup = class {
|
|
|
935
1033
|
throw error;
|
|
936
1034
|
}
|
|
937
1035
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
* @param options Processing options
|
|
943
|
-
* @returns Promise that resolves when processing stops (due to signal or error)
|
|
944
|
-
*/
|
|
945
|
-
async subscribe(signal, handler, options = {}) {
|
|
946
|
-
const pollingInterval = options.pollingInterval || 1e3;
|
|
947
|
-
while (!signal.aborted) {
|
|
948
|
-
try {
|
|
949
|
-
for await (const message of this.client.receiveMessages(
|
|
1036
|
+
async consume(handler, options) {
|
|
1037
|
+
if (options?.messageId) {
|
|
1038
|
+
if (options.skipPayload) {
|
|
1039
|
+
const response = await this.client.receiveMessageById(
|
|
950
1040
|
{
|
|
951
1041
|
queueName: this.topicName,
|
|
952
1042
|
consumerGroup: this.consumerGroupName,
|
|
1043
|
+
messageId: options.messageId,
|
|
953
1044
|
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
954
|
-
|
|
955
|
-
// Always process one message at a time
|
|
1045
|
+
skipPayload: true
|
|
956
1046
|
},
|
|
957
1047
|
this.transport
|
|
958
|
-
)
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
if (error instanceof MessageLockedError) {
|
|
999
|
-
const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
|
|
1000
|
-
if (!signal.aborted) {
|
|
1001
|
-
await new Promise((resolve) => {
|
|
1002
|
-
const timeoutId = setTimeout(resolve, waitTime);
|
|
1003
|
-
signal.addEventListener(
|
|
1004
|
-
"abort",
|
|
1005
|
-
() => {
|
|
1006
|
-
clearTimeout(timeoutId);
|
|
1007
|
-
resolve();
|
|
1008
|
-
},
|
|
1009
|
-
{ once: true }
|
|
1010
|
-
);
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
continue;
|
|
1014
|
-
}
|
|
1015
|
-
console.error("Error polling topic:", error);
|
|
1016
|
-
throw error;
|
|
1048
|
+
);
|
|
1049
|
+
await this.processMessage(
|
|
1050
|
+
response.message,
|
|
1051
|
+
handler
|
|
1052
|
+
);
|
|
1053
|
+
} else {
|
|
1054
|
+
const response = await this.client.receiveMessageById(
|
|
1055
|
+
{
|
|
1056
|
+
queueName: this.topicName,
|
|
1057
|
+
consumerGroup: this.consumerGroupName,
|
|
1058
|
+
messageId: options.messageId,
|
|
1059
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1060
|
+
},
|
|
1061
|
+
this.transport
|
|
1062
|
+
);
|
|
1063
|
+
await this.processMessage(
|
|
1064
|
+
response.message,
|
|
1065
|
+
handler
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
} else {
|
|
1069
|
+
let messageFound = false;
|
|
1070
|
+
for await (const message of this.client.receiveMessages(
|
|
1071
|
+
{
|
|
1072
|
+
queueName: this.topicName,
|
|
1073
|
+
consumerGroup: this.consumerGroupName,
|
|
1074
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1075
|
+
limit: 1
|
|
1076
|
+
},
|
|
1077
|
+
this.transport
|
|
1078
|
+
)) {
|
|
1079
|
+
messageFound = true;
|
|
1080
|
+
await this.processMessage(message, handler);
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
if (!messageFound) {
|
|
1084
|
+
throw new Error("No messages available");
|
|
1017
1085
|
}
|
|
1018
1086
|
}
|
|
1019
1087
|
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Receive and process a specific message by its ID with full payload
|
|
1022
|
-
* @param messageId The ID of the message to receive and process
|
|
1023
|
-
* @param handler Function to process the message with full payload
|
|
1024
|
-
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1025
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1026
|
-
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1027
|
-
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1028
|
-
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1029
|
-
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1030
|
-
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1031
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1032
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1033
|
-
* @throws {ForbiddenError} When access is denied
|
|
1034
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1035
|
-
*/
|
|
1036
|
-
async receiveMessage(messageId, handler) {
|
|
1037
|
-
const response = await this.client.receiveMessageById(
|
|
1038
|
-
{
|
|
1039
|
-
queueName: this.topicName,
|
|
1040
|
-
consumerGroup: this.consumerGroupName,
|
|
1041
|
-
messageId,
|
|
1042
|
-
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1043
|
-
},
|
|
1044
|
-
this.transport
|
|
1045
|
-
);
|
|
1046
|
-
await this.processMessage(response.message, handler);
|
|
1047
|
-
}
|
|
1048
|
-
/**
|
|
1049
|
-
* Receive and process the next available message from the queue
|
|
1050
|
-
* @param handler Function to process the message
|
|
1051
|
-
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1052
|
-
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1053
|
-
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1054
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1055
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1056
|
-
* @throws {ForbiddenError} When access is denied
|
|
1057
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1058
|
-
*/
|
|
1059
|
-
async receiveNextMessage(handler) {
|
|
1060
|
-
let messageFound = false;
|
|
1061
|
-
for await (const message of this.client.receiveMessages(
|
|
1062
|
-
{
|
|
1063
|
-
queueName: this.topicName,
|
|
1064
|
-
consumerGroup: this.consumerGroupName,
|
|
1065
|
-
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1066
|
-
limit: 1
|
|
1067
|
-
},
|
|
1068
|
-
this.transport
|
|
1069
|
-
)) {
|
|
1070
|
-
messageFound = true;
|
|
1071
|
-
await this.processMessage(message, handler);
|
|
1072
|
-
break;
|
|
1073
|
-
}
|
|
1074
|
-
if (!messageFound) {
|
|
1075
|
-
throw new Error("No messages available");
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
/**
|
|
1079
|
-
* Receive and process multiple next available messages from the queue
|
|
1080
|
-
* @param limit Number of messages to process (1-10)
|
|
1081
|
-
* @param handler Function to process each message
|
|
1082
|
-
* @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
|
|
1083
|
-
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
1084
|
-
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1085
|
-
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1086
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1087
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1088
|
-
* @throws {ForbiddenError} When access is denied
|
|
1089
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1090
|
-
*/
|
|
1091
|
-
async receiveNextMessages(limit, handler) {
|
|
1092
|
-
if (limit < 1 || limit > 10) {
|
|
1093
|
-
throw new InvalidLimitError(limit);
|
|
1094
|
-
}
|
|
1095
|
-
const processingPromises = [];
|
|
1096
|
-
let messageCount = 0;
|
|
1097
|
-
for await (const message of this.client.receiveMessages(
|
|
1098
|
-
{
|
|
1099
|
-
queueName: this.topicName,
|
|
1100
|
-
consumerGroup: this.consumerGroupName,
|
|
1101
|
-
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1102
|
-
limit
|
|
1103
|
-
},
|
|
1104
|
-
this.transport
|
|
1105
|
-
)) {
|
|
1106
|
-
messageCount++;
|
|
1107
|
-
const wrappedPromise = this.processMessage(message, handler).then(
|
|
1108
|
-
(value) => ({
|
|
1109
|
-
status: "fulfilled",
|
|
1110
|
-
value
|
|
1111
|
-
}),
|
|
1112
|
-
(reason) => ({ status: "rejected", reason })
|
|
1113
|
-
);
|
|
1114
|
-
processingPromises.push(wrappedPromise);
|
|
1115
|
-
}
|
|
1116
|
-
if (messageCount === 0) {
|
|
1117
|
-
throw new Error("No messages available");
|
|
1118
|
-
}
|
|
1119
|
-
const results = await Promise.all(processingPromises);
|
|
1120
|
-
return results;
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* Handle a specific message by its ID without downloading the payload (metadata only)
|
|
1124
|
-
* @param messageId The ID of the message to handle
|
|
1125
|
-
* @param handler Function to process the message metadata (payload will be void)
|
|
1126
|
-
* @returns Promise that resolves when the message is handled or rejects with specific errors
|
|
1127
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1128
|
-
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1129
|
-
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1130
|
-
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1131
|
-
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1132
|
-
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1133
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1134
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1135
|
-
* @throws {ForbiddenError} When access is denied
|
|
1136
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1137
|
-
*/
|
|
1138
|
-
async handleMessage(messageId, handler) {
|
|
1139
|
-
const response = await this.client.receiveMessageById(
|
|
1140
|
-
{
|
|
1141
|
-
queueName: this.topicName,
|
|
1142
|
-
consumerGroup: this.consumerGroupName,
|
|
1143
|
-
messageId,
|
|
1144
|
-
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1145
|
-
skipPayload: true
|
|
1146
|
-
},
|
|
1147
|
-
this.transport
|
|
1148
|
-
);
|
|
1149
|
-
await this.processMessage(response.message, handler);
|
|
1150
|
-
}
|
|
1151
1088
|
/**
|
|
1152
1089
|
* Get the consumer group name
|
|
1153
1090
|
*/
|
|
@@ -1195,10 +1132,13 @@ var Topic = class {
|
|
|
1195
1132
|
payload,
|
|
1196
1133
|
idempotencyKey: options?.idempotencyKey,
|
|
1197
1134
|
retentionSeconds: options?.retentionSeconds,
|
|
1198
|
-
|
|
1135
|
+
deploymentId: options?.deploymentId
|
|
1199
1136
|
},
|
|
1200
1137
|
this.transport
|
|
1201
1138
|
);
|
|
1139
|
+
if (isDevMode()) {
|
|
1140
|
+
triggerDevCallbacks(this.topicName, result.messageId);
|
|
1141
|
+
}
|
|
1202
1142
|
return { messageId: result.messageId };
|
|
1203
1143
|
}
|
|
1204
1144
|
/**
|
|
@@ -1234,107 +1174,60 @@ var Topic = class {
|
|
|
1234
1174
|
};
|
|
1235
1175
|
|
|
1236
1176
|
// src/factory.ts
|
|
1237
|
-
function
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
if (
|
|
1251
|
-
|
|
1252
|
-
throw new InvalidCallbackError(
|
|
1253
|
-
`Missing required queue callback headers: ${missingHeaders.join(", ")}`
|
|
1254
|
-
);
|
|
1177
|
+
async function send(topicName, payload, options) {
|
|
1178
|
+
const transport = options?.transport || new JsonTransport();
|
|
1179
|
+
const client = new QueueClient();
|
|
1180
|
+
const result = await client.sendMessage(
|
|
1181
|
+
{
|
|
1182
|
+
queueName: topicName,
|
|
1183
|
+
payload,
|
|
1184
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1185
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1186
|
+
deploymentId: options?.deploymentId
|
|
1187
|
+
},
|
|
1188
|
+
transport
|
|
1189
|
+
);
|
|
1190
|
+
if (isDevMode()) {
|
|
1191
|
+
triggerDevCallbacks(topicName, result.messageId);
|
|
1255
1192
|
}
|
|
1256
|
-
return {
|
|
1257
|
-
messageId,
|
|
1258
|
-
queueName,
|
|
1259
|
-
consumerGroup
|
|
1260
|
-
};
|
|
1193
|
+
return { messageId: result.messageId };
|
|
1261
1194
|
}
|
|
1262
|
-
function
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
throw new Error(
|
|
1274
|
-
`Topic "${queueName}" has a single handler but received consumer group "${consumerGroup}". Expected "default".`
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
actualHandler = topicHandler;
|
|
1278
|
-
} else {
|
|
1279
|
-
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1280
|
-
if (!consumerGroupHandler) {
|
|
1281
|
-
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1282
|
-
throw new Error(
|
|
1283
|
-
`No handler found for consumer group "${consumerGroup}" in topic "${queueName}". Available groups: ${availableGroups}`
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
|
-
actualHandler = consumerGroupHandler;
|
|
1287
|
-
}
|
|
1288
|
-
const client = await QueueClient.fromVercelFunction();
|
|
1289
|
-
const topic = new Topic(client, queueName);
|
|
1290
|
-
const cg = topic.consumerGroup(consumerGroup);
|
|
1291
|
-
await cg.receiveMessage(messageId, async (message) => {
|
|
1292
|
-
const metadata = {
|
|
1293
|
-
messageId: message.messageId,
|
|
1294
|
-
deliveryCount: message.deliveryCount,
|
|
1295
|
-
timestamp: message.timestamp
|
|
1296
|
-
};
|
|
1297
|
-
return await actualHandler(message.payload, metadata);
|
|
1195
|
+
async function receive(topicName, consumerGroup, handler, options) {
|
|
1196
|
+
const transport = options?.transport || new JsonTransport();
|
|
1197
|
+
const client = new QueueClient();
|
|
1198
|
+
const topic = new Topic(client, topicName, transport);
|
|
1199
|
+
const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
|
|
1200
|
+
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
1201
|
+
if (messageId) {
|
|
1202
|
+
if (skipPayload) {
|
|
1203
|
+
return consumer.consume(handler, {
|
|
1204
|
+
messageId,
|
|
1205
|
+
skipPayload: true
|
|
1298
1206
|
});
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
console.error("Callback error:", error);
|
|
1302
|
-
if (error instanceof InvalidCallbackError) {
|
|
1303
|
-
return Response.json(
|
|
1304
|
-
{ error: "Invalid callback request" },
|
|
1305
|
-
{ status: 400 }
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
return Response.json(
|
|
1309
|
-
{ error: "Failed to process callback" },
|
|
1310
|
-
{ status: 500 }
|
|
1311
|
-
);
|
|
1207
|
+
} else {
|
|
1208
|
+
return consumer.consume(handler, { messageId });
|
|
1312
1209
|
}
|
|
1313
|
-
}
|
|
1210
|
+
} else {
|
|
1211
|
+
return consumer.consume(handler);
|
|
1212
|
+
}
|
|
1314
1213
|
}
|
|
1315
1214
|
export {
|
|
1316
1215
|
BadRequestError,
|
|
1317
1216
|
BufferTransport,
|
|
1318
|
-
ConsumerGroup,
|
|
1319
|
-
FailedDependencyError,
|
|
1320
|
-
FifoOrderingViolationError,
|
|
1321
1217
|
ForbiddenError,
|
|
1322
1218
|
InternalServerError,
|
|
1323
|
-
InvalidCallbackError,
|
|
1324
1219
|
InvalidLimitError,
|
|
1325
1220
|
JsonTransport,
|
|
1326
1221
|
MessageCorruptedError,
|
|
1327
1222
|
MessageLockedError,
|
|
1328
1223
|
MessageNotAvailableError,
|
|
1329
1224
|
MessageNotFoundError,
|
|
1330
|
-
QueueClient,
|
|
1331
1225
|
QueueEmptyError,
|
|
1332
1226
|
StreamTransport,
|
|
1333
|
-
Topic,
|
|
1334
1227
|
UnauthorizedError,
|
|
1335
|
-
createTopic,
|
|
1336
|
-
getVercelOidcToken,
|
|
1337
1228
|
handleCallback,
|
|
1338
|
-
|
|
1229
|
+
parseCallback,
|
|
1230
|
+
receive,
|
|
1231
|
+
send
|
|
1339
1232
|
};
|
|
1340
1233
|
//# sourceMappingURL=index.mjs.map
|