diff --git a/apps/site/next-data/generators/majorNodeReleases.mjs b/apps/site/next-data/generators/majorNodeReleases.mjs index 108bc14156160..28cb0dc07e1ed 100644 --- a/apps/site/next-data/generators/majorNodeReleases.mjs +++ b/apps/site/next-data/generators/majorNodeReleases.mjs @@ -2,7 +2,7 @@ import nodevu from '@nodevu/core'; -import { fetchWithRetry } from '#site/util/fetch'; +import { fetchWithRetry } from '#site/next.fetch.mjs'; /** * Filters Node.js release data to return only major releases with documented support. diff --git a/apps/site/next-data/generators/supportersData.mjs b/apps/site/next-data/generators/supportersData.mjs index fc77c4b383e8b..693da939b883f 100644 --- a/apps/site/next-data/generators/supportersData.mjs +++ b/apps/site/next-data/generators/supportersData.mjs @@ -1,5 +1,5 @@ import { OPENCOLLECTIVE_MEMBERS_URL } from '#site/next.constants.mjs'; -import { fetchWithRetry } from '#site/util/fetch'; +import { fetchWithRetry } from '#site/next.fetch.mjs'; /** * Fetches supporters data from Open Collective API, filters active backers, diff --git a/apps/site/next-data/generators/vulnerabilities.mjs b/apps/site/next-data/generators/vulnerabilities.mjs index d461d0f50d15a..cf57006d50d5e 100644 --- a/apps/site/next-data/generators/vulnerabilities.mjs +++ b/apps/site/next-data/generators/vulnerabilities.mjs @@ -1,5 +1,5 @@ import { VULNERABILITIES_URL } from '#site/next.constants.mjs'; -import { fetchWithRetry } from '#site/util/fetch'; +import { fetchWithRetry } from '#site/next.fetch.mjs'; const RANGE_REGEX = /([<>]=?)\s*(\d+)(?:\.(\d+))?/; const V0_REGEX = /^0\.\d+(\.x)?$/; diff --git a/apps/site/next.calendar.mjs b/apps/site/next.calendar.mjs index b7cdb666fcd73..1440e19c63cbe 100644 --- a/apps/site/next.calendar.mjs +++ b/apps/site/next.calendar.mjs @@ -4,7 +4,7 @@ import { BASE_CALENDAR_URL, SHARED_CALENDAR_KEY, } from './next.calendar.constants.mjs'; -import { fetchWithRetry } from './util/fetch'; +import { fetchWithRetry } from './next.fetch.mjs'; /** * diff --git a/apps/site/next.fetch.mjs b/apps/site/next.fetch.mjs new file mode 100644 index 0000000000000..9a6a3250ea2b0 --- /dev/null +++ b/apps/site/next.fetch.mjs @@ -0,0 +1,38 @@ +/** + * @typedef { RequestInit & { maxRetry?: number; delay?: number; }} RetryOptions + */ + +const isTimeoutError = e => + e instanceof Error && + typeof e.cause === 'object' && + e.cause !== null && + 'code' in e.cause && + e.cause.code === 'ETIMEDOUT'; + +const sleep = ms => new Promise(r => setTimeout(r, ms)); + +/** + * Does a fetch with retry logic for network errors and timeouts. + * + * @param {string} url + * @param {RetryOptions} [options] + * @returns {Promise} + */ +export const fetchWithRetry = async ( + url, + { maxRetry = 3, delay = 100, ...options } = {} +) => { + const retries = Math.max(1, Number(maxRetry) || 1); + const backoff = Math.max(0, Number(delay) || 0); + + const attemptFetch = attempt => + fetch(url, options).catch(e => { + if (attempt === retries || !isTimeoutError(e)) { + throw e; + } + + return sleep(backoff * attempt).then(() => attemptFetch(attempt + 1)); + }); + + return attemptFetch(1); +}; diff --git a/apps/site/util/fetch.ts b/apps/site/util/fetch.ts deleted file mode 100644 index b998879503ef1..0000000000000 --- a/apps/site/util/fetch.ts +++ /dev/null @@ -1,33 +0,0 @@ -type RetryOptions = RequestInit & { - maxRetry?: number; - delay?: number; -}; - -const isTimeoutError = (e: unknown): boolean => - e instanceof Error && - typeof e.cause === 'object' && - e.cause !== null && - 'code' in e.cause && - e.cause.code === 'ETIMEDOUT'; - -export const fetchWithRetry = async ( - url: string, - { maxRetry = 3, delay = 100, ...options }: RetryOptions = {} -) => { - for (let i = 1; i <= maxRetry; i++) { - try { - return await fetch(url, options); - } catch (e) { - console.debug( - `fetch of ${url} failed at ${Date.now()}, attempt ${i}/${maxRetry}`, - e - ); - - if (i === maxRetry || !isTimeoutError(e)) { - throw e; - } - - await new Promise(resolve => setTimeout(resolve, delay * i)); - } - } -};