@@ -903,19 +903,19 @@ export const useOpeningDrillController = (
903903 [ currentMaiaModel , stockfish ] ,
904904 )
905905
906- // Background analysis: run Maia + Stockfish on positions as they are played
907- // so that post-drill analysis is already complete (or nearly so) when the
908- // drill ends.
906+ // Background analysis: run Maia + Stockfish on drill positions as they are
907+ // played so post-drill analysis has less (or no) work to do.
909908 //
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- // Refs to the latest versions of the analysis functions so the long-lived
918- // loop always calls the current closure without needing to restart.
909+ // There is a single Stockfish WASM instance, so we MUST NOT run background
910+ // and post-drill Stockfish concurrently. When the drill ends we:
911+ // 1. Set bgCancelledRef = true (loop exits after current position)
912+ // 2. Call stockfish.stopEvaluation() to abort any in-progress eval
913+ // 3. Await bgLoopPromiseRef to ensure the loop has fully exited
914+ // 4. Only then start ensureDrillAnalysis
915+ const bgQueueRef = useRef < GameNode [ ] > ( [ ] )
916+ const bgRunningRef = useRef ( false )
917+ const bgCancelledRef = useRef ( false )
918+ const bgLoopPromiseRef = useRef < Promise < void > | null > ( null )
919919 const ensureMaiaRef = useRef ( ensureMaiaForNode )
920920 const ensureStockfishRef = useRef ( ensureStockfishForNode )
921921 useEffect ( ( ) => {
@@ -925,51 +925,29 @@ export const useOpeningDrillController = (
925925 ensureStockfishRef . current = ensureStockfishForNode
926926 } , [ ensureStockfishForNode ] )
927927
928- // The long-lived loop that drains the queue.
929- // Uses refs for everything so it never needs to be recreated.
930- const runBackgroundLoop = useCallback ( async ( ) => {
931- if ( backgroundRunningRef . current ) return
932- backgroundRunningRef . current = true
933-
934- console . log ( '[bg-analysis] loop started' )
928+ const runBgLoop = useCallback ( async ( ) => {
929+ if ( bgRunningRef . current ) return
930+ bgRunningRef . current = true
935931 try {
936- while ( backgroundQueueRef . current . length > 0 ) {
937- if ( backgroundCancelledRef . current ) {
938- console . log ( '[bg-analysis] cancelled, stopping' )
939- break
940- }
941- const node = backgroundQueueRef . current [ 0 ]
942- console . log (
943- '[bg-analysis] processing node:' ,
944- node . fen . split ( ' ' ) . slice ( 0 , 2 ) . join ( ' ' ) ,
945- )
932+ while ( bgQueueRef . current . length > 0 ) {
933+ if ( bgCancelledRef . current ) break
934+ const node = bgQueueRef . current [ 0 ]
946935
947936 await ensureMaiaRef . current ( node )
948- if ( backgroundCancelledRef . current ) break
937+ if ( bgCancelledRef . current ) break
949938
950- console . log (
951- '[bg-analysis] maia done, starting stockfish for:' ,
952- node . fen . split ( ' ' ) . slice ( 0 , 2 ) . join ( ' ' ) ,
953- )
954939 await ensureStockfishRef . current ( node )
955-
956- // Only remove from queue after both analyses complete (or if cancelled)
957- backgroundQueueRef . current . shift ( )
958- console . log (
959- '[bg-analysis] node complete, remaining:' ,
960- backgroundQueueRef . current . length ,
961- )
940+ bgQueueRef . current . shift ( )
962941 }
963942 } catch ( error ) {
964943 console . error ( '[bg-analysis] error:' , error )
965944 } finally {
966- backgroundRunningRef . current = false
967- console . log ( '[bg-analysis] loop ended' )
945+ bgRunningRef . current = false
968946 }
969947 // eslint-disable-next-line react-hooks/exhaustive-deps
970948 } , [ ] )
971949
972- // Enqueue new drill nodes whenever the tree grows
950+ // Enqueue new drill positions for background analysis
973951 useEffect ( ( ) => {
974952 if ( ! currentDrillGame || isAnalyzingDrill ) return
975953
@@ -980,8 +958,7 @@ export const useOpeningDrillController = (
980958 : 0
981959 const drillNodes = mainLine . slice ( startIndex )
982960
983- // Track FENs already in the queue to avoid duplicates
984- const queuedFens = new Set ( backgroundQueueRef . current . map ( ( n ) => n . fen ) )
961+ const queuedFens = new Set ( bgQueueRef . current . map ( ( n ) => n . fen ) )
985962
986963 const newNodes = drillNodes . filter ( ( node ) => {
987964 if ( queuedFens . has ( node . fen ) ) return false
@@ -995,47 +972,47 @@ export const useOpeningDrillController = (
995972 } )
996973
997974 if ( newNodes . length > 0 ) {
998- console . log (
999- '[bg-analysis] enqueueing' ,
1000- newNodes . length ,
1001- 'nodes, loop running:' ,
1002- backgroundRunningRef . current ,
1003- )
1004- backgroundQueueRef . current . push ( ...newNodes )
1005- runBackgroundLoop ( )
975+ bgQueueRef . current . push ( ...newNodes )
976+ const promise = runBgLoop ( )
977+ if ( promise ) bgLoopPromiseRef . current = promise
1006978 }
1007979 } , [
1008980 currentDrillGame ,
1009981 gameTree ,
1010982 isAnalyzingDrill ,
1011- runBackgroundLoop ,
983+ runBgLoop ,
1012984 treeController . currentNode ,
1013985 ] )
1014986
1015- // Cancel background analysis when a new drill starts (not on every move).
1016- // currentDrillGame changes on every move, so we track the drill ID instead.
1017- const backgroundDrillIdRef = useRef < string | null > ( null )
987+ // Cancel background analysis when a new drill starts
988+ const bgDrillIdRef = useRef < string | null > ( null )
1018989 useEffect ( ( ) => {
1019990 const drillId = currentDrillGame ?. id ?? null
1020- if ( drillId !== backgroundDrillIdRef . current ) {
1021- // New drill or drill cleared — cancel any running background work
1022- backgroundCancelledRef . current = true
1023- backgroundQueueRef . current = [ ]
1024- backgroundRunningRef . current = false
1025-
1026- backgroundDrillIdRef . current = drillId
991+ if ( drillId !== bgDrillIdRef . current ) {
992+ bgCancelledRef . current = true
993+ bgQueueRef . current = [ ]
994+ bgDrillIdRef . current = drillId
1027995 if ( drillId ) {
1028- // Reset for the new drill
1029- backgroundCancelledRef . current = false
996+ bgCancelledRef . current = false
1030997 }
1031998 }
1032999 } , [ currentDrillGame ?. id ] )
10331000
1001+ // Helper: stop background loop and wait for it to fully exit
1002+ const stopBackgroundAnalysis = useCallback ( async ( ) => {
1003+ bgCancelledRef . current = true
1004+ bgQueueRef . current = [ ]
1005+ stockfish . stopEvaluation ( )
1006+ if ( bgLoopPromiseRef . current ) {
1007+ await bgLoopPromiseRef . current
1008+ bgLoopPromiseRef . current = null
1009+ }
1010+ } , [ stockfish ] )
1011+
10341012 const ensureDrillAnalysis = useCallback (
10351013 async ( drillGame : OpeningDrillGame ) : Promise < boolean > => {
1036- // Stop background analysis so it doesn't compete for stockfish
1037- backgroundCancelledRef . current = true
1038- backgroundQueueRef . current = [ ]
1014+ // Stop background loop and wait for it to fully exit before using stockfish
1015+ await stopBackgroundAnalysis ( )
10391016
10401017 const mainLine = drillGame . tree . getMainLine ( )
10411018 const startingNode = drillGame . openingEndNode || mainLine [ 0 ]
@@ -1111,7 +1088,12 @@ export const useOpeningDrillController = (
11111088
11121089 return ! wasCancelled
11131090 } ,
1114- [ ensureMaiaForNode , ensureStockfishForNode , setDrillAnalysisProgress ] ,
1091+ [
1092+ ensureMaiaForNode ,
1093+ ensureStockfishForNode ,
1094+ setDrillAnalysisProgress ,
1095+ stopBackgroundAnalysis ,
1096+ ] ,
11151097 )
11161098
11171099 const cancelDrillAnalysis = useCallback ( ( ) => {
0 commit comments