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