Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shared): introduce retryable and throwable to fetch-utils #2921

Merged
merged 7 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)));
kuitos marked this conversation as resolved.
Show resolved Hide resolved

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';
Loading