From 7ec8205f194656dcac81b92fe615ea375ce9095f Mon Sep 17 00:00:00 2001 From: issue-solver-bot Date: Sun, 18 Oct 2026 23:06:13 +0100 Subject: [PATCH] fix: resolve issue #569 --- src/hooks/useSmartPolling.test.tsx | 44 ++++++++++++++++++++++++++++++ src/hooks/useSmartPolling.ts | 30 +++++++++++++++++--- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/hooks/useSmartPolling.test.tsx b/src/hooks/useSmartPolling.test.tsx index af0e3ea9..6bb1ed08 100644 --- a/src/hooks/useSmartPolling.test.tsx +++ b/src/hooks/useSmartPolling.test.tsx @@ -78,6 +78,50 @@ describe("useSmartPolling", () => { expect(result.current.isBackingOff).toBe(true); }); + it("actually slows the timer on backoff (capped at maxBackoff) and resets on refetch", async () => { + const fetchFn = jest.fn(async () => ({ n: 1 })); + const { result } = renderHook(() => + useSmartPolling({ + fetchFn, + interval: 1000, + unchangedThreshold: 2, + backoffMultiplier: 2, + maxBackoff: 4000, + }), + ); + const advance = async (ms: number) => { + await act(async () => { + await jest.advanceTimersByTimeAsync(ms); + }); + }; + + await advance(0); + expect(fetchFn).toHaveBeenCalledTimes(1); + await advance(1000); // call 2 (unchanged 1) + await advance(1000); // call 3 (unchanged 2) → interval 2000 + expect(fetchFn).toHaveBeenCalledTimes(3); + expect(result.current.isBackingOff).toBe(true); + + await advance(1000); + expect(fetchFn).toHaveBeenCalledTimes(3); // no call at the old 1s cadence + await advance(1000); // call 4 → interval 4000 (max) + expect(fetchFn).toHaveBeenCalledTimes(4); + + await advance(3000); + expect(fetchFn).toHaveBeenCalledTimes(4); + await advance(1000); // call 5, still capped at 4000 + expect(fetchFn).toHaveBeenCalledTimes(5); + + // refetch resets to the base interval + await act(async () => { + await result.current.refetch(); + }); + expect(fetchFn).toHaveBeenCalledTimes(6); + expect(result.current.isBackingOff).toBe(false); + await advance(1000); + expect(fetchFn).toHaveBeenCalledTimes(7); + }); + it("does not poll when enabled is false", async () => { const ref = { count: 0 }; const fetchFn = createFetchFn([{ n: 1 }], ref); diff --git a/src/hooks/useSmartPolling.ts b/src/hooks/useSmartPolling.ts index 81507fc4..58d0cf06 100644 --- a/src/hooks/useSmartPolling.ts +++ b/src/hooks/useSmartPolling.ts @@ -73,6 +73,19 @@ export function useSmartPolling({ const previousDataRef = useRef(null); const isBackgroundedRef = useRef(false); + const fetchDataRef = useRef<() => Promise>(async () => {}); + + // Restart the running timer so a changed currentIntervalRef takes effect. + // No-op when polling isn't active (no timer running). + const rescheduleInterval = useCallback(() => { + if (!intervalRef.current) return; + clearInterval(intervalRef.current); + intervalRef.current = setInterval( + () => fetchDataRef.current(), + currentIntervalRef.current + ); + }, []); + const fetchData = useCallback(async () => { if (!isMountedRef.current) return; @@ -90,7 +103,10 @@ export function useSmartPolling({ setData(result); previousDataRef.current = result; unchangedCountRef.current = 0; - currentIntervalRef.current = interval; + if (currentIntervalRef.current !== interval) { + currentIntervalRef.current = interval; + rescheduleInterval(); + } dispatch({ type: 'STOP_BACKOFF' }); onDataChange?.(result); } else { @@ -103,6 +119,7 @@ export function useSmartPolling({ ); if (newInterval > currentIntervalRef.current) { currentIntervalRef.current = newInterval; + rescheduleInterval(); dispatch({ type: 'START_BACKOFF' }); } } @@ -118,14 +135,19 @@ export function useSmartPolling({ setIsLoading(false); } } - }, [fetchFn, compareFn, interval, backoffMultiplier, maxBackoff, unchangedThreshold, onDataChange]); + }, [fetchFn, compareFn, interval, backoffMultiplier, maxBackoff, unchangedThreshold, onDataChange, rescheduleInterval]); + + fetchDataRef.current = fetchData; const refetch = useCallback(async () => { - currentIntervalRef.current = interval; + if (currentIntervalRef.current !== interval) { + currentIntervalRef.current = interval; + rescheduleInterval(); + } dispatch({ type: 'STOP_BACKOFF' }); unchangedCountRef.current = 0; await fetchData(); - }, [fetchData, interval]); + }, [fetchData, interval, rescheduleInterval]); // Handle visibility change useEffect(() => {