Skip to content

Commit 000f0e4

Browse files
BrunoQuaresmaclaude
andcommitted
feat(hooks): remove max attempts limit from useWithRetry hook
Remove the MAX_ATTEMPTS constant and associated logic to allow unlimited retry attempts. The hook now retries indefinitely with exponential backoff (capped at 10 minutes delay) until the operation succeeds or is cancelled. Update tests to verify the new unlimited retry behavior while maintaining all existing functionality like cancellation, cleanup, and proper state management. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 55036a4 commit 000f0e4

File tree

2 files changed

+29
-31
lines changed

2 files changed

+29
-31
lines changed

site/src/hooks/useWithRetry.test.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe("useWithRetry", () => {
9696
expect(result.current.retryAt).toBe(undefined);
9797
});
9898

99-
it("should stop retrying after max attempts (10)", async () => {
99+
it("should continue retrying without limit", async () => {
100100
mockFn.mockRejectedValue(new Error("Always fails"));
101101

102102
const { result } = renderHook(() => useWithRetry(mockFn));
@@ -110,18 +110,19 @@ describe("useWithRetry", () => {
110110
expect(result.current.isLoading).toBe(false);
111111
expect(result.current.retryAt).not.toBe(null);
112112

113-
// Fast-forward through all retries
114-
for (let i = 1; i < 10; i++) {
113+
// Fast-forward through multiple retries to verify it continues
114+
for (let i = 1; i < 15; i++) {
115115
const delay = Math.min(1000 * 2 ** (i - 1), 600000); // exponential backoff with max delay
116116
await act(async () => {
117117
jest.advanceTimersByTime(delay);
118118
});
119119
expect(mockFn).toHaveBeenCalledTimes(i + 1);
120+
expect(result.current.isLoading).toBe(false);
121+
expect(result.current.retryAt).not.toBe(null);
120122
}
121123

122-
// After 10 attempts, should stop retrying
123-
expect(result.current.isLoading).toBe(false);
124-
expect(result.current.retryAt).toBe(undefined);
124+
// Should still be retrying after 15 attempts
125+
expect(result.current.retryAt).not.toBe(null);
125126
});
126127

127128
it("should respect max delay of 10 minutes", async () => {
@@ -158,7 +159,15 @@ describe("useWithRetry", () => {
158159

159160
expect(mockFn).toHaveBeenCalledTimes(10);
160161
expect(result.current.isLoading).toBe(false);
161-
expect(result.current.retryAt).toBe(undefined);
162+
expect(result.current.retryAt).not.toBe(null);
163+
164+
// Continue with more retries at max delay to verify it continues indefinitely
165+
await act(async () => {
166+
jest.advanceTimersByTime(600000);
167+
});
168+
169+
expect(mockFn).toHaveBeenCalledTimes(11);
170+
expect(result.current.retryAt).not.toBe(null);
162171
});
163172

164173
it("should cancel previous retry when call is invoked again", async () => {

site/src/hooks/useWithRetry.ts

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
22

3-
const MAX_ATTEMPTS = 10;
43
const DELAY_MS = 1_000;
54
const MAX_DELAY_MS = 600_000; // 10 minutes
65
// Determines how much the delay between retry attempts increases after each
@@ -49,31 +48,21 @@ export function useWithRetry(fn: () => Promise<void>): UseWithRetryResult {
4948
await fn();
5049
setState({ isLoading: false, retryAt: undefined, attemptCount: 0 });
5150
} catch (error) {
52-
// Since attempts start from 0, we need to add +1 to make the condition work
53-
// This ensures exactly MAX_ATTEMPTS total attempts (attempt 0, 1, 2, ..., 9)
54-
if (attempt + 1 < MAX_ATTEMPTS) {
55-
const delayMs = Math.min(
56-
DELAY_MS * MULTIPLIER ** attempt,
57-
MAX_DELAY_MS,
58-
);
51+
const delayMs = Math.min(
52+
DELAY_MS * MULTIPLIER ** attempt,
53+
MAX_DELAY_MS,
54+
);
5955

60-
setState((prev) => ({
61-
...prev,
62-
isLoading: false,
63-
retryAt: new Date(Date.now() + delayMs),
64-
}));
56+
setState((prev) => ({
57+
...prev,
58+
isLoading: false,
59+
retryAt: new Date(Date.now() + delayMs),
60+
}));
6561

66-
timeoutRef.current = window.setTimeout(() => {
67-
setState((prev) => ({ ...prev, retryAt: undefined }));
68-
executeAttempt(attempt + 1);
69-
}, delayMs);
70-
} else {
71-
setState((prev) => ({
72-
...prev,
73-
isLoading: false,
74-
retryAt: undefined,
75-
}));
76-
}
62+
timeoutRef.current = window.setTimeout(() => {
63+
setState((prev) => ({ ...prev, retryAt: undefined }));
64+
executeAttempt(attempt + 1);
65+
}, delayMs);
7766
}
7867
};
7968

0 commit comments

Comments
 (0)