From 8fd89a7b0eb62ad2653359cec27326ec9d674f5c Mon Sep 17 00:00:00 2001 From: Ivan Inozemtsev Date: Wed, 17 Dec 2025 17:02:26 +0100 Subject: [PATCH] Support reconnects on page refreshes for SSE client connections If we treat reloads as parts of an app lifecycle, it seems natural to preserve an app instance id on full reloads, and looks like a session storage is a good fit. Fixes #2726. --- dwds/CHANGELOG.md | 2 + dwds/lib/src/handlers/dev_handler.dart | 56 +++++++++++++------------- dwds/lib/src/injected/client.js | 16 ++++++-- dwds/web/client.dart | 17 +++++++- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index f2e116911..226aa5a9a 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -5,6 +5,8 @@ - Fix pausing starting of `main` after the hot restart. - Updating bootstrapper for DDC library bundler module format + Frontend Server. - Fix setting up breakpoints when handling in-app restarts with attached debugger. +- Fix setting up breakpoints when handling full reloads from attached + debugger / page refreshes. ## 26.2.2 diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart index 2a415b0f3..b92a646fb 100644 --- a/dwds/lib/src/handlers/dev_handler.dart +++ b/dwds/lib/src/handlers/dev_handler.dart @@ -636,21 +636,11 @@ class DevHandler { readyToRunMainCompleter.future, ); - // We can take over a connection if there is no connectedInstanceId (this - // means the client completely disconnected), or if the existing - // AppConnection is in the KeepAlive state (this means it disconnected but - // is still waiting for a possible reconnect - this happens during a page - // reload). - final canReconnect = - services != null && - (services.connectedInstanceId == null || - existingConnection?.isInKeepAlivePeriod == true); - - if (canReconnect) { + if (_canReconnect(existingConnection, message)) { // Disconnect any old connection (eg. those in the keep-alive waiting // state when reloading the page). existingConnection?.shutDown(); - final chromeProxy = services.proxyService; + final chromeProxy = services!.proxyService; if (chromeProxy is ChromeProxyService) { chromeProxy.destroyIsolate(); } @@ -703,24 +693,10 @@ class DevHandler { readyToRunMainCompleter.future, ); - // Allow connection reuse for page refreshes and same instance reconnections - final isSameInstance = - existingConnection?.request.instanceId == message.instanceId; - final isKeepAliveReconnect = - existingConnection?.isInKeepAlivePeriod == true; - final hasNoActiveConnection = services?.connectedInstanceId == null; - final noExistingConnection = existingConnection == null; - - final canReconnect = - services != null && - (isSameInstance || - (isKeepAliveReconnect && hasNoActiveConnection) || - (noExistingConnection && hasNoActiveConnection)); - - if (canReconnect) { + if (_canReconnect(existingConnection, message)) { // Reconnect to existing service. await _reconnectToService( - services, + services!, existingConnection, connection, message, @@ -749,6 +725,30 @@ class DevHandler { return connection; } + /// Allow connection reuse for page refreshes and same instance reconnections + bool _canReconnect( + AppConnection? existingConnection, + ConnectRequest message, + ) { + final services = _servicesByAppId[message.appId]; + + if (services == null) { + return false; + } + + if (existingConnection?.request.instanceId == message.instanceId) { + return true; + } + + final isKeepAliveReconnect = + existingConnection?.isInKeepAlivePeriod == true; + final hasNoActiveConnection = services.connectedInstanceId == null; + final noExistingConnection = existingConnection == null; + + return (isKeepAliveReconnect && hasNoActiveConnection) || + (noExistingConnection && hasNoActiveConnection); + } + /// Handles reconnection to existing services for web-socket mode. Future _reconnectToService( AppDebugServices services, diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js index 57b878ba0..6db572f68 100644 --- a/dwds/lib/src/injected/client.js +++ b/dwds/lib/src/injected/client.js @@ -27899,7 +27899,7 @@ $call$body$main_closure() { var $async$goto = 0, $async$completer = A._makeAsyncAwaitCompleter(type$.void), - uri, fixedPath, fixedUri, client, _0_0, t2, manager, t3, t4, t5, debugEventController, t6, _box_0, t1; + storedInstanceId, t2, t3, uri, fixedPath, fixedUri, client, _0_0, manager, t4, t5, debugEventController, t6, _box_0, t1; var $async$call$0 = A._wrapJsFunctionForAsync(function($async$errorCode, $async$result) { if ($async$errorCode === 1) return A._asyncRethrow($async$result, $async$completer); @@ -27909,8 +27909,18 @@ // Function start _box_0 = {}; t1 = init.G; - if (A._asStringQ(t1.$dartAppInstanceId) == null) - t1.$dartAppInstanceId = new A.UuidV1(null).generate$1$options(null); + if (A._asStringQ(t1.$dartAppInstanceId) == null) { + storedInstanceId = A._asStringQ(A._asJSObject(A._asJSObject(t1.window).sessionStorage).getItem("dartAppInstanceId")); + if (storedInstanceId != null) + t1.$dartAppInstanceId = storedInstanceId; + else { + t1.$dartAppInstanceId = new A.UuidV1(null).generate$1$options(null); + t2 = A._asJSObject(A._asJSObject(t1.window).sessionStorage); + t3 = A._asStringQ(t1.$dartAppInstanceId); + t3.toString; + t2.setItem("dartAppInstanceId", t3); + } + } uri = A.Uri_parse(A._asString(t1.$dwdsDevHandlerPath)); if (A._asString(A._asJSObject(A._asJSObject(t1.window).location).protocol) === "https:" && uri.get$scheme() === "http" && uri.get$host() !== "localhost") uri = uri.replace$1$scheme("https"); diff --git a/dwds/web/client.dart b/dwds/web/client.dart index 752ea68f2..c691c13ee 100644 --- a/dwds/web/client.dart +++ b/dwds/web/client.dart @@ -47,7 +47,22 @@ Future? main() { () async { // Set the unique id for this instance of the app. // Test apps may already have this set. - dartAppInstanceId ??= const Uuid().v1(); + const dartAppInstanceIdKey = 'dartAppInstanceId'; + if (dartAppInstanceId == null) { + // Check the session storage for the instance id. + final storedInstanceId = window.sessionStorage.getItem( + dartAppInstanceIdKey, + ); + if (storedInstanceId != null) { + dartAppInstanceId = storedInstanceId; + } else { + dartAppInstanceId = const Uuid().v1(); + window.sessionStorage.setItem( + dartAppInstanceIdKey, + dartAppInstanceId!, + ); + } + } final fixedPath = _fixProtocol(dwdsDevHandlerPath); final fixedUri = Uri.parse(fixedPath);