Spaces:
Sleeping
Sleeping
| // @ts-check | |
| import { CustomError } from "./error.js"; | |
| import { logger } from "./log.js"; | |
| // Script variables. | |
| // Count the number of GitHub API tokens available. | |
| const PATs = Object.keys(process.env).filter((key) => | |
| /PAT_\d*$/.exec(key), | |
| ).length; | |
| const RETRIES = process.env.NODE_ENV === "test" ? 7 : PATs; | |
| /** | |
| * @typedef {import("axios").AxiosResponse} AxiosResponse Axios response. | |
| * @typedef {(variables: any, token: string, retriesForTests?: number) => Promise<AxiosResponse>} FetcherFunction Fetcher function. | |
| */ | |
| /** | |
| * Try to execute the fetcher function until it succeeds or the max number of retries is reached. | |
| * | |
| * @param {FetcherFunction} fetcher The fetcher function. | |
| * @param {any} variables Object with arguments to pass to the fetcher function. | |
| * @param {number} retries How many times to retry. | |
| * @returns {Promise<any>} The response from the fetcher function. | |
| */ | |
| const retryer = async (fetcher, variables, retries = 0) => { | |
| if (!RETRIES) { | |
| throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS); | |
| } | |
| if (retries > RETRIES) { | |
| throw new CustomError( | |
| "Downtime due to GitHub API rate limiting", | |
| CustomError.MAX_RETRY, | |
| ); | |
| } | |
| try { | |
| // try to fetch with the first token since RETRIES is 0 index i'm adding +1 | |
| let response = await fetcher( | |
| variables, | |
| // @ts-ignore | |
| process.env[`PAT_${retries + 1}`], | |
| // used in tests for faking rate limit | |
| retries, | |
| ); | |
| // react on both type and message-based rate-limit signals. | |
| // https://github.com/anuraghazra/github-readme-stats/issues/4425 | |
| const errors = response?.data?.errors; | |
| const errorType = errors?.[0]?.type; | |
| const errorMsg = errors?.[0]?.message || ""; | |
| const isRateLimited = | |
| (errors && errorType === "RATE_LIMITED") || /rate limit/i.test(errorMsg); | |
| // if rate limit is hit increase the RETRIES and recursively call the retryer | |
| // with username, and current RETRIES | |
| if (isRateLimited) { | |
| logger.log(`PAT_${retries + 1} Failed`); | |
| retries++; | |
| // directly return from the function | |
| return retryer(fetcher, variables, retries); | |
| } | |
| // finally return the response | |
| return response; | |
| } catch (err) { | |
| /** @type {any} */ | |
| const e = err; | |
| // network/unexpected error → let caller treat as failure | |
| if (!e?.response) { | |
| throw e; | |
| } | |
| // prettier-ignore | |
| // also checking for bad credentials if any tokens gets invalidated | |
| const isBadCredential = | |
| e?.response?.data?.message === "Bad credentials"; | |
| const isAccountSuspended = | |
| e?.response?.data?.message === "Sorry. Your account was suspended."; | |
| if (isBadCredential || isAccountSuspended) { | |
| logger.log(`PAT_${retries + 1} Failed`); | |
| retries++; | |
| // directly return from the function | |
| return retryer(fetcher, variables, retries); | |
| } | |
| // HTTP error with a response → return it for caller-side handling | |
| return e.response; | |
| } | |
| }; | |
| export { retryer, RETRIES }; | |
| export default retryer; | |