Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c115f13
feat: Phase 1 - Terminal reconnection foundation
blink-so[bot] Jul 1, 2025
c10349b
fix: Improve useRetry hook logic
blink-so[bot] Jul 1, 2025
3f45b74
fix: Complete useRetry hook implementation and tests
blink-so[bot] Jul 1, 2025
834b2e5
style: Apply biome formatting fixes to useRetry hook
blink-so[bot] Jul 1, 2025
d398265
fix: Use window.setTimeout/setInterval for browser compatibility
blink-so[bot] Jul 1, 2025
dd7adda
refactor: consolidate useRetry state with useReducer
blink-so[bot] Jul 1, 2025
5766fc0
Reset TerminalPage files to main branch state
blink-so[bot] Jul 2, 2025
b1e453b
Add useWithRetry hook for simplified retry functionality
blink-so[bot] Jul 2, 2025
d4326fb
Clean up useWithRetry hook implementation
blink-so[bot] Jul 2, 2025
8323192
Remove useRetry hook and replace with useWithRetry
blink-so[bot] Jul 2, 2025
3022566
Refactor useWithRetry hook according to specifications
blink-so[bot] Jul 2, 2025
bde014c
Preserve attemptCount when max attempts reached
blink-so[bot] Jul 2, 2025
cb363db
Fix formatting
BrunoQuaresma Jul 2, 2025
55036a4
Fix hook and tests
BrunoQuaresma Jul 2, 2025
000f0e4
feat(hooks): remove max attempts limit from useWithRetry hook
BrunoQuaresma Jul 3, 2025
f9832c0
refactor(hooks): remove attemptCount from useWithRetry state and rena…
BrunoQuaresma Jul 3, 2025
4fd4885
fix(hooks): update useWithRetry tests for nextRetryAt API and add use…
BrunoQuaresma Jul 3, 2025
7b14acd
fix(hooks): prevent race condition in useWithRetry after unmount
BrunoQuaresma Jul 3, 2025
323f6ba
Fix formatting
BrunoQuaresma Jul 3, 2025
062bfa5
fix(hooks): prevent duplicate calls to useWithRetry while loading
BrunoQuaresma Jul 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(hooks): update useWithRetry tests for nextRetryAt API and add use…
…EffectEvent

- Update all test references from retryAt to nextRetryAt to match the new API
- Add useEffectEvent import to stabilize function reference and prevent unnecessary re-renders
- Update callback dependency from fn to stableFn for better performance
- All tests now pass with the updated API

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
BrunoQuaresma and claude committed Jul 3, 2025
commit 4fd48855a4e490147a1b9262962e34ff6ccb9b92
42 changes: 21 additions & 21 deletions site/src/hooks/useWithRetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("useWithRetry", () => {
const { result } = renderHook(() => useWithRetry(mockFn));

expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).toBe(undefined);
expect(result.current.nextRetryAt).toBe(undefined);
});

it("should execute function successfully on first attempt", async () => {
Expand All @@ -34,7 +34,7 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).toBe(undefined);
expect(result.current.nextRetryAt).toBe(undefined);
});

it("should set isLoading to true during execution", async () => {
Expand Down Expand Up @@ -75,7 +75,7 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);

// Fast-forward to first retry (1 second)
await act(async () => {
Expand All @@ -84,7 +84,7 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(2);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);

// Fast-forward to second retry (2 seconds)
await act(async () => {
Expand All @@ -93,7 +93,7 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(3);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).toBe(undefined);
expect(result.current.nextRetryAt).toBe(undefined);
});

it("should continue retrying without limit", async () => {
Expand All @@ -108,7 +108,7 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);

// Fast-forward through multiple retries to verify it continues
for (let i = 1; i < 15; i++) {
Expand All @@ -118,11 +118,11 @@ describe("useWithRetry", () => {
});
expect(mockFn).toHaveBeenCalledTimes(i + 1);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);
}

// Should still be retrying after 15 attempts
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);
});

