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

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.
@@ -1,5 +1,6 @@
1
1
  package com.backgroundthread
2
2
 
3
+ import android.net.Uri
3
4
  import com.facebook.react.ReactPackage
4
5
  import com.facebook.proguard.annotations.DoNotStrip
5
6
  import com.facebook.react.ReactInstanceEventListener
@@ -14,6 +15,7 @@ import com.facebook.react.fabric.ComponentFactory
14
15
  import com.facebook.react.runtime.ReactHostImpl
15
16
  import com.facebook.react.runtime.hermes.HermesInstance
16
17
  import com.facebook.react.shell.MainReactPackage
18
+ import java.io.File
17
19
 
18
20
  /**
19
21
  * Singleton manager for the background React Native runtime.
@@ -92,13 +94,61 @@ class BackgroundThreadManager private constructor() {
92
94
 
93
95
  // ── Background runner lifecycle ─────────────────────────────────────────
94
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
+
95
146
  @OptIn(UnstableReactNativeAPI::class)
96
147
  fun startBackgroundRunnerWithEntryURL(context: ReactApplicationContext, entryURL: String) {
97
148
  if (isStarted) {
98
149
  BTLogger.warn("Background runner already started")
99
150
  return
100
151
  }
101
- isStarted = true
102
152
  BTLogger.info("Starting background runner with entryURL: $entryURL")
103
153
 
104
154
  val appContext = context.applicationContext
@@ -106,34 +156,18 @@ class BackgroundThreadManager private constructor() {
106
156
  if (reactPackages.isNotEmpty()) {
107
157
  reactPackages
108
158
  } else {
109
- BTLogger.warn("No ReactPackages registered for background runtime; falling back to MainReactPackage only")
159
+ BTLogger.warn("No ReactPackages registered for background runtime; call setReactPackages(...) from host before start. Falling back to MainReactPackage only.")
110
160
  listOf(MainReactPackage())
111
161
  }
112
162
 
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
- }
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)
133
170
  }
134
- } else {
135
- JSBundleLoader.createAssetLoader(appContext, "assets://$entryURL", true)
136
- }
137
171
 
138
172
  val delegate = DefaultReactHostDelegate(
139
173
  jsMainModulePath = MODULE_NAME,
@@ -177,6 +211,7 @@ class BackgroundThreadManager private constructor() {
177
211
  })
178
212
 
179
213
  host.start()
214
+ isStarted = true
180
215
  }
181
216
 
182
217
  /**
@@ -207,6 +242,45 @@ class BackgroundThreadManager private constructor() {
207
242
  }
208
243
  }
209
244
 
245
+ // ── Segment Registration (Phase 2.5 spike) ─────────────────────────────
246
+
247
+ /**
248
+ * Register a HBC segment in the background runtime.
249
+ * Uses CatalystInstance.registerSegment() on the background ReactContext.
250
+ *
251
+ * @param segmentId The segment ID to register
252
+ * @param path Absolute file path to the .seg.hbc file
253
+ * @throws IllegalStateException if background runtime is not started
254
+ * @throws IllegalArgumentException if segment file does not exist
255
+ */
256
+ fun registerSegmentInBackground(segmentId: Int, path: String) {
257
+ if (!isStarted) {
258
+ throw IllegalStateException("Background runtime not started")
259
+ }
260
+
261
+ val file = File(path)
262
+ if (!file.exists()) {
263
+ throw IllegalArgumentException("Segment file not found: $path")
264
+ }
265
+
266
+ val context = bgReactHost?.currentReactContext
267
+ ?: throw IllegalStateException("Background ReactContext not available")
268
+
269
+ context.runOnJSQueueThread {
270
+ try {
271
+ if (context.hasCatalystInstance()) {
272
+ context.catalystInstance.registerSegment(segmentId, path)
273
+ BTLogger.info("Segment registered in background runtime: id=$segmentId, path=$path")
274
+ } else {
275
+ BTLogger.error("Background CatalystInstance not available for segment registration")
276
+ }
277
+ } catch (e: Exception) {
278
+ BTLogger.error("Failed to register segment in background runtime: ${e.message}")
279
+ throw e
280
+ }
281
+ }
282
+ }
283
+
210
284
  // ── Lifecycle ───────────────────────────────────────────────────────────
211
285
 
212
286
  val isBackgroundStarted: Boolean get() = isStarted
@@ -1,5 +1,6 @@
1
1
  package com.backgroundthread
