File size: 3,743 Bytes
d0b1ea0 c3e7865 fa1717e e4daa3b 900a32d e4daa3b 900a32d c3e7865 e4daa3b d0b1ea0 e4daa3b d0b1ea0 e4daa3b d0b1ea0 c3e7865 d0b1ea0 c3e7865 d0b1ea0 c3e7865 fa1717e d0b1ea0 e4daa3b c3e7865 e4daa3b d0b1ea0 e4daa3b c3e7865 e4daa3b 900a32d e4daa3b 900a32d e4daa3b 900a32d e4daa3b 900a32d e4daa3b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
import { useEffect, useState } from "react";
import { apiClient, ApiError } from "../api/client";
import { MAX_COLD_START_RETRIES, getRetryDelay } from "../utils/retry";
interface CaseSelectorProps {
selectedCase: string | null;
onSelectCase: (caseId: string) => void;
}
export function CaseSelector({
selectedCase,
onSelectCase,
}: CaseSelectorProps) {
const [cases, setCases] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [retryCount, setRetryCount] = useState(0);
const [isWakingUp, setIsWakingUp] = useState(false);
// Fetch cases on mount with cold-start retry logic
// Using inline async function pattern recommended by React docs for data fetching
useEffect(() => {
let isActive = true;
const abortController = new AbortController();
async function fetchCases() {
let attempts = 0;
while (attempts <= MAX_COLD_START_RETRIES && isActive) {
try {
const data = await apiClient.getCases(abortController.signal);
if (!isActive) return;
setCases(data.cases);
setIsWakingUp(false);
setRetryCount(0);
setIsLoading(false);
return; // Success
} catch (err) {
if (!isActive) return;
if (err instanceof Error && err.name === "AbortError") return;
const is503 = err instanceof ApiError && err.status === 503;
const isNetworkError =
err instanceof TypeError &&
err.message.toLowerCase().includes("fetch");
// Retry on cold start (503) or network errors
if ((is503 || isNetworkError) && attempts < MAX_COLD_START_RETRIES) {
attempts++;
setRetryCount(attempts);
setIsWakingUp(true);
// Exponential backoff with capped maximum
await new Promise((resolve) =>
setTimeout(resolve, getRetryDelay(attempts)),
);
continue;
}
// Max retries exceeded or non-retryable error
const message =
is503 || isNetworkError
? "Backend failed to wake up. Please refresh the page."
: err instanceof Error
? err.message
: "Unknown error";
setError(`Failed to load cases: ${message}`);
setIsWakingUp(false);
setIsLoading(false);
return;
}
}
}
fetchCases();
return () => {
isActive = false;
abortController.abort();
};
}, []);
if (isLoading) {
return (
<div className="bg-gray-800 rounded-lg p-4">
{isWakingUp ? (
<p className="text-yellow-400">
Backend waking up... Retry {retryCount}/{MAX_COLD_START_RETRIES}
</p>
) : (
<p className="text-gray-400">Loading cases...</p>
)}
</div>
);
}
if (error) {
return (
<div className="bg-red-900/50 rounded-lg p-4">
<p className="text-red-300">{error}</p>
</div>
);
}
return (
<div className="bg-gray-800 rounded-lg p-4">
<label className="block text-sm font-medium mb-2">Select Case</label>
<select
value={selectedCase || ""}
onChange={(e) => onSelectCase(e.target.value)}
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-3 py-2
text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Choose a case...</option>
{cases.map((caseId) => (
<option key={caseId} value={caseId}>
{caseId}
</option>
))}
</select>
</div>
);
}
|