From 520bc3657d99aa3200e8dae8d25f894bb09e030c Mon Sep 17 00:00:00 2001 From: Emily Brown Date: Wed, 11 Feb 2026 05:54:32 -0800 Subject: [PATCH] Extract shared GlobalStateObserver from FuseboxSessionObserver (#55485) Summary: The FuseboxSessionObserver pattern (a native global object with boolean state, subscribers Set, and state change callback) is going to be reused for PerformanceTracerObserver in D92527815. This diff extracts the shared logic into reusable components: JS: GlobalStateObserver class parameterized by global name and status property. C++: installGlobalStateObserver() and emitGlobalStateObserverChange() functions parameterized by global name, status property, and callback name. FuseboxSessionObserver and RuntimeTargetDebuggerSessionObserver now delegate to the shared implementations. No behavior change. Changelog: [GENERAL] [CHANGED] - extracting shared logic for fuseboxSessionObserver into reusable components Reviewed By: hoxyq Differential Revision: D92720212 --- .../jsinspector-modern/RuntimeTarget.cpp | 28 ++-- .../RuntimeTargetDebuggerSessionObserver.cpp | 101 +-------------- .../RuntimeTargetGlobalStateObserver.cpp | 122 ++++++++++++++++++ .../RuntimeTargetGlobalStateObserver.h | 42 ++++++ .../rndevtools/FuseboxSessionObserver.js | 37 ++---- .../rndevtools/GlobalStateObserver.js | 54 ++++++++ 6 files changed, 247 insertions(+), 137 deletions(-) create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.cpp create mode 100644 packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.h create mode 100644 packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp index 2fe2f51552bcf2..1a1807696fbf7d 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTarget.cpp @@ -8,6 +8,7 @@ #include "SessionState.h" #include +#include #include #include @@ -16,21 +17,6 @@ using namespace facebook::jsi; namespace facebook::react::jsinspector_modern { -namespace { - -void emitSessionStatusChangeForObserverWithValue( - jsi::Runtime& runtime, - const jsi::Value& value) { - auto globalObj = runtime.global(); - auto observer = - globalObj.getPropertyAsObject(runtime, "__DEBUGGER_SESSION_OBSERVER__"); - auto onSessionStatusChange = - observer.getPropertyAsFunction(runtime, "onSessionStatusChange"); - onSessionStatusChange.call(runtime, value); -} - -} // namespace - std::shared_ptr RuntimeTarget::create( const ExecutionContextDescription& executionContextDescription, RuntimeTargetDelegate& delegate, @@ -144,7 +130,11 @@ void RuntimeTarget::installBindingHandler(const std::string& bindingName) { void RuntimeTarget::emitDebuggerSessionCreated() { jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) { try { - emitSessionStatusChangeForObserverWithValue(runtime, jsi::Value(true)); + emitGlobalStateObserverChange( + runtime, + "__DEBUGGER_SESSION_OBSERVER__", + "onSessionStatusChange", + true); } catch (jsi::JSError&) { // Suppress any errors, they should not be visible to the user // and should not affect runtime. @@ -155,7 +145,11 @@ void RuntimeTarget::emitDebuggerSessionCreated() { void RuntimeTarget::emitDebuggerSessionDestroyed() { jsExecutor_([selfExecutor = executorFromThis()](jsi::Runtime& runtime) { try { - emitSessionStatusChangeForObserverWithValue(runtime, jsi::Value(false)); + emitGlobalStateObserverChange( + runtime, + "__DEBUGGER_SESSION_OBSERVER__", + "onSessionStatusChange", + false); } catch (jsi::JSError&) { // Suppress any errors, they should not be visible to the user // and should not affect runtime. diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetDebuggerSessionObserver.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetDebuggerSessionObserver.cpp index 48187008d0ad5f..d4b9682b3740be 100644 --- a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetDebuggerSessionObserver.cpp +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetDebuggerSessionObserver.cpp @@ -6,106 +6,17 @@ */ #include +#include namespace facebook::react::jsinspector_modern { void RuntimeTarget::installDebuggerSessionObserver() { jsExecutor_([](jsi::Runtime& runtime) { - auto globalObj = runtime.global(); - try { - auto observer = jsi::Object(runtime); - - observer.setProperty(runtime, "hasActiveSession", jsi::Value(false)); - - auto setFunction = globalObj.getPropertyAsFunction(runtime, "Set"); - auto set = setFunction.callAsConstructor(runtime); - observer.setProperty(runtime, "subscribers", set); - - observer.setProperty( - runtime, - "onSessionStatusChange", - jsi::Function::createFromHostFunction( - runtime, - jsi::PropNameID::forAscii(runtime, "onSessionStatusChange"), - 1, - [](jsi::Runtime& onSessionStatusChangeRuntime, - const jsi::Value& /* onSessionStatusChangeThisVal */, - const jsi::Value* onSessionStatusChangeArgs, - size_t onSessionStatusChangeArgsCount) { - if (onSessionStatusChangeArgsCount != 1 || - !onSessionStatusChangeArgs[0].isBool()) { - throw jsi::JSError( - onSessionStatusChangeRuntime, - "Invalid arguments: onSessionStatusChange expects 1 boolean argument"); - } - - bool updatedStatus = onSessionStatusChangeArgs[0].getBool(); - - auto observerInstanceFromOnSessionStatusChange = - onSessionStatusChangeRuntime.global().getPropertyAsObject( - onSessionStatusChangeRuntime, - "__DEBUGGER_SESSION_OBSERVER__"); - auto subscribersToNotify = - observerInstanceFromOnSessionStatusChange - .getPropertyAsObject( - onSessionStatusChangeRuntime, "subscribers"); - - observerInstanceFromOnSessionStatusChange.setProperty( - onSessionStatusChangeRuntime, - "hasActiveSession", - updatedStatus); - - if (subscribersToNotify - .getProperty(onSessionStatusChangeRuntime, "size") - .asNumber() == 0) { - return jsi::Value::undefined(); - } - - auto forEachSubscriber = - subscribersToNotify.getPropertyAsFunction( - onSessionStatusChangeRuntime, "forEach"); - auto forEachSubscriberCallback = - jsi::Function::createFromHostFunction( - onSessionStatusChangeRuntime, - jsi::PropNameID::forAscii( - onSessionStatusChangeRuntime, "forEachCallback"), - 1, - [updatedStatus]( - jsi::Runtime& forEachCallbackRuntime, - const jsi::Value& /* forEachCallbackThisVal */, - const jsi::Value* forEachCallbackArgs, - size_t forEachCallbackArgsCount) { - if (forEachCallbackArgsCount < 1 || - !forEachCallbackArgs[0].isObject() || - !forEachCallbackArgs[0] - .getObject(forEachCallbackRuntime) - .isFunction(forEachCallbackRuntime)) { - throw jsi::JSError( - forEachCallbackRuntime, - "Invalid arguments: forEachSubscriberCallback expects function as a first argument"); - } - - forEachCallbackArgs[0] - .getObject(forEachCallbackRuntime) - .asFunction(forEachCallbackRuntime) - .call(forEachCallbackRuntime, updatedStatus); - - return jsi::Value::undefined(); - }); - - forEachSubscriber.callWithThis( - onSessionStatusChangeRuntime, - subscribersToNotify, - forEachSubscriberCallback); - - return jsi::Value::undefined(); - })); - - globalObj.setProperty(runtime, "__DEBUGGER_SESSION_OBSERVER__", observer); - } catch (jsi::JSError&) { - // Suppress any errors, they should not be visible to the user - // and should not affect runtime. - } + installGlobalStateObserver( + runtime, + "__DEBUGGER_SESSION_OBSERVER__", + "hasActiveSession", + "onSessionStatusChange"); }); } diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.cpp b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.cpp new file mode 100644 index 00000000000000..4035e242866855 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "RuntimeTargetGlobalStateObserver.h" + +#include + +namespace facebook::react::jsinspector_modern { + +void installGlobalStateObserver( + jsi::Runtime& runtime, + const char* globalName, + const char* statusProperty, + const char* callbackName) { + auto globalObj = runtime.global(); + try { + auto observer = jsi::Object(runtime); + + observer.setProperty(runtime, statusProperty, jsi::Value(false)); + + auto setFunction = globalObj.getPropertyAsFunction(runtime, "Set"); + auto set = setFunction.callAsConstructor(runtime); + observer.setProperty(runtime, "subscribers", set); + + std::string globalNameStr(globalName); + std::string statusPropertyStr(statusProperty); + + observer.setProperty( + runtime, + callbackName, + jsi::Function::createFromHostFunction( + runtime, + jsi::PropNameID::forAscii(runtime, callbackName), + 1, + [globalNameStr, statusPropertyStr]( + jsi::Runtime& callbackRuntime, + const jsi::Value& /* thisVal */, + const jsi::Value* args, + size_t argsCount) { + if (argsCount != 1 || !args[0].isBool()) { + throw jsi::JSError( + callbackRuntime, + "Invalid arguments: state change callback expects 1 boolean argument"); + } + + bool updatedStatus = args[0].getBool(); + + auto observerInstance = + callbackRuntime.global().getPropertyAsObject( + callbackRuntime, globalNameStr.c_str()); + auto subscribersToNotify = observerInstance.getPropertyAsObject( + callbackRuntime, "subscribers"); + + observerInstance.setProperty( + callbackRuntime, statusPropertyStr.c_str(), updatedStatus); + + if (subscribersToNotify.getProperty(callbackRuntime, "size") + .asNumber() == 0) { + return jsi::Value::undefined(); + } + + auto forEachSubscriber = + subscribersToNotify.getPropertyAsFunction( + callbackRuntime, "forEach"); + auto forEachSubscriberCallback = jsi::Function::createFromHostFunction( + callbackRuntime, + jsi::PropNameID::forAscii(callbackRuntime, "forEachCallback"), + 1, + [updatedStatus]( + jsi::Runtime& forEachCallbackRuntime, + const jsi::Value& /* forEachCallbackThisVal */, + const jsi::Value* forEachCallbackArgs, + size_t forEachCallbackArgsCount) { + if (forEachCallbackArgsCount < 1 || + !forEachCallbackArgs[0].isObject() || + !forEachCallbackArgs[0] + .getObject(forEachCallbackRuntime) + .isFunction(forEachCallbackRuntime)) { + throw jsi::JSError( + forEachCallbackRuntime, + "Invalid arguments: forEachSubscriberCallback expects function as a first argument"); + } + + forEachCallbackArgs[0] + .getObject(forEachCallbackRuntime) + .asFunction(forEachCallbackRuntime) + .call(forEachCallbackRuntime, updatedStatus); + + return jsi::Value::undefined(); + }); + + forEachSubscriber.callWithThis( + callbackRuntime, + subscribersToNotify, + forEachSubscriberCallback); + + return jsi::Value::undefined(); + })); + + globalObj.setProperty(runtime, globalName, observer); + } catch (jsi::JSError&) { + // Suppress any errors, they should not be visible to the user + // and should not affect runtime. + } +} + +void emitGlobalStateObserverChange( + jsi::Runtime& runtime, + const char* globalName, + const char* callbackName, + bool value) { + auto globalObj = runtime.global(); + auto observer = globalObj.getPropertyAsObject(runtime, globalName); + auto callback = observer.getPropertyAsFunction(runtime, callbackName); + callback.call(runtime, jsi::Value(value)); +} + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.h b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.h new file mode 100644 index 00000000000000..af76002d6ecd18 --- /dev/null +++ b/packages/react-native/ReactCommon/jsinspector-modern/RuntimeTargetGlobalStateObserver.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +namespace facebook::react::jsinspector_modern { + +/** + * Installs a global state observer object on the JavaScript runtime's global + * object. The observer has a boolean status property, a Set of subscribers, + * and a callback that updates the status and notifies subscribers. + * + * @param globalName The name of the global object (e.g., + * "__DEBUGGER_SESSION_OBSERVER__"). + * @param statusProperty The name of the boolean property (e.g., + * "hasActiveSession"). + * @param callbackName The name of the state change callback (e.g., + * "onSessionStatusChange"). + */ +void installGlobalStateObserver( + jsi::Runtime &runtime, + const char *globalName, + const char *statusProperty, + const char *callbackName); + +/** + * Emits a state change to an installed global state observer by calling its + * callback function. + * + * @param globalName The name of the global object. + * @param callbackName The name of the state change callback. + * @param value The new boolean state value. + */ +void emitGlobalStateObserverChange(jsi::Runtime &runtime, const char *globalName, const char *callbackName, bool value); + +} // namespace facebook::react::jsinspector_modern diff --git a/packages/react-native/src/private/devsupport/rndevtools/FuseboxSessionObserver.js b/packages/react-native/src/private/devsupport/rndevtools/FuseboxSessionObserver.js index c0ac0b2c547d7f..79ba201bd4685b 100644 --- a/packages/react-native/src/private/devsupport/rndevtools/FuseboxSessionObserver.js +++ b/packages/react-native/src/private/devsupport/rndevtools/FuseboxSessionObserver.js @@ -8,34 +8,21 @@ * @format */ -class FuseboxSessionObserver { - #hasNativeSupport: boolean; +import GlobalStateObserver from './GlobalStateObserver'; - constructor() { - this.#hasNativeSupport = global.hasOwnProperty( - '__DEBUGGER_SESSION_OBSERVER__', - ); - } +const observer = new GlobalStateObserver( + '__DEBUGGER_SESSION_OBSERVER__', + 'hasActiveSession', +); +const FuseboxSessionObserver = { hasActiveSession(): boolean { - if (!this.#hasNativeSupport) { - return false; - } - - return global.__DEBUGGER_SESSION_OBSERVER__.hasActiveSession; - } + return observer.getStatus(); + }, subscribe(callback: (status: boolean) => void): () => void { - if (!this.#hasNativeSupport) { - return () => {}; - } - - global.__DEBUGGER_SESSION_OBSERVER__.subscribers.add(callback); - return () => { - global.__DEBUGGER_SESSION_OBSERVER__.subscribers.delete(callback); - }; - } -} + return observer.subscribe(callback); + }, +}; -const observerInstance: FuseboxSessionObserver = new FuseboxSessionObserver(); -export default observerInstance; +export default FuseboxSessionObserver; diff --git a/packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js b/packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js new file mode 100644 index 00000000000000..2a802c83429760 --- /dev/null +++ b/packages/react-native/src/private/devsupport/rndevtools/GlobalStateObserver.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +/** + * Generic observer for a boolean state exposed via a native global object. + * + * Native code installs a global object with the following shape: + * global[globalName] = { + * [statusProperty]: boolean, + * subscribers: Set<(status: boolean) => void>, + * [callbackName]: (status: boolean) => void, + * } + * + * This class provides a JS-friendly API over that global object. + */ +class GlobalStateObserver { + #hasNativeSupport: boolean; + #globalName: string; + #statusProperty: string; + + constructor(globalName: string, statusProperty: string) { + this.#globalName = globalName; + this.#statusProperty = statusProperty; + this.#hasNativeSupport = global.hasOwnProperty(globalName); + } + + getStatus(): boolean { + if (!this.#hasNativeSupport) { + return false; + } + + return global[this.#globalName][this.#statusProperty]; + } + + subscribe(callback: (status: boolean) => void): () => void { + if (!this.#hasNativeSupport) { + return () => {}; + } + + global[this.#globalName].subscribers.add(callback); + return () => { + global[this.#globalName].subscribers.delete(callback); + }; + } +} + +export default GlobalStateObserver;