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>
  );
}