@ -12,33 +12,161 @@ import electronServe from 'electron-serve';
import windowStateKeeper from 'electron-window-state' ;
import windowStateKeeper from 'electron-window-state' ;
import { join } from 'path' ;
import { join } from 'path' ;
// Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode.
/ * *
* Reload watcher configuration and state management
* Prevents infinite reload loops and implements rate limiting
*
* @author Matthew Raymer
* /
const RELOAD_CONFIG = {
DEBOUNCE_MS : 1500 ,
COOLDOWN_MS : 5000 ,
MAX_RELOADS_PER_MINUTE : 10 ,
MAX_RELOADS_PER_SESSION : 100
} ;
const reloadWatcher = {
const reloadWatcher = {
debouncer : null ,
debouncer : null as NodeJS . Timeout | null ,
ready : false ,
ready : false ,
watcher : null ,
watcher : null as chokidar . FSWatcher | null ,
lastReloadTime : 0 ,
reloadCount : 0 ,
sessionReloadCount : 0 ,
resetTimeout : null as NodeJS . Timeout | null ,
isReloading : false
} ;
} ;
/ * *
* Resets the reload counter after one minute
* /
const resetReloadCounter = ( ) = > {
reloadWatcher . reloadCount = 0 ;
reloadWatcher . resetTimeout = null ;
} ;
/ * *
* Checks if a reload is allowed based on rate limits and cooldown
* @returns { boolean } Whether a reload is allowed
* /
const canReload = ( ) : boolean = > {
const now = Date . now ( ) ;
// Check cooldown period
if ( now - reloadWatcher . lastReloadTime < RELOAD_CONFIG . COOLDOWN_MS ) {
console . warn ( '[Reload Watcher] Skipping reload - cooldown period active' ) ;
return false ;
}
// Check per-minute limit
if ( reloadWatcher . reloadCount >= RELOAD_CONFIG . MAX_RELOADS_PER_MINUTE ) {
console . warn ( '[Reload Watcher] Skipping reload - maximum reloads per minute reached' ) ;
return false ;
}
// Check session limit
if ( reloadWatcher . sessionReloadCount >= RELOAD_CONFIG . MAX_RELOADS_PER_SESSION ) {
console . error ( '[Reload Watcher] Maximum reloads per session reached. Please restart the application.' ) ;
return false ;
}
return true ;
} ;
/ * *
* Cleans up the current watcher instance
* /
const cleanupWatcher = ( ) = > {
if ( reloadWatcher . watcher ) {
reloadWatcher . watcher . close ( ) ;
reloadWatcher . watcher = null ;
}
if ( reloadWatcher . debouncer ) {
clearTimeout ( reloadWatcher . debouncer ) ;
reloadWatcher . debouncer = null ;
}
if ( reloadWatcher . resetTimeout ) {
clearTimeout ( reloadWatcher . resetTimeout ) ;
reloadWatcher . resetTimeout = null ;
}
} ;
/ * *
* Sets up the file watcher for development mode reloading
* Implements rate limiting and prevents infinite reload loops
*
* @param electronCapacitorApp - The Electron Capacitor app instance
* /
export function setupReloadWatcher ( electronCapacitorApp : ElectronCapacitorApp ) : void {
export function setupReloadWatcher ( electronCapacitorApp : ElectronCapacitorApp ) : void {
// Cleanup any existing watcher
cleanupWatcher ( ) ;
// Reset state
reloadWatcher . ready = false ;
reloadWatcher . isReloading = false ;
reloadWatcher . watcher = chokidar
reloadWatcher . watcher = chokidar
. watch ( join ( app . getAppPath ( ) , 'app' ) , {
. watch ( join ( app . getAppPath ( ) , 'app' ) , {
ignored : /[/\\]\./ ,
ignored : /[/\\]\./ ,
persistent : true ,
persistent : true ,
awaitWriteFinish : {
stabilityThreshold : 1000 ,
pollInterval : 100
}
} )
} )
. on ( 'ready' , ( ) = > {
. on ( 'ready' , ( ) = > {
reloadWatcher . ready = true ;
reloadWatcher . ready = true ;
console . log ( '[Reload Watcher] Ready to watch for changes' ) ;
} )
} )
. on ( 'all' , ( _event , _path ) = > {
. on ( 'all' , ( _event , _path ) = > {
if ( reloadWatcher . ready ) {
if ( ! reloadWatcher . ready || reloadWatcher . isReloading ) {
return ;
}
// Clear existing debouncer
if ( reloadWatcher . debouncer ) {
clearTimeout ( reloadWatcher . debouncer ) ;
clearTimeout ( reloadWatcher . debouncer ) ;
reloadWatcher . debouncer = setTimeout ( async ( ) = > {
}
electronCapacitorApp . getMainWindow ( ) . webContents . reload ( ) ;
// Set up new debouncer
reloadWatcher . debouncer = setTimeout ( async ( ) = > {
if ( ! canReload ( ) ) {
return ;
}
try {
reloadWatcher . isReloading = true ;
// Update reload counters
reloadWatcher . lastReloadTime = Date . now ( ) ;
reloadWatcher . reloadCount ++ ;
reloadWatcher . sessionReloadCount ++ ;
// Set up reset timeout for per-minute counter
if ( ! reloadWatcher . resetTimeout ) {
reloadWatcher . resetTimeout = setTimeout ( resetReloadCounter , 60000 ) ;
}
// Perform reload
console . log ( '[Reload Watcher] Reloading window...' ) ;
await electronCapacitorApp . getMainWindow ( ) . webContents . reload ( ) ;
// Reset state after reload
reloadWatcher . ready = false ;
reloadWatcher . ready = false ;
clearTimeout ( reloadWatcher . debouncer ) ;
reloadWatcher . isReloading = false ;
reloadWatcher . debouncer = null ;
reloadWatcher . watcher = null ;
// Re-setup watcher after successful reload
setupReloadWatcher ( electronCapacitorApp ) ;
setupReloadWatcher ( electronCapacitorApp ) ;
} , 1500 ) ;
}
} catch ( error ) {
console . error ( '[Reload Watcher] Error during reload:' , error ) ;
reloadWatcher . isReloading = false ;
reloadWatcher . ready = true ;
}
} , RELOAD_CONFIG . DEBOUNCE_MS ) ;
} )
. on ( 'error' , ( error ) = > {
console . error ( '[Reload Watcher] Error:' , error ) ;
cleanupWatcher ( ) ;
} ) ;
} ) ;
}
}