@onekeyfe/react-native-background-thread 1.1.50 → 1.1.52

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.
@@ -108,6 +108,88 @@ class BackgroundThreadManager private constructor() {
108
108
  return null
109
109
  }
110
110
 
111
+ /**
112
+ * Creates a JSBundleLoader that loads two bundles sequentially from Android assets:
113
+ * first the common bundle (polyfills + shared modules), then the
114
+ * entry-specific bundle (entry-only modules + require(entryId)).
115
+ */
116
+ private fun createSequentialAssetBundleLoader(
117
+ appContext: android.content.Context,
118
+ commonAssetName: String,
119
+ entryAssetName: String
120
+ ): JSBundleLoader {
121
+ return object : JSBundleLoader() {
122
+ override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
123
+ // Step 1: Load common bundle (polyfills + shared modules)
124
+ delegate.loadScriptFromAssets(appContext.assets, "assets://$commonAssetName", false)
125
+ BTLogger.info("Common bundle loaded from assets: $commonAssetName")
126
+
127
+ // Step 2: Load entry-specific bundle
128
+ delegate.loadScriptFromAssets(appContext.assets, "assets://$entryAssetName", false)
129
+ BTLogger.info("Entry bundle loaded from assets: $entryAssetName")
130
+
131
+ return "assets://$entryAssetName"
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Creates a JSBundleLoader that loads two bundles sequentially from local files:
138
+ * first the common bundle, then the entry-specific bundle.
139
+ */
140
+ private fun createSequentialFileBundleLoader(
141
+ commonPath: String,
142
+ entryPath: String,
143
+ entrySourceURL: String
144
+ ): JSBundleLoader {
145
+ return object : JSBundleLoader() {
146
+ override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
147
+ // Step 1: Load common bundle (polyfills + shared modules)
148
+ val commonFile = File(commonPath)
149
+ if (!commonFile.exists()) {
150
+ BTLogger.error("Common bundle file does not exist: $commonPath")
151
+ throw RuntimeException("Common bundle file does not exist: $commonPath")
152
+ }
153
+ delegate.loadScriptFromFile(commonFile.absolutePath, "common.bundle", false)
154
+ BTLogger.info("Common bundle loaded from file: $commonPath")
155
+
156
+ // Step 2: Load entry-specific bundle
157
+ val entryFile = File(entryPath)
158
+ if (!entryFile.exists()) {
159
+ BTLogger.error("Entry bundle file does not exist: $entryPath")
160
+ throw RuntimeException("Entry bundle file does not exist: $entryPath")
161
+ }
162
+ delegate.loadScriptFromFile(entryFile.absolutePath, entrySourceURL, false)
163
+ BTLogger.info("Entry bundle loaded from file: $entryPath")
164
+
165
+ return entrySourceURL
166
+ }
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Check if common.bundle exists in the Android assets directory.
172
+ */
173
+ private fun hasCommonBundleInAssets(appContext: android.content.Context): Boolean {
174
+ return try {
175
+ appContext.assets.open("common.bundle").close()
176
+ true
177
+ } catch (e: Exception) {
178
+ false
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Resolve the common bundle path for OTA (file-based) loading.
184
+ * Looks for common.bundle in the same directory as the entry bundle.
185
+ */
186
+ private fun resolveCommonBundlePath(entryBundlePath: String): String? {
187
+ val entryFile = File(entryBundlePath)
188
+ val parentDir = entryFile.parentFile ?: return null
189
+ val commonFile = File(parentDir, "common.bundle")
190
+ return if (commonFile.exists()) commonFile.absolutePath else null
191
+ }
192
+
111
193
  private fun createDownloadedBundleLoader(appContext: android.content.Context, entryURL: String): JSBundleLoader {
112
194
  return object : JSBundleLoader() {
113
195
  override fun loadScript(delegate: com.facebook.react.bridge.JSBundleLoaderDelegate): String {
@@ -163,10 +245,43 @@ class BackgroundThreadManager private constructor() {
163
245
  val localBundlePath = resolveLocalBundlePath(entryURL)
164
246
  val bundleLoader =
165
247
  when {
248
+ // Debug mode: remote URL — use single bundle (Metro dev server)
166
249
  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)
250
+
251
+ // OTA / local file path — try sequential loading with common bundle
252
+ localBundlePath != null -> {
253
+ val commonPath = resolveCommonBundlePath(localBundlePath)
254
+ if (commonPath != null) {
255
+ BTLogger.info("Using sequential file bundle loader: common=$commonPath, entry=$localBundlePath")
256
+ createSequentialFileBundleLoader(commonPath, localBundlePath, entryURL)
257
+ } else {
258
+ BTLogger.info("No common bundle found for OTA path, using single bundle: $localBundlePath")
259
+ createLocalFileBundleLoader(localBundlePath, entryURL)
260
+ }
261
+ }
262
+
263
+ // Assets-based loading — try sequential loading with common.bundle in assets
264
+ entryURL.startsWith("assets://") -> {
265
+ val entryAssetName = entryURL.removePrefix("assets://")
266
+ if (hasCommonBundleInAssets(appContext)) {
267
+ BTLogger.info("Using sequential asset bundle loader: common=common.bundle, entry=$entryAssetName")
268
+ createSequentialAssetBundleLoader(appContext, "common.bundle", entryAssetName)
269
+ } else {
270
+ BTLogger.info("No common.bundle in assets, using single bundle: $entryURL")
271
+ JSBundleLoader.createAssetLoader(appContext, entryURL, true)
272
+ }
273
+ }
274
+
275
+ // Bare filename (e.g. "background.bundle") — treat as asset
276
+ else -> {
277
+ if (hasCommonBundleInAssets(appContext)) {
278
+ BTLogger.info("Using sequential asset bundle loader: common=common.bundle, entry=$entryURL")
279
+ createSequentialAssetBundleLoader(appContext, "common.bundle", entryURL)
280
+ } else {
281
+ BTLogger.info("No common.bundle in assets, using single bundle: assets://$entryURL")
282
+ JSBundleLoader.createAssetLoader(appContext, "assets://$entryURL", true)
283
+ }
284
+ }
170
285
  }
171
286
 
172
287
  val delegate = DefaultReactHostDelegate(
@@ -253,30 +368,45 @@ class BackgroundThreadManager private constructor() {
253
368
  * @throws IllegalStateException if background runtime is not started
254
369
  * @throws IllegalArgumentException if segment file does not exist
255
370
  */
256
- fun registerSegmentInBackground(segmentId: Int, path: String) {
371
+ /**
372
+ * Register a HBC segment in the background runtime with completion callback.
373
+ * Dispatches to the background JS queue thread and invokes the callback
374
+ * only after registerSegment has actually executed.
375
+ *
376
+ * @param segmentId The segment ID to register
377
+ * @param path Absolute file path to the .seg.hbc file
378
+ * @param onComplete Called with null on success, or an Exception on failure
379
+ */
380
+ fun registerSegmentInBackground(segmentId: Int, path: String, onComplete: (Exception?) -> Unit) {
257
381
  if (!isStarted) {
258
- throw IllegalStateException("Background runtime not started")
382
+ onComplete(IllegalStateException("Background runtime not started"))
383
+ return
259
384
  }
260
385
 
261
386
  val file = File(path)
262
387
  if (!file.exists()) {
263
- throw IllegalArgumentException("Segment file not found: $path")
388
+ onComplete(IllegalArgumentException("Segment file not found: $path"))
389
+ return
264
390
  }
265
391
 
266
392
  val context = bgReactHost?.currentReactContext
267
- ?: throw IllegalStateException("Background ReactContext not available")
393
+ if (context == null) {
394
+ onComplete(IllegalStateException("Background ReactContext not available"))
395
+ return
396
+ }
268
397
 
269
398
  context.runOnJSQueueThread {
270
399
  try {
271
400
  if (context.hasCatalystInstance()) {
272
401
  context.catalystInstance.registerSegment(segmentId, path)
273
402
  BTLogger.info("Segment registered in background runtime: id=$segmentId, path=$path")
403
+ onComplete(null)
274
404
  } else {
275
- BTLogger.error("Background CatalystInstance not available for segment registration")
405
+ onComplete(IllegalStateException("Background CatalystInstance not available for segment registration"))
276
406
  }
277
407
  } catch (e: Exception) {
278
408
  BTLogger.error("Failed to register segment in background runtime: ${e.message}")
279
- throw e
409
+ onComplete(e)
280
410
  }
281
411
  }
282
412
  }
@@ -29,12 +29,13 @@ class BackgroundThreadModule(reactContext: ReactApplicationContext) :
29
29
  }
30
30
 
31
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
- }
32
+ BackgroundThreadManager.getInstance()
33
+ .registerSegmentInBackground(segmentId.toInt(), path) { error ->
34
+ if (error != null) {
35
+ promise.reject("BG_SEGMENT_LOAD_ERROR", error.message, error)
36
+ } else {
37
+ promise.resolve(null)
38
+ }
39
+ }
39
40
  }
40
41
  }
@@ -170,6 +170,8 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
170
170
 
171
171
  - (NSURL *)bundleURL
172
172
  {
173
+ // When _jsBundleSource is set (dev mode or explicit override), use it as-is.
174
+ // This is a single full bundle (not split), so DON'T use common+entry strategy.
173
175
  if (!_jsBundleSource.empty()) {
174
176
  NSString *jsBundleSourceNS = [NSString stringWithUTF8String:_jsBundleSource.c_str()];
175
177
  NSURL *resolvedURL = resolveBundleSourceURL(jsBundleSourceNS);
@@ -180,11 +182,19 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
180
182
  [BTLogger warn:[NSString stringWithFormat:@"Unable to resolve custom jsBundleSource=%@", jsBundleSourceNS]];
181
183
  }
182
184
 
183
- NSURL *defaultBundleURL = resolveMainBundleResourceURL(@"background.bundle");
184
- if (defaultBundleURL) {
185
- return defaultBundleURL;
185
+ // Default: load common bundle (shared polyfills + modules).
186
+ // The background entry bundle is loaded later in hostDidStart:.
187
+ NSURL *commonURL = resolveMainBundleResourceURL(@"common.jsbundle");
188
+ if (commonURL) {
189
+ return commonURL;
186
190
  }
187
- return [[NSBundle mainBundle] URLForResource:@"background" withExtension:@"bundle"];
191
+ return [[NSBundle mainBundle] URLForResource:@"common" withExtension:@"jsbundle"];
192
+ }
193
+
194
+ - (NSString *)resolveBackgroundEntryBundlePath
195
+ {
196
+ NSURL *url = resolveMainBundleResourceURL(@"background.bundle");
197
+ return url.path;
188
198
  }
189
199
 
190
200
  - (void)hostDidStart:(RCTHost *)host
@@ -203,6 +213,26 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
203
213
  return;
204
214
  }
205
215
 
216
+ // When _jsBundleSource is set, the bundle loaded in bundleURL was already
217
+ // a full single bundle (dev mode / explicit override), so skip entry loading.
218
+ BOOL isSplitBundle = _jsBundleSource.empty();
219
+
220
+ // Read the background entry bundle data before entering the executor block
221
+ // (only needed in split-bundle mode).
222
+ NSData *bgBundleData = nil;
223
+ NSString *bgBundleSourceURL = nil;
224
+ if (isSplitBundle) {
225
+ NSString *bgBundlePath = [self resolveBackgroundEntryBundlePath];
226
+ if (bgBundlePath) {
227
+ bgBundleData = [NSData dataWithContentsOfFile:bgBundlePath];
228
+ bgBundleSourceURL = bgBundlePath.lastPathComponent ?: @"background.bundle";
229
+ [BTLogger info:[NSString stringWithFormat:@"Background entry bundle loaded from %@ (%lu bytes)",
230
+ bgBundlePath, (unsigned long)bgBundleData.length]];
231
+ } else {
232
+ [BTLogger warn:@"Background entry bundle not found, __setupBackgroundRPCHandler may not be defined"];
233
+ }
234
+ }
235
+
206
236
  [_rctInstance callFunctionOnBufferedRuntimeExecutor:[=](jsi::Runtime &runtime) {
207
237
  [self setupErrorHandler:runtime];
208
238
 
@@ -218,6 +248,17 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
218
248
  };
219
249
  SharedRPC::install(runtime, std::move(bgExecutor), "background");
220
250
  [BTLogger info:@"SharedStore and SharedRPC installed in background runtime"];
251
+
252
+ // In split-bundle mode, evaluate the background entry bundle now.
253
+ // This must happen BEFORE invokeOptionalGlobalFunction since the entry
254
+ // bundle defines __setupBackgroundRPCHandler.
255
+ if (isSplitBundle && bgBundleData && bgBundleData.length > 0) {
256
+ auto buffer = std::make_shared<jsi::StringBuffer>(
257
+ std::string(static_cast<const char *>(bgBundleData.bytes), bgBundleData.length));
258
+ runtime.evaluateJavaScript(std::move(buffer), [bgBundleSourceURL UTF8String]);
259
+ [BTLogger info:@"Background entry bundle evaluated in runtime"];
260
+ }
261
+
221
262
  invokeOptionalGlobalFunction(runtime, "__setupBackgroundRPCHandler");
222
263
  }];
223
264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-background-thread",
3
- "version": "1.1.50",
3
+ "version": "1.1.52",
4
4
  "description": "react-native-background-thread",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",