@vercel/queue 0.0.0-alpha.1
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 +670 -0
- package/dist/index.d.mts +725 -0
- package/dist/index.d.ts +725 -0
- package/dist/index.js +1326 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1278 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1326 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BadRequestError: () => BadRequestError,
|
|
24
|
+
BufferTransport: () => BufferTransport,
|
|
25
|
+
ConsumerGroup: () => ConsumerGroup,
|
|
26
|
+
FailedDependencyError: () => FailedDependencyError,
|
|
27
|
+
FifoOrderingViolationError: () => FifoOrderingViolationError,
|
|
28
|
+
ForbiddenError: () => ForbiddenError,
|
|
29
|
+
InternalServerError: () => InternalServerError,
|
|
30
|
+
InvalidCallbackError: () => InvalidCallbackError,
|
|
31
|
+
InvalidLimitError: () => InvalidLimitError,
|
|
32
|
+
JsonTransport: () => JsonTransport,
|
|
33
|
+
MessageCorruptedError: () => MessageCorruptedError,
|
|
34
|
+
MessageLockedError: () => MessageLockedError,
|
|
35
|
+
MessageNotAvailableError: () => MessageNotAvailableError,
|
|
36
|
+
MessageNotFoundError: () => MessageNotFoundError,
|
|
37
|
+
QueueEmptyError: () => QueueEmptyError,
|
|
38
|
+
StreamTransport: () => StreamTransport,
|
|
39
|
+
Topic: () => Topic,
|
|
40
|
+
UnauthorizedError: () => UnauthorizedError,
|
|
41
|
+
VQSClient: () => VQSClient,
|
|
42
|
+
createTopic: () => createTopic,
|
|
43
|
+
getVercelOidcToken: () => getVercelOidcToken,
|
|
44
|
+
parseCallbackRequest: () => parseCallbackRequest
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(index_exports);
|
|
47
|
+
|
|
48
|
+
// src/oidc.ts
|
|
49
|
+
async function getVercelOidcToken() {
|
|
50
|
+
const SYMBOL_FOR_REQ_CONTEXT = Symbol.for("@vercel/request-context");
|
|
51
|
+
const fromSymbol = globalThis;
|
|
52
|
+
const context = fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {};
|
|
53
|
+
const token = context.headers?.["x-vercel-oidc-token"] ?? process.env.VERCEL_OIDC_TOKEN;
|
|
54
|
+
if (!token) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`The 'x-vercel-oidc-token' header is missing from the request. Do you have the OIDC option enabled in the Vercel project settings?`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return token;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/client.ts
|
|
63
|
+
var import_mixpart = require("mixpart");
|
|
64
|
+
|
|
65
|
+
// src/local.ts
|
|
66
|
+
var import_node_child_process = require("child_process");
|
|
67
|
+
function isLocalhostWithPort(url) {
|
|
68
|
+
try {
|
|
69
|
+
const parsedUrl = new URL(url);
|
|
70
|
+
const isLocalhost = parsedUrl.hostname === "localhost";
|
|
71
|
+
const port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 0;
|
|
72
|
+
return { isLocalhost, port };
|
|
73
|
+
} catch {
|
|
74
|
+
return { isLocalhost: false };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function isSupportedPlatform() {
|
|
78
|
+
const platform = process.platform;
|
|
79
|
+
return platform === "darwin" || platform === "linux";
|
|
80
|
+
}
|
|
81
|
+
function processDevelopmentCallbacks(callbacks) {
|
|
82
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
83
|
+
if (!isDevelopment) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
if (!isSupportedPlatform()) {
|
|
87
|
+
const hasLocalhostCallbacks = Object.values(callbacks).some((config) => {
|
|
88
|
+
const { isLocalhost } = isLocalhostWithPort(config.url);
|
|
89
|
+
return isLocalhost;
|
|
90
|
+
});
|
|
91
|
+
if (hasLocalhostCallbacks) {
|
|
92
|
+
console.warn(
|
|
93
|
+
`VQS 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.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const localhostCallbacks = [];
|
|
99
|
+
Object.entries(callbacks).forEach(([group, config]) => {
|
|
100
|
+
const { isLocalhost, port } = isLocalhostWithPort(config.url);
|
|
101
|
+
if (isLocalhost && port && port > 0) {
|
|
102
|
+
localhostCallbacks.push({ group, config, port });
|
|
103
|
+
} else {
|
|
104
|
+
console.warn(
|
|
105
|
+
`VQS Development Mode: Skipping non-localhost callback for group "${group}": ${config.url}. Only localhost callbacks with explicit ports are supported in development.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return localhostCallbacks;
|
|
110
|
+
}
|
|
111
|
+
function fireLocalhostCallbacks(localhostCallbacks, queueName, responseData) {
|
|
112
|
+
localhostCallbacks.forEach(({ group, config, port }) => {
|
|
113
|
+
const callbackHeaders = new Headers();
|
|
114
|
+
callbackHeaders.set("Vqs-Message-Id", responseData.messageId);
|
|
115
|
+
callbackHeaders.set("Vqs-Queue-Name", queueName);
|
|
116
|
+
callbackHeaders.set("Vqs-Consumer-Group", group);
|
|
117
|
+
fireAndForgetWaitForHttpReady(
|
|
118
|
+
config.url,
|
|
119
|
+
port,
|
|
120
|
+
config.delay || 0,
|
|
121
|
+
3,
|
|
122
|
+
// Default retry frequency
|
|
123
|
+
callbackHeaders
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function fireAndForgetWaitForHttpReady(url, port, initialDelaySeconds = 0, retryFrequencySeconds = 3, headers) {
|
|
128
|
+
if (!isSupportedPlatform()) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`VQS: fireAndForgetWaitForHttpReady is not supported on ${process.platform}. This function requires bash, nc, and curl which are available on macOS and Linux only.`
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
let headerArgs = "";
|
|
135
|
+
if (headers) {
|
|
136
|
+
const headerArray = [];
|
|
137
|
+
headers.forEach((value, key) => {
|
|
138
|
+
headerArray.push(`-H '${key}: ${value}'`);
|
|
139
|
+
});
|
|
140
|
+
headerArgs = headerArray.join(" ");
|
|
141
|
+
}
|
|
142
|
+
const bashScript = `
|
|
143
|
+
# Wait for any initial boot time
|
|
144
|
+
sleep ${initialDelaySeconds}
|
|
145
|
+
|
|
146
|
+
missed=0
|
|
147
|
+
while true; do
|
|
148
|
+
# 1) Check if TCP port is listening
|
|
149
|
+
if nc -z localhost ${port} 2>/dev/null; then
|
|
150
|
+
missed=0
|
|
151
|
+
# 2) If port is open, try HTTP POST check
|
|
152
|
+
if curl -sSL --fail -o /dev/null -X POST ${headerArgs} "${url}"; then
|
|
153
|
+
# Success: port is up AND HTTP returned 2xx (following redirects)
|
|
154
|
+
exit 0
|
|
155
|
+
fi
|
|
156
|
+
else
|
|
157
|
+
# Port was closed\u2014increment miss counter
|
|
158
|
+
((missed+=1))
|
|
159
|
+
# If closed twice in a row, give up immediately
|
|
160
|
+
if [ "$missed" -ge 2 ]; then
|
|
161
|
+
exit 1
|
|
162
|
+
fi
|
|
163
|
+
fi
|
|
164
|
+
# Wait before next cycle
|
|
165
|
+
sleep ${retryFrequencySeconds}
|
|
166
|
+
done
|
|
167
|
+
`;
|
|
168
|
+
const childProcess = (0, import_node_child_process.spawn)("bash", ["-c", bashScript], {
|
|
169
|
+
stdio: "ignore",
|
|
170
|
+
detached: true
|
|
171
|
+
});
|
|
172
|
+
childProcess.unref();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/types.ts
|
|
176
|
+
var MessageNotFoundError = class extends Error {
|
|
177
|
+
constructor(messageId) {
|
|
178
|
+
super(`Message ${messageId} not found`);
|
|
179
|
+
this.name = "MessageNotFoundError";
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var MessageNotAvailableError = class extends Error {
|
|
183
|
+
constructor(messageId, reason) {
|
|
184
|
+
super(
|
|
185
|
+
`Message ${messageId} not available for processing${reason ? `: ${reason}` : ""}`
|
|
186
|
+
);
|
|
187
|
+
this.name = "MessageNotAvailableError";
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var FifoOrderingViolationError = class extends Error {
|
|
191
|
+
nextMessageId;
|
|
192
|
+
constructor(messageId, nextMessageId, reason) {
|
|
193
|
+
super(
|
|
194
|
+
`FIFO ordering violation for message ${messageId}: ${reason}. Process message ${nextMessageId} first.`
|
|
195
|
+
);
|
|
196
|
+
this.name = "FifoOrderingViolationError";
|
|
197
|
+
this.nextMessageId = nextMessageId;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var MessageCorruptedError = class extends Error {
|
|
201
|
+
constructor(messageId, reason) {
|
|
202
|
+
super(`Message ${messageId} is corrupted: ${reason}`);
|
|
203
|
+
this.name = "MessageCorruptedError";
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var QueueEmptyError = class extends Error {
|
|
207
|
+
constructor(queueName, consumerGroup) {
|
|
208
|
+
super(
|
|
209
|
+
`No messages available in queue "${queueName}" for consumer group "${consumerGroup}"`
|
|
210
|
+
);
|
|
211
|
+
this.name = "QueueEmptyError";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var MessageLockedError = class extends Error {
|
|
215
|
+
retryAfter;
|
|
216
|
+
constructor(messageId, retryAfter) {
|
|
217
|
+
const retryMessage = retryAfter ? ` Retry after ${retryAfter} seconds.` : " Try again later.";
|
|
218
|
+
super(`Message ${messageId} is temporarily locked.${retryMessage}`);
|
|
219
|
+
this.name = "MessageLockedError";
|
|
220
|
+
this.retryAfter = retryAfter;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var UnauthorizedError = class extends Error {
|
|
224
|
+
constructor(message = "Missing or invalid authentication token") {
|
|
225
|
+
super(message);
|
|
226
|
+
this.name = "UnauthorizedError";
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var ForbiddenError = class extends Error {
|
|
230
|
+
constructor(message = "Queue environment doesn't match token environment") {
|
|
231
|
+
super(message);
|
|
232
|
+
this.name = "ForbiddenError";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
var BadRequestError = class extends Error {
|
|
236
|
+
constructor(message) {
|
|
237
|
+
super(message);
|
|
238
|
+
this.name = "BadRequestError";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var FailedDependencyError = class extends Error {
|
|
242
|
+
nextMessageId;
|
|
243
|
+
constructor(messageId, nextMessageId) {
|
|
244
|
+
super(
|
|
245
|
+
`Failed dependency: FIFO ordering violation for message ${messageId}. Must process message ${nextMessageId} first.`
|
|
246
|
+
);
|
|
247
|
+
this.name = "FailedDependencyError";
|
|
248
|
+
this.nextMessageId = nextMessageId;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
var InternalServerError = class extends Error {
|
|
252
|
+
constructor(message = "Unexpected server error") {
|
|
253
|
+
super(message);
|
|
254
|
+
this.name = "InternalServerError";
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
var InvalidLimitError = class extends Error {
|
|
258
|
+
constructor(limit, min = 1, max = 10) {
|
|
259
|
+
super(`Invalid limit: ${limit}. Limit must be between ${min} and ${max}.`);
|
|
260
|
+
this.name = "InvalidLimitError";
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var InvalidCallbackError = class extends Error {
|
|
264
|
+
constructor(message) {
|
|
265
|
+
super(message);
|
|
266
|
+
this.name = "InvalidCallbackError";
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/client.ts
|
|
271
|
+
async function consumeStream(stream) {
|
|
272
|
+
const reader = stream.getReader();
|
|
273
|
+
try {
|
|
274
|
+
while (true) {
|
|
275
|
+
const { done } = await reader.read();
|
|
276
|
+
if (done) break;
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
reader.releaseLock();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function parseVQSHeaders(headers) {
|
|
283
|
+
const messageId = headers.get("Vqs-Message-Id");
|
|
284
|
+
const deliveryCountStr = headers.get("Vqs-Delivery-Count") || "0";
|
|
285
|
+
const timestamp = headers.get("Vqs-Timestamp");
|
|
286
|
+
const contentType = headers.get("Content-Type") || "application/octet-stream";
|
|
287
|
+
const ticket = headers.get("Vqs-Ticket");
|
|
288
|
+
if (!messageId || !timestamp || !ticket) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const deliveryCount = parseInt(deliveryCountStr, 10);
|
|
292
|
+
if (isNaN(deliveryCount)) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
messageId,
|
|
297
|
+
deliveryCount,
|
|
298
|
+
timestamp,
|
|
299
|
+
contentType,
|
|
300
|
+
ticket
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var VQSClient = class _VQSClient {
|
|
304
|
+
baseUrl;
|
|
305
|
+
token;
|
|
306
|
+
/**
|
|
307
|
+
* Create a new Vercel Queue Service client
|
|
308
|
+
* @param options Client configuration options
|
|
309
|
+
*/
|
|
310
|
+
constructor(options) {
|
|
311
|
+
this.baseUrl = options.baseUrl || "https://vqs.vercel.sh";
|
|
312
|
+
this.token = options.token;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Create a VQSClient automatically configured for Vercel Functions
|
|
316
|
+
* This method automatically retrieves the OIDC token from the Vercel Function environment
|
|
317
|
+
* Always creates a fresh instance since OIDC tokens expire after 15 minutes
|
|
318
|
+
* @param baseUrl Optional base URL override
|
|
319
|
+
* @returns Promise resolving to a new VQSClient instance
|
|
320
|
+
*/
|
|
321
|
+
static async fromVercelFunction(baseUrl) {
|
|
322
|
+
const token = await getVercelOidcToken();
|
|
323
|
+
if (!token) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
"Failed to get OIDC token from Vercel Functions. Make sure you are running in a Vercel Function environment."
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return new _VQSClient({
|
|
329
|
+
token,
|
|
330
|
+
baseUrl
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Send a message to a queue
|
|
335
|
+
* @param options Send message options
|
|
336
|
+
* @param transport Serializer/deserializer for the payload
|
|
337
|
+
* @returns Promise with the message ID
|
|
338
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
339
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
340
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
341
|
+
* @throws {InternalServerError} When server encounters an error
|
|
342
|
+
*/
|
|
343
|
+
async sendMessage(options, transport) {
|
|
344
|
+
const { queueName, payload, idempotencyKey, retentionSeconds, callbacks } = options;
|
|
345
|
+
const headers = new Headers({
|
|
346
|
+
Authorization: `Bearer ${this.token}`,
|
|
347
|
+
"Vqs-Queue-Name": queueName,
|
|
348
|
+
"Content-Type": transport.contentType
|
|
349
|
+
});
|
|
350
|
+
if (idempotencyKey) {
|
|
351
|
+
headers.set("Vqs-Idempotency-Key", idempotencyKey);
|
|
352
|
+
}
|
|
353
|
+
if (retentionSeconds !== void 0) {
|
|
354
|
+
headers.set("Vqs-Retention-Seconds", retentionSeconds.toString());
|
|
355
|
+
}
|
|
356
|
+
let localhostCallbacks = [];
|
|
357
|
+
if (callbacks) {
|
|
358
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
359
|
+
if (isDevelopment) {
|
|
360
|
+
localhostCallbacks = processDevelopmentCallbacks(callbacks);
|
|
361
|
+
} else {
|
|
362
|
+
const endpoints = Object.entries(callbacks).map(
|
|
363
|
+
([group, config]) => `${group}=${Buffer.from(config.url).toString("base64")}`
|
|
364
|
+
).join(",");
|
|
365
|
+
headers.set("Vqs-Callback-Url", endpoints);
|
|
366
|
+
const delays = Object.entries(callbacks).filter(([, config]) => config.delay !== void 0).map(([group, config]) => `${group}=${config.delay}`).join(",");
|
|
367
|
+
if (delays) {
|
|
368
|
+
headers.set("Vqs-Callback-Delay", delays);
|
|
369
|
+
}
|
|
370
|
+
const frequencies = Object.entries(callbacks).filter(([, config]) => config.frequency !== void 0).map(([group, config]) => `${group}=${config.frequency}`).join(",");
|
|
371
|
+
if (frequencies) {
|
|
372
|
+
headers.set("Vqs-Callback-Frequency", frequencies);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const body = transport.serialize(payload);
|
|
377
|
+
const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers,
|
|
380
|
+
body
|
|
381
|
+
});
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
if (response.status === 400) {
|
|
384
|
+
const errorText = await response.text();
|
|
385
|
+
throw new BadRequestError(errorText || "Invalid parameters");
|
|
386
|
+
}
|
|
387
|
+
if (response.status === 401) {
|
|
388
|
+
throw new UnauthorizedError();
|
|
389
|
+
}
|
|
390
|
+
if (response.status === 403) {
|
|
391
|
+
throw new ForbiddenError();
|
|
392
|
+
}
|
|
393
|
+
if (response.status === 409) {
|
|
394
|
+
throw new Error("Duplicate idempotency key detected");
|
|
395
|
+
}
|
|
396
|
+
if (response.status >= 500) {
|
|
397
|
+
throw new InternalServerError(
|
|
398
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Failed to send message: ${response.status} ${response.statusText}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const responseData = await response.json();
|
|
406
|
+
if (localhostCallbacks.length > 0) {
|
|
407
|
+
fireLocalhostCallbacks(localhostCallbacks, queueName, responseData);
|
|
408
|
+
}
|
|
409
|
+
return responseData;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Receive messages from a queue
|
|
413
|
+
* @param options Receive messages options
|
|
414
|
+
* @param transport Serializer/deserializer for the payload
|
|
415
|
+
* @returns AsyncGenerator that yields messages as they arrive
|
|
416
|
+
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
417
|
+
* @throws {QueueEmptyError} When no messages are available (204)
|
|
418
|
+
* @throws {MessageLockedError} When FIFO queue has locked messages (423)
|
|
419
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
420
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
421
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
422
|
+
* @throws {InternalServerError} When server encounters an error
|
|
423
|
+
*/
|
|
424
|
+
async *receiveMessages(options, transport) {
|
|
425
|
+
const { queueName, consumerGroup, visibilityTimeoutSeconds, limit } = options;
|
|
426
|
+
if (limit !== void 0 && (limit < 1 || limit > 10)) {
|
|
427
|
+
throw new InvalidLimitError(limit);
|
|
428
|
+
}
|
|
429
|
+
const headers = new Headers({
|
|
430
|
+
Authorization: `Bearer ${this.token}`,
|
|
431
|
+
"Vqs-Queue-Name": queueName,
|
|
432
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
433
|
+
Accept: "multipart/mixed"
|
|
434
|
+
});
|
|
435
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
436
|
+
headers.set(
|
|
437
|
+
"Vqs-Visibility-Timeout",
|
|
438
|
+
visibilityTimeoutSeconds.toString()
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (limit !== void 0) {
|
|
442
|
+
headers.set("Vqs-Limit", limit.toString());
|
|
443
|
+
}
|
|
444
|
+
const response = await fetch(`${this.baseUrl}/api/v2/messages`, {
|
|
445
|
+
method: "GET",
|
|
446
|
+
headers
|
|
447
|
+
});
|
|
448
|
+
if (response.status === 204) {
|
|
449
|
+
throw new QueueEmptyError(queueName, consumerGroup);
|
|
450
|
+
}
|
|
451
|
+
if (!response.ok) {
|
|
452
|
+
if (response.status === 400) {
|
|
453
|
+
const errorText = await response.text();
|
|
454
|
+
throw new BadRequestError(errorText || "Invalid parameters");
|
|
455
|
+
}
|
|
456
|
+
if (response.status === 401) {
|
|
457
|
+
throw new UnauthorizedError();
|
|
458
|
+
}
|
|
459
|
+
if (response.status === 403) {
|
|
460
|
+
throw new ForbiddenError();
|
|
461
|
+
}
|
|
462
|
+
if (response.status === 423) {
|
|
463
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
464
|
+
let retryAfter;
|
|
465
|
+
if (retryAfterHeader) {
|
|
466
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
467
|
+
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
468
|
+
}
|
|
469
|
+
throw new MessageLockedError("next message in FIFO queue", retryAfter);
|
|
470
|
+
}
|
|
471
|
+
if (response.status >= 500) {
|
|
472
|
+
throw new InternalServerError(
|
|
473
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Failed to receive messages: ${response.status} ${response.statusText}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
481
|
+
try {
|
|
482
|
+
const parsedHeaders = parseVQSHeaders(multipartMessage.headers);
|
|
483
|
+
if (!parsedHeaders) {
|
|
484
|
+
console.warn("Missing required VQS headers in multipart part");
|
|
485
|
+
await consumeStream(multipartMessage.payload);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const deserializedPayload = await transport.deserialize(
|
|
489
|
+
multipartMessage.payload
|
|
490
|
+
);
|
|
491
|
+
const message = {
|
|
492
|
+
...parsedHeaders,
|
|
493
|
+
payload: deserializedPayload
|
|
494
|
+
};
|
|
495
|
+
yield message;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.warn("Failed to process multipart message:", error);
|
|
498
|
+
await consumeStream(multipartMessage.payload);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async receiveMessageById(options, transport) {
|
|
503
|
+
const {
|
|
504
|
+
queueName,
|
|
505
|
+
consumerGroup,
|
|
506
|
+
messageId,
|
|
507
|
+
visibilityTimeoutSeconds,
|
|
508
|
+
skipPayload
|
|
509
|
+
} = options;
|
|
510
|
+
const headers = new Headers({
|
|
511
|
+
Authorization: `Bearer ${this.token}`,
|
|
512
|
+
"Vqs-Queue-Name": queueName,
|
|
513
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
514
|
+
Accept: "multipart/mixed"
|
|
515
|
+
});
|
|
516
|
+
if (visibilityTimeoutSeconds !== void 0) {
|
|
517
|
+
headers.set(
|
|
518
|
+
"Vqs-Visibility-Timeout",
|
|
519
|
+
visibilityTimeoutSeconds.toString()
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (skipPayload) {
|
|
523
|
+
headers.set("Vqs-Skip-Payload", "1");
|
|
524
|
+
}
|
|
525
|
+
const response = await fetch(
|
|
526
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
527
|
+
{
|
|
528
|
+
method: "GET",
|
|
529
|
+
headers
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
if (!response.ok) {
|
|
533
|
+
if (response.status === 400) {
|
|
534
|
+
const errorText = await response.text();
|
|
535
|
+
throw new BadRequestError(errorText || "Invalid parameters");
|
|
536
|
+
}
|
|
537
|
+
if (response.status === 401) {
|
|
538
|
+
throw new UnauthorizedError();
|
|
539
|
+
}
|
|
540
|
+
if (response.status === 403) {
|
|
541
|
+
throw new ForbiddenError();
|
|
542
|
+
}
|
|
543
|
+
if (response.status === 404) {
|
|
544
|
+
throw new MessageNotFoundError(messageId);
|
|
545
|
+
}
|
|
546
|
+
if (response.status === 423) {
|
|
547
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
548
|
+
let retryAfter;
|
|
549
|
+
if (retryAfterHeader) {
|
|
550
|
+
const parsed = parseInt(retryAfterHeader, 10);
|
|
551
|
+
retryAfter = isNaN(parsed) ? void 0 : parsed;
|
|
552
|
+
}
|
|
553
|
+
throw new MessageLockedError(messageId, retryAfter);
|
|
554
|
+
}
|
|
555
|
+
if (response.status === 424) {
|
|
556
|
+
try {
|
|
557
|
+
const errorData = await response.json();
|
|
558
|
+
if (errorData.meta?.nextMessageId) {
|
|
559
|
+
throw new FailedDependencyError(
|
|
560
|
+
messageId,
|
|
561
|
+
errorData.meta.nextMessageId
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
} catch (parseError) {
|
|
565
|
+
if (parseError instanceof FailedDependencyError) {
|
|
566
|
+
throw parseError;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
throw new MessageNotAvailableError(
|
|
570
|
+
messageId,
|
|
571
|
+
"FIFO ordering violation"
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (response.status === 409) {
|
|
575
|
+
try {
|
|
576
|
+
const errorData = await response.json();
|
|
577
|
+
if (errorData.nextMessageId) {
|
|
578
|
+
throw new FifoOrderingViolationError(
|
|
579
|
+
messageId,
|
|
580
|
+
errorData.nextMessageId,
|
|
581
|
+
errorData.error
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
} catch (parseError) {
|
|
585
|
+
}
|
|
586
|
+
throw new MessageNotAvailableError(messageId);
|
|
587
|
+
}
|
|
588
|
+
if (response.status >= 500) {
|
|
589
|
+
throw new InternalServerError(
|
|
590
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
throw new Error(
|
|
594
|
+
`Failed to receive message by ID: ${response.status} ${response.statusText}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
if (skipPayload && response.status === 204) {
|
|
598
|
+
const parsedHeaders = parseVQSHeaders(response.headers);
|
|
599
|
+
if (!parsedHeaders) {
|
|
600
|
+
throw new MessageCorruptedError(
|
|
601
|
+
messageId,
|
|
602
|
+
"Missing required VQS headers in 204 response"
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
const message = {
|
|
606
|
+
...parsedHeaders,
|
|
607
|
+
payload: void 0
|
|
608
|
+
};
|
|
609
|
+
return { message };
|
|
610
|
+
}
|
|
611
|
+
if (!transport) {
|
|
612
|
+
throw new Error("Transport is required when skipPayload is not true");
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
for await (const multipartMessage of (0, import_mixpart.parseMultipartStream)(response)) {
|
|
616
|
+
try {
|
|
617
|
+
const parsedHeaders = parseVQSHeaders(multipartMessage.headers);
|
|
618
|
+
if (!parsedHeaders) {
|
|
619
|
+
console.warn("Missing required VQS headers in multipart part");
|
|
620
|
+
await consumeStream(multipartMessage.payload);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const deserializedPayload = await transport.deserialize(
|
|
624
|
+
multipartMessage.payload
|
|
625
|
+
);
|
|
626
|
+
const message = {
|
|
627
|
+
...parsedHeaders,
|
|
628
|
+
payload: deserializedPayload
|
|
629
|
+
};
|
|
630
|
+
return { message };
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.warn("Failed to deserialize message by ID:", error);
|
|
633
|
+
await consumeStream(multipartMessage.payload);
|
|
634
|
+
throw new MessageCorruptedError(
|
|
635
|
+
messageId,
|
|
636
|
+
`Failed to deserialize payload: ${error}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (error instanceof MessageCorruptedError) {
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
throw new MessageCorruptedError(
|
|
645
|
+
messageId,
|
|
646
|
+
`Failed to parse multipart response: ${error}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
throw new MessageNotFoundError(messageId);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Delete a message (acknowledge processing)
|
|
653
|
+
* @param options Delete message options
|
|
654
|
+
* @returns Promise with delete status
|
|
655
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
656
|
+
* @throws {MessageNotAvailableError} When message can't be deleted (409)
|
|
657
|
+
* @throws {BadRequestError} When ticket is missing or invalid (400)
|
|
658
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
659
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
660
|
+
* @throws {InternalServerError} When server encounters an error
|
|
661
|
+
*/
|
|
662
|
+
async deleteMessage(options) {
|
|
663
|
+
const { queueName, consumerGroup, messageId, ticket } = options;
|
|
664
|
+
const response = await fetch(
|
|
665
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
666
|
+
{
|
|
667
|
+
method: "DELETE",
|
|
668
|
+
headers: new Headers({
|
|
669
|
+
Authorization: `Bearer ${this.token}`,
|
|
670
|
+
"Vqs-Queue-Name": queueName,
|
|
671
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
672
|
+
"Vqs-Ticket": ticket
|
|
673
|
+
})
|
|
674
|
+
}
|
|
675
|
+
);
|
|
676
|
+
if (!response.ok) {
|
|
677
|
+
if (response.status === 400) {
|
|
678
|
+
throw new BadRequestError("Missing or invalid ticket");
|
|
679
|
+
}
|
|
680
|
+
if (response.status === 401) {
|
|
681
|
+
throw new UnauthorizedError();
|
|
682
|
+
}
|
|
683
|
+
if (response.status === 403) {
|
|
684
|
+
throw new ForbiddenError();
|
|
685
|
+
}
|
|
686
|
+
if (response.status === 404) {
|
|
687
|
+
throw new MessageNotFoundError(messageId);
|
|
688
|
+
}
|
|
689
|
+
if (response.status === 409) {
|
|
690
|
+
throw new MessageNotAvailableError(
|
|
691
|
+
messageId,
|
|
692
|
+
"Invalid ticket, message not in correct state, or already processed"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
if (response.status >= 500) {
|
|
696
|
+
throw new InternalServerError(
|
|
697
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
throw new Error(
|
|
701
|
+
`Failed to delete message: ${response.status} ${response.statusText}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
return { deleted: true };
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Change the visibility timeout of a message
|
|
708
|
+
* @param options Change visibility options
|
|
709
|
+
* @returns Promise with update status
|
|
710
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
711
|
+
* @throws {MessageNotAvailableError} When message can't be updated (409)
|
|
712
|
+
* @throws {BadRequestError} When ticket is missing or visibility timeout invalid (400)
|
|
713
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
714
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
715
|
+
* @throws {InternalServerError} When server encounters an error
|
|
716
|
+
*/
|
|
717
|
+
async changeVisibility(options) {
|
|
718
|
+
const {
|
|
719
|
+
queueName,
|
|
720
|
+
consumerGroup,
|
|
721
|
+
messageId,
|
|
722
|
+
ticket,
|
|
723
|
+
visibilityTimeoutSeconds
|
|
724
|
+
} = options;
|
|
725
|
+
const response = await fetch(
|
|
726
|
+
`${this.baseUrl}/api/v2/messages/${encodeURIComponent(messageId)}`,
|
|
727
|
+
{
|
|
728
|
+
method: "PATCH",
|
|
729
|
+
headers: new Headers({
|
|
730
|
+
Authorization: `Bearer ${this.token}`,
|
|
731
|
+
"Vqs-Queue-Name": queueName,
|
|
732
|
+
"Vqs-Consumer-Group": consumerGroup,
|
|
733
|
+
"Vqs-Ticket": ticket,
|
|
734
|
+
"Vqs-Visibility-Timeout": visibilityTimeoutSeconds.toString()
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
if (response.status === 400) {
|
|
740
|
+
throw new BadRequestError(
|
|
741
|
+
"Missing ticket or invalid visibility timeout"
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
if (response.status === 401) {
|
|
745
|
+
throw new UnauthorizedError();
|
|
746
|
+
}
|
|
747
|
+
if (response.status === 403) {
|
|
748
|
+
throw new ForbiddenError();
|
|
749
|
+
}
|
|
750
|
+
if (response.status === 404) {
|
|
751
|
+
throw new MessageNotFoundError(messageId);
|
|
752
|
+
}
|
|
753
|
+
if (response.status === 409) {
|
|
754
|
+
throw new MessageNotAvailableError(
|
|
755
|
+
messageId,
|
|
756
|
+
"Invalid ticket, message not in correct state, or already processed"
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (response.status >= 500) {
|
|
760
|
+
throw new InternalServerError(
|
|
761
|
+
`Server error: ${response.status} ${response.statusText}`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
throw new Error(
|
|
765
|
+
`Failed to change visibility: ${response.status} ${response.statusText}`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
return { updated: true };
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// src/transports.ts
|
|
773
|
+
var JsonTransport = class {
|
|
774
|
+
contentType = "application/json";
|
|
775
|
+
serialize(value) {
|
|
776
|
+
return Buffer.from(JSON.stringify(value), "utf8");
|
|
777
|
+
}
|
|
778
|
+
async deserialize(stream) {
|
|
779
|
+
const reader = stream.getReader();
|
|
780
|
+
const chunks = [];
|
|
781
|
+
try {
|
|
782
|
+
while (true) {
|
|
783
|
+
const { done, value } = await reader.read();
|
|
784
|
+
if (done) break;
|
|
785
|
+
chunks.push(value);
|
|
786
|
+
}
|
|
787
|
+
} finally {
|
|
788
|
+
reader.releaseLock();
|
|
789
|
+
}
|
|
790
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
791
|
+
const buffer = new Uint8Array(totalLength);
|
|
792
|
+
let offset = 0;
|
|
793
|
+
for (const chunk of chunks) {
|
|
794
|
+
buffer.set(chunk, offset);
|
|
795
|
+
offset += chunk.length;
|
|
796
|
+
}
|
|
797
|
+
return JSON.parse(Buffer.from(buffer).toString("utf8"));
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
var BufferTransport = class {
|
|
801
|
+
contentType = "application/octet-stream";
|
|
802
|
+
serialize(value) {
|
|
803
|
+
return value;
|
|
804
|
+
}
|
|
805
|
+
async deserialize(stream) {
|
|
806
|
+
const reader = stream.getReader();
|
|
807
|
+
const chunks = [];
|
|
808
|
+
try {
|
|
809
|
+
while (true) {
|
|
810
|
+
const { done, value } = await reader.read();
|
|
811
|
+
if (done) break;
|
|
812
|
+
chunks.push(value);
|
|
813
|
+
}
|
|
814
|
+
} finally {
|
|
815
|
+
reader.releaseLock();
|
|
816
|
+
}
|
|
817
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
818
|
+
const buffer = new Uint8Array(totalLength);
|
|
819
|
+
let offset = 0;
|
|
820
|
+
for (const chunk of chunks) {
|
|
821
|
+
buffer.set(chunk, offset);
|
|
822
|
+
offset += chunk.length;
|
|
823
|
+
}
|
|
824
|
+
return Buffer.from(buffer);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
var StreamTransport = class {
|
|
828
|
+
contentType = "application/octet-stream";
|
|
829
|
+
serialize(value) {
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
async deserialize(stream) {
|
|
833
|
+
return stream;
|
|
834
|
+
}
|
|
835
|
+
async finalize(payload) {
|
|
836
|
+
const reader = payload.getReader();
|
|
837
|
+
try {
|
|
838
|
+
while (true) {
|
|
839
|
+
const { done } = await reader.read();
|
|
840
|
+
if (done) break;
|
|
841
|
+
}
|
|
842
|
+
} finally {
|
|
843
|
+
reader.releaseLock();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/consumer-group.ts
|
|
849
|
+
var ConsumerGroup = class {
|
|
850
|
+
client;
|
|
851
|
+
topicName;
|
|
852
|
+
consumerGroupName;
|
|
853
|
+
visibilityTimeout;
|
|
854
|
+
refreshInterval;
|
|
855
|
+
transport;
|
|
856
|
+
/**
|
|
857
|
+
* Create a new ConsumerGroup instance
|
|
858
|
+
* @param client VQSClient instance to use for API calls
|
|
859
|
+
* @param topicName Name of the topic to consume from
|
|
860
|
+
* @param consumerGroupName Name of the consumer group
|
|
861
|
+
* @param options Optional configuration
|
|
862
|
+
*/
|
|
863
|
+
constructor(client, topicName, consumerGroupName, options = {}) {
|
|
864
|
+
this.client = client;
|
|
865
|
+
this.topicName = topicName;
|
|
866
|
+
this.consumerGroupName = consumerGroupName;
|
|
867
|
+
this.visibilityTimeout = options.visibilityTimeoutSeconds || 30;
|
|
868
|
+
this.refreshInterval = options.refreshInterval || 10;
|
|
869
|
+
this.transport = options.transport || new JsonTransport();
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Starts a background loop that periodically extends the visibility timeout for a message.
|
|
873
|
+
* This prevents the message from becoming visible to other consumers while it's being processed.
|
|
874
|
+
*
|
|
875
|
+
* The extension loop runs every `refreshInterval` seconds and updates the message's
|
|
876
|
+
* visibility timeout to `visibilityTimeout` seconds from the current time.
|
|
877
|
+
*
|
|
878
|
+
* @param messageId - The unique identifier of the message to extend visibility for
|
|
879
|
+
* @param ticket - The receipt ticket that proves ownership of the message
|
|
880
|
+
* @returns A function that when called will stop the extension loop
|
|
881
|
+
*
|
|
882
|
+
* @remarks
|
|
883
|
+
* - The first extension attempt occurs after `refreshInterval` seconds, not immediately
|
|
884
|
+
* - If an extension fails, the loop terminates with an error logged to console
|
|
885
|
+
* - The returned stop function is idempotent - calling it multiple times is safe
|
|
886
|
+
* - By default, the stop function returns immediately without waiting for in-flight
|
|
887
|
+
* - Pass `true` to the stop function to wait for any in-flight extension to complete
|
|
888
|
+
*/
|
|
889
|
+
startVisibilityExtension(messageId, ticket) {
|
|
890
|
+
let isRunning = true;
|
|
891
|
+
let resolveLifecycle;
|
|
892
|
+
let timeoutId = null;
|
|
893
|
+
const lifecyclePromise = new Promise((resolve) => {
|
|
894
|
+
resolveLifecycle = resolve;
|
|
895
|
+
});
|
|
896
|
+
const extend = async () => {
|
|
897
|
+
if (!isRunning) {
|
|
898
|
+
resolveLifecycle();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
await this.client.changeVisibility({
|
|
903
|
+
queueName: this.topicName,
|
|
904
|
+
consumerGroup: this.consumerGroupName,
|
|
905
|
+
messageId,
|
|
906
|
+
ticket,
|
|
907
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
908
|
+
});
|
|
909
|
+
if (isRunning) {
|
|
910
|
+
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
911
|
+
} else {
|
|
912
|
+
resolveLifecycle();
|
|
913
|
+
}
|
|
914
|
+
} catch (error) {
|
|
915
|
+
console.error(
|
|
916
|
+
`Failed to extend visibility for message ${messageId}:`,
|
|
917
|
+
error
|
|
918
|
+
);
|
|
919
|
+
resolveLifecycle();
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
timeoutId = setTimeout(() => extend(), this.refreshInterval * 1e3);
|
|
923
|
+
return async (waitForCompletion = false) => {
|
|
924
|
+
isRunning = false;
|
|
925
|
+
if (timeoutId) {
|
|
926
|
+
clearTimeout(timeoutId);
|
|
927
|
+
timeoutId = null;
|
|
928
|
+
}
|
|
929
|
+
if (waitForCompletion) {
|
|
930
|
+
await lifecyclePromise;
|
|
931
|
+
} else {
|
|
932
|
+
resolveLifecycle();
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Process a single message with the given handler
|
|
938
|
+
* @param message The message to process
|
|
939
|
+
* @param handler Function to process the message
|
|
940
|
+
*/
|
|
941
|
+
async processMessage(message, handler) {
|
|
942
|
+
const stopExtension = this.startVisibilityExtension(
|
|
943
|
+
message.messageId,
|
|
944
|
+
message.ticket
|
|
945
|
+
);
|
|
946
|
+
try {
|
|
947
|
+
const result = await handler(message);
|
|
948
|
+
await stopExtension();
|
|
949
|
+
if (result && "timeoutSeconds" in result) {
|
|
950
|
+
await this.client.changeVisibility({
|
|
951
|
+
queueName: this.topicName,
|
|
952
|
+
consumerGroup: this.consumerGroupName,
|
|
953
|
+
messageId: message.messageId,
|
|
954
|
+
ticket: message.ticket,
|
|
955
|
+
visibilityTimeoutSeconds: result.timeoutSeconds
|
|
956
|
+
});
|
|
957
|
+
} else {
|
|
958
|
+
await this.client.deleteMessage({
|
|
959
|
+
queueName: this.topicName,
|
|
960
|
+
consumerGroup: this.consumerGroupName,
|
|
961
|
+
messageId: message.messageId,
|
|
962
|
+
ticket: message.ticket
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
} catch (error) {
|
|
966
|
+
await stopExtension();
|
|
967
|
+
if (this.transport.finalize && message.payload !== void 0 && message.payload !== null) {
|
|
968
|
+
try {
|
|
969
|
+
await this.transport.finalize(message.payload);
|
|
970
|
+
} catch (finalizeError) {
|
|
971
|
+
console.warn("Failed to finalize message payload:", finalizeError);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Start continuous processing of messages from the topic
|
|
979
|
+
* @param signal AbortSignal to control when to stop processing
|
|
980
|
+
* @param handler Function to process each message
|
|
981
|
+
* @param options Processing options
|
|
982
|
+
* @returns Promise that resolves when processing stops (due to signal or error)
|
|
983
|
+
*/
|
|
984
|
+
async subscribe(signal, handler, options = {}) {
|
|
985
|
+
const pollingInterval = options.pollingInterval || 1e3;
|
|
986
|
+
while (!signal.aborted) {
|
|
987
|
+
try {
|
|
988
|
+
for await (const message of this.client.receiveMessages(
|
|
989
|
+
{
|
|
990
|
+
queueName: this.topicName,
|
|
991
|
+
consumerGroup: this.consumerGroupName,
|
|
992
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
993
|
+
limit: 1
|
|
994
|
+
// Always process one message at a time
|
|
995
|
+
},
|
|
996
|
+
this.transport
|
|
997
|
+
)) {
|
|
998
|
+
if (signal.aborted) {
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
await this.processMessage(message, handler);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
console.error("Error processing message:", error);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (!signal.aborted) {
|
|
1008
|
+
await new Promise((resolve) => {
|
|
1009
|
+
const timeoutId = setTimeout(resolve, pollingInterval);
|
|
1010
|
+
signal.addEventListener(
|
|
1011
|
+
"abort",
|
|
1012
|
+
() => {
|
|
1013
|
+
clearTimeout(timeoutId);
|
|
1014
|
+
resolve();
|
|
1015
|
+
},
|
|
1016
|
+
{ once: true }
|
|
1017
|
+
);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (error instanceof QueueEmptyError) {
|
|
1022
|
+
if (!signal.aborted) {
|
|
1023
|
+
await new Promise((resolve) => {
|
|
1024
|
+
const timeoutId = setTimeout(resolve, pollingInterval);
|
|
1025
|
+
signal.addEventListener(
|
|
1026
|
+
"abort",
|
|
1027
|
+
() => {
|
|
1028
|
+
clearTimeout(timeoutId);
|
|
1029
|
+
resolve();
|
|
1030
|
+
},
|
|
1031
|
+
{ once: true }
|
|
1032
|
+
);
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if (error instanceof MessageLockedError) {
|
|
1038
|
+
const waitTime = error.retryAfter ? error.retryAfter * 1e3 : pollingInterval;
|
|
1039
|
+
if (!signal.aborted) {
|
|
1040
|
+
await new Promise((resolve) => {
|
|
1041
|
+
const timeoutId = setTimeout(resolve, waitTime);
|
|
1042
|
+
signal.addEventListener(
|
|
1043
|
+
"abort",
|
|
1044
|
+
() => {
|
|
1045
|
+
clearTimeout(timeoutId);
|
|
1046
|
+
resolve();
|
|
1047
|
+
},
|
|
1048
|
+
{ once: true }
|
|
1049
|
+
);
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
console.error("Error polling topic:", error);
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Receive and process a specific message by its ID with full payload
|
|
1061
|
+
* @param messageId The ID of the message to receive and process
|
|
1062
|
+
* @param handler Function to process the message with full payload
|
|
1063
|
+
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1064
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1065
|
+
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1066
|
+
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1067
|
+
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1068
|
+
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1069
|
+
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1070
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1071
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1072
|
+
* @throws {ForbiddenError} When access is denied
|
|
1073
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1074
|
+
*/
|
|
1075
|
+
async receiveMessage(messageId, handler) {
|
|
1076
|
+
const response = await this.client.receiveMessageById(
|
|
1077
|
+
{
|
|
1078
|
+
queueName: this.topicName,
|
|
1079
|
+
consumerGroup: this.consumerGroupName,
|
|
1080
|
+
messageId,
|
|
1081
|
+
visibilityTimeoutSeconds: this.visibilityTimeout
|
|
1082
|
+
},
|
|
1083
|
+
this.transport
|
|
1084
|
+
);
|
|
1085
|
+
await this.processMessage(response.message, handler);
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Receive and process the next available message from the queue
|
|
1089
|
+
* @param handler Function to process the message
|
|
1090
|
+
* @returns Promise that resolves when the message is processed or rejects with specific errors
|
|
1091
|
+
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1092
|
+
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1093
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1094
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1095
|
+
* @throws {ForbiddenError} When access is denied
|
|
1096
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1097
|
+
*/
|
|
1098
|
+
async receiveNextMessage(handler) {
|
|
1099
|
+
let messageFound = false;
|
|
1100
|
+
for await (const message of this.client.receiveMessages(
|
|
1101
|
+
{
|
|
1102
|
+
queueName: this.topicName,
|
|
1103
|
+
consumerGroup: this.consumerGroupName,
|
|
1104
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1105
|
+
limit: 1
|
|
1106
|
+
},
|
|
1107
|
+
this.transport
|
|
1108
|
+
)) {
|
|
1109
|
+
messageFound = true;
|
|
1110
|
+
await this.processMessage(message, handler);
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
if (!messageFound) {
|
|
1114
|
+
throw new Error("No messages available");
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Receive and process multiple next available messages from the queue
|
|
1119
|
+
* @param limit Number of messages to process (1-10)
|
|
1120
|
+
* @param handler Function to process each message
|
|
1121
|
+
* @returns Promise that resolves to an array of PromiseSettledResult (same as Promise.allSettled)
|
|
1122
|
+
* @throws {InvalidLimitError} When limit parameter is not between 1 and 10
|
|
1123
|
+
* @throws {QueueEmptyError} When no messages are available in the queue (204)
|
|
1124
|
+
* @throws {MessageLockedError} When the next message in a FIFO queue is locked (423)
|
|
1125
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1126
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1127
|
+
* @throws {ForbiddenError} When access is denied
|
|
1128
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1129
|
+
*/
|
|
1130
|
+
async receiveNextMessages(limit, handler) {
|
|
1131
|
+
if (limit < 1 || limit > 10) {
|
|
1132
|
+
throw new InvalidLimitError(limit);
|
|
1133
|
+
}
|
|
1134
|
+
const processingPromises = [];
|
|
1135
|
+
let messageCount = 0;
|
|
1136
|
+
for await (const message of this.client.receiveMessages(
|
|
1137
|
+
{
|
|
1138
|
+
queueName: this.topicName,
|
|
1139
|
+
consumerGroup: this.consumerGroupName,
|
|
1140
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1141
|
+
limit
|
|
1142
|
+
},
|
|
1143
|
+
this.transport
|
|
1144
|
+
)) {
|
|
1145
|
+
messageCount++;
|
|
1146
|
+
const wrappedPromise = this.processMessage(message, handler).then(
|
|
1147
|
+
(value) => ({
|
|
1148
|
+
status: "fulfilled",
|
|
1149
|
+
value
|
|
1150
|
+
}),
|
|
1151
|
+
(reason) => ({ status: "rejected", reason })
|
|
1152
|
+
);
|
|
1153
|
+
processingPromises.push(wrappedPromise);
|
|
1154
|
+
}
|
|
1155
|
+
if (messageCount === 0) {
|
|
1156
|
+
throw new Error("No messages available");
|
|
1157
|
+
}
|
|
1158
|
+
const results = await Promise.all(processingPromises);
|
|
1159
|
+
return results;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Handle a specific message by its ID without downloading the payload (metadata only)
|
|
1163
|
+
* @param messageId The ID of the message to handle
|
|
1164
|
+
* @param handler Function to process the message metadata (payload will be void)
|
|
1165
|
+
* @returns Promise that resolves when the message is handled or rejects with specific errors
|
|
1166
|
+
* @throws {MessageNotFoundError} When the message doesn't exist (404)
|
|
1167
|
+
* @throws {MessageNotAvailableError} When the message exists but isn't available for processing (409)
|
|
1168
|
+
* @throws {MessageLockedError} When the message is temporarily locked (423)
|
|
1169
|
+
* @throws {FifoOrderingViolationError} When there's a FIFO ordering violation (409 with nextMessageId)
|
|
1170
|
+
* @throws {FailedDependencyError} When FIFO ordering is violated (424)
|
|
1171
|
+
* @throws {MessageCorruptedError} When the message data is corrupted
|
|
1172
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1173
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1174
|
+
* @throws {ForbiddenError} When access is denied
|
|
1175
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1176
|
+
*/
|
|
1177
|
+
async handleMessage(messageId, handler) {
|
|
1178
|
+
const response = await this.client.receiveMessageById(
|
|
1179
|
+
{
|
|
1180
|
+
queueName: this.topicName,
|
|
1181
|
+
consumerGroup: this.consumerGroupName,
|
|
1182
|
+
messageId,
|
|
1183
|
+
visibilityTimeoutSeconds: this.visibilityTimeout,
|
|
1184
|
+
skipPayload: true
|
|
1185
|
+
},
|
|
1186
|
+
this.transport
|
|
1187
|
+
);
|
|
1188
|
+
await this.processMessage(response.message, handler);
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Get the consumer group name
|
|
1192
|
+
*/
|
|
1193
|
+
get name() {
|
|
1194
|
+
return this.consumerGroupName;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Get the topic name this consumer group is subscribed to
|
|
1198
|
+
*/
|
|
1199
|
+
get topic() {
|
|
1200
|
+
return this.topicName;
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
// src/topic.ts
|
|
1205
|
+
var Topic = class {
|
|
1206
|
+
client;
|
|
1207
|
+
topicName;
|
|
1208
|
+
transport;
|
|
1209
|
+
/**
|
|
1210
|
+
* Create a new Topic instance
|
|
1211
|
+
* @param client VQSClient instance to use for API calls
|
|
1212
|
+
* @param topicName Name of the topic to work with
|
|
1213
|
+
* @param transport Optional serializer/deserializer for the payload (defaults to JSON)
|
|
1214
|
+
*/
|
|
1215
|
+
constructor(client, topicName, transport) {
|
|
1216
|
+
this.client = client;
|
|
1217
|
+
this.topicName = topicName;
|
|
1218
|
+
this.transport = transport || new JsonTransport();
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Publish a message to the topic
|
|
1222
|
+
* @param payload The data to publish
|
|
1223
|
+
* @param options Optional publish options
|
|
1224
|
+
* @returns An object containing the message ID
|
|
1225
|
+
* @throws {BadRequestError} When request parameters are invalid
|
|
1226
|
+
* @throws {UnauthorizedError} When authentication fails
|
|
1227
|
+
* @throws {ForbiddenError} When access is denied (environment mismatch)
|
|
1228
|
+
* @throws {InternalServerError} When server encounters an error
|
|
1229
|
+
*/
|
|
1230
|
+
async publish(payload, options) {
|
|
1231
|
+
const result = await this.client.sendMessage(
|
|
1232
|
+
{
|
|
1233
|
+
queueName: this.topicName,
|
|
1234
|
+
payload,
|
|
1235
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1236
|
+
retentionSeconds: options?.retentionSeconds,
|
|
1237
|
+
callbacks: options?.callbacks
|
|
1238
|
+
},
|
|
1239
|
+
this.transport
|
|
1240
|
+
);
|
|
1241
|
+
return { messageId: result.messageId };
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Create a consumer group for this topic
|
|
1245
|
+
* @param consumerGroupName Name of the consumer group
|
|
1246
|
+
* @param options Optional configuration for the consumer group
|
|
1247
|
+
* @returns A ConsumerGroup instance
|
|
1248
|
+
*/
|
|
1249
|
+
consumerGroup(consumerGroupName, options) {
|
|
1250
|
+
const consumerOptions = {
|
|
1251
|
+
...options,
|
|
1252
|
+
transport: options?.transport || this.transport
|
|
1253
|
+
};
|
|
1254
|
+
return new ConsumerGroup(
|
|
1255
|
+
this.client,
|
|
1256
|
+
this.topicName,
|
|
1257
|
+
consumerGroupName,
|
|
1258
|
+
consumerOptions
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Get the topic name
|
|
1263
|
+
*/
|
|
1264
|
+
get name() {
|
|
1265
|
+
return this.topicName;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Get the transport used by this topic
|
|
1269
|
+
*/
|
|
1270
|
+
get serializer() {
|
|
1271
|
+
return this.transport;
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
// src/factory.ts
|
|
1276
|
+
function createTopic(client, topicName, transport) {
|
|
1277
|
+
return new Topic(client, topicName, transport);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/callback.ts
|
|
1281
|
+
function parseCallbackRequest(request) {
|
|
1282
|
+
const headers = request.headers;
|
|
1283
|
+
const messageId = headers.get("Vqs-Message-Id");
|
|
1284
|
+
const queueName = headers.get("Vqs-Queue-Name");
|
|
1285
|
+
const consumerGroup = headers.get("Vqs-Consumer-Group");
|
|
1286
|
+
const missingHeaders = [];
|
|
1287
|
+
if (!messageId) missingHeaders.push("Vqs-Message-Id");
|
|
1288
|
+
if (!queueName) missingHeaders.push("Vqs-Queue-Name");
|
|
1289
|
+
if (!consumerGroup) missingHeaders.push("Vqs-Consumer-Group");
|
|
1290
|
+
if (missingHeaders.length > 0) {
|
|
1291
|
+
throw new InvalidCallbackError(
|
|
1292
|
+
`Missing required VQS callback headers: ${missingHeaders.join(", ")}`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
messageId,
|
|
1297
|
+
queueName,
|
|
1298
|
+
consumerGroup
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1302
|
+
0 && (module.exports = {
|
|
1303
|
+
BadRequestError,
|
|
1304
|
+
BufferTransport,
|
|
1305
|
+
ConsumerGroup,
|
|
1306
|
+
FailedDependencyError,
|
|
1307
|
+
FifoOrderingViolationError,
|
|
1308
|
+
ForbiddenError,
|
|
1309
|
+
InternalServerError,
|
|
1310
|
+
InvalidCallbackError,
|
|
1311
|
+
InvalidLimitError,
|
|
1312
|
+
JsonTransport,
|
|
1313
|
+
MessageCorruptedError,
|
|
1314
|
+
MessageLockedError,
|
|
1315
|
+
MessageNotAvailableError,
|
|
1316
|
+
MessageNotFoundError,
|
|
1317
|
+
QueueEmptyError,
|
|
1318
|
+
StreamTransport,
|
|
1319
|
+
Topic,
|
|
1320
|
+
UnauthorizedError,
|
|
1321
|
+
VQSClient,
|
|
1322
|
+
createTopic,
|
|
1323
|
+
getVercelOidcToken,
|
|
1324
|
+
parseCallbackRequest
|
|
1325
|
+
});
|
|
1326
|
+
//# sourceMappingURL=index.js.map
|