Skip to content

Commit 3022566

Browse files
Refactor useWithRetry hook according to specifications
- Remove options parameter - hook now uses fixed configuration - Update max attempts to 10 (from 3) - Update max delay to 10 minutes (from 8 seconds) - Remove countdown logic - no interval ref needed - Consolidate state into single RetryState object - Calculate delay inline where it's used - Remove separate executeFunction - logic moved into call function - Only use functions for reusable logic (clearTimer) - Update tests to match new implementation - Run formatting and linting checks Co-authored-by: BrunoQuaresma <3165839+BrunoQuaresma@users.noreply.github.com>
1 parent 8323192 commit 3022566

File tree

2 files changed

+86
-171
lines changed

2 files changed

+86
-171
lines changed

site/src/hooks/useWithRetry.test.ts

Lines changed: 50 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ describe("useWithRetry", () => {
7474
});
7575

7676
expect(mockFn).toHaveBeenCalledTimes(1);
77-
expect(result.current.isLoading).toBe(true);
77+
expect(result.current.isLoading).toBe(false);
7878
expect(result.current.retryAt).not.toBe(null);
7979

8080
// Fast-forward to first retry (1 second)
@@ -83,7 +83,7 @@ describe("useWithRetry", () => {
8383
});
8484

8585
expect(mockFn).toHaveBeenCalledTimes(2);
86-
expect(result.current.isLoading).toBe(true);
86+
expect(result.current.isLoading).toBe(false);
8787
expect(result.current.retryAt).not.toBe(null);
8888

8989
// Fast-forward to second retry (2 seconds)
@@ -96,96 +96,69 @@ describe("useWithRetry", () => {
9696
expect(result.current.retryAt).toBe(null);
9797
});
9898

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

102-
const { result } = renderHook(() =>
103-
useWithRetry(mockFn, { maxAttempts: 2 }),
104-
);
102+
const { result } = renderHook(() => useWithRetry(mockFn));
105103

106104
// Start the call
107105
await act(async () => {
108106
await result.current.call();
109107
});
110108

111109
expect(mockFn).toHaveBeenCalledTimes(1);
112-
expect(result.current.isLoading).toBe(true);
113-
114-
// Fast-forward to first retry
115-
await act(async () => {
116-
jest.advanceTimersByTime(1000);
117-
});
118-
119-
expect(mockFn).toHaveBeenCalledTimes(2);
120110
expect(result.current.isLoading).toBe(false);
121-
expect(result.current.retryAt).toBe(null);
122-
});
123-
124-
it("should use custom retry options", async () => {
125-
mockFn
126-
.mockRejectedValueOnce(new Error("First failure"))
127-
.mockResolvedValueOnce(undefined);
128-
129-
const { result } = renderHook(() =>
130-
useWithRetry(mockFn, {
131-
initialDelay: 500,
132-
multiplier: 3,
133-
maxAttempts: 2,
134-
}),
135-
);
136-
137-
// Start the call
138-
await act(async () => {
139-
await result.current.call();
140-
});
141-
142-
expect(mockFn).toHaveBeenCalledTimes(1);
143-
expect(result.current.isLoading).toBe(true);
144111
expect(result.current.retryAt).not.toBe(null);
145112

146-
// Fast-forward by custom initial delay (500ms)
147-
await act(async () => {
148-
jest.advanceTimersByTime(500);
149-
});
113+
// Fast-forward through all retries
114+
for (let i = 1; i < 10; i++) {
115+
const delay = Math.min(1000 * 2 ** (i - 1), 600000); // exponential backoff with max delay
116+
await act(async () => {
117+
jest.advanceTimersByTime(delay);
118+
});
119+
expect(mockFn).toHaveBeenCalledTimes(i + 1);
120+
}
150121

151-
expect(mockFn).toHaveBeenCalledTimes(2);
122+
// After 10 attempts, should stop retrying
152123
expect(result.current.isLoading).toBe(false);
153124
expect(result.current.retryAt).toBe(null);
154125
});
155126

