Skip to content

The 10s auto-refresh on the code dashboard makes the Refresh button strobe, and overlapping responses can arrive out of order #40

Description

@loganbail
  const loadSessions = async () => {
    setIsLoading(true)
    setError(null)
    try {
      const data = await listSessions()
      setSessions(data.sessions)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to load sessions')
    } finally {
      setIsLoading(false)
    }
  }

  useEffect(() => {
    loadSessions()
    const interval = setInterval(loadSessions, 10000)
    return () => clearInterval(interval)
  }, [])

(frontend/src/pages/Code/CodeDashboard.tsx:32-49)

Three separate problems compound here.

1. isLoading is shared between the background poll and the explicit user action. The Refresh button is bound to it:

          <Button variant="outline" onClick={loadSessions} disabled={isLoading}>
            <RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />

(CodeDashboard.tsx:64-65)

Trigger: leave the page open. Observed: every 10 seconds the button greys out and its icon spins for the duration of the request, with no user action — and a click landing in that window is dropped, because the button is disabled. Expected: background refresh is invisible; the spinner means "you asked for this".

2. No in-flight guard. If the backend takes longer than 10s (plausible while a code-generation session is running and the process is loaded), a second poll fires while the first is outstanding. Whichever setSessions resolves last wins, regardless of which request was issued last — a classic response-ordering race. The stale list then persists for a full 10s cycle.

3. No abort on unmount. clearInterval stops future polls, but an outstanding listSessions() still resolves and calls setSessions/setIsLoading on an unmounted component. React 18 no longer warns about this, which makes it easier to miss, but it is still a leak of a component that should be gone — and with React.StrictMode on (main.tsx:20) the mount/unmount/remount cycle in dev doubles it.

A version that fixes all three:

const inFlight = useRef(false)

const loadSessions = useCallback(async (opts?: { userInitiated?: boolean }) => {
  if (inFlight.current) return
  inFlight.current = true
  if (opts?.userInitiated) setIsRefreshing(true)
  try {
    const data = await listSessions()
    setSessions(data.sessions)
    setError(null)
  } catch (err) {
    setError(err instanceof Error ? err.message : 'Failed to load sessions')
  } finally {
    inFlight.current = false
    setIsRefreshing(false)
    setInitialLoad(false)
  }
}, [])

Worth noting: @tanstack/react-query is already a dependency (package.json:21) and QueryClient is already provisioned at main.tsx:8. useQuery with refetchInterval: 10000 gives you the in-flight guard, the ordering guarantee, the separate isFetching/isLoading flags, and cancellation for free. Several other pages in this repo hand-roll the same polling loop — CodeProjectWorkspace.tsx:177, IdeaGenerationPanel.tsx:267, PlanGenerationPanel.tsx:390 — so there is a real consolidation win here, not just a local fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions