[ DWDS ] Launch DDS when using the web socket proxy service (#2706)

DDS is needed to serve DevTools, so the web socket proxy service should
be setup to launch DDS at startup.

This change also includes some significant refactors to reduce the
amount of duplicate code that could be shared by the Chrome and
web socket service implementations.
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 1e45a48..ddfa389 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -1,6 +1,10 @@
+## 26.2.1-wip
+
+- Add support for DDS APIs and serving Dart DevTools when no Chrome Debugger is available.
+
 ## 26.2.0
 
-- Add support for more service APIs over websocket connections with no Chrome Debugger available.
+- Add support for more service APIs over websocket connections when no Chrome Debugger is available.
 
 ## 26.1.0
 
diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart
index 5610ceb..a11ae81 100644
--- a/dwds/lib/dwds.dart
+++ b/dwds/lib/dwds.dart
@@ -34,7 +34,8 @@
     show FrontendServerAssetReader;
 export 'src/readers/proxy_server_asset_reader.dart' show ProxyServerAssetReader;
 export 'src/servers/devtools.dart';
-export 'src/services/chrome_debug_exception.dart' show ChromeDebugException;
+export 'src/services/chrome/chrome_debug_exception.dart'
+    show ChromeDebugException;
 export 'src/services/expression_compiler.dart'
     show
         ExpressionCompilationResult,
diff --git a/dwds/lib/src/connections/debug_connection.dart b/dwds/lib/src/connections/debug_connection.dart
index 01a9223..cdf3151 100644
--- a/dwds/lib/src/connections/debug_connection.dart
+++ b/dwds/lib/src/connections/debug_connection.dart
@@ -5,7 +5,7 @@
 import 'dart:async';
 
 import 'package:dwds/src/services/app_debug_services.dart';
-import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:dwds/src/services/chrome/chrome_proxy_service.dart';
 import 'package:vm_service/vm_service.dart';
 
 /// A debug connection between the application in the browser and DWDS.
diff --git a/dwds/lib/src/debugging/classes.dart b/dwds/lib/src/debugging/classes.dart
index 9eaf39d..eee4fde 100644
--- a/dwds/lib/src/debugging/classes.dart
+++ b/dwds/lib/src/debugging/classes.dart
@@ -5,7 +5,7 @@
 import 'package:dwds/src/config/tool_configuration.dart';
 import 'package:dwds/src/debugging/chrome_inspector.dart';
 import 'package:dwds/src/debugging/metadata/class.dart';
-import 'package:dwds/src/services/chrome_debug_exception.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart
index 832727e..38e063e 100644
--- a/dwds/lib/src/debugging/debugger.dart
+++ b/dwds/lib/src/debugging/debugger.dart
@@ -12,7 +12,7 @@
 import 'package:dwds/src/debugging/location.dart';
 import 'package:dwds/src/debugging/remote_debugger.dart';
 import 'package:dwds/src/debugging/skip_list.dart';
-import 'package:dwds/src/services/chrome_debug_exception.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
 import 'package:dwds/src/utilities/dart_uri.dart';
 import 'package:dwds/src/utilities/domain.dart';
 import 'package:dwds/src/utilities/objects.dart' show Property;
diff --git a/dwds/lib/src/debugging/libraries.dart b/dwds/lib/src/debugging/libraries.dart
index a4ce5a0..d551f77 100644
--- a/dwds/lib/src/debugging/libraries.dart
+++ b/dwds/lib/src/debugging/libraries.dart
@@ -8,7 +8,7 @@
 import 'package:dwds/src/debugging/inspector.dart';
 import 'package:dwds/src/debugging/metadata/class.dart';
 import 'package:dwds/src/debugging/metadata/provider.dart';
-import 'package:dwds/src/services/chrome_debug_exception.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
 import 'package:logging/logging.dart';
 import 'package:meta/meta.dart';
 import 'package:vm_service/vm_service.dart';
diff --git a/dwds/lib/src/debugging/metadata/class.dart b/dwds/lib/src/debugging/metadata/class.dart
index 2e33627..79037e6 100644
--- a/dwds/lib/src/debugging/metadata/class.dart
+++ b/dwds/lib/src/debugging/metadata/class.dart
@@ -4,7 +4,7 @@
 
 import 'package:dwds/src/config/tool_configuration.dart';
 import 'package:dwds/src/debugging/chrome_inspector.dart';
-import 'package:dwds/src/services/chrome_debug_exception.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
 import 'package:logging/logging.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
diff --git a/dwds/lib/src/debugging/web_socket_inspector.dart b/dwds/lib/src/debugging/web_socket_inspector.dart
index d7c7412..5b5a6f0 100644
--- a/dwds/lib/src/debugging/web_socket_inspector.dart
+++ b/dwds/lib/src/debugging/web_socket_inspector.dart
@@ -5,7 +5,7 @@
 import 'package:dwds/src/connections/app_connection.dart';
 import 'package:dwds/src/debugging/inspector.dart';
 import 'package:dwds/src/debugging/libraries.dart';
-import 'package:dwds/src/services/web_socket_proxy_service.dart';
+import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:vm_service/vm_service.dart';
 
@@ -38,6 +38,7 @@
       breakpoints: [],
       isSystemIsolate: false,
       isolateFlags: [],
+      extensionRPCs: [],
     );
     final inspector = WebSocketAppInspector._(
       appConnection,
diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart
index 72549e5..a6426cb 100644
--- a/dwds/lib/src/dwds_vm_client.dart
+++ b/dwds/lib/src/dwds_vm_client.dart
@@ -8,13 +8,16 @@
 import 'package:dwds/src/config/tool_configuration.dart';
 import 'package:dwds/src/events.dart';
 import 'package:dwds/src/loaders/ddc_library_bundle.dart';
-import 'package:dwds/src/services/chrome_debug_exception.dart';
-import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_service.dart';
+import 'package:dwds/src/services/chrome/chrome_proxy_service.dart';
 import 'package:dwds/src/services/debug_service.dart';
 import 'package:dwds/src/services/proxy_service.dart';
-import 'package:dwds/src/services/web_socket_proxy_service.dart';
+import 'package:dwds/src/services/web_socket/web_socket_debug_service.dart';
+import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart';
 import 'package:dwds/src/utilities/synchronized.dart';
 import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
 import 'package:uuid/uuid.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:vm_service/vm_service_io.dart';
@@ -41,104 +44,57 @@
 }
 
 /// Common interface for DWDS VM clients.
