@@ -10,6 +10,72 @@ import toast from 'react-hot-toast'
1010import { StockfishStatus , StockfishEngine } from 'src/types'
1111import Engine from 'src/lib/engine/stockfish'
1212
13+ const STOCKFISH_LOADING_TOAST_DELAY_MS = 800
14+ const STOCKFISH_DEBUG_LOADING_KEY = 'maia.stockfishDebugLoading'
15+
16+ const isTruthy = ( value : string | null | undefined ) : boolean => {
17+ if ( ! value ) return false
18+ return [ '1' , 'true' , 'yes' , 'on' ] . includes ( value . toLowerCase ( ) )
19+ }
20+
21+ const isStockfishDebugLoadingEnabled = ( ) : boolean => {
22+ if ( typeof window !== 'undefined' ) {
23+ try {
24+ const localValue = window . localStorage . getItem (
25+ STOCKFISH_DEBUG_LOADING_KEY ,
26+ )
27+ if ( localValue !== null ) return isTruthy ( localValue )
28+ } catch {
29+ // ignore localStorage access failures
30+ }
31+ }
32+
33+ return isTruthy ( process . env . NEXT_PUBLIC_STOCKFISH_DEBUG_LOADING )
34+ }
35+
36+ const getStockfishLoadingLabel = (
37+ engine : Engine | null ,
38+ debugLoadingEnabled : boolean ,
39+ ) : string => {
40+ if ( ! debugLoadingEnabled ) {
41+ return 'Loading Stockfish...'
42+ }
43+
44+ if ( ! engine ) return 'Loading Stockfish...'
45+
46+ switch ( engine . initializationPhase ) {
47+ case 'checking-cache' :
48+ return 'Checking local Stockfish cache...'
49+ case 'downloading-nnue' :
50+ return 'Downloading Stockfish model weights...'
51+ case 'loading-nnue' :
52+ return 'Loading Stockfish from local cache...'
53+ case 'loading-module' :
54+ return 'Starting Stockfish engine...'
55+ default :
56+ return 'Loading Stockfish...'
57+ }
58+ }
59+
60+ let sharedClientStockfishEngine : Engine | null = null
61+
62+ const getOrCreateStockfishEngine = ( ) : Engine => {
63+ if ( typeof window === 'undefined' ) {
64+ return new Engine ( )
65+ }
66+
67+ if (
68+ ! sharedClientStockfishEngine ||
69+ ( sharedClientStockfishEngine . initializationError &&
70+ ! sharedClientStockfishEngine . ready &&
71+ ! sharedClientStockfishEngine . initializing )
72+ ) {
73+ sharedClientStockfishEngine = new Engine ( )
74+ }
75+
76+ return sharedClientStockfishEngine
77+ }
78+
1379export const StockfishEngineContext = React . createContext < StockfishEngine > ( {
1480 streamEvaluations : ( ) => {
1581 throw new Error (
@@ -32,13 +98,24 @@ export const StockfishEngineContextProvider: React.FC<{
3298 children : ReactNode
3399} > = ( { children } : { children : ReactNode } ) => {
34100 const engineRef = useRef < Engine | null > ( null )
35- const [ status , setStatus ] = useState < StockfishStatus > ( 'loading' )
36- const [ error , setError ] = useState < string | null > ( null )
37- const toastId = useRef < string | null > ( null )
38-
39101 if ( ! engineRef . current ) {
40- engineRef . current = new Engine ( )
102+ engineRef . current = getOrCreateStockfishEngine ( )
41103 }
104+ const [ status , setStatus ] = useState < StockfishStatus > ( ( ) => {
105+ if ( engineRef . current ?. initializationError ) return 'error'
106+ return engineRef . current ?. ready ? 'ready' : 'loading'
107+ } )
108+ const [ error , setError ] = useState < string | null > (
109+ ( ) => engineRef . current ?. initializationError ?? null ,
110+ )
111+ const [ debugLoadingEnabled ] = useState < boolean > ( ( ) =>
112+ isStockfishDebugLoadingEnabled ( ) ,
113+ )
114+ const [ loadingLabel , setLoadingLabel ] = useState < string > ( ( ) =>
115+ getStockfishLoadingLabel ( engineRef . current , debugLoadingEnabled ) ,
116+ )
117+ const toastId = useRef < string | null > ( null )
118+ const loadingToastTimerRef = useRef < number | null > ( null )
42119
43120 const streamEvaluations = useCallback (
44121 ( fen : string , legalMoveCount : number , depth ?: number ) => {
@@ -61,10 +138,21 @@ export const StockfishEngineContextProvider: React.FC<{
61138
62139 useEffect ( ( ) => {
63140 const checkEngineStatus = ( ) => {
64- if ( engineRef . current ?. ready ) {
141+ const engine = engineRef . current
142+ if ( ! engine ) return
143+
144+ setLoadingLabel ( ( prev ) => {
145+ const next = getStockfishLoadingLabel ( engine , debugLoadingEnabled )
146+ return prev === next ? prev : next
147+ } )
148+
149+ if ( engine . initializationError ) {
150+ setStatus ( 'error' )
151+ setError ( engine . initializationError )
152+ } else if ( engine . ready ) {
65153 setStatus ( 'ready' )
66154 setError ( null )
67- } else if ( engineRef . current && ! engineRef . current . ready ) {
155+ } else {
68156 setStatus ( 'loading' )
69157 }
70158 }
@@ -73,38 +161,81 @@ export const StockfishEngineContextProvider: React.FC<{
73161 const interval = setInterval ( checkEngineStatus , 100 )
74162
75163 return ( ) => clearInterval ( interval )
76- } , [ ] )
164+ } , [ debugLoadingEnabled ] )
77165
78166 // Toast notifications for Stockfish engine status
79167 useEffect ( ( ) => {
80168 return ( ) => {
81- toast . dismiss ( )
169+ if (
170+ loadingToastTimerRef . current !== null &&
171+ typeof window !== 'undefined'
172+ ) {
173+ window . clearTimeout ( loadingToastTimerRef . current )
174+ loadingToastTimerRef . current = null
175+ }
176+ if ( toastId . current ) {
177+ toast . dismiss ( toastId . current )
178+ toastId . current = null
179+ }
82180 }
83181 } , [ ] )
84182
85183 useEffect ( ( ) => {
86- if ( status === 'loading' && ! toastId . current ) {
87- toastId . current = toast . loading ( 'Loading Stockfish Engine...' )
88- } else if ( status === 'ready' ) {
184+ if ( status === 'loading' ) {
185+ if ( typeof window === 'undefined' ) {
186+ return
187+ }
188+
189+ if ( toastId . current ) {
190+ toastId . current = toast . loading ( loadingLabel , { id : toastId . current } )
191+ return
192+ }
193+
194+ if ( loadingToastTimerRef . current !== null ) {
195+ return
196+ }
197+
198+ loadingToastTimerRef . current = window . setTimeout ( ( ) => {
199+ loadingToastTimerRef . current = null
200+ if ( ! toastId . current && engineRef . current && ! engineRef . current . ready ) {
201+ toastId . current = toast . loading (
202+ getStockfishLoadingLabel ( engineRef . current , debugLoadingEnabled ) ,
203+ )
204+ }
205+ } , STOCKFISH_LOADING_TOAST_DELAY_MS )
206+ return
207+ }
208+
209+ if (
210+ loadingToastTimerRef . current !== null &&
211+ typeof window !== 'undefined'
212+ ) {
213+ window . clearTimeout ( loadingToastTimerRef . current )
214+ loadingToastTimerRef . current = null
215+ }
216+
217+ if ( status === 'ready' ) {
218+ // Only show a success toast when we previously showed a loading toast.
89219 if ( toastId . current ) {
90220 toast . success ( 'Loaded Stockfish! Engine is ready' , {
91221 id : toastId . current ,
92222 } )
93223 toastId . current = null
94- } else {
95- toast . success ( 'Loaded Stockfish! Engine is ready' )
96224 }
97225 } else if ( status === 'error' ) {
226+ const message = error
227+ ? `Failed to load Stockfish engine: ${ error } `
228+ : 'Failed to load Stockfish engine'
98229 if ( toastId . current ) {
99- toast . error ( 'Failed to load Stockfish engine' , {
230+ toast . error ( message , {
100231 id : toastId . current ,
101232 } )
102233 toastId . current = null
103234 } else {
104- toast . error ( 'Failed to load Stockfish engine' )
235+ toast . error ( message )
105236 }
106237 }
107- } , [ status ] )
238+ } , [ status , error , loadingLabel , debugLoadingEnabled ] )
108239
109240 const contextValue = useMemo (
110241 ( ) => ( {
0 commit comments