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

Fix/cloned method from cache #602

Merged
merged 7 commits into from
Nov 29, 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
5 changes: 5 additions & 0 deletions .changeset/itchy-bottles-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'alova': patch
---

fix that `fromCache` is undefined in cache hit console
5 changes: 5 additions & 0 deletions .changeset/selfish-onions-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'alova': patch
---

do not share request when special request body is exists
7 changes: 7 additions & 0 deletions .changeset/sour-games-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@alova/mock': patch
'@alova/shared': patch
'alova': patch
---

split response and cache data with different reference value
5 changes: 5 additions & 0 deletions .changeset/ten-lies-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'alova': patch
---

correct default `multiplier` 0 to 1 in comment
3 changes: 2 additions & 1 deletion packages/adapter-mock/src/MockRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
falseValue,
globalToString,
isFn,
isNumber,
isString,
Expand Down Expand Up @@ -211,7 +212,7 @@ export default function MockRequest<RequestConfig, Response, ResponseHeader>(
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: () => {
Expand Down
18 changes: 13 additions & 5 deletions packages/alova/src/functions/sendRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PromiseCls,
STORAGE_RESTORE,
buildCompletedURL,
deepClone,
deleteAttr,
falseValue,
getConfig,
Expand Down Expand Up @@ -92,7 +93,11 @@ export default function sendRequest<AG extends AlovaGenerics>(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<AG> = $self;
let responseErrorHandler: ResponseErrorHandler<AG> | undefined = undefinedValue;
let responseCompleteHandler: ResponseCompleteHandler<AG> = noop;
Expand All @@ -111,7 +116,8 @@ export default function sendRequest<AG extends AlovaGenerics>(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;
}
Expand Down Expand Up @@ -155,8 +161,7 @@ export default function sendRequest<AG extends AlovaGenerics>(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) {
Expand All @@ -176,7 +181,10 @@ export default function sendRequest<AG extends AlovaGenerics>(methodInstance: Me
]);
} catch {}
}
return transformedData;

// Deep clone the transformed data before returning to avoid reference issues
// the `deepClone` will only clone array and plain object
return deepClone(transformedData);
};

return promiseFinally(
Expand Down
102 changes: 98 additions & 4 deletions packages/alova/test/browser/behavior/l1Cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -192,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<Response>('/unit-test', {
cacheFor: 1000
});
const response2 = await Get2;
const cacheData2 = await queryCache(Get2);
expect(response2).toBeInstanceOf(Response);
expect(cacheData2).toBeInstanceOf(Response);
expect(response2).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()
});
Expand All @@ -214,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()
});
Expand All @@ -235,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()
});
Expand All @@ -250,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()
});
Expand Down
34 changes: 30 additions & 4 deletions packages/alova/test/browser/behavior/share-request.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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');
});
});
1 change: 1 addition & 0 deletions packages/alova/test/browser/global/createAlova.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
2 changes: 1 addition & 1 deletion packages/alova/typings/clienthook/hooks/useSQRequest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion packages/client/typings/clienthook/hooks/useSQRequest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
23 changes: 23 additions & 0 deletions packages/shared/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
STORAGE_RESTORE,
falseValue,
filterItem,
forEach,
isArray,
len,
mapItem,
nullValue,
Expand Down Expand Up @@ -394,3 +396,24 @@ export const buildCompletedURL = (baseURL: string, url: string, params: Record<s
: `${completeURL}?${paramsStr}`
: completeURL;
};

/**
* Deep clone an object.
*
* @param obj The object to be cloned.
* @returns The cloned object.
*/
export const deepClone = <T>(obj: T): T => {
if (isArray(obj)) {
return mapItem(obj, deepClone) as T;
}

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;
};
2 changes: 1 addition & 1 deletion packages/shared/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading