@onekeyfe/react-native-background-thread 1.1.46 → 1.1.48

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 +258 -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 +83 -123
  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 +39 -30
  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,258 @@
1
+ package com.backgroundthread
2
+
3
+ import android.net.Uri
4
+ import com.facebook.react.ReactPackage
5
+ import com.facebook.proguard.annotations.DoNotStrip
6
+ import com.facebook.react.ReactInstanceEventListener
7
+ import com.facebook.react.bridge.JSBundleLoader
8
+ import com.facebook.react.bridge.ReactApplicationContext
9
+ import com.facebook.react.bridge.ReactContext
10
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
11
+ import com.facebook.react.defaults.DefaultComponentsRegistry
12
+ import com.facebook.react.defaults.DefaultReactHostDelegate
13
+ import com.facebook.react.defaults.DefaultTurboModuleManagerDelegate
14
+ import com.facebook.react.fabric.ComponentFactory
15
+ import com.facebook.react.runtime.ReactHostImpl
16
+ import com.facebook.react.runtime.hermes.HermesInstance
17
+ import com.facebook.react.shell.MainReactPackage
18
+ import java.io.File
19
+
20
+ /**
21
+ * Singleton manager for the background React Native runtime.
22
+ * Mirrors iOS BackgroundThreadManager.
23
+ *
24
+ * Responsibilities:
25
+ * - Manages background ReactHostImpl lifecycle
26
+ * - Installs SharedBridge into main and background runtimes
27
+ * - Cross-runtime communication via SharedRPC onWrite notifications
28
+ */
29
+ class BackgroundThreadManager private constructor() {
30
+
31
+ private var bgReactHost: ReactHostImpl? = null
32
+ private var reactPackages: List<ReactPackage> = emptyList()
33
+
34
+ @Volatile
35
+ private var bgRuntimePtr: Long = 0
36
+
37
+ @Volatile
38
+ private var mainRuntimePtr: Long = 0
39
+ private var mainReactContext: ReactApplicationContext? = null
40
+ private var isStarted = false
41
+
42
+ companion object {
43
+ private const val MODULE_NAME = "background"
44
+
45
+ init {
46
+ System.loadLibrary("background_thread")
47
+ }
48
+
49
+ @Volatile
50
+ private var instance: BackgroundThreadManager? = null
51
+
52
+ @JvmStatic
53
+ fun getInstance(): BackgroundThreadManager {
54
+ return instance ?: synchronized(this) {
55
+ instance ?: BackgroundThreadManager().also { instance = it }
56
+ }
57
+ }
58
+ }
59
+
60
+ // ── JNI declarations ────────────────────────────────────────────────────
61
+
62
+ private external fun nativeInstallSharedBridge(runtimePtr: Long, isMain: Boolean)
63
+ private external fun nativeSetupErrorHandler(runtimePtr: Long)
64
+ private external fun nativeDestroy()
65
+ private external fun nativeExecuteWork(runtimePtr: Long, workId: Long)
66
+
67
+ // ── SharedBridge ────────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Install SharedBridge HostObject into the main (UI) runtime.
71
+ * Call this from installSharedBridge().
72
+ */
73
+ fun setReactPackages(packages: List<ReactPackage>) {
74
+ reactPackages = packages.toList()
75
+ }
76
+
77
+ fun installSharedBridgeInMainRuntime(context: ReactApplicationContext) {
78
+ mainReactContext = context
79
+ context.runOnJSQueueThread {
80
+ try {
81
+ val ptr = context.javaScriptContextHolder?.get() ?: 0L
82
+ if (ptr != 0L) {
83
+ mainRuntimePtr = ptr
84
+ nativeInstallSharedBridge(ptr, true)
85
+ BTLogger.info("SharedBridge installed in main runtime")
86
+ } else {
87
+ BTLogger.warn("Main runtime pointer is 0, cannot install SharedBridge")
88
+ }
89
+ } catch (e: Exception) {
90
+ BTLogger.error("Error installing SharedBridge in main runtime: ${e.message}")
91
+ }
92
+ }
93
+ }
94
+
95
+ // ── Background runner lifecycle ─────────────────────────────────────────
96
+
97
+ private fun isRemoteBundleUrl(entryURL: String): Boolean {
98
+ return entryURL.startsWith("http://") || entryURL.startsWith("https://")
99
+ }
100
+
101
+ private fun resolveLocalBundlePath(entryURL: String): String? {
102
+ if (entryURL.startsWith("file://")) {
103
+ return Uri.parse(entryURL).path
104
+ }
105
+ if (entryURL.startsWith("/")) {
106
+ return entryURL
107
+ }
108
+ return null
109
+ }
110
+
111
+ private fun createDownloadedBundleLoader(appContext: android.content.Context, entryURL: String): JSBundleLoader {
112
+ return object : JSBundleLoader() {
113
+ override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
114
+ val tempFile = File(appContext.cacheDir, "background.bundle")
115
+ try {
116
+ java.net.URL(entryURL).openStream().use { input ->
117
+ tempFile.outputStream().use { output ->
118
+ input.copyTo(output)
119
+ }
120
+ }
121
+ BTLogger.info("Background bundle downloaded to ${tempFile.absolutePath}")
122
+ } catch (e: Exception) {
123
+ BTLogger.error("Failed to download background bundle: ${e.message}")
124
+ throw RuntimeException("Failed to download background bundle from $entryURL", e)
125
+ }
126
+ delegate.loadScriptFromFile(tempFile.absolutePath, entryURL, false)
127
+ return entryURL
128
+ }
129
+ }
130
+ }
131
+
132
+ private fun createLocalFileBundleLoader(localPath: String, sourceURL: String): JSBundleLoader {
133
+ return object : JSBundleLoader() {
134
+ override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
135
+ val bundleFile = File(localPath)
136
+ if (!bundleFile.exists()) {
137
+ BTLogger.error("Background bundle file does not exist: $localPath")
138
+ throw RuntimeException("Background bundle file does not exist: $localPath")
139
+ }
140
+ delegate.loadScriptFromFile(bundleFile.absolutePath, sourceURL, false)
141
+ return sourceURL
142
+ }
143
+ }
144
+ }
145
+
146
+ @OptIn(UnstableReactNativeAPI::class)
147
+ fun startBackgroundRunnerWithEntryURL(context: ReactApplicationContext, entryURL: String) {
148
+ if (isStarted) {
149
+ BTLogger.warn("Background runner already started")
150
+ return
151
+ }
152
+ BTLogger.info("Starting background runner with entryURL: $entryURL")
153
+
154
+ val appContext = context.applicationContext
155
+ val packages =
156
+ if (reactPackages.isNotEmpty()) {
157
+ reactPackages
158
+ } else {
159
+ BTLogger.warn("No ReactPackages registered for background runtime; call setReactPackages(...) from host before start. Falling back to MainReactPackage only.")
160
+ listOf(MainReactPackage())
161
+ }
162
+
163
+ val localBundlePath = resolveLocalBundlePath(entryURL)
164
+ val bundleLoader =
165
+ when {
166
+ isRemoteBundleUrl(entryURL) -> createDownloadedBundleLoader(appContext, entryURL)
167
+ localBundlePath != null -> createLocalFileBundleLoader(localBundlePath, entryURL)
168
+ entryURL.startsWith("assets://") -> JSBundleLoader.createAssetLoader(appContext, entryURL, true)
169
+ else -> JSBundleLoader.createAssetLoader(appContext, "assets://$entryURL", true)
170
+ }
171
+
172
+ val delegate = DefaultReactHostDelegate(
173
+ jsMainModulePath = MODULE_NAME,
174
+ jsBundleLoader = bundleLoader,
175
+ reactPackages = packages,
176
+ jsRuntimeFactory = HermesInstance(),
177
+ turboModuleManagerDelegateBuilder = DefaultTurboModuleManagerDelegate.Builder(),
178
+ )
179
+
180
+ val componentFactory = ComponentFactory()
181
+ DefaultComponentsRegistry.register(componentFactory)
182
+
183
+ val host = ReactHostImpl(
184
+ appContext,
185
+ delegate,
186
+ componentFactory,
187
+ true, /* allowPackagerServerAccess */
188
+ false, /* useDevSupport */
189
+ )
190
+ bgReactHost = host
191
+
192
+ host.addReactInstanceEventListener(object : ReactInstanceEventListener {
193
+ override fun onReactContextInitialized(context: ReactContext) {
194
+ BTLogger.info("Background ReactContext initialized")
195
+ context.runOnJSQueueThread {
196
+ try {
197
+ val ptr = context.javaScriptContextHolder?.get() ?: 0L
198
+ if (ptr != 0L) {
199
+ bgRuntimePtr = ptr
200
+ nativeInstallSharedBridge(ptr, false)
201
+ nativeSetupErrorHandler(ptr)
202
+ BTLogger.info("SharedBridge and error handler installed in background runtime")
203
+ } else {
204
+ BTLogger.error("Background runtime pointer is 0")
205
+ }
206
+ } catch (e: Exception) {
207
+ BTLogger.error("Error installing bindings in background runtime: ${e.message}")
208
+ }
209
+ }
210
+ }
211
+ })
212
+
213
+ host.start()
214
+ isStarted = true
215
+ }
216
+
217
+ /**
218
+ * Called from C++ RuntimeExecutor to schedule work on the correct JS thread.
219
+ * Routes to main or background runtime's JS queue thread, then calls nativeExecuteWork.
220
+ */
221
+ @DoNotStrip
222
+ fun scheduleOnJSThread(isMain: Boolean, workId: Long) {
223
+ val context = if (isMain) mainReactContext else bgReactHost?.currentReactContext
224
+ BTLogger.info("scheduleOnJSThread: isMain=$isMain, workId=$workId, context=${context != null}")
225
+ if (context == null) {
226
+ BTLogger.error("scheduleOnJSThread: context is null! isMain=$isMain, mainCtx=${mainReactContext != null}, bgHost=${bgReactHost != null}, bgCtx=${bgReactHost?.currentReactContext != null}")
227
+ }
228
+ context?.runOnJSQueueThread {
229
+ // Re-read ptr inside the block — if a reload happened between
230
+ // scheduling and execution, the old ptr may be stale.
231
+ val ptr = if (isMain) mainRuntimePtr else bgRuntimePtr
232
+ BTLogger.info("scheduleOnJSThread runOnJSQueueThread: isMain=$isMain, workId=$workId, ptr=$ptr")
233
+ if (ptr != 0L) {
234
+ try {
235
+ nativeExecuteWork(ptr, workId)
236
+ } catch (e: Exception) {
237
+ BTLogger.error("Error executing work on JS thread: ${e.message}")
238
+ }
239
+ } else {
240
+ BTLogger.error("scheduleOnJSThread: ptr is 0! isMain=$isMain")
241
+ }
242
+ }
243
+ }
244
+
245
+ // ── Lifecycle ───────────────────────────────────────────────────────────
246
+
247
+ val isBackgroundStarted: Boolean get() = isStarted
248
+
249
+ fun destroy() {
250
+ nativeDestroy()
251
+ bgRuntimePtr = 0
252
+ mainRuntimePtr = 0
253
+ mainReactContext = null
254
+ bgReactHost?.destroy("BackgroundThreadManager destroyed", null)
255
+ bgReactHost = null
256
+ isStarted = false
257
+ }
258
+ }
@@ -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
  }