2
2
 
3
+ import com.facebook.react.bridge.Promise
3
4
  import com.facebook.react.bridge.ReactApplicationContext
4
5
  import com.facebook.react.module.annotations.ReactModule
5
6
 
@@ -26,4 +27,14 @@ class BackgroundThreadModule(reactContext: ReactApplicationContext) :
26
27
  override fun startBackgroundRunnerWithEntryURL(entryURL: String) {
27
28
  BackgroundThreadManager.getInstance().startBackgroundRunnerWithEntryURL(reactApplicationContext, entryURL)
28
29
  }
30
+
31
+ override fun loadSegmentInBackground(segmentId: Double, path: String, promise: Promise) {
32
+ try {
33
+ BackgroundThreadManager.getInstance()
34
+ .registerSegmentInBackground(segmentId.toInt(), path)
35
+ promise.resolve(null)
36
+ } catch (e: Exception) {
37
+ promise.reject("BG_SEGMENT_LOAD_ERROR", e.message, e)
38
+ }
39
+ }
29
40
  }
@@ -31,6 +31,13 @@ NS_ASSUME_NONNULL_BEGIN
31
31
  */
32
32
  - (instancetype)init;
33
33
 
34
+ /**
35
+ * Register a HBC segment in the background runtime (Phase 2.5 spike).
36
+ * Uses RCTInstance's registerSegmentWithId:path: API.
37
+ * Must be called after hostDidStart: has completed.
38
+ */
39
+ - (BOOL)registerSegmentWithId:(NSNumber *)segmentId path:(NSString *)path;
40
+
34
41
  @end
35
42
 
36
43
  NS_ASSUME_NONNULL_END
@@ -71,6 +71,51 @@ static void invokeOptionalGlobalFunction(jsi::Runtime &runtime, const char *name
71
71
  }
72
72
  }
73
73
 
