From eb345a6073f92928f56a62a4ee76fba1c72c0ef2 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Wed, 27 May 2026 08:38:33 +0200 Subject: [PATCH] refactor: clean threaded runtime globals --- example/App.tsx | 11 +-- example/index.js | 20 ++--- .../src/examples/fibonacciRuntimeFunction.ts | 13 +--- .../examples/heavyWorkloadRuntimeFunction.ts | 13 +--- .../src/examples/twoRuntimesArchitecture.ts | 11 +-- packages/core/README.md | 45 ++++++++++-- .../threadedruntime/ThreadedRuntime.kt | 13 +--- .../threadedruntime/RuntimeFunctionJsi.cpp | 4 - packages/core/ios/ThreadedRuntime.mm | 12 --- packages/core/src/ThreadedRuntime.tsx | 73 +++++++++++-------- website/docs/installation.md | 24 +++++- 11 files changed, 126 insertions(+), 113 deletions(-) diff --git a/example/App.tsx b/example/App.tsx index cee22de..393df48 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -40,6 +40,7 @@ import { ChatBubble } from './src/chat/ChatBubble'; import type { RenderedChatItem } from './src/native/ComposeChatListNativeComponent'; import { call, + getCurrentRuntime, OnRuntime, threadedComponent, Threaded, @@ -2918,15 +2919,7 @@ function titleForRnMode(mode: RnBenchmarkMode) { } function runtimeKind() { - const globals = globalThis as { - __COMPOSE_CHAT_LIST_ENV__?: { kind?: string }; - __THREADED_RUNTIME_ENV__?: { kind?: string }; - }; - return ( - globals.__THREADED_RUNTIME_ENV__?.kind ?? - globals.__COMPOSE_CHAT_LIST_ENV__?.kind ?? - 'main' - ); + return getCurrentRuntime().kind ?? 'main'; } function scrollRnListToIndex( diff --git a/example/index.js b/example/index.js index 85a7431..ddac9e6 100644 --- a/example/index.js +++ b/example/index.js @@ -3,6 +3,10 @@ */ const { AppRegistry } = require('react-native'); +const { + getCurrentRuntime, + isMainRuntime, +} = require('@react-native-runtimes/core'); // Register threaded roots/callable modules in every runtime. Component modules // stay lazy; production runtime-specific entries are gated inside the generated @@ -15,17 +19,13 @@ if (typeof __DEV__ !== 'undefined' && __DEV__) { require('./index.business-runtime'); } -const threadedRuntimeEnv = global.__THREADED_RUNTIME_ENV__; -const isListEnvironment = - global._is_it_a_list_env === true && global.__COMPOSE_CHAT_LIST_ENV__; +const currentRuntime = getCurrentRuntime(); -if (threadedRuntimeEnv || isListEnvironment) { - if (threadedRuntimeEnv?.kind !== 'business-runtime') { - AppRegistry.registerComponent( - 'ComposeChatSecondRuntimeRnList', - () => require('./App').SecondRuntimeRnListApp, - ); - } +if (!isMainRuntime() && currentRuntime.kind !== 'business-runtime') { + AppRegistry.registerComponent( + 'ComposeChatSecondRuntimeRnList', + () => require('./App').SecondRuntimeRnListApp, + ); } const { name: appName } = require('./app.json'); diff --git a/example/src/examples/fibonacciRuntimeFunction.ts b/example/src/examples/fibonacciRuntimeFunction.ts index f58c482..0527d10 100644 --- a/example/src/examples/fibonacciRuntimeFunction.ts +++ b/example/src/examples/fibonacciRuntimeFunction.ts @@ -1,4 +1,4 @@ -import { runtimeFunction } from '@react-native-runtimes/core'; +import { getCurrentRuntime, runtimeFunction } from '@react-native-runtimes/core'; export type FibonacciResult = { input: number; @@ -9,15 +9,10 @@ export type FibonacciResult = { }; function runtimeInfo() { - const globals = globalThis as { - __THREADED_RUNTIME_ENV__?: { kind?: string; runtimeName?: string }; - __COMPOSE_CHAT_LIST_ENV__?: { kind?: string; runtimeName?: string }; - }; - const threadedEnv = globals.__THREADED_RUNTIME_ENV__; - const listEnv = globals.__COMPOSE_CHAT_LIST_ENV__; + const runtime = getCurrentRuntime(); return { - runtimeKind: threadedEnv?.kind ?? listEnv?.kind ?? 'main', - runtimeName: threadedEnv?.runtimeName ?? listEnv?.runtimeName ?? 'main', + runtimeKind: runtime.kind ?? 'main', + runtimeName: runtime.name, }; } diff --git a/example/src/examples/heavyWorkloadRuntimeFunction.ts b/example/src/examples/heavyWorkloadRuntimeFunction.ts index c5bcf54..0e97f8f 100644 --- a/example/src/examples/heavyWorkloadRuntimeFunction.ts +++ b/example/src/examples/heavyWorkloadRuntimeFunction.ts @@ -1,4 +1,4 @@ -import { runtimeFunction } from '@react-native-runtimes/core'; +import { getCurrentRuntime, runtimeFunction } from '@react-native-runtimes/core'; export type HeavyRunResult = { n: number; @@ -33,15 +33,10 @@ function recursiveFibonacci(n: number): number { } function runtimeInfo() { - const globals = globalThis as { - __THREADED_RUNTIME_ENV__?: { kind?: string; runtimeName?: string }; - __COMPOSE_CHAT_LIST_ENV__?: { kind?: string; runtimeName?: string }; - }; - const threadedEnv = globals.__THREADED_RUNTIME_ENV__; - const listEnv = globals.__COMPOSE_CHAT_LIST_ENV__; + const runtime = getCurrentRuntime(); return { - runtimeKind: threadedEnv?.kind ?? listEnv?.kind ?? 'main', - runtimeName: threadedEnv?.runtimeName ?? listEnv?.runtimeName ?? 'main', + runtimeKind: runtime.kind ?? 'main', + runtimeName: runtime.name, }; } diff --git a/example/src/examples/twoRuntimesArchitecture.ts b/example/src/examples/twoRuntimesArchitecture.ts index bad791e..1473142 100644 --- a/example/src/examples/twoRuntimesArchitecture.ts +++ b/example/src/examples/twoRuntimesArchitecture.ts @@ -1,4 +1,5 @@ import { + getCurrentRuntime, ThreadedRuntime, registerThreadedHeadlessTask, runtimeFunction, @@ -98,15 +99,7 @@ export const twoRuntimeMetrics = ); function runtimeKind() { - const globals = globalThis as { - __COMPOSE_CHAT_LIST_ENV__?: { kind?: string }; - __THREADED_RUNTIME_ENV__?: { kind?: string }; - }; - return ( - globals.__THREADED_RUNTIME_ENV__?.kind ?? - globals.__COMPOSE_CHAT_LIST_ENV__?.kind ?? - 'main' - ); + return getCurrentRuntime().kind ?? 'main'; } function nextMetrics( diff --git a/packages/core/README.md b/packages/core/README.md index 0776beb..6b7f256 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -59,7 +59,11 @@ Add the generated directory to `.gitignore`: Load the generated entry only in the secondary runtime path: ```js -if (global.__THREADED_RUNTIME_ENV__ || global._is_it_a_list_env === true) { +const { + isMainRuntime, +} = require('@react-native-runtimes/core'); + +if (!isMainRuntime()) { require('./.threaded-runtime/entry'); } ``` @@ -91,8 +95,7 @@ index.two-runtimes-business-runtime.ts Only files matching `index..ts` in the project root are discovered. The generated entry requires a file when `` matches either -`global.__THREADED_RUNTIME_ENV__.kind` or -`global.__THREADED_RUNTIME_ENV__.runtimeName`. +`getCurrentRuntime().kind` or `getCurrentRuntime().name`. The component module is required only when `ThreadedRuntimeHost` receives that component name. @@ -536,8 +539,11 @@ creates a surface: ```js const { AppRegistry } = require('react-native'); +const { + isMainRuntime, +} = require('@react-native-runtimes/core'); -if (global._is_it_a_list_env === true) { +if (!isMainRuntime()) { require('./App'); // component registrations AppRegistry.registerComponent( 'ThreadedRuntimeHost', @@ -612,13 +618,42 @@ ThreadedRuntime.prewarmBusinessRuntime(applicationContext, "business-runtime") That runtime receives `global.__THREADED_RUNTIME_ENV__` before the bundle runs: ```tsx -if (global.__THREADED_RUNTIME_ENV__?.kind === 'business-runtime') { +import { getCurrentRuntime } from '@react-native-runtimes/core'; + +if (getCurrentRuntime().kind === 'business-runtime') { require('./src/businessRuntimeEntry'); } else { require('./src/mainRuntimeEntry'); } ``` +## Runtime Globals + +Supported runtime identity is exposed through the public API: + +```ts +import { getCurrentRuntime, isMainRuntime } from '@react-native-runtimes/core'; + +const runtime = getCurrentRuntime(); +``` + +- `getCurrentRuntime()` returns `{ isMain, name, kind }` +- `isMainRuntime()` is the preferred bootstrap check + +The only supported runtime global is `global.__THREADED_RUNTIME_ENV__`, which is +installed before bundle execution on secondary runtimes and currently contains: + +- `runtimeName` +- `kind` + +Internal globals used by runtime-function dispatch still exist, but they are not +part of the app-facing API: + +- `global.__rnrRegisterRuntimeFunction` +- `global.__rnrCallRuntimeFunction` + +Apps should not depend on those names directly. + iOS threaded runtimes already use the configured React Native delegate for native-module lookup, so `ThreadedRuntime.prewarmBusinessRuntime("business-runtime")` uses the app's module resolution path. diff --git a/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt b/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt index 95bc117..0c075f8 100644 --- a/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt +++ b/packages/core/android/src/main/java/com/nativecompose/threadedruntime/ThreadedRuntime.kt @@ -498,20 +498,9 @@ private class ThreadedRuntimeBundleLoader( """ var __threadedRuntimeGlobal = typeof globalThis !== 'undefined' ? globalThis : Function('return this')(); - __threadedRuntimeGlobal.global = __threadedRuntimeGlobal; - __threadedRuntimeGlobal.globalThis = __threadedRuntimeGlobal; - __threadedRuntimeGlobal._is_it_a_list_env = true; __threadedRuntimeGlobal.__THREADED_RUNTIME_ENV__ = { kind: ${jsString(options.kind)}, - runtimeName: ${jsString(runtimeName)}, - isBackgroundRuntime: ${options.kind != ThreadedRuntime.DEFAULT_RUNTIME_KIND}, - useMainNativeModules: ${options.useMainNativeModules}, - version: 1 - }; - __threadedRuntimeGlobal.__COMPOSE_CHAT_LIST_ENV__ = { - kind: 'background-list', - runtimeName: ${jsString(runtimeName)}, - version: 1 + runtimeName: ${jsString(runtimeName)} }; """.trimIndent(), ) diff --git a/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionJsi.cpp b/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionJsi.cpp index c4cae28..d8c549f 100644 --- a/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionJsi.cpp +++ b/packages/core/cpp/nativecompose/threadedruntime/RuntimeFunctionJsi.cpp @@ -140,10 +140,6 @@ void installRuntimeFunctionJsi(Runtime &runtime, const std::string &runtimeName) runtime, "__rnrCallRuntimeFunction", std::move(callFunction)); - runtime.global().setProperty( - runtime, - "__rnrRuntimeFunctionCacheRuntimeName", - String::createFromUtf8(runtime, runtimeName)); } } // namespace nativecompose::threadedruntime diff --git a/packages/core/ios/ThreadedRuntime.mm b/packages/core/ios/ThreadedRuntime.mm index 6e01cc0..3e7556d 100644 --- a/packages/core/ios/ThreadedRuntime.mm +++ b/packages/core/ios/ThreadedRuntime.mm @@ -78,24 +78,12 @@ - (void)hostDidStart:(RCTHost *)host - (void)host:(RCTHost *)host didInitializeRuntime:(facebook::jsi::Runtime &)runtime { auto global = runtime.global(); - global.setProperty(runtime, "global", global); - global.setProperty(runtime, "globalThis", global); - global.setProperty(runtime, "_is_it_a_list_env", true); auto threadedEnv = facebook::jsi::Object(runtime); threadedEnv.setProperty(runtime, "kind", facebook::jsi::String::createFromUtf8(runtime, [_kind UTF8String])); threadedEnv.setProperty(runtime, "runtimeName", facebook::jsi::String::createFromUtf8(runtime, [_runtimeName UTF8String])); - threadedEnv.setProperty(runtime, "isBackgroundRuntime", ![_kind isEqualToString:ThreadedRuntimeDefaultRuntimeKind]); - threadedEnv.setProperty(runtime, "useMainNativeModules", true); - threadedEnv.setProperty(runtime, "version", 1); global.setProperty(runtime, "__THREADED_RUNTIME_ENV__", threadedEnv); - auto listEnv = facebook::jsi::Object(runtime); - listEnv.setProperty(runtime, "kind", facebook::jsi::String::createFromUtf8(runtime, "background-list")); - listEnv.setProperty(runtime, "runtimeName", facebook::jsi::String::createFromUtf8(runtime, [_runtimeName UTF8String])); - listEnv.setProperty(runtime, "version", 1); - global.setProperty(runtime, "__COMPOSE_CHAT_LIST_ENV__", listEnv); - nativecompose::threadedruntime::installRuntimeFunctionJsi(runtime, [_runtimeName UTF8String]); if ([_delegate respondsToSelector:@selector(host:didInitializeRuntime:)]) { diff --git a/packages/core/src/ThreadedRuntime.tsx b/packages/core/src/ThreadedRuntime.tsx index d4641b9..414a5e7 100644 --- a/packages/core/src/ThreadedRuntime.tsx +++ b/packages/core/src/ThreadedRuntime.tsx @@ -101,17 +101,10 @@ const nativeRuntime = NativeModules.ThreadedRuntime as let runtimeFunctionsNitro: ThreadedRuntimeFunctionsNitro | null | undefined; let didWarnRuntimeFunctionsNitroUnavailable = false; +let didInstallThreadedRuntimeEventEmitterFallback = false; function currentRuntimeName() { - const globals = globalThis as { - __THREADED_RUNTIME_ENV__?: { runtimeName?: string }; - __COMPOSE_CHAT_LIST_ENV__?: { runtimeName?: string }; - }; - return ( - globals.__THREADED_RUNTIME_ENV__?.runtimeName ?? - globals.__COMPOSE_CHAT_LIST_ENV__?.runtimeName ?? - DEFAULT_RUNTIME_NAME - ); + return getCurrentRuntime().name ?? DEFAULT_RUNTIME_NAME; } export const MAIN_RUNTIME_NAME = 'main'; @@ -125,10 +118,8 @@ export type CurrentRuntimeInfo = { export function getCurrentRuntime(): CurrentRuntimeInfo { const globals = globalThis as { __THREADED_RUNTIME_ENV__?: { runtimeName?: string; kind?: string }; - __COMPOSE_CHAT_LIST_ENV__?: { runtimeName?: string; kind?: string }; }; - const env = - globals.__THREADED_RUNTIME_ENV__ ?? globals.__COMPOSE_CHAT_LIST_ENV__; + const env = globals.__THREADED_RUNTIME_ENV__; if (env?.runtimeName) { return { isMain: false, @@ -147,6 +138,35 @@ export function isMainRuntime(): boolean { return getCurrentRuntime().isMain; } +function getRuntimeFunctionJsiBindings() { + const globals = globalThis as RuntimeFunctionJsiGlobal; + return { + registerRuntimeFunction: globals.__rnrRegisterRuntimeFunction, + callRuntimeFunction: globals.__rnrCallRuntimeFunction, + }; +} + +function hasRuntimeFunctionJsiBindings() { + const bindings = getRuntimeFunctionJsiBindings(); + return !!( + bindings.registerRuntimeFunction && bindings.callRuntimeFunction + ); +} + +function registerRuntimeFunctionBinding( + id: string, + loadFunction: RuntimeFunctionLoader, +) { + getRuntimeFunctionJsiBindings().registerRuntimeFunction?.(id, loadFunction); +} + +function callRuntimeFunctionBinding(functionId: string, argsJson: string) { + return getRuntimeFunctionJsiBindings().callRuntimeFunction?.( + functionId, + argsJson, + ); +} + function getRuntimeFunctionsNitro() { if (runtimeFunctionsNitro !== undefined) { return runtimeFunctionsNitro; @@ -181,11 +201,7 @@ function getRuntimeFunctionsNitro() { } function installRuntimeFunctionJsi() { - const globals = globalThis as RuntimeFunctionJsiGlobal; - if ( - globals.__rnrRegisterRuntimeFunction && - globals.__rnrCallRuntimeFunction - ) { + if (hasRuntimeFunctionJsiBindings()) { return; } @@ -308,10 +324,7 @@ export function registerRuntimeFunction( ) { installRuntimeFunctionJsi(); runtimeFunctions.set(id, loadFunction as RuntimeFunctionLoader); - (globalThis as RuntimeFunctionJsiGlobal).__rnrRegisterRuntimeFunction?.( - id, - loadFunction as RuntimeFunctionLoader, - ); + registerRuntimeFunctionBinding(id, loadFunction as RuntimeFunctionLoader); } function attachRuntimeFunction( @@ -607,10 +620,12 @@ function loadRegisteredRuntimeFunction(functionId: string) { function callRegisteredRuntimeFunction(functionId: string, argsJson: string) { installRuntimeFunctionJsi(); - const jsiCall = (globalThis as RuntimeFunctionJsiGlobal) - .__rnrCallRuntimeFunction; + const jsiCall = callRuntimeFunctionBinding; if (jsiCall) { - return jsiCall(functionId, argsJson); + const result = jsiCall(functionId, argsJson); + if (result !== undefined) { + return result; + } } let args: unknown[]; @@ -827,17 +842,11 @@ const registerCallableModule = ) => void; function installThreadedRuntimeEventEmitterFallback() { - const globals = globalThis as { - __THREADED_RUNTIME_EVENT_EMITTER_FALLBACK__?: boolean; - }; - if ( - Platform.OS !== 'ios' || - globals.__THREADED_RUNTIME_EVENT_EMITTER_FALLBACK__ - ) { + if (Platform.OS !== 'ios' || didInstallThreadedRuntimeEventEmitterFallback) { return; } - globals.__THREADED_RUNTIME_EVENT_EMITTER_FALLBACK__ = true; + didInstallThreadedRuntimeEventEmitterFallback = true; registerCallableModule('RCTEventEmitter', () => ({ receiveEvent() {}, receiveTouches() {}, diff --git a/website/docs/installation.md b/website/docs/installation.md index 042b196..9fe3679 100644 --- a/website/docs/installation.md +++ b/website/docs/installation.md @@ -124,7 +124,11 @@ Add the generated folder to `.gitignore`: Load the generated entry only inside threaded runtimes: ```js -if (global.__THREADED_RUNTIME_ENV__ || global._is_it_a_list_env === true) { +const { + isMainRuntime, +} = require('@react-native-runtimes/core'); + +if (!isMainRuntime()) { require('./.threaded-runtime/entry'); } ``` @@ -134,4 +138,20 @@ The generated entry registers lazy component loaders and the `ThreadedRuntimeHos For runtime-specific startup code, add root-level files named `index..ts`, for example `index.business-runtime.ts`. The generated entry emits static conditional requires for those files and matches `` -against `global.__THREADED_RUNTIME_ENV__.kind` and `.runtimeName`. +against `getCurrentRuntime().kind` and `.name`. + +## Runtime Globals + +Use the public runtime helpers in app code: + +```ts +import { getCurrentRuntime, isMainRuntime } from '@react-native-runtimes/core'; +``` + +`global.__THREADED_RUNTIME_ENV__` is the only supported runtime identity global. +It is installed before the secondary runtime bundle executes and currently +contains `kind` and `runtimeName`. + +`global.__rnrRegisterRuntimeFunction` and `global.__rnrCallRuntimeFunction` are +internal implementation details for runtime functions and should not be used by +app code.