@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/pages.mjs
ADDED
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { parseMultipartStream } from "mixpart";
|
|
3
|
+
|
|
4
|
+
// src/oidc.ts
|
|
5
|
+
import { getVercelOidcToken } from "@vercel/oidc";
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var MessageNotFoundError = class extends Error {
|
|
9
|
+
constructor(messageId) {
|
|
10
|
+
super(`Message ${messageId} not found`);
|
|
11
|
+
this.name = "MessageNotFoundError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var MessageNotAvailableError = class extends Error {
|
|
15
|
+
constructor(messageId, reason) {
|
|
16
|
+
super(
|
|
17
|
+
`Message ${messageId} not available for processing${reason ? `: ${reason}` : ""}`
|
|
18
|
+
);
|
|
19
|
+
this.name = "MessageNotAvailableError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var MessageCorruptedError = class extends Error {
|
|
23
|
+
constructor(messageId, reason) {
|
|
24
|
+
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
25
|
+
this.name = "MessageCorruptedError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var QueueEmptyError = class extends Error {
|
|
29
|
+
constructor(queueName, consumerGroup) {
|
|
30
|
+
super(
|
|
31
|
+
`No messages available in queue "${queueName}" for consumer group "${consumerGroup}"`
|
|
32
|
+
);
|
|
33
|
+
this.name = "QueueEmptyError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var MessageLockedError = class extends Error {
|
|
37
|
+
retryAfter;
|
|
38
|
+
constructor(messageId, retryAfter) {
|
|
39
|
+
const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
|
|
40
|
+
super(`Message ${messageId} is temporarily locked.${retryMessage}`);
|
|
41
|
+
this.name = "MessageLockedError";
|
|
42
|
+
this.retryAfter = retryAfter;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var UnauthorizedError = class extends Error {
|
|
46
|
+
constructor(message = "Missing or invalid authentication token") {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "UnauthorizedError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var ForbiddenError = class extends Error {
|
|
52
|
+
constructor(message = "Queue environment doesn't match token environment") {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "ForbiddenError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var BadRequestError = class extends Error {
|
|
58
|
+
constructor(message) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "BadRequestError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var InternalServerError = class extends Error {
|
|
64
|
+
constructor(message = "Unexpected server error") {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "InternalServerError";
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var InvalidLimitError = class extends Error {
|
|
70
|
+
constructor(limit, min = 1, max = 10) {
|
|
71
|
+
super(`Invalid limit: ${limit}. Limit must be between ${min} and ${max}.`);
|
|
72
|
+
this.name = "InvalidLimitError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/client.ts
|
|
77
|
+
async function consumeStream(stream) {
|
|
78
|
+
const reader = stream.getReader();
|
|
79
|
+
try {
|
|
80
|
+
while (true) {
|
|
81
|
+
const { done } = await reader.read();
|
|
82
|
+
if (done) break;
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
reader.releaseLock();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function parseQueueHeaders(headers) {
|
|
89
|
+
const messageId = headers.get("Vqs-Message-Id");
|
|
90
|
+
const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
|
|
91
|
+
const timestamp = headers.get("Vqs-Timestamp");
|
|
92
|
+
const contentType = headers.get("Content-Type") || "application/octet-stream";
|
|
93
|
+
const ticket = headers.get("Vqs-Ticket");
|
|
94
|
+
if (!messageId || !timestamp || !ticket) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const deliveryCount = parseInt(deliveryCountStr, 10);
|
|
98
|
+
if (isNaN(deliveryCount)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
messageId,
|
|
103
|
+
deliveryCount,
|
|
104
|
+
createdAt: new Date(timestamp),
|
|
105
|
+
contentType,
|
|
106
|
+
ticket
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
var QueueClient = class {
|
|
110
|
+
baseUrl;
|
|
111
|
+
basePath;
|
|
112
|
+
customHeaders = {};
|
|
113
|
+
/**
|
|
114
|
+
* Create a new Vercel Queue Service client
|
|
115
|
+
* @param options Client configuration options
|
|
116
|
+
*/
|
|
117
|
+
constructor(options = {}) {
|
|
118
|
+
this.baseUrl = options.baseUrl || process.env.VERCEL_QUEUE_BASE_URL || "https://vercel-queue.com";
|
|
119
|
+
this.basePath = options.basePath || process.env.VERCEL_QUEUE_BASE_PATH || "/api/v2/messages";
|
|
120
|
+
const VERCEL_QUEUE_HEADER_PREFIX = "VERCEL_QUEUE_HEADER_";
|
|
121
|
+
this.customHeaders = Object.fromEntries(
|
|
122
|
+
Object.entries(process.env).filter(([key]) => key.startsWith(VERCEL_QUEUE_HEADER_PREFIX)).map(([key, value]) => [
|
|
123
|
+
// This allows headers to use dashes independent of shell used
|
|
124
|
+
key.replace(VERCEL_QUEUE_HEADER_PREFIX, "").replaceAll("__", "-"),
|
|
125
|
+
value || ""
|
|
126
|
+
])
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
async getToken() {
|
|
130
|
+
const token = await getVercelOidcToken();
|
|
131
|
+
if (!token) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
"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'"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return token;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Send a message to a queue
|
|
140
|
+
* @param options Send message options
|
|
141
|
+
* @param transport Serializer/deserializer for the payload
|
|
142
|
+
* @returns Promise with the message ID
|
|
143
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
144
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
145
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
146
|
+
* @throws {InternalServerError} When server encounters an error
|
|
147
|
+
*/
|
|
148
|
+
async sendMessage(options, transport) {
|
|
149
|
+
const { queueName, payload, idempotencyKey, retentionSeconds } = options;
|
|
150
|
+
const headers = new Headers({
|
|
151
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
152
|
+
"Vqs-Queue-Name": queueName,
|
|
153
|
+
"Content-Type": transport.contentType,
|
|
154
|
+
...this.customHeaders
|
|
155
|
+
});
|
|
156
|
+
const deploymentId = options.deploymentId || process.env.VERCEL_DEPLOYMENT_ID;
|
|
157
|
+
if (deploymentId) {
|
|
158
|
+
headers.set("Vqs-Deployment-Id", deploymentId);
|
|
159
|
+
}
|
|
160
|
+
if (idempotencyKey) {
|
|
161
|
+
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
162
|
+
}
|
|
163
|
+
if (retentionSeconds !== void 0) {
|
|
164
|
+
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
165
|
+
}
|
|
166
|
+
const body = transport.serialize(payload);
|
|
167
|
+
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
body,
|
|
170
|
+
headers
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
if (response.status === 400) {
|
|
174
|
+
const errorText = await response.text();
|
|
175
|
+
throw new BadRequestError(errorText || "Invalid parameters");
|
|
176
|
+
}
|
|
177
|
+
if (response.status === 401) {
|
|
178
|
+
throw new UnauthorizedError();
|
|
179
|
+
}
|
|
180
|
+
if (response.status === 403) {
|
|
181
|
+
throw new ForbiddenError();
|
|
182
|
+
}
|
|
183
|
+
if (response.status === 409) {
|
|
184
|
+
throw new Error("Duplicate idempotency key detected");
|
|
185
|
+
}
|
|
186
|
+
if (response.status >= 500) {
|
|
187
|
+
throw new InternalServerError(
|
|
188
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Failed to send message: ${response.status} ${response.statusText}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const responseData = await response.json();
|
|
196
|
+
return responseData;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Receive messages from a queue
|
|
200
|
+
* @param options Receive messages options
|
|
201
|
+
* @param transport Serializer/deserializer for the payload
|
|
202
|
+
* @returns AsyncGenerator that yields messages as they arrive
|
|
203
|
+
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
204
|
+
* @throws {QueueEmptyError} When no messages are available (204)
|
|
205
|
+
* @throws {MessageLockedError} When messages are temporarily locked (423)
|
|
206
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
207
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
208
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
209
|
+
* @throws {InternalServerError} When server encounters an error
|
|
210
|
+
*/
|
|
211
|
+
async *receiveMessages(options, transport) {
|
|
212
|
+
const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
|
|
213
|
+
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
214
|
+
throw new InvalidLimitError(limit);
|
|
215
|
+
}
|
|
216
|
+
const headers = new Headers({
|
|
217
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
218
|
+
"Vqs-Queue-Name": queueName,
|
|
219
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
220
|
+
Accept: "multipart/mixed",
|
|
221
|
+
...this.customHeaders
|
|
222
|
+
});
|
|
223
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
224
|
+
headers.set(
|
|
225
|
+
"Vqs-Visibility-Timeout",
|
|
226
|
+
visibilityTimeoutSeconds.toString()
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
if (limit !== void 0) {
|
|
230
|
+
headers.set("Vqs-Limit", limit.toString());
|
|
231
|
+
}
|
|
232
|
+
const response = await fetch(`${this.baseUrl}${this.basePath}`, {
|
|
233
|
+
method: "GET",
|
|
234
|
+
headers
|
|
235
|
+
});
|
|
236
|
+
if (response.status === 204) {
|
|
237
|
+
throw new QueueEmptyError(queueName, consumerGroup);
|
|
238
|
+
}
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
if (response.status === 400) {
|
|
241
|
+
const errorText = await response.text();
|
|
242
|
+
throw new BadRequestError(errorText || "Invalid parameters");
|
|
243
|
+
}
|
|
244
|
+
if (response.status === 401) {
|
|
245
|
+
throw new UnauthorizedError();
|
|
246
|
+
}
|
|
247
|
+
if (response.status === 403) {
|
|
248
|
+
throw new ForbiddenError();
|
|
249
|
+
}
|
|
250
|
+
if (response.status === 423) {
|
|
251
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
252
|
+
let retryAfter;
|
|
253
|
+
if (retryAfterHeader) {
|
|
254
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
255
|
+
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
256
|
+
}
|
|
257
|
+
throw new MessageLockedError("next message", retryAfter);
|
|
258
|
+
}
|
|
259
|
+
if (response.status >= 500) {
|
|
260
|
+
throw new InternalServerError(
|
|
261
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
throw new Error(
|
|
265
|
+
`Failed to receive messages: ${response.status} ${response.statusText}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
for await (const multipartMessage of parseMultipartStream(response)) {
|
|
269
|
+
try {
|
|
270
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
271
|
+
if (!parsedHeaders) {
|
|
272
|
+
console.warn("Missing required queue headers in multipart part");
|
|
273
|
+
await consumeStream(multipartMessage.payload);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const deserializedPayload = await transport.deserialize(
|
|
277
|
+
multipartMessage.payload
|
|
278
|
+
);
|
|
279
|
+
const message = {
|
|
280
|
+
...parsedHeaders,
|
|
281
|
+
payload: deserializedPayload
|
|
282
|
+
};
|
|
283
|
+
yield message;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.warn("Failed to process multipart message:", error);
|
|
286
|
+
await consumeStream(multipartMessage.payload);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async receiveMessageById(options, transport) {
|
|
291
|
+
const {
|
|
292
|
+
queueName,
|
|
293
|
+
consumerGroup,
|
|
294
|
+
messageId,
|
|
295
|
+
visibilityTimeoutSeconds,
|
|
296
|
+
skipPayload
|
|
297
|
+
} = options;
|
|
298
|
+
const headers = new Headers({
|
|
299
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
300
|
+
"Vqs-Queue-Name": queueName,
|
|
301
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
302
|
+
Accept: "multipart/mixed",
|
|
303
|
+
...this.customHeaders
|
|
304
|
+
});
|
|
305
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
306
|
+
headers.set(
|
|
307
|
+
"Vqs-Visibility-Timeout",
|
|
308
|
+
visibilityTimeoutSeconds.toString()
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (skipPayload) {
|
|
312
|
+
headers.set("Vqs-Skip-Payload", "1");
|
|
313
|
+
}
|
|
314
|
+
const response = await fetch(
|
|
315
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
316
|
+
{
|
|
317
|
+
method: "GET",
|
|
318
|
+
headers
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
if (response.status === 400) {
|
|
323
|
+
const errorText = await response.text();
|
|
324
|
+
throw new BadRequestError(errorText || "Invalid parameters");
|
|
325
|
+
}
|
|
326
|
+
if (response.status === 401) {
|
|
327
|
+
throw new UnauthorizedError();
|
|
328
|
+
}
|
|
329
|
+
if (response.status === 403) {
|
|
330
|
+
throw new ForbiddenError();
|
|
331
|
+
}
|
|
332
|
+
if (response.status === 404) {
|
|
333
|
+
throw new MessageNotFoundError(messageId);
|
|
334
|
+
}
|
|
335
|
+
if (response.status === 423) {
|
|
336
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
337
|
+
let retryAfter;
|
|
338
|
+
if (retryAfterHeader) {
|
|
339
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
340
|
+
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
341
|
+
}
|
|
342
|
+
throw new MessageLockedError(messageId, retryAfter);
|
|
343
|
+
}
|
|
344
|
+
if (response.status === 409) {
|
|
345
|
+
throw new MessageNotAvailableError(messageId);
|
|
346
|
+
}
|
|
347
|
+
if (response.status >= 500) {
|
|
348
|
+
throw new InternalServerError(
|
|
349
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Failed to receive message by ID: ${response.status} ${response.statusText}`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (skipPayload && response.status === 204) {
|
|
357
|
+
const parsedHeaders = parseQueueHeaders(response.headers);
|
|
358
|
+
if (!parsedHeaders) {
|
|
359
|
+
throw new MessageCorruptedError(
|
|
360
|
+
messageId,
|
|
361
|
+
"Missing required queue headers in 204 response"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const message = {
|
|
365
|
+
...parsedHeaders,
|
|
366
|
+
payload: void 0
|
|
367
|
+
};
|
|
368
|
+
return { message };
|
|
369
|
+
}
|
|
370
|
+
if (!transport) {
|
|
371
|
+
throw new Error("Transport is required when skipPayload is not true");
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
for await (const multipartMessage of parseMultipartStream(response)) {
|
|
375
|
+
try {
|
|
376
|
+
const parsedHeaders = parseQueueHeaders(multipartMessage.headers);
|
|
377
|
+
if (!parsedHeaders) {
|
|
378
|
+
console.warn("Missing required queue headers in multipart part");
|
|
379
|
+
await consumeStream(multipartMessage.payload);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const deserializedPayload = await transport.deserialize(
|
|
383
|
+
multipartMessage.payload
|
|
384
|
+
);
|
|
385
|
+
const message = {
|
|
386
|
+
...parsedHeaders,
|
|
387
|
+
payload: deserializedPayload
|
|
388
|
+
};
|
|
389
|
+
return { message };
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.warn("Failed to deserialize message by ID:", error);
|
|
392
|
+
await consumeStream(multipartMessage.payload);
|
|
393
|
+
throw new MessageCorruptedError(
|
|
394
|
+
messageId,
|
|
395
|
+
`Failed to deserialize payload: ${error}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error instanceof MessageCorruptedError) {
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
throw new MessageCorruptedError(
|
|
404
|
+
messageId,
|
|
405
|
+
`Failed to parse multipart response: ${error}`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
throw new MessageNotFoundError(messageId);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Delete a message (acknowledge processing)
|
|
412
|
+
* @param options Delete message options
|
|
413
|
+
* @returns Promise with delete status
|
|
414
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
415
|
+
* @throws {MessageNotAvailableError} When message can't be deleted (409)
|
|
416
|
+
* @throws {BadRequestError} When ticket is missing or invalid (400)
|
|
417
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
418
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
419
|
+
* @throws {InternalServerError} When server encounters an error
|
|
420
|
+
*/
|
|
421
|
+
async deleteMessage(options) {
|
|
422
|
+
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
423
|
+
const response = await fetch(
|
|
424
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
425
|
+
{
|
|
426
|
+
method: "DELETE",
|
|
427
|
+
headers: new Headers({
|
|
428
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
429
|
+
"Vqs-Queue-Name": queueName,
|
|
430
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
431
|
+
"Vqs-Ticket": ticket,
|
|
432
|
+
...this.customHeaders
|
|
433
|
+
})
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
if (!response.ok) {
|
|
437
|
+
if (response.status === 400) {
|
|
438
|
+
throw new BadRequestError("Missing or invalid ticket");
|
|
439
|
+
}
|
|
440
|
+
if (response.status === 401) {
|
|
441
|
+
throw new UnauthorizedError();
|
|
442
|
+
}
|
|
443
|
+
if (response.status === 403) {
|
|
444
|
+
throw new ForbiddenError();
|
|
445
|
+
}
|
|
446
|
+
if (response.status === 404) {
|
|
447
|
+
throw new MessageNotFoundError(messageId);
|
|
448
|
+
}
|
|
449
|
+
if (response.status === 409) {
|
|
450
|
+
throw new MessageNotAvailableError(
|
|
451
|
+
messageId,
|
|
452
|
+
"Invalid ticket, message not in correct state, or already processed"
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (response.status >= 500) {
|
|
456
|
+
throw new InternalServerError(
|
|
457
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
throw new Error(
|
|
461
|
+
`Failed to delete message: ${response.status} ${response.statusText}`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return { deleted: true };
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Change the visibility timeout of a message
|
|
468
|
+
* @param options Change visibility options
|
|
469
|
+
* @returns Promise with update status
|
|
470
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
471
|
+
* @throws {MessageNotAvailableError} When message can't be updated (409)
|
|
472
|
+
* @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
|
|
473
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
474
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
475
|
+
* @throws {InternalServerError} When server encounters an error
|
|
476
|
+
*/
|
|
477
|
+
async changeVisibility(options) {
|
|
478
|
+
const {
|
|
479
|
+
queueName,
|
|
480
|
+
consumerGroup,
|
|
481
|
+
messageId,
|
|
482
|
+
ticket,
|
|
483
|
+
visibilityTimeoutSeconds
|
|
484
|
+
} = options;
|
|
485
|
+
const response = await fetch(
|
|
486
|
+
`${this.baseUrl}${this.basePath}/${encodeURIComponent(messageId)}`,
|
|
487
|
+
{
|
|
488
|
+
method: "PATCH",
|
|
489
|
+
headers: new Headers({
|
|
490
|
+
Authorization: `Bearer ${await this.getToken()}`,
|
|
491
|
+
"Vqs-Queue-Name": queueName,
|
|
492
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
493
|
+
"Vqs-Ticket": ticket,
|
|
494
|
+
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString(),
|
|
495
|
+
...this.customHeaders
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
if (response.status === 400) {
|
|
501
|
+
throw new BadRequestError(
|
|
502
|
+
"Missing ticket or invalid visibility timeout"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (response.status === 401) {
|
|
506
|
+
throw new UnauthorizedError();
|
|
507
|
+
}
|
|
508
|
+
if (response.status === 403) {
|
|
509
|
+
throw new ForbiddenError();
|
|
510
|
+
}
|
|
511
|
+
if (response.status === 404) {
|
|
512
|
+
throw new MessageNotFoundError(messageId);
|
|
513
|
+
}
|
|
514
|
+
if (response.status === 409) {
|
|
515
|
+
throw new MessageNotAvailableError(
|
|
516
|
+
messageId,
|
|
517
|
+
"Invalid ticket, message not in correct state, or already processed"
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
if (response.status >= 500) {
|
|
521
|
+
throw new InternalServerError(
|
|
522
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
throw new Error(
|
|
526
|
+
`Failed to change visibility: ${response.status} ${response.statusText}`
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
return { updated: true };
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/transports.ts
|
|
534
|
+
async function streamToBuffer(stream) {
|
|
535
|
+
let totalLength = 0;
|
|
536
|
+
const reader = stream.getReader();
|
|
537
|
+
const chunks = [];
|
|
538
|
+
try {
|
|
539
|
+
while (true) {
|
|
540
|
+
const { done, value } = await reader.read();
|
|
541
|
+
if (done) break;
|
|
542
|
+
chunks.push(value);
|
|
543
|
+
totalLength += value.length;
|
|
544
|
+
}
|
|
545
|
+
} finally {
|
|
546
|
+
reader.releaseLock();
|
|
547
|
+
}
|
|
548
|
+
return Buffer.concat(chunks, totalLength);
|
|
549
|
+
}
|
|
550
|
+
var JsonTransport = class {
|
|
551
|
+
contentType = "application/json";
|
|
552
|
+
replacer;
|
|
553
|
+
reviver;
|
|
554
|
+
constructor(options = {}) {
|
|
555
|
+
this.replacer = options.replacer;
|
|
556
|
+
this.reviver = options.reviver;
|
|
557
|
+
}
|
|
558
|
+
serialize(value) {
|
|
559
|
+
return Buffer.from(JSON.stringify(value, this.replacer), "utf8");
|
|
560
|
+
}
|
|
561
|
+
async deserialize(stream) {
|
|
562
|
+
const buffer = await streamToBuffer(stream);
|
|
563
|
+
return JSON.parse(buffer.toString("utf8"), this.reviver);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// src/dev.ts
|
|
568
|
+
var devRouteHandlers = /* @__PURE__ */ new Map();
|
|
569
|
+
var wildcardRouteHandlers = /* @__PURE__ */ new Map();
|
|
570
|
+
function cleanupDeadRefs(key, refs) {
|
|
571
|
+
const aliveRefs = refs.filter((ref) => ref.deref() !== void 0);
|
|
572
|
+
if (aliveRefs.length === 0) {
|
|
573
|
+
wildcardRouteHandlers.delete(key);
|
|
574
|
+
} else if (aliveRefs.length < refs.length) {
|
|
575
|
+
wildcardRouteHandlers.set(key, aliveRefs);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function isDevMode() {
|
|
579
|
+
return process.env.NODE_ENV === "development";
|
|
580
|
+
}
|
|
581
|
+
function registerDevRouteHandler(routeHandler, handlers) {
|
|
582
|
+
for (const topicName in handlers) {
|
|
583
|
+
for (const consumerGroup in handlers[topicName]) {
|
|
584
|
+
const key = `${topicName}:${consumerGroup}`;
|
|
585
|
+
if (topicName.includes("*")) {
|
|
586
|
+
const existing = wildcardRouteHandlers.get(key) || [];
|
|
587
|
+
cleanupDeadRefs(key, existing);
|
|
588
|
+
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
589
|
+
const weakRef = new WeakRef(routeHandler);
|
|
590
|
+
cleanedRefs.push(weakRef);
|
|
591
|
+
wildcardRouteHandlers.set(key, cleanedRefs);
|
|
592
|
+
} else {
|
|
593
|
+
devRouteHandlers.set(key, {
|
|
594
|
+
routeHandler,
|
|
595
|
+
topicPattern: topicName
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function findRouteHandlersForTopic(topicName) {
|
|
602
|
+
const handlersMap = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const [
|
|
604
|
+
key,
|
|
605
|
+
{ routeHandler, topicPattern }
|
|
606
|
+
] of devRouteHandlers.entries()) {
|
|
607
|
+
const [_, consumerGroup] = key.split(":");
|
|
608
|
+
if (topicPattern === topicName) {
|
|
609
|
+
if (!handlersMap.has(routeHandler)) {
|
|
610
|
+
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
611
|
+
}
|
|
612
|
+
handlersMap.get(routeHandler).add(consumerGroup);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const [key, refs] of wildcardRouteHandlers.entries()) {
|
|
616
|
+
const [pattern, consumerGroup] = key.split(":");
|
|
617
|
+
if (matchesWildcardPattern(topicName, pattern)) {
|
|
618
|
+
cleanupDeadRefs(key, refs);
|
|
619
|
+
const cleanedRefs = wildcardRouteHandlers.get(key) || [];
|
|
620
|
+
for (const ref of cleanedRefs) {
|
|
621
|
+
const routeHandler = ref.deref();
|
|
622
|
+
if (routeHandler) {
|
|
623
|
+
if (!handlersMap.has(routeHandler)) {
|
|
624
|
+
handlersMap.set(routeHandler, /* @__PURE__ */ new Set());
|
|
625
|
+
}
|
|
626
|
+
handlersMap.get(routeHandler).add(consumerGroup);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return handlersMap;
|
|
632
|
+
}
|
|
633
|
+
function createMockCloudEventRequest(topicName, consumerGroup, messageId) {
|
|
634
|
+
const cloudEvent = {
|
|
635
|
+
type: "com.vercel.queue.v1beta",
|
|
636
|
+
source: `/topic/${topicName}/consumer/${consumerGroup}`,
|
|
637
|
+
id: messageId,
|
|
638
|
+
datacontenttype: "application/json",
|
|
639
|
+
data: {
|
|
640
|
+
messageId,
|
|
641
|
+
queueName: topicName,
|
|
642
|
+
consumerGroup
|
|
643
|
+
},
|
|
644
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
645
|
+
specversion: "1.0"
|
|
646
|
+
};
|
|
647
|
+
return new Request("https://localhost/api/queue/callback", {
|
|
648
|
+
method: "POST",
|
|
649
|
+
headers: {
|
|
650
|
+
"Content-Type": "application/cloudevents+json"
|
|
651
|
+
},
|
|
652
|
+
body: JSON.stringify(cloudEvent)
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
var DEV_CALLBACK_DELAY = 1e3;
|
|
656
|
+
function scheduleDevTimeout(topicName, messageId, timeoutSeconds) {
|
|
657
|
+
console.log(
|
|
658
|
+
`[Dev Mode] Message ${messageId} timed out for ${timeoutSeconds}s, will re-trigger`
|
|
659
|
+
);
|
|
660
|
+
setTimeout(
|
|
661
|
+
() => {
|
|
662
|
+
console.log(
|
|
663
|
+
`[Dev Mode] Re-triggering callback for timed-out message ${messageId}`
|
|
664
|
+
);
|
|
665
|
+
triggerDevCallbacks(topicName, messageId);
|
|
666
|
+
},
|
|
667
|
+
timeoutSeconds * 1e3 + DEV_CALLBACK_DELAY
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
function triggerDevCallbacks(topicName, messageId) {
|
|
671
|
+
const handlersMap = findRouteHandlersForTopic(topicName);
|
|
672
|
+
if (handlersMap.size === 0) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const consumerGroups = Array.from(
|
|
676
|
+
new Set(
|
|
677
|
+
Array.from(handlersMap.values()).flatMap((groups) => Array.from(groups))
|
|
678
|
+
)
|
|
679
|
+
);
|
|
680
|
+
console.log(
|
|
681
|
+
`[Dev Mode] Triggering local callbacks for topic "${topicName}" \u2192 consumers: ${consumerGroups.join(", ")}`
|
|
682
|
+
);
|
|
683
|
+
setTimeout(async () => {
|
|
684
|
+
for (const [routeHandler, consumerGroups2] of handlersMap.entries()) {
|
|
685
|
+
for (const consumerGroup of consumerGroups2) {
|
|
686
|
+
try {
|
|
687
|
+
const request = createMockCloudEventRequest(
|
|
688
|
+
topicName,
|
|
689
|
+
consumerGroup,
|
|
690
|
+
messageId
|
|
691
|
+
);
|
|
692
|
+
const response = await routeHandler(request);
|
|
693
|
+
if (response.ok) {
|
|
694
|
+
try {
|
|
695
|
+
const responseData = await response.json();
|
|
696
|
+
if (responseData.status === "success") {
|
|
697
|
+
console.log(
|
|
698
|
+
`[Dev Mode] Message processed for ${topicName}/${consumerGroup}`
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
} catch (jsonError) {
|
|
702
|
+
console.error(
|
|
703
|
+
`[Dev Mode] Failed to parse success response for ${topicName}/${consumerGroup}:`,
|
|
704
|
+
jsonError
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
try {
|
|
709
|
+
const errorData = await response.json();
|
|
710
|
+
console.error(
|
|
711
|
+
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
712
|
+
errorData.error || response.statusText
|
|
713
|
+
);
|
|
714
|
+
} catch (jsonError) {
|
|
715
|
+
console.error(
|
|
716
|
+
`[Dev Mode] Failed to process message for ${topicName}/${consumerGroup}:`,
|
|
717
|
+
response.statusText
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.error(
|
|
723
|
+
`[Dev Mode] Error triggering callback for ${topicName}/${consumerGroup}:`,
|
|
724
|
+
error
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}, DEV_CALLBACK_DELAY);
|
|
730
|
+
}
|
|
731
|
+
function clearDevHandlers() {
|
|
732
|
+
devRouteHandlers.clear();
|
|
733
|
+
wildcardRouteHandlers.clear();
|
|
734
|
+
}
|
|
735
|
+
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
736
|
+
globalThis.__clearDevHandlers = clearDevHandlers;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/consumer-group.ts
|
|
740
|
+
var ConsumerGroup = class {
|
|
741
|
+
client;
|
|
742
|
+
topicName;
|
|
743
|
+
consumerGroupName;
|
|
744
|
+
visibilityTimeout;
|
|
745
|
+
refreshInterval;
|
|
746
|
+
transport;
|
|
747
|
+
/**
|
|
748
|
+
* Create a new ConsumerGroup instance
|
|
749
|
+
* @param client QueueClient instance to use for API calls
|
|
750
|
+
* @param topicName Name of the topic to consume from
|
|
751
|
+
* @param consumerGroupName Name of the consumer group
|
|
752
|
+
* @param options Optional configuration
|
|
753
|
+
*/
|
|
754
|
+
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
755
|
+
this.client = client;
|
|
756
|
+
this.topicName = topicName;
|
|
757
|
+
this.consumerGroupName = consumerGroupName;
|
|
758
|
+
this.visibilityTimeout = options.visibilityTimeoutSeconds || 30;
|
|
759
|
+
this.refreshInterval = options.refreshInterval || 10;
|
|
760
|
+
this.transport = options.transport || new JsonTransport();
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Starts a background loop that periodically extends the visibility timeout for a message.
|
|
764
|
+
* This prevents the message from becoming visible to other consumers while it's being processed.
|
|
765
|
+
*
|
|
766
|
+
* The extension loop runs every `refreshInterval` seconds and updates the message's
|
|
767
|
+
* visibility timeout to `visibilityTimeout` seconds from the current time.
|
|
768
|
+
*
|
|
769
|
+
* @param messageId - The unique identifier of the message to extend visibility for
|
|
770
|
+
* @param ticket - The receipt ticket that proves ownership of the message
|
|
771
|
+
* @returns A function that when called will stop the extension loop
|
|
772
|
+
*
|
|
773
|
+
* @remarks
|
|
774
|
+
* - The first extension attempt occurs after `refreshInterval` seconds, not immediately
|
|
775
|
+
* - If an extension fails, the loop terminates with an error logged to console
|
|
776
|
+
* - The returned stop function is idempotent - calling it multiple times is safe
|
|
777
|
+
* - By default, the stop function returns immediately without waiting for in-flight
|
|
778
|
+
* - Pass `true` to the stop function to wait for any in-flight extension to complete
|
|
779
|
+
*/
|
|
780
|
+
startVisibilityExtension(messageId, ticket) {
|
|
781
|
+
let isRunning = true;
|
|
782
|
+
let resolveLifecycle;
|
|
783
|
+
let timeoutId = null;
|
|
784
|
+
const lifecyclePromise = new Promise((resolve) => {
|
|
785
|
+
resolveLifecycle = resolve;
|
|
786
|
+
});
|
|
787
|
+
const extend = async () => {
|
|
788
|
+
if (!isRunning) {
|
|
789
|
+
resolveLifecycle();
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
await this.client.changeVisibility({
|
|
794
|
+
queueName: this.topicName,
|
|
795
|
+
consumerGroup: this.consumerGroupName,
|
|
796
|
+
messageId,
|
|
797
|
+
ticket,
|
|
798
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
799
|
+
});
|
|
800
|
+
if (isRunning) {
|
|
801
|
+
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
802
|
+
} else {
|
|
803
|
+
resolveLifecycle();
|
|
804
|
+
}
|
|
805
|
+
} catch (error) {
|
|
806
|
+
console.error(
|
|
807
|
+
`Failed to extend visibility for message ${messageId}:`,
|
|
808
|
+
error
|
|
809
|
+
);
|
|
810
|
+
resolveLifecycle();
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
814
|
+
return async (waitForCompletion = false) => {
|
|
815
|
+
isRunning = false;
|
|
816
|
+
if (timeoutId) {
|
|
817
|
+
clearTimeout(timeoutId);
|
|
818
|
+
timeoutId = null;
|
|
819
|
+
}
|
|
820
|
+
if (waitForCompletion) {
|
|
821
|
+
await lifecyclePromise;
|
|
822
|
+
} else {
|
|
823
|
+
resolveLifecycle();
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Process a single message with the given handler
|
|
829
|
+
* @param message The message to process
|
|
830
|
+
* @param handler Function to process the message
|
|
831
|
+
*/
|
|
832
|
+
async processMessage(message, handler) {
|
|
833
|
+
const stopExtension = this.startVisibilityExtension(
|
|
834
|
+
message.messageId,
|
|
835
|
+
message.ticket
|
|
836
|
+
);
|
|
837
|
+
try {
|
|
838
|
+
const result = await handler(message.payload, {
|
|
839
|
+
messageId: message.messageId,
|
|
840
|
+
deliveryCount: message.deliveryCount,
|
|
841
|
+
createdAt: message.createdAt,
|
|
842
|
+
topicName: this.topicName,
|
|
843
|
+
consumerGroup: this.consumerGroupName
|
|
844
|
+
});
|
|
845
|
+
await stopExtension();
|
|
846
|
+
if (result && "timeoutSeconds" in result) {
|
|
847
|
+
await this.client.changeVisibility({
|
|
848
|
+
queueName: this.topicName,
|
|
849
|
+
consumerGroup: this.consumerGroupName,
|
|
850
|
+
messageId: message.messageId,
|
|
851
|
+
ticket: message.ticket,
|
|
852
|
+
visibilityTimeoutSeconds: result.timeoutSeconds
|
|
853
|
+
});
|
|
854
|
+
if (isDevMode()) {
|
|
855
|
+
scheduleDevTimeout(
|
|
856
|
+
this.topicName,
|
|
857
|
+
message.messageId,
|
|
858
|
+
result.timeoutSeconds
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
} else {
|
|
862
|
+
await this.client.deleteMessage({
|
|
863
|
+
queueName: this.topicName,
|
|
864
|
+
consumerGroup: this.consumerGroupName,
|
|
865
|
+
messageId: message.messageId,
|
|
866
|
+
ticket: message.ticket
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
await stopExtension();
|
|
871
|
+
if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
872
|
+
try {
|
|
873
|
+
await this.transport.finalize(message.payload);
|
|
874
|
+
} catch (finalizeError) {
|
|
875
|
+
console.warn("Failed to finalize message payload:", finalizeError);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async consume(handler, options) {
|
|
882
|
+
if (options?.messageId) {
|
|
883
|
+
if (options.skipPayload) {
|
|
884
|
+
const response = await this.client.receiveMessageById(
|
|
885
|
+
{
|
|
886
|
+
queueName: this.topicName,
|
|
887
|
+
consumerGroup: this.consumerGroupName,
|
|
888
|
+
messageId: options.messageId,
|
|
889
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
890
|
+
skipPayload: true
|
|
891
|
+
},
|
|
892
|
+
this.transport
|
|
893
|
+
);
|
|
894
|
+
await this.processMessage(
|
|
895
|
+
response.message,
|
|
896
|
+
handler
|
|
897
|
+
);
|
|
898
|
+
} else {
|
|
899
|
+
const response = await this.client.receiveMessageById(
|
|
900
|
+
{
|
|
901
|
+
queueName: this.topicName,
|
|
902
|
+
consumerGroup: this.consumerGroupName,
|
|
903
|
+
messageId: options.messageId,
|
|
904
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
905
|
+
},
|
|
906
|
+
this.transport
|
|
907
|
+
);
|
|
908
|
+
await this.processMessage(
|
|
909
|
+
response.message,
|
|
910
|
+
handler
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
let messageFound = false;
|
|
915
|
+
for await (const message of this.client.receiveMessages(
|
|
916
|
+
{
|
|
917
|
+
queueName: this.topicName,
|
|
918
|
+
consumerGroup: this.consumerGroupName,
|
|
919
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
920
|
+
limit: 1
|
|
921
|
+
},
|
|
922
|
+
this.transport
|
|
923
|
+
)) {
|
|
924
|
+
messageFound = true;
|
|
925
|
+
await this.processMessage(message, handler);
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
if (!messageFound) {
|
|
929
|
+
throw new Error("No messages available");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Get the consumer group name
|
|
935
|
+
*/
|
|
936
|
+
get name() {
|
|
937
|
+
return this.consumerGroupName;
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get the topic name this consumer group is subscribed to
|
|
941
|
+
*/
|
|
942
|
+
get topic() {
|
|
943
|
+
return this.topicName;
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// src/topic.ts
|
|
948
|
+
var Topic = class {
|
|
949
|
+
client;
|
|
950
|
+
topicName;
|
|
951
|
+
transport;
|
|
952
|
+
/**
|
|
953
|
+
* Create a new Topic instance
|
|
954
|
+
* @param client QueueClient instance to use for API calls
|
|
955
|
+
* @param topicName Name of the topic to work with
|
|
956
|
+
* @param transport Optional serializer/deserializer for the payload (defaults to JSON)
|
|
957
|
+
*/
|
|
958
|
+
constructor(client, topicName, transport) {
|
|
959
|
+
this.client = client;
|
|
960
|
+
this.topicName = topicName;
|
|
961
|
+
this.transport = transport || new JsonTransport();
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Publish a message to the topic
|
|
965
|
+
* @param payload The data to publish
|
|
966
|
+
* @param options Optional publish options
|
|
967
|
+
* @returns An object containing the message ID
|
|
968
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
969
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
970
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
971
|
+
* @throws {InternalServerError} When server encounters an error
|
|
972
|
+
*/
|
|
973
|
+
async publish(payload, options) {
|
|
974
|
+
const result = await this.client.sendMessage(
|
|
975
|
+
{
|
|
976
|
+
queueName: this.topicName,
|
|
977
|
+
payload,
|
|
978
|
+
idempotencyKey: options?.idempotencyKey,
|
|
979
|
+
retentionSeconds: options?.retentionSeconds,
|
|
980
|
+
deploymentId: options?.deploymentId
|
|
981
|
+
},
|
|
982
|
+
this.transport
|
|
983
|
+
);
|
|
984
|
+
if (isDevMode()) {
|
|
985
|
+
triggerDevCallbacks(this.topicName, result.messageId);
|
|
986
|
+
}
|
|
987
|
+
return { messageId: result.messageId };
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Create a consumer group for this topic
|
|
991
|
+
* @param consumerGroupName Name of the consumer group
|
|
992
|
+
* @param options Optional configuration for the consumer group
|
|
993
|
+
* @returns A ConsumerGroup instance
|
|
994
|
+
*/
|
|
995
|
+
consumerGroup(consumerGroupName, options) {
|
|
996
|
+
const consumerOptions = {
|
|
997
|
+
...options,
|
|
998
|
+
transport: options?.transport || this.transport
|
|
999
|
+
};
|
|
1000
|
+
return new ConsumerGroup(
|
|
1001
|
+
this.client,
|
|
1002
|
+
this.topicName,
|
|
1003
|
+
consumerGroupName,
|
|
1004
|
+
consumerOptions
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Get the topic name
|
|
1009
|
+
*/
|
|
1010
|
+
get name() {
|
|
1011
|
+
return this.topicName;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Get the transport used by this topic
|
|
1015
|
+
*/
|
|
1016
|
+
get serializer() {
|
|
1017
|
+
return this.transport;
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
// src/callback.ts
|
|
1022
|
+
function validateWildcardPattern(pattern) {
|
|
1023
|
+
const firstIndex = pattern.indexOf("*");
|
|
1024
|
+
const lastIndex = pattern.lastIndexOf("*");
|
|
1025
|
+
if (firstIndex !== lastIndex) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
if (firstIndex === -1) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
if (firstIndex !== pattern.length - 1) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
function matchesWildcardPattern(topicName, pattern) {
|
|
1037
|
+
const prefix = pattern.slice(0, -1);
|
|
1038
|
+
return topicName.startsWith(prefix);
|
|
1039
|
+
}
|
|
1040
|
+
function findTopicHandler(queueName, handlers) {
|
|
1041
|
+
const exactHandler = handlers[queueName];
|
|
1042
|
+
if (exactHandler) {
|
|
1043
|
+
return exactHandler;
|
|
1044
|
+
}
|
|
1045
|
+
for (const pattern in handlers) {
|
|
1046
|
+
if (pattern.includes("*") && matchesWildcardPattern(queueName, pattern)) {
|
|
1047
|
+
return handlers[pattern];
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
async function parseCallback(request) {
|
|
1053
|
+
const contentType = request.headers.get("content-type");
|
|
1054
|
+
if (!contentType || !contentType.includes("application/cloudevents+json")) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
"Invalid content type: expected 'application/cloudevents+json'"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
let cloudEvent;
|
|
1060
|
+
try {
|
|
1061
|
+
cloudEvent = await request.json();
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
throw new Error("Failed to parse CloudEvent from request body");
|
|
1064
|
+
}
|
|
1065
|
+
if (!cloudEvent.type || !cloudEvent.source || !cloudEvent.id || typeof cloudEvent.data !== "object" || cloudEvent.data == null) {
|
|
1066
|
+
throw new Error("Invalid CloudEvent: missing required fields");
|
|
1067
|
+
}
|
|
1068
|
+
if (cloudEvent.type !== "com.vercel.queue.v1beta") {
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
`Invalid CloudEvent type: expected 'com.vercel.queue.v1beta', got '${cloudEvent.type}'`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
const missingFields = [];
|
|
1074
|
+
if (!("queueName" in cloudEvent.data)) missingFields.push("queueName");
|
|
1075
|
+
if (!("consumerGroup" in cloudEvent.data))
|
|
1076
|
+
missingFields.push("consumerGroup");
|
|
1077
|
+
if (!("messageId" in cloudEvent.data)) missingFields.push("messageId");
|
|
1078
|
+
if (missingFields.length > 0) {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
`Missing required CloudEvent data fields: ${missingFields.join(", ")}`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
const { messageId, queueName, consumerGroup } = cloudEvent.data;
|
|
1084
|
+
return {
|
|
1085
|
+
queueName,
|
|
1086
|
+
consumerGroup,
|
|
1087
|
+
messageId
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function handleCallback(handlers) {
|
|
1091
|
+
for (const topicPattern in handlers) {
|
|
1092
|
+
if (topicPattern.includes("*")) {
|
|
1093
|
+
if (!validateWildcardPattern(topicPattern)) {
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
`Invalid wildcard pattern "${topicPattern}": * may only appear once and must be at the end of the topic name`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const routeHandler = async (request) => {
|
|
1101
|
+
try {
|
|
1102
|
+
const { queueName, consumerGroup, messageId } = await parseCallback(request);
|
|
1103
|
+
const topicHandler = findTopicHandler(queueName, handlers);
|
|
1104
|
+
if (!topicHandler) {
|
|
1105
|
+
const availableTopics = Object.keys(handlers).join(", ");
|
|
1106
|
+
return Response.json(
|
|
1107
|
+
{
|
|
1108
|
+
error: `No handler found for topic: ${queueName}`,
|
|
1109
|
+
availableTopics
|
|
1110
|
+
},
|
|
1111
|
+
{ status: 404 }
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
const consumerGroupHandler = topicHandler[consumerGroup];
|
|
1115
|
+
if (!consumerGroupHandler) {
|
|
1116
|
+
const availableGroups = Object.keys(topicHandler).join(", ");
|
|
1117
|
+
return Response.json(
|
|
1118
|
+
{
|
|
1119
|
+
error: `No handler found for consumer group "${consumerGroup}" in topic "${queueName}".`,
|
|
1120
|
+
availableGroups
|
|
1121
|
+
},
|
|
1122
|
+
{ status: 404 }
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
const client = new QueueClient();
|
|
1126
|
+
const topic = new Topic(client, queueName);
|
|
1127
|
+
const cg = topic.consumerGroup(consumerGroup);
|
|
1128
|
+
await cg.consume(consumerGroupHandler, { messageId });
|
|
1129
|
+
return Response.json({ status: "success" });
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
console.error("Queue callback error:", error);
|
|
1132
|
+
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"))) {
|
|
1133
|
+
return Response.json({ error: error.message }, { status: 400 });
|
|
1134
|
+
}
|
|
1135
|
+
return Response.json(
|
|
1136
|
+
{ error: "Failed to process queue message" },
|
|
1137
|
+
{ status: 500 }
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
if (isDevMode()) {
|
|
1142
|
+
registerDevRouteHandler(routeHandler, handlers);
|
|
1143
|
+
}
|
|
1144
|
+
return routeHandler;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/pages.ts
|
|
1148
|
+
function getHeader(headers, name) {
|
|
1149
|
+
const value = headers[name];
|
|
1150
|
+
return Array.isArray(value) ? value[0] : value;
|
|
1151
|
+
}
|
|
1152
|
+
function readBody(req) {
|
|
1153
|
+
return new Promise((resolve, reject) => {
|
|
1154
|
+
const chunks = [];
|
|
1155
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
1156
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
1157
|
+
req.on("error", reject);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
function getBody(req) {
|
|
1161
|
+
if (req.body === void 0) {
|
|
1162
|
+
return readBody(req);
|
|
1163
|
+
}
|
|
1164
|
+
if (typeof req.body === "string") {
|
|
1165
|
+
return req.body;
|
|
1166
|
+
}
|
|
1167
|
+
return JSON.stringify(req.body);
|
|
1168
|
+
}
|
|
1169
|
+
async function createRequestFromNextApi(req) {
|
|
1170
|
+
const protocol = getHeader(req.headers, "x-forwarded-proto") ?? "https";
|
|
1171
|
+
const host = getHeader(req.headers, "host");
|
|
1172
|
+
if (!host) {
|
|
1173
|
+
throw new Error("Missing host header");
|
|
1174
|
+
}
|
|
1175
|
+
const url = `${protocol}://${host}${req.url}`;
|
|
1176
|
+
const headers = new Headers();
|
|
1177
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1178
|
+
if (value) {
|
|
1179
|
+
if (Array.isArray(value)) {
|
|
1180
|
+
value.forEach((v) => headers.append(key, v));
|
|
1181
|
+
} else {
|
|
1182
|
+
headers.set(key, value);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const body = await getBody(req);
|
|
1187
|
+
return new Request(url, {
|
|
1188
|
+
method: req.method || "POST",
|
|
1189
|
+
headers,
|
|
1190
|
+
body
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
async function sendResponseToNextApi(response, res) {
|
|
1194
|
+
res.status(response.status);
|
|
1195
|
+
response.headers.forEach((value, key) => {
|
|
1196
|
+
res.setHeader(key, value);
|
|
1197
|
+
});
|
|
1198
|
+
const contentType = response.headers.get("content-type");
|
|
1199
|
+
if (contentType?.includes("application/json")) {
|
|
1200
|
+
const data = await response.json();
|
|
1201
|
+
res.json(data);
|
|
1202
|
+
} else {
|
|
1203
|
+
const text = await response.text();
|
|
1204
|
+
res.send(text);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function handleCallback2(handlers) {
|
|
1208
|
+
const webHandler = handleCallback(handlers);
|
|
1209
|
+
return async (req, res) => {
|
|
1210
|
+
try {
|
|
1211
|
+
const request = await createRequestFromNextApi(req);
|
|
1212
|
+
const response = await webHandler(request);
|
|
1213
|
+
await sendResponseToNextApi(response, res);
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
console.error("Pages Router adapter error:", error);
|
|
1216
|
+
res.status(500).json({ error: "Internal server error" });
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
export {
|
|
1221
|
+
handleCallback2 as handleCallback
|
|
1222
|
+
};
|
|
1223
|
+
//# sourceMappingURL=pages.mjs.map
|