From 41b719bd36b9e649e6448c6911ed2ecf7334f5b5 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Wed, 6 May 2026 04:20:40 -0700 Subject: [PATCH] Auto-reconnect PackagerConnection to Metro dev server (#56625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: When MWA starts before Metro is running, PackagerConnection's WebSocket connection to Metro's `/message` endpoint silently fails with no retry. The developer must restart MWA after starting Metro to get HMR working. This diff adds reconnection logic to PackagerConnection so it automatically retries when Metro becomes available. On initial connect failure or WebSocket disconnect, PackagerConnection schedules a retry after 5 seconds using the existing `WebSocketClientFactory` to create a fresh client. On successful reconnect, it fires `liveReloadCallback_()` which triggers `FoxReactHost::reloadReactInstance()` to load the bundle from Metro and set up HMR. The reconnection is event-driven: each retry is a short-lived thread that sleeps 5 seconds then calls `attemptConnection()`. The connect callback handles success/failure — no long-lived polling thread. The `active_` flag is set to false in the destructor to stop retries during shutdown. Combined with D97570592's push-based route invalidation, this completes the dev loop: Metro can start at any time, PackagerConnection auto-reconnects, HMR keeps the bundle fresh, and route changes propagate automatically. Changelog: [Internal] Reviewed By: christophpurrer Differential Revision: D97823787 --- .../react/devsupport/PackagerConnection.cpp | 65 ++++++++++++++++--- .../react/devsupport/PackagerConnection.h | 14 +++- .../react/runtime/ReactHost.cpp | 1 - 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp index 05676162f68..d17bae6ce7d 100644 --- a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.cpp @@ -9,22 +9,40 @@ #include #include -#include namespace facebook::react { PackagerConnection::PackagerConnection( - const WebSocketClientFactory& webSocketClientFactory, - const std::string& packagerConnectionUrl, + WebSocketClientFactory webSocketClientFactory, + std::string packagerConnectionUrl, LiveReloadCallback&& liveReloadCallback, ShowDevMenuCallback&& showDevMenuCallback) - : liveReloadCallback_(std::move(liveReloadCallback)), + : webSocketClientFactory_(std::move(webSocketClientFactory)), + packagerConnectionUrl_(std::move(packagerConnectionUrl)), + liveReloadCallback_(std::move(liveReloadCallback)), showDevMenuCallback_(std::move(showDevMenuCallback)) { - websocket_ = webSocketClientFactory(); + attemptConnection(); +} + +PackagerConnection::~PackagerConnection() noexcept { + reconnectThread_.quit(); + if (websocket_) { + websocket_->setOnClosedCallback(nullptr); + websocket_->setOnMessageCallback(nullptr); + websocket_->close("PackagerConnection destroyed"); + } +} + +void PackagerConnection::attemptConnection() { + if (websocket_) { + websocket_->setOnClosedCallback(nullptr); + websocket_->close("reconnecting"); + } + websocket_ = webSocketClientFactory_(); websocket_->setOnMessageCallback([this](const std::string& message) { LOG(INFO) << "Received message from packager: " << message; - auto json = nlohmann::json::parse(message); - if (json.is_null() || json["version"] != 2) { + auto json = nlohmann::json::parse(message, nullptr, false); + if (json.is_discarded() || json.is_null() || json["version"] != 2) { return; } auto method = json["method"]; @@ -34,11 +52,38 @@ PackagerConnection::PackagerConnection( showDevMenuCallback_(); } }); - websocket_->connect(packagerConnectionUrl); + websocket_->setOnClosedCallback([this](const std::string& reason) { + LOG(INFO) << "PackagerConnection closed: " << reason; + scheduleReconnect(); + }); + websocket_->connect( + packagerConnectionUrl_, + [this](bool success, const std::string& /*error*/) { + if (success) { + if (!isInitialConnection_) { + LOG(INFO) + << "PackagerConnection connected to Metro - triggering live reload"; + liveReloadCallback_(); + } else { + LOG(INFO) << "PackagerConnection connected to Metro"; + } + } else { + scheduleReconnect(); + } + isInitialConnection_ = false; + }); } -PackagerConnection::~PackagerConnection() noexcept { - websocket_->close("PackagerConnection destroyed"); +void PackagerConnection::scheduleReconnect() { + if (reconnectPending_.exchange(true)) { + return; + } + reconnectThread_.runAsync( + [this]() { + reconnectPending_ = false; + attemptConnection(); + }, + std::chrono::milliseconds(5000)); } } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h index eee5149fd43..7e1596c0f2b 100644 --- a/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h +++ b/packages/react-native/ReactCxxPlatform/react/devsupport/PackagerConnection.h @@ -8,6 +8,8 @@ #pragma once #include +#include +#include #include #include #include @@ -20,8 +22,8 @@ class PackagerConnection { public: PackagerConnection( - const WebSocketClientFactory &webSocketClientFactory, - const std::string &packagerConnectionUrl, + WebSocketClientFactory webSocketClientFactory, + std::string packagerConnectionUrl, LiveReloadCallback &&liveReloadCallback, ShowDevMenuCallback &&showDevMenuCallback); ~PackagerConnection() noexcept; @@ -31,9 +33,17 @@ class PackagerConnection { PackagerConnection &operator=(PackagerConnection &&other) = delete; private: + void attemptConnection(); + void scheduleReconnect(); + + const WebSocketClientFactory webSocketClientFactory_; + const std::string packagerConnectionUrl_; const LiveReloadCallback liveReloadCallback_; const ShowDevMenuCallback showDevMenuCallback_; std::unique_ptr websocket_; + std::atomic isInitialConnection_{true}; + std::atomic reconnectPending_{false}; + TaskDispatchThread reconnectThread_{"PackagerReconnect"}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index 60fde138554..9093f7e5285 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -396,7 +396,6 @@ bool ReactHost::loadScriptFromDevServer() { devServerHelper_->setupHMRClient(); return true; } catch (...) { - devServerHelper_->setSourcePath(""); LOG(WARNING) << "Unable to download JS bundle from Metro, falling back to prebuilt JS bundle. " << "To start Metro, run in command line: 'cd ~/fbsource/xplat/js && js1 run'";