From 986ce76fb71e93298f63c9f650c37cff7c184e64 Mon Sep 17 00:00:00 2001 From: "Fabian @ Blax Software" Date: Mon, 2 Feb 2026 13:20:39 +0100 Subject: [PATCH] IA hotreloads --- config/websockets.php | 16 ++++- src/Websocket/Handler.php | 125 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/config/websockets.php b/config/websockets.php index db58fd3..3226717 100644 --- a/config/websockets.php +++ b/config/websockets.php @@ -7,9 +7,19 @@ return [ | Hot Reload (Development Mode) |-------------------------------------------------------------------------- | - | When enabled, controller files are reloaded on every request instead of - | being cached. This allows you to make code changes without restarting - | the WebSocket server. Disable in production for better performance. + | When enabled, ALL code is reloaded on every request in child processes. + | This includes Models, Resources, Services, Controllers, Config, and + | everything else - allowing code changes without restarting the server. + | + | How it works: + | - OPcache is cleared in child processes (forces PHP to recompile files) + | - Laravel container singletons are reset (forces fresh instantiation) + | - Config files are re-read from disk + | - View, route, translation, and validation caches are cleared + | - WebSocket ControllerResolver cache is cleared + | + | WARNING: Disable in production for better performance. Hot reload adds + | ~5-15ms overhead per request due to cache clearing and file re-reads. | */ 'hot_reload' => env('WEBSOCKET_HOT_RELOAD', env('APP_DEBUG', false)), diff --git a/src/Websocket/Handler.php b/src/Websocket/Handler.php index 7e04706..7c16850 100644 --- a/src/Websocket/Handler.php +++ b/src/Websocket/Handler.php @@ -63,6 +63,11 @@ class Handler implements MessageComponentInterface private int $gcCounter = 0; private const GC_INTERVAL = 100; + /** + * Whether hot reload is enabled (cached for performance) + */ + private static ?bool $hotReload = null; + /** * Initialize a new handler. */ @@ -361,6 +366,120 @@ class Handler implements MessageComponentInterface } } + /** + * Check if hot reload mode is enabled + */ + protected static function isHotReload(): bool + { + if (self::$hotReload === null) { + self::$hotReload = (bool) config('websockets.hot_reload', false); + } + return self::$hotReload; + } + + /** + * Hot reload: Clear all caches in child process for fresh code loading + * This allows Models, Resources, Services, and everything else to be reloaded + * without restarting the WebSocket server. + * + * Only called when websockets.hot_reload is enabled. + */ + protected function hotReloadChild(): void + { + if (!self::isHotReload()) { + return; + } + + // 1. Clear OPcache - forces PHP to recompile files from disk + if (function_exists('opcache_reset')) { + opcache_reset(); + } + + // 2. Clear Laravel's compiled services and config cache in container + $container = \Illuminate\Container\Container::getInstance(); + + // 3. Flush resolved instances - forces fresh instantiation + // This clears all singleton instances so they get rebuilt + $container->forgetScopedInstances(); + + // 4. Clear config repository cache (forces fresh config reads) + // Re-read all config files from disk + try { + /** @var \Illuminate\Config\Repository $config */ + $config = $container->make('config'); + + // Get the path to config files + $configPath = base_path('config'); + + if (is_dir($configPath)) { + $files = glob($configPath . '/*.php'); + foreach ($files as $file) { + $key = basename($file, '.php'); + // Invalidate opcache for this config file + if (function_exists('opcache_invalidate')) { + opcache_invalidate($file, true); + } + // Force re-require the config file + $freshConfig = require $file; + $config->set($key, $freshConfig); + } + } + } catch (\Throwable $e) { + // Config refresh failed, continue anyway + Log::channel('websocket')->debug('Hot reload config refresh failed: ' . $e->getMessage()); + } + + // 5. Clear view cache (if views are being used in responses) + try { + if ($container->bound('view')) { + $container->forgetInstance('view'); + } + } catch (\Throwable $e) { + // View refresh failed, continue anyway + } + + // 6. Clear route cache (if routes are dynamically resolved) + try { + if ($container->bound('router')) { + $container->forgetInstance('router'); + } + } catch (\Throwable $e) { + // Router refresh failed, continue anyway + } + + // 7. Clear translation cache + try { + if ($container->bound('translator')) { + $container->forgetInstance('translator'); + } + } catch (\Throwable $e) { + // Translator refresh failed, continue anyway + } + + // 8. Clear validation factory (for custom rules) + try { + if ($container->bound('validator')) { + $container->forgetInstance('validator'); + } + } catch (\Throwable $e) { + // Validator refresh failed, continue anyway + } + + // 9. Clear event dispatcher cache (for fresh event/listener bindings) + try { + if ($container->bound('events')) { + $container->forgetInstance('events'); + } + } catch (\Throwable $e) { + // Events refresh failed, continue anyway + } + + // 10. Clear WebSocket ControllerResolver cache for fresh controller loading + ControllerResolver::clearCache(); + + Log::channel('websocket')->debug('Hot reload: caches cleared in child process'); + } + /** * Fork with event-driven socket pair IPC (no polling!) * Parent is notified INSTANTLY when child sends data @@ -384,6 +503,9 @@ class Handler implements MessageComponentInterface // === CHILD PROCESS === $ipc->setupChild(); + // Hot reload: clear all caches for fresh code loading (only in dev mode) + $this->hotReloadChild(); + try { // Lazy DB reconnect: disconnect now, reconnect only when first query runs // This saves ~5-15ms for methods that don't use the database @@ -516,6 +638,9 @@ class Handler implements MessageComponentInterface array $message, string $requestId ): void { + // Hot reload: clear all caches for fresh code loading (only in dev mode) + $this->hotReloadChild(); + try { // Lazy DB reconnect: disconnect now, reconnect only when first query runs // This saves ~5-15ms for methods that don't use the database