Skip to content

Commit d3e1ef7

Browse files
treangenerall
authored andcommitted
add a backoff retry, fix test cleanup
1 parent 1ae6e28 commit d3e1ef7

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

src/components/Collections/Optimizations/Optimizations.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import OptimizationsTree from './Tree/OptimizationsTree';
88

99
/** Poll interval while at least one optimization is running and the tab is visible (2–5s range). */
1010
const POLL_ACTIVE_MS = 4000;
11+
/** Max delay between retries after a failed poll (exponential backoff cap). */
12+
const POLL_ERROR_RETRY_MAX_MS = 32000;
1113

1214
function isRequestCanceled(error) {
1315
return error?.code === 'ERR_CANCELED' || error?.name === 'CanceledError';
@@ -23,6 +25,7 @@ const Optimizations = ({ collectionName }) => {
2325
const pollTimeoutRef = useRef(null);
2426
const lastRunningRef = useRef(false);
2527
const mountedRef = useRef(true);
28+
const pollErrorBackoffMsRef = useRef(POLL_ACTIVE_MS);
2629

2730
const runFetch = useCallback(
2831
async ({ preserveSelection = false } = {}) => {
@@ -34,6 +37,7 @@ const Optimizations = ({ collectionName }) => {
3437
abortRef.current = ac;
3538

3639
if (!preserveSelection) {
40+
pollErrorBackoffMsRef.current = POLL_ACTIVE_MS;
3741
setIsRefreshing(true);
3842
setSelectedOptimization(null);
3943
}
@@ -52,6 +56,7 @@ const Optimizations = ({ collectionName }) => {
5256
const result = next?.result;
5357
const hasRunning = Array.isArray(result?.running) && result.running.length > 0;
5458
lastRunningRef.current = hasRunning;
59+
pollErrorBackoffMsRef.current = POLL_ACTIVE_MS;
5560

5661
clearTimeout(pollTimeoutRef.current);
5762
if (hasRunning && !document.hidden) {
@@ -63,6 +68,14 @@ const Optimizations = ({ collectionName }) => {
6368
} catch (error) {
6469
if (isRequestCanceled(error)) return;
6570
console.error('Error fetching optimizations:', error);
71+
if (mountedRef.current && lastRunningRef.current && !document.hidden) {
72+
const delay = pollErrorBackoffMsRef.current;
73+
pollErrorBackoffMsRef.current = Math.min(pollErrorBackoffMsRef.current * 2, POLL_ERROR_RETRY_MAX_MS);
74+
pollTimeoutRef.current = window.setTimeout(() => {
75+
pollTimeoutRef.current = null;
76+
void runFetch({ preserveSelection: true });
77+
}, delay);
78+
}
6679
} finally {
6780
if (!preserveSelection && mountedRef.current) {
6881
setIsRefreshing(false);

src/components/Collections/Optimizations/Optimizations.test.jsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@ const apiResponse = (running = []) => ({
1717
});
1818

1919
describe('Optimizations polling', () => {
20+
const originalHiddenDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'hidden');
21+
2022
beforeEach(() => {
2123
vi.useFakeTimers();
2224
getMock.mockReset();
2325
});
2426

2527
afterEach(() => {
2628
vi.useRealTimers();
29+
if (originalHiddenDescriptor) {
30+
Object.defineProperty(document, 'hidden', originalHiddenDescriptor);
31+
} else {
32+
delete document.hidden;
33+
}
2734
});
2835

2936
it('polls while running is non-empty and stops when empty', async () => {
@@ -52,6 +59,31 @@ describe('Optimizations polling', () => {
5259
expect(getMock).toHaveBeenCalledTimes(3);
5360
});
5461

62+
it('resumes polling after a transient error while optimizations are running', async () => {
63+
getMock
64+
.mockResolvedValueOnce(apiResponse([{ id: 1 }]))
65+
.mockRejectedValueOnce(new Error('network'))
66+
.mockResolvedValueOnce(apiResponse([{ id: 1 }]))
67+
.mockResolvedValueOnce(apiResponse([]));
68+
69+
await act(async () => {
70+
render(<Optimizations collectionName="test" />);
71+
});
72+
expect(getMock).toHaveBeenCalledTimes(1);
73+
74+
await act(async () => vi.advanceTimersByTime(4000));
75+
expect(getMock).toHaveBeenCalledTimes(2);
76+
77+
await act(async () => vi.advanceTimersByTime(4000));
78+
expect(getMock).toHaveBeenCalledTimes(3);
79+
80+
await act(async () => vi.advanceTimersByTime(4000));
81+
expect(getMock).toHaveBeenCalledTimes(4);
82+
83+
await act(async () => vi.advanceTimersByTime(8000));
84+
expect(getMock).toHaveBeenCalledTimes(4);
85+
});
86+
5587
it('pauses polling when document is hidden and resumes on visibility', async () => {
5688
getMock.mockResolvedValue(apiResponse([{ id: 1 }]));
5789

0 commit comments

Comments
 (0)