@vercel/queue 0.0.0-alpha.3 → 0.0.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +288 -800
- package/bin/local-discover.js +196 -0
- package/dist/index.d.mts +101 -717
- package/dist/index.d.ts +101 -717
- package/dist/index.js +490 -603
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +486 -593
- package/dist/index.mjs.map +1 -1
- package/dist/pages.d.mts +47 -0
- package/dist/pages.d.ts +47 -0
- package/dist/pages.js +1250 -0
- package/dist/pages.js.map +1 -0
- package/dist/pages.mjs +1223 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/types-JvOenjfT.d.mts +256 -0
- package/dist/types-JvOenjfT.d.ts +256 -0
- package/package.json +23 -6
package/dist/index.js
CHANGED
|
@@ -22,156 +22,92 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
BadRequestError: () => BadRequestError,
|
|
24
24
|
BufferTransport: () => BufferTransport,
|
|
25
|
-
ConsumerGroup: () => ConsumerGroup,
|
|
26
|
-
FailedDependencyError: () => FailedDependencyError,
|
|
27
|
-
FifoOrderingViolationError: () => FifoOrderingViolationError,
|
|
28
25
|
ForbiddenError: () => ForbiddenError,
|
|
29
26
|
InternalServerError: () => InternalServerError,
|
|
30
|
-
InvalidCallbackError: () => InvalidCallbackError,
|
|
31
27
|
InvalidLimitError: () => InvalidLimitError,
|
|
32
28
|
JsonTransport: () => JsonTransport,
|
|
33
29
|
MessageCorruptedError: () => MessageCorruptedError,
|
|
34
30
|
MessageLockedError: () => MessageLockedError,
|
|
35
31
|
MessageNotAvailableError: () => MessageNotAvailableError,
|
|
36
32
|
MessageNotFoundError: () => MessageNotFoundError,
|
|
37
|
-
QueueClient: () => QueueClient,
|
|
38
33
|
QueueEmptyError: () => QueueEmptyError,
|
|
39
34
|
StreamTransport: () => StreamTransport,
|
|
40
|
-
Topic: () => Topic,
|
|
41
35
|
UnauthorizedError: () => UnauthorizedError,
|
|
42
|
-
createTopic: () => createTopic,
|
|
43
|
-
getVercelOidcToken: () => getVercelOidcToken,
|
|
44
36
|
handleCallback: () => handleCallback,
|
|
45
|
-
|
|
37
|
+
parseCallback: () => parseCallback,
|
|
38
|
+
receive: () => receive,
|
|
39
|
+
send: () => send
|
|
46
40
|
});
|
|
47
41
|
module.exports = __toCommonJS(index_exports);
|
|
48
42
|
|
|
49
|
-
// src/
|
|
50
|
-
async function
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
|
|
55
|
-
if (!token) {
|
|
56
|
-
throw new Error(
|
|
57
|
-
`The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
return token;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// src/client.ts
|
|
64
|
-
var import_mixpart = require("mixpart");
|
|
65
|
-
|
|
66
|
-
// src/local.ts
|
|
67
|
-
var import_node_child_process = require("child_process");
|
|
68
|
-
function isLocalhostWithPort(url) {
|
|
43
|
+
// src/transports.ts
|
|
44
|
+
async function streamToBuffer(stream) {
|
|
45
|
+
let totalLength = 0;
|
|
46
|
+
const reader = stream.getReader();
|
|
47
|
+
const chunks = [];
|
|
69
48
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
49
|
+
while (true) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done) break;
|
|
52
|
+
chunks.push(value);
|
|
53
|
+
totalLength += value.length;
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
reader.releaseLock();
|
|
76
57
|
}
|
|
58
|
+
return Buffer.concat(chunks, totalLength);
|
|
77
59
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return [];
|
|
60
|
+
var JsonTransport = class {
|
|
61
|
+
contentType = "application/json";
|
|
62
|
+
replacer;
|
|
63
|
+
reviver;
|
|
64
|
+
constructor(options = {}) {
|
|
65
|
+
this.replacer = options.replacer;
|
|
66
|
+
this.reviver = options.reviver;
|
|
86
67
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const { isLocalhost } = isLocalhostWithPort(config.url);
|
|
90
|
-
return isLocalhost;
|
|
91
|
-
});
|
|
92
|
-
if (hasLocalhostCallbacks) {
|
|
93
|
-
console.warn(
|
|
94
|
-
`Queue Development Mode: Localhost callbacks are not supported on ${process.platform}. Localhost callback handling requires bash, nc, and curl which are available on macOS and Linux only. Consider using a production callback URL or developing on a supported platform.`
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
return [];
|
|
68
|
+
serialize(value) {
|
|
69
|
+
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
98
70
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (isLocalhost && port && port > 0) {
|
|
103
|
-
localhostCallbacks.push({ group, config, port });
|
|
104
|
-
} else {
|
|
105
|
-
console.warn(
|
|
106
|
-
`Queue Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
return localhostCallbacks;
|
|
111
|
-
}
|
|
112
|
-
function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
|
|
113
|
-
localhostCallbacks.forEach(({ group, config, port }) => {
|
|
114
|
-
const callbackHeaders = new Headers();
|
|
115
|
-
callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
|
|
116
|
-
callbackHeaders.set("Vqs-Queue-Name", queueName);
|
|
117
|
-
callbackHeaders.set("Vqs-Consumer-Group", group);
|
|
118
|
-
fireAndForgetWaitForHttpReady(
|
|
119
|
-
config.url,
|
|
120
|
-
port,
|
|
121
|
-
config.delay || 0,
|
|
122
|
-
3,
|
|
123
|
-
// Default retry frequency
|
|
124
|
-
callbackHeaders
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
|
|
129
|
-
if (!isSupportedPlatform()) {
|
|
130
|
-
console.warn(
|
|
131
|
-
`Queue: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
|
|
132
|
-
);
|
|
133
|
-
return;
|
|
71
|
+
async deserialize(stream) {
|
|
72
|
+
const buffer = await streamToBuffer(stream);
|
|
73
|
+
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
134
74
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
75
|
+
};
|
|
76
|
+
var BufferTransport = class {
|
|
77
|
+
contentType = "application/octet-stream";
|
|
78
|
+
serialize(value) {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
async deserialize(stream) {
|
|
82
|
+
return await streamToBuffer(stream);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var StreamTransport = class {
|
|
86
|
+
contentType = "application/octet-stream";
|
|
87
|
+
serialize(value) {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
async deserialize(stream) {
|
|
91
|
+
return stream;
|
|
142
92
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
93
|
+
async finalize(payload) {
|
|
94
|
+
const reader = payload.getReader();
|
|
95
|
+
try {
|
|
96
|
+
while (true) {
|
|
97
|
+
const { done } = await reader.read();
|
|
98
|
+
if (done) break;
|
|
99
|
+
}
|
|
100
|
+
} finally {
|
|
101
|
+
reader.releaseLock();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
146
105
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# 2) If port is open, try HTTP POST check
|
|
153
|
-
if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
|
|
154
|
-
# Success: port is up AND HTTP returned 2xx (following redirects)
|
|
155
|
-
exit 0
|
|
156
|
-
fi
|
|
157
|
-
else
|
|
158
|
-
# Port was closed\u2014increment miss counter
|
|
159
|
-
((missed+=1))
|
|
160
|
-
# If closed twice in a row, give up immediately
|
|
161
|
-
if [ "$missed" -ge 2 ]; then
|
|
162
|
-
exit 1
|
|
163
|
-
fi
|
|
164
|
-
fi
|
|
165
|
-
# Wait before next cycle
|
|
166
|
-
sleep ${retryFrequencySeconds}
|
|
167
|
-
done
|
|
168
|
-
`;
|
|
169
|
-
const childProcess = (0, import_node_child_process.spawn)("bash", ["-c", bashScript], {
|
|
170
|
-
stdio: "ignore",
|
|
171
|
-
detached: true
|
|
172
|
-
});
|
|
173
|
-
childProcess.unref();
|
|
174
|
-
}
|
|
106
|
+
// src/client.ts
|
|
107
|
+
var import_mixpart = require("mixpart");
|
|
108
|
+
|
|
109
|
+
// src/oidc.ts
|
|
110
|
+
var import_oidc = require("@vercel/oidc");
|
|
175
111
|
|
|
176
112
|
// src/types.ts
|
|
177
113
|
var MessageNotFoundError = class extends Error {
|
|
@@ -188,16 +124,6 @@ var MessageNotAvailableError = class extends Error {
|
|
|
188
124
|
this.name = "MessageNotAvailableError";
|
|
189
125
|
}
|
|
190
126
|
};
|
|
191
|
-
var FifoOrderingViolationError = class extends Error {
|
|
192
|
-
nextMessageId;
|
|
193
|
-
constructor(messageId, nextMessageId, reason) {
|
|
194
|
-
super(
|
|
195
|
-
`FIFO ordering violation for message ${messageId}: ${reason}. Process message ${nextMessageId} first.`
|
|
196
|
-
);
|
|
197
|
-
this.name = "FifoOrderingViolationError";
|
|
198
|
-
this.nextMessageId = nextMessageId;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
127
|
var MessageCorruptedError = class extends Error {
|
|
202
128
|
constructor(messageId, reason) {
|
|
203
129
|
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
@@ -239,16 +165,6 @@ var BadRequestError = class extends Error {
|
|
|
239
165
|
this.name = "BadRequestError";
|
|
240
166
|
}
|
|
241
167
|
};
|
|
242
|
-
var FailedDependencyError = class extends Error {
|
|
243
|
-
nextMessageId;
|
|
244
|
-
constructor(messageId, nextMessageId) {
|
|
245
|
-
super(
|
|
246
|
-
`Failed dependency: FIFO ordering violation for message ${messageId}. Must process message ${nextMessageId} first.`
|
|
247
|
-
);
|
|
248
|
-
this.name = "FailedDependencyError";
|
|
249
|
-
this.nextMessageId = nextMessageId;
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
168
|
var InternalServerError = class extends Error {
|
|
253
169
|
constructor(message = "Unexpected server error") {
|
|
254
170
|
super(message);
|
|
@@ -261,12 +177,6 @@ var InvalidLimitError = class extends Error {
|
|
|
261
177
|
this.name = "InvalidLimitError";
|
|
262
178
|
}
|
|
263
179
|
};
|
|
264
|
-
var InvalidCallbackError = class extends Error {
|
|
265
|
-
constructor(message) {
|
|
266
|
-
super(message);
|
|
267
|
-
this.name = "InvalidCallbackError";
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
180
|
|
|
271
181
|
// src/client.ts
|
|
272
182
|
async function consumeStream(stream) {
|
|
@@ -296,40 +206,39 @@ function parseQueueHeaders(headers) {
|
|
|
296
206
|
return {
|
|
297
207
|
messageId,
|
|
298
208
|
deliveryCount,
|
|
299
|
-
timestamp,
|
|
209
|
+
createdAt: new Date(timestamp),
|
|
300
210
|
contentType,
|
|
301
211
|
ticket
|
|
302
212
|
};
|
|
303
213
|
}
|
|
304
|
-
var QueueClient = class
|
|
214
|
+
var QueueClient = class {
|
|
305
215
|
baseUrl;
|
|
306
|
-
|
|
216
|
+
basePath;
|
|
217
|
+
customHeaders = {};
|
|
307
218
|
/**
|
|
308
219
|
* Create a new Vercel Queue Service client
|
|
309
220
|
* @param options Client configuration options
|
|
310
221
|
*/
|
|
311
|
-
constructor(options) {
|
|
312
|
-
this.baseUrl = options.baseUrl || "https://
|
|
313
|
-
this.
|
|
222
|
+
constructor(options = {}) {
|
|
223
|
+
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
224
|
+
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
|
|
225
|
+
const VERCEL_QUEUE_HEADER_PREFIX = "VERCEL_QUEUE_HEADER_";
|
|
226
|
+
this.customHeaders = Object.fromEntries(
|
|
227
|
+
Object.entries(process.env).filter(([key]) => key.startsWith(VERCEL_QUEUE_HEADER_PREFIX)).map(([key, value]) => [
|
|
228
|
+
// This allows headers to use dashes independent of shell used
|
|
229
|
+
key.replace(VERCEL_QUEUE_HEADER_PREFIX, "").replaceAll("__", "-"),
|
|
230
|
+
value || ""
|
|
231
|
+
])
|
|
232
|
+
);
|
|
314
233
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
* This method automatically retrieves the OIDC token from the Vercel Function environment
|
|
318
|
-
* Always creates a fresh instance since OIDC tokens expire after 15 minutes
|
|
319
|
-
* @param baseUrl Optional base URL override
|
|
320
|
-
* @returns Promise resolving to a new QueueClient instance
|
|
321
|
-
*/
|
|
322
|
-
static async fromVercelFunction(baseUrl) {
|
|
323
|
-
const token = await getVercelOidcToken();
|
|
234
|
+
async getToken() {
|
|
235
|
+
const token = await (0, import_oidc.getVercelOidcToken)();
|
|
324
236
|
if (!token) {
|
|
325
237
|
throw new Error(
|
|
326
|
-
"Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment."
|
|
238
|
+
"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'"
|
|
327
239
|
);
|
|
328
240
|
}
|
|
329
|
-
return
|
|
330
|
-
token,
|
|
331
|
-
baseUrl
|
|
332
|
-
});
|
|
241
|
+
return token;
|
|
333
242
|
}
|
|
334
243
|
/**
|
|
335
244
|
* Send a message to a queue
|
|
@@ -342,51 +251,28 @@ var QueueClient = class _QueueClient {
|
|
|
342
251
|
* @throws {InternalServerError} When server encounters an error
|
|
343
252
|
*/
|
|
344
253
|
async sendMessage(options, transport) {
|
|
345
|
-
const { queueName, payload, idempotencyKey, retentionSeconds
|
|
254
|
+
const { queueName, payload, idempotencyKey, retentionSeconds } = options;
|
|
346
255
|
const headers = new Headers({
|
|
347
|
-
Authorization: `Bearer ${this.
|
|
256
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
348
257
|
"Vqs-Queue-Name": queueName,
|
|
349
|
-
"Content-Type": transport.contentType
|
|
258
|
+
"Content-Type": transport.contentType,
|
|
259
|
+
...this.customHeaders
|
|
350
260
|
});
|
|
261
|
+
const deploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
262
|
+
if (deploymentId) {
|
|
263
|
+
headers.set("Vqs-Deployment-Id", deploymentId);
|
|
264
|
+
}
|
|
351
265
|
if (idempotencyKey) {
|
|
352
266
|
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
353
267
|
}
|
|
354
268
|
if (retentionSeconds !== void 0) {
|
|
355
269
|
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
356
270
|
}
|
|
357
|
-
let normalizedCallbacks;
|
|
358
|
-
if (callback) {
|
|
359
|
-
if ("url" in callback && typeof callback.url === "string") {
|
|
360
|
-
normalizedCallbacks = { default: callback };
|
|
361
|
-
} else {
|
|
362
|
-
normalizedCallbacks = callback;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
let localhostCallbacks = [];
|
|
366
|
-
if (normalizedCallbacks) {
|
|
367
|
-
const isDevelopment = process.env.NODE_ENV === "development";
|
|
368
|
-
if (isDevelopment) {
|
|
369
|
-
localhostCallbacks = processDevelopmentCallbacks(normalizedCallbacks);
|
|
370
|
-
} else {
|
|
371
|
-
const endpoints = Object.entries(normalizedCallbacks).map(
|
|
372
|
-
([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
|
|
373
|
-
).join(",");
|
|
374
|
-
headers.set("Vqs-Callback-Url", endpoints);
|
|
375
|
-
const delays = Object.entries(normalizedCallbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
|
|
376
|
-
if (delays) {
|
|
377
|
-
headers.set("Vqs-Callback-Delay", delays);
|
|
378
|
-
}
|
|
379
|
-
const frequencies = Object.entries(normalizedCallbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
|
|
380
|
-
if (frequencies) {
|
|
381
|
-
headers.set("Vqs-Callback-Frequency", frequencies);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
271
|
const body = transport.serialize(payload);
|
|
386
|
-
const response = await fetch(`${this.baseUrl}
|
|
272
|
+
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
387
273
|
method: "POST",
|
|
388
|
-
|
|
389
|
-
|
|
274
|
+
body,
|
|
275
|
+
headers
|
|
390
276
|
});
|
|
391
277
|
if (!response.ok) {
|
|
392
278
|
if (response.status === 400) {
|
|
@@ -412,9 +298,6 @@ var QueueClient = class _QueueClient {
|
|
|
412
298
|
);
|
|
413
299
|
}
|
|
414
300
|
const responseData = await response.json();
|
|
415
|
-
if (localhostCallbacks.length > 0) {
|
|
416
|
-
fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
|
|
417
|
-
}
|
|
418
301
|
return responseData;
|
|
419
302
|
}
|
|
420
303
|
/**
|
|
@@ -424,7 +307,7 @@ var QueueClient = class _QueueClient {
|
|
|
424
307
|
* @returns AsyncGenerator that yields messages as they arrive
|
|
425
308
|
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
426
309
|
* @throws {QueueEmptyError} When no messages are available (204)
|
|
427
|
-
* @throws {MessageLockedError} When
|
|
310
|
+
* @throws {MessageLockedError} When messages are temporarily locked (423)
|
|
428
311
|
* @throws {BadRequestError} When request parameters are invalid
|
|
429
312
|
* @throws {UnauthorizedError} When authentication fails
|
|
430
313
|
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
@@ -436,10 +319,11 @@ var QueueClient = class _QueueClient {
|
|
|
436
319
|
throw new InvalidLimitError(limit);
|
|
437
320
|
}
|
|
438
321
|
const headers = new Headers({
|
|
439
|
-
Authorization: `Bearer ${this.
|
|
322
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
440
323
|
"Vqs-Queue-Name": queueName,
|
|
441
324
|
"Vqs-Consumer-Group": consumerGroup,
|
|
442
|
-
Accept: "multipart/mixed"
|
|
325
|
+
Accept: "multipart/mixed",
|
|
326
|
+
...this.customHeaders
|
|
443
327
|
});
|
|
444
328
|
if (visibilityTimeoutSeconds !== void 0) {
|
|
445
329
|
headers.set(
|
|
@@ -450,7 +334,7 @@ var QueueClient = class _QueueClient {
|
|
|
450
334
|
if (limit !== void 0) {
|
|
451
335
|
headers.set("Vqs-Limit", limit.toString());
|
|
452
336
|
}
|
|
453
|
-
const response = await fetch(`${this.baseUrl}
|
|
337
|
+
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
454
338
|
method: "GET",
|
|
455
339
|
headers
|
|
456
340
|
});
|
|
@@ -475,7 +359,7 @@ var QueueClient = class _QueueClient {
|
|
|
475
359
|
const parsed = parseInt(retryAfterHeader, 10);
|
|
476
360
|
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
477
361
|
}
|
|
478
|
-
throw new MessageLockedError("next message
|
|
362
|
+
throw new MessageLockedError("next message", retryAfter);
|
|
479
363
|
}
|
|
480
364
|
if (response.status >= 500) {
|
|
481
365
|
throw new InternalServerError(
|
|
@@ -517,10 +401,11 @@ var QueueClient = class _QueueClient {
|
|
|
517
401
|
skipPayload
|
|
518
402
|
} = options;
|
|
519
403
|
const headers = new Headers({
|
|
520
|
-
Authorization: `Bearer ${this.
|
|
404
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
521
405
|
"Vqs-Queue-Name": queueName,
|
|
522
406
|
"Vqs-Consumer-Group": consumerGroup,
|
|
523
|
-
Accept: "multipart/mixed"
|
|
407
|
+
Accept: "multipart/mixed",
|
|
408
|
+
...this.customHeaders
|
|
524
409
|
});
|
|
525
410
|
if (visibilityTimeoutSeconds !== void 0) {
|
|
526
411
|
headers.set(
|
|
@@ -532,7 +417,7 @@ var QueueClient = class _QueueClient {
|
|
|
532
417
|
headers.set("Vqs-Skip-Payload", "1");
|
|
533
418
|
}
|
|
534
419
|
const response = await fetch(
|
|
535
|
-
`${this.baseUrl}
|
|
420
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
536
421
|
{
|
|
537
422
|
method: "GET",
|
|
538
423
|
headers
|
|
@@ -561,37 +446,7 @@ var QueueClient = class _QueueClient {
|
|
|
561
446
|
}
|
|
562
447
|
throw new MessageLockedError(messageId, retryAfter);
|
|
563
448
|
}
|
|
564
|
-
if (response.status === 424) {
|
|
565
|
-
try {
|
|
566
|
-
const errorData = await response.json();
|
|
567
|
-
if (errorData.meta?.nextMessageId) {
|
|
568
|
-
throw new FailedDependencyError(
|
|
569
|
-
messageId,
|
|
570
|
-
errorData.meta.nextMessageId
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
} catch (parseError) {
|
|
574
|
-
if (parseError instanceof FailedDependencyError) {
|
|
575
|
-
throw parseError;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
throw new MessageNotAvailableError(
|
|
579
|
-
messageId,
|
|
580
|
-
"FIFO ordering violation"
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
449
|
if (response.status === 409) {
|
|
584
|
-
try {
|
|
585
|
-
const errorData = await response.json();
|
|
586
|
-
if (errorData.nextMessageId) {
|
|
587
|
-
throw new FifoOrderingViolationError(
|
|
588
|
-
messageId,
|
|
589
|
-
errorData.nextMessageId,
|
|
590
|
-
errorData.error
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
} catch (parseError) {
|
|
594
|
-
}
|
|
595
450
|
throw new MessageNotAvailableError(messageId);
|
|
596
451
|
}
|
|
597
452
|
if (response.status >= 500) {
|
|
@@ -671,14 +526,15 @@ var QueueClient = class _QueueClient {
|
|
|
671
526
|
async deleteMessage(options) {
|
|
672
527
|
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
673
528
|
const response = await fetch(
|
|
674
|
-
`${this.baseUrl}
|
|
529
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
675
530
|
{
|
|
676
531
|
method: "DELETE",
|
|
677
532
|
headers: new Headers({
|
|
678
|
-
Authorization: `Bearer ${this.
|
|
533
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
679
534
|
"Vqs-Queue-Name": queueName,
|
|
680
535
|
"Vqs-Consumer-Group": consumerGroup,
|
|
681
|
-
"Vqs-Ticket": ticket
|
|
536
|
+
"Vqs-Ticket": ticket,
|
|
537
|
+
...this.customHeaders
|
|
682
538
|
})
|
|
683
539
|
}
|
|
684
540
|
);
|
|
@@ -732,15 +588,16 @@ var QueueClient = class _QueueClient {
|
|
|
732
588
|
visibilityTimeoutSeconds
|
|
733
589
|
} = options;
|
|
734
590
|
const response = await fetch(
|
|
735
|
-
`${this.baseUrl}
|
|
591
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
736
592
|
{
|
|
737
593
|
method: "PATCH",
|
|
738
594
|
headers: new Headers({
|
|
739
|
-
Authorization: `Bearer ${this.
|
|
595
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
740
596
|
"Vqs-Queue-Name": queueName,
|
|
741
597
|
"Vqs-Consumer-Group": consumerGroup,
|
|
742
598
|
"Vqs-Ticket": ticket,
|
|
743
|
-
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString()
|
|
599
|
+
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
|
|
600
|
+
...this.customHeaders
|
|
744
601
|
})
|
|
745
602
|
}
|
|
746
603
|
);
|
|
@@ -778,81 +635,303 @@ var QueueClient = class _QueueClient {
|
|
|
778
635
|
}
|
|
779
636
|
};
|
|
780
637
|
|
|
781
|
-
// src/
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
638
|
+
// src/callback.ts
|
|
639
|
+
function validateWildcardPattern(pattern) {
|
|
640
|
+
const firstIndex = pattern.indexOf("*");
|
|
641
|
+
const lastIndex = pattern.lastIndexOf("*");
|
|
642
|
+
if (firstIndex !== lastIndex) {
|
|
643
|
+
return false;
|
|
786
644
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
645
|
+
if (firstIndex === -1) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
if (firstIndex !== pattern.length - 1) {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
653
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
654
|
+
const prefix = pattern.slice(0, -1);
|
|
655
|
+
return topicName.startsWith(prefix);
|
|
656
|
+
}
|
|
657
|
+
function findTopicHandler(queueName, handlers) {
|
|
658
|
+
const exactHandler = handlers[queueName];
|
|
659
|
+
if (exactHandler) {
|
|
660
|
+
return exactHandler;
|
|
661
|
+
}
|
|
662
|
+
for (const pattern in handlers) {
|
|
663
|
+
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
664
|
+
return handlers[pattern];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
async function parseCallback(request) {
|
|
670
|
+
const contentType = request.headers.get("content-type");
|
|
671
|
+
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
"Invalid content type: expected 'application/cloudevents+json'"
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
let cloudEvent;
|
|
677
|
+
try {
|
|
678
|
+
cloudEvent = await request.json();
|
|
679
|
+
} catch (error) {
|
|
680
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
681
|
+
}
|
|
682
|
+
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
683
|
+
throw new Error("Invalid CloudEvent: missing required fields");
|
|
684
|
+
}
|
|
685
|
+
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
const missingFields = [];
|
|
691
|
+
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
692
|
+
if (!("consumerGroup" in cloudEvent.data))
|
|
693
|
+
missingFields.push("consumerGroup");
|
|
694
|
+
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
695
|
+
if (missingFields.length > 0) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
701
|
+
return {
|
|
702
|
+
queueName,
|
|
703
|
+
consumerGroup,
|
|
704
|
+
messageId
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function handleCallback(handlers) {
|
|
708
|
+
for (const topicPattern in handlers) {
|
|
709
|
+
if (topicPattern.includes("*")) {
|
|
710
|
+
if (!validateWildcardPattern(topicPattern)) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
713
|
+
);
|
|
795
714
|
}
|
|
796
|
-
} finally {
|
|
797
|
-
reader.releaseLock();
|
|
798
715
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
716
|
+
}
|
|
717
|
+
const routeHandler = async (request) => {
|
|
718
|
+
try {
|
|
719
|
+
const { queueName, consumerGroup, messageId } = await parseCallback(request);
|
|
720
|
+
const topicHandler = findTopicHandler(queueName, handlers);
|
|
721
|
+
if (!topicHandler) {
|
|
722
|
+
const availableTopics = Object.keys(handlers).join(", ");
|
|
723
|
+
return Response.json(
|
|
724
|
+
{
|
|
725
|
+
error: `No handler found for topic: ${queueName}`,
|
|
726
|
+
availableTopics
|
|
727
|
+
},
|
|
728
|
+
{ status: 404 }
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
732
|
+
if (!consumerGroupHandler) {
|
|
733
|
+
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
734
|
+
return Response.json(
|
|
735
|
+
{
|
|
736
|
+
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
737
|
+
availableGroups
|
|
738
|
+
},
|
|
739
|
+
{ status: 404 }
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
const client = new QueueClient();
|
|
743
|
+
const topic = new Topic(client, queueName);
|
|
744
|
+
const cg = topic.consumerGroup(consumerGroup);
|
|
745
|
+
await cg.consume(consumerGroupHandler, { messageId });
|
|
746
|
+
return Response.json({ status: "success" });
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.error("Queue callback error:", error);
|
|
749
|
+
if (error instanceof Error && (error.message.includes("Missing required CloudEvent data fields") || error.message.includes("Invalid CloudEvent") || error.message.includes("Invalid CloudEvent type") || error.message.includes("Invalid content type") || error.message.includes("Failed to parse CloudEvent"))) {
|
|
750
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
751
|
+
}
|
|
752
|
+
return Response.json(
|
|
753
|
+
{ error: "Failed to process queue message" },
|
|
754
|
+
{ status: 500 }
|
|
755
|
+
);
|
|
805
756
|
}
|
|
806
|
-
|
|
757
|
+
};
|
|
758
|
+
if (isDevMode()) {
|
|
759
|
+
registerDevRouteHandler(routeHandler, handlers);
|
|
807
760
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
761
|
+
return routeHandler;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// src/dev.ts
|
|
765
|
+
var devRouteHandlers = /* @__PURE__ */ new Map();
|
|
766
|
+
var wildcardRouteHandlers = /* @__PURE__ */ new Map();
|
|
767
|
+
function cleanupDeadRefs(key, refs) {
|
|
768
|
+
const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
|
|
769
|
+
if (aliveRefs.length === 0) {
|
|
770
|
+
wildcardRouteHandlers.delete(key);
|
|
771
|
+
} else if (aliveRefs.length < refs.length) {
|
|
772
|
+
wildcardRouteHandlers.set(key, aliveRefs);
|
|
813
773
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
774
|
+
}
|
|
775
|
+
function isDevMode() {
|
|
776
|
+
return process.env.NODE_ENV === "development";
|
|
777
|
+
}
|
|
778
|
+
function registerDevRouteHandler(routeHandler, handlers) {
|
|
779
|
+
for (const topicName in handlers) {
|
|
780
|
+
for (const consumerGroup in handlers[topicName]) {
|
|
781
|
+
const key = `${topicName}:${consumerGroup}`;
|
|
782
|
+
if (topicName.includes("*")) {
|
|
783
|
+
const existing = wildcardRouteHandlers.get(key) || [];
|
|
784
|
+
cleanupDeadRefs(key, existing);
|
|
785
|
+
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
786
|
+
const weakRef = new WeakRef(routeHandler);
|
|
787
|
+
cleanedRefs.push(weakRef);
|
|
788
|
+
wildcardRouteHandlers.set(key, cleanedRefs);
|
|
789
|
+
} else {
|
|
790
|
+
devRouteHandlers.set(key, {
|
|
791
|
+
routeHandler,
|
|
792
|
+
topicPattern: topicName
|
|
793
|
+
});
|
|
822
794
|
}
|
|
823
|
-
} finally {
|
|
824
|
-
reader.releaseLock();
|
|
825
795
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function findRouteHandlersForTopic(topicName) {
|
|
799
|
+
const handlersMap = /* @__PURE__ */ new Map();
|
|
800
|
+
for (const [
|
|
801
|
+
key,
|
|
802
|
+
{ routeHandler, topicPattern }
|
|
803
|
+
] of devRouteHandlers.entries()) {
|
|
804
|
+
const [_, consumerGroup] = key.split(":");
|
|
805
|
+
if (topicPattern === topicName) {
|
|
806
|
+
if (!handlersMap.has(routeHandler)) {
|
|
807
|
+
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
808
|
+
}
|
|
809
|
+
handlersMap.get(routeHandler).add(consumerGroup);
|
|
832
810
|
}
|
|
833
|
-
return Buffer.from(buffer);
|
|
834
811
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
812
|
+
for (const [key, refs] of wildcardRouteHandlers.entries()) {
|
|
813
|
+
const [pattern, consumerGroup] = key.split(":");
|
|
814
|
+
if (matchesWildcardPattern(topicName, pattern)) {
|
|
815
|
+
cleanupDeadRefs(key, refs);
|
|
816
|
+
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
817
|
+
for (const ref of cleanedRefs) {
|
|
818
|
+
const routeHandler = ref.deref();
|
|
819
|
+
if (routeHandler) {
|
|
820
|
+
if (!handlersMap.has(routeHandler)) {
|
|
821
|
+
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
822
|
+
}
|
|
823
|
+
handlersMap.get(routeHandler).add(consumerGroup);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
840
827
|
}
|
|
841
|
-
|
|
842
|
-
|
|
828
|
+
return handlersMap;
|
|
829
|
+
}
|
|
830
|
+
function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
|
|
831
|
+
const cloudEvent = {
|
|
832
|
+
type: "com.vercel.queue.v1beta",
|
|
833
|
+
source: `/topic/${topicName}/consumer/${consumerGroup}`,
|
|
834
|
+
id: messageId,
|
|
835
|
+
datacontenttype: "application/json",
|
|
836
|
+
data: {
|
|
837
|
+
messageId,
|
|
838
|
+
queueName: topicName,
|
|
839
|
+
consumerGroup
|
|
840
|
+
},
|
|
841
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
842
|
+
specversion: "1.0"
|
|
843
|
+
};
|
|
844
|
+
return new Request("https://localhost/api/queue/callback", {
|
|
845
|
+
method: "POST",
|
|
846
|
+
headers: {
|
|
847
|
+
"Content-Type": "application/cloudevents+json"
|
|
848
|
+
},
|
|
849
|
+
body: JSON.stringify(cloudEvent)
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
var DEV_CALLBACK_DELAY = 1e3;
|
|
853
|
+
function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
|
|
854
|
+
console.log(
|
|
855
|
+
`[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
|
|
856
|
+
);
|
|
857
|
+
setTimeout(
|
|
858
|
+
() => {
|
|
859
|
+
console.log(
|
|
860
|
+
`[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
|
|
861
|
+
);
|
|
862
|
+
triggerDevCallbacks(topicName, messageId);
|
|
863
|
+
},
|
|
864
|
+
timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
function triggerDevCallbacks(topicName, messageId) {
|
|
868
|
+
const handlersMap = findRouteHandlersForTopic(topicName);
|
|
869
|
+
if (handlersMap.size === 0) {
|
|
870
|
+
return;
|
|
843
871
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
872
|
+
const consumerGroups = Array.from(
|
|
873
|
+
new Set(
|
|
874
|
+
Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
console.log(
|
|
878
|
+
`[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
|
|
879
|
+
);
|
|
880
|
+
setTimeout(async () => {
|
|
881
|
+
for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
|
|
882
|
+
for (const consumerGroup of consumerGroups2) {
|
|
883
|
+
try {
|
|
884
|
+
const request = createMockCloudEventRequest(
|
|
885
|
+
topicName,
|
|
886
|
+
consumerGroup,
|
|
887
|
+
messageId
|
|
888
|
+
);
|
|
889
|
+
const response = await routeHandler(request);
|
|
890
|
+
if (response.ok) {
|
|
891
|
+
try {
|
|
892
|
+
const responseData = await response.json();
|
|
893
|
+
if (responseData.status === "success") {
|
|
894
|
+
console.log(
|
|
895
|
+
`[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
} catch (jsonError) {
|
|
899
|
+
console.error(
|
|
900
|
+
`[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
|
|
901
|
+
jsonError
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
} else {
|
|
905
|
+
try {
|
|
906
|
+
const errorData = await response.json();
|
|
907
|
+
console.error(
|
|
908
|
+
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
909
|
+
errorData.error || response.statusText
|
|
910
|
+
);
|
|
911
|
+
} catch (jsonError) {
|
|
912
|
+
console.error(
|
|
913
|
+
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
914
|
+
response.statusText
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
} catch (error) {
|
|
919
|
+
console.error(
|
|
920
|
+
`[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
|
|
921
|
+
error
|
|
922
|
+
);
|
|
923
|
+
}
|
|
850
924
|
}
|
|
851
|
-
} finally {
|
|
852
|
-
reader.releaseLock();
|
|
853
925
|
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
926
|
+
}, DEV_CALLBACK_DELAY);
|
|
927
|
+
}
|
|
928
|
+
function clearDevHandlers() {
|
|
929
|
+
devRouteHandlers.clear();
|
|
930
|
+
wildcardRouteHandlers.clear();
|
|
931
|
+
}
|
|
932
|
+
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
933
|
+
globalThis.__clearDevHandlers = clearDevHandlers;
|
|
934
|
+
}
|
|
856
935
|
|
|
857
936
|
// src/consumer-group.ts
|
|
858
937
|
var ConsumerGroup = class {
|
|
@@ -953,7 +1032,13 @@ var ConsumerGroup = class {
|
|
|
953
1032
|
message.ticket
|
|
954
1033
|
);
|
|
955
1034
|
try {
|
|
956
|
-
const result = await handler(message
|
|
1035
|
+
const result = await handler(message.payload, {
|
|
1036
|
+
messageId: message.messageId,
|
|
1037
|
+
deliveryCount: message.deliveryCount,
|
|
1038
|
+
createdAt: message.createdAt,
|
|
1039
|
+
topicName: this.topicName,
|
|
1040
|
+
consumerGroup: this.consumerGroupName
|
|
1041
|
+
});
|
|
957
1042
|
await stopExtension();
|
|
958
1043
|
if (result && "timeoutSeconds" in result) {
|
|
959
1044
|
await this.client.changeVisibility({
|
|
@@ -963,6 +1048,13 @@ var ConsumerGroup = class {
|
|
|
963
1048
|
ticket: message.ticket,
|
|
964
1049
|
visibilityTimeoutSeconds: result.timeoutSeconds
|
|
965
1050
|
});
|
|
1051
|
+
if (isDevMode()) {
|
|
1052
|
+
scheduleDevTimeout(
|
|
1053
|
+
this.topicName,
|
|
1054
|
+
message.messageId,
|
|
1055
|
+
result.timeoutSeconds
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
966
1058
|
} else {
|
|
967
1059
|
await this.client.deleteMessage({
|
|
968
1060
|
queueName: this.topicName,
|
|
@@ -983,219 +1075,58 @@ var ConsumerGroup = class {
|
|
|
983
1075
|
throw error;
|
|
984
1076
|
}
|
|
985
1077
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
* @param options Processing options
|
|
991
|
-
* @returns Promise that resolves when processing stops (due to signal or error)
|
|
992
|
-
*/
|
|
993
|
-
async subscribe(signal, handler, options = {}) {
|
|
994
|
-
const pollingInterval = options.pollingInterval || 1e3;
|
|
995
|
-
while (!signal.aborted) {
|
|
996
|
-
try {
|
|
997
|
-
for await (const message of this.client.receiveMessages(
|
|
1078
|
+
async consume(handler, options) {
|
|
1079
|
+
if (options?.messageId) {
|
|
1080
|
+
if (options.skipPayload) {
|
|
1081
|
+
const response = await this.client.receiveMessageById(
|
|
998
1082
|
{
|
|
999
1083
|
queueName: this.topicName,
|
|
1000
1084
|
consumerGroup: this.consumerGroupName,
|
|
1085
|
+
messageId: options.messageId,
|
|
1001
1086
|
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1002
|
-
|
|
1003
|
-
// Always process one message at a time
|
|
1087
|
+
skipPayload: true
|
|
1004
1088
|
},
|
|
1005
1089
|
this.transport
|
|
1006
|
-
)
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
}
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
if (error instanceof MessageLockedError) {
|
|
1047
|
-
const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
|
|
1048
|
-
if (!signal.aborted) {
|
|
1049
|
-
await new Promise((resolve) => {
|
|
1050
|
-
const timeoutId = setTimeout(resolve, waitTime);
|
|
1051
|
-
signal.addEventListener(
|
|
1052
|
-
"abort",
|
|
1053
|
-
() => {
|
|
1054
|
-
clearTimeout(timeoutId);
|
|
1055
|
-
resolve();
|
|
1056
|
-
},
|
|
1057
|
-
{ once: true }
|
|
1058
|
-
);
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
console.error("Error polling topic:", error);
|
|
1064
|
-
throw error;
|
|
1090
|
+
);
|
|
1091
|
+
await this.processMessage(
|
|
1092
|
+
response.message,
|
|
1093
|
+
handler
|
|
1094
|
+
);
|
|
1095
|
+
} else {
|
|
1096
|
+
const response = await this.client.receiveMessageById(
|
|
1097
|
+
{
|
|
1098
|
+
queueName: this.topicName,
|
|
1099
|
+
consumerGroup: this.consumerGroupName,
|
|
1100
|
+
messageId: options.messageId,
|
|
1101
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1102
|
+
},
|
|
1103
|
+
this.transport
|
|
1104
|
+
);
|
|
1105
|
+
await this.processMessage(
|
|
1106
|
+
response.message,
|
|
1107
|
+
handler
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
} else {
|
|
1111
|
+
let messageFound = false;
|
|
1112
|
+
for await (const message of this.client.receiveMessages(
|
|
1113
|
+
{
|
|
1114
|
+
queueName: this.topicName,
|
|
1115
|
+
consumerGroup: this.consumerGroupName,
|
|
1116
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1117
|
+
limit: 1
|
|
1118
|
+
},
|
|
1119
|
+
this.transport
|
|
1120
|
+
)) {
|
|
1121
|
+
messageFound = true;
|
|
1122
|
+
await this.processMessage(message, handler);
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
if (!messageFound) {
|
|
1126
|
+
throw new Error("No messages available");
|
|
1065
1127
|
}
|
|
1066
1128
|
}
|
|
1067
1129
|
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Receive and process a specific message by its ID with full payload
|
|
1070
|
-
* @param messageId The ID of the message to receive and process
|
|
1071
|
-
* @param handler Function to process the message with full payload
|
|
1072
|
-
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1073
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1074
|
-
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1075
|
-
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1076
|
-
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1077
|
-
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1078
|
-
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1079
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1080
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1081
|
-
* @throws {ForbiddenError} When access is denied
|
|
1082
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1083
|
-
*/
|
|
1084
|
-
async receiveMessage(messageId, handler) {
|
|
1085
|
-
const response = await this.client.receiveMessageById(
|
|
1086
|
-
{
|
|
1087
|
-
queueName: this.topicName,
|
|
1088
|
-
consumerGroup: this.consumerGroupName,
|
|
1089
|
-
messageId,
|
|
1090
|
-
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1091
|
-
},
|
|
1092
|
-
this.transport
|
|
1093
|
-
);
|
|
1094
|
-
await this.processMessage(response.message, handler);
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Receive and process the next available message from the queue
|
|
1098
|
-
* @param handler Function to process the message
|
|
1099
|
-
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1100
|
-
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1101
|
-
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1102
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1103
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1104
|
-
* @throws {ForbiddenError} When access is denied
|
|
1105
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1106
|
-
*/
|
|
1107
|
-
async receiveNextMessage(handler) {
|
|
1108
|
-
let messageFound = false;
|
|
1109
|
-
for await (const message of this.client.receiveMessages(
|
|
1110
|
-
{
|
|
1111
|
-
queueName: this.topicName,
|
|
1112
|
-
consumerGroup: this.consumerGroupName,
|
|
1113
|
-
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1114
|
-
limit: 1
|
|
1115
|
-
},
|
|
1116
|
-
this.transport
|
|
1117
|
-
)) {
|
|
1118
|
-
messageFound = true;
|
|
1119
|
-
await this.processMessage(message, handler);
|
|
1120
|
-
break;
|
|
1121
|
-
}
|
|
1122
|
-
if (!messageFound) {
|
|
1123
|
-
throw new Error("No messages available");
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
/**
|
|
1127
|
-
* Receive and process multiple next available messages from the queue
|
|
1128
|
-
* @param limit Number of messages to process (1-10)
|
|
1129
|
-
* @param handler Function to process each message
|
|
1130
|
-
* @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
|
|
1131
|
-
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
1132
|
-
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1133
|
-
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1134
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1135
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1136
|
-
* @throws {ForbiddenError} When access is denied
|
|
1137
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1138
|
-
*/
|
|
1139
|
-
async receiveNextMessages(limit, handler) {
|
|
1140
|
-
if (limit < 1 || limit > 10) {
|
|
1141
|
-
throw new InvalidLimitError(limit);
|
|
1142
|
-
}
|
|
1143
|
-
const processingPromises = [];
|
|
1144
|
-
let messageCount = 0;
|
|
1145
|
-
for await (const message of this.client.receiveMessages(
|
|
1146
|
-
{
|
|
1147
|
-
queueName: this.topicName,
|
|
1148
|
-
consumerGroup: this.consumerGroupName,
|
|
1149
|
-
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1150
|
-
limit
|
|
1151
|
-
},
|
|
1152
|
-
this.transport
|
|
1153
|
-
)) {
|
|
1154
|
-
messageCount++;
|
|
1155
|
-
const wrappedPromise = this.processMessage(message, handler).then(
|
|
1156
|
-
(value) => ({
|
|
1157
|
-
status: "fulfilled",
|
|
1158
|
-
value
|
|
1159
|
-
}),
|
|
1160
|
-
(reason) => ({ status: "rejected", reason })
|
|
1161
|
-
);
|
|
1162
|
-
processingPromises.push(wrappedPromise);
|
|
1163
|
-
}
|
|
1164
|
-
if (messageCount === 0) {
|
|
1165
|
-
throw new Error("No messages available");
|
|
1166
|
-
}
|
|
1167
|
-
const results = await Promise.all(processingPromises);
|
|
1168
|
-
return results;
|
|
1169
|
-
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Handle a specific message by its ID without downloading the payload (metadata only)
|
|
1172
|
-
* @param messageId The ID of the message to handle
|
|
1173
|
-
* @param handler Function to process the message metadata (payload will be void)
|
|
1174
|
-
* @returns Promise that resolves when the message is handled or rejects with specific errors
|
|
1175
|
-
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1176
|
-
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1177
|
-
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1178
|
-
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1179
|
-
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1180
|
-
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1181
|
-
* @throws {BadRequestError} When request parameters are invalid
|
|
1182
|
-
* @throws {UnauthorizedError} When authentication fails
|
|
1183
|
-
* @throws {ForbiddenError} When access is denied
|
|
1184
|
-
* @throws {InternalServerError} When server encounters an error
|
|
1185
|
-
*/
|
|
1186
|
-
async handleMessage(messageId, handler) {
|
|
1187
|
-
const response = await this.client.receiveMessageById(
|
|
1188
|
-
{
|
|
1189
|
-
queueName: this.topicName,
|
|
1190
|
-
consumerGroup: this.consumerGroupName,
|
|
1191
|
-
messageId,
|
|
1192
|
-
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1193
|
-
skipPayload: true
|
|
1194
|
-
},
|
|
1195
|
-
this.transport
|
|
1196
|
-
);
|
|
1197
|
-
await this.processMessage(response.message, handler);
|
|
1198
|
-
}
|
|
1199
1130
|
/**
|
|
1200
1131
|
* Get the consumer group name
|
|
1201
1132
|
*/
|
|
@@ -1243,10 +1174,13 @@ var Topic = class {
|
|
|
1243
1174
|
payload,
|
|
1244
1175
|
idempotencyKey: options?.idempotencyKey,
|
|
1245
1176
|
retentionSeconds: options?.retentionSeconds,
|
|
1246
|
-
|
|
1177
|
+
deploymentId: options?.deploymentId
|
|
1247
1178
|
},
|
|
1248
1179
|
this.transport
|
|
1249
1180
|
);
|
|
1181
|
+
if (isDevMode()) {
|
|
1182
|
+
triggerDevCallbacks(this.topicName, result.messageId);
|
|
1183
|
+
}
|
|
1250
1184
|
return { messageId: result.messageId };
|
|
1251
1185
|
}
|
|
1252
1186
|
/**
|
|
@@ -1282,108 +1216,61 @@ var Topic = class {
|
|
|
1282
1216
|
};
|
|
1283
1217
|
|
|
1284
1218
|
// src/factory.ts
|
|
1285
|
-
function
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
if (
|
|
1299
|
-
|
|
1300
|
-
throw new InvalidCallbackError(
|
|
1301
|
-
`Missing required queue callback headers: ${missingHeaders.join(", ")}`
|
|
1302
|
-
);
|
|
1219
|
+
async function send(topicName, payload, options) {
|
|
1220
|
+
const transport = options?.transport || new JsonTransport();
|
|
1221
|
+
const client = new QueueClient();
|
|
1222
|
+
const result = await client.sendMessage(
|
|
1223
|
+
{
|
|
1224
|
+
queueName: topicName,
|
|
1225
|
+
payload,
|
|
1226
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1227
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1228
|
+
deploymentId: options?.deploymentId
|
|
1229
|
+
},
|
|
1230
|
+
transport
|
|
1231
|
+
);
|
|
1232
|
+
if (isDevMode()) {
|
|
1233
|
+
triggerDevCallbacks(topicName, result.messageId);
|
|
1303
1234
|
}
|
|
1304
|
-
return {
|
|
1305
|
-
messageId,
|
|
1306
|
-
queueName,
|
|
1307
|
-
consumerGroup
|
|
1308
|
-
};
|
|
1235
|
+
return { messageId: result.messageId };
|
|
1309
1236
|
}
|
|
1310
|
-
function
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
throw new Error(
|
|
1322
|
-
`Topic "${queueName}" has a single handler but received consumer group "${consumerGroup}". Expected "default".`
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
actualHandler = topicHandler;
|
|
1326
|
-
} else {
|
|
1327
|
-
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1328
|
-
if (!consumerGroupHandler) {
|
|
1329
|
-
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1330
|
-
throw new Error(
|
|
1331
|
-
`No handler found for consumer group "${consumerGroup}" in topic "${queueName}". Available groups: ${availableGroups}`
|
|
1332
|
-
);
|
|
1333
|
-
}
|
|
1334
|
-
actualHandler = consumerGroupHandler;
|
|
1335
|
-
}
|
|
1336
|
-
const client = await QueueClient.fromVercelFunction();
|
|
1337
|
-
const topic = new Topic(client, queueName);
|
|
1338
|
-
const cg = topic.consumerGroup(consumerGroup);
|
|
1339
|
-
await cg.receiveMessage(messageId, async (message) => {
|
|
1340
|
-
const metadata = {
|
|
1341
|
-
messageId: message.messageId,
|
|
1342
|
-
deliveryCount: message.deliveryCount,
|
|
1343
|
-
timestamp: message.timestamp
|
|
1344
|
-
};
|
|
1345
|
-
return await actualHandler(message.payload, metadata);
|
|
1237
|
+
async function receive(topicName, consumerGroup, handler, options) {
|
|
1238
|
+
const transport = options?.transport || new JsonTransport();
|
|
1239
|
+
const client = new QueueClient();
|
|
1240
|
+
const topic = new Topic(client, topicName, transport);
|
|
1241
|
+
const { messageId, skipPayload, ...consumerGroupOptions } = options || {};
|
|
1242
|
+
const consumer = topic.consumerGroup(consumerGroup, consumerGroupOptions);
|
|
1243
|
+
if (messageId) {
|
|
1244
|
+
if (skipPayload) {
|
|
1245
|
+
return consumer.consume(handler, {
|
|
1246
|
+
messageId,
|
|
1247
|
+
skipPayload: true
|
|
1346
1248
|
});
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
console.error("Callback error:", error);
|
|
1350
|
-
if (error instanceof InvalidCallbackError) {
|
|
1351
|
-
return Response.json(
|
|
1352
|
-
{ error: "Invalid callback request" },
|
|
1353
|
-
{ status: 400 }
|
|
1354
|
-
);
|
|
1355
|
-
}
|
|
1356
|
-
return Response.json(
|
|
1357
|
-
{ error: "Failed to process callback" },
|
|
1358
|
-
{ status: 500 }
|
|
1359
|
-
);
|
|
1249
|
+
} else {
|
|
1250
|
+
return consumer.consume(handler, { messageId });
|
|
1360
1251
|
}
|
|
1361
|
-
}
|
|
1252
|
+
} else {
|
|
1253
|
+
return consumer.consume(handler);
|
|
1254
|
+
}
|
|
1362
1255
|
}
|
|
1363
1256
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1364
1257
|
0 && (module.exports = {
|
|
1365
1258
|
BadRequestError,
|
|
1366
1259
|
BufferTransport,
|
|
1367
|
-
ConsumerGroup,
|
|
1368
|
-
FailedDependencyError,
|
|
1369
|
-
FifoOrderingViolationError,
|
|
1370
1260
|
ForbiddenError,
|
|
1371
1261
|
InternalServerError,
|
|
1372
|
-
InvalidCallbackError,
|
|
1373
1262
|
InvalidLimitError,
|
|
1374
1263
|
JsonTransport,
|
|
1375
1264
|
MessageCorruptedError,
|
|
1376
1265
|
MessageLockedError,
|
|
1377
1266
|
MessageNotAvailableError,
|
|
1378
1267
|
MessageNotFoundError,
|
|
1379
|
-
QueueClient,
|
|
1380
1268
|
QueueEmptyError,
|
|
1381
1269
|
StreamTransport,
|
|
1382
|
-
Topic,
|
|
1383
1270
|
UnauthorizedError,
|
|
1384
|
-
createTopic,
|
|
1385
|
-
getVercelOidcToken,
|
|
1386
1271
|
handleCallback,
|
|
1387
|
-
|
|
1272
|
+
parseCallback,
|
|
1273
|
+
receive,
|
|
1274
|
+
send
|
|
1388
1275
|
});
|
|
1389
1276
|
//# sourceMappingURL=index.js.map
|