From ea18ce6cf9417dab8b362d9271f1bdfe567d0303 Mon Sep 17 00:00:00 2001 From: Kuitos Date: Wed, 6 Mar 2024 14:29:46 +0800 Subject: [PATCH] feat(shared): introduce retryable and throwable to fetch-utils (#2921) --- .changeset/lemon-seals-juggle.md | 6 ++++ .eslintrc.cjs | 5 ++- packages/qiankun/src/core/loadApp.ts | 12 ++++--- .../shared/src/assets-transpilers/script.ts | 4 +-- ...che.test.ts => makeFetchCacheable.test.ts} | 10 +++--- .../__tests__/makeFetchRetryable.test.ts | 33 ++++++++++++++++++ .../__tests__/makeFetchThrowable.test.ts | 31 +++++++++++++++++ ...etchWithCache.ts => makeFetchCacheable.ts} | 12 +++---- .../src/fetch-utils/makeFetchRetryable.ts | 34 +++++++++++++++++++ .../src/fetch-utils/makeFetchThrowable.ts | 22 ++++++++++++ packages/shared/src/fetch-utils/utils.ts | 5 +++ packages/shared/src/index.ts | 4 ++- 12 files changed, 156 insertions(+), 22 deletions(-) create mode 100644 .changeset/lemon-seals-juggle.md rename packages/shared/src/fetch-utils/__tests__/{wrapFetchWithCache.test.ts => makeFetchCacheable.test.ts} (88%) create mode 100644 packages/shared/src/fetch-utils/__tests__/makeFetchRetryable.test.ts create mode 100644 packages/shared/src/fetch-utils/__tests__/makeFetchThrowable.test.ts rename packages/shared/src/fetch-utils/{wrapFetchWithCache.ts => makeFetchCacheable.ts} (84%) create mode 100644 packages/shared/src/fetch-utils/makeFetchRetryable.ts create mode 100644 packages/shared/src/fetch-utils/makeFetchThrowable.ts create mode 100644 packages/shared/src/fetch-utils/utils.ts diff --git a/.changeset/lemon-seals-juggle.md b/.changeset/lemon-seals-juggle.md new file mode 100644 index 000000000..207726444 --- /dev/null +++ b/.changeset/lemon-seals-juggle.md @@ -0,0 +1,6 @@ +--- +"qiankun": patch +"@qiankunjs/shared": patch +--- + +feat(shared): introduce retryable and throwable to fetch-utils diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 35ab71ba7..ca73ff44a 100755 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -25,7 +25,10 @@ const tsConfig = { rules: { '@typescript-eslint/no-unnecessary-condition': 'error', '@typescript-eslint/no-explicit-any': ['error', { fixToUnknown: true }], - '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], '@typescript-eslint/consistent-type-exports': ['error', { fixMixedExportsWithInlineTypeSpecifier: true }], '@typescript-eslint/require-await': 'off', '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], diff --git a/packages/qiankun/src/core/loadApp.ts b/packages/qiankun/src/core/loadApp.ts index aa635cfc6..980a89007 100644 --- a/packages/qiankun/src/core/loadApp.ts +++ b/packages/qiankun/src/core/loadApp.ts @@ -9,10 +9,12 @@ import { createSandboxContainer, nativeGlobal } from '@qiankunjs/sandbox'; import { defineProperty, hasOwnProperty, + makeFetchCacheable, + makeFetchRetryable, + makeFetchThrowable, moduleResolver as defaultModuleResolver, transpileAssets, warn, - wrapFetchWithCache, } from '@qiankunjs/shared'; import { concat, isFunction, mergeWith } from 'lodash'; import type { ParcelConfigObject } from 'single-spa'; @@ -48,7 +50,7 @@ export default async function loadApp( ...restConfiguration } = configuration || {}; - const fetchWithLruCache = wrapFetchWithCache(fetch); + const enhancedFetch = makeFetchCacheable(makeFetchRetryable(makeFetchThrowable(fetch))); const markName = `[qiankun] App ${appName} Loading`; if (process.env.NODE_ENV === 'development') { @@ -69,7 +71,7 @@ export default async function loadApp( const sandboxContainer = createSandboxContainer(appName, () => microAppDOMContainer, { globalContext, extraGlobals: {}, - fetch: fetchWithLruCache, + fetch: enhancedFetch, nodeTransformer, }); @@ -85,7 +87,7 @@ export default async function loadApp( } const containerOpts: LoaderOpts = { - fetch: fetchWithLruCache, + fetch: enhancedFetch, sandbox: sandboxInstance, nodeTransformer, ...restConfiguration, @@ -139,7 +141,7 @@ export default async function loadApp( if (mountTimes > 1) { initContainer(mountContainer, appName, { sandboxCfg: sandbox, mountTimes, instanceId }); // html scripts should be removed to avoid repeatedly execute - const htmlString = await getPureHTMLStringWithoutScripts(entry, fetchWithLruCache); + const htmlString = await getPureHTMLStringWithoutScripts(entry, enhancedFetch); await loadEntry( { url: entry, res: new Response(htmlString, { status: 200, statusText: 'OK' }) }, mountContainer, diff --git a/packages/shared/src/assets-transpilers/script.ts b/packages/shared/src/assets-transpilers/script.ts index 7ee2512b4..00c86d046 100644 --- a/packages/shared/src/assets-transpilers/script.ts +++ b/packages/shared/src/assets-transpilers/script.ts @@ -112,7 +112,7 @@ export default function transpileScript( const codeFactory = beforeExecutedListenerScript + sandbox!.makeEvaluateFactory(code, src); if (syncMode) { - // if it's a sync script and there is a previous sync script, we should wait it until loaded to consistent with the browser behavior + // if it's a sync script and there is a previous sync script(mainly there are multiple defer scripts), we should wait it until loaded to consistent with the browser behavior if (prevScriptTranspiledDeferred && !prevScriptTranspiledDeferred.isSettled()) { await waitUntilSettled(prevScriptTranspiledDeferred.promise); } @@ -121,7 +121,7 @@ export default function transpileScript( script.fetchPriority = 'high'; } - // change the script src to the blob url to make it execute in the sandbox + // change the script src to the blob url to make it executed in the sandbox script.src = URL.createObjectURL(new Blob([codeFactory], { type: 'text/javascript' })); window.addEventListener(beforeScriptExecuteEvent, function listener(evt: CustomEventInit) { diff --git a/packages/shared/src/fetch-utils/__tests__/wrapFetchWithCache.test.ts b/packages/shared/src/fetch-utils/__tests__/makeFetchCacheable.test.ts similarity index 88% rename from packages/shared/src/fetch-utils/__tests__/wrapFetchWithCache.test.ts rename to packages/shared/src/fetch-utils/__tests__/makeFetchCacheable.test.ts index 74e14efc2..a0f5b0210 100644 --- a/packages/shared/src/fetch-utils/__tests__/wrapFetchWithCache.test.ts +++ b/packages/shared/src/fetch-utils/__tests__/makeFetchCacheable.test.ts @@ -1,7 +1,7 @@ // @vitest-environment edge-runtime import { expect, it, vi } from 'vitest'; -import { wrapFetchWithCache } from '../wrapFetchWithCache'; +import { makeFetchCacheable } from '../makeFetchCacheable'; const slogan = 'Hello Qiankun 3.0'; @@ -9,7 +9,7 @@ it('should just call fetch once while multiple request invoked parallel', () => const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 200, statusText: 'OK' })); }); - const wrappedFetch = wrapFetchWithCache(fetch); + const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://success.qiankun.org'; wrappedFetch(url); wrappedFetch(url); @@ -22,7 +22,7 @@ it('should support read response body as a stream multi times', async () => { const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 200, statusText: 'OK' })); }); - const wrappedFetch = wrapFetchWithCache(fetch); + const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://stream.qiankun.org'; const response1 = await wrappedFetch(url); @@ -43,7 +43,7 @@ it('should clear cache while respond error with invalid status code', async () = const fetch = vi.fn(() => { return Promise.resolve(new Response(slogan, { status: 400 })); }); - const wrappedFetch = wrapFetchWithCache(fetch); + const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://errorStatusCode.qiankun.org'; const response1 = await wrappedFetch(url); @@ -61,7 +61,7 @@ it('should clear cache while respond error', async () => { const fetch = vi.fn(() => { return Promise.reject(new Error('error')); }); - const wrappedFetch = wrapFetchWithCache(fetch); + const wrappedFetch = makeFetchCacheable(fetch); const url = 'https://error.qiankun.org'; await expect(wrappedFetch(url)).rejects.toThrow('error'); diff --git a/packages/shared/src/fetch-utils/__tests__/makeFetchRetryable.test.ts b/packages/shared/src/fetch-utils/__tests__/makeFetchRetryable.test.ts new file mode 100644 index 000000000..e617c7728 --- /dev/null +++ b/packages/shared/src/fetch-utils/__tests__/makeFetchRetryable.test.ts @@ -0,0 +1,33 @@ +// @vitest-environment edge-runtime +import { expect, it, vi } from 'vitest'; +import { makeFetchRetryable } from '../makeFetchRetryable'; + +const slogan = 'Hello Qiankun 3.0'; + +it('should retry automatically while fetch throw error', async () => { + const retryTimes = 3; + let count = 0; + const fetch = vi.fn(() => { + if (count < retryTimes) { + count++; + throw new Error('network error'); + } + return Promise.resolve(new Response(slogan, { status: 201 })); + }); + const wrappedFetch = makeFetchRetryable(fetch, retryTimes); + const url = 'https://success.qiankun.org'; + const res = await wrappedFetch(url); + expect(res.status).toBe(201); + expect(fetch).toHaveBeenCalledTimes(4); +}); + +it('should work well while response status is 200', async () => { + const fetch = vi.fn(() => { + return Promise.resolve(new Response(slogan, { status: 200 })); + }); + const wrappedFetch = makeFetchRetryable(fetch); + const url = 'https://success.qiankun.org'; + const res = await wrappedFetch(url); + expect(res.status).toBe(200); + expect(fetch).toHaveBeenCalledTimes(1); +}); diff --git a/packages/shared/src/fetch-utils/__tests__/makeFetchThrowable.test.ts b/packages/shared/src/fetch-utils/__tests__/makeFetchThrowable.test.ts new file mode 100644 index 000000000..c4f9c5479 --- /dev/null +++ b/packages/shared/src/fetch-utils/__tests__/makeFetchThrowable.test.ts @@ -0,0 +1,31 @@ +/** + * @author Kuitos + * @since 2024-03-05 + */ +import { expect, it, vi } from 'vitest'; +import { makeFetchThrowable } from '../makeFetchThrowable'; + +const slogan = 'Hello Qiankun 3.0'; + +it('should throw error while response status is not 200~400', async () => { + const fetch = vi.fn(() => { + return Promise.resolve(new Response(slogan, { status: 400 })); + }); + const wrappedFetch = makeFetchThrowable(fetch); + const url = 'https://success.qiankun.org'; + try { + await wrappedFetch(url); + } catch (e) { + expect((e as unknown as Error).message).include('RESPONSE_ERROR_AS_STATUS_INVALID'); + } +}); + +it('should work well while response status is 200', async () => { + const fetch = vi.fn(() => { + return Promise.resolve(new Response(slogan, { status: 200 })); + }); + const wrappedFetch = makeFetchThrowable(fetch); + const url = 'https://success.qiankun.org'; + const res = await wrappedFetch(url); + expect(res.status).toBe(200); +}); diff --git a/packages/shared/src/fetch-utils/wrapFetchWithCache.ts b/packages/shared/src/fetch-utils/makeFetchCacheable.ts similarity index 84% rename from packages/shared/src/fetch-utils/wrapFetchWithCache.ts rename to packages/shared/src/fetch-utils/makeFetchCacheable.ts index c42d5ab12..cd9eb87fd 100644 --- a/packages/shared/src/fetch-utils/wrapFetchWithCache.ts +++ b/packages/shared/src/fetch-utils/makeFetchCacheable.ts @@ -1,11 +1,11 @@ /** * @author Kuitos * @since 2023-11-06 + * wrap fetch with lru cache */ import { once } from 'lodash'; import { LRUCache } from './miniLruCache'; - -type Fetch = typeof window.fetch; +import { type Fetch, isValidResponse } from './utils'; const getCacheKey = (input: Parameters[0]): string => { return typeof input === 'string' ? input : 'url' in input ? input.url : input.href; @@ -15,11 +15,7 @@ const getGlobalCache = once(() => { return new LRUCache>(50); }); -const isValidaResponse = (status: number): boolean => { - return status >= 200 && status < 400; -}; - -export const wrapFetchWithCache: (fetch: Fetch) => Fetch = (fetch) => { +export const makeFetchCacheable: (fetch: Fetch) => Fetch = (fetch) => { const lruCache = getGlobalCache(); const cachedFetch: Fetch = (input, init) => { @@ -30,7 +26,7 @@ export const wrapFetchWithCache: (fetch: Fetch) => Fetch = (fetch) => { const res = await promise; const { status } = res; - if (!isValidaResponse(status)) { + if (!isValidResponse(status)) { lruCache.delete(cacheKey); } diff --git a/packages/shared/src/fetch-utils/makeFetchRetryable.ts b/packages/shared/src/fetch-utils/makeFetchRetryable.ts new file mode 100644 index 000000000..5bf71b74f --- /dev/null +++ b/packages/shared/src/fetch-utils/makeFetchRetryable.ts @@ -0,0 +1,34 @@ +/** + * @author Kuitos + * @since 2024-03-05 + */ + +import { type Fetch } from './utils'; + +export const makeFetchRetryable: (fetch: Fetch, retryTimes?: number) => Fetch = (fetch, retryTimes = 1) => { + let retryCount = 0; + + const fetchWithRetryable: Fetch = async (input, init) => { + try { + return await fetch(input, init); + } catch (e) { + if (retryCount < retryTimes) { + retryCount++; + + if (process.env.NODE_ENV === 'development') { + console.debug( + `[qiankun] fetch retrying --> url: ${ + typeof input === 'string' ? input : 'url' in input ? input.url : input.href + } , time: ${retryCount}`, + ); + } + + return await fetchWithRetryable(input, init); + } + + throw e; + } + }; + + return fetchWithRetryable; +}; diff --git a/packages/shared/src/fetch-utils/makeFetchThrowable.ts b/packages/shared/src/fetch-utils/makeFetchThrowable.ts new file mode 100644 index 000000000..d4ab0656b --- /dev/null +++ b/packages/shared/src/fetch-utils/makeFetchThrowable.ts @@ -0,0 +1,22 @@ +/** + * @author Kuitos + * @since 2024-03-05 + * wrap fetch to throw error when response status is not 200~400 + */ + +import { type Fetch, isValidResponse } from './utils'; + +export const makeFetchThrowable: (fetch: Fetch) => Fetch = (fetch) => { + return async (url, init) => { + const res = await fetch(url, init); + if (!isValidResponse(res.status)) { + throw new Error( + `[RESPONSE_ERROR_AS_STATUS_INVALID] ${res.status} ${res.statusText} ${ + typeof url === 'string' ? url : 'url' in url ? url.url : url.href + } ${JSON.stringify(init)}`, + ); + } + + return res; + }; +}; diff --git a/packages/shared/src/fetch-utils/utils.ts b/packages/shared/src/fetch-utils/utils.ts new file mode 100644 index 000000000..4345476f3 --- /dev/null +++ b/packages/shared/src/fetch-utils/utils.ts @@ -0,0 +1,5 @@ +export type Fetch = typeof window.fetch; + +export const isValidResponse = (status: number): boolean => { + return status >= 200 && status < 400; +}; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 836bb9340..db11f4ecb 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -7,5 +7,7 @@ export * from './utils'; export * from './module-resolver'; export * from './common'; export * from './reporter'; -export * from './fetch-utils/wrapFetchWithCache'; +export * from './fetch-utils/makeFetchCacheable'; +export * from './fetch-utils/makeFetchRetryable'; +export * from './fetch-utils/makeFetchThrowable'; export * from './deferred-queue';