-abstract class DwdsVmClient {
-  /// The VM service client.
-  VmService get client;
+abstract base class DwdsVmClient<
+  T extends ProxyService,
+  U extends DebugService<T>
+> {
+  late final VmService client;
+  final _requestController = StreamController<VmRequest>();
+  final _responseController = StreamController<VmResponse>();
+  late final _responseSink = _responseController.sink;
+  late final _responseStream = _responseController.stream.asBroadcastStream();
+  late final _requestSink = _requestController.sink;
+  late final _requestStream = _requestController.stream;
 
-  /// Closes the VM client and releases resources.
-  Future<void> close();
-}
+  Logger get logger;
 
-// Chrome-based DWDS VM client logger.
-final _chromeLogger = Logger('DwdsVmClient');
+  DwdsVmClient({required this.debugService});
 
-// A client of the vm service that registers some custom extensions like
-// hotRestart.
-class ChromeDwdsVmClient implements DwdsVmClient {
-  @override
-  final VmService client;
-  final StreamController<Map<String, Object>> _requestController;
-  final StreamController<Map<String, Object?>> _responseController;
+  final U debugService;
 
-  /// Null until [close] is called.
-  ///
-  /// All subsequent calls to [close] will return this future.
   Future<void>? _closed;
 
-  /// Synchronizes hot restarts to avoid races.
-  final _hotRestartQueue = AtomicQueue();
-
-  ChromeDwdsVmClient(
-    this.client,
-    this._requestController,
-    this._responseController,
-  );
-
-  @override
+  /// Closes the VM client and releases resources.
   Future<void> close() => _closed ??= () async {
     await _requestController.close();
     await _responseController.close();
     await client.dispose();
   }();
 
-  static Future<ChromeDwdsVmClient> create(
-    ChromeDebugService debugService,
-    DwdsStats dwdsStats,
-    Uri? ddsUri,
-  ) async {
-    final chromeProxyService = debugService.chromeProxyService;
-    final responseController = StreamController<VmResponse>();
-    final responseSink = responseController.sink;
-    // Response stream must be a broadcast stream so that it can have multiple
-    // listeners:
-    final responseStream = responseController.stream.asBroadcastStream();
-    final requestController = StreamController<VmRequest>();
-    final requestSink = requestController.sink;
-    final requestStream = requestController.stream;
-
-    final clientCompleter = Completer<VmService>();
-
-    _setUpVmServerConnection(
-      chromeProxyService: chromeProxyService,
+  @mustCallSuper
+  Future<void> initialize({required Uri? ddsUri}) async {
+    setUpVmServerConnection(
+      proxyService: debugService.proxyService,
       debugService: debugService,
-      responseStream: responseStream,
-      responseSink: responseSink,
-      requestStream: requestStream,
-      requestSink: requestSink,
-      dwdsStats: dwdsStats,
-      clientFuture: clientCompleter.future,
+      responseStream: _responseStream,
+      responseSink: _responseSink,
+      requestStream: _requestStream,
+      requestSink: _requestSink,
     );
 
-    final client = ddsUri == null
-        ? _setUpVmClient(
-            responseStream: responseStream,
-            requestController: requestController,
-            requestSink: requestSink,
-          )
+    client = ddsUri == null
+        ? _setUpVmClient()
         : await _setUpDdsClient(ddsUri: ddsUri);
 
-    if (!clientCompleter.isCompleted) {
-      clientCompleter.complete(client);
-    }
-
-    final dwdsVmClient = ChromeDwdsVmClient(
-      client,
-      requestController,
-      responseController,
-    );
-
-    await _registerServiceExtensions(
+    await registerServiceExtensions(
       client: client,
-      chromeProxyService: chromeProxyService,
-      dwdsVmClient: dwdsVmClient,
+      proxyService: debugService.proxyService,
     );
-
-    return dwdsVmClient;
   }
 
   /// Establishes a VM service client that is connected via DDS and registers
   /// the service extensions on that client.
-  static Future<VmService> _setUpDdsClient({required Uri ddsUri}) async {
+  Future<VmService> _setUpDdsClient({required Uri ddsUri}) async {
     final client = await vmServiceConnectUri(ddsUri.toString());
     return client;
   }
@@ -147,22 +103,16 @@
   /// extensions on that client.
   ///
   /// Note: This is only used in the rare cases where DDS is disabled.
-  static VmService _setUpVmClient({
-    required Stream<VmResponse> responseStream,
-    required StreamSink<VmRequest> requestSink,
-    required StreamController<VmRequest> requestController,
-  }) {
-    final client = VmService(responseStream.map(jsonEncode), (request) {
-      if (requestController.isClosed) {
-        _chromeLogger.warning(
-          'Attempted to send a request but the connection is closed:\n\n'
-          '$request',
+  VmService _setUpVmClient() {
+    final client = VmService(_responseStream.map(jsonEncode), (request) {
+      if (_requestController.isClosed) {
+        logger.warning(
+          'Attempted to send a request but the connection is closed:\n\n$request',
         );
         return;
       }
-      requestSink.add(Map<String, Object>.from(jsonDecode(request)));
+      _requestSink.add(Map<String, Object>.from(jsonDecode(request)));
     });
-
     return client;
   }
 
@@ -176,20 +126,20 @@
   /// should register all Flutter service extensions. However, to do so we will
   /// need to implement the missing isolate-related dart:developer APIs so that
   /// the engine has access to this information.
-  static void _setUpVmServerConnection({
-    required ChromeProxyService chromeProxyService,
-    required DwdsStats dwdsStats,
-    required ChromeDebugService debugService,
+  void setUpVmServerConnection({
+    required T proxyService,
+    required DebugService<T> debugService,
     required Stream<VmResponse> responseStream,
     required StreamSink<VmResponse> responseSink,
     required Stream<VmRequest> requestStream,
     required StreamSink<VmRequest> requestSink,
-    required Future<VmService> clientFuture,
+    DwdsStats? dwdsStats,
+    Future<VmService>? clientFuture,
   }) {
     responseStream.listen((request) async {
       final response = await _maybeHandleServiceExtensionRequest(
         request,
-        chromeProxyService: chromeProxyService,
+        proxyService: proxyService,
         dwdsStats: dwdsStats,
         clientFuture: clientFuture,
       );
@@ -202,10 +152,12 @@
       requestStream,
       responseSink,
       debugService.serviceExtensionRegistry,
-      debugService.chromeProxyService,
+      proxyService,
     );
 
+    // Register service extensions
     for (final extension in _NamespacedServiceExtension.values) {
+      logger.finest('Registering service extension: ${extension.method}');
       debugService.serviceExtensionRegistry.registerExtension(
         extension.method,
         vmServerConnection,
@@ -213,33 +165,18 @@
     }
   }
 
-  static Future<VmRequest?> _maybeHandleServiceExtensionRequest(
+  Future<VmRequest?> _maybeHandleServiceExtensionRequest(
     VmResponse request, {
-    required ChromeProxyService chromeProxyService,
-    required DwdsStats dwdsStats,
-    required Future<VmService> clientFuture,
+    required T proxyService,
+    required DwdsStats? dwdsStats,
+    required Future<VmService>? clientFuture,
   }) async {
-    VmRequest? response;
-    final method = request['method'];
-    if (method == _NamespacedServiceExtension.flutterListViews.method) {
-      response = await flutterListViewsHandler(chromeProxyService);
-    } else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) {
-      response = extDwdsEmitEventHandler(request, _chromeLogger);
-    } else if (method == _NamespacedServiceExtension.extDwdsReload.method) {
-      response = await _extDwdsReloadHandler(chromeProxyService);
-    } else if (method == _NamespacedServiceExtension.extDwdsRestart.method) {
-      final client = await clientFuture;
-      response = await _extDwdsRestartHandler(chromeProxyService, client);
-    } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) {
-      response = await extDwdsSendEventHandler(
-        request,
-        dwdsStats,
-        _chromeLogger,
-      );
-    } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) {
-      response = await _extDwdsScreenshotHandler(chromeProxyService);
-    }
-
+    final response = await maybeHandleServiceExtensionRequestImpl(
+      request,
+      proxyService: proxyService,
+      dwdsStats: dwdsStats,
+      clientFuture: clientFuture,
+    );
     if (response != null) {
       response['id'] = request['id'] as String;
       // This is necessary even though DWDS doesn't use package:json_rpc_2.
@@ -247,7 +184,191 @@
       // https://github.com/dart-lang/json_rpc_2/blob/639857be892050159f5164c749d7947694976a4a/lib/src/server.dart#L252
       response['jsonrpc'] = '2.0';
     }
+    return response;
+  }
 
+  @mustBeOverridden
+  Future<VmRequest?> maybeHandleServiceExtensionRequestImpl(
+    VmResponse request, {
+    required T proxyService,
+    DwdsStats? dwdsStats,
+    Future<VmService>? clientFuture,
+  });
+
+  @mustBeOverridden
+  Future<void> registerServiceExtensions({
+    required VmService client,
+    required T proxyService,
+  });
+
+  /// Shared handler for DWDS send event service extension.
+  Future<Map<String, Object>> extDwdsSendEventHandler(
+    VmResponse request,
+    DwdsStats? dwdsStats,
+    Logger logger,
+  ) async {
+    logger.fine('SendEvent: $request');
+    if (dwdsStats != null) {
+      _processSendEvent(request, dwdsStats);
+    }
+    return {'result': Success().toJson()};
+  }
+
+  void _processSendEvent(Map<String, dynamic> request, DwdsStats dwdsStats) {
+    final event = request['params'] as Map<String, dynamic>?;
+    if (event == null) return;
+    final type = event['type'] as String?;
+    final payload = event['payload'] as Map<String, dynamic>?;
+    switch (type) {
+      case 'DevtoolsEvent':
+        {
+          logger.finest('Received DevTools event: $event');
+          final action = payload?['action'] as String?;
+          final screen = payload?['screen'] as String?;
+          if (screen != null && action == 'pageReady') {
+            _recordDwdsStats(dwdsStats, screen);
+          } else {
+            logger.finest('Ignoring unknown event: $event');
+          }
+        }
+    }
+  }
+
+  void _recordDwdsStats(DwdsStats dwdsStats, String screen) {
+    if (dwdsStats.isFirstDebuggerReady) {
+      final devToolsStart = dwdsStats.devToolsStart;
+      final debuggerStart = dwdsStats.debuggerStart;
+      if (devToolsStart != null) {
+        final devToolLoadTime = DateTime.now()
+            .difference(devToolsStart)
+            .inMilliseconds;
+        emitEvent(DwdsEvent.devToolsLoad(devToolLoadTime, screen));
+        logger.fine('DevTools load time: $devToolLoadTime ms');
+      }
+      if (debuggerStart != null) {
+        final debuggerReadyTime = DateTime.now()
+            .difference(debuggerStart)
+            .inMilliseconds;
+        emitEvent(DwdsEvent.debuggerReady(debuggerReadyTime, screen));
+        logger.fine('Debugger ready time: $debuggerReadyTime ms');
+      }
+    } else {
+      logger.finest('Debugger and DevTools stats are already recorded.');
+    }
+  }
+
+  /// Shared handler for DWDS emit event service extension.
+  Map<String, Object> extDwdsEmitEventHandler(
+    VmResponse request,
+    Logger logger,
+  ) {
+    final event = request['params'] as Map<String, dynamic>?;
+    if (event != null) {
+      final type = event['type'] as String?;
+      final payload = event['payload'] as Map<String, dynamic>?;
+      if (type != null && payload != null) {
+        logger.fine('EmitEvent: $type $payload');
+        emitEvent(DwdsEvent(type, payload));
+      }
+    }
+    return {'result': Success().toJson()};
+  }
+
+  /// Shared handler for Flutter list views service extension.
+  Future<Map<String, Object>> flutterListViewsHandler(
+    ProxyService proxyService,
+  ) async {
+    final vm = await proxyService.getVM();
+    final isolates = vm.isolates;
+    return <String, Object>{
+      'result': <String, Object>{
+        'views': <Object>[
+          for (final isolate in isolates ?? [])
+            <String, Object>{'id': isolate.id, 'isolate': isolate.toJson()},
+        ],
+      },
+    };
+  }
+}
+
+// A client of the vm service that registers some custom extensions like
+// hotRestart.
+final class ChromeDwdsVmClient
+    extends DwdsVmClient<ChromeProxyService, ChromeDebugService> {
+  /// Synchronizes hot restarts to avoid races.
+  final _hotRestartQueue = AtomicQueue();
+
+  final _clientCompleter = Completer<VmService>();
+
+  @override
+  final logger = Logger('DwdsVmClient');
+
+  ChromeDwdsVmClient({required super.debugService});
+
+  static Future<ChromeDwdsVmClient> create(
+    ChromeDebugService debugService,
+    Uri? ddsUri,
+  ) async {
+    final dwdsVmClient = ChromeDwdsVmClient(debugService: debugService);
+    await dwdsVmClient.initialize(ddsUri: ddsUri);
+    return dwdsVmClient;
+  }
+
+  @override
+  Future<void> initialize({required Uri? ddsUri}) async {
+    await super.initialize(ddsUri: ddsUri);
+    if (!_clientCompleter.isCompleted) {
+      _clientCompleter.complete(client);
+    }
+  }
+
+  @override
+  Future<void> registerServiceExtensions({
+    required VmService client,
+    required ChromeProxyService proxyService,
+  }) async {
+    client.registerServiceCallback(
+      'hotRestart',
+      (request) => captureElapsedTime(
+        () => hotRestart(proxyService, client),
+        (_) => DwdsEvent.hotRestart(),
+      ),
+    );
+    await client.registerService('hotRestart', 'DWDS');
+
+    client.registerServiceCallback(
+      'fullReload',
+      (request) => captureElapsedTime(
+        () => _fullReload(proxyService),
+        (_) => DwdsEvent.fullReload(),
+      ),
+    );
+    await client.registerService('fullReload', 'DWDS');
+  }
+
+  @override
+  Future<VmRequest?> maybeHandleServiceExtensionRequestImpl(
+    VmResponse request, {
+    required ChromeProxyService proxyService,
+    DwdsStats? dwdsStats,
+    Future<VmService>? clientFuture,
+  }) async {
+    VmRequest? response;
+    final method = request['method'];
+    if (method == _NamespacedServiceExtension.flutterListViews.method) {
+      response = await flutterListViewsHandler(proxyService);
+    } else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) {
+      response = extDwdsEmitEventHandler(request, logger);
+    } else if (method == _NamespacedServiceExtension.extDwdsReload.method) {
+      response = await _extDwdsReloadHandler(proxyService);
+    } else if (method == _NamespacedServiceExtension.extDwdsRestart.method) {
+      final client = await clientFuture;
+      response = await _extDwdsRestartHandler(proxyService, client!);
+    } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) {
+      response = await extDwdsSendEventHandler(request, dwdsStats, logger);
+    } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) {
+      response = await _extDwdsScreenshotHandler(proxyService);
+    }
     return response;
   }
 
@@ -261,14 +382,14 @@
     return {'result': response.result as Object};
   }
 
-  static Future<Map<String, Object>> _extDwdsReloadHandler(
+  Future<Map<String, Object>> _extDwdsReloadHandler(
     ChromeProxyService chromeProxyService,
   ) async {
     await _fullReload(chromeProxyService);
     return {'result': Success().toJson()};
   }
 
-  static Future<Map<String, Object>> _extDwdsRestartHandler(
+  Future<Map<String, Object>> _extDwdsRestartHandler(
     ChromeProxyService chromeProxyService,
     VmService client,
   ) async {
@@ -276,475 +397,265 @@
     return {'result': Success().toJson()};
   }
 
-  static Future<void> _registerServiceExtensions({
-    required VmService client,
-    required ChromeProxyService chromeProxyService,
-    required ChromeDwdsVmClient dwdsVmClient,
-  }) async {
-    client.registerServiceCallback(
-      'hotRestart',
-      (request) => captureElapsedTime(
-        () => dwdsVmClient.hotRestart(chromeProxyService, client),
-        (_) => DwdsEvent.hotRestart(),
-      ),
-    );
-    await client.registerService('hotRestart', 'DWDS');
-
-    client.registerServiceCallback(
-      'fullReload',
-      (request) => captureElapsedTime(
-        () => _fullReload(chromeProxyService),
-        (_) => DwdsEvent.fullReload(),
-      ),
-    );
-    await client.registerService('fullReload', 'DWDS');
-  }
-
   Future<Map<String, dynamic>> hotRestart(
     ChromeProxyService chromeProxyService,
     VmService client,
   ) {
     return _hotRestartQueue.run(() => _hotRestart(chromeProxyService, client));
   }
+
+  Future<int> tryGetContextId(
+    ChromeProxyService chromeProxyService, {
+    int retries = 3,
+  }) async {
+    const waitInMs = 50;
+    for (var retry = 0; retry < retries; retry++) {
+      final tryId = await chromeProxyService.executionContext.id;
+      if (tryId != null) return tryId;
+      await Future.delayed(const Duration(milliseconds: waitInMs));
+    }
+    throw StateError('No context with the running Dart application.');
+  }
+
+  Future<Map<String, dynamic>> _hotRestart(
+    ChromeProxyService chromeProxyService,
+    VmService client,
+  ) async {
+    logger.info('Attempting a hot restart');
+
+    chromeProxyService.terminatingIsolates = true;
+    await _disableBreakpointsAndResume(client, chromeProxyService);
+    try {
+      logger.info('Attempting to get execution context ID.');
+      await tryGetContextId(chromeProxyService);
+      logger.info('Got execution context ID.');
+    } on StateError catch (e) {
+      // We couldn't find the execution context. `hotRestart` may have been
+      // triggered in the middle of a full reload.
+      return {
+        'error': {
+          'code': RPCErrorKind.kInternalError.code,
+          'message': e.message,
+        },
+      };
+    }
+    // Start listening for isolate create events before issuing a hot
+    // restart. Only return success after the isolate has fully started.
+    final stream = chromeProxyService.onEvent('Isolate');
+    final waitForIsolateStarted = stream.firstWhere(
+      (event) => event.kind == EventKind.kIsolateStart,
+    );
+    try {
+      // If we should pause isolates on start, then only run main once we get a
+      // resume event.
+      final pauseIsolatesOnStart = chromeProxyService.pauseIsolatesOnStart;
+      if (pauseIsolatesOnStart) {
+        _waitForResumeEventToRunMain(chromeProxyService);
+      }
+      // Generate run id to hot restart all apps loaded into the tab.
+      final runId = const Uuid().v4().toString();
+
+      // When using the DDC library bundle format, we determine the sources that
+      // were reloaded during a hot restart to then wait until all the sources are
+      // parsed before finishing hot restart. This is necessary before we can
+      // recompute any source location metadata in the `ChromeProxyService`.
+      // TODO(srujzs): We don't do this for the AMD module format, should we? It
+      // would require adding an extra parameter in the AMD strategy. As we're
+      // planning to deprecate it, for now, do nothing.
+      final isDdcLibraryBundle =
+          globalToolConfiguration.loadStrategy is DdcLibraryBundleStrategy;
+      final computedReloadedSrcs = Completer<void>();
+      final reloadedSrcs = <String>{};
+      late StreamSubscription<String> parsedScriptsSubscription;
+      if (isDdcLibraryBundle) {
+        // Injected client should send a request to recreate the isolate after the
+        // hot restart. The creation of the isolate should in turn wait until all
+        // scripts are parsed.
+        chromeProxyService.allowedToCreateIsolate = Completer<void>();
+        final debugger = await chromeProxyService.debuggerFuture;
+        parsedScriptsSubscription = debugger.parsedScriptsController.stream
+            .listen((url) {
+              computedReloadedSrcs.future.then((_) async {
+                reloadedSrcs.remove(Uri.parse(url).normalizePath().path);
+                if (reloadedSrcs.isEmpty &&
+                    !chromeProxyService.allowedToCreateIsolate.isCompleted) {
+                  chromeProxyService.allowedToCreateIsolate.complete();
+                }
+              });
+            });
+      }
+      logger.info('Issuing \$dartHotRestartDwds request');
+      final remoteObject = await chromeProxyService.inspector.jsEvaluate(
+        '\$dartHotRestartDwds(\'$runId\', $pauseIsolatesOnStart);',
+        awaitPromise: true,
+        returnByValue: true,
+      );
+      if (isDdcLibraryBundle) {
+        final reloadedSrcModuleLibraries = (remoteObject.value as List)
+            .cast<Map>();
+        for (final srcModuleLibrary in reloadedSrcModuleLibraries) {
+          final srcModuleLibraryCast = srcModuleLibrary.cast<String, Object>();
+          reloadedSrcs.add(
+            Uri.parse(
+              srcModuleLibraryCast['src'] as String,
+            ).normalizePath().path,
+          );
+        }
+        if (reloadedSrcs.isEmpty) {
+          chromeProxyService.allowedToCreateIsolate.complete();
+        }
+        computedReloadedSrcs.complete();
+        await chromeProxyService.allowedToCreateIsolate.future;
+        await parsedScriptsSubscription.cancel();
+      } else {
+        assert(remoteObject.value == null);
+      }
+      logger.info('\$dartHotRestartDwds request complete.');
+    } on WipError catch (exception) {
+      final code = exception.error?['code'];
+      final message = exception.error?['message'];
+      // This corresponds to `Execution context was destroyed` which can
+      // occur during a hot restart that must fall back to a full reload.
+      if (code != RPCErrorKind.kServerError.code) {
+        return {
+          'error': {'code': code, 'message': message, 'data': exception},
+        };
+      }
+    } on ChromeDebugException catch (exception) {
+      // Exceptions thrown by the injected client during hot restart.
+      return {
+        'error': {
+          'code': RPCErrorKind.kInternalError.code,
+          'message': '$exception',
+        },
+      };
+    }
+    logger.info('Waiting for Isolate Start event.');
+    await waitForIsolateStarted;
+    chromeProxyService.terminatingIsolates = false;
+
+    logger.info('Successful hot restart');
+    return {'result': Success().toJson()};
+  }
+
+  void _waitForResumeEventToRunMain(ChromeProxyService chromeProxyService) {
+    StreamSubscription<String>? resumeEventsSubscription;
+    resumeEventsSubscription = chromeProxyService.resumeAfterRestartEventsStream
+        .listen((_) async {
+          await resumeEventsSubscription!.cancel();
+          await chromeProxyService.inspector.jsEvaluate(
+            '\$dartReadyToRunMain();',
+          );
+        });
+  }
+
+  Future<Map<String, dynamic>> _fullReload(
+    ChromeProxyService chromeProxyService,
+  ) async {
+    logger.info('Attempting a full reload');
+    await chromeProxyService.remoteDebugger.enablePage();
+    await chromeProxyService.remoteDebugger.pageReload();
+    logger.info('Successful full reload');
+    return {'result': Success().toJson()};
+  }
+
+  Future<void> _disableBreakpointsAndResume(
+    VmService client,
+    ChromeProxyService chromeProxyService,
+  ) async {
+    logger.info('Attempting to disable breakpoints and resume the isolate');
+    final vm = await client.getVM();
+    final isolates = vm.isolates;
+    if (isolates == null || isolates.isEmpty) {
+      throw StateError('No active isolate to resume.');
+    }
+    final isolateId = isolates.first.id;
+    if (isolateId == null) {
+      throw StateError('No active isolate to resume.');
+    }
+    await chromeProxyService.disableBreakpoints();
+    try {
+      // Any checks for paused status result in race conditions or hangs
+      // at this point:
+      //
+      // - `getIsolate()` and check for status:
+      //    the app might still pause on existing breakpoint.
+      //
+      // - `pause()` and wait for `Debug.paused` event:
+      //   chrome does not send the `Debug.Paused `notification
+      //   without shifting focus to chrome.
+      //
+      // Instead, just try resuming and
+      // ignore failures indicating that the app is already running:
+      //
+      // WipError -32000 Can only perform operation while paused.
+      await client.resume(isolateId);
+    } on RPCError catch (e, s) {
+      if (!e.message.contains('Can only perform operation while paused')) {
+        logger.severe('Hot restart failed to resume exiting isolate', e, s);
+        rethrow;
+      }
+    }
+    logger.info('Successfully disabled breakpoints and resumed the isolate');
+  }
 }
 
-// WebSocket-based DWDS VM client logger.
-final _webSocketLogger = Logger('WebSocketDwdsVmClient');
-
 /// WebSocket-based DWDS VM client.
-class WebSocketDwdsVmClient implements DwdsVmClient {
+final class WebSocketDwdsVmClient
+    extends DwdsVmClient<WebSocketProxyService, WebSocketDebugService> {
   @override
-  final VmService client;
-  final StreamController<VmRequest> _requestController;
-  final StreamController<VmResponse> _responseController;
-  Future<void>? _closed;
-
-  WebSocketDwdsVmClient(
-    this.client,
-    this._requestController,
-    this._responseController,
-  );
-
-  @override
-  Future<void> close() => _closed ??= () async {
-    await _requestController.close();
-    await _responseController.close();
-    await client.dispose();
-  }();
+  final logger = Logger('WebSocketDwdsVmClient');
 
   static Future<WebSocketDwdsVmClient> create(
     WebSocketDebugService debugService,
+    Uri? ddsUri,
   ) async {
-    _webSocketLogger.fine('Creating WebSocket DWDS VM client');
-    final webSocketProxyService = debugService.webSocketProxyService;
-    final responseController = StreamController<VmResponse>();
-    final responseSink = responseController.sink;
-    final responseStream = responseController.stream.asBroadcastStream();
-    final requestController = StreamController<VmRequest>();
-    final requestSink = requestController.sink;
-    final requestStream = requestController.stream;
-
-    _setUpWebSocketVmServerConnection(
-      webSocketProxyService: webSocketProxyService,
-      debugService: debugService,
-      responseStream: responseStream,
-      responseSink: responseSink,
-      requestStream: requestStream,
-      requestSink: requestSink,
-    );
-
-    final client = _setUpWebSocketVmClient(
-      responseStream: responseStream,
-      requestController: requestController,
-      requestSink: requestSink,
-    );
-
-    await _registerServiceExtensions(
-      client: client,
-      webSocketProxyService: webSocketProxyService,
-    );
-
-    _webSocketLogger.fine('WebSocket DWDS VM client created successfully');
-    return WebSocketDwdsVmClient(client, requestController, responseController);
+    final dwdsVmClient = WebSocketDwdsVmClient(debugService: debugService);
+    dwdsVmClient.logger.fine('Creating WebSocket DWDS VM client');
+    await dwdsVmClient.initialize(ddsUri: ddsUri);
+    return dwdsVmClient;
   }
 
-  static VmService _setUpWebSocketVmClient({
-    required Stream<VmResponse> responseStream,
-    required StreamSink<VmRequest> requestSink,
-    required StreamController<VmRequest> requestController,
-  }) {
-    final client = VmService(responseStream.map(jsonEncode), (request) {
-      if (requestController.isClosed) {
-        _webSocketLogger.warning(
-          'Attempted to send a request but the connection is closed:\n\n$request',
-        );
-        return;
-      }
-      requestSink.add(Map<String, Object>.from(jsonDecode(request)));
-    });
-    return client;
-  }
+  WebSocketDwdsVmClient({required super.debugService});
 
-  static void _setUpWebSocketVmServerConnection({
-    required WebSocketProxyService webSocketProxyService,
-    required WebSocketDebugService debugService,
-    required Stream<VmResponse> responseStream,
-    required StreamSink<VmResponse> responseSink,
-    required Stream<VmRequest> requestStream,
-    required StreamSink<VmRequest> requestSink,
-  }) {
-    responseStream.listen((request) async {
-      final response = await _maybeHandleWebSocketServiceExtensionRequest(
-        request,
-        webSocketProxyService: webSocketProxyService,
-      );
-      if (response != null) {
-        requestSink.add(response);
-      }
-    });
-
-    final vmServerConnection = VmServerConnection(
-      requestStream,
-      responseSink,
-      debugService.serviceExtensionRegistry,
-      webSocketProxyService,
-    );
-
-    // Register service extensions
-    for (final extension in _NamespacedServiceExtension.values) {
-      _webSocketLogger.finest(
-        'Registering service extension: ${extension.method}',
-      );
-      debugService.serviceExtensionRegistry.registerExtension(
-        extension.method,
-        vmServerConnection,
-      );
-    }
-  }
-
-  static Future<VmRequest?> _maybeHandleWebSocketServiceExtensionRequest(
-    VmResponse request, {
-    required WebSocketProxyService webSocketProxyService,
-  }) async {
-    VmRequest? response;
-    final method = request['method'];
-
-    _webSocketLogger.finest('Processing service extension method: $method');
-
-    if (method == _NamespacedServiceExtension.flutterListViews.method) {
-      response = await flutterListViewsHandler(webSocketProxyService);
-    } else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) {
-      response = extDwdsEmitEventHandler(request, _webSocketLogger);
-    } else if (method == _NamespacedServiceExtension.extDwdsReload.method) {
-      response = {'result': 'Reload not implemented'};
-    } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) {
-      response = await extDwdsSendEventHandler(request, null, _webSocketLogger);
-    } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) {
-      response = {'result': 'Screenshot not implemented'};
-    }
-
-    if (response != null) {
-      response['id'] = request['id'] as String;
-      response['jsonrpc'] = '2.0';
-    }
-    return response;
-  }
-
-  static Future<void> _registerServiceExtensions({
+  @override
+  Future<void> registerServiceExtensions({
     required VmService client,
-    required WebSocketProxyService webSocketProxyService,
+    required WebSocketProxyService proxyService,
   }) async {
     client.registerServiceCallback(
       'hotRestart',
       (request) => captureElapsedTime(
-        () => webSocketProxyService.hotRestart(),
+        () => proxyService.hotRestart(),
         (_) => DwdsEvent.hotRestart(),
       ),
     );
     await client.registerService('hotRestart', 'DWDS');
   }
-}
 
-/// Shared handler for Flutter list views service extension.
-Future<Map<String, Object>> flutterListViewsHandler(
-  ProxyService proxyService,
-) async {
-  final vm = await proxyService.getVM();
-  final isolates = vm.isolates;
-  return <String, Object>{
-    'result': <String, Object>{
-      'views': <Object>[
-        for (final isolate in isolates ?? [])
-          <String, Object>{'id': isolate.id, 'isolate': isolate.toJson()},
-      ],
-    },
-  };
-}
+  @override
+  Future<VmRequest?> maybeHandleServiceExtensionRequestImpl(
+    VmResponse request, {
+    required WebSocketProxyService proxyService,
+    DwdsStats? dwdsStats,
+    Future<void>? clientFuture,
+  }) async {
+    VmRequest? response;
+    final method = request['method'];
 
-/// Shared handler for DWDS emit event service extension.
-Map<String, Object> extDwdsEmitEventHandler(VmResponse request, Logger logger) {
-  final event = request['params'] as Map<String, dynamic>?;
-  if (event != null) {
-    final type = event['type'] as String?;
-    final payload = event['payload'] as Map<String, dynamic>?;
-    if (type != null && payload != null) {
-      logger.fine('EmitEvent: $type $payload');
-      emitEvent(DwdsEvent(type, payload));
+    logger.finest('Processing service extension method: $method');
+
+    if (method == _NamespacedServiceExtension.flutterListViews.method) {
+      response = await flutterListViewsHandler(proxyService);
+    } else if (method == _NamespacedServiceExtension.extDwdsEmitEvent.method) {
+      response = extDwdsEmitEventHandler(request, logger);
+    } else if (method == _NamespacedServiceExtension.extDwdsReload.method) {
+      response = {'result': 'Reload not implemented'};
+    } else if (method == _NamespacedServiceExtension.extDwdsSendEvent.method) {
+      response = await extDwdsSendEventHandler(request, null, logger);
+    } else if (method == _NamespacedServiceExtension.extDwdsScreenshot.method) {
+      response = {'result': 'Screenshot not implemented'};
     }
+    return response;
   }
-  return {'result': Success().toJson()};
-}
-
-/// Shared handler for DWDS send event service extension.
-Future<Map<String, Object>> extDwdsSendEventHandler(
-  VmResponse request,
-  DwdsStats? dwdsStats,
-  Logger logger,
-) async {
-  logger.fine('SendEvent: $request');
-  if (dwdsStats != null) {
-    _processSendEvent(request, dwdsStats);
-  }
-  return {'result': Success().toJson()};
-}
-
-void _processSendEvent(Map<String, dynamic> request, DwdsStats dwdsStats) {
-  final event = request['params'] as Map<String, dynamic>?;
-  if (event == null) return;
-  final type = event['type'] as String?;
-  final payload = event['payload'] as Map<String, dynamic>?;
-  switch (type) {
-    case 'DevtoolsEvent':
-      {
-        _chromeLogger.finest('Received DevTools event: $event');
-        final action = payload?['action'] as String?;
-        final screen = payload?['screen'] as String?;
-        if (screen != null && action == 'pageReady') {
-          _recordDwdsStats(dwdsStats, screen);
-        } else {
-          _chromeLogger.finest('Ignoring unknown event: $event');
-        }
-      }
-  }
-}
-
-void _recordDwdsStats(DwdsStats dwdsStats, String screen) {
-  if (dwdsStats.isFirstDebuggerReady) {
-    final devToolsStart = dwdsStats.devToolsStart;
-    final debuggerStart = dwdsStats.debuggerStart;
-    if (devToolsStart != null) {
-      final devToolLoadTime = DateTime.now()
-          .difference(devToolsStart)
-          .inMilliseconds;
-      emitEvent(DwdsEvent.devToolsLoad(devToolLoadTime, screen));
-      _chromeLogger.fine('DevTools load time: $devToolLoadTime ms');
-    }
-    if (debuggerStart != null) {
-      final debuggerReadyTime = DateTime.now()
-          .difference(debuggerStart)
-          .inMilliseconds;
-      emitEvent(DwdsEvent.debuggerReady(debuggerReadyTime, screen));
-      _chromeLogger.fine('Debugger ready time: $debuggerReadyTime ms');
-    }
-  } else {
-    _chromeLogger.finest('Debugger and DevTools stats are already recorded.');
-  }
-}
-
-Future<int> tryGetContextId(
-  ChromeProxyService chromeProxyService, {
-  int retries = 3,
-}) async {
-  const waitInMs = 50;
-  for (var retry = 0; retry < retries; retry++) {
-    final tryId = await chromeProxyService.executionContext.id;
-    if (tryId != null) return tryId;
-    await Future.delayed(const Duration(milliseconds: waitInMs));
-  }
-  throw StateError('No context with the running Dart application.');
-}
-
-Future<Map<String, dynamic>> _hotRestart(
-  ChromeProxyService chromeProxyService,
-  VmService client,
-) async {
-  _chromeLogger.info('Attempting a hot restart');
-
-  chromeProxyService.terminatingIsolates = true;
-  await _disableBreakpointsAndResume(client, chromeProxyService);
-  try {
-    _chromeLogger.info('Attempting to get execution context ID.');
-    await tryGetContextId(chromeProxyService);
-    _chromeLogger.info('Got execution context ID.');
-  } on StateError catch (e) {
-    // We couldn't find the execution context. `hotRestart` may have been
-    // triggered in the middle of a full reload.
-    return {
-      'error': {'code': RPCErrorKind.kInternalError.code, 'message': e.message},
-    };
-  }
-  // Start listening for isolate create events before issuing a hot
-  // restart. Only return success after the isolate has fully started.
-  final stream = chromeProxyService.onEvent('Isolate');
-  final waitForIsolateStarted = stream.firstWhere(
-    (event) => event.kind == EventKind.kIsolateStart,
-  );
-  try {
-    // If we should pause isolates on start, then only run main once we get a
-    // resume event.
-    final pauseIsolatesOnStart = chromeProxyService.pauseIsolatesOnStart;
-    if (pauseIsolatesOnStart) {
-      _waitForResumeEventToRunMain(chromeProxyService);
-    }
-    // Generate run id to hot restart all apps loaded into the tab.
-    final runId = const Uuid().v4().toString();
-
-    // When using the DDC library bundle format, we determine the sources that
-    // were reloaded during a hot restart to then wait until all the sources are
-    // parsed before finishing hot restart. This is necessary before we can
-    // recompute any source location metadata in the `ChromeProxyService`.
-    // TODO(srujzs): We don't do this for the AMD module format, should we? It
-    // would require adding an extra parameter in the AMD strategy. As we're
-    // planning to deprecate it, for now, do nothing.
-    final isDdcLibraryBundle =
-        globalToolConfiguration.loadStrategy is DdcLibraryBundleStrategy;
-    final computedReloadedSrcs = Completer<void>();
-    final reloadedSrcs = <String>{};
-    late StreamSubscription<String> parsedScriptsSubscription;
-    if (isDdcLibraryBundle) {
-      // Injected client should send a request to recreate the isolate after the
-      // hot restart. The creation of the isolate should in turn wait until all
-      // scripts are parsed.
-      chromeProxyService.allowedToCreateIsolate = Completer<void>();
-      final debugger = await chromeProxyService.debuggerFuture;
-      parsedScriptsSubscription = debugger.parsedScriptsController.stream
-          .listen((url) {
-            computedReloadedSrcs.future.then((_) async {
-              reloadedSrcs.remove(Uri.parse(url).normalizePath().path);
-              if (reloadedSrcs.isEmpty &&
-                  !chromeProxyService.allowedToCreateIsolate.isCompleted) {
-                chromeProxyService.allowedToCreateIsolate.complete();
-              }
-            });
-          });
-    }
-    _chromeLogger.info('Issuing \$dartHotRestartDwds request');
-    final remoteObject = await chromeProxyService.inspector.jsEvaluate(
-      '\$dartHotRestartDwds(\'$runId\', $pauseIsolatesOnStart);',
-      awaitPromise: true,
-      returnByValue: true,
-    );
-    if (isDdcLibraryBundle) {
-      final reloadedSrcModuleLibraries = (remoteObject.value as List)
-          .cast<Map>();
-      for (final srcModuleLibrary in reloadedSrcModuleLibraries) {
-        final srcModuleLibraryCast = srcModuleLibrary.cast<String, Object>();
-        reloadedSrcs.add(
-          Uri.parse(srcModuleLibraryCast['src'] as String).normalizePath().path,
-        );
-      }
-      if (reloadedSrcs.isEmpty) {
-        chromeProxyService.allowedToCreateIsolate.complete();
-      }
-      computedReloadedSrcs.complete();
-      await chromeProxyService.allowedToCreateIsolate.future;
-      await parsedScriptsSubscription.cancel();
-    } else {
-      assert(remoteObject.value == null);
-    }
-    _chromeLogger.info('\$dartHotRestartDwds request complete.');
-  } on WipError catch (exception) {
-    final code = exception.error?['code'];
-    final message = exception.error?['message'];
-    // This corresponds to `Execution context was destroyed` which can
-    // occur during a hot restart that must fall back to a full reload.
-    if (code != RPCErrorKind.kServerError.code) {
-      return {
-        'error': {'code': code, 'message': message, 'data': exception},
-      };
-    }
-  } on ChromeDebugException catch (exception) {
-    // Exceptions thrown by the injected client during hot restart.
-    return {
-      'error': {
-        'code': RPCErrorKind.kInternalError.code,
-        'message': '$exception',
-      },
-    };
-  }
-  _chromeLogger.info('Waiting for Isolate Start event.');
-  await waitForIsolateStarted;
-  chromeProxyService.terminatingIsolates = false;
-
-  _chromeLogger.info('Successful hot restart');
-  return {'result': Success().toJson()};
-}
-
-void _waitForResumeEventToRunMain(ChromeProxyService chromeProxyService) {
-  StreamSubscription<String>? resumeEventsSubscription;
-  resumeEventsSubscription = chromeProxyService.resumeAfterRestartEventsStream
-      .listen((_) async {
-        await resumeEventsSubscription!.cancel();
-        await chromeProxyService.inspector.jsEvaluate(
-          '\$dartReadyToRunMain();',
-        );
-      });
-}
-
-Future<Map<String, dynamic>> _fullReload(
-  ChromeProxyService chromeProxyService,
-) async {
-  _chromeLogger.info('Attempting a full reload');
-  await chromeProxyService.remoteDebugger.enablePage();
-  await chromeProxyService.remoteDebugger.pageReload();
-  _chromeLogger.info('Successful full reload');
-  return {'result': Success().toJson()};
-}
-
-Future<void> _disableBreakpointsAndResume(
-  VmService client,
-  ChromeProxyService chromeProxyService,
-) async {
-  _chromeLogger.info(
-    'Attempting to disable breakpoints and resume the isolate',
-  );
-  final vm = await client.getVM();
-  final isolates = vm.isolates;
-  if (isolates == null || isolates.isEmpty) {
-    throw StateError('No active isolate to resume.');
-  }
-  final isolateId = isolates.first.id;
-  if (isolateId == null) {
-    throw StateError('No active isolate to resume.');
-  }
-  await chromeProxyService.disableBreakpoints();
-  try {
-    // Any checks for paused status result in race conditions or hangs
-    // at this point:
-    //
-    // - `getIsolate()` and check for status:
-    //    the app might still pause on existing breakpoint.
-    //
-    // - `pause()` and wait for `Debug.paused` event:
-    //   chrome does not send the `Debug.Paused `notification
-    //   without shifting focus to chrome.
-    //
-    // Instead, just try resuming and
-    // ignore failures indicating that the app is already running:
-    //
-    // WipError -32000 Can only perform operation while paused.
-    await client.resume(isolateId);
-  } on RPCError catch (e, s) {
-    if (!e.message.contains('Can only perform operation while paused')) {
-      _chromeLogger.severe(
-        'Hot restart failed to resume exiting isolate',
-        e,
-        s,
-      );
-      rethrow;
-    }
-  }
-  _chromeLogger.info(
-    'Successfully disabled breakpoints and resumed the isolate',
-  );
 }
diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart
index a5d25e0..94d9e32 100644
--- a/dwds/lib/src/handlers/dev_handler.dart
+++ b/dwds/lib/src/handlers/dev_handler.dart
@@ -34,10 +34,11 @@
 import 'package:dwds/src/servers/extension_backend.dart';
 import 'package:dwds/src/servers/extension_debugger.dart';
 import 'package:dwds/src/services/app_debug_services.dart';
-import 'package:dwds/src/services/chrome_proxy_service.dart';
-import 'package:dwds/src/services/debug_service.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_service.dart';
+import 'package:dwds/src/services/chrome/chrome_proxy_service.dart';
 import 'package:dwds/src/services/expression_compiler.dart';
-import 'package:dwds/src/services/web_socket_proxy_service.dart';
+import 'package:dwds/src/services/web_socket/web_socket_debug_service.dart';
+import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:logging/logging.dart';
 import 'package:shelf/shelf.dart';
@@ -120,9 +121,7 @@
   };
 
   Future<void> close() => _closed ??= () async {
-    for (final sub in _subs) {
-      await sub.cancel();
-    }
+    await Future.wait([for (final sub in _subs) sub.cancel()]);
     for (final handler in _sseHandlers.values) {
       handler.shutdown();
     }
@@ -230,12 +229,12 @@
       // machine. This allows consumers of DWDS to provide a `hostname` for
       // debugging through the Dart Debug Extension without impacting the local
       // debug workflow.
-      'localhost',
-      webkitDebugger,
-      executionContext,
-      _assetReader,
-      appConnection,
-      _urlEncoder,
+      hostname: 'localhost',
+      remoteDebugger: webkitDebugger,
+      executionContext: executionContext,
+      assetReader: _assetReader,
+      appConnection: appConnection,
+      urlEncoder: _urlEncoder,
       onResponse: (response) {
         if (response['error'] == null) return;
         _logger.finest('VmService proxy responded with an error:\n$response');
@@ -285,10 +284,11 @@
     AppConnection appConnection,
   ) async {
     final webSocketDebugService = await WebSocketDebugService.start(
-      'localhost',
-      appConnection,
-      _assetReader,
+      hostname: 'localhost',
+      appConnection: appConnection,
+      assetReader: _assetReader,
       sendClientRequest: _sendRequestToClients,
+      ddsConfig: _ddsConfig,
     );
     return _createAppDebugServicesWebSocketMode(
       webSocketDebugService,
@@ -851,18 +851,12 @@
     if (_ddsConfig.enable) {
       dds = await debugService.startDartDevelopmentService();
     }
-    final vmClient = await ChromeDwdsVmClient.create(
-      debugService,
-      dwdsStats,
-      dds?.wsUri,
-    );
-    final appDebugService = ChromeAppDebugServices(
-      debugService,
-      vmClient,
-      dwdsStats,
-      dds?.wsUri,
-      dds?.devToolsUri,
-      dds?.dtdUri,
+    final vmClient = await ChromeDwdsVmClient.create(debugService, dds?.wsUri);
+    final appDebugService = AppDebugServices(
+      debugService: debugService,
+      dwdsVmClient: vmClient,
+      dwdsStats: dwdsStats,
+      dds: dds,
     );
     final encodedUri = await debugService.encodedUri;
     _logger.info('Debug service listening on $encodedUri\n');
@@ -889,12 +883,18 @@
     WebSocketDebugService webSocketDebugService,
     AppConnection appConnection,
   ) async {
+    DartDevelopmentServiceLauncher? dds;
+    if (_ddsConfig.enable) {
+      dds = await webSocketDebugService.startDartDevelopmentService();
+    }
     final wsVmClient = await WebSocketDwdsVmClient.create(
       webSocketDebugService,
+      dds?.wsUri,
     );
-    final wsAppDebugService = WebSocketAppDebugServices(
-      webSocketDebugService,
-      wsVmClient,
+    final wsAppDebugService = AppDebugServices(
+      debugService: webSocketDebugService,
+      dwdsVmClient: wsVmClient,
+      dds: dds,
     );
 
     safeUnawaited(_handleIsolateStart(appConnection));
@@ -968,12 +968,12 @@
     var appServices = _servicesByAppId[appId];
     if (appServices == null) {
       final debugService = await ChromeDebugService.start(
-        _hostname,
-        extensionDebugger,
-        executionContext,
-        _assetReader,
-        connection,
-        _urlEncoder,
+        hostname: _hostname,
+        remoteDebugger: extensionDebugger,
+        executionContext: executionContext,
+        assetReader: _assetReader,
+        appConnection: connection,
+        urlEncoder: _urlEncoder,
         onResponse: (response) {
           if (response['error'] == null) return;
           _logger.finest('VmService proxy responded with an error:\n$response');
diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js
index 001713a..f89987f 100644
--- a/dwds/lib/src/injected/client.js
+++ b/dwds/lib/src/injected/client.js
@@ -1,4 +1,4 @@
-// Generated by dart2js (, csp, intern-composite-values), the Dart to JavaScript compiler version: 3.11.0-13.0.dev.
+// Generated by dart2js (, csp, intern-composite-values), the Dart to JavaScript compiler version: 3.11.0-88.0.dev.
 // The code supports the following hooks:
 // dartPrint(message):
 //    if this function is defined it is called instead of the Dart [print]
diff --git a/dwds/lib/src/servers/extension_debugger.dart b/dwds/lib/src/servers/extension_debugger.dart
index 11f9a32..3c8d242 100644
--- a/dwds/lib/src/servers/extension_debugger.dart
+++ b/dwds/lib/src/servers/extension_debugger.dart
@@ -12,7 +12,7 @@
 import 'package:dwds/src/debugging/execution_context.dart';
 import 'package:dwds/src/debugging/remote_debugger.dart';
 import 'package:dwds/src/handlers/socket_connections.dart';
-import 'package:dwds/src/services/chrome_debug_exception.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
 import 'package:logging/logging.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
     hide StackTrace;
diff --git a/dwds/lib/src/services/app_debug_services.dart b/dwds/lib/src/services/app_debug_services.dart
index fe3a2e0..dd2f1ab 100644
--- a/dwds/lib/src/services/app_debug_services.dart
+++ b/dwds/lib/src/services/app_debug_services.dart
@@ -2,111 +2,38 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:dds/dds_launcher.dart';
 import 'package:dwds/src/dwds_vm_client.dart';
 import 'package:dwds/src/events.dart';
 import 'package:dwds/src/services/debug_service.dart';
 import 'package:dwds/src/services/proxy_service.dart';
 
 /// Common interface for debug service containers.
-abstract class AppDebugServices {
-  DebugService get debugService;
-  DwdsVmClient get dwdsVmClient;
-  DwdsStats? get dwdsStats;
-  Uri? get ddsUri;
-  Uri? get devToolsUri;
-  Uri? get dtdUri;
-  String? get connectedInstanceId;
-  set connectedInstanceId(String? id);
-  Future<void> close();
-  ProxyService get proxyService;
-}
-
-/// Chrome-based debug services container.
-class ChromeAppDebugServices implements AppDebugServices {
-  final ChromeDebugService _debugService;
-  final ChromeDwdsVmClient _dwdsVmClient;
-  final DwdsStats _dwdsStats;
-  final Uri? _ddsUri;
-  final Uri? _devToolsUri;
-  final Uri? _dtdUri;
-  Future<void>? _closed;
-  String? _connectedInstanceId;
-
-  ChromeAppDebugServices(
-    this._debugService,
-    this._dwdsVmClient,
-    this._dwdsStats,
-    this._ddsUri,
-    this._devToolsUri,
-    this._dtdUri,
-  );
-
-  @override
-  ChromeDebugService get debugService => _debugService;
-
-  @override
-  DwdsVmClient get dwdsVmClient => _dwdsVmClient;
-
-  @override
-  DwdsStats get dwdsStats => _dwdsStats;
-
-  @override
-  Uri? get ddsUri => _ddsUri;
-
-  @override
-  Uri? get devToolsUri => _devToolsUri;
-
-  @override
-  Uri? get dtdUri => _dtdUri;
-
-  @override
-  String? get connectedInstanceId => _connectedInstanceId;
-
-  @override
-  set connectedInstanceId(String? id) => _connectedInstanceId = id;
-
-  @override
-  ProxyService get proxyService => debugService.chromeProxyService;
-
-  @override
-  Future<void> close() =>
-      _closed ??= Future.wait([debugService.close(), dwdsVmClient.close()]);
-}
-
-/// WebSocket-based implementation of app debug services.
-class WebSocketAppDebugServices implements AppDebugServices {
-  final WebSocketDebugService _debugService;
-  final WebSocketDwdsVmClient _dwdsVmClient;
-  Future<void>? _closed;
-  @override
+class AppDebugServices<
+  T extends DebugService<U>,
+  U extends ProxyService,
+  V extends DwdsVmClient<U, T>
+> {
+  final T debugService;
+  final V dwdsVmClient;
+  final DartDevelopmentServiceLauncher? _dds;
+  Uri? get ddsUri => _dds?.wsUri;
+  Uri? get devToolsUri => _dds?.devToolsUri;
+  Uri? get dtdUri => _dds?.dtdUri;
+  final DwdsStats? dwdsStats;
   String? connectedInstanceId;
 
-  WebSocketAppDebugServices(this._debugService, this._dwdsVmClient);
+  Future<void>? _closed;
 
-  @override
-  WebSocketDebugService get debugService => _debugService;
+  AppDebugServices({
+    required this.debugService,
+    required this.dwdsVmClient,
+    required DartDevelopmentServiceLauncher? dds,
+    this.dwdsStats,
+  }) : _dds = dds;
 
-  @override
-  DwdsVmClient get dwdsVmClient => _dwdsVmClient;
+  ProxyService get proxyService => debugService.proxyService;
 
-  // WebSocket-only service - Chrome/DDS features not available
-  @override
-  DwdsStats? get dwdsStats => null;
-
-  @override
-  // TODO(bkonyi): DDS should still start in WebSocket mode.
-  Uri? get ddsUri => null;
-
-  @override
-  Uri? get devToolsUri => null;
-
-  @override
-  Uri? get dtdUri => null;
-
-  @override
-  ProxyService get proxyService => _debugService.webSocketProxyService;
-
-  @override
   Future<void> close() {
     return _closed ??= Future.wait([
       debugService.close(),
diff --git a/dwds/lib/src/services/chrome_debug_exception.dart b/dwds/lib/src/services/chrome/chrome_debug_exception.dart
similarity index 95%
rename from dwds/lib/src/services/chrome_debug_exception.dart
rename to dwds/lib/src/services/chrome/chrome_debug_exception.dart
index 4f8e8b5..3a33022 100644
--- a/dwds/lib/src/services/chrome_debug_exception.dart
+++ b/dwds/lib/src/services/chrome/chrome_debug_exception.dart
@@ -4,7 +4,7 @@
 
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
-class ChromeDebugException extends ExceptionDetails implements Exception {
+final class ChromeDebugException extends ExceptionDetails implements Exception {
   /// Optional, additional information about the exception.
   final Object? additionalDetails;
 
diff --git a/dwds/lib/src/services/chrome/chrome_debug_service.dart b/dwds/lib/src/services/chrome/chrome_debug_service.dart
new file mode 100644
index 0000000..6fa2731
--- /dev/null
+++ b/dwds/lib/src/services/chrome/chrome_debug_service.dart
@@ -0,0 +1,116 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:dwds/asset_reader.dart';
+import 'package:dwds/src/config/tool_configuration.dart';
+import 'package:dwds/src/connections/app_connection.dart';
+import 'package:dwds/src/debugging/execution_context.dart';
+import 'package:dwds/src/debugging/remote_debugger.dart';
+import 'package:dwds/src/services/chrome/chrome_proxy_service.dart';
+import 'package:dwds/src/services/debug_service.dart';
+import 'package:dwds/src/services/expression_compiler.dart';
+import 'package:dwds/src/utilities/shared.dart';
+import 'package:meta/meta.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:sse/server/sse_handler.dart';
+
+/// A Dart Web Debug Service.
+///
+/// Creates a [ChromeProxyService] from an existing Chrome instance.
+final class ChromeDebugService extends DebugService<ChromeProxyService> {
+  ChromeDebugService._({
+    required super.serverHostname,
+    required super.useSse,
+    required super.ddsConfig,
+    required super.urlEncoder,
+  });
+
+  static const _kSseHandlerPath = '\$debugHandler';
+
+  @protected
+  @override
+  Future<void> initialize({
+    required ChromeProxyService proxyService,
+    void Function(Map<String, Object>)? onRequest,
+    void Function(Map<String, Object?>)? onResponse,
+  }) async {
+    await super.initialize(proxyService: proxyService);
+    shelf.Handler handler;
+    // DDS will always connect to DWDS via web sockets.
+    if (useSse && !ddsConfig.enable) {
+      handler = _initializeSSEHandler(
+        chromeProxyService: proxyService,
+        onRequest: onRequest,
+        onResponse: onResponse,
+      );
+    } else {
+      handler = initializeWebSocketHandler(
+        proxyService: proxyService,
+        onRequest: onRequest,
+        onResponse: onResponse,
+      );
+    }
+    await serve(handler: handler);
+  }
+
+  static Future<ChromeDebugService> start({
+    required String hostname,
+    required RemoteDebugger remoteDebugger,
+    required ExecutionContext executionContext,
+    required AssetReader assetReader,
+    required AppConnection appConnection,
+    UrlEncoder? urlEncoder,
+    void Function(Map<String, Object>)? onRequest,
+    void Function(Map<String, Object?>)? onResponse,
+    required DartDevelopmentServiceConfiguration ddsConfig,
+    bool useSse = false,
+    ExpressionCompiler? expressionCompiler,
+  }) async {
+    final debugService = ChromeDebugService._(
+      serverHostname: hostname,
+      useSse: useSse,
+      ddsConfig: ddsConfig,
+      urlEncoder: urlEncoder,
+    );
+    final chromeProxyService = await ChromeProxyService.create(
+      remoteDebugger: remoteDebugger,
+      debugService: debugService,
+      assetReader: assetReader,
+      appConnection: appConnection,
+      executionContext: executionContext,
+      expressionCompiler: expressionCompiler,
+    );
+    await debugService.initialize(
+      proxyService: chromeProxyService,
+      onRequest: onRequest,
+      onResponse: onResponse,
+    );
+    return debugService;
+  }
+
+  shelf.Handler _initializeSSEHandler({
+    required ChromeProxyService chromeProxyService,
+    void Function(Map<String, Object>)? onRequest,
+    void Function(Map<String, Object?>)? onResponse,
+  }) {
+    final sseHandler = SseHandler(
+      Uri.parse('/$authToken/$_kSseHandlerPath'),
+      keepAlive: const Duration(seconds: 5),
+    );
+    final handler = sseHandler.handler;
+    safeUnawaited(() async {
+      while (await sseHandler.connections.hasNext) {
+        final connection = await sseHandler.connections.next;
+        handleConnection(
+          connection,
+          chromeProxyService,
+          serviceExtensionRegistry,
+          onRequest: onRequest,
+          onResponse: onResponse,
+        );
+      }
+    }());
+    return handler;
+  }
+}
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome/chrome_proxy_service.dart
similarity index 81%
rename from dwds/lib/src/services/chrome_proxy_service.dart
rename to dwds/lib/src/services/chrome/chrome_proxy_service.dart
index 7f84ac1..e5745e6 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome/chrome_proxy_service.dart
@@ -1,4 +1,4 @@
-// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
@@ -22,7 +22,7 @@
 import 'package:dwds/src/events.dart';
 import 'package:dwds/src/readers/asset_reader.dart';
 import 'package:dwds/src/services/batched_expression_evaluator.dart';
-import 'package:dwds/src/services/debug_service.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_service.dart';
 import 'package:dwds/src/services/expression_compiler.dart';
 import 'package:dwds/src/services/expression_evaluator.dart';
 import 'package:dwds/src/services/proxy_service.dart';
@@ -33,40 +33,8 @@
 import 'package:vm_service_interface/vm_service_interface.dart';
 import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
 
-// This event is identical to the one sent by the VM service from
-// sdk/lib/vmservice/vmservice.dart before existing VM service clients are
-// disconnected.
-final class DartDevelopmentServiceConnectedEvent extends Event {
-  DartDevelopmentServiceConnectedEvent({
-    required super.timestamp,
-    required this.uri,
-  }) : message =
-           'A Dart Developer Service instance has connected and this direct '
-           'connection to the VM service will now be closed. Please reconnect to '
-           'the Dart Development Service at $uri.',
-       super(kind: 'DartDevelopmentServiceConnected');
-
-  final String message;
-  final String uri;
-
-  @override
-  Map<String, Object?> toJson() => {
-    ...super.toJson(),
-    'uri': uri,
-    'message': message,
-  };
-}
-
-final class DisconnectNonDartDevelopmentServiceClients extends RPCError {
-  DisconnectNonDartDevelopmentServiceClients()
-    : super('_yieldControlToDDS', kErrorCode);
-
-  // Arbitrary error code that's unlikely to be used elsewhere.
-  static const kErrorCode = -199328;
-}
-
 /// A proxy from the chrome debug protocol to the dart vm service protocol.
-class ChromeProxyService extends ProxyService<ChromeAppInspector> {
+final class ChromeProxyService extends ProxyService<ChromeAppInspector> {
   /// Signals when isolate starts.
   Future<void> get isStarted => _startedCompleter.future;
   Completer<void> _startedCompleter = Completer<void>();
@@ -111,17 +79,22 @@
 
   bool terminatingIsolates = false;
 
-  ChromeProxyService._(
-    super.vm,
-    super.root,
-    this._assetReader,
-    this.remoteDebugger,
-    this._modules,
-    this._locations,
-    this._skipLists,
-    this.executionContext,
-    this._compiler,
-  ) {
+  ChromeProxyService._({
+    required super.vm,
+    required super.debugService,
+    required AssetReader assetReader,
+    required this.remoteDebugger,
+    required Modules modules,
+    required Locations locations,
+    required SkipLists skipLists,
+    required this.executionContext,
+    required ExpressionCompiler? compiler,
+  }) : _assetReader = assetReader,
+       _modules = modules,
+       _locations = locations,
+       _skipLists = skipLists,
+       _compiler = compiler,
+       super(root: assetReader.basePath) {
     final debugger = Debugger.create(
       remoteDebugger,
       streamNotify,
@@ -132,14 +105,14 @@
     debugger.then(_debuggerCompleter.complete);
   }
 
-  static Future<ChromeProxyService> create(
-    RemoteDebugger remoteDebugger,
-    String root,
-    AssetReader assetReader,
-    AppConnection appConnection,
-    ExecutionContext executionContext,
-    ExpressionCompiler? expressionCompiler,
-  ) async {
+  static Future<ChromeProxyService> create({
+    required RemoteDebugger remoteDebugger,
+    required ChromeDebugService debugService,
+    required AssetReader assetReader,
+    required AppConnection appConnection,
+    required ExecutionContext executionContext,
+    required ExpressionCompiler? expressionCompiler,
+  }) async {
     final vm = VM(
       name: 'ChromeDebugProxy',
       operatingSystem: Platform.operatingSystem,
@@ -155,19 +128,20 @@
       pid: -1,
     );
 
+    final root = assetReader.basePath;
     final modules = Modules(root);
     final locations = Locations(assetReader, modules, root);
     final skipLists = SkipLists(root);
     final service = ChromeProxyService._(
-      vm,
-      root,
-      assetReader,
-      remoteDebugger,
-      modules,
-      locations,
-      skipLists,
-      executionContext,
-      expressionCompiler,
+      vm: vm,
+      debugService: debugService,
+      assetReader: assetReader,
+      remoteDebugger: remoteDebugger,
+      modules: modules,
+      locations: locations,
+      skipLists: skipLists,
+      executionContext: executionContext,
+      compiler: expressionCompiler,
     );
     safeUnawaited(service.createIsolate(appConnection, newConnection: true));
     return service;
@@ -457,7 +431,7 @@
     int? column,
   }) async {
     await isInitialized;
-    _checkIsolate('addBreakpoint', isolateId);
+    checkIsolate('addBreakpoint', isolateId);
     return (await debuggerFuture).addBreakpoint(scriptId, line, column: column);
   }
 
@@ -485,7 +459,7 @@
     int? column,
   }) async {
     await isInitialized;
-    _checkIsolate('addBreakpointWithScriptUri', isolateId);
+    checkIsolate('addBreakpointWithScriptUri', isolateId);
     if (Uri.parse(scriptUri).scheme == 'dart') {
       // TODO(annagrin): Support setting breakpoints in dart SDK locations.
       // Issue: https://github.com/dart-lang/webdev/issues/1584
@@ -529,7 +503,7 @@
   }) async {
     await isInitialized;
     isolateId ??= inspector.isolate.id;
-    _checkIsolate('callServiceExtension', isolateId);
+    checkIsolate('callServiceExtension', isolateId);
     args ??= <String, String>{};
     final stringArgs = args.map(
       (k, v) => MapEntry(
@@ -666,7 +640,7 @@
       final evaluator = _expressionEvaluator;
       if (evaluator != null) {
         await isCompilerInitialized;
-        _checkIsolate('evaluate', isolateId);
+        checkIsolate('evaluate', isolateId);
 
         late Obj object;
         try {
@@ -756,7 +730,7 @@
       final evaluator = _expressionEvaluator;
       if (evaluator != null) {
         await isCompilerInitialized;
-        _checkIsolate('evaluateInFrame', isolateId);
+        checkIsolate('evaluateInFrame', isolateId);
 
         return await _getEvaluationResult(
           isolateId,
@@ -778,18 +752,6 @@
   }
 
   @override
-  Future<Isolate> getIsolate(String isolateId) =>
-      wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId));
-
-  Future<Isolate> _getIsolate(String isolateId) {
-    return captureElapsedTime(() async {
-      await isInitialized;
-      _checkIsolate('getIsolate', isolateId);
-      return inspector.isolate;
-    }, (result) => DwdsEvent.getIsolate());
-  }
-
-  @override
   Future<MemoryUsage> getMemoryUsage(String isolateId) =>
       wrapInErrorHandlerAsync(
         'getMemoryUsage',
@@ -798,7 +760,7 @@
 
   Future<MemoryUsage> _getMemoryUsage(String isolateId) async {
     await isInitialized;
-    _checkIsolate('getMemoryUsage', isolateId);
+    checkIsolate('getMemoryUsage', isolateId);
     return inspector.getMemoryUsage();
   }
 
@@ -825,23 +787,11 @@
     int? count,
   }) async {
     await isInitialized;
-    _checkIsolate('getObject', isolateId);
+    checkIsolate('getObject', isolateId);
     return inspector.getObject(objectId, offset: offset, count: count);
   }
 
   @override
-  Future<ScriptList> getScripts(String isolateId) =>
-      wrapInErrorHandlerAsync('getScripts', () => _getScripts(isolateId));
-
-  Future<ScriptList> _getScripts(String isolateId) {
-    return captureElapsedTime(() async {
-      await isInitialized;
-      _checkIsolate('getScripts', isolateId);
-      return inspector.getScripts();
-    }, (result) => DwdsEvent.getScripts());
-  }
-
-  @override
   Future<SourceReport> getSourceReport(
     String isolateId,
     List<String> reports, {
@@ -866,7 +816,7 @@
   }) {
     return captureElapsedTime(() async {
       await isInitialized;
-      _checkIsolate('getSourceReport', isolateId);
+      checkIsolate('getSourceReport', isolateId);
       return await inspector.getSourceReport(reports, scriptId: scriptId);
     }, (result) => DwdsEvent.getSourceReport());
   }
@@ -893,21 +843,11 @@
   Future<Stack> _getStack(String isolateId, {int? limit}) async {
     await isInitialized;
     await isStarted;
-    _checkIsolate('getStack', isolateId);
+    checkIsolate('getStack', isolateId);
     return (await debuggerFuture).getStack(limit: limit);
   }
 
   @override
-  Future<VM> getVM() => wrapInErrorHandlerAsync('getVM', _getVM);
-
-  Future<VM> _getVM() {
-    return captureElapsedTime(() async {
-      await isInitialized;
-      return vm;
-    }, (result) => DwdsEvent.getVM());
-  }
-
-  @override
   Future<Response> invoke(
     String isolateId,
     String targetId,
@@ -932,7 +872,7 @@
     List argumentIds,
   ) async {
     await isInitialized;
-    _checkIsolate('invoke', isolateId);
+    checkIsolate('invoke', isolateId);
     final remote = await inspector.invoke(targetId, selector, argumentIds);
     return _instanceRef(remote);
   }
@@ -994,52 +934,10 @@
     required bool internalPause,
   }) async {
     await isInitialized;
-    _checkIsolate('pause', isolateId);
+    checkIsolate('pause', isolateId);
     return (await debuggerFuture).pause(internalPause: internalPause);
   }
 
-  // Note: Ignore the optional local parameter, when it is set to `true` the
-  // request is intercepted and handled by DDS.
-  @override
-  Future<UriList> lookupResolvedPackageUris(
-    String isolateId,
-    List<String> uris, {
-    bool? local,
-  }) => wrapInErrorHandlerAsync(
-    'lookupResolvedPackageUris',
-    () => _lookupResolvedPackageUris(isolateId, uris),
-  );
-
-  Future<UriList> _lookupResolvedPackageUris(
-    String isolateId,
-    List<String> uris,
-  ) async {
-    await isInitialized;
-    _checkIsolate('lookupResolvedPackageUris', isolateId);
-    return UriList(uris: uris.map(DartUri.toResolvedUri).toList());
-  }
-
-  @override
-  Future<UriList> lookupPackageUris(String isolateId, List<String> uris) =>
-      wrapInErrorHandlerAsync(
-        'lookupPackageUris',
-        () => _lookupPackageUris(isolateId, uris),
-      );
-
-  Future<UriList> _lookupPackageUris(
-    String isolateId,
-    List<String> uris,
-  ) async {
-    await isInitialized;
-    _checkIsolate('lookupPackageUris', isolateId);
-    return UriList(uris: uris.map(DartUri.toPackageUri).toList());
-  }
-
-  @override
-  Future<Success> registerService(String service, String alias) {
-    return rpcNotSupportedFuture('registerService');
-  }
-
   @override
   Future<ReloadReport> reloadSources(
     String isolateId, {
@@ -1049,7 +947,7 @@
     String? packagesUri,
   }) async {
     await isInitialized;
-    _checkIsolate('reloadSources', isolateId);
+    checkIsolate('reloadSources', isolateId);
 
     ReloadReport getFailedReloadReport(String error) =>
         _ReloadReportWithMetadata(success: false)
@@ -1167,7 +1065,7 @@
     String breakpointId,
   ) async {
     await isInitialized;
-    _checkIsolate('removeBreakpoint', isolateId);
+    checkIsolate('removeBreakpoint', isolateId);
     return (await debuggerFuture).removeBreakpoint(breakpointId);
   }
 
@@ -1194,7 +1092,7 @@
       await captureElapsedTime(() async {
         await isInitialized;
         await isStarted;
-        _checkIsolate('resume', isolateId);
+        checkIsolate('resume', isolateId);
         final debugger = await debuggerFuture;
         return await debugger.resume(step: step, frameIndex: frameIndex);
       }, (result) => DwdsEvent.resume(step));
@@ -1239,76 +1137,12 @@
     String? exceptionPauseMode,
   }) async {
     await isInitialized;
-    _checkIsolate('setIsolatePauseMode', isolateId);
+    checkIsolate('setIsolatePauseMode', isolateId);
     return (await debuggerFuture).setExceptionPauseMode(
       exceptionPauseMode ?? ExceptionPauseMode.kNone,
     );
   }
 
-  @override
-  Future<Success> setFlag(String name, String value) =>
-      wrapInErrorHandlerAsync('setFlag', () => _setFlag(name, value));
-
-  Future<Success> _setFlag(String name, String value) async {
-    if (!currentVmServiceFlags.containsKey(name)) {
-      return rpcNotSupportedFuture('setFlag');
-    }
-
-    assert(value == 'true' || value == 'false');
-    currentVmServiceFlags[name] = value == 'true';
-
-    return Success();
-  }
-
-  @override
-  Future<Success> setLibraryDebuggable(
-    String isolateId,
-    String libraryId,
-    bool isDebuggable,
-  ) {
-    return rpcNotSupportedFuture('setLibraryDebuggable');
-  }
-
-  @override
-  Future<Success> setName(String isolateId, String name) =>
-      wrapInErrorHandlerAsync('setName', () => _setName(isolateId, name));
-
-  Future<Success> _setName(String isolateId, String name) async {
-    await isInitialized;
-    _checkIsolate('setName', isolateId);
-    inspector.isolate.name = name;
-    return Success();
-  }
-
-  @override
-  Future<Success> setVMName(String name) =>
-      wrapInErrorHandlerAsync('setVMName', () => _setVMName(name));
-
-  Future<Success> _setVMName(String name) async {
-    vm.name = name;
-    streamNotify(
-      'VM',
-      Event(
-        kind: EventKind.kVMUpdate,
-        timestamp: DateTime.now().millisecondsSinceEpoch,
-        // We are not guaranteed to have an isolate at this point in time.
-        isolate: null,
-      )..vm = toVMRef(vm),
-    );
-    return Success();
-  }
-
-  @override
-  Future<Success> streamListen(String streamId) =>
-      wrapInErrorHandlerAsync('streamListen', () => _streamListen(streamId));
-
-  Future<Success> _streamListen(String streamId) async {
-    // TODO: This should return an error if the stream is already being listened
-    // to.
-    onEvent(streamId);
-    return Success();
-  }
-
   /// Returns a streamController that listens for console logs from chrome and
   /// adds all events passing [filter] to the stream.
   StreamController<Event> _chromeConsoleStreamController(
@@ -1371,35 +1205,12 @@
     return controller;
   }
 
-  /// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service
-  /// protocol [Event]s.
-  @override
-  void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) {
-    for (final debugEvent in debugEvents.events) {
-      parseDebugEvent(debugEvent);
-    }
-  }
-
   /// Parses the [DebugEvent] and emits a corresponding Dart VM Service
   /// protocol [Event].
   @override
   void parseDebugEvent(DebugEvent debugEvent) {
     if (terminatingIsolates) return;
-    if (!isIsolateRunning) return;
-    final isolateRef = inspector.isolateRef;
-
-    streamNotify(
-      EventStreams.kExtension,
-      Event(
-          kind: EventKind.kExtension,
-          timestamp: DateTime.now().millisecondsSinceEpoch,
-          isolate: isolateRef,
-        )
-        ..extensionKind = debugEvent.kind
-        ..extensionData = ExtensionData.parse(
-          jsonDecode(debugEvent.eventData) as Map<String, dynamic>,
-        ),
-    );
+    super.parseDebugEvent(debugEvent);
   }
 
   /// Parses the [RegisterEvent] and emits a corresponding Dart VM Service
@@ -1407,21 +1218,7 @@
   @override
   void parseRegisterEvent(RegisterEvent registerEvent) {
     if (terminatingIsolates) return;
-    if (!isIsolateRunning) return;
-
-    final isolate = inspector.isolate;
-    final isolateRef = inspector.isolateRef;
-    final service = registerEvent.eventData;
-    isolate.extensionRPCs?.add(service);
-
-    streamNotify(
-      EventStreams.kIsolate,
-      Event(
-        kind: EventKind.kServiceExtensionAdded,
-        timestamp: DateTime.now().millisecondsSinceEpoch,
-        isolate: isolateRef,
-      )..extensionRPC = service,
-    );
+    super.parseRegisterEvent(registerEvent);
   }
 
   /// Listens for chrome console events and handles the ones we care about.
@@ -1541,54 +1338,10 @@
     return logParams;
   }
 
-  @override
-  Future<void> yieldControlToDDS(String uri) async {
-    // This will throw an RPCError if there's already an existing DDS instance.
-    ChromeDebugService.yieldControlToDDS(uri);
-
-    // Notify existing clients that DDS has connected and they're about to be
-    // disconnected.
-    final event = DartDevelopmentServiceConnectedEvent(
-      timestamp: DateTime.now().millisecondsSinceEpoch,
-      uri: uri,
-    );
-    streamNotify(EventStreams.kService, event);
-
-    // We throw since we have no other way to control what the response content
-    // is for this RPC. The debug service will check for this particular
-    // exception as a signal to close connections to all other clients.
-    throw DisconnectNonDartDevelopmentServiceClients();
-  }
-
   Future<InstanceRef> _instanceRef(RemoteObject? obj) async {
     final instance = obj == null ? null : await inspector.instanceRefFor(obj);
     return instance ?? ChromeAppInstanceHelper.kNullInstanceRef;
   }
-
-  /// Validate that isolateId matches the current isolate we're connected to and
-  /// return that isolate.
-  ///
-  /// This is useful to call at the beginning of API methods that are passed an
-  /// isolate id.
-  Isolate _checkIsolate(String methodName, String? isolateId) {
-    final currentIsolateId = inspector.isolate.id;
-    if (currentIsolateId == null) {
-      throw StateError('No running isolate ID');
-    }
-    if (isolateId != currentIsolateId) {
-      _throwSentinel(
-        methodName,
-        SentinelKind.kCollected,
-        'Unrecognized isolateId: $isolateId',
-      );
-    }
-    return inspector.isolate;
-  }
-
-  static Never _throwSentinel(String method, String kind, String message) {
-    final data = <String, String>{'kind': kind, 'valueAsString': message};
-    throw SentinelException.parse(method, data);
-  }
 }
 
 // The default `ReloadReport`'s `toJson` only emits the type and success of the
diff --git a/dwds/lib/src/services/debug_service.dart b/dwds/lib/src/services/debug_service.dart
index d6a9fa4..787a277 100644
--- a/dwds/lib/src/services/debug_service.dart
+++ b/dwds/lib/src/services/debug_service.dart
@@ -10,27 +10,16 @@
 
 import 'package:dds/dds_launcher.dart';
 import 'package:dwds/src/config/tool_configuration.dart';
-import 'package:dwds/src/connections/app_connection.dart';
-import 'package:dwds/src/debugging/execution_context.dart';
-import 'package:dwds/src/debugging/remote_debugger.dart';
 import 'package:dwds/src/events.dart';
-import 'package:dwds/src/readers/asset_reader.dart';
-import 'package:dwds/src/services/chrome_proxy_service.dart';
-import 'package:dwds/src/services/expression_compiler.dart';
-import 'package:dwds/src/services/web_socket_proxy_service.dart';
+import 'package:dwds/src/services/proxy_service.dart';
 import 'package:dwds/src/utilities/server.dart';
-import 'package:dwds/src/utilities/shared.dart';
 import 'package:logging/logging.dart';
+import 'package:meta/meta.dart';
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf/shelf.dart' hide Response;
 import 'package:shelf_web_socket/shelf_web_socket.dart';
-import 'package:sse/server/sse_handler.dart';
 import 'package:stream_channel/stream_channel.dart';
 import 'package:vm_service/vm_service.dart';
 import 'package:vm_service_interface/vm_service_interface.dart';
-import 'package:web_socket_channel/web_socket_channel.dart';
-
-const _kSseHandlerPath = '\$debugHandler';
 
 bool _acceptNewConnections = true;
 
@@ -39,157 +28,89 @@
 
 Logger _logger = Logger('DebugService');
 
-void _handleConnection(
-  StreamChannel channel,
-  ChromeProxyService chromeProxyService,
-  ServiceExtensionRegistry serviceExtensionRegistry, {
-  void Function(Map<String, Object>)? onRequest,
-  void Function(Map<String, Object?>)? onResponse,
-}) {
-  final clientId = _clientId++;
-  final responseController = StreamController<Map<String, Object?>>();
-  responseController.stream
-      .asyncMap<String>((response) async {
-        // This error indicates a successful invocation to _yieldControlToDDS.
-        // We don't have a good way to access the list of connected clients
-        // while also being able to determine which client invoked the RPC
-        // without some form of client ID.
-        //
-        // We can probably do better than this, but it will likely involve some
-        // refactoring.
-        if (response case {
-          'error': {
-            'code': DisconnectNonDartDevelopmentServiceClients.kErrorCode,
-          },
-        }) {
-          final nonDdsClients = _clientConnections.entries
-              .where((MapEntry<int, StreamChannel> e) => e.key != clientId)
-              .map((e) => e.value);
-          await Future.wait([
-            for (final client in nonDdsClients) client.sink.close(),
-          ]);
-          // Remove the artificial error and return Success.
-          response.remove('error');
-          response['result'] = Success().toJson();
-        }
-        if (onResponse != null) onResponse(response);
-        return jsonEncode(response);
-      })
-      .listen(channel.sink.add, onError: channel.sink.addError);
-  final inputStream = channel.stream.map((value) {
-    if (value is List<int>) {
-      value = utf8.decode(value);
-    } else if (value is! String) {
-      throw StateError(
-        'Got value with unexpected type ${value.runtimeType} from web '
-        'socket, expected a List<int> or String.',
-      );
-    }
-    final request = Map<String, Object>.from(jsonDecode(value));
-    if (onRequest != null) onRequest(request);
-    return request;
-  });
-  VmServerConnection(
-    inputStream,
-    responseController.sink,
-    serviceExtensionRegistry,
-    chromeProxyService,
-  ).done.whenComplete(() {
-    _clientConnections.remove(clientId);
-    if (!_acceptNewConnections && _clientConnections.isEmpty) {
-      // DDS has disconnected so we can allow for clients to connect directly
-      // to DWDS.
-      ChromeDebugService._ddsUri = null;
-      _acceptNewConnections = true;
-    }
-  });
-  _clientConnections[clientId] = channel;
-}
-
-void Function(WebSocketChannel, String?) _createNewConnectionHandler(
-  ChromeProxyService chromeProxyService,
-  ServiceExtensionRegistry serviceExtensionRegistry, {
-  void Function(Map<String, Object>)? onRequest,
-  void Function(Map<String, Object?>)? onResponse,
-}) {
-  return (webSocket, subprotocol) {
-    _handleConnection(
-      webSocket,
-      chromeProxyService,
-      serviceExtensionRegistry,
-      onRequest: onRequest,
-      onResponse: onResponse,
-    );
-  };
-}
-
-Future<void> _handleSseConnections(
-  SseHandler handler,
-  ChromeProxyService chromeProxyService,
-  ServiceExtensionRegistry serviceExtensionRegistry, {
-  void Function(Map<String, Object>)? onRequest,
-  void Function(Map<String, Object?>)? onResponse,
-}) async {
-  while (await handler.connections.hasNext) {
-    final connection = await handler.connections.next;
-    _handleConnection(
-      connection,
-      chromeProxyService,
-      serviceExtensionRegistry,
-      onRequest: onRequest,
-      onResponse: onResponse,
-    );
-  }
-}
-
 /// Common interface for debug services (Chrome or WebSocket based).
-abstract class DebugService {
-  String get hostname;
-  int get port;
-  String get uri;
-  Future<String> get encodedUri;
-  ServiceExtensionRegistry get serviceExtensionRegistry;
-  Future<void> close();
-}
+abstract class DebugService<T extends ProxyService> {
+  DebugService({
+    required this.serverHostname,
+    required this.ddsConfig,
+    required this.urlEncoder,
+    required this.useSse,
+  });
 
-/// A Dart Web Debug Service.
-///
-/// Creates a [ChromeProxyService] from an existing Chrome instance.
-class ChromeDebugService implements DebugService {
-  static String? _ddsUri;
+  /// The URI pointing to the VM service implementation hosted by the [DebugService].
+  String get uri => _uri.toString();
 
-  final ChromeProxyService chromeProxyService;
-  @override
-  final String hostname;
-  @override
-  final ServiceExtensionRegistry serviceExtensionRegistry;
-  @override
-  final int port;
-  final String authToken;
-  final HttpServer _server;
-  final bool _useSse;
-  final DartDevelopmentServiceConfiguration _ddsConfig;
-  final UrlEncoder? _urlEncoder;
+  Uri get _uri => _cachedUri ??= () {
+    final dds = _dds;
+    if (ddsConfig.enable && dds != null) {
+      return useSse ? dds.sseUri : dds.wsUri;
+    }
+    return useSse
+        ? Uri(
+            scheme: 'sse',
+            host: _server.address.host,
+            port: _server.port,
+            path: '$authToken/\$debugHandler',
+          )
+        : Uri(
+            scheme: 'ws',
+            host: _server.address.host,
+            port: _server.port,
+            path: authToken,
+          );
+  }();
+
+  Uri? _cachedUri;
+  String? _ddsUri;
+
+  late final T proxyService;
+
+  final UrlEncoder? urlEncoder;
+
+  late final String authToken = _makeAuthToken();
+  final bool useSse;
+
+  Future<String> get encodedUri async {
+    return _encodedUri ??= await urlEncoder?.call(uri) ?? uri;
+  }
+
+  String? _encodedUri;
+
+  DartDevelopmentServiceConfiguration ddsConfig;
   DartDevelopmentServiceLauncher? _dds;
 
+  final String serverHostname;
+  late final HttpServer _server;
+
+  String get hostname => _uri.host;
+  int get port => _uri.port;
+
+  final serviceExtensionRegistry = ServiceExtensionRegistry();
+
   /// Null until [close] is called.
   ///
   /// All subsequent calls to [close] will return this future.
   Future<void>? _closed;
 
-  ChromeDebugService._(
-    this.chromeProxyService,
-    this.hostname,
-    this.port,
-    this.authToken,
-    this.serviceExtensionRegistry,
-    this._server,
-    this._useSse,
-    this._ddsConfig,
-    this._urlEncoder,
-  );
+  @protected
+  @mustCallSuper
+  @mustBeOverridden
+  // False positive
+  // ignore: avoid-redundant-async
+  Future<void> initialize({required T proxyService}) async {
+    this.proxyService = proxyService;
+  }
 
-  @override
+  @protected
+  Future<void> serve({required shelf.Handler handler}) async {
+    _server = await startHttpServer(serverHostname, port: 44456);
+    serveHttpRequests(_server, handler, (e, s) {
+      _logger.warning('Error serving requests', e);
+      emitEvent(DwdsEvent.httpRequestException('$runtimeType', '$e:$s'));
+    });
+  }
+
+  /// Closes the debug service and associated resources.
   Future<void> close() => _closed ??= Future.wait([
     _server.close(),
     if (_dds != null) _dds!.shutdown(),
@@ -198,51 +119,29 @@
   Future<DartDevelopmentServiceLauncher> startDartDevelopmentService() async {
     // Note: DDS can handle both web socket and SSE connections with no
     // additional configuration.
+    final hostname = _server.address.host;
     _dds = await DartDevelopmentServiceLauncher.start(
       remoteVmServiceUri: Uri(
         scheme: 'http',
         host: hostname,
-        port: port,
+        port: _server.port,
         path: authToken,
       ),
       serviceUri: Uri(
         scheme: 'http',
         host: hostname,
-        port: _ddsConfig.port ?? 0,
+        port: ddsConfig.port ?? 0,
       ),
-      devToolsServerAddress: _ddsConfig.devToolsServerAddress,
-      serveDevTools: _ddsConfig.serveDevTools,
+      devToolsServerAddress: ddsConfig.devToolsServerAddress,
+      serveDevTools: ddsConfig.serveDevTools,
     );
     return _dds!;
   }
 
-  @override
-  String get uri {
-    final dds = _dds;
-    if (_ddsConfig.enable && dds != null) {
-      return (_useSse ? dds.sseUri : dds.wsUri).toString();
-    }
-    return (_useSse
-            ? Uri(
-                scheme: 'sse',
-                host: hostname,
-                port: port,
-                path: '$authToken/\$debugHandler',
-              )
-            : Uri(scheme: 'ws', host: hostname, port: port, path: authToken))
-        .toString();
-  }
-
-  String? _encodedUri;
-  @override
-  Future<String> get encodedUri async {
-    if (_encodedUri != null) return _encodedUri!;
-    var encoded = uri;
-    if (_urlEncoder != null) encoded = await _urlEncoder(encoded);
-    return _encodedUri = encoded;
-  }
-
-  static void yieldControlToDDS(String uri) {
+  void yieldControlToDDS(String uri) {
+    // We track the URI of the connected DDS instance seperately instead of
+    // relying on _dds being non-null as there's no guarantee that DWDS is the
+    // tool starting DDS.
     if (_ddsUri != null) {
       // This exception is identical to the one thrown from
       // sdk/lib/vmservice/vmservice.dart
@@ -257,255 +156,119 @@
     _ddsUri = uri;
   }
 
-  static Future<ChromeDebugService> start(
-    String hostname,
-    RemoteDebugger remoteDebugger,
-    ExecutionContext executionContext,
-    AssetReader assetReader,
-    AppConnection appConnection,
-    UrlEncoder? urlEncoder, {
+  @protected
+  shelf.Handler initializeWebSocketHandler({
+    required ProxyService proxyService,
     void Function(Map<String, Object>)? onRequest,
     void Function(Map<String, Object?>)? onResponse,
-    required DartDevelopmentServiceConfiguration ddsConfig,
-    bool useSse = false,
-    ExpressionCompiler? expressionCompiler,
-  }) async {
-    final root = assetReader.basePath;
-    final chromeProxyService = await ChromeProxyService.create(
-      remoteDebugger,
-      root,
-      assetReader,
-      appConnection,
-      executionContext,
-      expressionCompiler,
-    );
-    final authToken = _makeAuthToken();
-    final serviceExtensionRegistry = ServiceExtensionRegistry();
-    Handler handler;
-    // DDS will always connect to DWDS via web sockets.
-    if (useSse && !ddsConfig.enable) {
-      final sseHandler = SseHandler(
-        Uri.parse('/$authToken/$_kSseHandlerPath'),
-        keepAlive: const Duration(seconds: 5),
-      );
-      handler = sseHandler.handler;
-      safeUnawaited(
-        _handleSseConnections(
-          sseHandler,
-          chromeProxyService,
+  }) {
+    return _wrapHandler(
+      webSocketHandler((webSocket, subprotocol) {
+        handleConnection(
+          webSocket,
+          proxyService,
           serviceExtensionRegistry,
           onRequest: onRequest,
           onResponse: onResponse,
-        ),
-      );
-    } else {
-      final innerHandler = webSocketHandler(
-        _createNewConnectionHandler(
-          chromeProxyService,
-          serviceExtensionRegistry,
-          onRequest: onRequest,
-          onResponse: onResponse,
-        ),
-      );
-      handler = (shelf.Request request) {
-        if (!_acceptNewConnections) {
-          return shelf.Response.forbidden(
-            'Cannot connect directly to the VM service as a Dart Development '
-            'Service (DDS) instance has taken control and can be found at '
-            '$_ddsUri.',
-          );
-        }
-        if (request.url.pathSegments.first != authToken) {
-          return shelf.Response.forbidden('Incorrect auth token');
-        }
-        return innerHandler(request);
-      };
+        );
+      }),
+      authToken: authToken,
+    );
+  }
+
+  shelf.Handler _wrapHandler(shelf.Handler innerHandler, {String? authToken}) {
+    return (shelf.Request request) {
+      if (!_acceptNewConnections) {
+        return shelf.Response.forbidden(
+          'Cannot connect directly to the VM service as a Dart Development '
+          'Service (DDS) instance has taken control and can be found at $_ddsUri.'
+          '$_ddsUri.',
+        );
+      }
+      if (authToken != null && request.url.pathSegments.first != authToken) {
+        return shelf.Response.forbidden('Incorrect auth token');
+      }
+      return innerHandler(request);
+    };
+  }
+
+  @protected
+  @mustCallSuper
+  void handleConnection(
+    StreamChannel channel,
+    ProxyService proxyService,
+    ServiceExtensionRegistry serviceExtensionRegistry, {
+    void Function(Map<String, Object>)? onRequest,
+    void Function(Map<String, Object?>)? onResponse,
+  }) {
+    final clientId = _clientId++;
+    final responseController = StreamController<Map<String, Object?>>();
+    responseController.stream
+        .asyncMap<String>((response) async {
+          // This error indicates a successful invocation to _yieldControlToDDS.
+          // We don't have a good way to access the list of connected clients
+          // while also being able to determine which client invoked the RPC
+          // without some form of client ID.
+          //
+          // We can probably do better than this, but it will likely involve some
+          // refactoring.
+          if (response case {
+            'error': {
+              'code': DisconnectNonDartDevelopmentServiceClients.kErrorCode,
+            },
+          }) {
+            final nonDdsClients = _clientConnections.entries
+                .where((MapEntry<int, StreamChannel> e) => e.key != clientId)
+                .map((e) => e.value);
+            await Future.wait([
+              for (final client in nonDdsClients) client.sink.close(),
+            ]);
+            // Remove the artificial error and return Success.
+            response.remove('error');
+            response['result'] = Success().toJson();
+          }
+          if (onResponse != null) onResponse(response);
+          return jsonEncode(response);
+        })
+        .listen(channel.sink.add, onError: channel.sink.addError);
+    final inputStream = channel.stream.map((value) {
+      if (value is List<int>) {
+        value = utf8.decode(value);
+      } else if (value is! String) {
+        throw StateError(
+          'Got value with unexpected type ${value.runtimeType} from web '
+          'socket, expected a List<int> or String.',
+        );
+      }
+      final request = Map<String, Object>.from(jsonDecode(value));
+      if (onRequest != null) onRequest(request);
+      return request;
+    });
+    VmServerConnection(
+      inputStream,
+      responseController.sink,
+      serviceExtensionRegistry,
+      proxyService,
+    ).done.whenComplete(() {
+      _clientConnections.remove(clientId);
+      if (!_acceptNewConnections && _clientConnections.isEmpty) {
+        // DDS has disconnected so we can allow for clients to connect directly
+        // to DWDS.
+        _ddsUri = null;
+        _acceptNewConnections = true;
+      }
+    });
+    _clientConnections[clientId] = channel;
+  }
+
+  // Creates a random auth token for more secure connections.
+  String _makeAuthToken() {
+    final tokenBytes = 8;
+    final bytes = Uint8List(tokenBytes);
+    final random = Random.secure();
+    for (var i = 0; i < tokenBytes; i++) {
+      bytes[i] = random.nextInt(256);
     }
-    final server = await startHttpServer(hostname, port: 44456);
-    serveHttpRequests(server, handler, (e, s) {
-      _logger.warning('Error serving requests', e);
-      emitEvent(DwdsEvent.httpRequestException('DebugService', '$e:$s'));
-    });
-    return ChromeDebugService._(
-      chromeProxyService,
-      server.address.host,
-      server.port,
-      authToken,
-      serviceExtensionRegistry,
-      server,
-      useSse,
-      ddsConfig,
-      urlEncoder,
-    );
+    return base64Url.encode(bytes);
   }
 }
-
-/// Defines callbacks for sending messages to the connected client.
-/// Returns the number of clients the request was successfully sent to.
-typedef SendClientRequest = int Function(Object request);
-
-/// WebSocket-based debug service for web debugging.
-class WebSocketDebugService implements DebugService {
-  @override
-  final String hostname;
-  @override
-  final int port;
-  final String authToken;
-  final HttpServer _server;
-  final WebSocketProxyService _webSocketProxyService;
-  final ServiceExtensionRegistry _serviceExtensionRegistry;
-  final UrlEncoder? _urlEncoder;
-
-  Future<void>? _closed;
-  DartDevelopmentServiceLauncher? _dds;
-  String? _encodedUri;
-
-  WebSocketDebugService._(
-    this.hostname,
-    this.port,
-    this.authToken,
-    this._webSocketProxyService,
-    this._serviceExtensionRegistry,
-    this._server,
-    this._urlEncoder,
-  );
-
-  /// Returns the WebSocketProxyService instance.
-  WebSocketProxyService get webSocketProxyService => _webSocketProxyService;
-
-  /// Returns the ServiceExtensionRegistry instance.
-  @override
-  ServiceExtensionRegistry get serviceExtensionRegistry =>
-      _serviceExtensionRegistry;
-
-  /// Closes the debug service and associated resources.
-  @override
-  Future<void> close() => _closed ??= Future.wait([
-    _server.close(),
-    if (_dds != null) _dds!.shutdown(),
-  ]);
-
-  /// Starts DDS (Dart Development Service).
-  Future<DartDevelopmentServiceLauncher> startDartDevelopmentService({
-    int? ddsPort,
-  }) async {
-    const timeout = Duration(seconds: 10);
-
-    try {
-      _dds = await DartDevelopmentServiceLauncher.start(
-        remoteVmServiceUri: Uri(
-          scheme: 'http',
-          host: hostname,
-          port: port,
-          path: authToken,
-        ),
-        serviceUri: Uri(scheme: 'http', host: hostname, port: ddsPort ?? 0),
-      ).timeout(timeout);
-    } catch (e) {
-      throw Exception('Failed to start DDS: $e');
-    }
-    return _dds!;
-  }
-
-  @override
-  String get uri =>
-      Uri(scheme: 'ws', host: hostname, port: port, path: authToken).toString();
-
-  @override
-  Future<String> get encodedUri async {
-    if (_encodedUri != null) return _encodedUri!;
-    var encoded = uri;
-    if (_urlEncoder != null) encoded = await _urlEncoder(encoded);
-    return _encodedUri = encoded;
-  }
-
-  static Future<WebSocketDebugService> start(
-    String hostname,
-    AppConnection appConnection,
-    AssetReader assetReader, {
-    required SendClientRequest sendClientRequest,
-    UrlEncoder? urlEncoder,
-  }) async {
-    final authToken = _makeAuthToken();
-    final serviceExtensionRegistry = ServiceExtensionRegistry();
-
-    final webSocketProxyService = await WebSocketProxyService.create(
-      sendClientRequest,
-      appConnection,
-      assetReader.basePath,
-    );
-
-    final handler = _createWebSocketHandler(
-      serviceExtensionRegistry,
-      webSocketProxyService,
-    );
-
-    final server = await startHttpServer(hostname, port: 44456);
-    serveHttpRequests(server, handler, (e, s) {
-      Logger('WebSocketDebugService').warning('Error serving requests', e);
-    });
-
-    return WebSocketDebugService._(
-      server.address.host,
-      server.port,
-      authToken,
-      webSocketProxyService,
-      serviceExtensionRegistry,
-      server,
-      urlEncoder,
-    );
-  }
-
-  /// Creates the WebSocket handler for incoming connections.
-  static Handler _createWebSocketHandler(
-    ServiceExtensionRegistry serviceExtensionRegistry,
-    WebSocketProxyService webSocketProxyService,
-  ) {
-    return webSocketHandler((
-      WebSocketChannel webSocket,
-      String? subprotocol,
-    ) async {
-      final clientId = _clientId++;
-      final responseController = StreamController<Map<String, Object?>>();
-      unawaited(
-        webSocket.sink.addStream(responseController.stream.map(jsonEncode)),
-      );
-
-      final inputStream = webSocket.stream.map((value) {
-        if (value is List<int>) {
-          value = utf8.decode(value);
-        } else if (value is! String) {
-          throw StateError(
-            'Unexpected value type from web socket: ${value.runtimeType}',
-          );
-        }
-        return Map<String, Object>.from(jsonDecode(value));
-      });
-
-      _clientConnections[clientId] = webSocket;
-
-      unawaited(
-        VmServerConnection(
-          inputStream,
-          responseController.sink,
-          serviceExtensionRegistry,
-          webSocketProxyService,
-        ).done.whenComplete(() {
-          _clientConnections.remove(clientId);
-        }),
-      );
-      await webSocketProxyService.sendServiceExtensionRegisteredEvents();
-    });
-  }
-}
-
-// Creates a random auth token for more secure connections.
-String _makeAuthToken() {
-  final tokenBytes = 8;
-  final bytes = Uint8List(tokenBytes);
-  final random = Random.secure();
-  for (var i = 0; i < tokenBytes; i++) {
-    bytes[i] = random.nextInt(256);
-  }
-  return base64Url.encode(bytes);
-}
diff --git a/dwds/lib/src/services/proxy_service.dart b/dwds/lib/src/services/proxy_service.dart
index 6140cf0..5e551a3 100644
--- a/dwds/lib/src/services/proxy_service.dart
+++ b/dwds/lib/src/services/proxy_service.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 
 import 'package:dwds/data/debug_event.dart';
 import 'package:dwds/data/hot_reload_response.dart';
@@ -12,16 +13,49 @@
 import 'package:dwds/src/connections/app_connection.dart';
 import 'package:dwds/src/debugging/inspector.dart';
 import 'package:dwds/src/events.dart';
+import 'package:dwds/src/services/debug_service.dart';
+import 'package:dwds/src/utilities/dart_uri.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:meta/meta.dart';
 import 'package:pub_semver/pub_semver.dart' as semver;
 import 'package:vm_service/vm_service.dart' as vm_service;
+import 'package:vm_service/vm_service.dart';
 import 'package:vm_service_interface/vm_service_interface.dart';
 
-const pauseIsolatesOnStartFlag = 'pause_isolates_on_start';
+// This event is identical to the one sent by the VM service from
+// sdk/lib/vmservice/vmservice.dart before existing VM service clients are
+// disconnected.
+final class DartDevelopmentServiceConnectedEvent extends Event {
+  DartDevelopmentServiceConnectedEvent({
+    required super.timestamp,
+    required this.uri,
+  }) : message =
+           'A Dart Developer Service instance has connected and this direct '
+           'connection to the VM service will now be closed. Please reconnect to '
+           'the Dart Development Service at $uri.',
+       super(kind: 'DartDevelopmentServiceConnected');
+
+  final String message;
+  final String uri;
+
+  @override
+  Map<String, Object?> toJson() => {
+    ...super.toJson(),
+    'uri': uri,
+    'message': message,
+  };
+}
+
+final class DisconnectNonDartDevelopmentServiceClients extends RPCError {
+  DisconnectNonDartDevelopmentServiceClients()
+    : super('_yieldControlToDDS', kErrorCode);
+
+  // Arbitrary error code that's unlikely to be used elsewhere.
+  static const kErrorCode = -199328;
+}
 
 /// Abstract base class for VM service proxy implementations.
-abstract class ProxyService<InspectorT extends AppInspector>
+abstract base class ProxyService<InspectorT extends AppInspector>
     implements VmServiceInterface {
   /// Cache of all existing StreamControllers.
   ///
@@ -50,24 +84,29 @@
   /// a hot restart.
   bool get isIsolateRunning => _inspector != null;
 
+  /// The [DebugService] implementation.
+  final DebugService debugService;
+
   /// The root `VM` instance.
-  final vm_service.VM _vm;
+  final vm_service.VM vm;
 
   /// Signals when isolate is initialized.
   Future<void> get isInitialized => initializedCompleter.future;
   Completer<void> initializedCompleter = Completer<void>();
 
+  static const _kPauseIsolatesOnStartFlag = 'pause_isolates_on_start';
+
   /// The flags that can be set at runtime via [setFlag] and their respective
   /// values.
   final Map<String, bool> _currentVmServiceFlags = {
-    pauseIsolatesOnStartFlag: false,
+    _kPauseIsolatesOnStartFlag: false,
   };
 
-  /// The value of the [pauseIsolatesOnStartFlag].
+  /// The value of the [_kPauseIsolatesOnStartFlag].
   ///
   /// This value can be updated at runtime via [setFlag].
   bool get pauseIsolatesOnStart =>
-      _currentVmServiceFlags[pauseIsolatesOnStartFlag] ?? false;
+      _currentVmServiceFlags[_kPauseIsolatesOnStartFlag] ?? false;
 
   /// Stream controller for resume events after restart.
   final _resumeAfterRestartEventsController =
@@ -87,7 +126,6 @@
   bool get hasPendingRestart => _resumeAfterRestartEventsController.hasListener;
 
   // Protected accessors for subclasses
-  vm_service.VM get vm => _vm;
   Map<String, StreamController<vm_service.Event>> get streamControllers =>
       _streamControllers;
   StreamController<String> get resumeAfterRestartEventsController =>
@@ -97,7 +135,11 @@
   /// The root at which we're serving.
   final String root;
 
-  ProxyService(this._vm, this.root);
+  ProxyService({
+    required this.vm,
+    required this.root,
+    required this.debugService,
+  });
 
   /// Sends events to stream controllers.
   void streamNotify(String streamId, vm_service.Event event) {
@@ -115,10 +157,31 @@
   }
 
   @override
+  Future<void> yieldControlToDDS(String uri) async {
+    // This will throw an RPCError if there's already an existing DDS instance.
+    debugService.yieldControlToDDS(uri);
+
+    // Notify existing clients that DDS has connected and they're about to be
+    // disconnected.
+    final event = DartDevelopmentServiceConnectedEvent(
+      timestamp: DateTime.now().millisecondsSinceEpoch,
+      uri: uri,
+    );
+    streamNotify(EventStreams.kService, event);
+
+    // We throw since we have no other way to control what the response content
+    // is for this RPC. The debug service will check for this particular
+    // exception as a signal to close connections to all other clients.
+    throw DisconnectNonDartDevelopmentServiceClients();
+  }
+
+  @override
   Future<vm_service.Success> streamListen(String streamId) =>
       wrapInErrorHandlerAsync('streamListen', () => _streamListen(streamId));
 
   Future<vm_service.Success> _streamListen(String streamId) async {
+    // TODO: This should return an error if the stream is already being listened
+    // to.
     onEvent(streamId);
     return vm_service.Success();
   }
@@ -135,11 +198,53 @@
 
   Future<vm_service.VM> _getVM() {
     return captureElapsedTime(() async {
-      return _vm;
+      return vm;
     }, (result) => DwdsEvent.getVM());
   }
 
   @override
+  Future<Isolate> getIsolate(String isolateId) =>
+      wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId));
+
+  Future<Isolate> _getIsolate(String isolateId) {
+    return captureElapsedTime(() async {
+      await isInitialized;
+      checkIsolate('getIsolate', isolateId);
+      return inspector.isolate;
+    }, (result) => DwdsEvent.getIsolate());
+  }
+
+  @override
+  Future<Success> setName(String isolateId, String name) =>
+      wrapInErrorHandlerAsync('setName', () => _setName(isolateId, name));
+
+  Future<Success> _setName(String isolateId, String name) async {
+    await isInitialized;
+    checkIsolate('setName', isolateId);
+    inspector.isolate.name = name;
+    return Success();
+  }
+
+  @override
+  Future<Success> setVMName(String name) =>
+      wrapInErrorHandlerAsync('setVMName', () => _setVMName(name));
+
+  Future<Success> _setVMName(String name) async {
+    vm.name = name;
+    streamNotify(
+      'VM',
+      Event(
+        kind: EventKind.kVMUpdate,
+        timestamp: DateTime.now().millisecondsSinceEpoch,
+        // We are not guaranteed to have an isolate at this point in time.
+        isolate: null,
+        vm: toVMRef(vm),
+      ),
+    );
+    return Success();
+  }
+
+  @override
   Future<vm_service.FlagList> getFlagList() =>
       wrapInErrorHandlerAsync('getFlagList', _getFlagList);
 
@@ -195,6 +300,55 @@
     return vm_service.Version(major: version.major, minor: version.minor);
   }
 
+  // Note: Ignore the optional local parameter, when it is set to `true` the
+  // request is intercepted and handled by DDS.
+  @override
+  Future<UriList> lookupResolvedPackageUris(
+    String isolateId,
+    List<String> uris, {
+    bool? local,
+  }) => wrapInErrorHandlerAsync(
+    'lookupResolvedPackageUris',
+    () => _lookupResolvedPackageUris(isolateId, uris),
+  );
+
+  Future<UriList> _lookupResolvedPackageUris(
+    String isolateId,
+    List<String> uris,
+  ) async {
+    await isInitialized;
+    checkIsolate('lookupResolvedPackageUris', isolateId);
+    return UriList(uris: uris.map(DartUri.toResolvedUri).toList());
+  }
+
+  @override
+  Future<UriList> lookupPackageUris(String isolateId, List<String> uris) =>
+      wrapInErrorHandlerAsync(
+        'lookupPackageUris',
+        () => _lookupPackageUris(isolateId, uris),
+      );
+
+  Future<UriList> _lookupPackageUris(
+    String isolateId,
+    List<String> uris,
+  ) async {
+    await isInitialized;
+    checkIsolate('lookupPackageUris', isolateId);
+    return UriList(uris: uris.map(DartUri.toPackageUri).toList());
+  }
+
+  @override
+  Future<ScriptList> getScripts(String isolateId) =>
+      wrapInErrorHandlerAsync('getScripts', () => _getScripts(isolateId));
+
+  Future<ScriptList> _getScripts(String isolateId) {
+    return captureElapsedTime(() async {
+      await isInitialized;
+      checkIsolate('getScripts', isolateId);
+      return inspector.getScripts();
+    }, (result) => DwdsEvent.getScripts());
+  }
+
   /// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service
   /// protocol [Event]s.
   void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) {
@@ -205,11 +359,45 @@
 
   /// Parses the [DebugEvent] and emits a corresponding Dart VM Service
   /// protocol [Event].
-  void parseDebugEvent(DebugEvent debugEvent);
+  @mustCallSuper
+  void parseDebugEvent(DebugEvent debugEvent) {
+    if (!isIsolateRunning) return;
+    final isolateRef = inspector.isolateRef;
+
+    streamNotify(
+      EventStreams.kExtension,
+      Event(
+          kind: EventKind.kExtension,
+          timestamp: DateTime.now().millisecondsSinceEpoch,
+          isolate: isolateRef,
+        )
+        ..extensionKind = debugEvent.kind
+        ..extensionData = ExtensionData.parse(
+          jsonDecode(debugEvent.eventData) as Map<String, dynamic>,
+        ),
+    );
+  }
 
   /// Parses the [RegisterEvent] and emits a corresponding Dart VM Service
   /// protocol [Event].
-  void parseRegisterEvent(RegisterEvent registerEvent);
+  @mustCallSuper
+  void parseRegisterEvent(RegisterEvent registerEvent) {
+    if (!isIsolateRunning) return;
+
+    final isolate = inspector.isolate;
+    final isolateRef = inspector.isolateRef;
+    final service = registerEvent.eventData;
+    isolate.extensionRPCs?.add(service);
+
+    streamNotify(
+      EventStreams.kIsolate,
+      Event(
+        kind: EventKind.kServiceExtensionAdded,
+        timestamp: DateTime.now().millisecondsSinceEpoch,
+        isolate: isolateRef,
+      )..extensionRPC = service,
+    );
+  }
 
   /// Completes hot reload with response from client.
   ///
@@ -372,6 +560,15 @@
       _rpcNotSupportedFuture('getProcessMemoryUsage');
 
   @override
+  Future<Success> setLibraryDebuggable(
+    String isolateId,
+    String libraryId,
+    bool isDebuggable,
+  ) {
+    return rpcNotSupportedFuture('setLibraryDebuggable');
+  }
+
+  @override
   Future<vm_service.PortList> getPorts(String isolateId) =>
       throw UnimplementedError();
 
@@ -416,6 +613,11 @@
     return _rpcNotSupportedFuture('clearCpuSamples');
   }
 
+  @override
+  Future<Success> registerService(String service, String alias) {
+    return rpcNotSupportedFuture('registerService');
+  }
+
   /// Creates a new isolate for debugging.
   ///
   /// Implementations should handle isolate lifecycle management according to
@@ -431,6 +633,32 @@
   /// debugging mode and connection management strategy.
   void destroyIsolate();
 
+  /// Validate that isolateId matches the current isolate we're connected to and
+  /// return that isolate.
+  ///
+  /// This is useful to call at the beginning of API methods that are passed an
+  /// isolate id.
+  @protected
+  Isolate checkIsolate(String methodName, String? isolateId) {
+    final currentIsolateId = inspector.isolate.id;
+    if (currentIsolateId == null) {
+      throw StateError('No running isolate ID');
+    }
+    if (isolateId != currentIsolateId) {
+      _throwSentinel(
+        methodName,
+        SentinelKind.kCollected,
+        'Unrecognized isolateId: $isolateId',
+      );
+    }
+    return inspector.isolate;
+  }
+
+  static Never _throwSentinel(String method, String kind, String message) {
+    final data = <String, String>{'kind': kind, 'valueAsString': message};
+    throw SentinelException.parse(method, data);
+  }
+
   /// Prevent DWDS from blocking Dart SDK rolls if changes in package:vm_service
   /// are unimplemented in DWDS.
   @override
diff --git a/dwds/lib/src/services/web_socket/web_socket_debug_service.dart b/dwds/lib/src/services/web_socket/web_socket_debug_service.dart
new file mode 100644
index 0000000..36ab4da
--- /dev/null
+++ b/dwds/lib/src/services/web_socket/web_socket_debug_service.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:dwds/asset_reader.dart';
+import 'package:dwds/src/config/tool_configuration.dart';
+import 'package:dwds/src/connections/app_connection.dart';
+import 'package:dwds/src/services/debug_service.dart';
+import 'package:dwds/src/services/web_socket/web_socket_proxy_service.dart';
+import 'package:meta/meta.dart';
+
+/// Defines callbacks for sending messages to the connected client.
+/// Returns the number of clients the request was successfully sent to.
+typedef SendClientRequest = int Function(Object request);
+
+/// WebSocket-based debug service for web debugging.
+final class WebSocketDebugService extends DebugService<WebSocketProxyService> {
+  WebSocketDebugService._({
+    required super.serverHostname,
+    required super.ddsConfig,
+    required super.urlEncoder,
+  }) : super(
+         // The web socket debug service doesn't support SSE connections.
+         useSse: false,
+       );
+
+  @protected
+  @override
+  Future<void> initialize({required WebSocketProxyService proxyService}) async {
+    await super.initialize(proxyService: proxyService);
+    await serve(
+      handler: initializeWebSocketHandler(proxyService: proxyService),
+    );
+  }
+
+  static Future<WebSocketDebugService> start({
+    required String hostname,
+    required AppConnection appConnection,
+    required AssetReader assetReader,
+    required SendClientRequest sendClientRequest,
+    required DartDevelopmentServiceConfiguration ddsConfig,
+    UrlEncoder? urlEncoder,
+  }) async {
+    final debugService = WebSocketDebugService._(
+      serverHostname: hostname,
+      ddsConfig: ddsConfig,
+      urlEncoder: urlEncoder,
+    );
+    final webSocketProxyService = await WebSocketProxyService.create(
+      sendClientRequest,
+      appConnection,
+      assetReader.basePath,
+      debugService,
+    );
+    await debugService.initialize(proxyService: webSocketProxyService);
+    return debugService;
+  }
+}
diff --git a/dwds/lib/src/services/web_socket_proxy_service.dart b/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart
similarity index 81%
rename from dwds/lib/src/services/web_socket_proxy_service.dart
rename to dwds/lib/src/services/web_socket/web_socket_proxy_service.dart
index 49faa71..53b01c4 100644
--- a/dwds/lib/src/services/web_socket_proxy_service.dart
+++ b/dwds/lib/src/services/web_socket/web_socket_proxy_service.dart
@@ -5,19 +5,16 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:dwds/data/debug_event.dart';
 import 'package:dwds/data/hot_reload_request.dart';
 import 'package:dwds/data/hot_reload_response.dart';
 import 'package:dwds/data/hot_restart_request.dart';
 import 'package:dwds/data/hot_restart_response.dart';
-import 'package:dwds/data/register_event.dart';
 import 'package:dwds/data/service_extension_request.dart';
 import 'package:dwds/data/service_extension_response.dart';
 import 'package:dwds/src/connections/app_connection.dart';
 import 'package:dwds/src/debugging/web_socket_inspector.dart';
-import 'package:dwds/src/events.dart';
 import 'package:dwds/src/services/proxy_service.dart';
-import 'package:dwds/src/utilities/dart_uri.dart';
+import 'package:dwds/src/services/web_socket/web_socket_debug_service.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:logging/logging.dart';
 import 'package:vm_service/vm_service.dart' as vm_service;
@@ -132,12 +129,11 @@
 }
 
 /// WebSocket-based VM service proxy for web debugging.
-class WebSocketProxyService extends ProxyService<WebSocketAppInspector> {
+final class WebSocketProxyService extends ProxyService<WebSocketAppInspector> {
   final _logger = Logger('WebSocketProxyService');
 
   /// Active service extension trackers by request ID.
-  final Map<String, _ServiceExtensionTracker> _pendingServiceExtensionTrackers =
-      {};
+  final _pendingServiceExtensionTrackers = <String, _ServiceExtensionTracker>{};
 
   /// Sends messages to the client.
   final SendClientRequest sendClientRequest;
@@ -146,24 +142,55 @@
   AppConnection appConnection;
 
   /// Active hot reload trackers by request ID.
-  final Map<String, _HotReloadTracker> _pendingHotReloads = {};
+  final _pendingHotReloads = <String, _HotReloadTracker>{};
 
   /// Active hot restart trackers by request ID.
-  final Map<String, _HotRestartTracker> _pendingHotRestarts = {};
+  final _pendingHotRestarts = <String, _HotRestartTracker>{};
 
   /// App connection cleanup subscriptions by connection instance ID.
-  final Map<String, StreamSubscription<void>> _appConnectionDoneSubscriptions =
-      {};
+  final _appConnectionDoneSubscriptions = <String, StreamSubscription<void>>{};
 
   /// Active connection count for this service.
   int _activeConnectionCount = 0;
 
-  WebSocketProxyService._(
-    this.sendClientRequest,
-    vm_service.VM vm,
+  WebSocketProxyService._({
+    required this.sendClientRequest,
+    required super.vm,
+    required super.root,
+    required super.debugService,
+    required this.appConnection,
+  });
+
+  static Future<WebSocketProxyService> create(
+    SendClientRequest sendClientRequest,
+    AppConnection appConnection,
     String root,
-    this.appConnection,
-  ) : super(vm, root);
+    WebSocketDebugService debugService,
+  ) async {
+    final vm = vm_service.VM(
+      name: 'WebSocketDebugProxy',
+      operatingSystem: 'web',
+      startTime: DateTime.now().millisecondsSinceEpoch,
+      version: 'unknown',
+      isolates: [],
+      isolateGroups: [],
+      systemIsolates: [],
+      systemIsolateGroups: [],
+      targetCPU: 'Web',
+      hostCPU: 'DWDS',
+      architectureBits: -1,
+      pid: -1,
+    );
+    final service = WebSocketProxyService._(
+      sendClientRequest: sendClientRequest,
+      vm: vm,
+      root: root,
+      debugService: debugService,
+      appConnection: appConnection,
+    );
+    safeUnawaited(service.createIsolate(appConnection));
+    return service;
+  }
 
   // Isolate state
   vm_service.Event? _currentPauseEvent;
@@ -225,7 +252,7 @@
     );
 
     // Send lifecycle events
-    _streamNotify(
+    streamNotify(
       vm_service.EventStreams.kIsolate,
       vm_service.Event(
         kind: vm_service.EventKind.kIsolateStart,
@@ -233,7 +260,7 @@
         isolate: isolateRef,
       ),
     );
-    _streamNotify(
+    streamNotify(
       vm_service.EventStreams.kIsolate,
       vm_service.Event(
         kind: vm_service.EventKind.kIsolateRunnable,
@@ -250,7 +277,7 @@
         isolate: isolateRef,
       );
       _currentPauseEvent = pauseEvent;
-      _streamNotify(vm_service.EventStreams.kDebug, pauseEvent);
+      streamNotify(vm_service.EventStreams.kDebug, pauseEvent);
     }
 
     // Complete initialization after isolate is set up
@@ -343,7 +370,7 @@
 
     final isolateRef = inspector.isolateRef;
     // Send exit event
-    _streamNotify(
+    streamNotify(
       vm_service.EventStreams.kIsolate,
       vm_service.Event(
         kind: vm_service.EventKind.kIsolateExit,
@@ -364,22 +391,6 @@
     }
   }
 
-  /// Sends events to stream controllers.
-  void _streamNotify(String streamId, vm_service.Event event) {
-    final controller = streamControllers[streamId];
-    if (controller == null) return;
-    controller.add(event);
-  }
-
-  @override
-  Future<Success> setLibraryDebuggable(
-    String isolateId,
-    String libraryId,
-    bool isDebuggable,
-  ) {
-    return rpcNotSupportedFuture('setLibraryDebuggable');
-  }
-
   @override
   Future<Success> setIsolatePauseMode(
     String isolateId, {
@@ -391,104 +402,6 @@
   }
 
   @override
-  Future<vm_service.Isolate> getIsolate(String isolateId) =>
-      wrapInErrorHandlerAsync('getIsolate', () => _getIsolate(isolateId));
-
-  Future<vm_service.Isolate> _getIsolate(String isolateId) async {
-    final isolate = inspector.isolate;
-    if (!isIsolateRunning) {
-      throw vm_service.RPCError(
-        'getIsolate',
-        vm_service.RPCErrorKind.kInvalidParams.code,
-        'No running isolate found for id: $isolateId',
-      );
-    }
-    if (isolate.id != isolateId) {
-      throw vm_service.RPCError(
-        'getIsolate',
-        vm_service.RPCErrorKind.kInvalidParams.code,
-        'Isolate with id $isolateId not found.',
-      );
-    }
-
-    return isolate;
-  }
-
-  @override
-  Future<ScriptList> getScripts(String isolateId) => inspector.getScripts();
-
-  /// Adds events to stream controllers.
-  void addEvent(String streamId, vm_service.Event event) {
-    final controller = streamControllers[streamId];
-    if (controller != null && !controller.isClosed) {
-      controller.add(event);
-    } else {
-      _logger.warning('Cannot add event to closed/missing stream: $streamId');
-    }
-  }
-
-  static Future<WebSocketProxyService> create(
-    SendClientRequest sendClientRequest,
-    AppConnection appConnection,
-    String root,
-  ) async {
-    final vm = vm_service.VM(
-      name: 'WebSocketDebugProxy',
-      operatingSystem: 'web',
-      startTime: DateTime.now().millisecondsSinceEpoch,
-      version: 'unknown',
-      isolates: [],
-      isolateGroups: [],
-      systemIsolates: [],
-      systemIsolateGroups: [],
-      targetCPU: 'Web',
-      hostCPU: 'DWDS',
-      architectureBits: -1,
-      pid: -1,
-    );
-    final service = WebSocketProxyService._(
-      sendClientRequest,
-      vm,
-      root,
-      appConnection,
-    );
-    safeUnawaited(service.createIsolate(appConnection));
-    return service;
-  }
-
-  /// Returns the root VM object.
-  @override
-  Future<vm_service.VM> getVM() => wrapInErrorHandlerAsync('getVM', _getVM);
-
-  Future<vm_service.VM> _getVM() {
-    return captureElapsedTime(() async {
-      // Ensure the VM's isolate list is synchronized with our actual state
-      if (isIsolateRunning) {
-        final isolateRef = inspector.isolateRef;
-        // Make sure our isolate is in the VM's isolate list
-        final isolateExists =
-            vm.isolates?.any((ref) => ref.id == isolateRef.id) ?? false;
-        if (!isolateExists) {
-          vm.isolates?.add(isolateRef);
-        }
-      } else {
-        // If no isolate is running, make sure the list is empty
-        vm.isolates?.clear();
-      }
-
-      return vm;
-    }, (result) => DwdsEvent.getVM());
-  }
-
-  /// Not available in WebSocket mode.
-  dynamic get remoteDebugger {
-    throw UnsupportedError(
-      'remoteDebugger not available in WebSocketProxyService.\n'
-      'Called from:\n${StackTrace.current}',
-    );
-  }
-
-  @override
   Future<vm_service.ReloadReport> reloadSources(
     String isolateId, {
     bool? force,
@@ -516,7 +429,7 @@
   }
 
   /// Handles hot restart requests.
-  Future<Map<String, dynamic>> hotRestart() async {
+  Future<Map<String, Object?>> hotRestart() async {
     _logger.info('Attempting a hot restart');
 
     try {
@@ -853,60 +766,6 @@
     }
   }
 
-  /// Parses the [RegisterEvent] and emits a corresponding Dart VM Service
-  /// protocol [Event].
-  @override
-  void parseRegisterEvent(RegisterEvent registerEvent) {
-    if (!isIsolateRunning) {
-      _logger.warning('Cannot register service extension - no isolate running');
-      return;
-    }
-
-    final service = registerEvent.eventData;
-
-    // Emit ServiceExtensionAdded event for tooling
-    final event = vm_service.Event(
-      kind: vm_service.EventKind.kServiceExtensionAdded,
-      timestamp: DateTime.now().millisecondsSinceEpoch,
-      isolate: inspector.isolateRef,
-    );
-    event.extensionRPC = service;
-
-    _streamNotify(vm_service.EventStreams.kIsolate, event);
-  }
-
-  /// Parses the [BatchedDebugEvents] and emits corresponding Dart VM Service
-  /// protocol [Event]s.
-  @override
-  void parseBatchedDebugEvents(BatchedDebugEvents debugEvents) {
-    for (final debugEvent in debugEvents.events) {
-      parseDebugEvent(debugEvent);
-    }
-  }
-
-  /// Parses the [DebugEvent] and emits a corresponding Dart VM Service
-  /// protocol [Event].
-  @override
-  void parseDebugEvent(DebugEvent debugEvent) {
-    if (!isIsolateRunning) {
-      _logger.warning('Cannot parse debug event - no isolate running');
-      return;
-    }
-
-    _streamNotify(
-      vm_service.EventStreams.kExtension,
-      vm_service.Event(
-          kind: vm_service.EventKind.kExtension,
-          timestamp: DateTime.now().millisecondsSinceEpoch,
-          isolate: inspector.isolateRef,
-        )
-        ..extensionKind = debugEvent.kind
-        ..extensionData = vm_service.ExtensionData.parse(
-          jsonDecode(debugEvent.eventData) as Map<String, dynamic>,
-        ),
-    );
-  }
-
   @override
   Future<Success> setFlag(String name, String value) =>
       wrapInErrorHandlerAsync('setFlag', () => _setFlag(name, value));
@@ -932,31 +791,13 @@
           isolate: inspector.isolateRef,
         );
         _currentPauseEvent = pauseEvent;
-        _streamNotify(vm_service.EventStreams.kDebug, pauseEvent);
+        streamNotify(vm_service.EventStreams.kDebug, pauseEvent);
       }
     }
 
     return Success();
   }
 
-  @override
-  Future<UriList> lookupResolvedPackageUris(
-    String isolateId,
-    List<String> uris, {
-    bool? local,
-  }) => wrapInErrorHandlerAsync(
-    'lookupResolvedPackageUris',
-    () => _lookupResolvedPackageUris(isolateId, uris),
-  );
-
-  Future<UriList> _lookupResolvedPackageUris(
-    String _,
-    List<String> uris,
-  ) async {
-    await isInitialized;
-    return UriList(uris: uris.map(DartUri.toResolvedUri).toList());
-  }
-
   /// Pauses execution of the isolate.
   @override
   Future<Success> pause(String isolateId) =>
@@ -994,30 +835,13 @@
         timestamp: DateTime.now().millisecondsSinceEpoch,
         isolate: inspector.isolateRef,
       );
-      _streamNotify(vm_service.EventStreams.kDebug, resumeEvent);
+      streamNotify(vm_service.EventStreams.kDebug, resumeEvent);
     }
 
     return Success();
   }
 
   @override
-  Future<UriList> lookupPackageUris(String isolateId, List<String> uris) =>
-      wrapInErrorHandlerAsync(
-        'lookupPackageUris',
-        () => _lookupPackageUris(isolateId, uris),
-      );
-
-  Future<UriList> _lookupPackageUris(String _, List<String> uris) async {
-    await isInitialized;
-    return UriList(uris: uris.map(DartUri.toPackageUri).toList());
-  }
-
-  @override
-  Future<Success> registerService(String service, String alias) {
-    return rpcNotSupportedFuture('registerService');
-  }
-
-  @override
   Future<FlagList> getFlagList() =>
       wrapInErrorHandlerAsync('getFlagList', _getFlagList);
 
diff --git a/dwds/lib/src/utilities/server.dart b/dwds/lib/src/utilities/server.dart
index 81a0763..e67dfe3 100644
--- a/dwds/lib/src/utilities/server.dart
+++ b/dwds/lib/src/utilities/server.dart
@@ -4,7 +4,7 @@
 
 import 'dart:io';
 
-import 'package:dwds/src/services/chrome_debug_exception.dart';
+import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
 import 'package:http_multi_server/http_multi_server.dart';
 import 'package:shelf/shelf.dart';
 import 'package:shelf/shelf_io.dart';
diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart
index ef59e4b..78eae7b 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.2.0';
+const packageVersion = '26.2.1-wip';
diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml
index 05edd4e..738e970 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.2.0
+version: 26.2.1-wip
 
 description: >-
   A service that proxies between the Chrome debug protocol and the Dart VM
diff --git a/dwds/test/common/chrome_proxy_service_common.dart b/dwds/test/common/chrome_proxy_service_common.dart
index 4affe62..0c403d4 100644
--- a/dwds/test/common/chrome_proxy_service_common.dart
+++ b/dwds/test/common/chrome_proxy_service_common.dart
@@ -12,7 +12,7 @@
 import 'dart:io';
 
 import 'package:dwds/expression_compiler.dart';
-import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:dwds/src/services/chrome/chrome_proxy_service.dart';
 import 'package:dwds/src/utilities/dart_uri.dart';
 import 'package:dwds/src/utilities/shared.dart';
 import 'package:http/http.dart' as http;
diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart
index 22e5326..5b7f4da 100644
--- a/dwds/test/fixtures/context.dart
+++ b/dwds/test/fixtures/context.dart
@@ -18,7 +18,7 @@
 import 'package:dwds/src/loaders/frontend_server_strategy_provider.dart';
 import 'package:dwds/src/loaders/strategy.dart';
 import 'package:dwds/src/readers/proxy_server_asset_reader.dart';
-import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:dwds/src/services/chrome/chrome_proxy_service.dart';
 import 'package:dwds/src/services/expression_compiler.dart';
 import 'package:dwds/src/services/expression_compiler_service.dart';
 import 'package:dwds/src/utilities/dart_uri.dart';
diff --git a/dwds/test/variable_scope_test.dart b/dwds/test/variable_scope_test.dart
index 7f56319..14bd0cf 100644
--- a/dwds/test/variable_scope_test.dart
+++ b/dwds/test/variable_scope_test.dart
@@ -7,7 +7,7 @@
 library;
 
 import 'package:dwds/src/debugging/dart_scope.dart';
-import 'package:dwds/src/services/chrome_proxy_service.dart';
+import 'package:dwds/src/services/chrome/chrome_proxy_service.dart';
 import 'package:test/test.dart';
 import 'package:test_common/logging.dart';
 import 'package:test_common/test_sdk_configuration.dart';