@vercel/queue 0.3.0 → 0.3.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/dist/index.mjs CHANGED
@@ -120,6 +120,15 @@ var MessageLockedError = class extends Error {
120
120
  this.retryAfter = retryAfter;
121
121
  }
122
122
  };
123
+ var TooManyRequestsError = class extends Error {
124
+ /** Suggested retry delay in seconds, from the Retry-After header, if sent. */
125
+ retryAfter;
126
+ constructor(message = "Too many requests", retryAfter) {
127
+ super(message);
128
+ this.name = "TooManyRequestsError";
129
+ this.retryAfter = retryAfter;
130
+ }
131
+ };
123
132
  var UnauthorizedError = class extends Error {
124
133
  constructor(message = "Missing or invalid authentication token") {
125
134
  super(message);
@@ -185,6 +194,8 @@ var MIN_VISIBILITY_TIMEOUT_SECONDS = 30;
185
194
  var MAX_RENEWAL_INTERVAL_SECONDS = 60;
186
195
  var MIN_RENEWAL_INTERVAL_SECONDS = 10;
187
196
  var RETRY_INTERVAL_MS = 3e3;
197
+ var DIRECTIVE_CALL_ATTEMPTS = 3;
198
+ var DIRECTIVE_CALL_RETRY_DELAY_MS = 250;
188
199
  function calculateRenewalInterval(visibilityTimeoutSeconds) {
189
200
  return Math.min(
190
201
  MAX_RENEWAL_INTERVAL_SECONDS,
@@ -228,6 +239,54 @@ var ConsumerGroup = class {
228
239
  error instanceof UnauthorizedError || // 401 - auth failed
229
240
  error instanceof ForbiddenError;
230
241
  }
242
+ /**
243
+ * Network-level failures (DNS, connection reset, socket close) surface
244
+ * from fetch as TypeError with the cause attached; any response that
245
+ * actually reached the server — whatever its HTTP status — does not.
246
+ */
247
+ isNetworkError(error) {
248
+ return error instanceof TypeError;
249
+ }
250
+ /**
251
+ * Run a directive call (acknowledge / changeVisibility) with bounded
252
+ * retries. Only failures that are worth re-attempting in process are
253
+ * retried:
254
+ * - network-level failures (the request may never have reached the
255
+ * server), with jittered linear backoff;
256
+ * - 429 responses that carry a Retry-After header, waiting the
257
+ * indicated delay.
258
+ * Everything else — other 4xx, 5xx, 429 without Retry-After — is
259
+ * thrown immediately.
260
+ */
261
+ async directiveCallWithRetries(fn) {
262
+ let lastError;
263
+ for (let attempt = 1; attempt <= DIRECTIVE_CALL_ATTEMPTS; attempt++) {
264
+ try {
265
+ return await fn();
266
+ } catch (error) {
267
+ lastError = error;
268
+ if (attempt === DIRECTIVE_CALL_ATTEMPTS) {
269
+ throw error;
270
+ }
271
+ if (error instanceof TooManyRequestsError) {
272
+ if (error.retryAfter === void 0) {
273
+ throw error;
274
+ }
275
+ await new Promise(
276
+ (resolve2) => setTimeout(resolve2, error.retryAfter * 1e3)
277
+ );
278
+ continue;
279
+ }
280
+ if (!this.isNetworkError(error)) {
281
+ throw error;
282
+ }
283
+ const baseDelayMs = DIRECTIVE_CALL_RETRY_DELAY_MS * attempt;
284
+ const delayMs = baseDelayMs / 2 + Math.random() * (baseDelayMs / 2);
285
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
286
+ }
287
+ }
288
+ throw lastError;
289
+ }
231
290
  /**
232
291
  * Starts a background loop that periodically extends the visibility timeout for a message.
233
292
  *
@@ -372,11 +431,13 @@ var ConsumerGroup = class {
372
431
  if (directive) {
373
432
  if ("acknowledge" in directive && directive.acknowledge) {
374
433
  try {
375
- await this.client.acknowledgeMessage({
376
- queueName: this.topicName,
377
- consumerGroup: this.consumerGroupName,
378
- receiptHandle: message.receiptHandle
379
- });
434
+ await this.directiveCallWithRetries(
435
+ () => this.client.acknowledgeMessage({
436
+ queueName: this.topicName,
437
+ consumerGroup: this.consumerGroupName,
438
+ receiptHandle: message.receiptHandle
439
+ })
440
+ );
380
441
  } catch (ackError) {
381
442
  console.warn("Failed to acknowledge message:", ackError);
382
443
  }
@@ -385,12 +446,14 @@ var ConsumerGroup = class {
385
446
  }
386
447
  if ("afterSeconds" in directive && typeof directive.afterSeconds === "number") {
387
448
  try {
388
- await this.client.changeVisibility({
389
- queueName: this.topicName,
390
- consumerGroup: this.consumerGroupName,
391
- receiptHandle: message.receiptHandle,
392
- visibilityTimeoutSeconds: directive.afterSeconds
393
- });
449
+ await this.directiveCallWithRetries(
450
+ () => this.client.changeVisibility({
451
+ queueName: this.topicName,
452
+ consumerGroup: this.consumerGroupName,
453
+ receiptHandle: message.receiptHandle,
454
+ visibilityTimeoutSeconds: directive.afterSeconds
455
+ })
456
+ );
394
457
  } catch (changeError) {
395
458
  console.warn(
396
459
  "Failed to reschedule message for retry:",
@@ -1452,10 +1515,28 @@ async function consumeStream(stream) {
1452
1515
  reader.releaseLock();
1453
1516
  }
1454
1517
  }
1455
- function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters") {
1518
+ function parseRetryAfterSeconds(value) {
1519
+ if (!value) return void 0;
1520
+ const seconds = Number(value);
1521
+ if (Number.isFinite(seconds) && seconds >= 0) {
1522
+ return seconds;
1523
+ }
1524
+ const dateMs = Date.parse(value);
1525
+ if (!Number.isNaN(dateMs)) {
1526
+ return Math.max(0, (dateMs - Date.now()) / 1e3);
1527
+ }
1528
+ return void 0;
1529
+ }
1530
+ function throwCommonHttpError(status, statusText, errorText, operation, badRequestDefault = "Invalid parameters", retryAfterHeader) {
1456
1531
  if (status === 400) {
1457
1532
  throw new BadRequestError(errorText || badRequestDefault);
1458
1533
  }
1534
+ if (status === 429) {
1535
+ throw new TooManyRequestsError(
1536
+ errorText || `Too many requests: ${operation}`,
1537
+ parseRetryAfterSeconds(retryAfterHeader)
1538
+ );
1539
+ }
1459
1540
  if (status === 401) {
1460
1541
  throw new UnauthorizedError(errorText || void 0);
1461
1542
  }
@@ -1632,7 +1713,7 @@ Cause: ${cause}`
1632
1713
  }
1633
1714
  console.debug("[VQS Debug] Request:", JSON.stringify(logData, null, 2));
1634
1715
  }
1635
- init.headers.set("User-Agent", `@vercel/queue/${"0.3.0"}`);
1716
+ init.headers.set("User-Agent", `@vercel/queue/${"0.3.1"}`);
1636
1717
  init.headers.set("Vqs-Client-Ts", (/* @__PURE__ */ new Date()).toISOString());
1637
1718
  const fetchInit = this.dispatcher ? { ...init, dispatcher: this.dispatcher } : init;
1638
1719
  const response = await fetch(url, fetchInit);
@@ -1911,7 +1992,8 @@ Cause: ${cause}`
1911
1992
  response.statusText,
1912
1993
  errorText,
1913
1994
  "acknowledge message",
1914
- "Missing or invalid receipt handle"
1995
+ "Missing or invalid receipt handle",
1996
+ response.headers?.get("Retry-After") ?? null
1915
1997
  );
1916
1998
  }
1917
1999
  await response.text();
@@ -1963,7 +2045,8 @@ Cause: ${cause}`
1963
2045
  response.statusText,
1964
2046
  errorText,
1965
2047
  "change visibility",
1966
- "Missing receipt handle or invalid visibility timeout"
2048
+ "Missing receipt handle or invalid visibility timeout",
2049
+ response.headers?.get("Retry-After") ?? null
1967
2050
  );
1968
2051
  }
1969
2052
  await response.text();
@@ -2321,6 +2404,7 @@ export {
2321
2404
  QueueClient,
2322
2405
  QueueEmptyError,
2323
2406
  StreamTransport,
2407
+ TooManyRequestsError,
2324
2408
  UnauthorizedError,
2325
2409
  handleCallback2 as handleCallback,
2326
2410
  parseCallback,