@@ -903,13 +903,40 @@ 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.
909- const backgroundAnalysisQueueRef = useRef < Set < string > > ( new Set ( ) )
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.
908+ //
909+ // Uses a simple promise-chain pattern: each node's analysis is chained onto
910+ // a single promise ref. No queue management, no running flags — just append
911+ // work to the chain. Nodes are tracked by FEN in a Set to avoid duplicates.
912+ const bgChainRef = useRef < Promise < void > > ( Promise . resolve ( ) )
913+ const bgAnalyzedFensRef = useRef < Set < string > > ( new Set ( ) )
914+ const bgCancelledRef = useRef ( false )
915+ const bgDrillIdRef = useRef < string | null > ( null )
916+ const ensureMaiaRef = useRef ( ensureMaiaForNode )
917+ const ensureStockfishRef = useRef ( ensureStockfishForNode )
918+ useEffect ( ( ) => {
919+ ensureMaiaRef . current = ensureMaiaForNode
920+ } , [ ensureMaiaForNode ] )
921+ useEffect ( ( ) => {
922+ ensureStockfishRef . current = ensureStockfishForNode
923+ } , [ ensureStockfishForNode ] )
910924
911925 useEffect ( ( ) => {
912- if ( ! currentDrillGame || isAnalyzingDrill ) return
926+ if ( ! currentDrillGame || isAnalyzingDrill ) {
927+ return
928+ }
929+
930+ // If drill changed, reset for the new drill
931+ if ( currentDrillGame . id !== bgDrillIdRef . current ) {
932+ bgCancelledRef . current = true
933+ // Let any in-flight work finish with the cancelled flag, then reset
934+ bgChainRef . current = bgChainRef . current . then ( ( ) => {
935+ bgCancelledRef . current = false
936+ } )
937+ bgAnalyzedFensRef . current = new Set ( )
938+ bgDrillIdRef . current = currentDrillGame . id
939+ }
913940
914941 const mainLine = gameTree . getMainLine ( )
915942 const openingEndNode = currentDrillGame . openingEndNode
@@ -918,50 +945,50 @@ export const useOpeningDrillController = (
918945 : 0
919946 const drillNodes = mainLine . slice ( startIndex )
920947
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
924- const hasMaia =
925- node . analysis . maia &&
926- MAIA_MODELS . every ( ( model ) => node . analysis . maia ?. [ model ] )
927- const hasStockfish =
928- node . analysis . stockfish &&
929- node . analysis . stockfish . depth >= DRILL_STOCKFISH_TARGET_DEPTH
930- return ! hasMaia || ! hasStockfish
931- } )
932-
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 ( )
948+ for ( const node of drillNodes ) {
949+ if ( bgAnalyzedFensRef . current . has ( node . fen ) ) continue
950+ bgAnalyzedFensRef . current . add ( node . fen )
949951
950- return ( ) => {
951- cancelled = true
952+ // Chain this node's analysis onto the promise chain.
953+ // Wrapped in try/catch so one failure doesn't break the whole chain.
954+ bgChainRef . current = bgChainRef . current . then ( async ( ) => {
955+ try {
956+ if ( bgCancelledRef . current ) return
957+ console . log ( '[bg] maia start:' , node . san || node . move || '?' )
958+ await ensureMaiaRef . current ( node )
959+ const hasMaia = ! ! (
960+ node . analysis . maia &&
961+ MAIA_MODELS . every ( ( m ) => node . analysis . maia ?. [ m ] )
962+ )
963+ console . log ( '[bg] maia done:' , hasMaia , '| sf start' )
964+ if ( bgCancelledRef . current ) return
965+ await ensureStockfishRef . current ( node )
966+ console . log (
967+ '[bg] sf done, depth:' ,
968+ node . analysis . stockfish ?. depth ?? 0 ,
969+ )
970+ } catch ( error ) {
971+ console . error ( '[bg] error analyzing node:' , error )
972+ }
973+ } )
952974 }
953- } , [
954- currentDrillGame ,
955- gameTree ,
956- isAnalyzingDrill ,
957- ensureMaiaForNode ,
958- ensureStockfishForNode ,
959- // Re-run when tree grows (new moves played)
960- treeController . currentNode ,
961- ] )
975+ } , [ currentDrillGame , gameTree , isAnalyzingDrill , treeController . currentNode ] )
976+
977+ // Stop background analysis. Signals cancellation and stops stockfish so
978+ // ensureDrillAnalysis can use stockfish immediately. The chain's remaining
979+ // .then() callbacks will see the cancelled flag and return quickly.
980+ // Does NOT await the chain — avoids hanging if a step is stuck.
981+ const stopBackgroundAnalysis = useCallback ( ( ) => {
982+ bgCancelledRef . current = true
983+ stockfish . stopEvaluation ( )
984+ } , [ stockfish ] )
962985
963986 const ensureDrillAnalysis = useCallback (
964987 async ( drillGame : OpeningDrillGame ) : Promise < boolean > => {
988+ // Signal background to stop and give the generator a tick to clean up
989+ stopBackgroundAnalysis ( )
990+ await delay ( 50 )
991+
965992 const mainLine = drillGame . tree . getMainLine ( )
966993 const startingNode = drillGame . openingEndNode || mainLine [ 0 ]
967994 const startIndex = startingNode
@@ -1036,7 +1063,12 @@ export const useOpeningDrillController = (
10361063
10371064 return ! wasCancelled
10381065 } ,
1039- [ ensureMaiaForNode , ensureStockfishForNode , setDrillAnalysisProgress ] ,
1066+ [
1067+ ensureMaiaForNode ,
1068+ ensureStockfishForNode ,
1069+ setDrillAnalysisProgress ,
1070+ stopBackgroundAnalysis ,
1071+ ] ,
10401072 )
10411073
10421074 const cancelDrillAnalysis = useCallback ( ( ) => {
0 commit comments