@onekeyfe/react-native-background-thread 3.0.20 → 3.0.22

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.
@@ -95,6 +95,49 @@ static NSURL *resolveMainBundleResourceURL(NSString *resourceName)
95
95
  withExtension:extension.length > 0 ? extension : nil];
96
96
  }
97
97
 
98
+ /// Reflectively query BundleUpdateStore for an OTA-installed bundle path.
99
+ /// Mirrors the cross-framework reflection pattern used by SplitBundleLoader
100
+ /// (we can't import the Swift module directly because its umbrella header
101
+ /// pulls in C++/Nitro headers that break the Clang dependency scanner).
102
+ /// Returns nil when the selector is absent (older bundle-update package) or
103
+ /// when no OTA is currently active.
104
+ static NSString *resolveOtaBundlePath(NSString *selectorName)
105
+ {
106
+ Class cls = NSClassFromString(@"ReactNativeBundleUpdate.BundleUpdateStore");
107
+ if (!cls) return nil;
108
+ SEL sel = NSSelectorFromString(selectorName);
109
+ if (![cls respondsToSelector:sel]) return nil;
110
+ NSMethodSignature *sig = [cls methodSignatureForSelector:sel];
111
+ if (!sig || strcmp(sig.methodReturnType, @encode(id)) != 0) return nil;
112
+ NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
113
+ inv.target = cls;
114
+ inv.selector = sel;
115
+ [inv invoke];
116
+ __unsafe_unretained id raw = nil;
117
+ [inv getReturnValue:&raw];
118
+ if (![raw isKindOfClass:[NSString class]]) return nil;
119
+ NSString *result = (NSString *)raw;
120
+ if (result.length == 0) return nil;
121
+ if ([result hasPrefix:@"file://"]) {
122
+ result = [[NSURL URLWithString:result] path];
123
+ }
124
+ if (![[NSFileManager defaultManager] fileExistsAtPath:result]) return nil;
125
+ return result;
126
+ }
127
+
128
+ /// True when an OTA-installed main bundle is currently active. Used to
129
+ /// prevent falling back to IPA built-in common/background bundles when the
130
+ /// foreground main runtime has already loaded an OTA main: a mixed
131
+ /// OTA-main + IPA-built-in pair would moduleId-mismatch and crash on first
132
+ /// require(). Without this guard, package skew (split-bundle-loader/
133
+ /// background-thread upgraded but bundle-update still on a version that
134
+ /// doesn't expose currentBundleCommonJSBundle) would silently reintroduce
135
+ /// the very crash this patch was added to fix.
136
+ static BOOL isOtaMainBundleActive(void)
137
+ {
138
+ return resolveOtaBundlePath(@"currentBundleMainJSBundle") != nil;
139
+ }
140
+
98
141
  static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