74
+ static NSURL *resolveMainBundleResourceURL(NSString *resourceName)
75
+ {
76
+ if (resourceName.length == 0) {
77
+ return nil;
78
+ }
79
+
80
+ NSURL *directURL = [[NSBundle mainBundle] URLForResource:resourceName withExtension:nil];
81
+ if (directURL) {
82
+ return directURL;
83
+ }
84
+
85
+ NSString *normalizedName = [resourceName hasPrefix:@"/"]
86
+ ? resourceName.lastPathComponent
87
+ : resourceName;
88
+ NSString *extension = normalizedName.pathExtension;
89
+ NSString *baseName = normalizedName.stringByDeletingPathExtension;
90
+ if (baseName.length == 0) {
91
+ return nil;
92
+ }
93
+
94
+ return [[NSBundle mainBundle] URLForResource:baseName
95
+ withExtension:extension.length > 0 ? extension : nil];
96
+ }
97
+
98
+ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
99
+ {
100
+ if (jsBundleSourceNS.length == 0) {
101
+ return nil;
102
+ }
103
+
104
+ NSURL *parsedURL = [NSURL URLWithString:jsBundleSourceNS];
105
+ if (parsedURL.scheme.length > 0) {
106
+ if (parsedURL.isFileURL && parsedURL.path.length > 0) {
107
+ return [NSURL fileURLWithPath:parsedURL.path];
108
+ }
109
+ return parsedURL;
110
+ }
111
+
112
+ if ([jsBundleSourceNS hasPrefix:@"/"]) {
113
+ return [NSURL fileURLWithPath:jsBundleSourceNS];
114
+ }
115
+
116
+ return resolveMainBundleResourceURL(jsBundleSourceNS);
117
+ }
118
+
74
119
  @interface BackgroundReactNativeDelegate () {
75
120
  RCTInstance *_rctInstance;
76
121
  std::string _origin;
@@ -127,17 +172,19 @@ static void invokeOptionalGlobalFunction(jsi::Runtime &runtime, const char *name
127
172
  {
128
173
  if (!_jsBundleSource.empty()) {
129
174
  NSString *jsBundleSourceNS = [NSString stringWithUTF8String:_jsBundleSource.c_str()];
130
- NSURL *url = [NSURL URLWithString:jsBundleSourceNS];
131
- if (url && url.scheme) {
132
- return url;
175
+ NSURL *resolvedURL = resolveBundleSourceURL(jsBundleSourceNS);
176
+ if (resolvedURL) {
177
+ return resolvedURL;
133
178
  }
134
179
 
135
- if ([jsBundleSourceNS hasSuffix:@".jsbundle"]) {
136
- return [[NSBundle mainBundle] URLForResource:jsBundleSourceNS withExtension:nil];
137
- }
180
+ [BTLogger warn:[NSString stringWithFormat:@"Unable to resolve custom jsBundleSource=%@", jsBundleSourceNS]];
138
181
  }
139
182
 
140
- return [[NSBundle mainBundle] URLForResource: @"background" withExtension: @"bundle"];
183
+ NSURL *defaultBundleURL = resolveMainBundleResourceURL(@"background.bundle");
184
+ if (defaultBundleURL) {
185
+ return defaultBundleURL;
186
+ }
187
+ return [[NSBundle mainBundle] URLForResource:@"background" withExtension:@"bundle"];
141
188
  }
142
189
 
143
190
  - (void)hostDidStart:(RCTHost *)host
@@ -175,6 +222,34 @@ static void invokeOptionalGlobalFunction(jsi::Runtime &runtime, const char *name
175
222
  }];
176
223
  }
177
224
 
225
+ #pragma mark - Segment Registration (Phase 2.5 spike)
226
+
227
+ - (BOOL)registerSegmentWithId:(NSNumber *)segmentId path:(NSString *)path
228
+ {
229
+ if (!_rctInstance) {
230
+ [BTLogger error:@"Cannot register segment: background RCTInstance not available"];
231
+ return NO;
232
+ }
233
+
234
+ @try {
235
+ SEL sel = NSSelectorFromString(@"registerSegmentWithId:path:");
236
+ if ([_rctInstance respondsToSelector:sel]) {
237
+ #pragma clang diagnostic push
238
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
239
+ [_rctInstance performSelector:sel withObject:segmentId withObject:path];
240
+ #pragma clang diagnostic pop
241
+ [BTLogger info:[NSString stringWithFormat:@"Segment registered in background runtime: id=%@, path=%@", segmentId, path]];
242
+ return YES;
243
+ } else {
244
+ [BTLogger error:@"RCTInstance does not respond to registerSegmentWithId:path:"];
245
+ return NO;
246
+ }
247
+ } @catch (NSException *exception) {
248
+ [BTLogger error:[NSString stringWithFormat:@"Failed to register segment: %@", exception.reason]];
249
+ return NO;
250
+ }
251
+ }
252
+
178
253
  #pragma mark - RCTTurboModuleManagerDelegate
179
254
 
180
255
  - (id<RCTModuleProvider>)getModuleProvider:(const char *)name
@@ -5,5 +5,9 @@
5
5
  - (void)startBackgroundRunner;
6
6
  - (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL;
7
7
  - (void)installSharedBridge;
8
+ - (void)loadSegmentInBackground:(double)segmentId
9
+ path:(NSString *)path
10
+ resolve:(RCTPromiseResolveBlock)resolve
11
+ reject:(RCTPromiseRejectBlock)reject;
8
12
 
9
13
  @end
@@ -29,6 +29,22 @@
29
29
  [BTLogger info:@"installSharedBridge called (no-op on iOS, installed from AppDelegate)"];
30
30
  }
31
31
 
32
+ - (void)loadSegmentInBackground:(double)segmentId
33
+ path:(NSString *)path
34
+ resolve:(RCTPromiseResolveBlock)resolve
35
+ reject:(RCTPromiseRejectBlock)reject {
36
+ BackgroundThreadManager *manager = [BackgroundThreadManager sharedInstance];
37
+ [manager registerSegmentInBackground:@((int)segmentId)
38
+ path:path
39
+ completion:^(NSError * _Nullable error) {
40
+ if (error) {
41
+ reject(@"BG_SEGMENT_LOAD_ERROR", error.localizedDescription, error);
42
+ } else {
43
+ resolve(nil);
44
+ }
45
+ }];
46
+ }
47
+
32
48
  + (NSString *)moduleName
33
49
  {
34
50
  return @"BackgroundThread";
@@ -26,6 +26,14 @@ NS_ASSUME_NONNULL_BEGIN
26
26
  /// Check if background runner is started
27
27
  @property (nonatomic, readonly) BOOL isStarted;
28
28
 
29
+ /// Register a HBC segment in the background runtime (Phase 2.5 spike)
30
+ /// @param segmentId The segment ID to register
31
+ /// @param path Absolute file path to the .seg.hbc file
32
+ /// @param completion Callback with nil error on success, or NSError on failure
33
+ - (void)registerSegmentInBackground:(NSNumber *)segmentId
34
+ path:(NSString *)path
35
+ completion:(void (^)(NSError * _Nullable error))completion;
36
+
29
37
  @end
30
38
 
31
39
  NS_ASSUME_NONNULL_END
@@ -107,9 +107,7 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
107
107
  self.reactNativeFactoryDelegate = [[BackgroundReactNativeDelegate alloc] init];
108
108
  self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self.reactNativeFactoryDelegate];
109
109
 
110
- #if DEBUG
111
- [self.reactNativeFactoryDelegate setJsBundleSource:std::string([entryURL UTF8String])];
112
- #endif
110
+ [self.reactNativeFactoryDelegate setJsBundleSource:std::string([entryURL UTF8String])];
113
111
 
114
112
  [self.reactNativeFactory.rootViewFactory viewWithModuleName:MODULE_NAME
115
113
  initialProperties:initialProperties
@@ -117,4 +115,40 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
117
115
  });
