1 Commits

Author SHA1 Message Date
gsxdsm
ae48065820 Fix dev server hang by reducing log spam and event frequency (#828)
* Changes from fix/dev-server-hang

* fix: Address PR #828 review feedback

- Reset RAF buffer on context changes (worktree switch, dev-server restart)
  to prevent stale output from flushing into new sessions
- Fix high-frequency WebSocket filter to catch auto-mode:event wrapping
  (auto_mode_progress is wrapped in auto-mode:event) and add feature:progress
- Reorder Vite aliases so explicit jsx-runtime entries aren't shadowed by
  the broad /^react(\/|$)/ regex (Vite uses first-match-wins)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: Batch dev server logs and fix React module resolution order

* feat: Add fallback timer for flushing dev server logs in background tabs

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 23:52:44 -08:00

View File

@@ -77,14 +77,21 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
// Buffer for batching rapid output events into fewer setState calls. // Buffer for batching rapid output events into fewer setState calls.
// Content accumulates here and is flushed via requestAnimationFrame, // Content accumulates here and is flushed via requestAnimationFrame,
// ensuring at most one React re-render per animation frame (~60fps max). // ensuring at most one React re-render per animation frame (~60fps max).
// A fallback setTimeout ensures the buffer is flushed even when RAF is
// throttled (e.g., when the tab is in the background).
const pendingOutputRef = useRef(''); const pendingOutputRef = useRef('');
const rafIdRef = useRef<number | null>(null); const rafIdRef = useRef<number | null>(null);
const timerIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const resetPendingOutput = useCallback(() => { const resetPendingOutput = useCallback(() => {
if (rafIdRef.current !== null) { if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current); cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null; rafIdRef.current = null;
} }
if (timerIdRef.current !== null) {
clearTimeout(timerIdRef.current);
timerIdRef.current = null;
}
pendingOutputRef.current = ''; pendingOutputRef.current = '';
}, []); }, []);
@@ -162,7 +169,12 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
}, [resetPendingOutput]); }, [resetPendingOutput]);
const flushPendingOutput = useCallback(() => { const flushPendingOutput = useCallback(() => {
// Clear both scheduling handles to prevent duplicate flushes
rafIdRef.current = null; rafIdRef.current = null;
if (timerIdRef.current !== null) {
clearTimeout(timerIdRef.current);
timerIdRef.current = null;
}
const content = pendingOutputRef.current; const content = pendingOutputRef.current;
if (!content) return; if (!content) return;
pendingOutputRef.current = ''; pendingOutputRef.current = '';
@@ -192,13 +204,31 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
* *
* Uses requestAnimationFrame to batch rapid output events into at most * Uses requestAnimationFrame to batch rapid output events into at most
* one React state update per frame, preventing excessive re-renders. * one React state update per frame, preventing excessive re-renders.
* A fallback setTimeout(250ms) ensures the buffer is flushed even when
* RAF is throttled (e.g., when the tab is in the background).
* If the pending buffer reaches MAX_LOG_BUFFER_SIZE, flushes immediately
* to prevent unbounded memory growth.
*/ */
const appendLogs = useCallback( const appendLogs = useCallback(
(content: string) => { (content: string) => {
pendingOutputRef.current += content; pendingOutputRef.current += content;
// Flush immediately if buffer has reached the size limit
if (pendingOutputRef.current.length >= MAX_LOG_BUFFER_SIZE) {
flushPendingOutput();
return;
}
// Schedule a RAF flush if not already scheduled
if (rafIdRef.current === null) { if (rafIdRef.current === null) {
rafIdRef.current = requestAnimationFrame(flushPendingOutput); rafIdRef.current = requestAnimationFrame(flushPendingOutput);
} }
// Schedule a fallback timer flush if not already scheduled,
// to handle cases where RAF is throttled (background tab)
if (timerIdRef.current === null) {
timerIdRef.current = setTimeout(flushPendingOutput, 250);
}
}, },
[flushPendingOutput] [flushPendingOutput]
); );