ag-common 0.0.905 → 0.0.906

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.
@@ -1,4 +1,6 @@
1
+ export declare const isRetryableError: (error: unknown) => boolean;
1
2
  export declare const withRetry: <T>(operation: () => Promise<T>, operationName: string, opt?: {
2
3
  /** default 3. null for infinite */
3
4
  maxRetries?: number | null;
5
+ sleep?: (delay: number) => Promise<void>;
4
6
  }) => Promise<T>;
@@ -1,36 +1,120 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.withRetry = void 0;
3
+ exports.withRetry = exports.isRetryableError = void 0;
4
+ const const_1 = require("../const");
4
5
  const log_1 = require("./log");
5
6
  const sleep_1 = require("./sleep");
7
+ const RETRYABLE_ERROR_NAMES = new Set([
8
+ "BandwidthLimitExceeded",
9
+ "EC2ThrottledException",
10
+ "LimitExceededException",
11
+ "PriorRequestNotComplete",
12
+ "ProvisionedThroughputExceededException",
13
+ "RequestLimitExceeded",
14
+ "RequestThrottled",
15
+ "RequestThrottledException",
16
+ "SlowDown",
17
+ "ThrottledException",
18
+ "Throttling",
19
+ "ThrottlingException",
20
+ "TimeoutError",
21
+ "TooManyRequestsException",
22
+ "TransactionInProgressException",
23
+ ].map((name) => name.toLowerCase()));
24
+ const RETRYABLE_NETWORK_CODES = new Set([
25
+ "EAI_AGAIN",
26
+ "ECONNREFUSED",
27
+ "ECONNRESET",
28
+ "EHOSTUNREACH",
29
+ "ENETDOWN",
30
+ "ENETUNREACH",
31
+ "EPIPE",
32
+ "ETIMEDOUT",
33
+ "UND_ERR_CONNECT_TIMEOUT",
34
+ "UND_ERR_HEADERS_TIMEOUT",
35
+ ].map((code) => code.toLowerCase()));
36
+ const RETRYABLE_MESSAGE_PATTERNS = [
37
+ /\bthrottl(?:e|ed|ing|ingexception)\b/i,
38
+ /\bprovisioned\s*throughput\s*exceeded\b/i,
39
+ /\bthroughput\s*exceeds\b/i,
40
+ /\brate[\s-]*limit(?:ed|ing)?\b/i,
41
+ /\btoo\s*many\s*requests\b/i,
42
+ /\btoo\s*large\b/i,
43
+ ];
44
+ const HTTP_STATUS_MESSAGE_PATTERN = new RegExp(`\\b(?:http(?:\\s+error)?|status(?:\\s*code)?)[^0-9]{0,16}(?:${const_1.retryHttpCodes.join("|")})\\b`, "i");
45
+ const isRecord = (value) => typeof value === "object" && value !== null;
46
+ const isRetryableStatus = (value) => {
47
+ const status = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
48
+ return typeof status === "number" && const_1.retryHttpCodes.includes(status);
49
+ };
50
+ const getErrorText = (error, record) => {
51
+ if (typeof error === "string")
52
+ return error;
53
+ const message = typeof record?.message === "string" ? record.message : "";
54
+ const name = typeof record?.name === "string" ? record.name : "";
55
+ return `${name} ${message}`.trim();
56
+ };
57
+ const isRetryableErrorInternal = (error, seen) => {
58
+ if (seen.has(error))
59
+ return false;
60
+ seen.add(error);
61
+ if (isRetryableStatus(error))
62
+ return true;
63
+ const record = isRecord(error) ? error : undefined;
64
+ const retryable = isRecord(record?.$retryable) ? record.$retryable : undefined;
65
+ if (retryable?.throttling === true)
66
+ return true;
67
+ const metadata = isRecord(record?.$metadata) ? record.$metadata : undefined;
68
+ const response = isRecord(record?.response) ? record.response : undefined;
69
+ if ([
70
+ record?.code,
71
+ record?.status,
72
+ record?.statusCode,
73
+ metadata?.httpStatusCode,
74
+ response?.status,
75
+ response?.statusCode,
76
+ ].some(isRetryableStatus)) {
77
+ return true;
78
+ }
79
+ const errorName = typeof record?.name === "string" ? record.name.toLowerCase() : "";
80
+ const errorCode = typeof record?.code === "string" ? record.code.toLowerCase() : "";
81
+ if (RETRYABLE_ERROR_NAMES.has(errorName) ||
82
+ RETRYABLE_ERROR_NAMES.has(errorCode) ||
83
+ RETRYABLE_NETWORK_CODES.has(errorCode)) {
84
+ return true;
85
+ }
86
+ const errorText = getErrorText(error, record);
87
+ if (/^429$/i.test(errorText.trim()) ||
88
+ HTTP_STATUS_MESSAGE_PATTERN.test(errorText) ||
89
+ RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(errorText))) {
90
+ return true;
91
+ }
92
+ return record?.cause !== undefined ? isRetryableErrorInternal(record.cause, seen) : false;
93
+ };
94
+ const isRetryableError = (error) => isRetryableErrorInternal(error, new Set());
95
+ exports.isRetryableError = isRetryableError;
6
96
  const withRetry = async (operation, operationName, opt) => {
7
97
  let retryCount = 0;
8
98
  const baseDelay = 2000;
9
- const { maxRetries = 3 } = opt ?? {};
99
+ const { maxRetries = 3, sleep: sleepFor = sleep_1.sleep } = opt ?? {};
10
100
  for (;;) {
11
101
  try {
12
102
  // oxlint-disable-next-line no-await-in-loop -- retries are intentionally sequential
13
- return operation();
103
+ const result = await operation();
104
+ return result;
14
105
  }
15
- catch (e) {
16
- const error = e;
17
- const errorString = error.toString().toLowerCase().replace(/\s+/gim, "");
18
- if (errorString.includes("429") ||
19
- errorString.includes("provisionedthroughputexceeded") ||
20
- errorString.includes("toolarge") ||
21
- errorString.includes("ratelimited")) {
22
- retryCount++;
23
- if (maxRetries !== null && retryCount >= maxRetries) {
24
- (0, log_1.warn)(`${operationName}: Max retries exceeded`);
25
- throw error;
26
- }
27
- const delay = baseDelay + retryCount * 1000;
28
- (0, log_1.warn)(`${operationName}: Throttled. Retry ${retryCount}. Sleeping for ${delay}ms`);
29
- // oxlint-disable-next-line no-await-in-loop -- backoff must complete before retrying
30
- await (0, sleep_1.sleep)(delay);
31
- continue;
106
+ catch (error) {
107
+ if (!(0, exports.isRetryableError)(error))
108
+ throw error;
109
+ if (maxRetries !== null && retryCount >= maxRetries) {
110
+ (0, log_1.warn)(`${operationName}: Max retries exceeded`);
111
+ throw error;
32
112
  }
33
- throw error;
113
+ retryCount++;
114
+ const delay = baseDelay + retryCount * 1000;
115
+ (0, log_1.warn)(`${operationName}: Retryable failure. Retry ${retryCount}. Sleeping for ${delay}ms`);
116
+ // oxlint-disable-next-line no-await-in-loop -- backoff must complete before retrying
117
+ await sleepFor(delay);
34
118
  }
35
119
  }
36
120
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ag-common",
3
- "version": "0.0.905",
3
+ "version": "0.0.906",
4
4
  "license": "ISC",
5
5
  "author": "admin@gec.dev",
6
6
  "repository": {