Skip to content

Commit

Permalink
feat(shared): introduce retryable and throwable to fetch-utils (#2921)
Browse files Browse the repository at this point in the history
  • Loading branch information
kuitos authored Mar 6, 2024
1 parent 7d77699 commit ea18ce6
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 22 deletions.
6 changes: 6 additions & 0 deletions .changeset/lemon-seals-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"qiankun": patch
"@qiankunjs/shared": patch
---

feat(shared): introduce retryable and throwable to fetch-utils
5 changes: 4 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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' }],
Expand Down
12 changes: 7 additions & 5 deletions packages/qiankun/src/core/loadApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,7 +50,7 @@ export default async function loadApp<T extends ObjectType>(
...restConfiguration
} = configuration || {};

const fetchWithLruCache = wrapFetchWithCache(fetch);
const enhancedFetch = makeFetchCacheable(makeFetchRetryable(makeFetchThrowable(fetch)));

const markName = `[qiankun] App ${appName} Loading`;
if (process.env.NODE_ENV === 'development') {
Expand All @@ -69,7 +71,7 @@ export default async function loadApp<T extends ObjectType>(
const sandboxContainer = createSandboxContainer(appName, () => microAppDOMContainer, {
globalContext,
extraGlobals: {},
fetch: fetchWithLruCache,
fetch: enhancedFetch,
nodeTransformer,
});

Expand All @@ -85,7 +87,7 @@ export default async function loadApp<T extends ObjectType>(
}

const containerOpts: LoaderOpts = {
fetch: fetchWithLruCache,
fetch: enhancedFetch,
sandbox: sandboxInstance,
nodeTransformer,
...restConfiguration,
Expand Down Expand Up @@ -139,7 +141,7 @@ export default async function loadApp<T extends ObjectType>(
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,
Expand Down
4 changes: 2 additions & 2 deletions packages/shared/src/assets-transpilers/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// @vitest-environment edge-runtime

import { expect, it, vi } from 'vitest';
import { wrapFetchWithCache } from '../wrapFetchWithCache';
import { makeFetchCacheable } from '../makeFetchCacheable';

const slogan = 'Hello Qiankun 3.0';

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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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');
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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<Fetch>[0]): string => {
return typeof input === 'string' ? input : 'url' in input ? input.url : input.href;
Expand All @@ -15,11 +15,7 @@ const getGlobalCache = once(() => {
return new LRUCache<string, Promise<Response>>(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) => {
Expand All @@ -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);
}

Expand Down
34 changes: 34 additions & 0 deletions packages/shared/src/fetch-utils/makeFetchRetryable.ts
Original file line number Diff line number Diff line change
@@ -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;
};
22 changes: 22 additions & 0 deletions packages/shared/src/fetch-utils/makeFetchThrowable.ts
Original file line number Diff line number Diff line change
@@ -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;
};
};
5 changes: 5 additions & 0 deletions packages/shared/src/fetch-utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Fetch = typeof window.fetch;

export const isValidResponse = (status: number): boolean => {
return status >= 200 && status < 400;
};
4 changes: 3 additions & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

0 comments on commit ea18ce6

Please sign in to comment.