156-
it("should respect max delay", async () => {
127+
it("should respect max delay of 10 minutes", async () => {
157128
mockFn.mockRejectedValue(new Error("Always fails"));
158129

159-
const { result } = renderHook(() =>
160-
useWithRetry(mockFn, {
161-
initialDelay: 1000,
162-
multiplier: 10,
163-
maxDelay: 2000,
164-
maxAttempts: 3,
165-
}),
166-
);
130+
const { result } = renderHook(() => useWithRetry(mockFn));
167131

168132
// Start the call
169133
await act(async () => {
170134
await result.current.call();
171135
});
172136

173-
expect(result.current.isLoading).toBe(true);
174-
175-
// First retry should be at 1000ms (initial delay)
176-
await act(async () => {
177-
jest.advanceTimersByTime(1000);
178-
});
137+
expect(result.current.isLoading).toBe(false);
179138

180-
expect(mockFn).toHaveBeenCalledTimes(2);
139+
// Fast-forward through several retries to reach max delay
140+
// After attempt 9, delay would be 1000 * 2^9 = 512000ms, which is less than 600000ms (10 min)
141+
// After attempt 10, delay would be 1000 * 2^10 = 1024000ms, which should be capped at 600000ms
142+
143+
// Skip to attempt 9 (delay calculation: 1000 * 2^8 = 256000ms)
144+
for (let i = 1; i < 9; i++) {
145+
const delay = 1000 * 2 ** (i - 1);
146+
await act(async () => {
147+
jest.advanceTimersByTime(delay);
148+
});
149+
}
150+
151+
expect(mockFn).toHaveBeenCalledTimes(9);
152+
expect(result.current.retryAt).not.toBe(null);
181153

182-
// Second retry should be at 2000ms (max delay, not 10000ms)
154+
// The 9th retry should use max delay (600000ms = 10 minutes)
183155
await act(async () => {
184-
jest.advanceTimersByTime(2000);
156+
jest.advanceTimersByTime(600000);
185157
});
186158

187-
expect(mockFn).toHaveBeenCalledTimes(3);
159+
expect(mockFn).toHaveBeenCalledTimes(10);
188160
expect(result.current.isLoading).toBe(false);
161+
expect(result.current.retryAt).toBe(null);
189162
});
190163

191164
it("should cancel previous retry when call is invoked again", async () => {
@@ -201,7 +174,7 @@ describe("useWithRetry", () => {
201174
});
202175

203176
expect(mockFn).toHaveBeenCalledTimes(1);
204-
expect(result.current.isLoading).toBe(true);
177+
expect(result.current.isLoading).toBe(false);
205178
expect(result.current.retryAt).not.toBe(null);
206179

207180
// Call again before retry happens
@@ -221,61 +194,50 @@ describe("useWithRetry", () => {
221194
expect(mockFn).toHaveBeenCalledTimes(2); // Should not have been called again
222195
});
223196

224-
it("should update retryAt countdown", async () => {
197+
it("should set retryAt when scheduling retry", async () => {
225198
mockFn.mockRejectedValue(new Error("Failure"));
226199

227-
const { result } = renderHook(() =>
228-
useWithRetry(mockFn, { initialDelay: 1000 }),
229-
);
200+
const { result } = renderHook(() => useWithRetry(mockFn));
230201

231202
// Start the call
232203
await act(async () => {
233204
await result.current.call();
234205
});
235206

236-
const initialRetryAt = result.current.retryAt;
237-
expect(initialRetryAt).not.toBe(null);
238-
239-
// Advance time by 100ms (countdown update interval)
240-
await act(async () => {
241-
jest.advanceTimersByTime(100);
242-
});
243-
244-
// retryAt should still be set but countdown should be updating
245-
expect(result.current.retryAt).not.toBe(null);
246-
247-
// Advance to just before retry time
248-
await act(async () => {
249-
jest.advanceTimersByTime(850);
250-
});
207+
const retryAt = result.current.retryAt;
208+
expect(retryAt).not.toBe(null);
209+
expect(retryAt).toBeInstanceOf(Date);
251210

252-
expect(result.current.retryAt).not.toBe(null);
211+
// retryAt should be approximately 1 second in the future
212+
const expectedTime = Date.now() + 1000;
213+
const actualTime = retryAt!.getTime();
214+
expect(Math.abs(actualTime - expectedTime)).toBeLessThan(100); // Allow 100ms tolerance
253215

254216
// Advance past retry time
255217
await act(async () => {
256-
jest.advanceTimersByTime(100);
218+
jest.advanceTimersByTime(1000);
257219
});
258220

259221
expect(result.current.retryAt).toBe(null);
260222
});
261223

262-
it("should cleanup timers on unmount", async () => {
224+
it("should cleanup timer on unmount", async () => {
263225
mockFn.mockRejectedValue(new Error("Failure"));
264226

265227
const { result, unmount } = renderHook(() => useWithRetry(mockFn));
266228

267-
// Start the call to create timers
229+
// Start the call to create timer
268230
await act(async () => {
269231
await result.current.call();
270232
});
271233

272-
expect(result.current.isLoading).toBe(true);
234+
expect(result.current.isLoading).toBe(false);
273235
expect(result.current.retryAt).not.toBe(null);
274236

275-
// Unmount should cleanup timers
237+
// Unmount should cleanup timer
276238
unmount();
277239

278-
// Advance time to ensure timers were cleared
240+
// Advance time to ensure timer was cleared
279241
await act(async () => {
280242
jest.advanceTimersByTime(5000);
281243
});

0 commit comments

Comments
 (0)