From b870d879074f3826d793154c97e854daafda9931 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 15 Oct 2025 14:21:04 -0400 Subject: [PATCH 01/11] added NoClientsAvailableException logic --- dwds/CHANGELOG.md | 1 + .../services/web_socket_proxy_service.dart | 59 ++++++++++++++++--- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 9bfd99e19..a4ac9bb89 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,5 +1,6 @@ ## 26.1.0-wip +- Added `NoClientsAvailableException` and structured error responses with `noClientsAvailable` field for graceful handling when no browser clients are connected during hot reload or hot restart operations. - `pause` now does not send a `PauseInterrupted` event in `WebSocketProxyService` as we didn't actually pause. diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index cb97514f3..8f2b35534 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -117,6 +117,17 @@ class _ServiceExtensionTracker { } } +/// Exception thrown when no browser clients are connected to DWDS. +class NoClientsAvailableException implements Exception { + final String message; + final String operation; + + NoClientsAvailableException(this.message, {required this.operation}); + + @override + String toString() => 'NoClientsAvailableException: $message'; +} + /// WebSocket-based VM service proxy for web debugging. class WebSocketProxyService extends ProxyService { final _logger = Logger('WebSocketProxyService'); @@ -504,6 +515,14 @@ class WebSocketProxyService extends ProxyService { await _performWebSocketHotReload(); _logger.info('Hot reload completed successfully'); return _ReloadReportWithMetadata(success: true); + } on NoClientsAvailableException catch (e) { + // Gracefully handle no clients scenario + _logger.info('No clients available for hot reload'); + return _ReloadReportWithMetadata( + success: false, + notices: [e.message], + noClientsAvailable: true, + ); } catch (e) { _logger.warning('Hot reload failed: $e'); return _ReloadReportWithMetadata(success: false, notices: [e.toString()]); @@ -518,6 +537,16 @@ class WebSocketProxyService extends ProxyService { await _performWebSocketHotRestart(); _logger.info('Hot restart completed successfully'); return {'result': vm_service.Success().toJson()}; + } on NoClientsAvailableException catch (e) { + // Return structured response indicating no clients available + _logger.info('No clients available for hot restart'); + return { + 'result': { + 'type': 'Success', + 'noClientsAvailable': true, + 'message': e.message, + }, + }; } catch (e) { _logger.warning('Hot restart failed: $e'); return { @@ -611,7 +640,11 @@ class WebSocketProxyService extends ProxyService { }); if (clientCount == 0) { - throw StateError('No clients available for hot reload'); + _logger.warning('No clients available for hot reload'); + throw NoClientsAvailableException( + 'No clients available for hot reload', + operation: 'hot reload', + ); } // Create tracker for this hot reload request @@ -671,7 +704,11 @@ class WebSocketProxyService extends ProxyService { }); if (clientCount == 0) { - throw StateError('No clients available for hot restart'); + _logger.warning('No clients available for hot restart'); + throw NoClientsAvailableException( + 'No clients available for hot restart', + operation: 'hot restart', + ); } // Create tracker for this hot restart request @@ -737,9 +774,8 @@ class WebSocketProxyService extends ProxyService { final request = ServiceExtensionRequest.fromArgs( id: requestId, method: method, - args: args != null - ? Map.from(args) - : {}, + args: + args != null ? Map.from(args) : {}, ); // Send the request and get the number of connected clients @@ -940,8 +976,8 @@ class WebSocketProxyService extends ProxyService { /// Pauses execution of the isolate. @override Future pause(String isolateId) => - // Can't pause with the web socket implementation, so do nothing. - Future.value(Success()); + // Can't pause with the web socket implementation, so do nothing. + Future.value(Success()); /// Resumes execution of the isolate. @override @@ -1049,13 +1085,20 @@ class WebSocketProxyService extends ProxyService { /// Extended ReloadReport that includes additional metadata in JSON output. class _ReloadReportWithMetadata extends vm_service.ReloadReport { final List? notices; - _ReloadReportWithMetadata({super.success, this.notices}); + final bool noClientsAvailable; + + _ReloadReportWithMetadata({ + super.success, + this.notices, + this.noClientsAvailable = false, + }); @override Map toJson() { final jsonified = { 'type': 'ReloadReport', 'success': success ?? false, + 'noClientsAvailable': noClientsAvailable, }; if (notices != null) { jsonified['notices'] = notices!.map((e) => {'message': e}).toList(); From 77b297efe0f0b5c4cb0e7ca012eb3182d1ecab93 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 15 Oct 2025 14:27:49 -0400 Subject: [PATCH 02/11] updated version to prepare for release --- dwds/lib/src/version.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart index 0b0c9bb3b..c1361a8f5 100644 --- a/dwds/lib/src/version.dart +++ b/dwds/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '26.1.0-wip'; +const packageVersion = '26.1.0'; From 4b82b4c2615d66040ad0166e229e4c0b40500432 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 15 Oct 2025 14:33:01 -0400 Subject: [PATCH 03/11] updated version number --- dwds/CHANGELOG.md | 2 +- dwds/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index a4ac9bb89..e447c4477 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,4 +1,4 @@ -## 26.1.0-wip +## 26.1.0 - Added `NoClientsAvailableException` and structured error responses with `noClientsAvailable` field for graceful handling when no browser clients are connected during hot reload or hot restart operations. - `pause` now does not send a `PauseInterrupted` event in diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml index 53b399218..19616af0a 100644 --- a/dwds/pubspec.yaml +++ b/dwds/pubspec.yaml @@ -1,6 +1,6 @@ name: dwds # Every time this changes you need to run `dart run build_runner build`. -version: 26.1.0-wip +version: 26.1.0 description: >- A service that proxies between the Chrome debug protocol and the Dart VM From 308478f10fbda05e3e7ed638aaca6ab0523c4df8 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 15 Oct 2025 16:18:59 -0400 Subject: [PATCH 04/11] addressed comments --- .../services/web_socket_proxy_service.dart | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index 8f2b35534..4e45e9ca5 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -120,9 +120,8 @@ class _ServiceExtensionTracker { /// Exception thrown when no browser clients are connected to DWDS. class NoClientsAvailableException implements Exception { final String message; - final String operation; - NoClientsAvailableException(this.message, {required this.operation}); + NoClientsAvailableException(this.message); @override String toString() => 'NoClientsAvailableException: $message'; @@ -517,7 +516,6 @@ class WebSocketProxyService extends ProxyService { return _ReloadReportWithMetadata(success: true); } on NoClientsAvailableException catch (e) { // Gracefully handle no clients scenario - _logger.info('No clients available for hot reload'); return _ReloadReportWithMetadata( success: false, notices: [e.message], @@ -541,11 +539,7 @@ class WebSocketProxyService extends ProxyService { // Return structured response indicating no clients available _logger.info('No clients available for hot restart'); return { - 'result': { - 'type': 'Success', - 'noClientsAvailable': true, - 'message': e.message, - }, + 'result': {'noClientsAvailable': true, 'message': e.message}, }; } catch (e) { _logger.warning('Hot restart failed: $e'); @@ -641,10 +635,7 @@ class WebSocketProxyService extends ProxyService { if (clientCount == 0) { _logger.warning('No clients available for hot reload'); - throw NoClientsAvailableException( - 'No clients available for hot reload', - operation: 'hot reload', - ); + throw NoClientsAvailableException('No clients available for hot reload'); } // Create tracker for this hot reload request @@ -705,10 +696,7 @@ class WebSocketProxyService extends ProxyService { if (clientCount == 0) { _logger.warning('No clients available for hot restart'); - throw NoClientsAvailableException( - 'No clients available for hot restart', - operation: 'hot restart', - ); + throw NoClientsAvailableException('No clients available for hot restart'); } // Create tracker for this hot restart request @@ -774,8 +762,7 @@ class WebSocketProxyService extends ProxyService { final request = ServiceExtensionRequest.fromArgs( id: requestId, method: method, - args: - args != null ? Map.from(args) : {}, + args: {...?args}, ); // Send the request and get the number of connected clients From 64dcb6f7d6ca6f7341a1fa716d8401f99c9a1b79 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Wed, 15 Oct 2025 16:21:57 -0400 Subject: [PATCH 05/11] remove duplicate logging --- dwds/lib/src/services/web_socket_proxy_service.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index 4e45e9ca5..76b3af8a3 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -537,7 +537,6 @@ class WebSocketProxyService extends ProxyService { return {'result': vm_service.Success().toJson()}; } on NoClientsAvailableException catch (e) { // Return structured response indicating no clients available - _logger.info('No clients available for hot restart'); return { 'result': {'noClientsAvailable': true, 'message': e.message}, }; From eed8fe484318057b955bf3464fe2c851057df6f7 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 16 Oct 2025 16:18:28 -0400 Subject: [PATCH 06/11] updated to throw RpcError --- .../services/web_socket_proxy_service.dart | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index 76b3af8a3..62268f090 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -515,11 +515,12 @@ class WebSocketProxyService extends ProxyService { _logger.info('Hot reload completed successfully'); return _ReloadReportWithMetadata(success: true); } on NoClientsAvailableException catch (e) { - // Gracefully handle no clients scenario - return _ReloadReportWithMetadata( - success: false, - notices: [e.message], - noClientsAvailable: true, + // Throw RPC error with kServerError code when no browser clients are + // connected. + throw vm_service.RPCError( + 'reloadSources', + vm_service.RPCErrorKind.kServerError.code, + 'Hot reload failed: ${e.message}', ); } catch (e) { _logger.warning('Hot reload failed: $e'); @@ -536,10 +537,13 @@ class WebSocketProxyService extends ProxyService { _logger.info('Hot restart completed successfully'); return {'result': vm_service.Success().toJson()}; } on NoClientsAvailableException catch (e) { - // Return structured response indicating no clients available - return { - 'result': {'noClientsAvailable': true, 'message': e.message}, - }; + // Throw RPC error with kServerError code when no browser clients are + // connected. + throw vm_service.RPCError( + 'hotRestart', + vm_service.RPCErrorKind.kServerError.code, + 'Hot restart failed: ${e.message}', + ); } catch (e) { _logger.warning('Hot restart failed: $e'); return { From effc385f0e6240901bd2e043dfeb586856fa9e70 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 16 Oct 2025 16:35:13 -0400 Subject: [PATCH 07/11] reverted chnages to _ReloadReportWithMetadata --- dwds/lib/src/services/web_socket_proxy_service.dart | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index 62268f090..74115eafa 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -1075,20 +1075,13 @@ class WebSocketProxyService extends ProxyService { /// Extended ReloadReport that includes additional metadata in JSON output. class _ReloadReportWithMetadata extends vm_service.ReloadReport { final List? notices; - final bool noClientsAvailable; - - _ReloadReportWithMetadata({ - super.success, - this.notices, - this.noClientsAvailable = false, - }); + _ReloadReportWithMetadata({super.success, this.notices}); @override Map toJson() { final jsonified = { 'type': 'ReloadReport', 'success': success ?? false, - 'noClientsAvailable': noClientsAvailable, }; if (notices != null) { jsonified['notices'] = notices!.map((e) => {'message': e}).toList(); From 7c1a074e85a369bfddf5093e6677a9f955dac167 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Thu, 16 Oct 2025 16:39:55 -0400 Subject: [PATCH 08/11] updated changelog --- dwds/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index e447c4477..860c51bf8 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,6 +1,6 @@ ## 26.1.0 -- Added `NoClientsAvailableException` and structured error responses with `noClientsAvailable` field for graceful handling when no browser clients are connected during hot reload or hot restart operations. +- `reloadSources` and `hotRestart` now throw an RPC error with `kServerError` code when `NoClientsAvailableException` is caught (no browser clients are connected), allowing tooling to detect and handle this scenario. - `pause` now does not send a `PauseInterrupted` event in `WebSocketProxyService` as we didn't actually pause. From 0ebf7f573b4520f8daf66a70a47d0bff23a7902c Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 17 Oct 2025 10:01:00 -0400 Subject: [PATCH 09/11] refactored string No clients available for .... --- .../services/web_socket_proxy_service.dart | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index 74115eafa..0077a1381 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -33,6 +33,12 @@ const _pauseIsolatesOnStartFlag = 'pause_isolates_on_start'; /// closes before the new connection is established, preventing premature isolate destruction. const _isolateDestructionGracePeriod = Duration(seconds: 15); +/// Error message when no clients are available for hot reload. +const _noClientsForHotReload = 'No clients available for hot reload'; + +/// Error message when no clients are available for hot restart. +const _noClientsForHotRestart = 'No clients available for hot restart'; + /// Tracks hot reload responses from multiple browser windows/tabs. class _HotReloadTracker { final String requestId; @@ -121,7 +127,13 @@ class _ServiceExtensionTracker { class NoClientsAvailableException implements Exception { final String message; - NoClientsAvailableException(this.message); + NoClientsAvailableException._(this.message); + + factory NoClientsAvailableException.hotReload() => + NoClientsAvailableException._(_noClientsForHotReload); + + factory NoClientsAvailableException.hotRestart() => + NoClientsAvailableException._(_noClientsForHotRestart); @override String toString() => 'NoClientsAvailableException: $message'; @@ -637,8 +649,8 @@ class WebSocketProxyService extends ProxyService { }); if (clientCount == 0) { - _logger.warning('No clients available for hot reload'); - throw NoClientsAvailableException('No clients available for hot reload'); + _logger.warning(_noClientsForHotReload); + throw NoClientsAvailableException.hotReload(); } // Create tracker for this hot reload request @@ -698,8 +710,8 @@ class WebSocketProxyService extends ProxyService { }); if (clientCount == 0) { - _logger.warning('No clients available for hot restart'); - throw NoClientsAvailableException('No clients available for hot restart'); + _logger.warning(_noClientsForHotRestart); + throw NoClientsAvailableException.hotRestart(); } // Create tracker for this hot restart request From 095fc8aa480f880491dece47ec9bb5adabf5545c Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 17 Oct 2025 12:14:15 -0400 Subject: [PATCH 10/11] updated to use kIsolateCannotReload --- dwds/lib/src/services/web_socket_proxy_service.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index 0077a1381..fc1cd2932 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -527,11 +527,11 @@ class WebSocketProxyService extends ProxyService { _logger.info('Hot reload completed successfully'); return _ReloadReportWithMetadata(success: true); } on NoClientsAvailableException catch (e) { - // Throw RPC error with kServerError code when no browser clients are + // Throw RPC error with kIsolateCannotReload code when no browser clients are // connected. throw vm_service.RPCError( 'reloadSources', - vm_service.RPCErrorKind.kServerError.code, + vm_service.RPCErrorKind.kIsolateCannotReload.code, 'Hot reload failed: ${e.message}', ); } catch (e) { @@ -549,11 +549,11 @@ class WebSocketProxyService extends ProxyService { _logger.info('Hot restart completed successfully'); return {'result': vm_service.Success().toJson()}; } on NoClientsAvailableException catch (e) { - // Throw RPC error with kServerError code when no browser clients are + // Throw RPC error with kIsolateCannotReload code when no browser clients are // connected. throw vm_service.RPCError( 'hotRestart', - vm_service.RPCErrorKind.kServerError.code, + vm_service.RPCErrorKind.kIsolateCannotReload.code, 'Hot restart failed: ${e.message}', ); } catch (e) { From 7d9192aeac998101eb3eb1f10fd6d19cbbfc9dd7 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 17 Oct 2025 13:55:13 -0400 Subject: [PATCH 11/11] updated string --- .../services/web_socket_proxy_service.dart | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket_proxy_service.dart index fc1cd2932..c9eefc80b 100644 --- a/dwds/lib/src/services/web_socket_proxy_service.dart +++ b/dwds/lib/src/services/web_socket_proxy_service.dart @@ -33,11 +33,8 @@ const _pauseIsolatesOnStartFlag = 'pause_isolates_on_start'; /// closes before the new connection is established, preventing premature isolate destruction. const _isolateDestructionGracePeriod = Duration(seconds: 15); -/// Error message when no clients are available for hot reload. -const _noClientsForHotReload = 'No clients available for hot reload'; - -/// Error message when no clients are available for hot restart. -const _noClientsForHotRestart = 'No clients available for hot restart'; +/// Error message when no clients are available for hot reload/restart. +const kNoClientsAvailable = 'No clients available.'; /// Tracks hot reload responses from multiple browser windows/tabs. class _HotReloadTracker { @@ -129,12 +126,6 @@ class NoClientsAvailableException implements Exception { NoClientsAvailableException._(this.message); - factory NoClientsAvailableException.hotReload() => - NoClientsAvailableException._(_noClientsForHotReload); - - factory NoClientsAvailableException.hotRestart() => - NoClientsAvailableException._(_noClientsForHotRestart); - @override String toString() => 'NoClientsAvailableException: $message'; } @@ -649,8 +640,8 @@ class WebSocketProxyService extends ProxyService { }); if (clientCount == 0) { - _logger.warning(_noClientsForHotReload); - throw NoClientsAvailableException.hotReload(); + _logger.warning(kNoClientsAvailable); + throw NoClientsAvailableException._(kNoClientsAvailable); } // Create tracker for this hot reload request @@ -710,8 +701,8 @@ class WebSocketProxyService extends ProxyService { }); if (clientCount == 0) { - _logger.warning(_noClientsForHotRestart); - throw NoClientsAvailableException.hotRestart(); + _logger.warning(kNoClientsAvailable); + throw NoClientsAvailableException._(kNoClientsAvailable); } // Create tracker for this hot restart request