@vercel/queue 0.0.0-alpha.9 → 0.0.2
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 +478 -603
- package/dist/index.d.mts +497 -188
- package/dist/index.d.ts +497 -188
- package/dist/index.js +1419 -765
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1395 -757
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -22,45 +32,66 @@ var index_exports = {};
|
|
|
22
32
|
__export(index_exports, {
|
|
23
33
|
BadRequestError: () => BadRequestError,
|
|
24
34
|
BufferTransport: () => BufferTransport,
|
|
35
|
+
CLOUD_EVENT_TYPE_V1BETA: () => CLOUD_EVENT_TYPE_V1BETA,
|
|
36
|
+
CLOUD_EVENT_TYPE_V2BETA: () => CLOUD_EVENT_TYPE_V2BETA,
|
|
37
|
+
ConsumerDiscoveryError: () => ConsumerDiscoveryError,
|
|
38
|
+
ConsumerRegistryNotConfiguredError: () => ConsumerRegistryNotConfiguredError,
|
|
39
|
+
DuplicateMessageError: () => DuplicateMessageError,
|
|
25
40
|
ForbiddenError: () => ForbiddenError,
|
|
26
41
|
InternalServerError: () => InternalServerError,
|
|
27
42
|
InvalidLimitError: () => InvalidLimitError,
|
|
28
43
|
JsonTransport: () => JsonTransport,
|
|
44
|
+
MessageAlreadyProcessedError: () => MessageAlreadyProcessedError,
|
|
29
45
|
MessageCorruptedError: () => MessageCorruptedError,
|
|
30
46
|
MessageLockedError: () => MessageLockedError,
|
|
31
47
|
MessageNotAvailableError: () => MessageNotAvailableError,
|
|
32
48
|
MessageNotFoundError: () => MessageNotFoundError,
|
|
49
|
+
QueueClient: () => QueueClient,
|
|
33
50
|
QueueEmptyError: () => QueueEmptyError,
|
|
34
51
|
StreamTransport: () => StreamTransport,
|
|
35
52
|
UnauthorizedError: () => UnauthorizedError,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
send: () => send
|
|
53
|
+
parseCallback: () => parseCallback,
|
|
54
|
+
parseRawCallback: () => parseRawCallback
|
|
39
55
|
});
|
|
40
56
|
module.exports = __toCommonJS(index_exports);
|
|
41
57
|
|
|
42
58
|
// src/transports.ts
|
|
59
|
+
async function streamToBuffer(stream) {
|
|
60
|
+
let totalLength = 0;
|
|
61
|
+
const reader = stream.getReader();
|
|
62
|
+
const chunks = [];
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const { done, value } = await reader.read();
|
|
66
|
+
if (done) break;
|
|
67
|
+
chunks.push(value);
|
|
68
|
+
totalLength += value.length;
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
reader.releaseLock();
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat(chunks, totalLength);
|
|
74
|
+
}
|
|
43
75
|
var JsonTransport = class {
|
|
44
76
|
contentType = "application/json";
|
|
77
|
+
replacer;
|
|
78
|
+
reviver;
|
|
79
|
+
/**
|
|
80
|
+
* Create a new JsonTransport.
|
|
81
|
+
* @param options - Optional JSON serialization options
|
|
82
|
+
* @param options.replacer - Custom replacer for JSON.stringify
|
|
83
|
+
* @param options.reviver - Custom reviver for JSON.parse
|
|
84
|
+
*/
|
|
85
|
+
constructor(options = {}) {
|
|
86
|
+
this.replacer = options.replacer;
|
|
87
|
+
this.reviver = options.reviver;
|
|
88
|
+
}
|
|
45
89
|
serialize(value) {
|
|
46
|
-
return Buffer.from(JSON.stringify(value), "utf8");
|
|
90
|
+
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
47
91
|
}
|
|
48
92
|
async deserialize(stream) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const chunks = [];
|
|
52
|
-
try {
|
|
53
|
-
while (true) {
|
|
54
|
-
const { done, value } = await reader.read();
|
|
55
|
-
if (done) break;
|
|
56
|
-
chunks.push(value);
|
|
57
|
-
totalLength += value.length;
|
|
58
|
-
}
|
|
59
|
-
} finally {
|
|
60
|
-
reader.releaseLock();
|
|
61
|
-
}
|
|
62
|
-
const buffer = Buffer.concat(chunks, totalLength);
|
|
63
|
-
return JSON.parse(buffer.toString("utf8"));
|
|
93
|
+
const buffer = await streamToBuffer(stream);
|
|
94
|
+
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
64
95
|
}
|
|
65
96
|
};
|
|
66
97
|
var BufferTransport = class {
|
|
@@ -69,25 +100,7 @@ var BufferTransport = class {
|
|
|
69
100
|
return value;
|
|
70
101
|
}
|
|
71
102
|
async deserialize(stream) {
|
|
72
|
-
|
|
73
|
-
const chunks = [];
|
|
74
|
-
try {
|
|
75
|
-
while (true) {
|
|
76
|
-
const { done, value } = await reader.read();
|
|
77
|
-
if (done) break;
|
|
78
|
-
chunks.push(value);
|
|
79
|
-
}
|
|
80
|
-
} finally {
|
|
81
|
-
reader.releaseLock();
|
|
82
|
-
}
|
|
83
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
84
|
-
const buffer = new Uint8Array(totalLength);
|
|
85
|
-
let offset = 0;
|
|
86
|
-
for (const chunk of chunks) {
|
|
87
|
-
buffer.set(chunk, offset);
|
|
88
|
-
offset += chunk.length;
|
|
89
|
-
}
|
|
90
|
-
return Buffer.from(buffer);
|
|
103
|
+
return await streamToBuffer(stream);
|
|
91
104
|
}
|
|
92
105
|
};
|
|
93
106
|
var StreamTransport = class {
|
|
@@ -98,6 +111,10 @@ var StreamTransport = class {
|
|
|
98
111
|
async deserialize(stream) {
|
|
99
112
|
return stream;
|
|
100
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Consume any remaining stream data to prevent resource leaks.
|
|
116
|
+
* Called automatically by ConsumerGroup; manual call required for direct client usage.
|
|
117
|
+
*/
|
|
101
118
|
async finalize(payload) {
|
|
102
119
|
const reader = payload.getReader();
|
|
103
120
|
try {
|
|
@@ -111,20 +128,12 @@ var StreamTransport = class {
|
|
|
111
128
|
}
|
|
112
129
|
};
|
|
113
130
|
|
|
114
|
-
// src/client.ts
|
|
131
|
+
// src/api-client.ts
|
|
115
132
|
var import_mixpart = require("mixpart");
|
|
116
133
|
|
|
117
|
-
// src/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const fromSymbol = globalThis;
|
|
121
|
-
const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
|
|
122
|
-
const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
|
|
123
|
-
if (!token) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
return token;
|
|
127
|
-
}
|
|
134
|
+
// src/dev.ts
|
|
135
|
+
var fs = __toESM(require("fs"));
|
|
136
|
+
var path = __toESM(require("path"));
|
|
128
137
|
|
|
129
138
|
// src/types.ts
|
|
130
139
|
var MessageNotFoundError = class extends Error {
|
|
@@ -156,6 +165,7 @@ var QueueEmptyError = class extends Error {
|
|
|
156
165
|
}
|
|
157
166
|
};
|
|
158
167
|
var MessageLockedError = class extends Error {
|
|
168
|
+
/** Suggested retry delay in seconds, if provided by the server. */
|
|
159
169
|
retryAfter;
|
|
160
170
|
constructor(messageId, retryAfter) {
|
|
161
171
|
const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
|
|
@@ -194,635 +204,305 @@ var InvalidLimitError = class extends Error {
|
|
|
194
204
|
this.name = "InvalidLimitError";
|
|
195
205
|
}
|
|
196
206
|
};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
while (true) {
|
|
203
|
-
const { done } = await reader.read();
|
|
204
|
-
if (done) break;
|
|
205
|
-
}
|
|
206
|
-
} finally {
|
|
207
|
-
reader.releaseLock();
|
|
207
|
+
var MessageAlreadyProcessedError = class extends Error {
|
|
208
|
+
constructor(messageId) {
|
|
209
|
+
super(`Message ${messageId} has already been processed`);
|
|
210
|
+
this.name = "MessageAlreadyProcessedError";
|
|
208
211
|
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (!messageId || !timestamp || !ticket) {
|
|
217
|
-
return null;
|
|
212
|
+
};
|
|
213
|
+
var DuplicateMessageError = class extends Error {
|
|
214
|
+
idempotencyKey;
|
|
215
|
+
constructor(message, idempotencyKey) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.name = "DuplicateMessageError";
|
|
218
|
+
this.idempotencyKey = idempotencyKey;
|
|
218
219
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
};
|
|
221
|
+
var ConsumerDiscoveryError = class extends Error {
|
|
222
|
+
deploymentId;
|
|
223
|
+
constructor(message, deploymentId) {
|
|
224
|
+
super(message);
|
|
225
|
+
this.name = "ConsumerDiscoveryError";
|
|
226
|
+
this.deploymentId = deploymentId;
|
|
222
227
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
};
|
|
229
|
+
var ConsumerRegistryNotConfiguredError = class extends Error {
|
|
230
|
+
constructor(message = "Consumer registry not configured") {
|
|
231
|
+
super(message);
|
|
232
|
+
this.name = "ConsumerRegistryNotConfiguredError";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/consumer-group.ts
|
|
237
|
+
var DEFAULT_VISIBILITY_TIMEOUT_SECONDS = 300;
|
|
238
|
+
var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
|
|
239
|
+
var MAX_RENEWAL_INTERVAL_SECONDS = 60;
|
|
240
|
+
var MIN_RENEWAL_INTERVAL_SECONDS = 10;
|
|
241
|
+
var RETRY_INTERVAL_MS = 3e3;
|
|
242
|
+
function calculateRenewalInterval(visibilityTimeoutSeconds) {
|
|
243
|
+
return Math.min(
|
|
244
|
+
MAX_RENEWAL_INTERVAL_SECONDS,
|
|
245
|
+
Math.max(MIN_RENEWAL_INTERVAL_SECONDS, visibilityTimeoutSeconds / 5)
|
|
246
|
+
);
|
|
230
247
|
}
|
|
231
|
-
var
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
248
|
+
var ConsumerGroup = class {
|
|
249
|
+
client;
|
|
250
|
+
topicName;
|
|
251
|
+
consumerGroupName;
|
|
252
|
+
visibilityTimeout;
|
|
235
253
|
/**
|
|
236
|
-
* Create a new
|
|
237
|
-
*
|
|
254
|
+
* Create a new ConsumerGroup instance.
|
|
255
|
+
*
|
|
256
|
+
* @param client - ApiClient instance to use for API calls (transport is configured on the client)
|
|
257
|
+
* @param topicName - Name of the topic to consume from (pattern: `[A-Za-z0-9_-]+`)
|
|
258
|
+
* @param consumerGroupName - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
|
|
259
|
+
* @param options - Optional configuration
|
|
260
|
+
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
|
|
238
261
|
*/
|
|
239
|
-
constructor(options = {}) {
|
|
240
|
-
this.
|
|
241
|
-
this.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
throw new Error(
|
|
248
|
-
"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'"
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
this.token = token;
|
|
252
|
-
}
|
|
262
|
+
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
263
|
+
this.client = client;
|
|
264
|
+
this.topicName = topicName;
|
|
265
|
+
this.consumerGroupName = consumerGroupName;
|
|
266
|
+
this.visibilityTimeout = Math.max(
|
|
267
|
+
MIN_VISIBILITY_TIMEOUT_SECONDS,
|
|
268
|
+
options.visibilityTimeoutSeconds ?? DEFAULT_VISIBILITY_TIMEOUT_SECONDS
|
|
269
|
+
);
|
|
253
270
|
}
|
|
254
271
|
/**
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
261
|
-
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
262
|
-
* @throws {InternalServerError} When server encounters an error
|
|
272
|
+
* Check if an error is a 4xx client error that should stop retries.
|
|
273
|
+
* 4xx errors indicate the request is fundamentally invalid and retrying won't help.
|
|
274
|
+
* - 409: Ticket mismatch (lost ownership to another consumer)
|
|
275
|
+
* - 404: Message/receipt handle not found
|
|
276
|
+
* - 400, 401, 403: Other client errors
|
|
263
277
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
});
|
|
271
|
-
if (process.env.VERCEL_DEPLOYMENT_ID) {
|
|
272
|
-
headers.set("Vqs-Deployment-Id", process.env.VERCEL_DEPLOYMENT_ID);
|
|
273
|
-
}
|
|
274
|
-
if (idempotencyKey) {
|
|
275
|
-
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
276
|
-
}
|
|
277
|
-
if (retentionSeconds !== void 0) {
|
|
278
|
-
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
279
|
-
}
|
|
280
|
-
const body = transport.serialize(payload);
|
|
281
|
-
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
282
|
-
method: "POST",
|
|
283
|
-
headers,
|
|
284
|
-
body
|
|
285
|
-
});
|
|
286
|
-
if (!response.ok) {
|
|
287
|
-
if (response.status === 400) {
|
|
288
|
-
const errorText = await response.text();
|
|
289
|
-
throw new BadRequestError(errorText || "Invalid parameters");
|
|
290
|
-
}
|
|
291
|
-
if (response.status === 401) {
|
|
292
|
-
throw new UnauthorizedError();
|
|
293
|
-
}
|
|
294
|
-
if (response.status === 403) {
|
|
295
|
-
throw new ForbiddenError();
|
|
296
|
-
}
|
|
297
|
-
if (response.status === 409) {
|
|
298
|
-
throw new Error("Duplicate idempotency key detected");
|
|
299
|
-
}
|
|
300
|
-
if (response.status >= 500) {
|
|
301
|
-
throw new InternalServerError(
|
|
302
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
throw new Error(
|
|
306
|
-
`Failed to send message: ${response.status} ${response.statusText}`
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
const responseData = await response.json();
|
|
310
|
-
return responseData;
|
|
278
|
+
isClientError(error) {
|
|
279
|
+
return error instanceof MessageNotAvailableError || // 409 - ticket mismatch, lost ownership
|
|
280
|
+
error instanceof MessageNotFoundError || // 404 - receipt handle not found
|
|
281
|
+
error instanceof BadRequestError || // 400 - invalid parameters
|
|
282
|
+
error instanceof UnauthorizedError || // 401 - auth failed
|
|
283
|
+
error instanceof ForbiddenError;
|
|
311
284
|
}
|
|
312
285
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
286
|
+
* Starts a background loop that periodically extends the visibility timeout for a message.
|
|
287
|
+
*
|
|
288
|
+
* Timing strategy:
|
|
289
|
+
* - Renewal interval: min(60s, max(10s, visibilityTimeout/5))
|
|
290
|
+
* - Extensions request the same duration as the initial visibility timeout
|
|
291
|
+
* - When `visibilityDeadline` is provided (binary mode small body), the first
|
|
292
|
+
* extension delay is calculated from the time remaining until the deadline
|
|
293
|
+
* using the same renewal formula, ensuring the first extension fires before
|
|
294
|
+
* the server-assigned lease expires. Subsequent renewals use the standard interval.
|
|
295
|
+
*
|
|
296
|
+
* Retry strategy:
|
|
297
|
+
* - On transient failures (5xx, network errors): retry every 3 seconds
|
|
298
|
+
* - On 4xx client errors: stop retrying (the lease is lost or invalid)
|
|
299
|
+
*
|
|
300
|
+
* @param receiptHandle - The receipt handle to extend visibility for
|
|
301
|
+
* @param options - Optional configuration
|
|
302
|
+
* @param options.visibilityDeadline - Absolute deadline (from server's `ce-vqsvisibilitydeadline`)
|
|
303
|
+
* when the current visibility timeout expires. Used to calculate the first extension delay.
|
|
324
304
|
*/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
if (limit !== void 0) {
|
|
343
|
-
headers.set("Vqs-Limit", limit.toString());
|
|
305
|
+
startVisibilityExtension(receiptHandle, options) {
|
|
306
|
+
let isRunning = true;
|
|
307
|
+
let isResolved = false;
|
|
308
|
+
let resolveLifecycle;
|
|
309
|
+
let timeoutId = null;
|
|
310
|
+
const renewalIntervalMs = calculateRenewalInterval(this.visibilityTimeout) * 1e3;
|
|
311
|
+
let firstDelayMs = renewalIntervalMs;
|
|
312
|
+
if (options?.visibilityDeadline) {
|
|
313
|
+
const timeRemainingMs = options.visibilityDeadline.getTime() - Date.now();
|
|
314
|
+
if (timeRemainingMs > 0) {
|
|
315
|
+
const timeRemainingSeconds = timeRemainingMs / 1e3;
|
|
316
|
+
firstDelayMs = calculateRenewalInterval(timeRemainingSeconds) * 1e3;
|
|
317
|
+
} else {
|
|
318
|
+
firstDelayMs = 0;
|
|
319
|
+
}
|
|
344
320
|
}
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
headers
|
|
321
|
+
const lifecyclePromise = new Promise((resolve) => {
|
|
322
|
+
resolveLifecycle = resolve;
|
|
348
323
|
});
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (response.status === 400) {
|
|
354
|
-
const errorText = await response.text();
|
|
355
|
-
throw new BadRequestError(errorText || "Invalid parameters");
|
|
356
|
-
}
|
|
357
|
-
if (response.status === 401) {
|
|
358
|
-
throw new UnauthorizedError();
|
|
324
|
+
const safeResolve = () => {
|
|
325
|
+
if (!isResolved) {
|
|
326
|
+
isResolved = true;
|
|
327
|
+
resolveLifecycle();
|
|
359
328
|
}
|
|
360
|
-
|
|
361
|
-
|
|
329
|
+
};
|
|
330
|
+
const extend = async () => {
|
|
331
|
+
if (!isRunning) {
|
|
332
|
+
safeResolve();
|
|
333
|
+
return;
|
|
362
334
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
335
|
+
try {
|
|
336
|
+
await this.client.changeVisibility({
|
|
337
|
+
queueName: this.topicName,
|
|
338
|
+
consumerGroup: this.consumerGroupName,
|
|
339
|
+
receiptHandle,
|
|
340
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
341
|
+
});
|
|
342
|
+
if (isRunning) {
|
|
343
|
+
timeoutId = setTimeout(() => extend(), renewalIntervalMs);
|
|
344
|
+
} else {
|
|
345
|
+
safeResolve();
|
|
369
346
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
347
|
+
} catch (error) {
|
|
348
|
+
if (this.isClientError(error)) {
|
|
349
|
+
console.error(
|
|
350
|
+
`Visibility extension failed with client error for receipt handle ${receiptHandle} (stopping retries):`,
|
|
351
|
+
error
|
|
352
|
+
);
|
|
353
|
+
safeResolve();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
console.error(
|
|
357
|
+
`Failed to extend visibility for receipt handle ${receiptHandle} (will retry in ${RETRY_INTERVAL_MS / 1e3}s):`,
|
|
358
|
+
error
|
|
375
359
|
);
|
|
360
|
+
if (isRunning) {
|
|
361
|
+
timeoutId = setTimeout(() => extend(), RETRY_INTERVAL_MS);
|
|
362
|
+
} else {
|
|
363
|
+
safeResolve();
|
|
364
|
+
}
|
|
376
365
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
366
|
+
};
|
|
367
|
+
timeoutId = setTimeout(() => extend(), firstDelayMs);
|
|
368
|
+
return async (waitForCompletion = false) => {
|
|
369
|
+
isRunning = false;
|
|
370
|
+
if (timeoutId) {
|
|
371
|
+
clearTimeout(timeoutId);
|
|
372
|
+
timeoutId = null;
|
|
373
|
+
}
|
|
374
|
+
if (waitForCompletion) {
|
|
375
|
+
await lifecyclePromise;
|
|
376
|
+
} else {
|
|
377
|
+
safeResolve();
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Clean up the message payload if the transport supports it and payload exists.
|
|
383
|
+
*/
|
|
384
|
+
async finalizePayload(payload) {
|
|
385
|
+
const transport = this.client.getTransport();
|
|
386
|
+
if (transport.finalize && payload !== void 0 && payload !== null) {
|
|
382
387
|
try {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
await consumeStream(multipartMessage.payload);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
const deserializedPayload = await transport.deserialize(
|
|
390
|
-
multipartMessage.payload
|
|
391
|
-
);
|
|
392
|
-
const message = {
|
|
393
|
-
...parsedHeaders,
|
|
394
|
-
payload: deserializedPayload
|
|
395
|
-
};
|
|
396
|
-
yield message;
|
|
397
|
-
} catch (error) {
|
|
398
|
-
console.warn("Failed to process multipart message:", error);
|
|
399
|
-
await consumeStream(multipartMessage.payload);
|
|
388
|
+
await transport.finalize(payload);
|
|
389
|
+
} catch (finalizeError) {
|
|
390
|
+
console.warn("Failed to finalize message payload:", finalizeError);
|
|
400
391
|
}
|
|
401
392
|
}
|
|
402
393
|
}
|
|
403
|
-
async
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
messageId,
|
|
408
|
-
visibilityTimeoutSeconds,
|
|
409
|
-
skipPayload
|
|
410
|
-
} = options;
|
|
411
|
-
const headers = new Headers({
|
|
412
|
-
Authorization: `Bearer ${this.token}`,
|
|
413
|
-
"Vqs-Queue-Name": queueName,
|
|
414
|
-
"Vqs-Consumer-Group": consumerGroup,
|
|
415
|
-
Accept: "multipart/mixed"
|
|
416
|
-
});
|
|
417
|
-
if (visibilityTimeoutSeconds !== void 0) {
|
|
418
|
-
headers.set(
|
|
419
|
-
"Vqs-Visibility-Timeout",
|
|
420
|
-
visibilityTimeoutSeconds.toString()
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
if (skipPayload) {
|
|
424
|
-
headers.set("Vqs-Skip-Payload", "1");
|
|
425
|
-
}
|
|
426
|
-
const response = await fetch(
|
|
427
|
-
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
428
|
-
{
|
|
429
|
-
method: "GET",
|
|
430
|
-
headers
|
|
431
|
-
}
|
|
394
|
+
async processMessage(message, handler, options) {
|
|
395
|
+
const stopExtension = this.startVisibilityExtension(
|
|
396
|
+
message.receiptHandle,
|
|
397
|
+
options
|
|
432
398
|
);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
399
|
+
const metadata = {
|
|
400
|
+
messageId: message.messageId,
|
|
401
|
+
deliveryCount: message.deliveryCount,
|
|
402
|
+
createdAt: message.createdAt,
|
|
403
|
+
expiresAt: message.expiresAt,
|
|
404
|
+
topicName: this.topicName,
|
|
405
|
+
consumerGroup: this.consumerGroupName,
|
|
406
|
+
region: this.client.getRegion()
|
|
407
|
+
};
|
|
408
|
+
try {
|
|
409
|
+
await handler(message.payload, metadata);
|
|
410
|
+
await stopExtension();
|
|
411
|
+
await this.client.acknowledgeMessage({
|
|
412
|
+
queueName: this.topicName,
|
|
413
|
+
consumerGroup: this.consumerGroupName,
|
|
414
|
+
receiptHandle: message.receiptHandle
|
|
415
|
+
});
|
|
416
|
+
} catch (error) {
|
|
417
|
+
await stopExtension();
|
|
418
|
+
if (options?.retry) {
|
|
419
|
+
let directive;
|
|
420
|
+
try {
|
|
421
|
+
directive = options.retry(error, metadata);
|
|
422
|
+
} catch (retryError) {
|
|
423
|
+
console.warn("retry handler threw:", retryError);
|
|
424
|
+
}
|
|
425
|
+
if (directive) {
|
|
426
|
+
if ("acknowledge" in directive && directive.acknowledge) {
|
|
427
|
+
try {
|
|
428
|
+
await this.client.acknowledgeMessage({
|
|
429
|
+
queueName: this.topicName,
|
|
430
|
+
consumerGroup: this.consumerGroupName,
|
|
431
|
+
receiptHandle: message.receiptHandle
|
|
432
|
+
});
|
|
433
|
+
} catch (ackError) {
|
|
434
|
+
console.warn("Failed to acknowledge message:", ackError);
|
|
435
|
+
}
|
|
436
|
+
await this.finalizePayload(message.payload);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if ("afterSeconds" in directive && typeof directive.afterSeconds === "number") {
|
|
440
|
+
try {
|
|
441
|
+
await this.client.changeVisibility({
|
|
442
|
+
queueName: this.topicName,
|
|
443
|
+
consumerGroup: this.consumerGroupName,
|
|
444
|
+
receiptHandle: message.receiptHandle,
|
|
445
|
+
visibilityTimeoutSeconds: directive.afterSeconds
|
|
446
|
+
});
|
|
447
|
+
} catch (changeError) {
|
|
448
|
+
console.warn(
|
|
449
|
+
"Failed to reschedule message for retry:",
|
|
450
|
+
changeError
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
await this.finalizePayload(message.payload);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
453
456
|
}
|
|
454
|
-
throw new MessageLockedError(messageId, retryAfter);
|
|
455
|
-
}
|
|
456
|
-
if (response.status === 409) {
|
|
457
|
-
throw new MessageNotAvailableError(messageId);
|
|
458
|
-
}
|
|
459
|
-
if (response.status >= 500) {
|
|
460
|
-
throw new InternalServerError(
|
|
461
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
462
|
-
);
|
|
463
457
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
);
|
|
458
|
+
await this.finalizePayload(message.payload);
|
|
459
|
+
throw error;
|
|
467
460
|
}
|
|
468
|
-
if (skipPayload && response.status === 204) {
|
|
469
|
-
const parsedHeaders = parseQueueHeaders(response.headers);
|
|
470
|
-
if (!parsedHeaders) {
|
|
471
|
-
throw new MessageCorruptedError(
|
|
472
|
-
messageId,
|
|
473
|
-
"Missing required queue headers in 204 response"
|
|
474
|
-
);
|
|
475
|
-
}
|
|
476
|
-
const message = {
|
|
477
|
-
...parsedHeaders,
|
|
478
|
-
payload: void 0
|
|
479
|
-
};
|
|
480
|
-
return { message };
|
|
481
|
-
}
|
|
482
|
-
if (!transport) {
|
|
483
|
-
throw new Error("Transport is required when skipPayload is not true");
|
|
484
|
-
}
|
|
485
|
-
try {
|
|
486
|
-
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
487
|
-
try {
|
|
488
|
-
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
489
|
-
if (!parsedHeaders) {
|
|
490
|
-
console.warn("Missing required queue headers in multipart part");
|
|
491
|
-
await consumeStream(multipartMessage.payload);
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
const deserializedPayload = await transport.deserialize(
|
|
495
|
-
multipartMessage.payload
|
|
496
|
-
);
|
|
497
|
-
const message = {
|
|
498
|
-
...parsedHeaders,
|
|
499
|
-
payload: deserializedPayload
|
|
500
|
-
};
|
|
501
|
-
return { message };
|
|
502
|
-
} catch (error) {
|
|
503
|
-
console.warn("Failed to deserialize message by ID:", error);
|
|
504
|
-
await consumeStream(multipartMessage.payload);
|
|
505
|
-
throw new MessageCorruptedError(
|
|
506
|
-
messageId,
|
|
507
|
-
`Failed to deserialize payload: ${error}`
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
} catch (error) {
|
|
512
|
-
if (error instanceof MessageCorruptedError) {
|
|
513
|
-
throw error;
|
|
514
|
-
}
|
|
515
|
-
throw new MessageCorruptedError(
|
|
516
|
-
messageId,
|
|
517
|
-
`Failed to parse multipart response: ${error}`
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
throw new MessageNotFoundError(messageId);
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* Delete a message (acknowledge processing)
|
|
524
|
-
* @param options Delete message options
|
|
525
|
-
* @returns Promise with delete status
|
|
526
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
527
|
-
* @throws {MessageNotAvailableError} When message can't be deleted (409)
|
|
528
|
-
* @throws {BadRequestError} When ticket is missing or invalid (400)
|
|
529
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
530
|
-
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
531
|
-
* @throws {InternalServerError} When server encounters an error
|
|
532
|
-
*/
|
|
533
|
-
async deleteMessage(options) {
|
|
534
|
-
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
535
|
-
const response = await fetch(
|
|
536
|
-
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
537
|
-
{
|
|
538
|
-
method: "DELETE",
|
|
539
|
-
headers: new Headers({
|
|
540
|
-
Authorization: `Bearer ${this.token}`,
|
|
541
|
-
"Vqs-Queue-Name": queueName,
|
|
542
|
-
"Vqs-Consumer-Group": consumerGroup,
|
|
543
|
-
"Vqs-Ticket": ticket
|
|
544
|
-
})
|
|
545
|
-
}
|
|
546
|
-
);
|
|
547
|
-
if (!response.ok) {
|
|
548
|
-
if (response.status === 400) {
|
|
549
|
-
throw new BadRequestError("Missing or invalid ticket");
|
|
550
|
-
}
|
|
551
|
-
if (response.status === 401) {
|
|
552
|
-
throw new UnauthorizedError();
|
|
553
|
-
}
|
|
554
|
-
if (response.status === 403) {
|
|
555
|
-
throw new ForbiddenError();
|
|
556
|
-
}
|
|
557
|
-
if (response.status === 404) {
|
|
558
|
-
throw new MessageNotFoundError(messageId);
|
|
559
|
-
}
|
|
560
|
-
if (response.status === 409) {
|
|
561
|
-
throw new MessageNotAvailableError(
|
|
562
|
-
messageId,
|
|
563
|
-
"Invalid ticket, message not in correct state, or already processed"
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
if (response.status >= 500) {
|
|
567
|
-
throw new InternalServerError(
|
|
568
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
throw new Error(
|
|
572
|
-
`Failed to delete message: ${response.status} ${response.statusText}`
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
return { deleted: true };
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Change the visibility timeout of a message
|
|
579
|
-
* @param options Change visibility options
|
|
580
|
-
* @returns Promise with update status
|
|
581
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
582
|
-
* @throws {MessageNotAvailableError} When message can't be updated (409)
|
|
583
|
-
* @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
|
|
584
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
585
|
-
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
586
|
-
* @throws {InternalServerError} When server encounters an error
|
|
587
|
-
*/
|
|
588
|
-
async changeVisibility(options) {
|
|
589
|
-
const {
|
|
590
|
-
queueName,
|
|
591
|
-
consumerGroup,
|
|
592
|
-
messageId,
|
|
593
|
-
ticket,
|
|
594
|
-
visibilityTimeoutSeconds
|
|
595
|
-
} = options;
|
|
596
|
-
const response = await fetch(
|
|
597
|
-
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
598
|
-
{
|
|
599
|
-
method: "PATCH",
|
|
600
|
-
headers: new Headers({
|
|
601
|
-
Authorization: `Bearer ${this.token}`,
|
|
602
|
-
"Vqs-Queue-Name": queueName,
|
|
603
|
-
"Vqs-Consumer-Group": consumerGroup,
|
|
604
|
-
"Vqs-Ticket": ticket,
|
|
605
|
-
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString()
|
|
606
|
-
})
|
|
607
|
-
}
|
|
608
|
-
);
|
|
609
|
-
if (!response.ok) {
|
|
610
|
-
if (response.status === 400) {
|
|
611
|
-
throw new BadRequestError(
|
|
612
|
-
"Missing ticket or invalid visibility timeout"
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
if (response.status === 401) {
|
|
616
|
-
throw new UnauthorizedError();
|
|
617
|
-
}
|
|
618
|
-
if (response.status === 403) {
|
|
619
|
-
throw new ForbiddenError();
|
|
620
|
-
}
|
|
621
|
-
if (response.status === 404) {
|
|
622
|
-
throw new MessageNotFoundError(messageId);
|
|
623
|
-
}
|
|
624
|
-
if (response.status === 409) {
|
|
625
|
-
throw new MessageNotAvailableError(
|
|
626
|
-
messageId,
|
|
627
|
-
"Invalid ticket, message not in correct state, or already processed"
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
if (response.status >= 500) {
|
|
631
|
-
throw new InternalServerError(
|
|
632
|
-
`Server error: ${response.status} ${response.statusText}`
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
throw new Error(
|
|
636
|
-
`Failed to change visibility: ${response.status} ${response.statusText}`
|
|
637
|
-
);
|
|
638
|
-
}
|
|
639
|
-
return { updated: true };
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
// src/consumer-group.ts
|
|
644
|
-
var ConsumerGroup = class {
|
|
645
|
-
client;
|
|
646
|
-
topicName;
|
|
647
|
-
consumerGroupName;
|
|
648
|
-
visibilityTimeout;
|
|
649
|
-
refreshInterval;
|
|
650
|
-
transport;
|
|
651
|
-
/**
|
|
652
|
-
* Create a new ConsumerGroup instance
|
|
653
|
-
* @param client QueueClient instance to use for API calls
|
|
654
|
-
* @param topicName Name of the topic to consume from
|
|
655
|
-
* @param consumerGroupName Name of the consumer group
|
|
656
|
-
* @param options Optional configuration
|
|
657
|
-
*/
|
|
658
|
-
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
659
|
-
this.client = client;
|
|
660
|
-
this.topicName = topicName;
|
|
661
|
-
this.consumerGroupName = consumerGroupName;
|
|
662
|
-
this.visibilityTimeout = options.visibilityTimeoutSeconds || 30;
|
|
663
|
-
this.refreshInterval = options.refreshInterval || 10;
|
|
664
|
-
this.transport = options.transport || new JsonTransport();
|
|
665
461
|
}
|
|
666
462
|
/**
|
|
667
|
-
*
|
|
668
|
-
* This prevents the message from becoming visible to other consumers while it's being processed.
|
|
463
|
+
* Process a pre-fetched message directly, without calling `receiveMessageById`.
|
|
669
464
|
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
465
|
+
* Used by the binary mode (v2beta) small body fast path, where the server
|
|
466
|
+
* pushes the full message payload in the callback request. The message is
|
|
467
|
+
* processed with the same lifecycle guarantees as `consume()`:
|
|
468
|
+
* - Visibility timeout is extended periodically during processing
|
|
469
|
+
* - Message is acknowledged on successful handler completion
|
|
470
|
+
* - Payload is finalized on error if the transport supports it
|
|
672
471
|
*
|
|
673
|
-
* @param
|
|
674
|
-
* @param
|
|
675
|
-
* @
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
* - If an extension fails, the loop terminates with an error logged to console
|
|
680
|
-
* - The returned stop function is idempotent - calling it multiple times is safe
|
|
681
|
-
* - By default, the stop function returns immediately without waiting for in-flight
|
|
682
|
-
* - Pass `true` to the stop function to wait for any in-flight extension to complete
|
|
683
|
-
*/
|
|
684
|
-
startVisibilityExtension(messageId, ticket) {
|
|
685
|
-
let isRunning = true;
|
|
686
|
-
let resolveLifecycle;
|
|
687
|
-
let timeoutId = null;
|
|
688
|
-
const lifecyclePromise = new Promise((resolve) => {
|
|
689
|
-
resolveLifecycle = resolve;
|
|
690
|
-
});
|
|
691
|
-
const extend = async () => {
|
|
692
|
-
if (!isRunning) {
|
|
693
|
-
resolveLifecycle();
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
try {
|
|
697
|
-
await this.client.changeVisibility({
|
|
698
|
-
queueName: this.topicName,
|
|
699
|
-
consumerGroup: this.consumerGroupName,
|
|
700
|
-
messageId,
|
|
701
|
-
ticket,
|
|
702
|
-
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
703
|
-
});
|
|
704
|
-
if (isRunning) {
|
|
705
|
-
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
706
|
-
} else {
|
|
707
|
-
resolveLifecycle();
|
|
708
|
-
}
|
|
709
|
-
} catch (error) {
|
|
710
|
-
console.error(
|
|
711
|
-
`Failed to extend visibility for message ${messageId}:`,
|
|
712
|
-
error
|
|
713
|
-
);
|
|
714
|
-
resolveLifecycle();
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
718
|
-
return async (waitForCompletion = false) => {
|
|
719
|
-
isRunning = false;
|
|
720
|
-
if (timeoutId) {
|
|
721
|
-
clearTimeout(timeoutId);
|
|
722
|
-
timeoutId = null;
|
|
723
|
-
}
|
|
724
|
-
if (waitForCompletion) {
|
|
725
|
-
await lifecyclePromise;
|
|
726
|
-
} else {
|
|
727
|
-
resolveLifecycle();
|
|
728
|
-
}
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Process a single message with the given handler
|
|
733
|
-
* @param message The message to process
|
|
734
|
-
* @param handler Function to process the message
|
|
472
|
+
* @param handler - Function to process the message payload and metadata
|
|
473
|
+
* @param message - The complete message including payload and receipt handle
|
|
474
|
+
* @param options - Optional configuration
|
|
475
|
+
* @param options.visibilityDeadline - Absolute deadline when the server-assigned
|
|
476
|
+
* visibility timeout expires (from `ce-vqsvisibilitydeadline`). Used to
|
|
477
|
+
* schedule the first visibility extension before the lease expires.
|
|
735
478
|
*/
|
|
736
|
-
async
|
|
737
|
-
|
|
738
|
-
message.messageId,
|
|
739
|
-
message.ticket
|
|
740
|
-
);
|
|
741
|
-
try {
|
|
742
|
-
const result = await handler(message.payload, {
|
|
743
|
-
messageId: message.messageId,
|
|
744
|
-
deliveryCount: message.deliveryCount,
|
|
745
|
-
timestamp: message.timestamp
|
|
746
|
-
});
|
|
747
|
-
await stopExtension();
|
|
748
|
-
if (result && "timeoutSeconds" in result) {
|
|
749
|
-
await this.client.changeVisibility({
|
|
750
|
-
queueName: this.topicName,
|
|
751
|
-
consumerGroup: this.consumerGroupName,
|
|
752
|
-
messageId: message.messageId,
|
|
753
|
-
ticket: message.ticket,
|
|
754
|
-
visibilityTimeoutSeconds: result.timeoutSeconds
|
|
755
|
-
});
|
|
756
|
-
} else {
|
|
757
|
-
await this.client.deleteMessage({
|
|
758
|
-
queueName: this.topicName,
|
|
759
|
-
consumerGroup: this.consumerGroupName,
|
|
760
|
-
messageId: message.messageId,
|
|
761
|
-
ticket: message.ticket
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
} catch (error) {
|
|
765
|
-
await stopExtension();
|
|
766
|
-
if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
767
|
-
try {
|
|
768
|
-
await this.transport.finalize(message.payload);
|
|
769
|
-
} catch (finalizeError) {
|
|
770
|
-
console.warn("Failed to finalize message payload:", finalizeError);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
throw error;
|
|
774
|
-
}
|
|
479
|
+
async consumeMessage(handler, message, options) {
|
|
480
|
+
await this.processMessage(message, handler, options);
|
|
775
481
|
}
|
|
776
482
|
async consume(handler, options) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
this.transport
|
|
788
|
-
);
|
|
789
|
-
await this.processMessage(
|
|
790
|
-
response.message,
|
|
791
|
-
handler
|
|
792
|
-
);
|
|
793
|
-
} else {
|
|
794
|
-
const response = await this.client.receiveMessageById(
|
|
795
|
-
{
|
|
796
|
-
queueName: this.topicName,
|
|
797
|
-
consumerGroup: this.consumerGroupName,
|
|
798
|
-
messageId: options.messageId,
|
|
799
|
-
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
800
|
-
},
|
|
801
|
-
this.transport
|
|
802
|
-
);
|
|
803
|
-
await this.processMessage(
|
|
804
|
-
response.message,
|
|
805
|
-
handler
|
|
806
|
-
);
|
|
807
|
-
}
|
|
483
|
+
const retry = options?.retry;
|
|
484
|
+
if (options && "messageId" in options) {
|
|
485
|
+
const response = await this.client.receiveMessageById({
|
|
486
|
+
queueName: this.topicName,
|
|
487
|
+
consumerGroup: this.consumerGroupName,
|
|
488
|
+
messageId: options.messageId,
|
|
489
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
490
|
+
});
|
|
491
|
+
await this.processMessage(response.message, handler, { retry });
|
|
492
|
+
return 1;
|
|
808
493
|
} else {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
messageFound = true;
|
|
820
|
-
await this.processMessage(message, handler);
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
if (!messageFound) {
|
|
824
|
-
throw new Error("No messages available");
|
|
494
|
+
const limit = options && "limit" in options ? options.limit : 1;
|
|
495
|
+
let messagesProcessed = 0;
|
|
496
|
+
for await (const message of this.client.receiveMessages({
|
|
497
|
+
queueName: this.topicName,
|
|
498
|
+
consumerGroup: this.consumerGroupName,
|
|
499
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
500
|
+
limit
|
|
501
|
+
})) {
|
|
502
|
+
messagesProcessed++;
|
|
503
|
+
await this.processMessage(message, handler, { retry });
|
|
825
504
|
}
|
|
505
|
+
return messagesProcessed;
|
|
826
506
|
}
|
|
827
507
|
}
|
|
828
508
|
/**
|
|
@@ -843,38 +523,40 @@ var ConsumerGroup = class {
|
|
|
843
523
|
var Topic = class {
|
|
844
524
|
client;
|
|
845
525
|
topicName;
|
|
846
|
-
transport;
|
|
847
526
|
/**
|
|
848
|
-
*
|
|
849
|
-
* @param client QueueClient instance to use for API calls
|
|
527
|
+
* @param client ApiClient instance to use for API calls
|
|
850
528
|
* @param topicName Name of the topic to work with
|
|
851
|
-
* @param transport Optional serializer/deserializer for the payload (defaults to JSON)
|
|
852
529
|
*/
|
|
853
|
-
constructor(client, topicName
|
|
530
|
+
constructor(client, topicName) {
|
|
854
531
|
this.client = client;
|
|
855
532
|
this.topicName = topicName;
|
|
856
|
-
this.transport = transport || new JsonTransport();
|
|
857
533
|
}
|
|
858
534
|
/**
|
|
859
535
|
* Publish a message to the topic
|
|
860
536
|
* @param payload The data to publish
|
|
861
537
|
* @param options Optional publish options
|
|
862
|
-
* @returns
|
|
538
|
+
* @returns `{ messageId }` — `messageId` is `null` when deferred
|
|
863
539
|
* @throws {BadRequestError} When request parameters are invalid
|
|
864
540
|
* @throws {UnauthorizedError} When authentication fails
|
|
865
541
|
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
866
542
|
* @throws {InternalServerError} When server encounters an error
|
|
867
543
|
*/
|
|
868
544
|
async publish(payload, options) {
|
|
869
|
-
const result = await this.client.sendMessage(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
)
|
|
545
|
+
const result = await this.client.sendMessage({
|
|
546
|
+
queueName: this.topicName,
|
|
547
|
+
payload,
|
|
548
|
+
idempotencyKey: options?.idempotencyKey,
|
|
549
|
+
retentionSeconds: options?.retentionSeconds,
|
|
550
|
+
delaySeconds: options?.delaySeconds,
|
|
551
|
+
headers: options?.headers
|
|
552
|
+
});
|
|
553
|
+
if (result.messageId && isDevMode()) {
|
|
554
|
+
triggerDevCallbacks(
|
|
555
|
+
this.topicName,
|
|
556
|
+
result.messageId,
|
|
557
|
+
this.client.getRegion()
|
|
558
|
+
);
|
|
559
|
+
}
|
|
878
560
|
return { messageId: result.messageId };
|
|
879
561
|
}
|
|
880
562
|
/**
|
|
@@ -884,15 +566,11 @@ var Topic = class {
|
|
|
884
566
|
* @returns A ConsumerGroup instance
|
|
885
567
|
*/
|
|
886
568
|
consumerGroup(consumerGroupName, options) {
|
|
887
|
-
const consumerOptions = {
|
|
888
|
-
...options,
|
|
889
|
-
transport: options?.transport || this.transport
|
|
890
|
-
};
|
|
891
569
|
return new ConsumerGroup(
|
|
892
570
|
this.client,
|
|
893
571
|
this.topicName,
|
|
894
572
|
consumerGroupName,
|
|
895
|
-
|
|
573
|
+
options
|
|
896
574
|
);
|
|
897
575
|
}
|
|
898
576
|
/**
|
|
@@ -901,148 +579,1124 @@ var Topic = class {
|
|
|
901
579
|
get name() {
|
|
902
580
|
return this.topicName;
|
|
903
581
|
}
|
|
904
|
-
/**
|
|
905
|
-
* Get the transport used by this topic
|
|
906
|
-
*/
|
|
907
|
-
get serializer() {
|
|
908
|
-
return this.transport;
|
|
909
|
-
}
|
|
910
582
|
};
|
|
911
583
|
|
|
912
|
-
// src/
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
|
|
918
|
-
queueName: topicName,
|
|
919
|
-
payload,
|
|
920
|
-
idempotencyKey: options?.idempotencyKey,
|
|
921
|
-
retentionSeconds: options?.retentionSeconds
|
|
922
|
-
},
|
|
923
|
-
transport
|
|
924
|
-
);
|
|
925
|
-
return { messageId: result.messageId };
|
|
584
|
+
// src/callback.ts
|
|
585
|
+
var CLOUD_EVENT_TYPE_V1BETA = "com.vercel.queue.v1beta";
|
|
586
|
+
var CLOUD_EVENT_TYPE_V2BETA = "com.vercel.queue.v2beta";
|
|
587
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
588
|
+
const prefix = pattern.slice(0, -1);
|
|
589
|
+
return topicName.startsWith(prefix);
|
|
926
590
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const client = new QueueClient();
|
|
930
|
-
const topic = new Topic(client, topicName, transport);
|
|
931
|
-
const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
|
|
932
|
-
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
933
|
-
if (messageId) {
|
|
934
|
-
if (skipPayload) {
|
|
935
|
-
return consumer.consume(handler, {
|
|
936
|
-
messageId,
|
|
937
|
-
skipPayload: true
|
|
938
|
-
});
|
|
939
|
-
} else {
|
|
940
|
-
return consumer.consume(handler, { messageId });
|
|
941
|
-
}
|
|
942
|
-
} else {
|
|
943
|
-
return consumer.consume(handler);
|
|
944
|
-
}
|
|
591
|
+
function isRecord(value) {
|
|
592
|
+
return typeof value === "object" && value !== null;
|
|
945
593
|
}
|
|
946
|
-
|
|
947
|
-
// src/callback.ts
|
|
948
|
-
async function parseCallbackRequest(request) {
|
|
949
|
-
const contentType = request.headers.get("content-type");
|
|
594
|
+
function parseV1StructuredBody(body, contentType) {
|
|
950
595
|
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
951
596
|
throw new Error(
|
|
952
597
|
"Invalid content type: expected 'application/cloudevents+json'"
|
|
953
598
|
);
|
|
954
599
|
}
|
|
955
|
-
|
|
956
|
-
try {
|
|
957
|
-
cloudEvent = await request.json();
|
|
958
|
-
} catch (error) {
|
|
959
|
-
throw new Error("Failed to parse CloudEvent from request body");
|
|
960
|
-
}
|
|
961
|
-
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
600
|
+
if (!isRecord(body) || !body.type || !body.source || !body.id || !isRecord(body.data)) {
|
|
962
601
|
throw new Error("Invalid CloudEvent: missing required fields");
|
|
963
602
|
}
|
|
964
|
-
if (
|
|
603
|
+
if (body.type !== CLOUD_EVENT_TYPE_V1BETA) {
|
|
965
604
|
throw new Error(
|
|
966
|
-
`Invalid CloudEvent type: expected '
|
|
605
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V1BETA}', got '${String(body.type)}'`
|
|
967
606
|
);
|
|
968
607
|
}
|
|
608
|
+
const { data } = body;
|
|
969
609
|
const missingFields = [];
|
|
970
|
-
if (!("queueName" in
|
|
971
|
-
if (!("consumerGroup" in
|
|
972
|
-
|
|
973
|
-
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
610
|
+
if (!("queueName" in data)) missingFields.push("queueName");
|
|
611
|
+
if (!("consumerGroup" in data)) missingFields.push("consumerGroup");
|
|
612
|
+
if (!("messageId" in data)) missingFields.push("messageId");
|
|
974
613
|
if (missingFields.length > 0) {
|
|
975
614
|
throw new Error(
|
|
976
615
|
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
977
616
|
);
|
|
978
617
|
}
|
|
979
|
-
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
980
618
|
return {
|
|
619
|
+
queueName: String(data.queueName),
|
|
620
|
+
consumerGroup: String(data.consumerGroup),
|
|
621
|
+
messageId: String(data.messageId)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function getHeader(headers, name) {
|
|
625
|
+
if (headers instanceof Headers) {
|
|
626
|
+
return headers.get(name);
|
|
627
|
+
}
|
|
628
|
+
const value = headers[name];
|
|
629
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
630
|
+
return value ?? null;
|
|
631
|
+
}
|
|
632
|
+
function parseBinaryHeaders(headers) {
|
|
633
|
+
const ceType = getHeader(headers, "ce-type");
|
|
634
|
+
if (ceType !== CLOUD_EVENT_TYPE_V2BETA) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`Invalid CloudEvent type: expected '${CLOUD_EVENT_TYPE_V2BETA}', got '${ceType}'`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const queueName = getHeader(headers, "ce-vqsqueuename");
|
|
640
|
+
const consumerGroup = getHeader(headers, "ce-vqsconsumergroup");
|
|
641
|
+
const messageId = getHeader(headers, "ce-vqsmessageid");
|
|
642
|
+
const missingFields = [];
|
|
643
|
+
if (!queueName) missingFields.push("ce-vqsqueuename");
|
|
644
|
+
if (!consumerGroup) missingFields.push("ce-vqsconsumergroup");
|
|
645
|
+
if (!messageId) missingFields.push("ce-vqsmessageid");
|
|
646
|
+
if (missingFields.length > 0) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Missing required CloudEvent headers: ${missingFields.join(", ")}`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const region = getHeader(headers, "ce-vqsregion") ?? void 0;
|
|
652
|
+
const base = {
|
|
981
653
|
queueName,
|
|
982
654
|
consumerGroup,
|
|
983
|
-
messageId
|
|
655
|
+
messageId,
|
|
656
|
+
region
|
|
984
657
|
};
|
|
658
|
+
const receiptHandle = getHeader(headers, "ce-vqsreceipthandle");
|
|
659
|
+
if (!receiptHandle) {
|
|
660
|
+
return base;
|
|
661
|
+
}
|
|
662
|
+
const result = { ...base, receiptHandle };
|
|
663
|
+
const deliveryCount = getHeader(headers, "ce-vqsdeliverycount");
|
|
664
|
+
if (deliveryCount) {
|
|
665
|
+
result.deliveryCount = parseInt(deliveryCount, 10);
|
|
666
|
+
}
|
|
667
|
+
const createdAt = getHeader(headers, "ce-vqscreatedat");
|
|
668
|
+
if (createdAt) {
|
|
669
|
+
result.createdAt = createdAt;
|
|
670
|
+
}
|
|
671
|
+
const expiresAt = getHeader(headers, "ce-vqsexpiresat");
|
|
672
|
+
if (expiresAt) {
|
|
673
|
+
result.expiresAt = expiresAt;
|
|
674
|
+
}
|
|
675
|
+
const contentType = getHeader(headers, "content-type");
|
|
676
|
+
if (contentType) {
|
|
677
|
+
result.contentType = contentType;
|
|
678
|
+
}
|
|
679
|
+
const visibilityDeadline = getHeader(headers, "ce-vqsvisibilitydeadline");
|
|
680
|
+
if (visibilityDeadline) {
|
|
681
|
+
result.visibilityDeadline = visibilityDeadline;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
985
684
|
}
|
|
986
|
-
function
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const availableTopics = Object.keys(handlers).join(", ");
|
|
993
|
-
return Response.json(
|
|
994
|
-
{
|
|
995
|
-
error: `No handler found for topic: ${queueName}`,
|
|
996
|
-
availableTopics
|
|
997
|
-
},
|
|
998
|
-
{ status: 404 }
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1002
|
-
if (!consumerGroupHandler) {
|
|
1003
|
-
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1004
|
-
return Response.json(
|
|
1005
|
-
{
|
|
1006
|
-
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
1007
|
-
availableGroups
|
|
1008
|
-
},
|
|
1009
|
-
{ status: 404 }
|
|
1010
|
-
);
|
|
1011
|
-
}
|
|
1012
|
-
const client = new QueueClient();
|
|
1013
|
-
const topic = new Topic(client, queueName);
|
|
1014
|
-
const cg = topic.consumerGroup(consumerGroup);
|
|
1015
|
-
await cg.consume(consumerGroupHandler, { messageId });
|
|
1016
|
-
return Response.json({ status: "success" });
|
|
1017
|
-
} catch (error) {
|
|
1018
|
-
console.error("Queue callback error:", error);
|
|
1019
|
-
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"))) {
|
|
1020
|
-
return Response.json({ error: error.message }, { status: 400 });
|
|
1021
|
-
}
|
|
1022
|
-
return Response.json(
|
|
1023
|
-
{ error: "Failed to process queue message" },
|
|
1024
|
-
{ status: 500 }
|
|
1025
|
-
);
|
|
685
|
+
function parseRawCallback(body, headers) {
|
|
686
|
+
const ceType = getHeader(headers, "ce-type");
|
|
687
|
+
if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
|
|
688
|
+
const result = parseBinaryHeaders(headers);
|
|
689
|
+
if ("receiptHandle" in result) {
|
|
690
|
+
result.parsedPayload = body;
|
|
1026
691
|
}
|
|
1027
|
-
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
return parseV1StructuredBody(body, getHeader(headers, "content-type"));
|
|
695
|
+
}
|
|
696
|
+
async function parseCallback(request) {
|
|
697
|
+
const ceType = request.headers.get("ce-type");
|
|
698
|
+
if (ceType === CLOUD_EVENT_TYPE_V2BETA) {
|
|
699
|
+
const result = parseBinaryHeaders(request.headers);
|
|
700
|
+
if ("receiptHandle" in result && request.body) {
|
|
701
|
+
result.rawBody = request.body;
|
|
702
|
+
}
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
let body;
|
|
706
|
+
try {
|
|
707
|
+
body = await request.json();
|
|
708
|
+
} catch {
|
|
709
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
710
|
+
}
|
|
711
|
+
const headers = {};
|
|
712
|
+
request.headers.forEach((value, key) => {
|
|
713
|
+
headers[key] = value;
|
|
714
|
+
});
|
|
715
|
+
return parseRawCallback(body, headers);
|
|
1028
716
|
}
|
|
717
|
+
async function handleCallback(handler, request, options) {
|
|
718
|
+
const { queueName, consumerGroup, messageId } = request;
|
|
719
|
+
if (!options?.client) {
|
|
720
|
+
throw new Error("HandleCallbackOptions.client is required");
|
|
721
|
+
}
|
|
722
|
+
let api = getApiClient(options.client);
|
|
723
|
+
if (request.region) {
|
|
724
|
+
api = api.withRegion(request.region);
|
|
725
|
+
}
|
|
726
|
+
const topic = new Topic(api, queueName);
|
|
727
|
+
const cg = topic.consumerGroup(
|
|
728
|
+
consumerGroup,
|
|
729
|
+
options?.visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds: options.visibilityTimeoutSeconds } : void 0
|
|
730
|
+
);
|
|
731
|
+
if ("receiptHandle" in request) {
|
|
732
|
+
const transport = api.getTransport();
|
|
733
|
+
let payload;
|
|
734
|
+
if (request.rawBody) {
|
|
735
|
+
payload = await transport.deserialize(request.rawBody);
|
|
736
|
+
} else if (request.parsedPayload !== void 0) {
|
|
737
|
+
payload = request.parsedPayload;
|
|
738
|
+
} else {
|
|
739
|
+
throw new Error(
|
|
740
|
+
"Binary mode callback with receipt handle is missing payload"
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
const message = {
|
|
744
|
+
messageId,
|
|
745
|
+
payload,
|
|
746
|
+
deliveryCount: request.deliveryCount ?? 1,
|
|
747
|
+
createdAt: request.createdAt ? new Date(request.createdAt) : /* @__PURE__ */ new Date(),
|
|
748
|
+
expiresAt: request.expiresAt ? new Date(request.expiresAt) : void 0,
|
|
749
|
+
contentType: request.contentType ?? transport.contentType,
|
|
750
|
+
receiptHandle: request.receiptHandle
|
|
751
|
+
};
|
|
752
|
+
const visibilityDeadline = request.visibilityDeadline ? new Date(request.visibilityDeadline) : void 0;
|
|
753
|
+
await cg.consumeMessage(handler, message, {
|
|
754
|
+
visibilityDeadline,
|
|
755
|
+
retry: options?.retry
|
|
756
|
+
});
|
|
757
|
+
} else {
|
|
758
|
+
await cg.consume(handler, { messageId, retry: options?.retry });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/dev.ts
|
|
763
|
+
var ROUTE_MAPPINGS_KEY = Symbol.for("@vercel/queue.devRouteMappings");
|
|
764
|
+
function filePathToUrlPath(filePath) {
|
|
765
|
+
let urlPath = filePath.replace(/^app\//, "/").replace(/^pages\//, "/").replace(/\/route\.(ts|mts|js|mjs|tsx|jsx)$/, "").replace(/\.(ts|mts|js|mjs|tsx|jsx)$/, "");
|
|
766
|
+
if (!urlPath.startsWith("/")) {
|
|
767
|
+
urlPath = "/" + urlPath;
|
|
768
|
+
}
|
|
769
|
+
return urlPath;
|
|
770
|
+
}
|
|
771
|
+
function filePathToConsumerGroup(filePath) {
|
|
772
|
+
return filePath.replace(/_/g, "__").replace(/\//g, "_S").replace(/\./g, "_D");
|
|
773
|
+
}
|
|
774
|
+
function getDevRouteMappings() {
|
|
775
|
+
const g = globalThis;
|
|
776
|
+
if (ROUTE_MAPPINGS_KEY in g) {
|
|
777
|
+
return g[ROUTE_MAPPINGS_KEY] ?? null;
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
const vercelJsonPath = path.join(process.cwd(), "vercel.json");
|
|
781
|
+
if (!fs.existsSync(vercelJsonPath)) {
|
|
782
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
const vercelJson = JSON.parse(fs.readFileSync(vercelJsonPath, "utf-8"));
|
|
786
|
+
if (!vercelJson.functions) {
|
|
787
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
const mappings = [];
|
|
791
|
+
for (const [filePath, config] of Object.entries(vercelJson.functions)) {
|
|
792
|
+
if (!config.experimentalTriggers) continue;
|
|
793
|
+
for (const trigger of config.experimentalTriggers) {
|
|
794
|
+
if (trigger.type?.startsWith("queue/") && trigger.topic) {
|
|
795
|
+
mappings.push({
|
|
796
|
+
urlPath: filePathToUrlPath(filePath),
|
|
797
|
+
topic: trigger.topic,
|
|
798
|
+
consumer: filePathToConsumerGroup(filePath)
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
g[ROUTE_MAPPINGS_KEY] = mappings.length > 0 ? mappings : null;
|
|
804
|
+
return g[ROUTE_MAPPINGS_KEY];
|
|
805
|
+
} catch (error) {
|
|
806
|
+
console.warn("[Dev Mode] Failed to read vercel.json:", error);
|
|
807
|
+
g[ROUTE_MAPPINGS_KEY] = null;
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function findMatchingRoutes(topicName) {
|
|
812
|
+
const mappings = getDevRouteMappings();
|
|
813
|
+
if (!mappings) {
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
return mappings.filter((mapping) => {
|
|
817
|
+
if (mapping.topic.includes("*")) {
|
|
818
|
+
return matchesWildcardPattern(topicName, mapping.topic);
|
|
819
|
+
}
|
|
820
|
+
return mapping.topic === topicName;
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
function isDevMode() {
|
|
824
|
+
return process.env.NODE_ENV === "development";
|
|
825
|
+
}
|
|
826
|
+
var DEV_VISIBILITY_POLL_INTERVAL = 50;
|
|
827
|
+
var DEV_VISIBILITY_MAX_WAIT = 5e3;
|
|
828
|
+
var DEV_VISIBILITY_BACKOFF_MULTIPLIER = 2;
|
|
829
|
+
async function waitForMessageVisibility(topicName, consumerGroup, messageId, region) {
|
|
830
|
+
const client = new ApiClient({ region });
|
|
831
|
+
let elapsed = 0;
|
|
832
|
+
let interval = DEV_VISIBILITY_POLL_INTERVAL;
|
|
833
|
+
while (elapsed < DEV_VISIBILITY_MAX_WAIT) {
|
|
834
|
+
try {
|
|
835
|
+
await client.receiveMessageById({
|
|
836
|
+
queueName: topicName,
|
|
837
|
+
consumerGroup,
|
|
838
|
+
messageId,
|
|
839
|
+
visibilityTimeoutSeconds: 0
|
|
840
|
+
});
|
|
841
|
+
return true;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (error instanceof MessageNotFoundError) {
|
|
844
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
845
|
+
elapsed += interval;
|
|
846
|
+
interval = Math.min(
|
|
847
|
+
interval * DEV_VISIBILITY_BACKOFF_MULTIPLIER,
|
|
848
|
+
DEV_VISIBILITY_MAX_WAIT - elapsed
|
|
849
|
+
);
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (error instanceof MessageAlreadyProcessedError) {
|
|
853
|
+
console.log(
|
|
854
|
+
`[Dev Mode] Message already processed: topic="${topicName}" messageId="${messageId}"`
|
|
855
|
+
);
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
console.error(
|
|
859
|
+
`[Dev Mode] Error polling for message visibility: topic="${topicName}" messageId="${messageId}"`,
|
|
860
|
+
error
|
|
861
|
+
);
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
console.warn(
|
|
866
|
+
`[Dev Mode] Message visibility timeout after ${DEV_VISIBILITY_MAX_WAIT}ms: topic="${topicName}" messageId="${messageId}"`
|
|
867
|
+
);
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
function triggerDevCallbacks(topicName, messageId, region, delaySeconds) {
|
|
871
|
+
if (delaySeconds && delaySeconds > 0) {
|
|
872
|
+
console.log(
|
|
873
|
+
`[Dev Mode] Message sent with delay: topic="${topicName}" messageId="${messageId}" delay=${delaySeconds}s`
|
|
874
|
+
);
|
|
875
|
+
setTimeout(() => {
|
|
876
|
+
triggerDevCallbacks(topicName, messageId, region);
|
|
877
|
+
}, delaySeconds * 1e3);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
console.log(
|
|
881
|
+
`[Dev Mode] Message sent: topic="${topicName}" messageId="${messageId}"`
|
|
882
|
+
);
|
|
883
|
+
const matchingRoutes = findMatchingRoutes(topicName);
|
|
884
|
+
if (matchingRoutes.length === 0) {
|
|
885
|
+
console.log(
|
|
886
|
+
`[Dev Mode] No matching routes in vercel.json for topic "${topicName}"`
|
|
887
|
+
);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const consumerGroups = matchingRoutes.map((r) => r.consumer);
|
|
891
|
+
console.log(
|
|
892
|
+
`[Dev Mode] Scheduling callbacks for topic="${topicName}" messageId="${messageId}" \u2192 consumers: [${consumerGroups.join(", ")}]`
|
|
893
|
+
);
|
|
894
|
+
(async () => {
|
|
895
|
+
const firstRoute = matchingRoutes[0];
|
|
896
|
+
const isVisible = await waitForMessageVisibility(
|
|
897
|
+
topicName,
|
|
898
|
+
firstRoute.consumer,
|
|
899
|
+
messageId,
|
|
900
|
+
region
|
|
901
|
+
);
|
|
902
|
+
if (!isVisible) {
|
|
903
|
+
console.warn(
|
|
904
|
+
`[Dev Mode] Skipping callbacks - message not visible: topic="${topicName}" messageId="${messageId}"`
|
|
905
|
+
);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const port = process.env.PORT || 3e3;
|
|
909
|
+
const baseUrl = `http://localhost:${port}`;
|
|
910
|
+
for (const route of matchingRoutes) {
|
|
911
|
+
const url = `${baseUrl}${route.urlPath}`;
|
|
912
|
+
console.log(
|
|
913
|
+
`[Dev Mode] Invoking handler: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`
|
|
914
|
+
);
|
|
915
|
+
try {
|
|
916
|
+
const response = await fetch(url, {
|
|
917
|
+
method: "POST",
|
|
918
|
+
headers: {
|
|
919
|
+
"ce-type": CLOUD_EVENT_TYPE_V2BETA,
|
|
920
|
+
"ce-vqsqueuename": topicName,
|
|
921
|
+
"ce-vqsconsumergroup": route.consumer,
|
|
922
|
+
"ce-vqsmessageid": messageId,
|
|
923
|
+
"ce-vqsregion": region
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
if (response.ok) {
|
|
927
|
+
try {
|
|
928
|
+
const responseData = await response.json();
|
|
929
|
+
if (responseData.status === "success") {
|
|
930
|
+
console.log(
|
|
931
|
+
`[Dev Mode] \u2713 Message processed successfully: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}"`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
console.warn(
|
|
936
|
+
`[Dev Mode] Handler returned OK but response was not JSON: topic="${topicName}" consumer="${route.consumer}"`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
try {
|
|
941
|
+
const errorData = await response.json();
|
|
942
|
+
console.error(
|
|
943
|
+
`[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" error="${errorData.error || response.statusText}"`
|
|
944
|
+
);
|
|
945
|
+
} catch {
|
|
946
|
+
console.error(
|
|
947
|
+
`[Dev Mode] \u2717 Handler failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" status=${response.status}`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
} catch (error) {
|
|
952
|
+
console.error(
|
|
953
|
+
`[Dev Mode] \u2717 HTTP request failed: topic="${topicName}" consumer="${route.consumer}" messageId="${messageId}" url="${url}"`,
|
|
954
|
+
error
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
})();
|
|
959
|
+
}
|
|
960
|
+
function clearDevRouteMappings() {
|
|
961
|
+
const g = globalThis;
|
|
962
|
+
delete g[ROUTE_MAPPINGS_KEY];
|
|
963
|
+
}
|
|
964
|
+
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
965
|
+
globalThis.__clearDevRouteMappings = clearDevRouteMappings;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/oidc.ts
|
|
969
|
+
var import_oidc = require("@vercel/oidc");
|
|
970
|
+
|
|
971
|
+
// src/api-client.ts
|
|
972
|
+
function isDebugEnabled() {
|
|
973
|
+
return process.env.VERCEL_QUEUE_DEBUG === "1" || process.env.VERCEL_QUEUE_DEBUG === "true";
|
|
974
|
+
}
|
|
975
|
+
async function consumeStream(stream) {
|
|
976
|
+
const reader = stream.getReader();
|
|
977
|
+
try {
|
|
978
|
+
while (true) {
|
|
979
|
+
const { done } = await reader.read();
|
|
980
|
+
if (done) break;
|
|
981
|
+
}
|
|
982
|
+
} finally {
|
|
983
|
+
reader.releaseLock();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
|
|
987
|
+
if (status === 400) {
|
|
988
|
+
throw new BadRequestError(errorText || badRequestDefault);
|
|
989
|
+
}
|
|
990
|
+
if (status === 401) {
|
|
991
|
+
throw new UnauthorizedError(errorText || void 0);
|
|
992
|
+
}
|
|
993
|
+
if (status === 403) {
|
|
994
|
+
throw new ForbiddenError(errorText || void 0);
|
|
995
|
+
}
|
|
996
|
+
if (status >= 500) {
|
|
997
|
+
throw new InternalServerError(
|
|
998
|
+
errorText || `Server error: ${status} ${statusText}`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
throw new Error(`Failed to ${operation}: ${status} ${statusText}`);
|
|
1002
|
+
}
|
|
1003
|
+
function parseQueueHeaders(headers) {
|
|
1004
|
+
const messageId = headers.get("Vqs-Message-Id");
|
|
1005
|
+
const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
|
|
1006
|
+
const timestamp = headers.get("Vqs-Timestamp");
|
|
1007
|
+
const contentType = headers.get("Content-Type") || "application/octet-stream";
|
|
1008
|
+
const receiptHandle = headers.get("Vqs-Receipt-Handle");
|
|
1009
|
+
if (!messageId || !timestamp || !receiptHandle) {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
const deliveryCount = parseInt(deliveryCountStr, 10);
|
|
1013
|
+
if (Number.isNaN(deliveryCount)) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
messageId,
|
|
1018
|
+
deliveryCount,
|
|
1019
|
+
createdAt: new Date(timestamp),
|
|
1020
|
+
contentType,
|
|
1021
|
+
receiptHandle
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
var DEFAULT_BASE_URL_RESOLVER = (region) => new URL(`https://${region}.vercel-queue.com`);
|
|
1025
|
+
function resolveBaseUrl(region, resolver) {
|
|
1026
|
+
return (resolver ?? DEFAULT_BASE_URL_RESOLVER)(region);
|
|
1027
|
+
}
|
|
1028
|
+
var BASE_PATH = "/api/v3/topic";
|
|
1029
|
+
var ApiClient = class _ApiClient {
|
|
1030
|
+
baseUrl;
|
|
1031
|
+
customHeaders;
|
|
1032
|
+
providedToken;
|
|
1033
|
+
resolvedDeploymentId;
|
|
1034
|
+
pinSends;
|
|
1035
|
+
explicitlyUnpinned;
|
|
1036
|
+
transport;
|
|
1037
|
+
region;
|
|
1038
|
+
baseUrlResolver;
|
|
1039
|
+
constructor(options) {
|
|
1040
|
+
this.region = options.region;
|
|
1041
|
+
this.baseUrlResolver = options.resolveBaseUrl;
|
|
1042
|
+
this.baseUrl = resolveBaseUrl(this.region, this.baseUrlResolver);
|
|
1043
|
+
this.customHeaders = options.headers || {};
|
|
1044
|
+
this.providedToken = options.token;
|
|
1045
|
+
this.transport = options.transport || new JsonTransport();
|
|
1046
|
+
if (options.deploymentId === null) {
|
|
1047
|
+
this.pinSends = false;
|
|
1048
|
+
this.explicitlyUnpinned = true;
|
|
1049
|
+
} else {
|
|
1050
|
+
this.resolvedDeploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
1051
|
+
this.pinSends = true;
|
|
1052
|
+
this.explicitlyUnpinned = false;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Return a new ApiClient targeting the given region, sharing all other
|
|
1057
|
+
* configuration (token, transport, headers, deployment ID, resolver).
|
|
1058
|
+
* Used internally by handleCallback to route follow-up API calls to the
|
|
1059
|
+
* region indicated by the incoming `ce-vqsregion` header.
|
|
1060
|
+
*/
|
|
1061
|
+
withRegion(region) {
|
|
1062
|
+
return new _ApiClient({
|
|
1063
|
+
region,
|
|
1064
|
+
resolveBaseUrl: this.baseUrlResolver,
|
|
1065
|
+
token: this.providedToken,
|
|
1066
|
+
headers: { ...this.customHeaders },
|
|
1067
|
+
deploymentId: this.explicitlyUnpinned ? null : this.resolvedDeploymentId,
|
|
1068
|
+
transport: this.transport
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
getRegion() {
|
|
1072
|
+
return this.region;
|
|
1073
|
+
}
|
|
1074
|
+
getTransport() {
|
|
1075
|
+
return this.transport;
|
|
1076
|
+
}
|
|
1077
|
+
requireDeploymentId() {
|
|
1078
|
+
if (isDevMode() || this.explicitlyUnpinned || this.resolvedDeploymentId) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
'No deployment ID available. VERCEL_DEPLOYMENT_ID is not set.\n\nThis usually means the code is running outside a Vercel deployment (e.g. during build or in a non-Vercel environment).\n\nTo fix this, create a new QueueClient with an explicit deploymentId:\n new QueueClient({ region: "iad1", deploymentId: "dpl_xxx" })\nOr explicitly opt out of deployment pinning:\n new QueueClient({ region: "iad1", deploymentId: null })'
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
getSendDeploymentId() {
|
|
1086
|
+
if (isDevMode()) {
|
|
1087
|
+
return void 0;
|
|
1088
|
+
}
|
|
1089
|
+
this.requireDeploymentId();
|
|
1090
|
+
return this.pinSends ? this.resolvedDeploymentId : void 0;
|
|
1091
|
+
}
|
|
1092
|
+
getConsumeDeploymentId() {
|
|
1093
|
+
if (isDevMode()) {
|
|
1094
|
+
return void 0;
|
|
1095
|
+
}
|
|
1096
|
+
this.requireDeploymentId();
|
|
1097
|
+
return this.resolvedDeploymentId;
|
|
1098
|
+
}
|
|
1099
|
+
async getToken() {
|
|
1100
|
+
if (this.providedToken) {
|
|
1101
|
+
return this.providedToken;
|
|
1102
|
+
}
|
|
1103
|
+
const token = await (0, import_oidc.getVercelOidcToken)();
|
|
1104
|
+
if (!token) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
"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'"
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
return token;
|
|
1110
|
+
}
|
|
1111
|
+
buildUrl(queueName, ...pathSegments) {
|
|
1112
|
+
const encodedQueue = encodeURIComponent(queueName);
|
|
1113
|
+
const segments = pathSegments.map((s) => encodeURIComponent(s));
|
|
1114
|
+
const path2 = segments.length > 0 ? "/" + segments.join("/") : "";
|
|
1115
|
+
const basePath = this.baseUrl.pathname.replace(/\/+$/, "");
|
|
1116
|
+
return `${this.baseUrl.origin}${basePath}${BASE_PATH}/${encodedQueue}${path2}`;
|
|
1117
|
+
}
|
|
1118
|
+
async fetch(url, init) {
|
|
1119
|
+
const method = init.method || "GET";
|
|
1120
|
+
if (isDebugEnabled()) {
|
|
1121
|
+
const logData = {
|
|
1122
|
+
method,
|
|
1123
|
+
url,
|
|
1124
|
+
headers: init.headers
|
|
1125
|
+
};
|
|
1126
|
+
const body = init.body;
|
|
1127
|
+
if (body !== void 0 && body !== null) {
|
|
1128
|
+
if (body instanceof ArrayBuffer) {
|
|
1129
|
+
logData.bodySize = body.byteLength;
|
|
1130
|
+
} else if (body instanceof Uint8Array) {
|
|
1131
|
+
logData.bodySize = body.byteLength;
|
|
1132
|
+
} else if (typeof body === "string") {
|
|
1133
|
+
logData.bodySize = body.length;
|
|
1134
|
+
} else {
|
|
1135
|
+
logData.bodyType = typeof body;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
|
|
1139
|
+
}
|
|
1140
|
+
init.headers.set("User-Agent", `@vercel/queue/${"0.0.2"}`);
|
|
1141
|
+
init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
|
|
1142
|
+
const response = await fetch(url, init);
|
|
1143
|
+
if (isDebugEnabled()) {
|
|
1144
|
+
const logData = {
|
|
1145
|
+
method,
|
|
1146
|
+
url,
|
|
1147
|
+
status: response.status,
|
|
1148
|
+
statusText: response.statusText,
|
|
1149
|
+
headers: response.headers
|
|
1150
|
+
};
|
|
1151
|
+
console.debug("[VQS Debug] Response:", JSON.stringify(logData, null, 2));
|
|
1152
|
+
}
|
|
1153
|
+
return response;
|
|
1154
|
+
}
|
|
1155
|
+
async sendMessage(options) {
|
|
1156
|
+
const transport = this.transport;
|
|
1157
|
+
const {
|
|
1158
|
+
queueName,
|
|
1159
|
+
payload,
|
|
1160
|
+
idempotencyKey,
|
|
1161
|
+
retentionSeconds,
|
|
1162
|
+
delaySeconds,
|
|
1163
|
+
headers: optionHeaders
|
|
1164
|
+
} = options;
|
|
1165
|
+
const headers = new Headers();
|
|
1166
|
+
if (this.customHeaders) {
|
|
1167
|
+
for (const [name, value] of Object.entries(this.customHeaders)) {
|
|
1168
|
+
headers.append(name, value);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (optionHeaders) {
|
|
1172
|
+
const protectedHeaderNames = /* @__PURE__ */ new Set(["authorization", "content-type"]);
|
|
1173
|
+
const isProtectedHeader = (name) => {
|
|
1174
|
+
const lower = name.toLowerCase();
|
|
1175
|
+
if (protectedHeaderNames.has(lower)) return true;
|
|
1176
|
+
return lower.startsWith("vqs-");
|
|
1177
|
+
};
|
|
1178
|
+
for (const [name, value] of Object.entries(optionHeaders)) {
|
|
1179
|
+
if (!isProtectedHeader(name) && value !== void 0) {
|
|
1180
|
+
headers.append(name, value);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
headers.set("Authorization", `Bearer ${await this.getToken()}`);
|
|
1185
|
+
headers.set("Content-Type", transport.contentType);
|
|
1186
|
+
const deploymentId = this.getSendDeploymentId();
|
|
1187
|
+
if (deploymentId) {
|
|
1188
|
+
headers.set("Vqs-Deployment-Id", deploymentId);
|
|
1189
|
+
}
|
|
1190
|
+
if (idempotencyKey) {
|
|
1191
|
+
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
1192
|
+
}
|
|
1193
|
+
if (retentionSeconds !== void 0) {
|
|
1194
|
+
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
1195
|
+
}
|
|
1196
|
+
if (delaySeconds !== void 0) {
|
|
1197
|
+
headers.set("Vqs-Delay-Seconds", delaySeconds.toString());
|
|
1198
|
+
}
|
|
1199
|
+
const serialized = transport.serialize(payload);
|
|
1200
|
+
const body = Buffer.isBuffer(serialized) ? new Uint8Array(serialized) : serialized;
|
|
1201
|
+
const response = await this.fetch(this.buildUrl(queueName), {
|
|
1202
|
+
method: "POST",
|
|
1203
|
+
body,
|
|
1204
|
+
headers
|
|
1205
|
+
});
|
|
1206
|
+
if (!response.ok) {
|
|
1207
|
+
const errorText = await response.text();
|
|
1208
|
+
if (response.status === 409) {
|
|
1209
|
+
throw new DuplicateMessageError(
|
|
1210
|
+
errorText || "Duplicate idempotency key detected",
|
|
1211
|
+
idempotencyKey
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
if (response.status === 502) {
|
|
1215
|
+
throw new ConsumerDiscoveryError(
|
|
1216
|
+
errorText || "Consumer discovery failed",
|
|
1217
|
+
deploymentId
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (response.status === 503) {
|
|
1221
|
+
throw new ConsumerRegistryNotConfiguredError(
|
|
1222
|
+
errorText || "Consumer registry not configured"
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
throwCommonHttpError(
|
|
1226
|
+
response.status,
|
|
1227
|
+
response.statusText,
|
|
1228
|
+
errorText,
|
|
1229
|
+
"send message"
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
if (response.status === 202) {
|
|
1233
|
+
return { messageId: null };
|
|
1234
|
+
}
|
|
1235
|
+
const responseData = await response.json();
|
|
1236
|
+
return responseData;
|
|
1237
|
+
}
|
|
1238
|
+
async *receiveMessages(options) {
|
|
1239
|
+
const transport = this.transport;
|
|
1240
|
+
const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
|
|
1241
|
+
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
1242
|
+
throw new InvalidLimitError(limit);
|
|
1243
|
+
}
|
|
1244
|
+
const headers = new Headers({
|
|
1245
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
1246
|
+
Accept: "multipart/mixed",
|
|
1247
|
+
...this.customHeaders
|
|
1248
|
+
});
|
|
1249
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
1250
|
+
headers.set(
|
|
1251
|
+
"Vqs-Visibility-Timeout-Seconds",
|
|
1252
|
+
visibilityTimeoutSeconds.toString()
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
if (limit !== void 0) {
|
|
1256
|
+
headers.set("Vqs-Max-Messages", limit.toString());
|
|
1257
|
+
}
|
|
1258
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
1259
|
+
if (effectiveDeploymentId) {
|
|
1260
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
1261
|
+
}
|
|
1262
|
+
const response = await this.fetch(
|
|
1263
|
+
this.buildUrl(queueName, "consumer", consumerGroup),
|
|
1264
|
+
{
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
headers
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
if (response.status === 204) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
if (!response.ok) {
|
|
1273
|
+
const errorText = await response.text();
|
|
1274
|
+
throwCommonHttpError(
|
|
1275
|
+
response.status,
|
|
1276
|
+
response.statusText,
|
|
1277
|
+
errorText,
|
|
1278
|
+
"receive messages"
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
1282
|
+
try {
|
|
1283
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
1284
|
+
if (!parsedHeaders) {
|
|
1285
|
+
console.warn("Missing required queue headers in multipart part");
|
|
1286
|
+
await consumeStream(multipartMessage.payload);
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const deserializedPayload = await transport.deserialize(
|
|
1290
|
+
multipartMessage.payload
|
|
1291
|
+
);
|
|
1292
|
+
const message = {
|
|
1293
|
+
...parsedHeaders,
|
|
1294
|
+
payload: deserializedPayload
|
|
1295
|
+
};
|
|
1296
|
+
yield message;
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
console.warn("Failed to process multipart message:", error);
|
|
1299
|
+
await consumeStream(multipartMessage.payload);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async receiveMessageById(options) {
|
|
1304
|
+
const transport = this.transport;
|
|
1305
|
+
const { queueName, consumerGroup, messageId, visibilityTimeoutSeconds } = options;
|
|
1306
|
+
const headers = new Headers({
|
|
1307
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
1308
|
+
Accept: "multipart/mixed",
|
|
1309
|
+
...this.customHeaders
|
|
1310
|
+
});
|
|
1311
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
1312
|
+
headers.set(
|
|
1313
|
+
"Vqs-Visibility-Timeout-Seconds",
|
|
1314
|
+
visibilityTimeoutSeconds.toString()
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
1318
|
+
if (effectiveDeploymentId) {
|
|
1319
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
1320
|
+
}
|
|
1321
|
+
const response = await this.fetch(
|
|
1322
|
+
this.buildUrl(queueName, "consumer", consumerGroup, "id", messageId),
|
|
1323
|
+
{
|
|
1324
|
+
method: "POST",
|
|
1325
|
+
headers
|
|
1326
|
+
}
|
|
1327
|
+
);
|
|
1328
|
+
if (!response.ok) {
|
|
1329
|
+
const errorText = await response.text();
|
|
1330
|
+
if (response.status === 404) {
|
|
1331
|
+
throw new MessageNotFoundError(messageId);
|
|
1332
|
+
}
|
|
1333
|
+
if (response.status === 409) {
|
|
1334
|
+
let errorData = {};
|
|
1335
|
+
try {
|
|
1336
|
+
errorData = JSON.parse(errorText);
|
|
1337
|
+
} catch {
|
|
1338
|
+
}
|
|
1339
|
+
if (errorData.originalMessageId) {
|
|
1340
|
+
throw new MessageNotAvailableError(
|
|
1341
|
+
messageId,
|
|
1342
|
+
`This message was a duplicate - use originalMessageId: ${errorData.originalMessageId}`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
throw new MessageNotAvailableError(messageId);
|
|
1346
|
+
}
|
|
1347
|
+
if (response.status === 410) {
|
|
1348
|
+
throw new MessageAlreadyProcessedError(messageId);
|
|
1349
|
+
}
|
|
1350
|
+
throwCommonHttpError(
|
|
1351
|
+
response.status,
|
|
1352
|
+
response.statusText,
|
|
1353
|
+
errorText,
|
|
1354
|
+
"receive message by ID"
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
1358
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
1359
|
+
if (!parsedHeaders) {
|
|
1360
|
+
await consumeStream(multipartMessage.payload);
|
|
1361
|
+
throw new MessageCorruptedError(
|
|
1362
|
+
messageId,
|
|
1363
|
+
"Missing required queue headers in response"
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
const deserializedPayload = await transport.deserialize(
|
|
1367
|
+
multipartMessage.payload
|
|
1368
|
+
);
|
|
1369
|
+
const message = {
|
|
1370
|
+
...parsedHeaders,
|
|
1371
|
+
payload: deserializedPayload
|
|
1372
|
+
};
|
|
1373
|
+
return { message };
|
|
1374
|
+
}
|
|
1375
|
+
throw new MessageNotFoundError(messageId);
|
|
1376
|
+
}
|
|
1377
|
+
async acknowledgeMessage(options) {
|
|
1378
|
+
const { queueName, consumerGroup, receiptHandle } = options;
|
|
1379
|
+
const headers = new Headers({
|
|
1380
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
1381
|
+
...this.customHeaders
|
|
1382
|
+
});
|
|
1383
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
1384
|
+
if (effectiveDeploymentId) {
|
|
1385
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
1386
|
+
}
|
|
1387
|
+
const response = await this.fetch(
|
|
1388
|
+
this.buildUrl(
|
|
1389
|
+
queueName,
|
|
1390
|
+
"consumer",
|
|
1391
|
+
consumerGroup,
|
|
1392
|
+
"lease",
|
|
1393
|
+
receiptHandle
|
|
1394
|
+
),
|
|
1395
|
+
{
|
|
1396
|
+
method: "DELETE",
|
|
1397
|
+
headers
|
|
1398
|
+
}
|
|
1399
|
+
);
|
|
1400
|
+
if (!response.ok) {
|
|
1401
|
+
const errorText = await response.text();
|
|
1402
|
+
if (response.status === 404) {
|
|
1403
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
1404
|
+
}
|
|
1405
|
+
if (response.status === 409) {
|
|
1406
|
+
throw new MessageNotAvailableError(
|
|
1407
|
+
receiptHandle,
|
|
1408
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
throwCommonHttpError(
|
|
1412
|
+
response.status,
|
|
1413
|
+
response.statusText,
|
|
1414
|
+
errorText,
|
|
1415
|
+
"acknowledge message",
|
|
1416
|
+
"Missing or invalid receipt handle"
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
return { acknowledged: true };
|
|
1420
|
+
}
|
|
1421
|
+
async changeVisibility(options) {
|
|
1422
|
+
const {
|
|
1423
|
+
queueName,
|
|
1424
|
+
consumerGroup,
|
|
1425
|
+
receiptHandle,
|
|
1426
|
+
visibilityTimeoutSeconds
|
|
1427
|
+
} = options;
|
|
1428
|
+
const headers = new Headers({
|
|
1429
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
1430
|
+
"Content-Type": "application/json",
|
|
1431
|
+
...this.customHeaders
|
|
1432
|
+
});
|
|
1433
|
+
const effectiveDeploymentId = this.getConsumeDeploymentId();
|
|
1434
|
+
if (effectiveDeploymentId) {
|
|
1435
|
+
headers.set("Vqs-Deployment-Id", effectiveDeploymentId);
|
|
1436
|
+
}
|
|
1437
|
+
const response = await this.fetch(
|
|
1438
|
+
this.buildUrl(
|
|
1439
|
+
queueName,
|
|
1440
|
+
"consumer",
|
|
1441
|
+
consumerGroup,
|
|
1442
|
+
"lease",
|
|
1443
|
+
receiptHandle
|
|
1444
|
+
),
|
|
1445
|
+
{
|
|
1446
|
+
method: "PATCH",
|
|
1447
|
+
headers,
|
|
1448
|
+
body: JSON.stringify({ visibilityTimeoutSeconds })
|
|
1449
|
+
}
|
|
1450
|
+
);
|
|
1451
|
+
if (!response.ok) {
|
|
1452
|
+
const errorText = await response.text();
|
|
1453
|
+
if (response.status === 404) {
|
|
1454
|
+
throw new MessageNotFoundError(receiptHandle);
|
|
1455
|
+
}
|
|
1456
|
+
if (response.status === 409) {
|
|
1457
|
+
throw new MessageNotAvailableError(
|
|
1458
|
+
receiptHandle,
|
|
1459
|
+
errorText || "Invalid receipt handle, message not in correct state, or already processed"
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
throwCommonHttpError(
|
|
1463
|
+
response.status,
|
|
1464
|
+
response.statusText,
|
|
1465
|
+
errorText,
|
|
1466
|
+
"change visibility",
|
|
1467
|
+
"Missing receipt handle or invalid visibility timeout"
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
return { success: true };
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
// src/client.ts
|
|
1475
|
+
var apiClients = /* @__PURE__ */ new WeakMap();
|
|
1476
|
+
function getApiClient(client) {
|
|
1477
|
+
const api = apiClients.get(client);
|
|
1478
|
+
if (!api) {
|
|
1479
|
+
throw new Error("QueueClient not initialized");
|
|
1480
|
+
}
|
|
1481
|
+
return api;
|
|
1482
|
+
}
|
|
1483
|
+
var QueueClient = class {
|
|
1484
|
+
constructor(options) {
|
|
1485
|
+
apiClients.set(this, new ApiClient(options));
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Send a message to a topic.
|
|
1489
|
+
*
|
|
1490
|
+
* This is an arrow function property so it can be destructured:
|
|
1491
|
+
* ```typescript
|
|
1492
|
+
* const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
1493
|
+
* await send("my-topic", payload);
|
|
1494
|
+
* ```
|
|
1495
|
+
*
|
|
1496
|
+
* @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
|
|
1497
|
+
* @param payload - The data to send (serialized via the configured transport)
|
|
1498
|
+
* @param options - Optional send options (idempotencyKey, retentionSeconds, delaySeconds, headers)
|
|
1499
|
+
* @returns `{ messageId }` — `messageId` is `null` when the server accepted
|
|
1500
|
+
* the message for deferred processing (no ID available yet)
|
|
1501
|
+
*/
|
|
1502
|
+
send = async (topicName, payload, options) => {
|
|
1503
|
+
const api = getApiClient(this);
|
|
1504
|
+
const result = await api.sendMessage({
|
|
1505
|
+
queueName: topicName,
|
|
1506
|
+
payload,
|
|
1507
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1508
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1509
|
+
delaySeconds: options?.delaySeconds,
|
|
1510
|
+
headers: options?.headers
|
|
1511
|
+
});
|
|
1512
|
+
if (result.messageId && isDevMode()) {
|
|
1513
|
+
triggerDevCallbacks(
|
|
1514
|
+
topicName,
|
|
1515
|
+
result.messageId,
|
|
1516
|
+
api.getRegion(),
|
|
1517
|
+
options?.delaySeconds
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
return { messageId: result.messageId };
|
|
1521
|
+
};
|
|
1522
|
+
/**
|
|
1523
|
+
* Receive and process messages from a topic.
|
|
1524
|
+
*
|
|
1525
|
+
* Each message is automatically locked, kept alive via periodic visibility
|
|
1526
|
+
* extensions during processing, and acknowledged upon successful handler completion.
|
|
1527
|
+
* The handler is not called when the queue is empty — check `result.ok` instead.
|
|
1528
|
+
*
|
|
1529
|
+
* This is an arrow function property so it can be destructured:
|
|
1530
|
+
* ```typescript
|
|
1531
|
+
* const { receive } = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
1532
|
+
* const result = await receive("my-topic", "my-group", handler);
|
|
1533
|
+
* if (!result.ok) console.log(result.reason);
|
|
1534
|
+
* ```
|
|
1535
|
+
*
|
|
1536
|
+
* @param topicName - Name of the topic (pattern: `[A-Za-z0-9_-]+`)
|
|
1537
|
+
* @param consumerGroup - Name of the consumer group (pattern: `[A-Za-z0-9_-]+`)
|
|
1538
|
+
* @param handler - Function to process each message payload and metadata.
|
|
1539
|
+
* Not called when the queue is empty.
|
|
1540
|
+
* @param options - Optional receive options (visibilityTimeoutSeconds, limit, or messageId)
|
|
1541
|
+
* @returns Discriminated result: `{ ok: true }` on success, `{ ok: false, reason }` otherwise
|
|
1542
|
+
*/
|
|
1543
|
+
receive = async (topicName, consumerGroup, handler, options) => {
|
|
1544
|
+
const api = getApiClient(this);
|
|
1545
|
+
const topic = new Topic(api, topicName);
|
|
1546
|
+
const visibilityTimeoutSeconds = options && "visibilityTimeoutSeconds" in options ? options.visibilityTimeoutSeconds : void 0;
|
|
1547
|
+
const consumer = topic.consumerGroup(
|
|
1548
|
+
consumerGroup,
|
|
1549
|
+
visibilityTimeoutSeconds !== void 0 ? { visibilityTimeoutSeconds } : {}
|
|
1550
|
+
);
|
|
1551
|
+
try {
|
|
1552
|
+
let count;
|
|
1553
|
+
const retry = options?.retry;
|
|
1554
|
+
if (options && "messageId" in options) {
|
|
1555
|
+
count = await consumer.consume(handler, {
|
|
1556
|
+
messageId: options.messageId,
|
|
1557
|
+
retry
|
|
1558
|
+
});
|
|
1559
|
+
} else {
|
|
1560
|
+
const limit = options && "limit" in options ? options.limit : void 0;
|
|
1561
|
+
count = await consumer.consume(handler, {
|
|
1562
|
+
...limit !== void 0 ? { limit } : {},
|
|
1563
|
+
retry
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
if (count === 0) {
|
|
1567
|
+
return { ok: false, reason: "empty" };
|
|
1568
|
+
}
|
|
1569
|
+
return { ok: true };
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
if (options && "messageId" in options && error instanceof MessageNotFoundError) {
|
|
1572
|
+
return { ok: false, reason: "not_found", messageId: options.messageId };
|
|
1573
|
+
}
|
|
1574
|
+
if (options && "messageId" in options && error instanceof MessageNotAvailableError) {
|
|
1575
|
+
return {
|
|
1576
|
+
ok: false,
|
|
1577
|
+
reason: "not_available",
|
|
1578
|
+
messageId: options.messageId
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
if (options && "messageId" in options && error instanceof MessageAlreadyProcessedError) {
|
|
1582
|
+
return {
|
|
1583
|
+
ok: false,
|
|
1584
|
+
reason: "already_processed",
|
|
1585
|
+
messageId: options.messageId
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
throw error;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
/**
|
|
1592
|
+
* Create a Web API route handler for processing queue callback messages.
|
|
1593
|
+
*
|
|
1594
|
+
* Parses incoming `Request` as a CloudEvent and invokes the handler.
|
|
1595
|
+
* For use on Vercel — Vercel invokes this route when messages are available.
|
|
1596
|
+
*
|
|
1597
|
+
* This is an arrow function property so it can be destructured:
|
|
1598
|
+
* ```typescript
|
|
1599
|
+
* const { handleCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
1600
|
+
* export const POST = handleCallback(handler);
|
|
1601
|
+
* ```
|
|
1602
|
+
*
|
|
1603
|
+
* @param handler - Function to process the message payload and metadata
|
|
1604
|
+
* @param options - Optional configuration
|
|
1605
|
+
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
|
|
1606
|
+
* @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
|
|
1607
|
+
* reschedule the message for redelivery after N seconds.
|
|
1608
|
+
* @returns A `(request: Request) => Promise<Response>` route handler
|
|
1609
|
+
*/
|
|
1610
|
+
handleCallback = (handler, options) => {
|
|
1611
|
+
return async (request) => {
|
|
1612
|
+
try {
|
|
1613
|
+
const parsed = await parseCallback(request);
|
|
1614
|
+
await handleCallback(handler, parsed, {
|
|
1615
|
+
client: this,
|
|
1616
|
+
visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
|
|
1617
|
+
retry: options?.retry
|
|
1618
|
+
});
|
|
1619
|
+
return Response.json({ status: "success" });
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
console.error("Queue callback error:", error);
|
|
1622
|
+
if (error instanceof Error && (error.message.includes("Invalid content type") || error.message.includes("Invalid CloudEvent") || error.message.includes("Missing required CloudEvent") || error.message.includes("Failed to parse CloudEvent") || error.message.includes("Binary mode callback"))) {
|
|
1623
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
1624
|
+
}
|
|
1625
|
+
return Response.json(
|
|
1626
|
+
{ error: "Failed to process queue message" },
|
|
1627
|
+
{ status: 500 }
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
};
|
|
1632
|
+
/**
|
|
1633
|
+
* Create a Connect-style route handler for processing queue callback messages.
|
|
1634
|
+
* For use on Vercel — Vercel invokes this route when messages are available.
|
|
1635
|
+
*
|
|
1636
|
+
* For frameworks using the `(req, res)` middleware pattern where `req.body`
|
|
1637
|
+
* is pre-parsed (Next.js Pages Router, etc.).
|
|
1638
|
+
*
|
|
1639
|
+
* This is an arrow function property so it can be destructured:
|
|
1640
|
+
* ```typescript
|
|
1641
|
+
* const { handleNodeCallback } = new QueueClient({ region: process.env.QUEUE_REGION! });
|
|
1642
|
+
* app.post("/api/queue", handleNodeCallback(handler));
|
|
1643
|
+
* ```
|
|
1644
|
+
*
|
|
1645
|
+
* @param handler - Function to process the message payload and metadata
|
|
1646
|
+
* @param options - Optional configuration
|
|
1647
|
+
* @param options.visibilityTimeoutSeconds - Message lock duration (default: 300, max: 3600)
|
|
1648
|
+
* @param options.retry - Called when the handler throws. Return `{ afterSeconds: N }` to
|
|
1649
|
+
* reschedule the message for redelivery after N seconds.
|
|
1650
|
+
* @returns A `(req, res) => Promise<void>` route handler
|
|
1651
|
+
*/
|
|
1652
|
+
handleNodeCallback = (handler, options) => {
|
|
1653
|
+
return async (req, res) => {
|
|
1654
|
+
if (req.method !== "POST") {
|
|
1655
|
+
res.status(200).end();
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
try {
|
|
1659
|
+
const parsed = parseRawCallback(req.body, req.headers);
|
|
1660
|
+
await handleCallback(handler, parsed, {
|
|
1661
|
+
client: this,
|
|
1662
|
+
visibilityTimeoutSeconds: options?.visibilityTimeoutSeconds,
|
|
1663
|
+
retry: options?.retry
|
|
1664
|
+
});
|
|
1665
|
+
res.status(200).json({ status: "success" });
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
console.error("Queue callback error:", error);
|
|
1668
|
+
if (error instanceof Error && (error.message.includes("Invalid content type") || error.message.includes("Invalid CloudEvent") || error.message.includes("Missing required CloudEvent") || error.message.includes("Failed to parse CloudEvent") || error.message.includes("Binary mode callback"))) {
|
|
1669
|
+
res.status(400).json({ error: error.message });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
res.status(500).json({ error: "Failed to process queue message" });
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
};
|
|
1676
|
+
};
|
|
1029
1677
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1030
1678
|
0 && (module.exports = {
|
|
1031
1679
|
BadRequestError,
|
|
1032
1680
|
BufferTransport,
|
|
1681
|
+
CLOUD_EVENT_TYPE_V1BETA,
|
|
1682
|
+
CLOUD_EVENT_TYPE_V2BETA,
|
|
1683
|
+
ConsumerDiscoveryError,
|
|
1684
|
+
ConsumerRegistryNotConfiguredError,
|
|
1685
|
+
DuplicateMessageError,
|
|
1033
1686
|
ForbiddenError,
|
|
1034
1687
|
InternalServerError,
|
|
1035
1688
|
InvalidLimitError,
|
|
1036
1689
|
JsonTransport,
|
|
1690
|
+
MessageAlreadyProcessedError,
|
|
1037
1691
|
MessageCorruptedError,
|
|
1038
1692
|
MessageLockedError,
|
|
1039
1693
|
MessageNotAvailableError,
|
|
1040
1694
|
MessageNotFoundError,
|
|
1695
|
+
QueueClient,
|
|
1041
1696
|
QueueEmptyError,
|
|
1042
1697
|
StreamTransport,
|
|
1043
1698
|
UnauthorizedError,
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
send
|
|
1699
|
+
parseCallback,
|
|
1700
|
+
parseRawCallback
|
|
1047
1701
|
});
|
|
1048
1702
|
//# sourceMappingURL=index.js.map
|