@onekeyfe/react-native-background-thread 1.1.45 → 1.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/BackgroundThread.podspec +4 -2
  2. package/android/CMakeLists.txt +29 -0
  3. package/android/build.gradle +42 -0
  4. package/android/src/main/cpp/cpp-adapter.cpp +222 -0
  5. package/android/src/main/java/com/backgroundthread/BTLogger.kt +55 -0
  6. package/android/src/main/java/com/backgroundthread/BackgroundThreadManager.kt +223 -0
  7. package/android/src/main/java/com/backgroundthread/BackgroundThreadModule.kt +17 -17
  8. package/cpp/SharedRPC.cpp +223 -0
  9. package/cpp/SharedRPC.h +50 -0
  10. package/cpp/SharedStore.cpp +184 -0
  11. package/cpp/SharedStore.h +28 -0
  12. package/ios/BackgroundRunnerReactNativeDelegate.h +0 -17
  13. package/ios/BackgroundRunnerReactNativeDelegate.mm +30 -117
  14. package/ios/BackgroundThread.h +1 -0
  15. package/ios/BackgroundThread.mm +6 -22
  16. package/ios/BackgroundThreadManager.h +6 -12
  17. package/ios/BackgroundThreadManager.mm +38 -27
  18. package/lib/module/NativeBackgroundThread.js.map +1 -1
  19. package/lib/module/SharedRPC.js +6 -0
  20. package/lib/module/SharedRPC.js.map +1 -0
  21. package/lib/module/SharedStore.js +6 -0
  22. package/lib/module/SharedStore.js.map +1 -0
  23. package/lib/module/index.js +2 -0
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/typescript/src/NativeBackgroundThread.d.ts +2 -4
  26. package/lib/typescript/src/NativeBackgroundThread.d.ts.map +1 -1
  27. package/lib/typescript/src/SharedRPC.d.ts +12 -0
  28. package/lib/typescript/src/SharedRPC.d.ts.map +1 -0
  29. package/lib/typescript/src/SharedStore.d.ts +14 -0
  30. package/lib/typescript/src/SharedStore.d.ts.map +1 -0
  31. package/lib/typescript/src/index.d.ts +4 -0
  32. package/lib/typescript/src/index.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/NativeBackgroundThread.ts +2 -4
  35. package/src/SharedRPC.ts +16 -0
  36. package/src/SharedStore.ts +18 -0
  37. package/src/index.tsx +4 -0
@@ -13,8 +13,10 @@ Pod::Spec.new do |s|
13
13
  s.platforms = { :ios => min_ios_version_supported }
14
14
  s.source = { :git => "https://github.com/OneKeyHQ/app-modules/react-native-background-thread.git", :tag => "#{s.version}" }
15
15
 
16
- s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
- s.public_header_files = "ios/**/*.h"
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "cpp/**/*.{h,cpp}"
17
+ s.public_header_files = "ios/BackgroundThreadManager.h", "ios/BTLogger.h"
18
+ s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/cpp"', 'DEFINES_MODULE' => 'YES' }
19
+ s.user_target_xcconfig = { 'SWIFT_INCLUDE_PATHS' => '"$(PODS_ROOT)/Headers/Public/BackgroundThread"' }
18
20
 
19
21
  s.dependency 'ReactNativeNativeLogger'
20
22
 
@@ -0,0 +1,29 @@
1
+ cmake_minimum_required(VERSION 3.13)
2
+ project(background_thread)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+
7
+ find_package(fbjni REQUIRED CONFIG)
8
+ find_package(ReactAndroid REQUIRED CONFIG)
9
+
10
+ add_library(${PROJECT_NAME} SHARED
11
+ src/main/cpp/cpp-adapter.cpp
12
+ ../cpp/SharedStore.cpp
13
+ ../cpp/SharedRPC.cpp
14
+ )
15
+
16
+ target_include_directories(${PROJECT_NAME} PRIVATE
17
+ src/main/cpp
18
+ ../cpp
19
+ )
20
+
21
+ find_library(LOG_LIB log)
22
+
23
+ target_link_libraries(${PROJECT_NAME}
24
+ ${LOG_LIB}
25
+ fbjni::fbjni
26
+ ReactAndroid::jsi
27
+ ReactAndroid::reactnative
28
+ android
29
+ )
@@ -25,6 +25,11 @@ def getExtOrIntegerDefault(name) {
25
25
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["BackgroundThread_" + name]).toInteger()
26
26
  }
