diff --git a/packages/hooks/src/useRequest/__tests__/useThrottlePlugin.spec.ts b/packages/hooks/src/useRequest/__tests__/useThrottlePlugin.spec.ts index e396befaa2..a76a45b50c 100644 --- a/packages/hooks/src/useRequest/__tests__/useThrottlePlugin.spec.ts +++ b/packages/hooks/src/useRequest/__tests__/useThrottlePlugin.spec.ts @@ -41,4 +41,122 @@ describe('useThrottlePlugin', () => { expect(callback).toHaveBeenCalledTimes(2); }); + + test('useThrottlePlugin should work with runAsync', () => { + const callback = vi.fn(); + + act(() => { + hook = setUp( + () => { + callback(); + return request({}); + }, + { + manual: true, + throttleWait: 100, + }, + ); + }); + + act(() => { + hook.result.current.runAsync(1); + vi.advanceTimersByTime(50); + hook.result.current.runAsync(2); + vi.advanceTimersByTime(50); + hook.result.current.runAsync(3); + vi.advanceTimersByTime(50); + hook.result.current.runAsync(4); + vi.advanceTimersByTime(40); + }); + + expect(callback).toHaveBeenCalledTimes(2); + }); + + test('useThrottlePlugin should respect throttleLeading and throttleTrailing options with runAsync', () => { + const callback = vi.fn(); + + act(() => { + hook = setUp( + () => { + callback(); + return request({}); + }, + { + manual: true, + throttleWait: 3000, + throttleLeading: true, + throttleTrailing: false, + }, + ); + }); + + act(() => { + // First call should execute immediately (leading: true) + hook.result.current.runAsync(1); + // These calls should be ignored (within throttle window) + hook.result.current.runAsync(2); + hook.result.current.runAsync(3); + hook.result.current.runAsync(4); + hook.result.current.runAsync(5); + hook.result.current.runAsync(6); + hook.result.current.runAsync(7); + + vi.advanceTimersByTime(3000); + + // After throttle window, next call should execute + hook.result.current.runAsync(8); + }); + + // Should only execute twice: first call (leading) and call after throttle window + expect(callback).toHaveBeenCalledTimes(2); + }); + + test('useThrottlePlugin should resolve all promises when using runAsync', async () => { + let requestCount = 0; + + act(() => { + hook = setUp( + () => { + requestCount++; + return request({ id: requestCount }); + }, + { + manual: true, + throttleWait: 100, + throttleLeading: true, + throttleTrailing: false, + }, + ); + }); + + let resolved1 = false; + let resolved2 = false; + let rejected2 = false; + + // Make two calls within throttle window + const p1 = hook.result.current.runAsync(1); + const p2 = hook.result.current.runAsync(2); + + p1.then(() => { + resolved1 = true; + }); + p2.then(() => { + resolved2 = true; + }).catch(() => { + rejected2 = true; + }); + + // Advance time for throttle and request + await act(async () => { + vi.advanceTimersByTime(100); // throttle wait + vi.advanceTimersByTime(1000); // request wait + await Promise.resolve(); + }); + + // First promise should be resolved + expect(resolved1).toBe(true); + // Second promise should be either resolved or rejected (not hanging) + expect(resolved2 || rejected2).toBe(true); + expect(requestCount).toBe(1); // Only one request should be made + }); }); diff --git a/packages/hooks/src/useRequest/src/plugins/useThrottlePlugin.ts b/packages/hooks/src/useRequest/src/plugins/useThrottlePlugin.ts index 085492e4d9..24cd28865a 100644 --- a/packages/hooks/src/useRequest/src/plugins/useThrottlePlugin.ts +++ b/packages/hooks/src/useRequest/src/plugins/useThrottlePlugin.ts @@ -22,6 +22,10 @@ const useThrottlePlugin: Plugin = ( if (throttleWait) { const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance); + // Track the current promise and when it was created + let currentPromise: Promise | null = null; + let promiseCreatedAt = 0; + throttledRef.current = throttle( (callback) => { callback(); @@ -33,13 +37,42 @@ const useThrottlePlugin: Plugin = ( // throttle runAsync should be promise // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398 fetchInstance.runAsync = (...args) => { - return new Promise((resolve, reject) => { + const now = Date.now(); + + // If there's a current promise and it was created within the throttle window, + // return it to share the result + if (currentPromise && now - promiseCreatedAt < throttleWait) { + return currentPromise; + } + + // Create a new promise + promiseCreatedAt = now; + currentPromise = new Promise((resolve, reject) => { throttledRef.current?.(() => { + // Execute the request _originRunAsync(...args) - .then(resolve) - .catch(reject); + .then((result) => { + resolve(result); + // Clear current promise after a delay to allow trailing calls + setTimeout(() => { + if (currentPromise && Date.now() - promiseCreatedAt >= throttleWait) { + currentPromise = null; + } + }, 0); + }) + .catch((error) => { + reject(error); + // Clear current promise after a delay to allow trailing calls + setTimeout(() => { + if (currentPromise && Date.now() - promiseCreatedAt >= throttleWait) { + currentPromise = null; + } + }, 0); + }); }); }); + + return currentPromise; }; return () => { diff --git a/packages/hooks/src/useRequest/src/useRequestImplement.ts b/packages/hooks/src/useRequest/src/useRequestImplement.ts index d65cfba6cb..089ca1893b 100644 --- a/packages/hooks/src/useRequest/src/useRequestImplement.ts +++ b/packages/hooks/src/useRequest/src/useRequestImplement.ts @@ -68,7 +68,7 @@ function useRequestImplement( refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)), refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)), run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)), - runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)), + runAsync: useMemoizedFn((...args: TParams) => fetchInstance.runAsync(...args)), mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)), } as Result; }