@vercel/queue 0.0.0-alpha.12 → 0.0.0-alpha.3
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 +823 -204
- package/dist/index.d.mts +507 -132
- package/dist/index.d.ts +507 -132
- package/dist/index.js +589 -291
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +580 -288
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,90 +1,130 @@
|
|
|
1
|
-
// src/transports.ts
|
|
2
|
-
var JsonTransport = class {
|
|
3
|
-
contentType = "application/json";
|
|
4
|
-
serialize(value) {
|
|
5
|
-
return Buffer.from(JSON.stringify(value), "utf8");
|
|
6
|
-
}
|
|
7
|
-
async deserialize(stream) {
|
|
8
|
-
const reader = stream.getReader();
|
|
9
|
-
let totalLength = 0;
|
|
10
|
-
const chunks = [];
|
|
11
|
-
try {
|
|
12
|
-
while (true) {
|
|
13
|
-
const { done, value } = await reader.read();
|
|
14
|
-
if (done) break;
|
|
15
|
-
chunks.push(value);
|
|
16
|
-
totalLength += value.length;
|
|
17
|
-
}
|
|
18
|
-
} finally {
|
|
19
|
-
reader.releaseLock();
|
|
20
|
-
}
|
|
21
|
-
const buffer = Buffer.concat(chunks, totalLength);
|
|
22
|
-
return JSON.parse(buffer.toString("utf8"));
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
var BufferTransport = class {
|
|
26
|
-
contentType = "application/octet-stream";
|
|
27
|
-
serialize(value) {
|
|
28
|
-
return value;
|
|
29
|
-
}
|
|
30
|
-
async deserialize(stream) {
|
|
31
|
-
const reader = stream.getReader();
|
|
32
|
-
const chunks = [];
|
|
33
|
-
try {
|
|
34
|
-
while (true) {
|
|
35
|
-
const { done, value } = await reader.read();
|
|
36
|
-
if (done) break;
|
|
37
|
-
chunks.push(value);
|
|
38
|
-
}
|
|
39
|
-
} finally {
|
|
40
|
-
reader.releaseLock();
|
|
41
|
-
}
|
|
42
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
43
|
-
const buffer = new Uint8Array(totalLength);
|
|
44
|
-
let offset = 0;
|
|
45
|
-
for (const chunk of chunks) {
|
|
46
|
-
buffer.set(chunk, offset);
|
|
47
|
-
offset += chunk.length;
|
|
48
|
-
}
|
|
49
|
-
return Buffer.from(buffer);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
var StreamTransport = class {
|
|
53
|
-
contentType = "application/octet-stream";
|
|
54
|
-
serialize(value) {
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
async deserialize(stream) {
|
|
58
|
-
return stream;
|
|
59
|
-
}
|
|
60
|
-
async finalize(payload) {
|
|
61
|
-
const reader = payload.getReader();
|
|
62
|
-
try {
|
|
63
|
-
while (true) {
|
|
64
|
-
const { done } = await reader.read();
|
|
65
|
-
if (done) break;
|
|
66
|
-
}
|
|
67
|
-
} finally {
|
|
68
|
-
reader.releaseLock();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// src/client.ts
|
|
74
|
-
import { parseMultipartStream } from "mixpart";
|
|
75
|
-
|
|
76
1
|
// src/oidc.ts
|
|
77
|
-
function getVercelOidcToken() {
|
|
2
|
+
async function getVercelOidcToken() {
|
|
78
3
|
const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
|
|
79
4
|
const fromSymbol = globalThis;
|
|
80
5
|
const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
|
|
81
6
|
const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
|
|
82
7
|
if (!token) {
|
|
83
|
-
|
|
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
|
+
);
|
|
84
11
|
}
|
|
85
12
|
return token;
|
|
86
13
|
}
|
|
87
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) {
|
|
21
|
+
try {
|
|
22
|
+
const parsedUrl = new URL(url);
|
|
23
|
+
const isLocalhost = parsedUrl.hostname === "localhost";
|
|
24
|
+
const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 0;
|
|
25
|
+
return { isLocalhost, port };
|
|
26
|
+
} catch {
|
|
27
|
+
return { isLocalhost: false };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isSupportedPlatform() {
|
|
31
|
+
const platform = process.platform;
|
|
32
|
+
return platform === "darwin" || platform === "linux";
|
|
33
|
+
}
|
|
34
|
+
function processDevelopmentCallbacks(callbacks) {
|
|
35
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
36
|
+
if (!isDevelopment) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
if (!isSupportedPlatform()) {
|
|
40
|
+
const hasLocalhostCallbacks = Object.values(callbacks).some((config) => {
|
|
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 [];
|
|
50
|
+
}
|
|
51
|
+
const localhostCallbacks = [];
|
|
52
|
+
Object.entries(callbacks).forEach(([group, config]) => {
|
|
53
|
+
const { isLocalhost, port } = isLocalhostWithPort(config.url);
|
|
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;
|
|
86
|
+
}
|
|
87
|
+
let headerArgs = "";
|
|
88
|
+
if (headers) {
|
|
89
|
+
const headerArray = [];
|
|
90
|
+
headers.forEach((value, key) => {
|
|
91
|
+
headerArray.push(`-H '${key}: ${value}'`);
|
|
92
|
+
});
|
|
93
|
+
headerArgs = headerArray.join(" ");
|
|
94
|
+
}
|
|
95
|
+
const bashScript = `
|
|
96
|
+
# Wait for any initial boot time
|
|
97
|
+
sleep ${initialDelaySeconds}
|
|
98
|
+
|
|
99
|
+
missed=0
|
|
100
|
+
while true; do
|
|
101
|
+
# 1) Check if TCP port is listening
|
|
102
|
+
if nc -z localhost ${port} 2>/dev/null; then
|
|
103
|
+
missed=0
|
|
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
|
+
}
|
|
127
|
+
|
|
88
128
|
// src/types.ts
|
|
89
129
|
var MessageNotFoundError = class extends Error {
|
|
90
130
|
constructor(messageId) {
|
|
@@ -100,6 +140,16 @@ var MessageNotAvailableError = class extends Error {
|
|
|
100
140
|
this.name = "MessageNotAvailableError";
|
|
101
141
|
}
|
|
102
142
|
};
|
|
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
|
+
};
|
|
103
153
|
var MessageCorruptedError = class extends Error {
|
|
104
154
|
constructor(messageId, reason) {
|
|
105
155
|
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
@@ -141,6 +191,16 @@ var BadRequestError = class extends Error {
|
|
|
141
191
|
this.name = "BadRequestError";
|
|
142
192
|
}
|
|
143
193
|
};
|
|
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
|
+
};
|
|
144
204
|
var InternalServerError = class extends Error {
|
|
145
205
|
constructor(message = "Unexpected server error") {
|
|
146
206
|
super(message);
|
|
@@ -153,6 +213,12 @@ var InvalidLimitError = class extends Error {
|
|
|
153
213
|
this.name = "InvalidLimitError";
|
|
154
214
|
}
|
|
155
215
|
};
|
|
216
|
+
var InvalidCallbackError = class extends Error {
|
|
217
|
+
constructor(message) {
|
|
218
|
+
super(message);
|
|
219
|
+
this.name = "InvalidCallbackError";
|
|
220
|
+
}
|
|
221
|
+
};
|
|
156
222
|
|
|
157
223
|
// src/client.ts
|
|
158
224
|
async function consumeStream(stream) {
|
|
@@ -182,33 +248,40 @@ function parseQueueHeaders(headers) {
|
|
|
182
248
|
return {
|
|
183
249
|
messageId,
|
|
184
250
|
deliveryCount,
|
|
185
|
-
|
|
251
|
+
timestamp,
|
|
186
252
|
contentType,
|
|
187
253
|
ticket
|
|
188
254
|
};
|
|
189
255
|
}
|
|
190
|
-
var QueueClient = class {
|
|
256
|
+
var QueueClient = class _QueueClient {
|
|
191
257
|
baseUrl;
|
|
192
|
-
basePath;
|
|
193
258
|
token;
|
|
194
259
|
/**
|
|
195
260
|
* Create a new Vercel Queue Service client
|
|
196
|
-
* @param options Client configuration options
|
|
261
|
+
* @param options Client configuration options
|
|
197
262
|
*/
|
|
198
|
-
constructor(options
|
|
199
|
-
this.baseUrl = options.baseUrl || "https://vercel
|
|
200
|
-
this.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
263
|
+
constructor(options) {
|
|
264
|
+
this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
|
|
265
|
+
this.token = options.token;
|
|
266
|
+
}
|
|
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) {
|
|
275
|
+
const token = await getVercelOidcToken();
|
|
276
|
+
if (!token) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
"Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment."
|
|
279
|
+
);
|
|
211
280
|
}
|
|
281
|
+
return new _QueueClient({
|
|
282
|
+
token,
|
|
283
|
+
baseUrl
|
|
284
|
+
});
|
|
212
285
|
}
|
|
213
286
|
/**
|
|
214
287
|
* Send a message to a queue
|
|
@@ -221,23 +294,48 @@ var QueueClient = class {
|
|
|
221
294
|
* @throws {InternalServerError} When server encounters an error
|
|
222
295
|
*/
|
|
223
296
|
async sendMessage(options, transport) {
|
|
224
|
-
const { queueName, payload, idempotencyKey, retentionSeconds } = options;
|
|
297
|
+
const { queueName, payload, idempotencyKey, retentionSeconds, callback } = options;
|
|
225
298
|
const headers = new Headers({
|
|
226
299
|
Authorization: `Bearer ${this.token}`,
|
|
227
300
|
"Vqs-Queue-Name": queueName,
|
|
228
301
|
"Content-Type": transport.contentType
|
|
229
302
|
});
|
|
230
|
-
if (process.env.VERCEL_DEPLOYMENT_ID) {
|
|
231
|
-
headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
|
|
232
|
-
}
|
|
233
303
|
if (idempotencyKey) {
|
|
234
304
|
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
235
305
|
}
|
|
236
306
|
if (retentionSeconds !== void 0) {
|
|
237
307
|
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
238
308
|
}
|
|
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
|
+
}
|
|
239
337
|
const body = transport.serialize(payload);
|
|
240
|
-
const response = await fetch(`${this.baseUrl}
|
|
338
|
+
const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
|
|
241
339
|
method: "POST",
|
|
242
340
|
headers,
|
|
243
341
|
body
|
|
@@ -266,6 +364,9 @@ var QueueClient = class {
|
|
|
266
364
|
);
|
|
267
365
|
}
|
|
268
366
|
const responseData = await response.json();
|
|
367
|
+
if (localhostCallbacks.length > 0) {
|
|
368
|
+
fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
|
|
369
|
+
}
|
|
269
370
|
return responseData;
|
|
270
371
|
}
|
|
271
372
|
/**
|
|
@@ -275,7 +376,7 @@ var QueueClient = class {
|
|
|
275
376
|
* @returns AsyncGenerator that yields messages as they arrive
|
|
276
377
|
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
277
378
|
* @throws {QueueEmptyError} When no messages are available (204)
|
|
278
|
-
* @throws {MessageLockedError} When
|
|
379
|
+
* @throws {MessageLockedError} When FIFO queue has locked messages (423)
|
|
279
380
|
* @throws {BadRequestError} When request parameters are invalid
|
|
280
381
|
* @throws {UnauthorizedError} When authentication fails
|
|
281
382
|
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
@@ -301,7 +402,7 @@ var QueueClient = class {
|
|
|
301
402
|
if (limit !== void 0) {
|
|
302
403
|
headers.set("Vqs-Limit", limit.toString());
|
|
303
404
|
}
|
|
304
|
-
const response = await fetch(`${this.baseUrl}
|
|
405
|
+
const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
|
|
305
406
|
method: "GET",
|
|
306
407
|
headers
|
|
307
408
|
});
|
|
@@ -326,7 +427,7 @@ var QueueClient = class {
|
|
|
326
427
|
const parsed = parseInt(retryAfterHeader, 10);
|
|
327
428
|
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
328
429
|
}
|
|
329
|
-
throw new MessageLockedError("next message", retryAfter);
|
|
430
|
+
throw new MessageLockedError("next message in FIFO queue", retryAfter);
|
|
330
431
|
}
|
|
331
432
|
if (response.status >= 500) {
|
|
332
433
|
throw new InternalServerError(
|
|
@@ -383,7 +484,7 @@ var QueueClient = class {
|
|
|
383
484
|
headers.set("Vqs-Skip-Payload", "1");
|
|
384
485
|
}
|
|
385
486
|
const response = await fetch(
|
|
386
|
-
`${this.baseUrl}
|
|
487
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
387
488
|
{
|
|
388
489
|
method: "GET",
|
|
389
490
|
headers
|
|
@@ -412,7 +513,37 @@ var QueueClient = class {
|
|
|
412
513
|
}
|
|
413
514
|
throw new MessageLockedError(messageId, retryAfter);
|
|
414
515
|
}
|
|
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
|
+
}
|
|
415
535
|
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
|
+
}
|
|
416
547
|
throw new MessageNotAvailableError(messageId);
|
|
417
548
|
}
|
|
418
549
|
if (response.status >= 500) {
|
|
@@ -492,7 +623,7 @@ var QueueClient = class {
|
|
|
492
623
|
async deleteMessage(options) {
|
|
493
624
|
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
494
625
|
const response = await fetch(
|
|
495
|
-
`${this.baseUrl}
|
|
626
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
496
627
|
{
|
|
497
628
|
method: "DELETE",
|
|
498
629
|
headers: new Headers({
|
|
@@ -553,7 +684,7 @@ var QueueClient = class {
|
|
|
553
684
|
visibilityTimeoutSeconds
|
|
554
685
|
} = options;
|
|
555
686
|
const response = await fetch(
|
|
556
|
-
`${this.baseUrl}
|
|
687
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
557
688
|
{
|
|
558
689
|
method: "PATCH",
|
|
559
690
|
headers: new Headers({
|
|
@@ -599,6 +730,82 @@ var QueueClient = class {
|
|
|
599
730
|
}
|
|
600
731
|
};
|
|
601
732
|
|
|
733
|
+
// src/transports.ts
|
|
734
|
+
var JsonTransport = class {
|
|
735
|
+
contentType = "application/json";
|
|
736
|
+
serialize(value) {
|
|
737
|
+
return Buffer.from(JSON.stringify(value), "utf8");
|
|
738
|
+
}
|
|
739
|
+
async deserialize(stream) {
|
|
740
|
+
const reader = stream.getReader();
|
|
741
|
+
const chunks = [];
|
|
742
|
+
try {
|
|
743
|
+
while (true) {
|
|
744
|
+
const { done, value } = await reader.read();
|
|
745
|
+
if (done) break;
|
|
746
|
+
chunks.push(value);
|
|
747
|
+
}
|
|
748
|
+
} finally {
|
|
749
|
+
reader.releaseLock();
|
|
750
|
+
}
|
|
751
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
752
|
+
const buffer = new Uint8Array(totalLength);
|
|
753
|
+
let offset = 0;
|
|
754
|
+
for (const chunk of chunks) {
|
|
755
|
+
buffer.set(chunk, offset);
|
|
756
|
+
offset += chunk.length;
|
|
757
|
+
}
|
|
758
|
+
return JSON.parse(Buffer.from(buffer).toString("utf8"));
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var BufferTransport = class {
|
|
762
|
+
contentType = "application/octet-stream";
|
|
763
|
+
serialize(value) {
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
async deserialize(stream) {
|
|
767
|
+
const reader = stream.getReader();
|
|
768
|
+
const chunks = [];
|
|
769
|
+
try {
|
|
770
|
+
while (true) {
|
|
771
|
+
const { done, value } = await reader.read();
|
|
772
|
+
if (done) break;
|
|
773
|
+
chunks.push(value);
|
|
774
|
+
}
|
|
775
|
+
} finally {
|
|
776
|
+
reader.releaseLock();
|
|
777
|
+
}
|
|
778
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
779
|
+
const buffer = new Uint8Array(totalLength);
|
|
780
|
+
let offset = 0;
|
|
781
|
+
for (const chunk of chunks) {
|
|
782
|
+
buffer.set(chunk, offset);
|
|
783
|
+
offset += chunk.length;
|
|
784
|
+
}
|
|
785
|
+
return Buffer.from(buffer);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
var StreamTransport = class {
|
|
789
|
+
contentType = "application/octet-stream";
|
|
790
|
+
serialize(value) {
|
|
791
|
+
return value;
|
|
792
|
+
}
|
|
793
|
+
async deserialize(stream) {
|
|
794
|
+
return stream;
|
|
795
|
+
}
|
|
796
|
+
async finalize(payload) {
|
|
797
|
+
const reader = payload.getReader();
|
|
798
|
+
try {
|
|
799
|
+
while (true) {
|
|
800
|
+
const { done } = await reader.read();
|
|
801
|
+
if (done) break;
|
|
802
|
+
}
|
|
803
|
+
} finally {
|
|
804
|
+
reader.releaseLock();
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
602
809
|
// src/consumer-group.ts
|
|
603
810
|
var ConsumerGroup = class {
|
|
604
811
|
client;
|
|
@@ -698,13 +905,7 @@ var ConsumerGroup = class {
|
|
|
698
905
|
message.ticket
|
|
699
906
|
);
|
|
700
907
|
try {
|
|
701
|
-
const result = await handler(message
|
|
702
|
-
messageId: message.messageId,
|
|
703
|
-
deliveryCount: message.deliveryCount,
|
|
704
|
-
createdAt: message.createdAt,
|
|
705
|
-
topicName: this.topicName,
|
|
706
|
-
consumerGroup: this.consumerGroupName
|
|
707
|
-
});
|
|
908
|
+
const result = await handler(message);
|
|
708
909
|
await stopExtension();
|
|
709
910
|
if (result && "timeoutSeconds" in result) {
|
|
710
911
|
await this.client.changeVisibility({
|
|
@@ -734,58 +935,219 @@ var ConsumerGroup = class {
|
|
|
734
935
|
throw error;
|
|
735
936
|
}
|
|
736
937
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
938
|
+
/**
|
|
939
|
+
* Start continuous processing of messages from the topic
|
|
940
|
+
* @param signal AbortSignal to control when to stop processing
|
|
941
|
+
* @param handler Function to process each message
|
|
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(
|
|
741
950
|
{
|
|
742
951
|
queueName: this.topicName,
|
|
743
952
|
consumerGroup: this.consumerGroupName,
|
|
744
|
-
messageId: options.messageId,
|
|
745
953
|
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
746
|
-
|
|
954
|
+
limit: 1
|
|
955
|
+
// Always process one message at a time
|
|
747
956
|
},
|
|
748
957
|
this.transport
|
|
749
|
-
)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
958
|
+
)) {
|
|
959
|
+
if (signal.aborted) {
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
await this.processMessage(message, handler);
|
|
964
|
+
} catch (error) {
|
|
965
|
+
console.error("Error processing message:", error);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (!signal.aborted) {
|
|
969
|
+
await new Promise((resolve) => {
|
|
970
|
+
const timeoutId = setTimeout(resolve, pollingInterval);
|
|
971
|
+
signal.addEventListener(
|
|
972
|
+
"abort",
|
|
973
|
+
() => {
|
|
974
|
+
clearTimeout(timeoutId);
|
|
975
|
+
resolve();
|
|
976
|
+
},
|
|
977
|
+
{ once: true }
|
|
978
|
+
);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
} catch (error) {
|
|
982
|
+
if (error instanceof QueueEmptyError) {
|
|
983
|
+
if (!signal.aborted) {
|
|
984
|
+
await new Promise((resolve) => {
|
|
985
|
+
const timeoutId = setTimeout(resolve, pollingInterval);
|
|
986
|
+
signal.addEventListener(
|
|
987
|
+
"abort",
|
|
988
|
+
() => {
|
|
989
|
+
clearTimeout(timeoutId);
|
|
990
|
+
resolve();
|
|
991
|
+
},
|
|
992
|
+
{ once: true }
|
|
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;
|
|
786
1017
|
}
|
|
787
1018
|
}
|
|
788
1019
|
}
|
|
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
|
+
}
|
|
789
1151
|
/**
|
|
790
1152
|
* Get the consumer group name
|
|
791
1153
|
*/
|
|
@@ -832,7 +1194,8 @@ var Topic = class {
|
|
|
832
1194
|
queueName: this.topicName,
|
|
833
1195
|
payload,
|
|
834
1196
|
idempotencyKey: options?.idempotencyKey,
|
|
835
|
-
retentionSeconds: options?.retentionSeconds
|
|
1197
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1198
|
+
callback: options?.callback
|
|
836
1199
|
},
|
|
837
1200
|
this.transport
|
|
838
1201
|
);
|
|
@@ -871,156 +1234,79 @@ var Topic = class {
|
|
|
871
1234
|
};
|
|
872
1235
|
|
|
873
1236
|
// src/factory.ts
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const client = new QueueClient();
|
|
877
|
-
const result = await client.sendMessage(
|
|
878
|
-
{
|
|
879
|
-
queueName: topicName,
|
|
880
|
-
payload,
|
|
881
|
-
idempotencyKey: options?.idempotencyKey,
|
|
882
|
-
retentionSeconds: options?.retentionSeconds
|
|
883
|
-
},
|
|
884
|
-
transport
|
|
885
|
-
);
|
|
886
|
-
return { messageId: result.messageId };
|
|
887
|
-
}
|
|
888
|
-
async function receive(topicName, consumerGroup, handler, options) {
|
|
889
|
-
const transport = options?.transport || new JsonTransport();
|
|
890
|
-
const client = new QueueClient();
|
|
891
|
-
const topic = new Topic(client, topicName, transport);
|
|
892
|
-
const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
|
|
893
|
-
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
894
|
-
if (messageId) {
|
|
895
|
-
if (skipPayload) {
|
|
896
|
-
return consumer.consume(handler, {
|
|
897
|
-
messageId,
|
|
898
|
-
skipPayload: true
|
|
899
|
-
});
|
|
900
|
-
} else {
|
|
901
|
-
return consumer.consume(handler, { messageId });
|
|
902
|
-
}
|
|
903
|
-
} else {
|
|
904
|
-
return consumer.consume(handler);
|
|
905
|
-
}
|
|
1237
|
+
function createTopic(client, topicName, transport) {
|
|
1238
|
+
return new Topic(client, topicName, transport);
|
|
906
1239
|
}
|
|
907
1240
|
|
|
908
1241
|
// src/callback.ts
|
|
909
|
-
function
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
if (
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
return true;
|
|
922
|
-
}
|
|
923
|
-
function matchesWildcardPattern(topicName, pattern) {
|
|
924
|
-
const prefix = pattern.slice(0, -1);
|
|
925
|
-
return topicName.startsWith(prefix);
|
|
926
|
-
}
|
|
927
|
-
function findTopicHandler(queueName, handlers) {
|
|
928
|
-
const exactHandler = handlers[queueName];
|
|
929
|
-
if (exactHandler) {
|
|
930
|
-
return exactHandler;
|
|
931
|
-
}
|
|
932
|
-
for (const pattern in handlers) {
|
|
933
|
-
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
934
|
-
return handlers[pattern];
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
return null;
|
|
938
|
-
}
|
|
939
|
-
async function parseCallback(request) {
|
|
940
|
-
const contentType = request.headers.get("content-type");
|
|
941
|
-
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
942
|
-
throw new Error(
|
|
943
|
-
"Invalid content type: expected 'application/cloudevents+json'"
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
let cloudEvent;
|
|
947
|
-
try {
|
|
948
|
-
cloudEvent = await request.json();
|
|
949
|
-
} catch (error) {
|
|
950
|
-
throw new Error("Failed to parse CloudEvent from request body");
|
|
951
|
-
}
|
|
952
|
-
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
953
|
-
throw new Error("Invalid CloudEvent: missing required fields");
|
|
954
|
-
}
|
|
955
|
-
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
956
|
-
throw new Error(
|
|
957
|
-
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
const missingFields = [];
|
|
961
|
-
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
962
|
-
if (!("consumerGroup" in cloudEvent.data))
|
|
963
|
-
missingFields.push("consumerGroup");
|
|
964
|
-
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
965
|
-
if (missingFields.length > 0) {
|
|
966
|
-
throw new Error(
|
|
967
|
-
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1242
|
+
function parseCallbackRequest(request) {
|
|
1243
|
+
const headers = request.headers;
|
|
1244
|
+
const messageId = headers.get("Vqs-Message-Id");
|
|
1245
|
+
const queueName = headers.get("Vqs-Queue-Name");
|
|
1246
|
+
const consumerGroup = headers.get("Vqs-Consumer-Group");
|
|
1247
|
+
const missingHeaders = [];
|
|
1248
|
+
if (!messageId) missingHeaders.push("Vqs-Message-Id");
|
|
1249
|
+
if (!queueName) missingHeaders.push("Vqs-Queue-Name");
|
|
1250
|
+
if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
|
|
1251
|
+
if (missingHeaders.length > 0) {
|
|
1252
|
+
throw new InvalidCallbackError(
|
|
1253
|
+
`Missing required queue callback headers: ${missingHeaders.join(", ")}`
|
|
968
1254
|
);
|
|
969
1255
|
}
|
|
970
|
-
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
971
1256
|
return {
|
|
1257
|
+
messageId,
|
|
972
1258
|
queueName,
|
|
973
|
-
consumerGroup
|
|
974
|
-
messageId
|
|
1259
|
+
consumerGroup
|
|
975
1260
|
};
|
|
976
1261
|
}
|
|
977
1262
|
function handleCallback(handlers) {
|
|
978
|
-
for (const topicPattern in handlers) {
|
|
979
|
-
if (topicPattern.includes("*")) {
|
|
980
|
-
if (!validateWildcardPattern(topicPattern)) {
|
|
981
|
-
throw new Error(
|
|
982
|
-
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
1263
|
return async (request) => {
|
|
988
1264
|
try {
|
|
989
|
-
const { queueName, consumerGroup, messageId } =
|
|
990
|
-
const topicHandler =
|
|
1265
|
+
const { queueName, consumerGroup, messageId } = parseCallbackRequest(request);
|
|
1266
|
+
const topicHandler = handlers[queueName];
|
|
991
1267
|
if (!topicHandler) {
|
|
992
|
-
|
|
993
|
-
return Response.json(
|
|
994
|
-
{
|
|
995
|
-
error: `No handler found for topic: ${queueName}`,
|
|
996
|
-
availableTopics
|
|
997
|
-
},
|
|
998
|
-
{ status: 404 }
|
|
999
|
-
);
|
|
1268
|
+
throw new Error(`No handler found for topic: ${queueName}`);
|
|
1000
1269
|
}
|
|
1001
|
-
|
|
1002
|
-
if (
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1270
|
+
let actualHandler;
|
|
1271
|
+
if (typeof topicHandler === "function") {
|
|
1272
|
+
if (consumerGroup !== "default") {
|
|
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;
|
|
1011
1287
|
}
|
|
1012
|
-
const client =
|
|
1288
|
+
const client = await QueueClient.fromVercelFunction();
|
|
1013
1289
|
const topic = new Topic(client, queueName);
|
|
1014
1290
|
const cg = topic.consumerGroup(consumerGroup);
|
|
1015
|
-
await cg.
|
|
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);
|
|
1298
|
+
});
|
|
1016
1299
|
return Response.json({ status: "success" });
|
|
1017
1300
|
} catch (error) {
|
|
1018
|
-
console.error("
|
|
1019
|
-
if (error instanceof
|
|
1020
|
-
return Response.json(
|
|
1301
|
+
console.error("Callback error:", error);
|
|
1302
|
+
if (error instanceof InvalidCallbackError) {
|
|
1303
|
+
return Response.json(
|
|
1304
|
+
{ error: "Invalid callback request" },
|
|
1305
|
+
{ status: 400 }
|
|
1306
|
+
);
|
|
1021
1307
|
}
|
|
1022
1308
|
return Response.json(
|
|
1023
|
-
{ error: "Failed to process
|
|
1309
|
+
{ error: "Failed to process callback" },
|
|
1024
1310
|
{ status: 500 }
|
|
1025
1311
|
);
|
|
1026
1312
|
}
|
|
@@ -1029,20 +1315,26 @@ function handleCallback(handlers) {
|
|
|
1029
1315
|
export {
|
|
1030
1316
|
BadRequestError,
|
|
1031
1317
|
BufferTransport,
|
|
1318
|
+
ConsumerGroup,
|
|
1319
|
+
FailedDependencyError,
|
|
1320
|
+
FifoOrderingViolationError,
|
|
1032
1321
|
ForbiddenError,
|
|
1033
1322
|
InternalServerError,
|
|
1323
|
+
InvalidCallbackError,
|
|
1034
1324
|
InvalidLimitError,
|
|
1035
1325
|
JsonTransport,
|
|
1036
1326
|
MessageCorruptedError,
|
|
1037
1327
|
MessageLockedError,
|
|
1038
1328
|
MessageNotAvailableError,
|
|
1039
1329
|
MessageNotFoundError,
|
|
1330
|
+
QueueClient,
|
|
1040
1331
|
QueueEmptyError,
|
|
1041
1332
|
StreamTransport,
|
|
1333
|
+
Topic,
|
|
1042
1334
|
UnauthorizedError,
|
|
1335
|
+
createTopic,
|
|
1336
|
+
getVercelOidcToken,
|
|
1043
1337
|
handleCallback,
|
|
1044
|
-
|
|
1045
|
-
receive,
|
|
1046
|
-
send
|
|
1338
|
+
parseCallbackRequest
|
|
1047
1339
|
};
|
|
1048
1340
|
//# sourceMappingURL=index.mjs.map
|