Skip to content

Commit 09ed63f

Browse files
fix: background drill analysis now persists across moves
The previous implementation used a useEffect cleanup to cancel the background Stockfish/Maia analysis loop on every dependency change (including every new move). This killed evaluations mid-stream and marked nodes as queued without completing them. Replaced with a ref-based queue and persistent async loop that only cancels when the drill session itself changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f9e658b commit 09ed63f

1 file changed

Lines changed: 49 additions & 26 deletions

File tree

src/hooks/useOpeningDrillController/useOpeningDrillController.ts

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -906,8 +906,38 @@ export const useOpeningDrillController = (
906906
// Background analysis: run Maia + Stockfish on positions as they are played
907907
// so that post-drill analysis is already complete (or nearly so) when the
908908
// drill ends.
909-
const backgroundAnalysisQueueRef = useRef<Set<string>>(new Set())
909+
//
910+
// We use a ref-based queue so the async loop persists across re-renders.
911+
// The useEffect only *enqueues* new nodes; the loop processes them
912+
// independently of the React lifecycle to avoid being killed on every move.
913+
const backgroundQueueRef = useRef<GameNode[]>([])
914+
const backgroundRunningRef = useRef(false)
915+
const backgroundCancelledRef = useRef(false)
916+
917+
// The long-lived loop that drains the queue
918+
const runBackgroundLoop = useCallback(async () => {
919+
if (backgroundRunningRef.current) return
920+
backgroundRunningRef.current = true
910921

922+
try {
923+
while (backgroundQueueRef.current.length > 0) {
924+
if (backgroundCancelledRef.current) break
925+
const node = backgroundQueueRef.current[0]
926+
927+
await ensureMaiaForNode(node)
928+
if (backgroundCancelledRef.current) break
929+
930+
await ensureStockfishForNode(node)
931+
932+
// Only remove from queue after both analyses complete (or if cancelled)
933+
backgroundQueueRef.current.shift()
934+
}
935+
} finally {
936+
backgroundRunningRef.current = false
937+
}
938+
}, [ensureMaiaForNode, ensureStockfishForNode])
939+
940+
// Enqueue new drill nodes whenever the tree grows
911941
useEffect(() => {
912942
if (!currentDrillGame || isAnalyzingDrill) return
913943

@@ -918,9 +948,11 @@ export const useOpeningDrillController = (
918948
: 0
919949
const drillNodes = mainLine.slice(startIndex)
920950

921-
// Find nodes that still need analysis and haven't been queued yet
922-
const nodesNeedingWork = drillNodes.filter((node) => {
923-
if (backgroundAnalysisQueueRef.current.has(node.fen)) return false
951+
// Track FENs already in the queue to avoid duplicates
952+
const queuedFens = new Set(backgroundQueueRef.current.map((n) => n.fen))
953+
954+
const newNodes = drillNodes.filter((node) => {
955+
if (queuedFens.has(node.fen)) return false
924956
const hasMaia =
925957
node.analysis.maia &&
926958
MAIA_MODELS.every((model) => node.analysis.maia?.[model])
@@ -930,36 +962,27 @@ export const useOpeningDrillController = (
930962
return !hasMaia || !hasStockfish
931963
})
932964

933-
if (nodesNeedingWork.length === 0) return
934-
935-
let cancelled = false
936-
937-
const runBackgroundAnalysis = async () => {
938-
for (const node of nodesNeedingWork) {
939-
if (cancelled) break
940-
backgroundAnalysisQueueRef.current.add(node.fen)
941-
await ensureMaiaForNode(node)
942-
if (cancelled) break
943-
// Let Stockfish finish its current evaluation even if new moves come in
944-
await ensureStockfishForNode(node)
945-
}
946-
}
947-
948-
runBackgroundAnalysis()
949-
950-
return () => {
951-
cancelled = true
965+
if (newNodes.length > 0) {
966+
backgroundQueueRef.current.push(...newNodes)
967+
runBackgroundLoop()
952968
}
953969
}, [
954970
currentDrillGame,
955971
gameTree,
956972
isAnalyzingDrill,
957-
ensureMaiaForNode,
958-
ensureStockfishForNode,
959-
// Re-run when tree grows (new moves played)
973+
runBackgroundLoop,
960974
treeController.currentNode,
961975
])
962976

977+
// Cancel background analysis when drill session resets
978+
useEffect(() => {
979+
backgroundCancelledRef.current = false
980+
return () => {
981+
backgroundCancelledRef.current = true
982+
backgroundQueueRef.current = []
983+
}
984+
}, [currentDrillGame])
985+
963986
const ensureDrillAnalysis = useCallback(
964987
async (drillGame: OpeningDrillGame): Promise<boolean> => {
965988
const mainLine = drillGame.tree.getMainLine()

0 commit comments

Comments
 (0)