From e4f823f2a9377522c75c79b2b72a93d311c05ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=95=87?= Date: Fri, 29 Nov 2024 14:54:21 +0800 Subject: [PATCH 1/7] fix: fix that `fromCache` is undefined in cache hit console --- .changeset/itchy-bottles-tan.md | 5 +++++ packages/alova/src/functions/sendRequest.ts | 3 ++- packages/alova/test/browser/global/createAlova.spec.ts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/itchy-bottles-tan.md diff --git a/.changeset/itchy-bottles-tan.md b/.changeset/itchy-bottles-tan.md new file mode 100644 index 00000000..e8c984cf --- /dev/null +++ b/.changeset/itchy-bottles-tan.md @@ -0,0 +1,5 @@ +--- +'alova': patch +--- + +fix that `fromCache` is undefined in cache hit console diff --git a/packages/alova/src/functions/sendRequest.ts b/packages/alova/src/functions/sendRequest.ts index 8d100864..8c3b1ecd 100644 --- a/packages/alova/src/functions/sendRequest.ts +++ b/packages/alova/src/functions/sendRequest.ts @@ -111,7 +111,8 @@ export default function sendRequest(methodInstance: Me requestAdapterCtrlsPromiseResolveFn(); // Ctrls will not be passed in when cache is encountered // Print cache log - sloughFunction(cacheLogger, defaultCacheLogger)(cachedResponse, clonedMethod as any, cacheMode, tag); + clonedMethod.fromCache = trueValue; + sloughFunction(cacheLogger, defaultCacheLogger)(cachedResponse, clonedMethod, cacheMode, tag); responseCompleteHandler(clonedMethod); return cachedResponse; } diff --git a/packages/alova/test/browser/global/createAlova.spec.ts b/packages/alova/test/browser/global/createAlova.spec.ts index befbed9b..1e5a1d1f 100644 --- a/packages/alova/test/browser/global/createAlova.spec.ts +++ b/packages/alova/test/browser/global/createAlova.spec.ts @@ -462,6 +462,7 @@ describe('createAlova', () => { expect(calls1[1][2]).toBe('memory'); expect(calls1[2][0]).toBe('%c[Method]'); expect(calls1[2][2]).toBeInstanceOf(Method); + expect(calls1[2][2].fromCache).toBeTruthy(); const getter2 = alova1.Get('/unit-test', { params: { restore: '1' }, From 371666bd4974bd7fb19a7adc22802c477160b910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=95=87?= Date: Fri, 29 Nov 2024 15:02:44 +0800 Subject: [PATCH 2/7] test: add test of disabled global `cacheFor` --- .../test/browser/behavior/l1Cache.spec.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/alova/test/browser/behavior/l1Cache.spec.ts b/packages/alova/test/browser/behavior/l1Cache.spec.ts index 6d0b3171..e88cfe54 100644 --- a/packages/alova/test/browser/behavior/l1Cache.spec.ts +++ b/packages/alova/test/browser/behavior/l1Cache.spec.ts @@ -20,6 +20,64 @@ describe('l1cache cache data', () => { await Post; expect(await queryCache(Post)).toBeUndefined(); }); + + test('should disable the global cache', async () => { + const alova1 = getAlovaInstance({ + responseExpect: r => r.json(), + cacheFor: null + }); + + // disable GET cache + const Get1 = alova1.Get('/unit-test', { + transform: ({ data }: Result) => data + }); + await Get1; + expect(await queryCache(Get1)).toBeUndefined(); + + // disable POST cache + const Post1 = alova1.Post('/unit-test'); + await Post1; + expect(await queryCache(Post1)).toBeUndefined(); + + const alova2 = getAlovaInstance({ + responseExpect: r => r.json(), + cacheFor: { + GET: 0 + } + }); + + // disable GET cache + const Get2 = alova2.Get('/unit-test', { + transform: ({ data }: Result) => data + }); + await Get2; + expect(await queryCache(Get2)).toBeUndefined(); + + // disable POST cache + const Post2 = alova2.Post('/unit-test'); + await Post2; + expect(await queryCache(Post2)).toBeUndefined(); + + const alova3 = getAlovaInstance({ + responseExpect: r => r.json(), + cacheFor: { + GET: null + } + }); + + // disable GET cache + const Get3 = alova3.Get('/unit-test', { + transform: ({ data }: Result) => data + }); + await Get3; + expect(await queryCache(Get3)).toBeUndefined(); + + // disable POST cache + const Post3 = alova3.Post('/unit-test'); + await Post3; + expect(await queryCache(Post3)).toBeUndefined(); + }); + test("change the default cache's setting globally", async () => { const alova = getAlovaInstance({ cacheFor: { From 8d663b67489933488b354258e97cbcb41e6b2ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=95=87?= Date: Fri, 29 Nov 2024 16:59:58 +0800 Subject: [PATCH 3/7] docs: correct default `multiplier` 0 to 1 in comment --- .changeset/ten-lies-wave.md | 5 ++++ .../clienthook/hooks/useSQRequest.d.ts | 2 +- .../clienthook/hooks/useSQRequest.d.ts | 2 +- packages/shared/src/types.ts | 2 +- packages/shared/typings/alova-shared.d.ts | 26 +++++++++---------- 5 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 .changeset/ten-lies-wave.md diff --git a/.changeset/ten-lies-wave.md b/.changeset/ten-lies-wave.md new file mode 100644 index 00000000..84d61abe --- /dev/null +++ b/.changeset/ten-lies-wave.md @@ -0,0 +1,5 @@ +--- +'alova': patch +--- + +correct default `multiplier` 0 to 1 in comment diff --git a/packages/alova/typings/clienthook/hooks/useSQRequest.d.ts b/packages/alova/typings/clienthook/hooks/useSQRequest.d.ts index 88a4def2..a38e6882 100644 --- a/packages/alova/typings/clienthook/hooks/useSQRequest.d.ts +++ b/packages/alova/typings/clienthook/hooks/useSQRequest.d.ts @@ -142,7 +142,7 @@ export interface BackoffPolicy { delay?: number; /** * Specify the delay multiplier. For example, if the multiplier is set to 1.5 and the delay is 2 seconds, the first retry will be 2 seconds, the second retry will be 3 seconds, and the third retry will be 4.5 seconds. - * @default 0 + * @default 1 */ multiplier?: number; diff --git a/packages/client/typings/clienthook/hooks/useSQRequest.d.ts b/packages/client/typings/clienthook/hooks/useSQRequest.d.ts index 88a4def2..a38e6882 100644 --- a/packages/client/typings/clienthook/hooks/useSQRequest.d.ts +++ b/packages/client/typings/clienthook/hooks/useSQRequest.d.ts @@ -142,7 +142,7 @@ export interface BackoffPolicy { delay?: number; /** * Specify the delay multiplier. For example, if the multiplier is set to 1.5 and the delay is 2 seconds, the first retry will be 2 seconds, the second retry will be 3 seconds, and the third retry will be 4.5 seconds. - * @default 0 + * @default 1 */ multiplier?: number; diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 708b51a9..7efac4d4 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -35,7 +35,7 @@ export interface BackoffPolicy { delay?: number; /** * Specify the delay multiple. For example, if the multiplier is set to 1.5 and the delay is 2 seconds, the first retry will be 2 seconds, the second will be 3 seconds, and the third will be 4.5 seconds - * @default 0 + * @default 1 */ multiplier?: number; diff --git a/packages/shared/typings/alova-shared.d.ts b/packages/shared/typings/alova-shared.d.ts index a1557964..bd733fec 100644 --- a/packages/shared/typings/alova-shared.d.ts +++ b/packages/shared/typings/alova-shared.d.ts @@ -69,7 +69,7 @@ interface BackoffPolicy { delay?: number; /** * Specify the delay multiple. For example, if the multiplier is set to 1.5 and the delay is 2 seconds, the first retry will be 2 seconds, the second will be 3 seconds, and the third will be 4.5 seconds - * @default 0 + * @default 1 */ multiplier?: number; /** @@ -456,27 +456,16 @@ declare const STORAGE_RESTORE = 'restore'; export { $self, AlovaError, - type BackoffPolicy, - type CallbackFn, - type Equal, - type EventManager, FrameworkReadableState, FrameworkState, - type GeneralFn, - type GeneralState, - type IsAny, - type IsAssignable, - type IsUnknown, JSONParse, JSONStringify, MEMORY, ObjectCls, - type Omit, PromiseCls, QueueCallback, RegExpCls, STORAGE_RESTORE, - type UsePromiseExposure, buildCompletedURL, buildNamespacedCacheKey, clearTimeoutTimer, @@ -540,5 +529,16 @@ export { usePromise, uuid, valueObject, - walkObject + walkObject, + type BackoffPolicy, + type CallbackFn, + type Equal, + type EventManager, + type GeneralFn, + type GeneralState, + type IsAny, + type IsAssignable, + type IsUnknown, + type Omit, + type UsePromiseExposure }; From bdaacfad8b6ab6e3a6e3fbd102d5747783c78116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=95=87?= Date: Fri, 29 Nov 2024 18:16:16 +0800 Subject: [PATCH 4/7] fix: split response and cache data with different reference value --- .changeset/sour-games-grin.md | 7 +++ packages/adapter-mock/src/MockRequest.ts | 3 +- packages/alova/src/functions/sendRequest.ts | 17 +++-- .../test/browser/behavior/l1Cache.spec.ts | 44 +++++++++++-- packages/shared/src/function.ts | 24 +++++++ packages/shared/test/function.spec.ts | 62 +++++++++++++++++++ packages/shared/typings/alova-shared.d.ts | 32 ++++++---- 7 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 .changeset/sour-games-grin.md diff --git a/.changeset/sour-games-grin.md b/.changeset/sour-games-grin.md new file mode 100644 index 00000000..72ba8a47 --- /dev/null +++ b/.changeset/sour-games-grin.md @@ -0,0 +1,7 @@ +--- +'@alova/mock': patch +'@alova/shared': patch +'alova': patch +--- + +split response and cache data with different reference value diff --git a/packages/adapter-mock/src/MockRequest.ts b/packages/adapter-mock/src/MockRequest.ts index 527ece94..afa05907 100644 --- a/packages/adapter-mock/src/MockRequest.ts +++ b/packages/adapter-mock/src/MockRequest.ts @@ -1,5 +1,6 @@ import { falseValue, + globalToString, isFn, isNumber, isString, @@ -211,7 +212,7 @@ export default function MockRequest( return { response: () => resonpsePromise.then(({ response }) => - response && response.toString() === '[object Response]' ? (response as any).clone() : response + response && globalToString(response) === '[object Response]' ? (response as any).clone() : response ), headers: () => resonpsePromise.then(({ headers }) => headers), abort: () => { diff --git a/packages/alova/src/functions/sendRequest.ts b/packages/alova/src/functions/sendRequest.ts index 8c3b1ecd..26b2d211 100644 --- a/packages/alova/src/functions/sendRequest.ts +++ b/packages/alova/src/functions/sendRequest.ts @@ -7,6 +7,7 @@ import { PromiseCls, STORAGE_RESTORE, buildCompletedURL, + deepClone, deleteAttr, falseValue, getConfig, @@ -14,6 +15,7 @@ import { getLocalCacheConfigParam, getMethodInternalKey, getOptions, + globalToString, isFn, isPlainObject, isSpecialRequestBody, @@ -92,7 +94,11 @@ export default function sendRequest(methodInstance: Me const { baseURL, url: newUrl, type, data } = clonedMethod; const { params = {}, headers = {}, transform = $self, shareRequest } = getConfig(clonedMethod); const namespacedAdapterReturnMap = (adapterReturnMap[id] = adapterReturnMap[id] || {}); - let requestAdapterCtrls = namespacedAdapterReturnMap[methodKey]; + const requestBody = clonedMethod.data; + const requestBodyIsSpecial = isSpecialRequestBody(requestBody); + + // Will not share the request when requestBody is special data + let requestAdapterCtrls = requestBodyIsSpecial ? undefinedValue : namespacedAdapterReturnMap[methodKey]; let responseSuccessHandler: RespondedHandler = $self; let responseErrorHandler: ResponseErrorHandler | undefined = undefinedValue; let responseCompleteHandler: ResponseCompleteHandler = noop; @@ -156,8 +162,7 @@ export default function sendRequest(methodInstance: Me // Do not save cache when requestBody is special data // Reason 1: Special data is generally submitted and requires interaction with the server. // Reason 2: Special data is not convenient for generating cache keys - const requestBody = clonedMethod.data; - const toCache = !requestBody || !isSpecialRequestBody(requestBody); + const toCache = !requestBody || !requestBodyIsSpecial; // Use the latest expiration time after the response to cache data to avoid the problem of expiration time loss due to too long response time if (toCache && callInSuccess) { @@ -177,7 +182,11 @@ export default function sendRequest(methodInstance: Me ]); } catch {} } - return transformedData; + + // Deep clone the transformed data before returning to avoid reference issues + return globalToString(transformedData) === '[object Response]' + ? transformedData.clone() + : deepClone(transformedData); }; return promiseFinally( diff --git a/packages/alova/test/browser/behavior/l1Cache.spec.ts b/packages/alova/test/browser/behavior/l1Cache.spec.ts index e88cfe54..e3e92bcf 100644 --- a/packages/alova/test/browser/behavior/l1Cache.spec.ts +++ b/packages/alova/test/browser/behavior/l1Cache.spec.ts @@ -250,7 +250,43 @@ describe('l1cache cache data', () => { expect(Get.fromCache).toBeFalsy(); // Because the cache has been deleted, the request will be reissued }); - test('param localCache can also set to be a Date instance', async () => { + test("shouldn't the cache object and return object refer the same reference value", async () => { + const alova = getAlovaInstance({ + responseExpect: r => r.json() + }); + const Get = alova.Get('/unit-test', { + cacheFor: 1000, + transform: ({ data }: Result) => data + }); + const response = await Get; + const cacheData = await queryCache(Get); + expect(response).not.toBe(cacheData); + response.path = '/unit-test-modified'; + expect(response).not.toStrictEqual(cacheData); + + const alova2 = getAlovaInstance(); + const Get2 = alova2.Get('/unit-test', { + cacheFor: 1000 + }); + const response2 = await Get2; + const cacheData2 = await queryCache(Get2); + expect(response2).toBeInstanceOf(Response); + expect(cacheData2).toBeInstanceOf(Response); + expect(response2).not.toBe(cacheData2); + + const Get3 = alova2.Get('/unit-test?a=1', { + cacheFor: 1000, + transform() { + return null; + } + }); + const response3 = await Get3; + const cacheData3 = await queryCache(Get3); + expect(response3).toBeNull(); + expect(cacheData3).toBeUndefined(); + }); + + test('param cacheFor can also set to be a Date instance', async () => { const alova = getAlovaInstance({ responseExpect: r => r.json() }); @@ -272,7 +308,7 @@ describe('l1cache cache data', () => { expect(Get.fromCache).toBeFalsy(); // Because the cache has been deleted, the request will be reissued }); - test("cache data wouldn't be invalid when set localCache to `Infinity`", async () => { + test("cache data wouldn't be invalid when set cacheFor to `Infinity`", async () => { const alova = getAlovaInstance({ responseExpect: r => r.json() }); @@ -293,7 +329,7 @@ describe('l1cache cache data', () => { expect(Get.fromCache).toBeTruthy(); // Because the cache has not expired, the cached data will continue to be used and the loading will not change. }); - test('cache data will be invalid when set localCache to 0', async () => { + test('cache data will be invalid when set cacheFor to 0', async () => { const alova = getAlovaInstance({ responseExpect: r => r.json() }); @@ -308,7 +344,7 @@ describe('l1cache cache data', () => { expect(Get.fromCache).toBeFalsy(); }); - test('cache data will be invalid when set localCache to null', async () => { + test('cache data will be invalid when set cacheFor to null', async () => { const alova = getAlovaInstance({ responseExpect: r => r.json() }); diff --git a/packages/shared/src/function.ts b/packages/shared/src/function.ts index d9cc6bdc..06688323 100644 --- a/packages/shared/src/function.ts +++ b/packages/shared/src/function.ts @@ -8,6 +8,8 @@ import { STORAGE_RESTORE, falseValue, filterItem, + forEach, + isArray, len, mapItem, nullValue, @@ -394,3 +396,25 @@ export const buildCompletedURL = (baseURL: string, url: string, params: Record(obj: T): T => { + if (obj === null || typeof obj !== 'object') { + return obj; + } + + if (isArray(obj)) { + return mapItem(obj, deepClone) as T; + } + + const clone = {} as T; + forEach(objectKeys(obj), key => { + clone[key as keyof T] = deepClone(obj[key as keyof T]); + }); + return clone; +}; diff --git a/packages/shared/test/function.spec.ts b/packages/shared/test/function.spec.ts index f7e1b32b..3b8241e9 100644 --- a/packages/shared/test/function.spec.ts +++ b/packages/shared/test/function.spec.ts @@ -3,6 +3,7 @@ import { buildCompletedURL, createAsyncQueue, createSyncOnceRunner, + deepClone, getConfig, getContext, getContextOptions, @@ -640,4 +641,65 @@ describe('shared functions', () => { result = buildCompletedURL('http://example.com/api?existingParam=existingValue', '', { param1: 'value1' }); expect(result).toBe('http://example.com/api?existingParam=existingValue¶m1=value1'); }); + + test('deepClone', () => { + // primitive values + expect(deepClone(null)).toBe(null); + expect(deepClone(undefined)).toBe(undefined); + expect(deepClone(1)).toBe(1); + expect(deepClone('string')).toBe('string'); + expect(deepClone(true)).toBe(true); + + // arrays + const originalArray = [1, [2, 3], { a: 4 }]; + const clonedArray = deepClone(originalArray); + expect(clonedArray).toStrictEqual(originalArray); + expect(clonedArray).not.toBe(originalArray); + expect(clonedArray[1]).not.toBe(originalArray[1]); + expect(clonedArray[2]).not.toBe(originalArray[2]); + + // objects + const originalObject = { + a: 1, + b: { c: 2 }, + d: [3, 4], + e: null + }; + const clonedObject = deepClone(originalObject); + expect(clonedObject).toStrictEqual(originalObject); + expect(clonedObject).not.toBe(originalObject); + expect(clonedObject.b).not.toBe(originalObject.b); + expect(clonedObject.d).not.toBe(originalObject.d); + + // nested complex structures + const originalNested = { + arr: [1, { nested: true }], + obj: { + deep: { + deeper: [{ deepest: 'value' }] + } + } + }; + const clonedNested = deepClone(originalNested); + expect(clonedNested).toStrictEqual(originalNested); + expect(clonedNested.arr[1]).not.toBe(originalNested.arr[1]); + expect(clonedNested.obj.deep).not.toBe(originalNested.obj.deep); + expect(clonedNested.obj.deep.deeper[0]).not.toBe(originalNested.obj.deep.deeper[0]); + + // empty objects and arrays + const originalEmpty = { + emptyObj: {}, + emptyArr: [], + nested: { + emptyObj: {}, + emptyArr: [] + } + }; + const clonedEmpty = deepClone(originalEmpty); + expect(clonedEmpty).toStrictEqual(originalEmpty); + expect(clonedEmpty.emptyObj).not.toBe(originalEmpty.emptyObj); + expect(clonedEmpty.emptyArr).not.toBe(originalEmpty.emptyArr); + expect(clonedEmpty.nested.emptyObj).not.toBe(originalEmpty.nested.emptyObj); + expect(clonedEmpty.nested.emptyArr).not.toBe(originalEmpty.nested.emptyArr); + }); }); diff --git a/packages/shared/typings/alova-shared.d.ts b/packages/shared/typings/alova-shared.d.ts index bd733fec..e3085768 100644 --- a/packages/shared/typings/alova-shared.d.ts +++ b/packages/shared/typings/alova-shared.d.ts @@ -365,6 +365,13 @@ declare const delayWithBackoff: (backoff: BackoffPolicy, retryTimes: number) => * Build the complete url baseURL path url parameters complete url */ declare const buildCompletedURL: (baseURL: string, url: string, params: Record) => string; +/** + * Deep clone an object. + * + * @param obj The object to be cloned. + * @returns The cloned object. + */ +declare const deepClone: (obj: T) => T; type CallbackFn = () => void | Promise; declare class QueueCallback { @@ -456,16 +463,27 @@ declare const STORAGE_RESTORE = 'restore'; export { $self, AlovaError, + type BackoffPolicy, + type CallbackFn, + type Equal, + type EventManager, FrameworkReadableState, FrameworkState, + type GeneralFn, + type GeneralState, + type IsAny, + type IsAssignable, + type IsUnknown, JSONParse, JSONStringify, MEMORY, ObjectCls, + type Omit, PromiseCls, QueueCallback, RegExpCls, STORAGE_RESTORE, + type UsePromiseExposure, buildCompletedURL, buildNamespacedCacheKey, clearTimeoutTimer, @@ -474,6 +492,7 @@ export { createEventManager, createSyncOnceRunner, decorateEvent, + deepClone, defineProperty, delayWithBackoff, deleteAttr, @@ -529,16 +548,5 @@ export { usePromise, uuid, valueObject, - walkObject, - type BackoffPolicy, - type CallbackFn, - type Equal, - type EventManager, - type GeneralFn, - type GeneralState, - type IsAny, - type IsAssignable, - type IsUnknown, - type Omit, - type UsePromiseExposure + walkObject }; From 06e77896bb59301d316b751f197a936707af41b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E9=95=87?= Date: Fri, 29 Nov 2024 18:26:07 +0800 Subject: [PATCH 5/7] fix: do not share request when special request body is exist --- .changeset/selfish-onions-wash.md | 5 +++ .../browser/behavior/share-request.spec.ts | 34 ++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 .changeset/selfish-onions-wash.md diff --git a/.changeset/selfish-onions-wash.md b/.changeset/selfish-onions-wash.md new file mode 100644 index 00000000..d1b8c52e --- /dev/null +++ b/.changeset/selfish-onions-wash.md @@ -0,0 +1,5 @@ +--- +'alova': patch +--- + +do not share request when special request body is exists diff --git a/packages/alova/test/browser/behavior/share-request.spec.ts b/packages/alova/test/browser/behavior/share-request.spec.ts index ee9b51a1..1b6e8b18 100644 --- a/packages/alova/test/browser/behavior/share-request.spec.ts +++ b/packages/alova/test/browser/behavior/share-request.spec.ts @@ -2,13 +2,14 @@ import { getAlovaInstance } from '#/utils'; import { createAlova } from '@/index'; import { Result, delay, untilReject } from 'root/testUtils'; +const baseURL = process.env.NODE_BASE_URL as string; describe('Request shared', () => { test('should share request when use usehooks', async () => { const requestMockFn = vi.fn(); const beforeRequestMockFn = vi.fn(); const responseMockFn = vi.fn(); const alova = createAlova({ - baseURL: 'http://xxx', + baseURL, cacheFor: { GET: 0 }, @@ -56,7 +57,7 @@ describe('Request shared', () => { test('request shared promise will also remove when request error', async () => { let index = 0; const alova = createAlova({ - baseURL: 'http://xxx', + baseURL, cacheFor: { GET: 0 }, @@ -184,7 +185,7 @@ describe('Request shared', () => { test("shouldn't share request when close in global config", async () => { const requestMockFn = vi.fn(); const alova = createAlova({ - baseURL: 'http://xxx', + baseURL, cacheFor: { GET: 0 }, @@ -218,7 +219,7 @@ describe('Request shared', () => { test("shouldn't share request when close in method config", async () => { const requestMockFn = vi.fn(); const alova = createAlova({ - baseURL: 'http://xxx', + baseURL, cacheFor: { GET: 0 }, @@ -248,4 +249,29 @@ describe('Request shared', () => { // The sharing request is closed in the method configuration and executed twice expect(requestMockFn).toHaveBeenCalledTimes(2); }); + + test('should not share request when post with FormData', async () => { + const alova = getAlovaInstance({ + responseExpect: r => r.json() + }); + const formData1 = new FormData(); + formData1.append('file', 'file1'); + const formData2 = new FormData(); + formData2.append('file', 'file2'); + + // Create two POST requests with different FormData + const Post1 = alova.Post<{ status: number; data: { data: { file: string } } }>('/unit-test', formData1, { + shareRequest: true + }); + const Post2 = alova.Post<{ status: number; data: { data: { file: string } } }>('/unit-test', formData2, { + shareRequest: true + }); + + // Send requests simultaneously + const [response1, response2] = await Promise.all([Post1, Post2]); + + // Verify responses are different + expect(response1.data.data.file).toBe('file1'); + expect(response2.data.data.file).toBe('file2'); + }); }); From 7602aba74664bce048a11ee6223edb387e0ce130 Mon Sep 17 00:00:00 2001 From: JOU Amjs Date: Fri, 29 Nov 2024 20:27:50 +0800 Subject: [PATCH 6/7] fix: only clone array and plain object in `deepClone` --- packages/alova/src/functions/sendRequest.ts | 6 ++---- packages/shared/src/function.ts | 17 ++++++++--------- packages/shared/test/function.spec.ts | 5 +++++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/alova/src/functions/sendRequest.ts b/packages/alova/src/functions/sendRequest.ts index 26b2d211..53227154 100644 --- a/packages/alova/src/functions/sendRequest.ts +++ b/packages/alova/src/functions/sendRequest.ts @@ -15,7 +15,6 @@ import { getLocalCacheConfigParam, getMethodInternalKey, getOptions, - globalToString, isFn, isPlainObject, isSpecialRequestBody, @@ -184,9 +183,8 @@ export default function sendRequest(methodInstance: Me } // Deep clone the transformed data before returning to avoid reference issues - return globalToString(transformedData) === '[object Response]' - ? transformedData.clone() - : deepClone(transformedData); + // the `deepClone` will only clone array and plain object + return deepClone(transformedData); }; return promiseFinally( diff --git a/packages/shared/src/function.ts b/packages/shared/src/function.ts index 06688323..d7bf9fa8 100644 --- a/packages/shared/src/function.ts +++ b/packages/shared/src/function.ts @@ -404,17 +404,16 @@ export const buildCompletedURL = (baseURL: string, url: string, params: Record(obj: T): T => { - if (obj === null || typeof obj !== 'object') { - return obj; - } - if (isArray(obj)) { return mapItem(obj, deepClone) as T; } - const clone = {} as T; - forEach(objectKeys(obj), key => { - clone[key as keyof T] = deepClone(obj[key as keyof T]); - }); - return clone; + if (isPlainObject(obj)) { + const clone = {} as T; + forEach(objectKeys(obj), key => { + clone[key as keyof T] = deepClone(obj[key as keyof T]); + }); + return clone; + } + return obj; }; diff --git a/packages/shared/test/function.spec.ts b/packages/shared/test/function.spec.ts index 3b8241e9..3bdd1adb 100644 --- a/packages/shared/test/function.spec.ts +++ b/packages/shared/test/function.spec.ts @@ -650,6 +650,11 @@ describe('shared functions', () => { expect(deepClone('string')).toBe('string'); expect(deepClone(true)).toBe(true); + // non-array non-object values + const originalValue = new Blob(['abc']); + const clonedValue = deepClone(originalValue); + expect(clonedValue).toBe(originalValue); + // arrays const originalArray = [1, [2, 3], { a: 4 }]; const clonedArray = deepClone(originalArray); From df124ebb71bcc20fe035fe87b18f2ffad1439e31 Mon Sep 17 00:00:00 2001 From: JOU Amjs Date: Fri, 29 Nov 2024 20:49:19 +0800 Subject: [PATCH 7/7] test: correct unit test --- packages/alova/test/browser/behavior/l1Cache.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/alova/test/browser/behavior/l1Cache.spec.ts b/packages/alova/test/browser/behavior/l1Cache.spec.ts index e3e92bcf..4eccb72e 100644 --- a/packages/alova/test/browser/behavior/l1Cache.spec.ts +++ b/packages/alova/test/browser/behavior/l1Cache.spec.ts @@ -272,7 +272,7 @@ describe('l1cache cache data', () => { const cacheData2 = await queryCache(Get2); expect(response2).toBeInstanceOf(Response); expect(cacheData2).toBeInstanceOf(Response); - expect(response2).not.toBe(cacheData2); + expect(response2).toBe(cacheData2); const Get3 = alova2.Get('/unit-test?a=1', { cacheFor: 1000,