// @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} 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} 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;