From 985f1b0e318d5ed7303904ed2195334c0ae4d39b Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 1 Apr 2026 11:41:28 -0700 Subject: [PATCH] Add unstable_fastRefreshComplete CDP event (#56273) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56273 Adds a new, experimental `ReactNativeApplication.unstable_fastRefreshComplete` CDP event, emitted to subscribed active CDP sessions when a Fast Refresh update completes. **Notes** - As with D97486551, we reuse the `changeId` block in `HMRClient.js`, ensuring duplicate updates for the same change are not reported. Changelog: [Internal] Reviewed By: GijsWeterings, hoxyq Differential Revision: D98493216 fbshipit-source-id: b0b81a210fb84873e9358aa5484038062f110103 --- .../Libraries/Utilities/HMRClient.js | 28 +++++++++++++++- .../jsinspector-modern/RuntimeAgent.cpp | 19 +++++++++++ .../jsinspector-modern/RuntimeAgent.h | 7 ++++ .../jsinspector-modern/RuntimeTarget.cpp | 33 +++++++++++++++++++ .../jsinspector-modern/RuntimeTarget.h | 6 ++++ 5 files changed, 92 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Utilities/HMRClient.js b/packages/react-native/Libraries/Utilities/HMRClient.js index 685e79707c17..86a557bfba30 100644 --- a/packages/react-native/Libraries/Utilities/HMRClient.js +++ b/packages/react-native/Libraries/Utilities/HMRClient.js @@ -229,10 +229,15 @@ Error: ${e.message}`; } }); - client.on('update-done', () => { + client.on('update-done', body => { pendingUpdatesCount--; if (pendingUpdatesCount === 0) { DevLoadingView.hide(); + const changeId = body?.changeId; + if (changeId != null && changeId !== lastMarkerChangeId) { + lastMarkerChangeId = changeId; + emitFastRefreshCompleteEvents(); + } } }); @@ -378,4 +383,25 @@ function showCompileError() { throw error; } +function emitFastRefreshCompleteEvents() { + // Add marker entry in performance timeline + performance.mark('Fast Refresh - Update done', { + detail: { + devtools: { + dataType: 'marker', + color: 'primary', + tooltipText: 'Fast Refresh \u269b', + }, + }, + }); + + // Notify CDP clients via internal binding + if ( + // $FlowFixMe[prop-missing] - Injected by RuntimeTarget + typeof globalThis.__notifyFastRefreshComplete === 'function' + ) { + globalThis.__notifyFastRefreshComplete(); + } +} + export default HMRClient; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp index 7631e2fea752..70e8bd2b9f4e 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp @@ -8,6 +8,10 @@ #include "RuntimeAgent.h" #include "SessionState.h" +#include +#include + +#include #include namespace facebook::react::jsinspector_modern { @@ -119,6 +123,21 @@ void RuntimeAgent::notifyBindingCalled( "name", bindingName)("payload", payload))); } +void RuntimeAgent::notifyFastRefreshComplete() { + if (!sessionState_.isReactNativeApplicationDomainEnabled) { + return; + } + folly::dynamic params = folly::dynamic::object( + "timestamp", + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count()); + frontendChannel_( + cdp::jsonNotification( + "ReactNativeApplication.unstable_fastRefreshComplete", + std::move(params))); +} + RuntimeAgent::ExportedState RuntimeAgent::getExportedState() { return { .delegateState = delegate_ ? delegate_->getExportedState() : nullptr, diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h index 7e0b62b442e7..eabd54304dbf 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.h @@ -72,6 +72,13 @@ class RuntimeAgent final { void notifyBindingCalled(const std::string &bindingName, const std::string &payload); + /** + * Called by RuntimeTarget when JS calls __notifyFastRefreshComplete(). + * Emits a ReactNativeApplication.unstable_fastRefreshComplete CDP + * notification if the ReactNativeApplication domain is enabled. + */ + void notifyFastRefreshComplete(); + struct ExportedState { std::unique_ptr delegateState; }; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp index 0f84ecd0e179..15f40c39ee0c 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp @@ -60,6 +60,8 @@ void RuntimeTarget::installGlobals() { // NOTE: RuntimeTarget::installNetworkReporterAPI is in // RuntimeTargetNetwork.cpp installNetworkReporterAPI(); + + installFastRefreshHandler(); } std::shared_ptr RuntimeTarget::createAgent( @@ -141,6 +143,37 @@ void RuntimeTarget::installBindingHandler(const std::string& bindingName) { }); } +void RuntimeTarget::installFastRefreshHandler() { + jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) { + auto globalObj = runtime.global(); + try { + auto name = + jsi::PropNameID::forUtf8(runtime, "__notifyFastRefreshComplete"); + globalObj.setProperty( + runtime, + name, + jsi::Function::createFromHostFunction( + runtime, + name, + 0, + [selfExecutor]( + jsi::Runtime& /*rt*/, + const jsi::Value&, + const jsi::Value*, + size_t) -> jsi::Value { + selfExecutor([](auto& self) { + self.agents_.forEach( + [](auto& agent) { agent.notifyFastRefreshComplete(); }); + }); + + return jsi::Value::undefined(); + })); + } catch (jsi::JSError&) { + // Swallow JavaScript exceptions that occur while setting up the global. + } + }); +} + void RuntimeTarget::emitDebuggerSessionCreated() { jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) { try { diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h index 236cf83e7de3..c4e89f0ac630 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.h @@ -289,6 +289,12 @@ class JSINSPECTOR_EXPORT RuntimeTarget : public EnableExecutorFromThis