it("should respect max delay of 10 minutes", async () => {
Expand Down Expand Up @@ -150,7 +150,7 @@ describe("useWithRetry", () => {
}

expect(mockFn).toHaveBeenCalledTimes(9);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);

// The 9th retry should use max delay (600000ms = 10 minutes)
await act(async () => {
Expand All @@ -159,15 +159,15 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(10);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);

// Continue with more retries at max delay to verify it continues indefinitely
await act(async () => {
jest.advanceTimersByTime(600000);
});

expect(mockFn).toHaveBeenCalledTimes(11);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);
});

it("should cancel previous retry when call is invoked again", async () => {
Expand All @@ -184,7 +184,7 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);

// Call again before retry happens
await act(async () => {
Expand All @@ -193,7 +193,7 @@ describe("useWithRetry", () => {

expect(mockFn).toHaveBeenCalledTimes(2);
expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).toBe(undefined);
expect(result.current.nextRetryAt).toBe(undefined);

// Advance time to ensure previous retry was cancelled
await act(async () => {
Expand All @@ -203,7 +203,7 @@ describe("useWithRetry", () => {
expect(mockFn).toHaveBeenCalledTimes(2); // Should not have been called again
});

it("should set retryAt when scheduling retry", async () => {
it("should set nextRetryAt when scheduling retry", async () => {
mockFn
.mockRejectedValueOnce(new Error("Failure"))
.mockResolvedValueOnce(undefined);
Expand All @@ -215,21 +215,21 @@ describe("useWithRetry", () => {
await result.current.call();
});

const retryAt = result.current.retryAt;
expect(retryAt).not.toBe(null);
expect(retryAt).toBeInstanceOf(Date);
const nextRetryAt = result.current.nextRetryAt;
expect(nextRetryAt).not.toBe(null);
expect(nextRetryAt).toBeInstanceOf(Date);

// retryAt should be approximately 1 second in the future
// nextRetryAt should be approximately 1 second in the future
const expectedTime = Date.now() + 1000;
const actualTime = retryAt!.getTime();
const actualTime = nextRetryAt!.getTime();
expect(Math.abs(actualTime - expectedTime)).toBeLessThan(100); // Allow 100ms tolerance

// Advance past retry time
await act(async () => {
jest.advanceTimersByTime(1000);
});

expect(result.current.retryAt).toBe(undefined);
expect(result.current.nextRetryAt).toBe(undefined);
});

it("should cleanup timer on unmount", async () => {
Expand All @@ -243,7 +243,7 @@ describe("useWithRetry", () => {
});

expect(result.current.isLoading).toBe(false);
expect(result.current.retryAt).not.toBe(null);
expect(result.current.nextRetryAt).not.toBe(null);

// Unmount should cleanup timer
unmount();
Expand Down
7 changes: 5 additions & 2 deletions site/src/hooks/useWithRetry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffectEvent } from "./hookPolyfills";

const DELAY_MS = 1_000;
const MAX_DELAY_MS = 600_000; // 10 minutes
Expand Down Expand Up @@ -36,6 +37,8 @@ export function useWithRetry(fn: () => Promise<void>): UseWithRetryResult {
}
}, []);

const stableFn = useEffectEvent(fn)

const call = useCallback(async () => {
clearTimeout();

Expand All @@ -46,7 +49,7 @@ export function useWithRetry(fn: () => Promise<void>): UseWithRetryResult {
});

try {
await fn();
await stableFn();
setState({ isLoading: false, nextRetryAt: undefined });
} catch (error) {
const delayMs = Math.min(
Expand All @@ -70,7 +73,7 @@ export function useWithRetry(fn: () => Promise<void>): UseWithRetryResult {
};

await executeAttempt(0);
}, [fn, clearTimeout]);
}, [stableFn, clearTimeout]);

useEffect(() => {
return () => {
Expand Down