@@ -4,6 +4,8 @@ import StockfishWeb from 'lila-stockfish-web'
44import { StockfishEvaluation } from 'src/types'
55import { StockfishModelStorage } from './stockfishStorage'
66
7+ const DEFAULT_NNUE_FETCH_TIMEOUT_MS = 30000
8+
79class Engine {
810 private fen : string
911 private moves : string [ ]
@@ -21,6 +23,8 @@ class Engine {
2123 private evaluationRejecter : ( ( reason ?: unknown ) => void ) | null
2224 private evaluationPromise : Promise < StockfishEvaluation > | null
2325 private evaluationGenerator : AsyncGenerator < StockfishEvaluation > | null
26+ private initError : string | null
27+ private initInFlight : boolean
2428
2529 constructor ( ) {
2630 this . fen = ''
@@ -33,9 +37,17 @@ class Engine {
3337 this . evaluationRejecter = null
3438 this . evaluationPromise = null
3539 this . evaluationGenerator = null
40+ this . initError = null
41+ this . initInFlight = false
3642
3743 this . onMessage = this . onMessage . bind ( this )
3844
45+ // Skip browser-only Stockfish initialization during SSR/Node renders.
46+ if ( typeof window === 'undefined' || typeof Worker === 'undefined' ) {
47+ return
48+ }
49+
50+ this . initInFlight = true
3951 setupStockfish ( )
4052 . then ( ( stockfish : StockfishWeb ) => {
4153 this . stockfish = stockfish
@@ -44,26 +56,41 @@ class Engine {
4456 stockfish . uci ( 'setoption name MultiPV value 100' )
4557 stockfish . onError = this . onError
4658 stockfish . listen = this . onMessage
59+ this . initError = null
4760 this . isReady = true
4861 this . nnueLoaded = true
4962 } )
5063 . catch ( ( error ) => {
5164 console . error ( 'Failed to initialize Stockfish:' , error )
65+ this . initError =
66+ error instanceof Error ? error . message : 'Unknown initialization error'
5267 this . isReady = false
68+ this . nnueLoaded = false
69+ } )
70+ . finally ( ( ) => {
71+ this . initInFlight = false
5372 } )
5473 }
5574
5675 get ready ( ) : boolean {
5776 return this . isReady && this . stockfish !== null && this . nnueLoaded
5877 }
5978
79+ get initializationError ( ) : string | null {
80+ return this . initError
81+ }
82+
83+ get initializing ( ) : boolean {
84+ return this . initInFlight
85+ }
86+
6087 async * streamEvaluations (
6188 fen : string ,
6289 legalMoveCount : number ,
6390 targetDepth = 18 ,
6491 ) : AsyncGenerator < StockfishEvaluation > {
6592 if ( this . stockfish && this . isReady ) {
66- if ( typeof global . gc === 'function' ) {
93+ if ( typeof global !== 'undefined' && typeof global . gc === 'function' ) {
6794 global . gc ( )
6895 }
6996
@@ -340,16 +367,47 @@ const sharedWasmMemory = (lo: number, hi = 32767): WebAssembly.Memory => {
340367 }
341368}
342369
370+ const getNnueFetchTimeoutMs = ( ) : number => {
371+ const raw = process . env . NEXT_PUBLIC_STOCKFISH_NNUE_FETCH_TIMEOUT_MS
372+ const parsed = raw ? parseInt ( raw , 10 ) : NaN
373+ return Number . isFinite ( parsed ) && parsed > 0
374+ ? parsed
375+ : DEFAULT_NNUE_FETCH_TIMEOUT_MS
376+ }
377+
378+ const fetchWithTimeout = async (
379+ url : string ,
380+ timeoutMs : number ,
381+ ) : Promise < Response > => {
382+ if ( typeof AbortController === 'undefined' || timeoutMs <= 0 ) {
383+ return fetch ( url )
384+ }
385+
386+ const controller = new AbortController ( )
387+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , timeoutMs )
388+ try {
389+ return await fetch ( url , { signal : controller . signal } )
390+ } catch ( error ) {
391+ if ( error instanceof DOMException && error . name === 'AbortError' ) {
392+ throw new Error ( `Stockfish NNUE fetch timed out after ${ timeoutMs } ms: ${ url } ` )
393+ }
394+ throw error
395+ } finally {
396+ clearTimeout ( timeoutId )
397+ }
398+ }
399+
343400const loadNnueModel = async (
344401 modelUrl : string ,
345402 storage : StockfishModelStorage ,
403+ timeoutMs : number ,
346404) : Promise < ArrayBuffer > => {
347405 const cachedModel = await storage . getModel ( modelUrl )
348406 if ( cachedModel ) {
349407 return cachedModel
350408 }
351409
352- const response = await fetch ( modelUrl )
410+ const response = await fetchWithTimeout ( modelUrl , timeoutMs )
353411 if ( ! response . ok ) {
354412 throw new Error (
355413 `Failed to fetch Stockfish NNUE model (${ response . status } ) from ${ modelUrl } ` ,
@@ -361,45 +419,38 @@ const loadNnueModel = async (
361419 return buffer
362420}
363421
364- const setupStockfish = ( ) : Promise < StockfishWeb > => {
365- return new Promise < StockfishWeb > ( ( resolve , reject ) => {
366- // eslint-disable-next-line @typescript-eslint/no-explicit-any
367- import ( 'lila-stockfish-web/sf17-79.js' ) . then ( ( makeModule : any ) => {
368- makeModule
369- . default ( {
370- wasmMemory : sharedWasmMemory ( 2560 ) ,
371- onError : ( msg : string ) => reject ( new Error ( msg ) ) ,
372- locateFile : ( name : string ) => `/stockfish/${ name } ` ,
373- } )
374- . then ( async ( instance : StockfishWeb ) => {
375- // NNUE weights served via raw.githubusercontent.com permalink (CORS + COEP compatible).
376- // Override with NEXT_PUBLIC_STOCKFISH_NNUE_BASE_URL for self-hosted deployments.
377- const nnueBaseUrl =
378- process . env . NEXT_PUBLIC_STOCKFISH_NNUE_BASE_URL ??
379- 'https://raw.githubusercontent.com/CSSLab/maia-platform-frontend/e23a50e/public/stockfish'
380- const storage = new StockfishModelStorage ( )
381- await storage . requestPersistentStorage ( )
382-
383- const nnue0Url = `${ nnueBaseUrl } /${ instance . getRecommendedNnue ( 0 ) } `
384- const nnue1Url = `${ nnueBaseUrl } /${ instance . getRecommendedNnue ( 1 ) } `
385-
386- // Load NNUE models before resolving
387- Promise . all ( [
388- loadNnueModel ( nnue0Url , storage ) ,
389- loadNnueModel ( nnue1Url , storage ) ,
390- ] )
391- . then ( ( buffers ) => {
392- instance . setNnueBuffer ( new Uint8Array ( buffers [ 0 ] ) , 0 )
393- instance . setNnueBuffer ( new Uint8Array ( buffers [ 1 ] ) , 1 )
394- resolve ( instance )
395- } )
396- . catch ( ( error ) => {
397- console . error ( 'Failed to load NNUE models:' , error )
398- reject ( error )
399- } )
400- } )
401- } )
422+ const setupStockfish = async ( ) : Promise < StockfishWeb > => {
423+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
424+ const makeModule : any = await import ( 'lila-stockfish-web/sf17-79.js' )
425+ const instance : StockfishWeb = await makeModule . default ( {
426+ wasmMemory : sharedWasmMemory ( 2560 ) ,
427+ locateFile : ( name : string ) => `/stockfish/${ name } ` ,
402428 } )
429+
430+ // NNUE weights served via raw.githubusercontent.com permalink (CORS + COEP compatible).
431+ // Override with NEXT_PUBLIC_STOCKFISH_NNUE_BASE_URL for self-hosted deployments.
432+ const nnueBaseUrl =
433+ process . env . NEXT_PUBLIC_STOCKFISH_NNUE_BASE_URL ??
434+ 'https://raw.githubusercontent.com/CSSLab/maia-platform-frontend/e23a50e/public/stockfish'
435+ const storage = new StockfishModelStorage ( )
436+ await storage . requestPersistentStorage ( )
437+
438+ const nnue0Url = `${ nnueBaseUrl } /${ instance . getRecommendedNnue ( 0 ) } `
439+ const nnue1Url = `${ nnueBaseUrl } /${ instance . getRecommendedNnue ( 1 ) } `
440+ const timeoutMs = getNnueFetchTimeoutMs ( )
441+
442+ try {
443+ const buffers = await Promise . all ( [
444+ loadNnueModel ( nnue0Url , storage , timeoutMs ) ,
445+ loadNnueModel ( nnue1Url , storage , timeoutMs ) ,
446+ ] )
447+ instance . setNnueBuffer ( new Uint8Array ( buffers [ 0 ] ) , 0 )
448+ instance . setNnueBuffer ( new Uint8Array ( buffers [ 1 ] ) , 1 )
449+ return instance
450+ } catch ( error ) {
451+ console . error ( 'Failed to load NNUE models:' , error )
452+ throw error
453+ }
403454}
404455
405456export default Engine
0 commit comments