@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.js
CHANGED
|
@@ -22,111 +22,157 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
BadRequestError: () => BadRequestError,
|
|
24
24
|
BufferTransport: () => BufferTransport,
|
|
25
|
+
ConsumerGroup: () => ConsumerGroup,
|
|
26
|
+
FailedDependencyError: () => FailedDependencyError,
|
|
27
|
+
FifoOrderingViolationError: () => FifoOrderingViolationError,
|
|
25
28
|
ForbiddenError: () => ForbiddenError,
|
|
26
29
|
InternalServerError: () => InternalServerError,
|
|
30
|
+
InvalidCallbackError: () => InvalidCallbackError,
|
|
27
31
|
InvalidLimitError: () => InvalidLimitError,
|
|
28
32
|
JsonTransport: () => JsonTransport,
|
|
29
33
|
MessageCorruptedError: () => MessageCorruptedError,
|
|
30
34
|
MessageLockedError: () => MessageLockedError,
|
|
31
35
|
MessageNotAvailableError: () => MessageNotAvailableError,
|
|
32
36
|
MessageNotFoundError: () => MessageNotFoundError,
|
|
37
|
+
QueueClient: () => QueueClient,
|
|
33
38
|
QueueEmptyError: () => QueueEmptyError,
|
|
34
39
|
StreamTransport: () => StreamTransport,
|
|
40
|
+
Topic: () => Topic,
|
|
35
41
|
UnauthorizedError: () => UnauthorizedError,
|
|
42
|
+
createTopic: () => createTopic,
|
|
43
|
+
getVercelOidcToken: () => getVercelOidcToken,
|
|
36
44
|
handleCallback: () => handleCallback,
|
|
37
|
-
|
|
38
|
-
receive: () => receive,
|
|
39
|
-
send: () => send
|
|
45
|
+
parseCallbackRequest: () => parseCallbackRequest
|
|
40
46
|
});
|
|
41
47
|
module.exports = __toCommonJS(index_exports);
|
|
42
48
|
|
|
43
|
-
// src/transports.ts
|
|
44
|
-
var JsonTransport = class {
|
|
45
|
-
contentType = "application/json";
|
|
46
|
-
serialize(value) {
|
|
47
|
-
return Buffer.from(JSON.stringify(value), "utf8");
|
|
48
|
-
}
|
|
49
|
-
async deserialize(stream) {
|
|
50
|
-
const reader = stream.getReader();
|
|
51
|
-
let totalLength = 0;
|
|
52
|
-
const chunks = [];
|
|
53
|
-
try {
|
|
54
|
-
while (true) {
|
|
55
|
-
const { done, value } = await reader.read();
|
|
56
|
-
if (done) break;
|
|
57
|
-
chunks.push(value);
|
|
58
|
-
totalLength += value.length;
|
|
59
|
-
}
|
|
60
|
-
} finally {
|
|
61
|
-
reader.releaseLock();
|
|
62
|
-
}
|
|
63
|
-
const buffer = Buffer.concat(chunks, totalLength);
|
|
64
|
-
return JSON.parse(buffer.toString("utf8"));
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
var BufferTransport = class {
|
|
68
|
-
contentType = "application/octet-stream";
|
|
69
|
-
serialize(value) {
|
|
70
|
-
return value;
|
|
71
|
-
}
|
|
72
|
-
async deserialize(stream) {
|
|
73
|
-
const reader = stream.getReader();
|
|
74
|
-
const chunks = [];
|
|
75
|
-
try {
|
|
76
|
-
while (true) {
|
|
77
|
-
const { done, value } = await reader.read();
|
|
78
|
-
if (done) break;
|
|
79
|
-
chunks.push(value);
|
|
80
|
-
}
|
|
81
|
-
} finally {
|
|
82
|
-
reader.releaseLock();
|
|
83
|
-
}
|
|
84
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
85
|
-
const buffer = new Uint8Array(totalLength);
|
|
86
|
-
let offset = 0;
|
|
87
|
-
for (const chunk of chunks) {
|
|
88
|
-
buffer.set(chunk, offset);
|
|
89
|
-
offset += chunk.length;
|
|
90
|
-
}
|
|
91
|
-
return Buffer.from(buffer);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
var StreamTransport = class {
|
|
95
|
-
contentType = "application/octet-stream";
|
|
96
|
-
serialize(value) {
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
async deserialize(stream) {
|
|
100
|
-
return stream;
|
|
101
|
-
}
|
|
102
|
-
async finalize(payload) {
|
|
103
|
-
const reader = payload.getReader();
|
|
104
|
-
try {
|
|
105
|
-
while (true) {
|
|
106
|
-
const { done } = await reader.read();
|
|
107
|
-
if (done) break;
|
|
108
|
-
}
|
|
109
|
-
} finally {
|
|
110
|
-
reader.releaseLock();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// src/client.ts
|
|
116
|
-
var import_mixpart = require("mixpart");
|
|
117
|
-
|
|
118
49
|
// src/oidc.ts
|
|
119
|
-
function getVercelOidcToken() {
|
|
50
|
+
async function getVercelOidcToken() {
|
|
120
51
|
const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
|
|
121
52
|
const fromSymbol = globalThis;
|
|
122
53
|
const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
|
|
123
54
|
const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
|
|
124
55
|
if (!token) {
|
|
125
|
-
|
|
56
|
+
throw new Error(
|
|
57
|
+
`The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`
|
|
58
|
+
);
|
|
126
59
|
}
|
|
127
60
|
return token;
|
|
128
61
|
}
|
|
129
62
|
|
|
63
|
+
// src/client.ts
|
|
64
|
+
var import_mixpart = require("mixpart");
|
|
65
|
+
|
|
66
|
+
// src/local.ts
|
|
67
|
+
var import_node_child_process = require("child_process");
|
|
68
|
+
function isLocalhostWithPort(url) {
|
|
69
|
+
try {
|
|
70
|
+
const parsedUrl = new URL(url);
|
|
71
|
+
const isLocalhost = parsedUrl.hostname === "localhost";
|
|
72
|
+
const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 0;
|
|
73
|
+
return { isLocalhost, port };
|
|
74
|
+
} catch {
|
|
75
|
+
return { isLocalhost: false };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isSupportedPlatform() {
|
|
79
|
+
const platform = process.platform;
|
|
80
|
+
return platform === "darwin" || platform === "linux";
|
|
81
|
+
}
|
|
82
|
+
function processDevelopmentCallbacks(callbacks) {
|
|
83
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
84
|
+
if (!isDevelopment) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
if (!isSupportedPlatform()) {
|
|
88
|
+
const hasLocalhostCallbacks = Object.values(callbacks).some((config) => {
|
|
89
|
+
const { isLocalhost } = isLocalhostWithPort(config.url);
|
|
90
|
+
return isLocalhost;
|
|
91
|
+
});
|
|
92
|
+
if (hasLocalhostCallbacks) {
|
|
93
|
+
console.warn(
|
|
94
|
+
`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.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const localhostCallbacks = [];
|
|
100
|
+
Object.entries(callbacks).forEach(([group, config]) => {
|
|
101
|
+
const { isLocalhost, port } = isLocalhostWithPort(config.url);
|
|
102
|
+
if (isLocalhost && port && port > 0) {
|
|
103
|
+
localhostCallbacks.push({ group, config, port });
|
|
104
|
+
} else {
|
|
105
|
+
console.warn(
|
|
106
|
+
`Queue Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return localhostCallbacks;
|
|
111
|
+
}
|
|
112
|
+
function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
|
|
113
|
+
localhostCallbacks.forEach(({ group, config, port }) => {
|
|
114
|
+
const callbackHeaders = new Headers();
|
|
115
|
+
callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
|
|
116
|
+
callbackHeaders.set("Vqs-Queue-Name", queueName);
|
|
117
|
+
callbackHeaders.set("Vqs-Consumer-Group", group);
|
|
118
|
+
fireAndForgetWaitForHttpReady(
|
|
119
|
+
config.url,
|
|
120
|
+
port,
|
|
121
|
+
config.delay || 0,
|
|
122
|
+
3,
|
|
123
|
+
// Default retry frequency
|
|
124
|
+
callbackHeaders
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
|
|
129
|
+
if (!isSupportedPlatform()) {
|
|
130
|
+
console.warn(
|
|
131
|
+
`Queue: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
|
|
132
|
+
);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let headerArgs = "";
|
|
136
|
+
if (headers) {
|
|
137
|
+
const headerArray = [];
|
|
138
|
+
headers.forEach((value, key) => {
|
|
139
|
+
headerArray.push(`-H '${key}: ${value}'`);
|
|
140
|
+
});
|
|
141
|
+
headerArgs = headerArray.join(" ");
|
|
142
|
+
}
|
|
143
|
+
const bashScript = `
|
|
144
|
+
# Wait for any initial boot time
|
|
145
|
+
sleep ${initialDelaySeconds}
|
|
146
|
+
|
|
147
|
+
missed=0
|
|
148
|
+
while true; do
|
|
149
|
+
# 1) Check if TCP port is listening
|
|
150
|
+
if nc -z localhost ${port} 2>/dev/null; then
|
|
151
|
+
missed=0
|
|
152
|
+
# 2) If port is open, try HTTP POST check
|
|
153
|
+
if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
|
|
154
|
+
# Success: port is up AND HTTP returned 2xx (following redirects)
|
|
155
|
+
exit 0
|
|
156
|
+
fi
|
|
157
|
+
else
|
|
158
|
+
# Port was closed\u2014increment miss counter
|
|
159
|
+
((missed+=1))
|
|
160
|
+
# If closed twice in a row, give up immediately
|
|
161
|
+
if [ "$missed" -ge 2 ]; then
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
# Wait before next cycle
|
|
166
|
+
sleep ${retryFrequencySeconds}
|
|
167
|
+
done
|
|
168
|
+
`;
|
|
169
|
+
const childProcess = (0, import_node_child_process.spawn)("bash", ["-c", bashScript], {
|
|
170
|
+
stdio: "ignore",
|
|
171
|
+
detached: true
|
|
172
|
+
});
|
|
173
|
+
childProcess.unref();
|
|
174
|
+
}
|
|
175
|
+
|
|
130
176
|
// src/types.ts
|
|
131
177
|
var MessageNotFoundError = class extends Error {
|
|
132
178
|
constructor(messageId) {
|
|
@@ -142,6 +188,16 @@ var MessageNotAvailableError = class extends Error {
|
|
|
142
188
|
this.name = "MessageNotAvailableError";
|
|
143
189
|
}
|
|
144
190
|
};
|
|
191
|
+
var FifoOrderingViolationError = class extends Error {
|
|
192
|
+
nextMessageId;
|
|
193
|
+
constructor(messageId, nextMessageId, reason) {
|
|
194
|
+
super(
|
|
195
|
+
`FIFO ordering violation for message ${messageId}: ${reason}. Process message ${nextMessageId} first.`
|
|
196
|
+
);
|
|
197
|
+
this.name = "FifoOrderingViolationError";
|
|
198
|
+
this.nextMessageId = nextMessageId;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
145
201
|
var MessageCorruptedError = class extends Error {
|
|
146
202
|
constructor(messageId, reason) {
|
|
147
203
|
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
@@ -183,6 +239,16 @@ var BadRequestError = class extends Error {
|
|
|
183
239
|
this.name = "BadRequestError";
|
|
184
240
|
}
|
|
185
241
|
};
|
|
242
|
+
var FailedDependencyError = class extends Error {
|
|
243
|
+
nextMessageId;
|
|
244
|
+
constructor(messageId, nextMessageId) {
|
|
245
|
+
super(
|
|
246
|
+
`Failed dependency: FIFO ordering violation for message ${messageId}. Must process message ${nextMessageId} first.`
|
|
247
|
+
);
|
|
248
|
+
this.name = "FailedDependencyError";
|
|
249
|
+
this.nextMessageId = nextMessageId;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
186
252
|
var InternalServerError = class extends Error {
|
|
187
253
|
constructor(message = "Unexpected server error") {
|
|
188
254
|
super(message);
|
|
@@ -195,6 +261,12 @@ var InvalidLimitError = class extends Error {
|
|
|
195
261
|
this.name = "InvalidLimitError";
|
|
196
262
|
}
|
|
197
263
|
};
|
|
264
|
+
var InvalidCallbackError = class extends Error {
|
|
265
|
+
constructor(message) {
|
|
266
|
+
super(message);
|
|
267
|
+
this.name = "InvalidCallbackError";
|
|
268
|
+
}
|
|
269
|
+
};
|
|
198
270
|
|
|
199
271
|
// src/client.ts
|
|
200
272
|
async function consumeStream(stream) {
|
|
@@ -224,33 +296,40 @@ function parseQueueHeaders(headers) {
|
|
|
224
296
|
return {
|
|
225
297
|
messageId,
|
|
226
298
|
deliveryCount,
|
|
227
|
-
|
|
299
|
+
timestamp,
|
|
228
300
|
contentType,
|
|
229
301
|
ticket
|
|
230
302
|
};
|
|
231
303
|
}
|
|
232
|
-
var QueueClient = class {
|
|
304
|
+
var QueueClient = class _QueueClient {
|
|
233
305
|
baseUrl;
|
|
234
|
-
basePath;
|
|
235
306
|
token;
|
|
236
307
|
/**
|
|
237
308
|
* Create a new Vercel Queue Service client
|
|
238
|
-
* @param options Client configuration options
|
|
309
|
+
* @param options Client configuration options
|
|
239
310
|
*/
|
|
240
|
-
constructor(options
|
|
241
|
-
this.baseUrl = options.baseUrl || "https://vercel
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
311
|
+
constructor(options) {
|
|
312
|
+
this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
|
|
313
|
+
this.token = options.token;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Create a QueueClient automatically configured for Vercel Functions
|
|
317
|
+
* This method automatically retrieves the OIDC token from the Vercel Function environment
|
|
318
|
+
* Always creates a fresh instance since OIDC tokens expire after 15 minutes
|
|
319
|
+
* @param baseUrl Optional base URL override
|
|
320
|
+
* @returns Promise resolving to a new QueueClient instance
|
|
321
|
+
*/
|
|
322
|
+
static async fromVercelFunction(baseUrl) {
|
|
323
|
+
const token = await getVercelOidcToken();
|
|
324
|
+
if (!token) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
"Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment."
|
|
327
|
+
);
|
|
253
328
|
}
|
|
329
|
+
return new _QueueClient({
|
|
330
|
+
token,
|
|
331
|
+
baseUrl
|
|
332
|
+
});
|
|
254
333
|
}
|
|
255
334
|
/**
|
|
256
335
|
* Send a message to a queue
|
|
@@ -263,23 +342,48 @@ var QueueClient = class {
|
|
|
263
342
|
* @throws {InternalServerError} When server encounters an error
|
|
264
343
|
*/
|
|
265
344
|
async sendMessage(options, transport) {
|
|
266
|
-
const { queueName, payload, idempotencyKey, retentionSeconds } = options;
|
|
345
|
+
const { queueName, payload, idempotencyKey, retentionSeconds, callback } = options;
|
|
267
346
|
const headers = new Headers({
|
|
268
347
|
Authorization: `Bearer ${this.token}`,
|
|
269
348
|
"Vqs-Queue-Name": queueName,
|
|
270
349
|
"Content-Type": transport.contentType
|
|
271
350
|
});
|
|
272
|
-
if (process.env.VERCEL_DEPLOYMENT_ID) {
|
|
273
|
-
headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
|
|
274
|
-
}
|
|
275
351
|
if (idempotencyKey) {
|
|
276
352
|
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
277
353
|
}
|
|
278
354
|
if (retentionSeconds !== void 0) {
|
|
279
355
|
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
280
356
|
}
|
|
357
|
+
let normalizedCallbacks;
|
|
358
|
+
if (callback) {
|
|
359
|
+
if ("url" in callback && typeof callback.url === "string") {
|
|
360
|
+
normalizedCallbacks = { default: callback };
|
|
361
|
+
} else {
|
|
362
|
+
normalizedCallbacks = callback;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
let localhostCallbacks = [];
|
|
366
|
+
if (normalizedCallbacks) {
|
|
367
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
368
|
+
if (isDevelopment) {
|
|
369
|
+
localhostCallbacks = processDevelopmentCallbacks(normalizedCallbacks);
|
|
370
|
+
} else {
|
|
371
|
+
const endpoints = Object.entries(normalizedCallbacks).map(
|
|
372
|
+
([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
|
|
373
|
+
).join(",");
|
|
374
|
+
headers.set("Vqs-Callback-Url", endpoints);
|
|
375
|
+
const delays = Object.entries(normalizedCallbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
|
|
376
|
+
if (delays) {
|
|
377
|
+
headers.set("Vqs-Callback-Delay", delays);
|
|
378
|
+
}
|
|
379
|
+
const frequencies = Object.entries(normalizedCallbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
|
|
380
|
+
if (frequencies) {
|
|
381
|
+
headers.set("Vqs-Callback-Frequency", frequencies);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
281
385
|
const body = transport.serialize(payload);
|
|
282
|
-
const response = await fetch(`${this.baseUrl}
|
|
386
|
+
const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
|
|
283
387
|
method: "POST",
|
|
284
388
|
headers,
|
|
285
389
|
body
|
|
@@ -308,6 +412,9 @@ var QueueClient = class {
|
|
|
308
412
|
);
|
|
309
413
|
}
|
|
310
414
|
const responseData = await response.json();
|
|
415
|
+
if (localhostCallbacks.length > 0) {
|
|
416
|
+
fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
|
|
417
|
+
}
|
|
311
418
|
return responseData;
|
|
312
419
|
}
|
|
313
420
|
/**
|
|
@@ -317,7 +424,7 @@ var QueueClient = class {
|
|
|
317
424
|
* @returns AsyncGenerator that yields messages as they arrive
|
|
318
425
|
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
319
426
|
* @throws {QueueEmptyError} When no messages are available (204)
|
|
320
|
-
* @throws {MessageLockedError} When
|
|
427
|
+
* @throws {MessageLockedError} When FIFO queue has locked messages (423)
|
|
321
428
|
* @throws {BadRequestError} When request parameters are invalid
|
|
322
429
|
* @throws {UnauthorizedError} When authentication fails
|
|
323
430
|
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
@@ -343,7 +450,7 @@ var QueueClient = class {
|
|
|
343
450
|
if (limit !== void 0) {
|
|
344
451
|
headers.set("Vqs-Limit", limit.toString());
|
|
345
452
|
}
|
|
346
|
-
const response = await fetch(`${this.baseUrl}
|
|
453
|
+
const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
|
|
347
454
|
method: "GET",
|
|
348
455
|
headers
|
|
349
456
|
});
|
|
@@ -368,7 +475,7 @@ var QueueClient = class {
|
|
|
368
475
|
const parsed = parseInt(retryAfterHeader, 10);
|
|
369
476
|
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
370
477
|
}
|
|
371
|
-
throw new MessageLockedError("next message", retryAfter);
|
|
478
|
+
throw new MessageLockedError("next message in FIFO queue", retryAfter);
|
|
372
479
|
}
|
|
373
480
|
if (response.status >= 500) {
|
|
374
481
|
throw new InternalServerError(
|
|
@@ -425,7 +532,7 @@ var QueueClient = class {
|
|
|
425
532
|
headers.set("Vqs-Skip-Payload", "1");
|
|
426
533
|
}
|
|
427
534
|
const response = await fetch(
|
|
428
|
-
`${this.baseUrl}
|
|
535
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
429
536
|
{
|
|
430
537
|
method: "GET",
|
|
431
538
|
headers
|
|
@@ -454,7 +561,37 @@ var QueueClient = class {
|
|
|
454
561
|
}
|
|
455
562
|
throw new MessageLockedError(messageId, retryAfter);
|
|
456
563
|
}
|
|
564
|
+
if (response.status === 424) {
|
|
565
|
+
try {
|
|
566
|
+
const errorData = await response.json();
|
|
567
|
+
if (errorData.meta?.nextMessageId) {
|
|
568
|
+
throw new FailedDependencyError(
|
|
569
|
+
messageId,
|
|
570
|
+
errorData.meta.nextMessageId
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
} catch (parseError) {
|
|
574
|
+
if (parseError instanceof FailedDependencyError) {
|
|
575
|
+
throw parseError;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
throw new MessageNotAvailableError(
|
|
579
|
+
messageId,
|
|
580
|
+
"FIFO ordering violation"
|
|
581
|
+
);
|
|
582
|
+
}
|
|
457
583
|
if (response.status === 409) {
|
|
584
|
+
try {
|
|
585
|
+
const errorData = await response.json();
|
|
586
|
+
if (errorData.nextMessageId) {
|
|
587
|
+
throw new FifoOrderingViolationError(
|
|
588
|
+
messageId,
|
|
589
|
+
errorData.nextMessageId,
|
|
590
|
+
errorData.error
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
} catch (parseError) {
|
|
594
|
+
}
|
|
458
595
|
throw new MessageNotAvailableError(messageId);
|
|
459
596
|
}
|
|
460
597
|
if (response.status >= 500) {
|
|
@@ -534,7 +671,7 @@ var QueueClient = class {
|
|
|
534
671
|
async deleteMessage(options) {
|
|
535
672
|
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
536
673
|
const response = await fetch(
|
|
537
|
-
`${this.baseUrl}
|
|
674
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
538
675
|
{
|
|
539
676
|
method: "DELETE",
|
|
540
677
|
headers: new Headers({
|
|
@@ -595,7 +732,7 @@ var QueueClient = class {
|
|
|
595
732
|
visibilityTimeoutSeconds
|
|
596
733
|
} = options;
|
|
597
734
|
const response = await fetch(
|
|
598
|
-
`${this.baseUrl}
|
|
735
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
599
736
|
{
|
|
600
737
|
method: "PATCH",
|
|
601
738
|
headers: new Headers({
|
|
@@ -641,6 +778,82 @@ var QueueClient = class {
|
|
|
641
778
|
}
|
|
642
779
|
};
|
|
643
780
|
|
|
781
|
+
// src/transports.ts
|
|
782
|
+
var JsonTransport = class {
|
|
783
|
+
contentType = "application/json";
|
|
784
|
+
serialize(value) {
|
|
785
|
+
return Buffer.from(JSON.stringify(value), "utf8");
|
|
786
|
+
}
|
|
787
|
+
async deserialize(stream) {
|
|
788
|
+
const reader = stream.getReader();
|
|
789
|
+
const chunks = [];
|
|
790
|
+
try {
|
|
791
|
+
while (true) {
|
|
792
|
+
const { done, value } = await reader.read();
|
|
793
|
+
if (done) break;
|
|
794
|
+
chunks.push(value);
|
|
795
|
+
}
|
|
796
|
+
} finally {
|
|
797
|
+
reader.releaseLock();
|
|
798
|
+
}
|
|
799
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
800
|
+
const buffer = new Uint8Array(totalLength);
|
|
801
|
+
let offset = 0;
|
|
802
|
+
for (const chunk of chunks) {
|
|
803
|
+
buffer.set(chunk, offset);
|
|
804
|
+
offset += chunk.length;
|
|
805
|
+
}
|
|
806
|
+
return JSON.parse(Buffer.from(buffer).toString("utf8"));
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
var BufferTransport = class {
|
|
810
|
+
contentType = "application/octet-stream";
|
|
811
|
+
serialize(value) {
|
|
812
|
+
return value;
|
|
813
|
+
}
|
|
814
|
+
async deserialize(stream) {
|
|
815
|
+
const reader = stream.getReader();
|
|
816
|
+
const chunks = [];
|
|
817
|
+
try {
|
|
818
|
+
while (true) {
|
|
819
|
+
const { done, value } = await reader.read();
|
|
820
|
+
if (done) break;
|
|
821
|
+
chunks.push(value);
|
|
822
|
+
}
|
|
823
|
+
} finally {
|
|
824
|
+
reader.releaseLock();
|
|
825
|
+
}
|
|
826
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
827
|
+
const buffer = new Uint8Array(totalLength);
|
|
828
|
+
let offset = 0;
|
|
829
|
+
for (const chunk of chunks) {
|
|
830
|
+
buffer.set(chunk, offset);
|
|
831
|
+
offset += chunk.length;
|
|
832
|
+
}
|
|
833
|
+
return Buffer.from(buffer);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
var StreamTransport = class {
|
|
837
|
+
contentType = "application/octet-stream";
|
|
838
|
+
serialize(value) {
|
|
839
|
+
return value;
|
|
840
|
+
}
|
|
841
|
+
async deserialize(stream) {
|
|
842
|
+
return stream;
|
|
843
|
+
}
|
|
844
|
+
async finalize(payload) {
|
|
845
|
+
const reader = payload.getReader();
|
|
846
|
+
try {
|
|
847
|
+
while (true) {
|
|
848
|
+
const { done } = await reader.read();
|
|
849
|
+
if (done) break;
|
|
850
|
+
}
|
|
851
|
+
} finally {
|
|
852
|
+
reader.releaseLock();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
|
|
644
857
|
// src/consumer-group.ts
|
|
645
858
|
var ConsumerGroup = class {
|
|
646
859
|
client;
|
|
@@ -740,13 +953,7 @@ var ConsumerGroup = class {
|
|
|
740
953
|
message.ticket
|
|
741
954
|
);
|
|
742
955
|
try {
|
|
743
|
-
const result = await handler(message
|
|
744
|
-
messageId: message.messageId,
|
|
745
|
-
deliveryCount: message.deliveryCount,
|
|
746
|
-
createdAt: message.createdAt,
|
|
747
|
-
topicName: this.topicName,
|
|
748
|
-
consumerGroup: this.consumerGroupName
|
|
749
|
-
});
|
|
956
|
+
const result = await handler(message);
|
|
750
957
|
await stopExtension();
|
|
751
958
|
if (result && "timeoutSeconds" in result) {
|
|
752
959
|
await this.client.changeVisibility({
|
|
@@ -776,58 +983,219 @@ var ConsumerGroup = class {
|
|
|
776
983
|
throw error;
|
|
777
984
|
}
|
|
778
985
|
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
986
|
+
/**
|
|
987
|
+
* Start continuous processing of messages from the topic
|
|
988
|
+
* @param signal AbortSignal to control when to stop processing
|
|
989
|
+
* @param handler Function to process each message
|
|
990
|
+
* @param options Processing options
|
|
991
|
+
* @returns Promise that resolves when processing stops (due to signal or error)
|
|
992
|
+
*/
|
|
993
|
+
async subscribe(signal, handler, options = {}) {
|
|
994
|
+
const pollingInterval = options.pollingInterval || 1e3;
|
|
995
|
+
while (!signal.aborted) {
|
|
996
|
+
try {
|
|
997
|
+
for await (const message of this.client.receiveMessages(
|
|
783
998
|
{
|
|
784
999
|
queueName: this.topicName,
|
|
785
1000
|
consumerGroup: this.consumerGroupName,
|
|
786
|
-
messageId: options.messageId,
|
|
787
1001
|
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
788
|
-
|
|
1002
|
+
limit: 1
|
|
1003
|
+
// Always process one message at a time
|
|
789
1004
|
},
|
|
790
1005
|
this.transport
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1006
|
+
)) {
|
|
1007
|
+
if (signal.aborted) {
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
try {
|
|
1011
|
+
await this.processMessage(message, handler);
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
console.error("Error processing message:", error);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (!signal.aborted) {
|
|
1017
|
+
await new Promise((resolve) => {
|
|
1018
|
+
const timeoutId = setTimeout(resolve, pollingInterval);
|
|
1019
|
+
signal.addEventListener(
|
|
1020
|
+
"abort",
|
|
1021
|
+
() => {
|
|
1022
|
+
clearTimeout(timeoutId);
|
|
1023
|
+
resolve();
|
|
1024
|
+
},
|
|
1025
|
+
{ once: true }
|
|
1026
|
+
);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
if (error instanceof QueueEmptyError) {
|
|
1031
|
+
if (!signal.aborted) {
|
|
1032
|
+
await new Promise((resolve) => {
|
|
1033
|
+
const timeoutId = setTimeout(resolve, pollingInterval);
|
|
1034
|
+
signal.addEventListener(
|
|
1035
|
+
"abort",
|
|
1036
|
+
() => {
|
|
1037
|
+
clearTimeout(timeoutId);
|
|
1038
|
+
resolve();
|
|
1039
|
+
},
|
|
1040
|
+
{ once: true }
|
|
1041
|
+
);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (error instanceof MessageLockedError) {
|
|
1047
|
+
const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
|
|
1048
|
+
if (!signal.aborted) {
|
|
1049
|
+
await new Promise((resolve) => {
|
|
1050
|
+
const timeoutId = setTimeout(resolve, waitTime);
|
|
1051
|
+
signal.addEventListener(
|
|
1052
|
+
"abort",
|
|
1053
|
+
() => {
|
|
1054
|
+
clearTimeout(timeoutId);
|
|
1055
|
+
resolve();
|
|
1056
|
+
},
|
|
1057
|
+
{ once: true }
|
|
1058
|
+
);
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
console.error("Error polling topic:", error);
|
|
1064
|
+
throw error;
|
|
828
1065
|
}
|
|
829
1066
|
}
|
|
830
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Receive and process a specific message by its ID with full payload
|
|
1070
|
+
* @param messageId The ID of the message to receive and process
|
|
1071
|
+
* @param handler Function to process the message with full payload
|
|
1072
|
+
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1073
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1074
|
+
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1075
|
+
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1076
|
+
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1077
|
+
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1078
|
+
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1079
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1080
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1081
|
+
* @throws {ForbiddenError} When access is denied
|
|
1082
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1083
|
+
*/
|
|
1084
|
+
async receiveMessage(messageId, handler) {
|
|
1085
|
+
const response = await this.client.receiveMessageById(
|
|
1086
|
+
{
|
|
1087
|
+
queueName: this.topicName,
|
|
1088
|
+
consumerGroup: this.consumerGroupName,
|
|
1089
|
+
messageId,
|
|
1090
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1091
|
+
},
|
|
1092
|
+
this.transport
|
|
1093
|
+
);
|
|
1094
|
+
await this.processMessage(response.message, handler);
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Receive and process the next available message from the queue
|
|
1098
|
+
* @param handler Function to process the message
|
|
1099
|
+
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1100
|
+
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1101
|
+
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1102
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1103
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1104
|
+
* @throws {ForbiddenError} When access is denied
|
|
1105
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1106
|
+
*/
|
|
1107
|
+
async receiveNextMessage(handler) {
|
|
1108
|
+
let messageFound = false;
|
|
1109
|
+
for await (const message of this.client.receiveMessages(
|
|
1110
|
+
{
|
|
1111
|
+
queueName: this.topicName,
|
|
1112
|
+
consumerGroup: this.consumerGroupName,
|
|
1113
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1114
|
+
limit: 1
|
|
1115
|
+
},
|
|
1116
|
+
this.transport
|
|
1117
|
+
)) {
|
|
1118
|
+
messageFound = true;
|
|
1119
|
+
await this.processMessage(message, handler);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
if (!messageFound) {
|
|
1123
|
+
throw new Error("No messages available");
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Receive and process multiple next available messages from the queue
|
|
1128
|
+
* @param limit Number of messages to process (1-10)
|
|
1129
|
+
* @param handler Function to process each message
|
|
1130
|
+
* @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
|
|
1131
|
+
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
1132
|
+
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1133
|
+
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1134
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1135
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1136
|
+
* @throws {ForbiddenError} When access is denied
|
|
1137
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1138
|
+
*/
|
|
1139
|
+
async receiveNextMessages(limit, handler) {
|
|
1140
|
+
if (limit < 1 || limit > 10) {
|
|
1141
|
+
throw new InvalidLimitError(limit);
|
|
1142
|
+
}
|
|
1143
|
+
const processingPromises = [];
|
|
1144
|
+
let messageCount = 0;
|
|
1145
|
+
for await (const message of this.client.receiveMessages(
|
|
1146
|
+
{
|
|
1147
|
+
queueName: this.topicName,
|
|
1148
|
+
consumerGroup: this.consumerGroupName,
|
|
1149
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1150
|
+
limit
|
|
1151
|
+
},
|
|
1152
|
+
this.transport
|
|
1153
|
+
)) {
|
|
1154
|
+
messageCount++;
|
|
1155
|
+
const wrappedPromise = this.processMessage(message, handler).then(
|
|
1156
|
+
(value) => ({
|
|
1157
|
+
status: "fulfilled",
|
|
1158
|
+
value
|
|
1159
|
+
}),
|
|
1160
|
+
(reason) => ({ status: "rejected", reason })
|
|
1161
|
+
);
|
|
1162
|
+
processingPromises.push(wrappedPromise);
|
|
1163
|
+
}
|
|
1164
|
+
if (messageCount === 0) {
|
|
1165
|
+
throw new Error("No messages available");
|
|
1166
|
+
}
|
|
1167
|
+
const results = await Promise.all(processingPromises);
|
|
1168
|
+
return results;
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Handle a specific message by its ID without downloading the payload (metadata only)
|
|
1172
|
+
* @param messageId The ID of the message to handle
|
|
1173
|
+
* @param handler Function to process the message metadata (payload will be void)
|
|
1174
|
+
* @returns Promise that resolves when the message is handled or rejects with specific errors
|
|
1175
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1176
|
+
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1177
|
+
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1178
|
+
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1179
|
+
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1180
|
+
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1181
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1182
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1183
|
+
* @throws {ForbiddenError} When access is denied
|
|
1184
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1185
|
+
*/
|
|
1186
|
+
async handleMessage(messageId, handler) {
|
|
1187
|
+
const response = await this.client.receiveMessageById(
|
|
1188
|
+
{
|
|
1189
|
+
queueName: this.topicName,
|
|
1190
|
+
consumerGroup: this.consumerGroupName,
|
|
1191
|
+
messageId,
|
|
1192
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1193
|
+
skipPayload: true
|
|
1194
|
+
},
|
|
1195
|
+
this.transport
|
|
1196
|
+
);
|
|
1197
|
+
await this.processMessage(response.message, handler);
|
|
1198
|
+
}
|
|
831
1199
|
/**
|
|
832
1200
|
* Get the consumer group name
|
|
833
1201
|
*/
|
|
@@ -874,7 +1242,8 @@ var Topic = class {
|
|
|
874
1242
|
queueName: this.topicName,
|
|
875
1243
|
payload,
|
|
876
1244
|
idempotencyKey: options?.idempotencyKey,
|
|
877
|
-
retentionSeconds: options?.retentionSeconds
|
|
1245
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1246
|
+
callback: options?.callback
|
|
878
1247
|
},
|
|
879
1248
|
this.transport
|
|
880
1249
|
);
|
|
@@ -913,156 +1282,79 @@ var Topic = class {
|
|
|
913
1282
|
};
|
|
914
1283
|
|
|
915
1284
|
// src/factory.ts
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const client = new QueueClient();
|
|
919
|
-
const result = await client.sendMessage(
|
|
920
|
-
{
|
|
921
|
-
queueName: topicName,
|
|
922
|
-
payload,
|
|
923
|
-
idempotencyKey: options?.idempotencyKey,
|
|
924
|
-
retentionSeconds: options?.retentionSeconds
|
|
925
|
-
},
|
|
926
|
-
transport
|
|
927
|
-
);
|
|
928
|
-
return { messageId: result.messageId };
|
|
929
|
-
}
|
|
930
|
-
async function receive(topicName, consumerGroup, handler, options) {
|
|
931
|
-
const transport = options?.transport || new JsonTransport();
|
|
932
|
-
const client = new QueueClient();
|
|
933
|
-
const topic = new Topic(client, topicName, transport);
|
|
934
|
-
const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
|
|
935
|
-
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
936
|
-
if (messageId) {
|
|
937
|
-
if (skipPayload) {
|
|
938
|
-
return consumer.consume(handler, {
|
|
939
|
-
messageId,
|
|
940
|
-
skipPayload: true
|
|
941
|
-
});
|
|
942
|
-
} else {
|
|
943
|
-
return consumer.consume(handler, { messageId });
|
|
944
|
-
}
|
|
945
|
-
} else {
|
|
946
|
-
return consumer.consume(handler);
|
|
947
|
-
}
|
|
1285
|
+
function createTopic(client, topicName, transport) {
|
|
1286
|
+
return new Topic(client, topicName, transport);
|
|
948
1287
|
}
|
|
949
1288
|
|
|
950
1289
|
// src/callback.ts
|
|
951
|
-
function
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
if (
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
return true;
|
|
964
|
-
}
|
|
965
|
-
function matchesWildcardPattern(topicName, pattern) {
|
|
966
|
-
const prefix = pattern.slice(0, -1);
|
|
967
|
-
return topicName.startsWith(prefix);
|
|
968
|
-
}
|
|
969
|
-
function findTopicHandler(queueName, handlers) {
|
|
970
|
-
const exactHandler = handlers[queueName];
|
|
971
|
-
if (exactHandler) {
|
|
972
|
-
return exactHandler;
|
|
973
|
-
}
|
|
974
|
-
for (const pattern in handlers) {
|
|
975
|
-
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
976
|
-
return handlers[pattern];
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
return null;
|
|
980
|
-
}
|
|
981
|
-
async function parseCallback(request) {
|
|
982
|
-
const contentType = request.headers.get("content-type");
|
|
983
|
-
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
984
|
-
throw new Error(
|
|
985
|
-
"Invalid content type: expected 'application/cloudevents+json'"
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
let cloudEvent;
|
|
989
|
-
try {
|
|
990
|
-
cloudEvent = await request.json();
|
|
991
|
-
} catch (error) {
|
|
992
|
-
throw new Error("Failed to parse CloudEvent from request body");
|
|
993
|
-
}
|
|
994
|
-
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
995
|
-
throw new Error("Invalid CloudEvent: missing required fields");
|
|
996
|
-
}
|
|
997
|
-
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
998
|
-
throw new Error(
|
|
999
|
-
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
const missingFields = [];
|
|
1003
|
-
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
1004
|
-
if (!("consumerGroup" in cloudEvent.data))
|
|
1005
|
-
missingFields.push("consumerGroup");
|
|
1006
|
-
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
1007
|
-
if (missingFields.length > 0) {
|
|
1008
|
-
throw new Error(
|
|
1009
|
-
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1290
|
+
function parseCallbackRequest(request) {
|
|
1291
|
+
const headers = request.headers;
|
|
1292
|
+
const messageId = headers.get("Vqs-Message-Id");
|
|
1293
|
+
const queueName = headers.get("Vqs-Queue-Name");
|
|
1294
|
+
const consumerGroup = headers.get("Vqs-Consumer-Group");
|
|
1295
|
+
const missingHeaders = [];
|
|
1296
|
+
if (!messageId) missingHeaders.push("Vqs-Message-Id");
|
|
1297
|
+
if (!queueName) missingHeaders.push("Vqs-Queue-Name");
|
|
1298
|
+
if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
|
|
1299
|
+
if (missingHeaders.length > 0) {
|
|
1300
|
+
throw new InvalidCallbackError(
|
|
1301
|
+
`Missing required queue callback headers: ${missingHeaders.join(", ")}`
|
|
1010
1302
|
);
|
|
1011
1303
|
}
|
|
1012
|
-
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
1013
1304
|
return {
|
|
1305
|
+
messageId,
|
|
1014
1306
|
queueName,
|
|
1015
|
-
consumerGroup
|
|
1016
|
-
messageId
|
|
1307
|
+
consumerGroup
|
|
1017
1308
|
};
|
|
1018
1309
|
}
|
|
1019
1310
|
function handleCallback(handlers) {
|
|
1020
|
-
for (const topicPattern in handlers) {
|
|
1021
|
-
if (topicPattern.includes("*")) {
|
|
1022
|
-
if (!validateWildcardPattern(topicPattern)) {
|
|
1023
|
-
throw new Error(
|
|
1024
|
-
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
1025
|
-
);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
1311
|
return async (request) => {
|
|
1030
1312
|
try {
|
|
1031
|
-
const { queueName, consumerGroup, messageId } =
|
|
1032
|
-
const topicHandler =
|
|
1313
|
+
const { queueName, consumerGroup, messageId } = parseCallbackRequest(request);
|
|
1314
|
+
const topicHandler = handlers[queueName];
|
|
1033
1315
|
if (!topicHandler) {
|
|
1034
|
-
|
|
1035
|
-
return Response.json(
|
|
1036
|
-
{
|
|
1037
|
-
error: `No handler found for topic: ${queueName}`,
|
|
1038
|
-
availableTopics
|
|
1039
|
-
},
|
|
1040
|
-
{ status: 404 }
|
|
1041
|
-
);
|
|
1316
|
+
throw new Error(`No handler found for topic: ${queueName}`);
|
|
1042
1317
|
}
|
|
1043
|
-
|
|
1044
|
-
if (
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1318
|
+
let actualHandler;
|
|
1319
|
+
if (typeof topicHandler === "function") {
|
|
1320
|
+
if (consumerGroup !== "default") {
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`Topic "${queueName}" has a single handler but received consumer group "${consumerGroup}". Expected "default".`
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
actualHandler = topicHandler;
|
|
1326
|
+
} else {
|
|
1327
|
+
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1328
|
+
if (!consumerGroupHandler) {
|
|
1329
|
+
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1330
|
+
throw new Error(
|
|
1331
|
+
`No handler found for consumer group "${consumerGroup}" in topic "${queueName}". Available groups: ${availableGroups}`
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
actualHandler = consumerGroupHandler;
|
|
1053
1335
|
}
|
|
1054
|
-
const client =
|
|
1336
|
+
const client = await QueueClient.fromVercelFunction();
|
|
1055
1337
|
const topic = new Topic(client, queueName);
|
|
1056
1338
|
const cg = topic.consumerGroup(consumerGroup);
|
|
1057
|
-
await cg.
|
|
1339
|
+
await cg.receiveMessage(messageId, async (message) => {
|
|
1340
|
+
const metadata = {
|
|
1341
|
+
messageId: message.messageId,
|
|
1342
|
+
deliveryCount: message.deliveryCount,
|
|
1343
|
+
timestamp: message.timestamp
|
|
1344
|
+
};
|
|
1345
|
+
return await actualHandler(message.payload, metadata);
|
|
1346
|
+
});
|
|
1058
1347
|
return Response.json({ status: "success" });
|
|
1059
1348
|
} catch (error) {
|
|
1060
|
-
console.error("
|
|
1061
|
-
if (error instanceof
|
|
1062
|
-
return Response.json(
|
|
1349
|
+
console.error("Callback error:", error);
|
|
1350
|
+
if (error instanceof InvalidCallbackError) {
|
|
1351
|
+
return Response.json(
|
|
1352
|
+
{ error: "Invalid callback request" },
|
|
1353
|
+
{ status: 400 }
|
|
1354
|
+
);
|
|
1063
1355
|
}
|
|
1064
1356
|
return Response.json(
|
|
1065
|
-
{ error: "Failed to process
|
|
1357
|
+
{ error: "Failed to process callback" },
|
|
1066
1358
|
{ status: 500 }
|
|
1067
1359
|
);
|
|
1068
1360
|
}
|
|
@@ -1072,20 +1364,26 @@ function handleCallback(handlers) {
|
|
|
1072
1364
|
0 && (module.exports = {
|
|
1073
1365
|
BadRequestError,
|
|
1074
1366
|
BufferTransport,
|
|
1367
|
+
ConsumerGroup,
|
|
1368
|
+
FailedDependencyError,
|
|
1369
|
+
FifoOrderingViolationError,
|
|
1075
1370
|
ForbiddenError,
|
|
1076
1371
|
InternalServerError,
|
|
1372
|
+
InvalidCallbackError,
|
|
1077
1373
|
InvalidLimitError,
|
|
1078
1374
|
JsonTransport,
|
|
1079
1375
|
MessageCorruptedError,
|
|
1080
1376
|
MessageLockedError,
|
|
1081
1377
|
MessageNotAvailableError,
|
|
1082
1378
|
MessageNotFoundError,
|
|
1379
|
+
QueueClient,
|
|
1083
1380
|
QueueEmptyError,
|
|
1084
1381
|
StreamTransport,
|
|
1382
|
+
Topic,
|
|
1085
1383
|
UnauthorizedError,
|
|
1384
|
+
createTopic,
|
|
1385
|
+
getVercelOidcToken,
|
|
1086
1386
|
handleCallback,
|
|
1087
|
-
|
|
1088
|
-
receive,
|
|
1089
|
-
send
|
|
1387
|
+
parseCallbackRequest
|
|
1090
1388
|
});
|
|
1091
1389
|
//# sourceMappingURL=index.js.map
|