Skip to content
Merged
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
refactor: consolidate useRetry state with useReducer
Convert useRetry hook from multiple useState calls to a single useReducer
for cleaner state management. This improves code clarity and makes state
transitions more predictable.

Changes:
- Replace 5 useState calls with single useReducer
- Add RetryState interface and RetryAction union type
- Implement retryReducer function for all state transitions
- Update all state access to use state object
- Replace setState calls with dispatch calls throughout

Co-authored-by: BrunoQuaresma <3165839+BrunoQuaresma@users.noreply.github.com>
  • Loading branch information
blink-so[bot] and BrunoQuaresma committed Jul 1, 2025
commit dd7adda3b8f32b8d56fec26cbb86b4c2bbea5b82
144 changes: 104 additions & 40 deletions site/src/hooks/useRetry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useReducer, useRef } from "react";
import { useEffectEvent } from "./hookPolyfills";

interface UseRetryOptions {
Expand Down Expand Up @@ -55,18 +55,89 @@ interface UseRetryReturn {
stopRetrying: () => void;
}

interface RetryState {
isRetrying: boolean;
currentDelay: number | null;
attemptCount: number;
timeUntilNextRetry: number | null;
isManualRetry: boolean;
}

type RetryAction =
| { type: "START_RETRY" }
| { type: "RETRY_SUCCESS" }
| { type: "RETRY_FAILURE" }
| { type: "SCHEDULE_RETRY"; delay: number }
| { type: "UPDATE_COUNTDOWN"; timeRemaining: number }
| { type: "CANCEL_RETRY" }
| { type: "RESET" }
| { type: "SET_MANUAL_RETRY"; isManual: boolean };

const initialState: RetryState = {
isRetrying: false,
currentDelay: null,
attemptCount: 0,
timeUntilNextRetry: null,
isManualRetry: false,
};

function retryReducer(state: RetryState, action: RetryAction): RetryState {
switch (action.type) {
case "START_RETRY":
return {
...state,
isRetrying: true,
currentDelay: null,
timeUntilNextRetry: null,
attemptCount: state.attemptCount + 1,
};
case "RETRY_SUCCESS":
return {
...initialState,
};
case "RETRY_FAILURE":
return {
...state,
isRetrying: false,
isManualRetry: false,
};
case "SCHEDULE_RETRY":
return {
...state,
currentDelay: action.delay,
timeUntilNextRetry: action.delay,
};
case "UPDATE_COUNTDOWN":
return {
...state,
timeUntilNextRetry: action.timeRemaining,
};
case "CANCEL_RETRY":
return {
...state,
currentDelay: null,
timeUntilNextRetry: null,
};
case "RESET":
return {
...initialState,
};
case "SET_MANUAL_RETRY":
return {
...state,
isManualRetry: action.isManual,
};
default:
return state;
}
}

/**
* Hook for handling exponential backoff retry logic
*/
export function useRetry(options: UseRetryOptions): UseRetryReturn {
const { onRetry, maxAttempts, initialDelay, maxDelay, multiplier } = options;
const [isRetrying, setIsRetrying] = useState(false);
const [currentDelay, setCurrentDelay] = useState<number | null>(null);
const [attemptCount, setAttemptCount] = useState(0);
const [timeUntilNextRetry, setTimeUntilNextRetry] = useState<number | null>(
null,
);
const [isManualRetry, setIsManualRetry] = useState(false);
const [state, dispatch] = useReducer(retryReducer, initialState);

const timeoutRef = useRef<number | null>(null);
const countdownRef = useRef<number | null>(null);
Expand Down Expand Up @@ -95,23 +166,16 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
);

const performRetry = useCallback(async () => {
setIsRetrying(true);
setTimeUntilNextRetry(null);
setCurrentDelay(null);
dispatch({ type: "START_RETRY" });
clearTimers();
// Increment attempt count when starting the retry
setAttemptCount((prev) => prev + 1);

try {
await onRetryEvent();
// If retry succeeds, reset everything
setAttemptCount(0);
setIsRetrying(false);
setIsManualRetry(false);
dispatch({ type: "RETRY_SUCCESS" });
} catch (error) {
// If retry fails, just update state (attemptCount already incremented)
setIsRetrying(false);
setIsManualRetry(false);
// If retry fails, just update state
dispatch({ type: "RETRY_FAILURE" });
}
}, [onRetryEvent, clearTimers]);

Expand All @@ -123,16 +187,15 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {

// Calculate delay based on attempt - 1 (so first retry gets initialDelay)
const delay = calculateDelay(Math.max(0, attempt - 1));
setCurrentDelay(delay);
setTimeUntilNextRetry(delay);
dispatch({ type: "SCHEDULE_RETRY", delay });
startTimeRef.current = Date.now();

// Start countdown timer
countdownRef.current = window.setInterval(() => {
if (startTimeRef.current) {
const elapsed = Date.now() - startTimeRef.current;
const remaining = Math.max(0, delay - elapsed);
setTimeUntilNextRetry(remaining);
dispatch({ type: "UPDATE_COUNTDOWN", timeRemaining: remaining });

if (remaining <= 0) {
if (countdownRef.current) {
Expand All @@ -154,20 +217,25 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {
// Effect to schedule next retry after a failed attempt
useEffect(() => {
if (
!isRetrying &&
!isManualRetry &&
attemptCount > 0 &&
attemptCount < maxAttempts
!state.isRetrying &&
!state.isManualRetry &&
state.attemptCount > 0 &&
state.attemptCount < maxAttempts
) {
scheduleNextRetry(attemptCount);
scheduleNextRetry(state.attemptCount);
}
}, [attemptCount, isRetrying, isManualRetry, maxAttempts, scheduleNextRetry]);
}, [
state.attemptCount,
state.isRetrying,
state.isManualRetry,
maxAttempts,
scheduleNextRetry,
]);

const retry = useCallback(() => {
setIsManualRetry(true);
dispatch({ type: "SET_MANUAL_RETRY", isManual: true });
clearTimers();
setTimeUntilNextRetry(null);
setCurrentDelay(null);
dispatch({ type: "CANCEL_RETRY" });
performRetry();
}, [clearTimers, performRetry]);

Expand All @@ -178,11 +246,7 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {

const stopRetrying = useCallback(() => {
clearTimers();
setIsRetrying(false);
setCurrentDelay(null);
setAttemptCount(0);
setTimeUntilNextRetry(null);
setIsManualRetry(false);
dispatch({ type: "RESET" });
}, [clearTimers]);

// Cleanup on unmount
Expand All @@ -194,10 +258,10 @@ export function useRetry(options: UseRetryOptions): UseRetryReturn {

return {
retry,
isRetrying,
currentDelay,
attemptCount,
timeUntilNextRetry,
isRetrying: state.isRetrying,
currentDelay: state.currentDelay,
attemptCount: state.attemptCount,
timeUntilNextRetry: state.timeUntilNextRetry,
startRetrying,
stopRetrying,
};
Expand Down
Loading