Merged tasks are endlessly re-rendering (vibe-kanban) (#938)

* ##  Fix Complete

**Problem:** Merged tasks caused endless WebSocket reconnections because `useJsonPatchWsStream` incorrectly treated `{finished: true}` messages as signals to reconnect.

**Solution:** Modified [useJsonPatchWsStream.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/0f2d-merged-tasks-are/frontend/src/hooks/useJsonPatchWsStream.ts) to treat "finished" as terminal:

1. Added `finishedRef` to track when stream completes
2. On `{finished: true}`: set flag, close cleanly (code 1000), **no reconnect**
3. On socket close: skip reconnection if finished flag is set or clean close
4. Reset flag on cleanup and new connections

**Result:**
- Merged tasks connect once, receive final state, and stop cleanly
- Active tasks still reconnect on network errors
- Aligned behavior with existing `streamJsonPatchEntries` utility
- All type checks pass 

* Cleanup script changes for task attempt 0f2d0086-1de2-4517-a023-1ee8cf133181
This commit is contained in:
Louis Knight-Webb
2025-10-06 12:49:08 +01:00
committed by GitHub
parent fef06cf00e
commit 73f49cae9f

View File

@@ -40,6 +40,7 @@ export const useJsonPatchWsStream = <T>(
const retryTimerRef = useRef<number | null>(null); const retryTimerRef = useRef<number | null>(null);
const retryAttemptsRef = useRef<number>(0); const retryAttemptsRef = useRef<number>(0);
const [retryNonce, setRetryNonce] = useState(0); const [retryNonce, setRetryNonce] = useState(0);
const finishedRef = useRef<boolean>(false);
function scheduleReconnect() { function scheduleReconnect() {
if (retryTimerRef.current) return; // already scheduled if (retryTimerRef.current) return; // already scheduled
@@ -64,6 +65,7 @@ export const useJsonPatchWsStream = <T>(
retryTimerRef.current = null; retryTimerRef.current = null;
} }
retryAttemptsRef.current = 0; retryAttemptsRef.current = 0;
finishedRef.current = false;
setData(undefined); setData(undefined);
setIsConnected(false); setIsConnected(false);
setError(null); setError(null);
@@ -85,6 +87,9 @@ export const useJsonPatchWsStream = <T>(
// Create WebSocket if it doesn't exist // Create WebSocket if it doesn't exist
if (!wsRef.current) { if (!wsRef.current) {
// Reset finished flag for new connection
finishedRef.current = false;
// Convert HTTP endpoint to WebSocket endpoint // Convert HTTP endpoint to WebSocket endpoint
const wsEndpoint = endpoint.replace(/^http/, 'ws'); const wsEndpoint = endpoint.replace(/^http/, 'ws');
const ws = new WebSocket(wsEndpoint); const ws = new WebSocket(wsEndpoint);
@@ -124,13 +129,12 @@ export const useJsonPatchWsStream = <T>(
} }
// Handle finished messages ({finished: true}) // Handle finished messages ({finished: true})
// Treat finished as terminal - do NOT reconnect
if ('finished' in msg) { if ('finished' in msg) {
ws.close(); finishedRef.current = true;
ws.close(1000, 'finished');
wsRef.current = null; wsRef.current = null;
setIsConnected(false); setIsConnected(false);
// Treat finished as terminal and schedule reconnect; servers may rotate
retryAttemptsRef.current += 1;
scheduleReconnect();
} }
} catch (err) { } catch (err) {
console.error('Failed to process WebSocket message:', err); console.error('Failed to process WebSocket message:', err);
@@ -142,9 +146,16 @@ export const useJsonPatchWsStream = <T>(
setError('Connection failed'); setError('Connection failed');
}; };
ws.onclose = () => { ws.onclose = (evt) => {
setIsConnected(false); setIsConnected(false);
wsRef.current = null; wsRef.current = null;
// Do not reconnect if we received a finished message or clean close
if (finishedRef.current || (evt?.code === 1000 && evt?.wasClean)) {
return;
}
// Otherwise, reconnect on unexpected/error closures
retryAttemptsRef.current += 1; retryAttemptsRef.current += 1;
scheduleReconnect(); scheduleReconnect();
}; };
@@ -170,6 +181,7 @@ export const useJsonPatchWsStream = <T>(
window.clearTimeout(retryTimerRef.current); window.clearTimeout(retryTimerRef.current);
retryTimerRef.current = null; retryTimerRef.current = null;
} }
finishedRef.current = false;
dataRef.current = undefined; dataRef.current = undefined;
setData(undefined); setData(undefined);
}; };