diff --git a/packages/core/fetch/src/fetch.ts b/packages/core/fetch/src/fetch.ts index 2bc7e5031..10580685d 100644 --- a/packages/core/fetch/src/fetch.ts +++ b/packages/core/fetch/src/fetch.ts @@ -23,6 +23,9 @@ export interface FetchOptions extends RequestInit { /** * A timeout for the fetch request. + * Set `0` for disable it. + * + * Use with cation, you will have memory leak issue in nodejs. * * @default 10_000 ms */ @@ -40,7 +43,7 @@ export interface FetchOptions extends RequestInit { * * @default 1_000 ms */ - retryDelay: number; + retryDelay: number; /** * Simple memory caching for remove duplicate/parallel requests. @@ -113,7 +116,7 @@ const duplicateRequestStorage: Record> = {}; export function fetch(_options: Partial & {url: string}): Promise { const options = _processOptions(_options); logger.logMethodArgs('fetch', {options}); - return _handleRemoveDuplicate(options); + return _handleCacheStrategy(options); } /** @@ -152,7 +155,7 @@ function _processOptions(options: Partial & {url: string}): FetchO } } - if (options.body != null && options.bodyJson != null) { + if (options.bodyJson != null) { options.body = JSON.stringify(options.bodyJson); options.headers = { ...options.headers, @@ -164,10 +167,10 @@ function _processOptions(options: Partial & {url: string}): FetchO } /** - * Handle Remove Duplicates over `_handleCacheStrategy`. + * Handle Remove Duplicates over `_handleRetryPattern`. */ async function _handleRemoveDuplicate(options: FetchOptions): Promise { - if (options.removeDuplicate === 'never') return _handleCacheStrategy(options); + if (options.removeDuplicate === 'never') return _handleRetryPattern(options); logger.logMethod('_handleRemoveDuplicate'); @@ -175,7 +178,7 @@ async function _handleRemoveDuplicate(options: FetchOptions): Promise const firstRequest = duplicateRequestStorage[cacheKey] == null; // We must cache fetch promise without await for handle other parallel requests. - duplicateRequestStorage[cacheKey] ??= _handleCacheStrategy(options); + duplicateRequestStorage[cacheKey] ??= _handleRetryPattern(options); try { // For all requests need to await for clone responses. @@ -197,11 +200,11 @@ async function _handleRemoveDuplicate(options: FetchOptions): Promise } /** - * Handle Cache Strategy over `_handleRetryPattern`. + * Handle Cache Strategy over `_handleRemoveDuplicate`. */ async function _handleCacheStrategy(options: FetchOptions): Promise { if (options.cacheStrategy === 'network_only') { - return _handleRetryPattern(options); + return _handleRemoveDuplicate(options); } // else handle cache strategies! logger.logMethod('_handleCacheStrategy'); @@ -219,7 +222,7 @@ async function _handleCacheStrategy(options: FetchOptions): Promise { case 'cache_first': { const cachedResponse = await cacheStorage.match(request); if (cachedResponse != null) return cachedResponse; - const response = await _handleRetryPattern(options); + const response = await _handleRemoveDuplicate(options); if (response.ok) { cacheStorage.put(request, response.clone()); } @@ -234,7 +237,7 @@ async function _handleCacheStrategy(options: FetchOptions): Promise { case 'network_first': { try { - const networkResponse = await _handleRetryPattern(options); + const networkResponse = await _handleRemoveDuplicate(options); if (networkResponse.ok) { cacheStorage.put(request, networkResponse.clone()); } @@ -249,7 +252,7 @@ async function _handleCacheStrategy(options: FetchOptions): Promise { case 'stale_while_revalidate': { const cachedResponse = await cacheStorage.match(request); - const fetchedResponsePromise = _handleRetryPattern(options); + const fetchedResponsePromise = _handleRemoveDuplicate(options); fetchedResponsePromise.then((networkResponse) => { if (networkResponse.ok) { @@ -264,7 +267,7 @@ async function _handleCacheStrategy(options: FetchOptions): Promise { } default: { - return _handleRetryPattern(options); + return _handleRemoveDuplicate(options); } } } @@ -273,40 +276,30 @@ async function _handleCacheStrategy(options: FetchOptions): Promise { * Handle retry pattern over `_handleTimeout`. */ async function _handleRetryPattern(options: FetchOptions): Promise { - if (!(options.retry >= 1)) return _handleTimeout(options); + if (!(options.retry > 1)) return _handleTimeout(options); logger.logMethod('_handleRetryPattern'); + options.retry--; const externalAbortSignal = options.signal; - const retryFetch = async (): Promise => { - options.retry--; - options.signal = externalAbortSignal; - await _wait(options.retryDelay); - return _handleRetryPattern(options); - }; - try { const response = await _handleTimeout(options); - if (options.retry > 1 && response.status >= 502 && response.status <= 504) { - logger.accident('fetch', 'fetch_not_valid', 'fetch not valid and retry', { - response, - }); - return retryFetch(); + if (response.status >= 500) { + logger.incident('fetch', 'fetch_server_error', 'fetch server error ' + response.status); + throw new Error('fetch_server_error'); } - // else - return response; + + else return response; } - catch (reason) { - if ((reason as Error)?.message === 'fetch_timeout' && options.retry > 1) { - logger.incident('fetch', 'fetch_timeout', 'fetch timeout and retry', { - reason, - }); - return retryFetch(); - } - // else - throw reason; + catch (err) { + logger.accident('fetch', (err as Error)?.name ?? 'fetch_failed', 'fetch failed and retry', {err}); + + await _wait(options.retryDelay); + + options.signal = externalAbortSignal; + return _handleRetryPattern(options); } } @@ -314,9 +307,13 @@ async function _handleRetryPattern(options: FetchOptions): Promise { * It's a wrapper around the browser's `fetch` with timeout. */ function _handleTimeout(options: FetchOptions): Promise { + if (options.timeout === 0) { + return globalThis.fetch(options.url, options); + } + // else logger.logMethod('_handleTimeout'); return new Promise((resolved, reject) => { - // @TODO: AbortController polyfill + // TODO: AbortController polyfill const abortController = new AbortController(); const externalAbortSignal = options.signal; options.signal = abortController.signal; @@ -328,23 +325,23 @@ function _handleTimeout(options: FetchOptions): Promise { if (externalAbortSignal != null) { // Respect external abort signal - externalAbortSignal.addEventListener('abort', () => { - abortController.abort(`external abort signal: ${externalAbortSignal.reason}`); - clearTimeout(timeoutId); - }); + externalAbortSignal.addEventListener('abort', () => abortController.abort(), {once: true}); } - abortController.signal.addEventListener('abort', () => { - logger.incident('fetch', 'fetch_abort_signal', 'fetch abort signal received', { - reason: abortController.signal.reason, - }); - }); + // abortController.signal.addEventListener('abort', () => { + // logger.incident('fetch', 'fetch_abort_signal', 'fetch abort signal received', { + // reason: abortController.signal.reason, + // }); + // }); globalThis .fetch(options.url, options) .then((response) => resolved(response)) .catch((reason) => reject(reason)) - .finally(() => clearTimeout(timeoutId)); + .finally(() => { + delete options.signal; // try to avoid memory leak in nodejs! + clearTimeout(timeoutId); + }); }); }