118
116
  }
119
117
 
118
+ #pragma mark - Segment Registration (Phase 2.5 spike)
119
+
120
+ - (void)registerSegmentInBackground:(NSNumber *)segmentId
121
+ path:(NSString *)path
122
+ completion:(void (^)(NSError * _Nullable error))completion
123
+ {
124
+ if (!self.isStarted || !self.reactNativeFactoryDelegate) {
125
+ NSError *error = [NSError errorWithDomain:@"BackgroundThread"
126
+ code:1
127
+ userInfo:@{NSLocalizedDescriptionKey: @"Background runtime not started"}];
128
+ if (completion) completion(error);
129
+ return;
130
+ }
131
+
132
+ // Verify the file exists
133
+ if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
134
+ NSError *error = [NSError errorWithDomain:@"BackgroundThread"
135
+ code:2
136
+ userInfo:@{NSLocalizedDescriptionKey:
137
+ [NSString stringWithFormat:@"Segment file not found: %@", path]}];
138
+ if (completion) completion(error);
139
+ return;
140
+ }
141
+
142
+ BOOL success = [self.reactNativeFactoryDelegate registerSegmentWithId:segmentId path:path];
143
+ if (success) {
144
+ if (completion) completion(nil);
145
+ } else {
146
+ NSError *error = [NSError errorWithDomain:@"BackgroundThread"
147
+ code:3
148
+ userInfo:@{NSLocalizedDescriptionKey:
149
+ @"Failed to register segment in background runtime"}];
150
+ if (completion) completion(error);
151
+ }
152
+ }
153
+
120
154
  @end
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeBackgroundThread.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,cAAc;AAQlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,kBAAkB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeBackgroundThread.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,cAAc;AAYlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,kBAAkB,CAAC","ignoreList":[]}
@@ -2,6 +2,7 @@ import type { TurboModule } from 'react-native';
2
2
  export interface Spec extends TurboModule {
3
3
  startBackgroundRunnerWithEntryURL(entryURL: string): void;
4
4
  installSharedBridge(): void;
5
+ loadSegmentInBackground(segmentId: number, path: string): Promise<void>;
5
6
  }
6
7
  declare const _default: Spec;
7
8
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeBackgroundThread.d.ts","sourceRoot":"","sources":["../../../src/NativeBackgroundThread.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1D,mBAAmB,IAAI,IAAI,CAAC;CAC7B;;AAED,wBAA0E"}
1
+ {"version":3,"file":"NativeBackgroundThread.d.ts","sourceRoot":"","sources":["../../../src/NativeBackgroundThread.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1D,mBAAmB,IAAI,IAAI,CAAC;IAC5B,uBAAuB,CACrB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;;AAED,wBAA0E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-background-thread",
3
- "version": "1.1.47",
3
+ "version": "1.1.49",
4
4
  "description": "react-native-background-thread",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -4,6 +4,10 @@ import type { TurboModule } from 'react-native';
4
4
  export interface Spec extends TurboModule {
5
5
  startBackgroundRunnerWithEntryURL(entryURL: string): void;
6
6
  installSharedBridge(): void;
7
+ loadSegmentInBackground(
8
+ segmentId: number,
9
+ path: string,
10
+ ): Promise<void>;
7
11
  }
8
12
 
9
13
  export default TurboModuleRegistry.getEnforcing<Spec>('BackgroundThread');