27
27
 
28
+ def reactNativeArchitectures() {
29
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
30
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
31
+ }
32
+
28
33
  android {
29
34
  namespace "com.backgroundthread"
30
35
 
@@ -33,10 +38,47 @@ android {
33
38
  defaultConfig {
34
39
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
35
40
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
41
+
42
+ externalNativeBuild {
43
+ cmake {
44
+ cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
45
+ arguments "-DANDROID_STL=c++_shared",
46
+ "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
47
+ abiFilters(*reactNativeArchitectures())
48
+ }
49
+ }
50
+ }
51
+
52
+ externalNativeBuild {
53
+ cmake {
54
+ path "CMakeLists.txt"
55
+ }
36
56
  }
37
57
 
38
58
  buildFeatures {
39
59
  buildConfig true
60
+ prefab true
61
+ }
62
+
63
+ packagingOptions {
64
+ excludes = [
65
+ "META-INF",
66
+ "META-INF/**",
67
+ "**/libc++_shared.so",
68
+ "**/libfbjni.so",
69
+ "**/libjsi.so",
70
+ "**/libfolly_json.so",
71
+ "**/libfolly_runtime.so",
72
+ "**/libglog.so",
73
+ "**/libhermes.so",
74
+ "**/libhermes-executor-debug.so",
75
+ "**/libhermes_executor.so",
76
+ "**/libreactnative.so",
77
+ "**/libreactnativejni.so",
78
+ "**/libturbomodulejsijni.so",
79
+ "**/libreact_nativemodule_core.so",
80
+ "**/libjscexecutor.so"
81
+ ]
40
82
  }
41
83
 
42
84
  buildTypes {
@@ -0,0 +1,222 @@
1
+ #include <jni.h>
2
+ #include <jsi/jsi.h>
3
+ #include <android/log.h>
4
+ #include <memory>
5
+ #include <mutex>
6
+ #include <string>
7
+ #include <unordered_map>
8
+
9
+ #include "SharedStore.h"
10
+ #include "SharedRPC.h"
11
+
12
+ #define LOG_TAG "BackgroundThread"
13
+ #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
14
+ #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
15
+
16
+ namespace jsi = facebook::jsi;
17
+
18
+ static JavaVM *gJavaVM = nullptr;
19
+
20
+ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
21
+ gJavaVM = vm;
22
+ return JNI_VERSION_1_6;
23
+ }
24
+
25
+ static JNIEnv *getJNIEnv() {
26
+ JNIEnv *env = nullptr;
27
+ if (gJavaVM) {
28
+ gJavaVM->AttachCurrentThread(&env, nullptr);
29
+ }
30
+ return env;
31
+ }
32
+
33
+ // Stub a JSI function on an object (replaces it with a no-op).
34
+ static void stubJsiFunction(jsi::Runtime &runtime, jsi::Object &object, const char *name) {
35
+ object.setProperty(
36
+ runtime,
37
+ name,
38
+ jsi::Function::createFromHostFunction(
39
+ runtime, jsi::PropNameID::forUtf8(runtime, name), 1,
40
+ [](auto &, const auto &, const auto *, size_t) {
41
+ return jsi::Value::undefined();
42
+ }));
43
+ }
44
+
45
+ static void invokeOptionalGlobalFunction(jsi::Runtime &runtime, const char *name) {
46
+ try {
47
+ auto fnValue = runtime.global().getProperty(runtime, name);
48
+ if (!fnValue.isObject() || !fnValue.asObject(runtime).isFunction(runtime)) {
49
+ return;
50
+ }
51
+
52
+ auto fn = fnValue.asObject(runtime).asFunction(runtime);
53
+ fn.call(runtime);
54
+ } catch (const jsi::JSError &e) {
55
+ LOGE("JSError calling global function %s: %s", name, e.getMessage().c_str());
56
+ } catch (const std::exception &e) {
57
+ LOGE("Error calling global function %s: %s", name, e.what());
58
+ }
59
+ }
60
+
61
+ // ── Pending work map for cross-runtime executor ───────────────────────
62
+ static std::mutex gWorkMutex;
63
+ static std::unordered_map<int64_t, std::function<void(jsi::Runtime &)>> gPendingWork;
64
+ static int64_t gNextWorkId = 0;
65
+
66
+ // Called from Kotlin after runOnJSQueueThread dispatches to the correct thread.
67
+ extern "C" JNIEXPORT void JNICALL
68
+ Java_com_backgroundthread_BackgroundThreadManager_nativeExecuteWork(
69
+ JNIEnv *env, jobject thiz, jlong runtimePtr, jlong workId) {
70
+ LOGI("nativeExecuteWork: runtimePtr=%ld, workId=%ld", (long)runtimePtr, (long)workId);
71
+ auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
72
+ if (!rt) return;
73
+
74
+ std::function<void(jsi::Runtime &)> work;
75
+ {
76
+ std::lock_guard<std::mutex> lock(gWorkMutex);
77
+ auto it = gPendingWork.find(workId);
78
+ if (it == gPendingWork.end()) return;
79
+ work = std::move(it->second);
80
+ gPendingWork.erase(it);
81
+ }
82
+ try {
83
+ work(*rt);
84
+ } catch (const jsi::JSError &e) {
85
+ LOGE("JSError in nativeExecuteWork: %s", e.getMessage().c_str());
86
+ } catch (const std::exception &e) {
87
+ LOGE("Error in nativeExecuteWork: %s", e.what());
88
+ }
89
+ }
90
+
91
+ // ── nativeInstallSharedBridge ───────────────────────────────────────────
92
+ // Install SharedStore and SharedRPC into a runtime.
93
+ extern "C" JNIEXPORT void JNICALL
94
+ Java_com_backgroundthread_BackgroundThreadManager_nativeInstallSharedBridge(
95
+ JNIEnv *env, jobject thiz, jlong runtimePtr, jboolean isMain) {
96
+
97
+ auto *rt = reinterpret_cast<jsi::Runtime *>(runtimePtr);
98
+ if (!rt) return;
99
+
100
+ SharedStore::install(*rt);
101
+
102
+ // Create executor that schedules work on this runtime's JS thread via Kotlin
103
+ jobject ref = env->NewGlobalRef(thiz);
104
+ bool capturedIsMain = static_cast<bool>(isMain);
105
+
106
+ RPCRuntimeExecutor executor = [ref, capturedIsMain](std::function<void(jsi::Runtime &)> work) {
107
+ JNIEnv *env = getJNIEnv();
108
+ if (!env || !ref) {
109
+ LOGE("executor: env=%p, ref=%p — aborting", env, ref);
110
+ return;
111
+ }
112
+
113
+ int64_t workId;
114
+ {
115
+ std::lock_guard<std::mutex> lock(gWorkMutex);
116
+ workId = gNextWorkId++;
117
+ gPendingWork[workId] = std::move(work);
118
+ }
119
+
120
+ jclass cls = env->GetObjectClass(ref);
121
+ jmethodID mid = env->GetMethodID(cls, "scheduleOnJSThread", "(ZJ)V");
122
+ if (mid) {
123
+ LOGI("executor: calling scheduleOnJSThread(isMain=%d, workId=%ld)", capturedIsMain, (long)workId);
124
+ env->CallVoidMethod(ref, mid, static_cast<jboolean>(capturedIsMain), static_cast<jlong>(workId));
125
+ if (env->ExceptionCheck()) {
126
+ LOGE("executor: JNI exception after scheduleOnJSThread");
127
+ env->ExceptionDescribe();
128
+ env->ExceptionClear();
129
+ }
130
+ } else {
131
+ LOGE("executor: scheduleOnJSThread method not found!");
132
+ if (env->ExceptionCheck()) {
133
+ env->ExceptionDescribe();
134
+ env->ExceptionClear();
135
+ }
136
+ }
137
+ env->DeleteLocalRef(cls);
138
+ };
139
+
140
+ std::string runtimeId = isMain ? "main" : "background";
141
+ SharedRPC::install(*rt, std::move(executor), runtimeId);
142
+ LOGI("SharedStore and SharedRPC installed (isMain=%d)", static_cast<int>(isMain));
143
+ if (!capturedIsMain) {
144
+ invokeOptionalGlobalFunction(*rt, "__setupBackgroundRPCHandler");
145
+ }
146
+ }
147
+
148
+ // ── nativeSetupErrorHandler ─────────────────────────────────────────────
149
+ // Wrap the global error handler in the background runtime.
150
+ // Mirrors iOS BackgroundRunnerReactNativeDelegate.setupErrorHandler:.
151
+ extern "C" JNIEXPORT void JNICALL
152
+ Java_com_backgroundthread_BackgroundThreadManager_nativeSetupErrorHandler(
153
+ JNIEnv *env, jobject thiz, jlong runtimePtr) {
154
+
155
+ auto *runtime = reinterpret_cast<jsi::Runtime *>(runtimePtr);
156
+ if (!runtime) return;
157
+
158
+ try {
159
+ jsi::Object global = runtime->global();
160
+ jsi::Value errorUtilsVal = global.getProperty(*runtime, "ErrorUtils");
161
+ if (!errorUtilsVal.isObject()) {
162
+ LOGE("ErrorUtils is not available on global object");
163
+ return;
164
+ }
165
+
166
+ jsi::Object errorUtils = errorUtilsVal.asObject(*runtime);
167
+
168
+ // Capture the current global error handler
169
+ auto originalHandler = std::make_shared<jsi::Value>(
170
+ errorUtils.getProperty(*runtime, "getGlobalHandler")
171
+ .asObject(*runtime).asFunction(*runtime).call(*runtime));
172
+
173
+ // Create a custom handler that delegates to the original
174
+ auto handlerFunc = jsi::Function::createFromHostFunction(
175
+ *runtime,
176
+ jsi::PropNameID::forAscii(*runtime, "customGlobalErrorHandler"),
177
+ 2,
178
+ [originalHandler](
179
+ jsi::Runtime &rt, const jsi::Value &,
180
+ const jsi::Value *args, size_t count) -> jsi::Value {
181
+ if (count < 2) {
182
+ return jsi::Value::undefined();
183
+ }
184
+
185
+ if (originalHandler->isObject() &&
186
+ originalHandler->asObject(rt).isFunction(rt)) {
187
+ jsi::Function original =
188
+ originalHandler->asObject(rt).asFunction(rt);
189
+ original.call(rt, args, count);
190
+ }
191
+
192
+ return jsi::Value::undefined();
193
+ });
194
+
195
+ // Set the new global error handler
196
+ jsi::Function setHandler =
197
+ errorUtils.getProperty(*runtime, "setGlobalHandler")
198
+ .asObject(*runtime).asFunction(*runtime);
199
+ setHandler.call(*runtime, {std::move(handlerFunc)});
200
+
201
+ // Disable further setGlobalHandler from background JS
202
+ stubJsiFunction(*runtime, errorUtils, "setGlobalHandler");
203
+
204
+ LOGI("Error handler installed in background runtime");
205
+ } catch (const jsi::JSError &e) {
206
+ LOGE("JSError setting up error handler: %s", e.getMessage().c_str());
207
+ } catch (const std::exception &e) {
208
+ LOGE("Error setting up error handler: %s", e.what());
209
+ }
210
+ }
211
+
212
+ // ── nativeDestroy ───────────────────────────────────────────────────────
213
+ // Clean up native resources.
214
+ // Called from BackgroundThreadManager.destroy().
215
+ extern "C" JNIEXPORT void JNICALL
216
+ Java_com_backgroundthread_BackgroundThreadManager_nativeDestroy(
217
+ JNIEnv *env, jobject thiz) {
218
+
219
+ SharedRPC::reset();
220
+
221
+ LOGI("Native resources cleaned up");
222
+ }
@@ -0,0 +1,55 @@
1
+ package com.backgroundthread
2
+
3
+ /**
4
+ * Lightweight logging wrapper that dynamically dispatches to OneKeyLog.
5
+ * Uses reflection to avoid a hard dependency on the native-logger module.
6
+ * Falls back to android.util.Log when OneKeyLog is not available.
7
+ *
8
+ * Mirrors iOS BTLogger.
9
+ */
10
+ object BTLogger {
11
+ private const val TAG = "BackgroundThread"
12
+
13
+ private val logClass: Class<*>? by lazy {
14
+ try {
15
+ Class.forName("com.margelo.nitro.nativelogger.OneKeyLog")
16
+ } catch (_: ClassNotFoundException) {
17
+ null
18
+ }
19
+ }
20
+
21
+ private val methods by lazy {
22
+ val cls = logClass ?: return@lazy null
23
+ mapOf(
24
+ "debug" to cls.getMethod("debug", String::class.java, String::class.java),
25
+ "info" to cls.getMethod("info", String::class.java, String::class.java),
26
+ "warn" to cls.getMethod("warn", String::class.java, String::class.java),
27
+ "error" to cls.getMethod("error", String::class.java, String::class.java),
28
+ )
29
+ }
30
+
31
+ @JvmStatic
32
+ fun debug(message: String) = log("debug", message, android.util.Log.DEBUG)
33
+
34
+ @JvmStatic
35
+ fun info(message: String) = log("info", message, android.util.Log.INFO)
36
+
37
+ @JvmStatic
38
+ fun warn(message: String) = log("warn", message, android.util.Log.WARN)
39
+
40
+ @JvmStatic
41
+ fun error(message: String) = log("error", message, android.util.Log.ERROR)
42
+
43
+ private fun log(level: String, message: String, androidLogLevel: Int) {
44
+ val method = methods?.get(level)
45
+ if (method != null) {
46
+ try {
47
+ method.invoke(null, TAG, message)
48
+ return
49
+ } catch (_: Exception) {
50
+ // Fall through to android.util.Log
51
+ }
52
+ }
53
+ android.util.Log.println(androidLogLevel, TAG, message)
54
+ }
55
+ }
@@ -0,0 +1,223 @@
1
+ package com.backgroundthread
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.proguard.annotations.DoNotStrip
5
+ import com.facebook.react.ReactInstanceEventListener
6
+ import com.facebook.react.bridge.JSBundleLoader
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.ReactContext
9
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
10
+ import com.facebook.react.defaults.DefaultComponentsRegistry
11
+ import com.facebook.react.defaults.DefaultReactHostDelegate
12
+ import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
13
+ import com.facebook.react.fabric.ComponentFactory
14
+ import com.facebook.react.runtime.ReactHostImpl
15
+ import com.facebook.react.runtime.hermes.HermesInstance
16
+ import com.facebook.react.shell.MainReactPackage
17
+
18
+ /**
19
+ * Singleton manager for the background React Native runtime.
20
+ * Mirrors iOS BackgroundThreadManager.
21
+ *
22
+ * Responsibilities:
23
+ * - Manages background ReactHostImpl lifecycle
24
+ * - Installs SharedBridge into main and background runtimes
25
+ * - Cross-runtime communication via SharedRPC onWrite notifications
26
+ */
27
+ class BackgroundThreadManager private constructor() {
28
+
29
+ private var bgReactHost: ReactHostImpl? = null
30
+ private var reactPackages: List<ReactPackage> = emptyList()
31
+
32
+ @Volatile
33
+ private var bgRuntimePtr: Long = 0
34
+
35
+ @Volatile
36
+ private var mainRuntimePtr: Long = 0
37
+ private var mainReactContext: ReactApplicationContext? = null
38
+ private var isStarted = false
39
+
40
+ companion object {
41
+ private const val MODULE_NAME = "background"
42
+
43
+ init {
44
+ System.loadLibrary("background_thread")
45
+ }
46
+
47
+ @Volatile
48
+ private var instance: BackgroundThreadManager? = null
49
+
50
+ @JvmStatic
51
+ fun getInstance(): BackgroundThreadManager {
52
+ return instance ?: synchronized(this) {
53
+ instance ?: BackgroundThreadManager().also { instance = it }
54
+ }
55
+ }
56
+ }
57
+
58
+ // ── JNI declarations ────────────────────────────────────────────────────
59
+
60
+ private external fun nativeInstallSharedBridge(runtimePtr: Long, isMain: Boolean)
61
+ private external fun nativeSetupErrorHandler(runtimePtr: Long)
62
+ private external fun nativeDestroy()
63
+ private external fun nativeExecuteWork(runtimePtr: Long, workId: Long)
64
+
65
+ // ── SharedBridge ────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Install SharedBridge HostObject into the main (UI) runtime.
69
+ * Call this from installSharedBridge().
70
+ */
71
+ fun setReactPackages(packages: List<ReactPackage>) {
72
+ reactPackages = packages.toList()
73
+ }
74
+
75
+ fun installSharedBridgeInMainRuntime(context: ReactApplicationContext) {
76
+ mainReactContext = context
77
+ context.runOnJSQueueThread {
78
+ try {
79
+ val ptr = context.javaScriptContextHolder?.get() ?: 0L
80
+ if (ptr != 0L) {
81
+ mainRuntimePtr = ptr
82
+ nativeInstallSharedBridge(ptr, true)
83
+ BTLogger.info("SharedBridge installed in main runtime")
84
+ } else {
85
+ BTLogger.warn("Main runtime pointer is 0, cannot install SharedBridge")
86
+ }
87
+ } catch (e: Exception) {
88
+ BTLogger.error("Error installing SharedBridge in main runtime: ${e.message}")
89
+ }
90
+ }
91
+ }
92
+
93
+ // ── Background runner lifecycle ─────────────────────────────────────────
94
+
95
+ @OptIn(UnstableReactNativeAPI::class)
96
+ fun startBackgroundRunnerWithEntryURL(context: ReactApplicationContext, entryURL: String) {
97
+ if (isStarted) {
98
+ BTLogger.warn("Background runner already started")
99
+ return
100
+ }
101
+ isStarted = true
102
+ BTLogger.info("Starting background runner with entryURL: $entryURL")
103
+
104
+ val appContext = context.applicationContext
105
+ val packages =
106
+ if (reactPackages.isNotEmpty()) {
107
+ reactPackages
108
+ } else {
109
+ BTLogger.warn("No ReactPackages registered for background runtime; falling back to MainReactPackage only")
110
+ listOf(MainReactPackage())
111
+ }
112
+
113
+ val bundleLoader = if (entryURL.startsWith("http")) {
114
+ // Dev server: download bundle to temp file first, then load from file.
115
+ // loadScriptFromFile only accepts local file paths, not HTTP URLs.
116
+ object : JSBundleLoader() {
117
+ override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
118
+ val tempFile = java.io.File(appContext.cacheDir, "background.bundle")
119
+ try {
120
+ java.net.URL(entryURL).openStream().use { input ->
121
+ tempFile.outputStream().use { output ->
122
+ input.copyTo(output)
123
+ }
124
+ }
125
+ BTLogger.info("Background bundle downloaded to ${tempFile.absolutePath}")
126
+ } catch (e: Exception) {
127
+ BTLogger.error("Failed to download background bundle: ${e.message}")
128
+ throw RuntimeException("Failed to download background bundle from $entryURL", e)
129
+ }
130
+ delegate.loadScriptFromFile(tempFile.absolutePath, entryURL, false)
131
+ return entryURL
132
+ }
133
+ }
134
+ } else {
135
+ JSBundleLoader.createAssetLoader(appContext, "assets://$entryURL", true)
136
+ }
137
+
138
+ val delegate = DefaultReactHostDelegate(
139
+ jsMainModulePath = MODULE_NAME,
140
+ jsBundleLoader = bundleLoader,
141
+ reactPackages = packages,
142
+ jsRuntimeFactory = HermesInstance(),
143
+ turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
144
+ )
145
+
146
+ val componentFactory = ComponentFactory()
147
+ DefaultComponentsRegistry.register(componentFactory)
148
+
149
+ val host = ReactHostImpl(
150
+ appContext,
151
+ delegate,
152
+ componentFactory,
153
+ true, /* allowPackagerServerAccess */
154
+ false, /* useDevSupport */
155
+ )
156
+ bgReactHost = host
157
+
158
+ host.addReactInstanceEventListener(object : ReactInstanceEventListener {
159
+ override fun onReactContextInitialized(context: ReactContext) {
160
+ BTLogger.info("Background ReactContext initialized")
161
+ context.runOnJSQueueThread {
162
+ try {
163
+ val ptr = context.javaScriptContextHolder?.get() ?: 0L
164
+ if (ptr != 0L) {
165
+ bgRuntimePtr = ptr
166
+ nativeInstallSharedBridge(ptr, false)
167
+ nativeSetupErrorHandler(ptr)
168
+ BTLogger.info("SharedBridge and error handler installed in background runtime")
169
+ } else {
170
+ BTLogger.error("Background runtime pointer is 0")
171
+ }
172
+ } catch (e: Exception) {
173
+ BTLogger.error("Error installing bindings in background runtime: ${e.message}")
174
+ }
175
+ }
176
+ }
177
+ })
178
+
179
+ host.start()
180
+ }
181
+
182
+ /**
183
+ * Called from C++ RuntimeExecutor to schedule work on the correct JS thread.
184
+ * Routes to main or background runtime's JS queue thread, then calls nativeExecuteWork.
185
+ */
186
+ @DoNotStrip
187
+ fun scheduleOnJSThread(isMain: Boolean, workId: Long) {
188
+ val context = if (isMain) mainReactContext else bgReactHost?.currentReactContext
189
+ BTLogger.info("scheduleOnJSThread: isMain=$isMain, workId=$workId, context=${context != null}")
190
+ if (context == null) {
191
+ BTLogger.error("scheduleOnJSThread: context is null! isMain=$isMain, mainCtx=${mainReactContext != null}, bgHost=${bgReactHost != null}, bgCtx=${bgReactHost?.currentReactContext != null}")
192
+ }
193
+ context?.runOnJSQueueThread {
194
+ // Re-read ptr inside the block — if a reload happened between
195
+ // scheduling and execution, the old ptr may be stale.
196
+ val ptr = if (isMain) mainRuntimePtr else bgRuntimePtr
197
+ BTLogger.info("scheduleOnJSThread runOnJSQueueThread: isMain=$isMain, workId=$workId, ptr=$ptr")
198
+ if (ptr != 0L) {
199
+ try {
200
+ nativeExecuteWork(ptr, workId)
201
+ } catch (e: Exception) {
202
+ BTLogger.error("Error executing work on JS thread: ${e.message}")
203
+ }
204
+ } else {
205
+ BTLogger.error("scheduleOnJSThread: ptr is 0! isMain=$isMain")
206
+ }
207
+ }
208
+ }
209
+
210
+ // ── Lifecycle ───────────────────────────────────────────────────────────
211
+
212
+ val isBackgroundStarted: Boolean get() = isStarted
213
+
214
+ fun destroy() {
215
+ nativeDestroy()
216
+ bgRuntimePtr = 0
217
+ mainRuntimePtr = 0
218
+ mainReactContext = null
219
+ bgReactHost?.destroy("BackgroundThreadManager destroyed", null)
220
+ bgReactHost = null
221
+ isStarted = false
222
+ }
223
+ }
@@ -3,27 +3,27 @@ package com.backgroundthread
3
3
  import com.facebook.react.bridge.ReactApplicationContext
4
4
  import com.facebook.react.module.annotations.ReactModule
5
5
 
6
+ /**
7
+ * TurboModule entry point for BackgroundThread.
8
+ * Delegates all heavy lifting to [BackgroundThreadManager] singleton.
9
+ *
10
+ * Mirrors iOS BackgroundThread.mm.
11
+ */
6
12
  @ReactModule(name = BackgroundThreadModule.NAME)
7
13
  class BackgroundThreadModule(reactContext: ReactApplicationContext) :
8
- NativeBackgroundThreadSpec(reactContext) {
14
+ NativeBackgroundThreadSpec(reactContext) {
9
15
 
10
- override fun getName(): String {
11
- return NAME
12
- }
16
+ companion object {
17
+ const val NAME = "BackgroundThread"
18
+ }
13
19
 
14
- override fun initBackgroundThread() {
15
- // TODO: Implement initBackgroundThread
16
- }
20
+ override fun getName(): String = NAME
17
21
 
18
- override fun postBackgroundMessage(message: String) {
19
- // TODO: Implement postBackgroundMessage
20
- }
22
+ override fun installSharedBridge() {
23
+ BackgroundThreadManager.getInstance().installSharedBridgeInMainRuntime(reactApplicationContext)
24
+ }
21
25
 
22
- override fun startBackgroundRunnerWithEntryURL(entryURL: String) {
23
- // TODO: Implement startBackgroundRunnerWithEntryURL
24
- }
25
-
26
- companion object {
27
- const val NAME = "BackgroundThread"
28
- }
26
+ override fun startBackgroundRunnerWithEntryURL(entryURL: String) {
27
+ BackgroundThreadManager.getInstance().startBackgroundRunnerWithEntryURL(reactApplicationContext, entryURL)
28
+ }
29
29
  }