diff --git a/packages/react-native/Libraries/Utilities/HMRClient.js b/packages/react-native/Libraries/Utilities/HMRClient.js index 668c254cd67c..6c7fe32e02dd 100644 --- a/packages/react-native/Libraries/Utilities/HMRClient.js +++ b/packages/react-native/Libraries/Utilities/HMRClient.js @@ -237,6 +237,8 @@ Error: ${e.message}`; const changeId = body?.changeId; if (changeId != null && changeId !== lastMarkerChangeId) { lastMarkerChangeId = changeId; + + // Add marker entry in performance timeline performance.mark('Fast Refresh - Update done', { detail: { devtools: { @@ -246,6 +248,17 @@ Error: ${e.message}`; }, }, }); + + // Notify CDP clients via lifecycle event + if ( + // $FlowFixMe[prop-missing] - Injected by HostRuntimeBinding + typeof globalThis.__react_native_application_cdp_binding === + 'function' + ) { + globalThis.__react_native_application_cdp_binding( + 'fastRefreshComplete', + ); + } } } }); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp index 8fd200024d3a..d49495aa1662 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/HostAgent.cpp @@ -242,6 +242,19 @@ class HostAgent::Impl final { "Expected to find at least one session eligible to receive a background trace after ReactNativeApplication.enable"); (void)emitted; + static constexpr auto kBindingName = + "__react_native_application_cdp_binding"; + sessionState_.privateBindings.insert(std::string(kBindingName)); + sessionState_.onPrivateBindingCalled = + [this](const std::string& name, const std::string& payload) { + if (name == kBindingName && payload == "fastRefreshComplete") { + emitFastRefreshComplete(); + } + }; + if (instanceAgent_) { + instanceAgent_->installPrivateBindingHandler(kBindingName); + } + return { .isFinishedHandlingRequest = true, .shouldSendOKResponse = true, @@ -249,6 +262,9 @@ class HostAgent::Impl final { } if (req.method == "ReactNativeApplication.disable") { sessionState_.isReactNativeApplicationDomainEnabled = false; + sessionState_.privateBindings.erase( + "__react_native_application_cdp_binding"); + sessionState_.onPrivateBindingCalled = nullptr; return { .isFinishedHandlingRequest = true, @@ -392,6 +408,20 @@ class HostAgent::Impl final { frontendChannel_(cdp::jsonNotification("Network.disable")); } + void emitFastRefreshComplete() { + if (!sessionState_.isReactNativeApplicationDomainEnabled) { + return; + } + folly::dynamic params = folly::dynamic::object( + "timestamp", + duration_cast(system_clock::now().time_since_epoch()) + .count()); + frontendChannel_( + cdp::jsonNotification( + "ReactNativeApplication.unstable_fastRefreshComplete", + std::move(params))); + } + private: enum class FuseboxClientType { Unknown, Fusebox, NonFusebox }; @@ -497,6 +527,7 @@ class HostAgent::Impl final { return false; } void emitSystemStateChanged(bool isSingleHost) {} + void emitFastRefreshComplete() {} }; #endif // REACT_NATIVE_DEBUGGER_ENABLED diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp index f1eae37bb82f..7af1e82e8740 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.cpp @@ -62,6 +62,12 @@ void InstanceAgent::setCurrentRuntime(RuntimeTarget* runtimeTarget) { maybeSendPendingConsoleMessages(); } +void InstanceAgent::installPrivateBindingHandler(const std::string& name) { + if (runtimeAgent_) { + runtimeAgent_->installPrivateBindingHandler(name); + } +} + void InstanceAgent::maybeSendExecutionContextCreatedNotification() { if (runtimeAgent_ != nullptr) { auto& newContext = runtimeAgent_->getExecutionContextDescription(); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h index e073067c14e9..9b8017b0bb46 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/InstanceAgent.h @@ -57,6 +57,11 @@ class InstanceAgent final { */ void sendConsoleMessage(SimpleConsoleMessage message); + /** + * Install a private binding handler on the current runtime, if one exists. + */ + void installPrivateBindingHandler(const std::string &name); + private: void maybeSendExecutionContextCreatedNotification(); void sendConsoleMessageImmediately(SimpleConsoleMessage message); diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp index 24b89fa9336a..4834fe80cc35 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeAgent.cpp @@ -29,6 +29,10 @@ RuntimeAgent::RuntimeAgent( } } + for (auto& name : sessionState_.privateBindings) { + targetController_.installBindingHandler(name); + } + if (sessionState_.isRuntimeDomainEnabled) { targetController_.notifyDomainStateChanged( RuntimeTargetController::Domain::Runtime, true, *this); @@ -97,6 +101,13 @@ bool RuntimeAgent::handleRequest(const cdp::PreparsedRequest& req) { void RuntimeAgent::notifyBindingCalled( const std::string& bindingName, const std::string& payload) { + // Notify private binding subscribers (managed separately from client- + // initiated subscribedBindings). + if (sessionState_.privateBindings.count(bindingName) != 0u && + sessionState_.onPrivateBindingCalled) { + sessionState_.onPrivateBindingCalled(bindingName, payload); + } + // NOTE: When dispatching @cdp Runtime.bindingCalled notifications, we don't // re-check whether the session is expecting notifications from the current // context - only that it's subscribed to that binding name. @@ -119,6 +130,11 @@ void RuntimeAgent::notifyBindingCalled( "name", bindingName)("payload", payload))); } +void RuntimeAgent::installPrivateBindingHandler( + const std::string& bindingName) { + targetController_.installBindingHandler(bindingName); +} + 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 74d5c935a571..4c13442680c2 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); + /** + * Install a private binding handler on the runtime. Private bindings are + * tracked separately from client-initiated subscribedBindings and do not + * emit @cdp Runtime.bindingCalled events. + */ + void installPrivateBindingHandler(const std::string &bindingName); + struct ExportedState { std::unique_ptr delegateState; }; diff --git a/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h b/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h index 7ea71a08dbae..59baf78d1aeb 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h +++ b/packages/react-native/ReactCommon/jsinspector-modern/SessionState.h @@ -10,6 +10,7 @@ #include "ExecutionContext.h" #include "RuntimeAgent.h" +#include #include #include #include @@ -59,6 +60,20 @@ struct SessionState { */ RuntimeAgent::ExportedState lastRuntimeAgentExportedState; + /** + * Binding names installed privately by the backend (e.g. for + * ReactNativeApplication domain events). These are separate from + * subscribedBindings (which tracks client-initiated @cdp Runtime.addBinding + * requests) and do not emit @cdp Runtime.bindingCalled to the frontend. + */ + std::unordered_set privateBindings; + + /** + * Callback invoked by RuntimeAgent when a private binding is called from + * JS. Parameters are the binding name and payload. + */ + std::function onPrivateBindingCalled; + // Here, we will eventually allow RuntimeAgents to store their own arbitrary // state (e.g. some sort of K/V storage of folly::dynamic?)