99
142
  {
100
143
  if (jsBundleSourceNS.length == 0) {
@@ -120,6 +163,21 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
120
163
  RCTInstance *_rctInstance;
121
164
  std::string _origin;
122
165
  std::string _jsBundleSource;
166
+ // YES when `bundleURL` observed an active OTA main bundle on its most
167
+ // recent invocation, regardless of whether OTA common actually resolved.
168
+ // Captures the invariant "this delegate is locked to OTA territory; IPA
169
+ // built-in fallbacks for the matching common/background bundle are
170
+ // unsafe", covering both:
171
+ // 1. OTA common was resolved (loaded). IPA built-in background would
172
+ // moduleId-mismatch the OTA common.
173
+ // 2. OTA common was unresolvable but OTA main was active (`bundleURL`
174
+ // returned nil). hostDidStart shouldn't fire in this case under
175
+ // normal RCTHost lifecycle, but if it ever does, we must not let
176
+ // `resolveBackgroundEntryBundlePath` happily fall back to IPA bg.
177
+ // `resolveBackgroundEntryBundlePath` and `hostDidStart` consult the flag
178
+ // to distinguish those fatal cases from the legitimate "this build was
179
+ // never split" case (no OTA, no background bundled — warn and continue).
180
+ BOOL _otaActiveAtBundleResolve;
123
181
  }
124
182
 
125
183
  - (void)cleanupResources;
@@ -136,6 +194,7 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
136
194
  if (self = [super init]) {
137
195
  _hasOnMessageHandler = NO;
138
196
  _hasOnErrorHandler = NO;
197
+ _otaActiveAtBundleResolve = NO;
139
198
  self.dependencyProvider = [[RCTAppDependencyProvider alloc] init];
140
199
  }
141
200
  return self;
@@ -170,6 +229,10 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
170
229
 
171
230
  - (NSURL *)bundleURL
172
231
  {
232
+ // Reset on every call so a re-entry (e.g. host restart) can't carry over
233
+ // a stale OTA assertion from a previous load.
234
+ _otaActiveAtBundleResolve = NO;
235
+
173
236
  // When _jsBundleSource is set (dev mode or explicit override), use it as-is.
174
237
  // This is a single full bundle (not split), so DON'T use common+entry strategy.
175
238
  if (!_jsBundleSource.empty()) {
@@ -183,16 +246,64 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
183
246
  }
184
247
 
185
248
  // Default: load common bundle (shared polyfills + modules).
186
- // The background entry bundle is loaded later in hostDidStart:.
187
- NSURL *commonURL = resolveMainBundleResourceURL(@"common.jsbundle");
249
+ // Prefer OTA-installed common.bundle so a three-bundle OTA update is
250
+ // actually picked up by the background runtime; fall back to the IPA
251
+ // built-in common.bundle when no OTA is active. Without this, OTA
252
+ // would push a new common.bundle to disk but the background runtime
253
+ // would keep loading the stale built-in copy and crash on
254
+ // moduleId mismatch with the OTA-loaded background.bundle.
255
+ NSString *otaCommonPath = resolveOtaBundlePath(@"currentBundleCommonJSBundle");
256
+ if (otaCommonPath) {
257
+ [BTLogger info:[NSString stringWithFormat:@"BackgroundRuntime: using OTA common bundle at %@", otaCommonPath]];
258
+ // OTA common resolved implies OTA main is active; the matching OTA
259
+ // background MUST be used (IPA bg would moduleId-mismatch).
260
+ _otaActiveAtBundleResolve = YES;
261
+ return [NSURL fileURLWithPath:otaCommonPath];
262
+ }
263
+
264
+ // Mixed-state guard: if OTA main is loaded but OTA common is unresolvable,
265
+ // refusing the IPA fallback is safer than crashing on moduleId mismatch.
266
+ // Returning nil aborts the background runtime; the foreground main runtime
267
+ // would have crashed anyway, so this just makes the failure mode loud.
268
+ if (isOtaMainBundleActive()) {
269
+ // Set the flag here too so the invariant ("OTA was active when bundleURL
270
+ // ran") holds regardless of whether OTA common resolved. Under normal
271
+ // RCTHost lifecycle hostDidStart won't run after we return nil, but if
272
+ // it ever does (host retry, lifecycle bug, future refactor), the flag
273
+ // ensures the same fatal-abort branch is taken.
274
+ _otaActiveAtBundleResolve = YES;
275
+ [BTLogger error:@"BackgroundRuntime: OTA main is active but OTA common bundle is unresolvable — refusing IPA fallback to avoid moduleId mismatch crash"];
276
+ return nil;
277
+ }
278
+
279
+ NSURL *commonURL = resolveMainBundleResourceURL(@"common.bundle");
188
280
  if (commonURL) {
189
281
  return commonURL;
190
282
  }
191
- return [[NSBundle mainBundle] URLForResource:@"common" withExtension:@"jsbundle"];
283
+ return [[NSBundle mainBundle] URLForResource:@"common" withExtension:@"bundle"];
192
284
  }
193
285
 
194
286
  - (NSString *)resolveBackgroundEntryBundlePath
195
287
  {
288
+ // Prefer OTA-installed background.bundle; fall back to IPA built-in.
289
+ NSString *otaBackgroundPath = resolveOtaBundlePath(@"currentBundleBackgroundJSBundle");
290
+ if (otaBackgroundPath) {
291
+ [BTLogger info:[NSString stringWithFormat:@"BackgroundRuntime: using OTA background bundle at %@", otaBackgroundPath]];
292
+ return otaBackgroundPath;
293
+ }
294
+
295
+ // Mixed-state guard: bundleURL set _otaActiveAtBundleResolve when it
296
+ // observed an active OTA main on its most recent invocation (whether or
297
+ // not OTA common itself resolved). The IPA built-in background.bundle was
298
+ // built against the IPA common.bundle, so its moduleIds won't line up
299
+ // with whatever OTA bundle the foreground runtime is using — IPA fallback
300
+ // would crash on first require(). Return nil; hostDidStart consults the
301
+ // same flag and aborts loudly instead of continuing with a broken runtime.
302
+ if (_otaActiveAtBundleResolve) {
303
+ [BTLogger error:@"BackgroundRuntime: OTA main is active but OTA background is unresolvable — refusing IPA fallback to avoid moduleId mismatch crash"];
304
+ return nil;
305
+ }
306
+
196
307
  NSURL *url = resolveMainBundleResourceURL(@"background.bundle");
197
308
  return url.path;
198
309
  }
@@ -228,6 +339,22 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
228
339
  bgBundleSourceURL = bgBundlePath.lastPathComponent ?: @"background.bundle";
229
340
  [BTLogger info:[NSString stringWithFormat:@"Background entry bundle loaded from %@ (%lu bytes)",
230
341
  bgBundlePath, (unsigned long)bgBundleData.length]];
342
+ } else if (_otaActiveAtBundleResolve) {
343
+ // Fatal: bundleURL committed to OTA territory (loaded OTA common, or
344
+ // detected OTA main and refused IPA fallback) but the matching OTA
345
+ // background couldn't be resolved. Setting up SharedStore / SharedRPC
346
+ // and calling __setupBackgroundRPCHandler against a runtime with no
347
+ // entry bundle would leave RPC silently broken; falling back to IPA
348
+ // bg would moduleId-mismatch and crash. Abort loudly.
349
+ //
350
+ // Clear _rctInstance before returning so registerSegmentWithId (and
351
+ // any other downstream method that gates on `_rctInstance != nil`)
352
+ // doesn't operate on a half-initialized runtime where SharedStore /
353
+ // SharedRPC / error handler / __setupBackgroundRPCHandler were all
354
+ // skipped.
355
+ [BTLogger error:@"BackgroundRuntime: aborting hostDidStart — OTA bundle is loaded but OTA background bundle is unresolvable"];
356
+ _rctInstance = nil;
357
+ return;
231
358
  } else {
232
359
  [BTLogger warn:@"Background entry bundle not found, __setupBackgroundRPCHandler may not be defined"];
233
360
  }
@@ -109,7 +109,7 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
109
109
 
110
110
  // Only set jsBundleSource for debug HTTP URLs or explicit OTA overrides.
111
111
  // Leaving the default release name ("background.bundle") unset lets the
112
- // delegate fall back to split-bundle mode (common.jsbundle + entry).
112
+ // delegate fall back to split-bundle mode (common.bundle + entry).
113
113
  if (![entryURL isEqualToString:@"background.bundle"]) {
114
114
  [self.reactNativeFactoryDelegate setJsBundleSource:std::string([entryURL UTF8String])];
115
115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-background-thread",
3
- "version": "3.0.20",
3
+ "version": "3.0.22",
4
4
  "description": "react-native-background-thread",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -84,6 +84,7 @@
84
84
  "typescript": "^5.9.2"
85
85
  },
86
86
  "peerDependencies": {
87
+ "@onekeyfe/react-native-bundle-update": ">=3.0.22",
87
88
  "react": "*",
88
89
  "react-native": "*"
89
90
  },