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;