Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitc115f13

Browse files
feat: Phase 1 - Terminal reconnection foundation
- Update ConnectionStatus type: replace 'initializing' with 'connecting'- Create useRetry hook with exponential backoff logic- Add comprehensive tests for useRetry hook- Export useRetry from hooks indexImplements:- Initial delay: 1 second- Max delay: 30 seconds- Backoff multiplier: 2- Max retry attempts: 10Co-authored-by: BrunoQuaresma <3165839+BrunoQuaresma@users.noreply.github.com>
1 parent6f2834f commitc115f13

File tree

5 files changed

+521
-2
lines changed

5 files changed

+521
-2
lines changed

‎site/src/hooks/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./useClickable";
33
export*from"./useClickableTableRow";
44
export*from"./useClipboard";
55
export*from"./usePagination";
6+
export*from"./useRetry";

‎site/src/hooks/useRetry.test.ts‎

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import{act,renderHook}from"@testing-library/react";
2+
import{useRetry}from"./useRetry";
3+
4+
// Mock timers
5+
jest.useFakeTimers();
6+
7+
describe("useRetry",()=>{
8+
constdefaultOptions={
9+
maxAttempts:3,
10+
initialDelay:1000,
11+
maxDelay:8000,
12+
multiplier:2,
13+
};
14+
15+
letmockOnRetry:jest.Mock;
16+
17+
beforeEach(()=>{
18+
mockOnRetry=jest.fn();
19+
jest.clearAllTimers();
20+
});
21+
22+
afterEach(()=>{
23+
jest.clearAllMocks();
24+
});
25+
26+
it("should initialize with correct default state",()=>{
27+
const{ result}=renderHook(()=>
28+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
29+
);
30+
31+
expect(result.current.isRetrying).toBe(false);
32+
expect(result.current.currentDelay).toBe(null);
33+
expect(result.current.attemptCount).toBe(0);
34+
expect(result.current.timeUntilNextRetry).toBe(null);
35+
});
36+
37+
it("should start retrying when startRetrying is called",async()=>{
38+
mockOnRetry.mockRejectedValue(newError("Connection failed"));
39+
40+
const{ result}=renderHook(()=>
41+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
42+
);
43+
44+
act(()=>{
45+
result.current.startRetrying();
46+
});
47+
48+
expect(result.current.attemptCount).toBe(1);
49+
expect(result.current.isRetrying).toBe(true);
50+
51+
// Wait for the retry to complete
52+
awaitact(async()=>{
53+
awaitPromise.resolve();
54+
});
55+
56+
expect(mockOnRetry).toHaveBeenCalledTimes(1);
57+
expect(result.current.isRetrying).toBe(false);
58+
});
59+
60+
it("should calculate exponential backoff delays correctly",async()=>{
61+
mockOnRetry.mockRejectedValue(newError("Connection failed"));
62+
63+
const{ result}=renderHook(()=>
64+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
65+
);
66+
67+
act(()=>{
68+
result.current.startRetrying();
69+
});
70+
71+
// Wait for first retry to fail
72+
awaitact(async()=>{
73+
awaitPromise.resolve();
74+
});
75+
76+
// Should schedule next retry with initial delay (1000ms)
77+
expect(result.current.currentDelay).toBe(1000);
78+
expect(result.current.timeUntilNextRetry).toBe(1000);
79+
80+
// Fast forward to trigger second retry
81+
act(()=>{
82+
jest.advanceTimersByTime(1000);
83+
});
84+
85+
awaitact(async()=>{
86+
awaitPromise.resolve();
87+
});
88+
89+
// Should schedule third retry with doubled delay (2000ms)
90+
expect(result.current.currentDelay).toBe(2000);
91+
});
92+
93+
it("should respect maximum delay",async()=>{
94+
mockOnRetry.mockRejectedValue(newError("Connection failed"));
95+
96+
constoptions={
97+
...defaultOptions,
98+
maxDelay:1500,// Lower max delay
99+
onRetry:mockOnRetry,
100+
};
101+
102+
const{ result}=renderHook(()=>useRetry(options));
103+
104+
act(()=>{
105+
result.current.startRetrying();
106+
});
107+
108+
// Wait for first retry to fail
109+
awaitact(async()=>{
110+
awaitPromise.resolve();
111+
});
112+
113+
// Fast forward to trigger second retry
114+
act(()=>{
115+
jest.advanceTimersByTime(1000);
116+
});
117+
118+
awaitact(async()=>{
119+
awaitPromise.resolve();
120+
});
121+
122+
// Should cap at maxDelay instead of 2000ms
123+
expect(result.current.currentDelay).toBe(1500);
124+
});
125+
126+
it("should stop retrying after max attempts",async()=>{
127+
mockOnRetry.mockRejectedValue(newError("Connection failed"));
128+
129+
const{ result}=renderHook(()=>
130+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
131+
);
132+
133+
act(()=>{
134+
result.current.startRetrying();
135+
});
136+
137+
// Simulate all retry attempts
138+
for(leti=0;i<defaultOptions.maxAttempts;i++){
139+
awaitact(async()=>{
140+
awaitPromise.resolve();
141+
});
142+
143+
if(i<defaultOptions.maxAttempts-1){
144+
// Fast forward to next retry
145+
act(()=>{
146+
jest.advanceTimersByTime(result.current.currentDelay||0);
147+
});
148+
}
149+
}
150+
151+
expect(mockOnRetry).toHaveBeenCalledTimes(defaultOptions.maxAttempts);
152+
expect(result.current.attemptCount).toBe(defaultOptions.maxAttempts);
153+
expect(result.current.currentDelay).toBe(null);
154+
expect(result.current.timeUntilNextRetry).toBe(null);
155+
});
156+
157+
it("should handle manual retry",async()=>{
158+
mockOnRetry.mockRejectedValueOnce(newError("Connection failed"));
159+
mockOnRetry.mockResolvedValueOnce(undefined);
160+
161+
const{ result}=renderHook(()=>
162+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
163+
);
164+
165+
act(()=>{
166+
result.current.startRetrying();
167+
});
168+
169+
// Wait for first retry to fail
170+
awaitact(async()=>{
171+
awaitPromise.resolve();
172+
});
173+
174+
expect(result.current.currentDelay).toBe(1000);
175+
176+
// Trigger manual retry before automatic retry
177+
act(()=>{
178+
result.current.retry();
179+
});
180+
181+
// Should cancel automatic retry
182+
expect(result.current.currentDelay).toBe(null);
183+
expect(result.current.timeUntilNextRetry).toBe(null);
184+
expect(result.current.isRetrying).toBe(true);
185+
186+
awaitact(async()=>{
187+
awaitPromise.resolve();
188+
});
189+
190+
// Should succeed and reset state
191+
expect(result.current.attemptCount).toBe(0);
192+
expect(result.current.isRetrying).toBe(false);
193+
});
194+
195+
it("should reset state when retry succeeds",async()=>{
196+
mockOnRetry.mockRejectedValueOnce(newError("Connection failed"));
197+
mockOnRetry.mockResolvedValueOnce(undefined);
198+
199+
const{ result}=renderHook(()=>
200+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
201+
);
202+
203+
act(()=>{
204+
result.current.startRetrying();
205+
});
206+
207+
// Wait for first retry to fail
208+
awaitact(async()=>{
209+
awaitPromise.resolve();
210+
});
211+
212+
expect(result.current.attemptCount).toBe(1);
213+
214+
// Fast forward to trigger second retry (which will succeed)
215+
act(()=>{
216+
jest.advanceTimersByTime(1000);
217+
});
218+
219+
awaitact(async()=>{
220+
awaitPromise.resolve();
221+
});
222+
223+
// Should reset all state
224+
expect(result.current.attemptCount).toBe(0);
225+
expect(result.current.isRetrying).toBe(false);
226+
expect(result.current.currentDelay).toBe(null);
227+
expect(result.current.timeUntilNextRetry).toBe(null);
228+
});
229+
230+
it("should stop retrying when stopRetrying is called",async()=>{
231+
mockOnRetry.mockRejectedValue(newError("Connection failed"));
232+
233+
const{ result}=renderHook(()=>
234+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
235+
);
236+
237+
act(()=>{
238+
result.current.startRetrying();
239+
});
240+
241+
// Wait for first retry to fail
242+
awaitact(async()=>{
243+
awaitPromise.resolve();
244+
});
245+
246+
expect(result.current.currentDelay).toBe(1000);
247+
248+
// Stop retrying
249+
act(()=>{
250+
result.current.stopRetrying();
251+
});
252+
253+
// Should reset all state
254+
expect(result.current.attemptCount).toBe(0);
255+
expect(result.current.isRetrying).toBe(false);
256+
expect(result.current.currentDelay).toBe(null);
257+
expect(result.current.timeUntilNextRetry).toBe(null);
258+
259+
// Fast forward past when retry would have happened
260+
act(()=>{
261+
jest.advanceTimersByTime(2000);
262+
});
263+
264+
// Should not have triggered additional retries
265+
expect(mockOnRetry).toHaveBeenCalledTimes(1);
266+
});
267+
268+
it("should update countdown timer correctly",async()=>{
269+
mockOnRetry.mockRejectedValue(newError("Connection failed"));
270+
271+
const{ result}=renderHook(()=>
272+
useRetry({ ...defaultOptions,onRetry:mockOnRetry}),
273+
);
274+
275+
act(()=>{
276+
result.current.startRetrying();
277+
});
278+
279+
// Wait for first retry to fail
280+
awaitact(async()=>{
281+
awaitPromise.resolve();
282+
});
283+
284+
expect(result.current.timeUntilNextRetry).toBe(1000);
285+
286+
// Advance time partially
287+
act(()=>{
288+
jest.advanceTimersByTime(300);
289+
});
290+
291+
// Should update countdown
292+
expect(result.current.timeUntilNextRetry).toBeLessThan(1000);
293+
expect(result.current.timeUntilNextRetry).toBeGreaterThan(0);
294+
});
295+
296+
it("should handle the specified backoff configuration",async()=>{
297+
mockOnRetry.mockRejectedValue(newError("Connection failed"));
298+
299+
// Test with the exact configuration from the issue
300+
constissueConfig={
301+
onRetry:mockOnRetry,
302+
maxAttempts:10,
303+
initialDelay:1000,// 1 second
304+
maxDelay:30000,// 30 seconds
305+
multiplier:2,
306+
};
307+
308+
const{ result}=renderHook(()=>useRetry(issueConfig));
309+
310+
act(()=>{
311+
result.current.startRetrying();
312+
});
313+
314+
// Test first few delays
315+
constexpectedDelays=[1000,2000,4000,8000,16000,30000];// Caps at 30000
316+
317+
for(leti=0;i<expectedDelays.length;i++){
318+
awaitact(async()=>{
319+
awaitPromise.resolve();
320+
});
321+
322+
if(i<expectedDelays.length-1){
323+
expect(result.current.currentDelay).toBe(expectedDelays[i]);
324+
act(()=>{
325+
jest.advanceTimersByTime(expectedDelays[i]);
326+
});
327+
}
328+
}
329+
});
330+
});

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp