@onekeyfe/react-native-background-thread 3.0.90 → 3.0.92

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.
@@ -30,7 +30,13 @@ NS_ASSUME_NONNULL_BEGIN
30
30
  * @return Initialized delegate instance with filtered module access
31
31
  */
32
32
  - (instancetype)init;
33
-
33
+ /// Configure debug startup as local common HBC followed by a remote
34
+ /// modules-only background entry bundle.
35
+ - (void)configureDevVendorCommonBundlePath:(NSString *)commonBundlePath
36
+ entryURL:(NSURL *)entryURL
37
+ fingerprint:(NSString *)fingerprint
38
+ backgroundHMREnabled:(BOOL)backgroundHMREnabled
39
+ runtimeGeneration:(NSUInteger)runtimeGeneration;
34
40
  /**
35
41
  * Register a HBC segment in the background runtime (Phase 2.5 spike).
36
42
  * Uses RCTInstance's registerSegmentWithId:path: API.
@@ -12,8 +12,9 @@
12
12
  #include "SharedRPC.h"
13
13
 
14
14
  #import <React/RCTBridge.h>
15
+ #import <React/RCTAssert.h>
15
16
  #import <React/RCTBundleURLProvider.h>
16
-
17
+ #import <React/RCTJavaScriptLoader.h>
17
18
  #if __has_include(<react/utils/FollyConvert.h>)
18
19
  // static libs / header maps (no use_frameworks!)
19
20
  #import <react/utils/FollyConvert.h>
@@ -42,7 +43,33 @@
42
43
  namespace jsi = facebook::jsi;
43
44
  namespace TurboModuleConvertUtils = facebook::react::TurboModuleConvertUtils;
44
45
  using namespace facebook::react;
45
-
46
+ namespace {
47
+ constexpr int64_t kDevVendorLoadTimeoutSeconds = 60;
48
+ class BackgroundNSDataJSIBuffer : public facebook::jsi::Buffer {
49
+ public:
50
+ explicit BackgroundNSDataJSIBuffer(NSData *data) : data_(data) {}
51
+ size_t size() const override { return data_.length; }
52
+ const uint8_t *data() const override {
53
+ return static_cast<const uint8_t *>(data_.bytes);
54
+ }
55
+ private:
56
+ NSData *data_;
57
+ };
58
+ } // namespace
59
+ @interface BTDevVendorDownloadState : NSObject
60
+ @property (nonatomic, strong, nullable) NSData *data;
61
+ @property (nonatomic, strong, nullable) NSError *error;
62
+ @property (nonatomic, strong) dispatch_semaphore_t ready;
63
+ @end
64
+ @implementation BTDevVendorDownloadState
65
+ - (instancetype)init
66
+ {
67
+ if (self = [super init]) {
68
+ _ready = dispatch_semaphore_create(0);
69
+ }
70
+ return self;
71
+ }
72
+ @end
46
73
  static void stubJsiFunction(jsi::Runtime &runtime, jsi::Object &object, const char *name)
47
74
  {
48
75
  object.setProperty(
@@ -163,6 +190,11 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
163
190
  RCTInstance *_rctInstance;
164
191
  std::string _origin;
165
192
  std::string _jsBundleSource;
193
+ NSString *_devVendorCommonBundlePath;
194
+ NSURL *_devVendorEntryURL;
195
+ NSString *_devVendorFingerprint;
196
+ BOOL _devVendorBackgroundHMREnabled;
197
+ NSUInteger _backgroundRuntimeGeneration;
166
198
  // YES when `bundleURL` observed an active OTA main bundle on its most
167
199
  // recent invocation, regardless of whether OTA common actually resolved.
168
200
  // Captures the invariant "this delegate is locked to OTA territory; IPA
@@ -182,7 +214,11 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
182
214
 
183
215
  - (void)cleanupResources;
184
216
  - (void)setupErrorHandler:(jsi::Runtime &)runtime;
185
-
217
+ - (void)finishHostStartWithBundleData:(NSData * _Nullable)bundleData
218
+ sourceURL:(NSString * _Nullable)sourceURL
219
+ fingerprint:(NSString * _Nullable)fingerprint
220
+ evaluateEntry:(BOOL)evaluateEntry
221
+ downloadState:(BTDevVendorDownloadState * _Nullable)downloadState;
186
222
  @end
187
223
 
188
224
  @implementation BackgroundReactNativeDelegate
@@ -221,7 +257,18 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
221
257
  {
222
258
  _jsBundleSource = jsBundleSource;
223
259
  }
224
-
260
+ - (void)configureDevVendorCommonBundlePath:(NSString *)commonBundlePath
261
+ entryURL:(NSURL *)entryURL
262
+ fingerprint:(NSString *)fingerprint
263
+ backgroundHMREnabled:(BOOL)backgroundHMREnabled
264
+ runtimeGeneration:(NSUInteger)runtimeGeneration
265
+ {
266
+ _devVendorCommonBundlePath = [commonBundlePath copy];
267
+ _devVendorEntryURL = [entryURL copy];
268
+ _devVendorFingerprint = [fingerprint copy];
269
+ _devVendorBackgroundHMREnabled = backgroundHMREnabled;
270
+ _backgroundRuntimeGeneration = runtimeGeneration;
271
+ }
225
272
  - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
226
273
  {
227
274
  return [self bundleURL];
@@ -232,7 +279,9 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
232
279
  // Reset on every call so a re-entry (e.g. host restart) can't carry over
233
280
  // a stale OTA assertion from a previous load.
234
281
  _otaActiveAtBundleResolve = NO;
235
-
282
+ if (_devVendorCommonBundlePath.length > 0) {
283
+ return [NSURL fileURLWithPath:_devVendorCommonBundlePath];
284
+ }
236
285
  // When _jsBundleSource is set (dev mode or explicit override), use it as-is.
237
286
  // This is a single full bundle (not split), so DON'T use common+entry strategy.
238
287
  if (!_jsBundleSource.empty()) {
@@ -323,7 +372,56 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
323
372
  if (!_rctInstance) {
324
373
  return;
325
374
  }
326
-
375
+ if (_devVendorEntryURL) {
376
+ NSURL *entryURL = _devVendorEntryURL;
377
+ NSString *fingerprint = _devVendorFingerprint;
378
+ BTDevVendorDownloadState *downloadState = [[BTDevVendorDownloadState alloc] init];
379
+ [RCTJavaScriptLoader loadBundleAtURL:entryURL
380
+ onProgress:^(RCTLoadingProgress *progress) { (void)progress; }
381
+ onComplete:^(NSError *error, RCTSource *source) {
382
+ downloadState.error = error;
383
+ downloadState.data = source.data;
384
+ dispatch_semaphore_signal(downloadState.ready);
385
+ }];
386
+ [self finishHostStartWithBundleData:nil
387
+ sourceURL:entryURL.absoluteString
388
+ fingerprint:fingerprint
389
+ evaluateEntry:YES
390
+ downloadState:downloadState];
391
+ if (_devVendorBackgroundHMREnabled) {
392
+ NSURLComponents *hmrComponents = [NSURLComponents componentsWithURL:entryURL
393
+ resolvingAgainstBaseURL:NO];
394
+ hmrComponents.path = @"/apps/mobile/background.bundle";
395
+ NSURL *hmrRegistrationURL = hmrComponents.URL;
396
+ if (!hmrRegistrationURL) {
397
+ [BTLogger error:@"[BackgroundHMR] failed to construct registration URL"];
398
+ return;
399
+ }
400
+ NSString *path = hmrRegistrationURL.path;
401
+ if ([path hasPrefix:@"/"]) {
402
+ path = [path substringFromIndex:1];
403
+ }
404
+ NSString *scheme = hmrRegistrationURL.scheme ?: @"http";
405
+ NSNumber *port = hmrRegistrationURL.port ?: @([scheme isEqualToString:@"https"] ? 443 : 80);
406
+ [host callFunctionOnJSModule:@"HMRClient"
407
+ method:@"setup"
408
+ args:@[
409
+ @"ios",
410
+ path ?: @"apps/mobile/background.bundle",
411
+ hmrRegistrationURL.host ?: @"127.0.0.1",
412
+ port,
413
+ @YES,
414
+ scheme,
415
+ hmrRegistrationURL.absoluteString
416
+ ]];
417
+ [BTLogger info:[NSString stringWithFormat:
418
+ @"[BackgroundHMR] client setup queued generation=%lu downloadURL=%@ registrationURL=%@",
419
+ (unsigned long)_backgroundRuntimeGeneration,
420
+ entryURL.absoluteString,
421
+ hmrRegistrationURL.absoluteString]];
422
+ }
423
+ return;
424
+ }
327
425
  // When _jsBundleSource is set, the bundle loaded in bundleURL was already
328
426
  // a full single bundle (dev mode / explicit override), so skip entry loading.
329
427
  BOOL isSplitBundle = _jsBundleSource.empty();
@@ -359,49 +457,118 @@ static NSURL *resolveBundleSourceURL(NSString *jsBundleSourceNS)
359
457
  [BTLogger warn:@"Background entry bundle not found, __setupBackgroundRPCHandler may not be defined"];
360
458
  }
361
459
  }
362
-
460
+ [self finishHostStartWithBundleData:bgBundleData
461
+ sourceURL:bgBundleSourceURL
462
+ fingerprint:nil
463
+ evaluateEntry:isSplitBundle
464
+ downloadState:nil];
465
+ }
466
+ - (void)finishHostStartWithBundleData:(NSData * _Nullable)bundleData
467
+ sourceURL:(NSString * _Nullable)sourceURL
468
+ fingerprint:(NSString * _Nullable)fingerprint
469
+ evaluateEntry:(BOOL)evaluateEntry
470
+ downloadState:(BTDevVendorDownloadState * _Nullable)downloadState
471
+ {
472
+ RCTInstance *instance = _rctInstance;
473
+ if (!instance) {
474
+ return;
475
+ }
363
476
  CFAbsoluteTime bgStartTime = CFAbsoluteTimeGetCurrent();
364
-
365
- [_rctInstance callFunctionOnBufferedRuntimeExecutor:[=](jsi::Runtime &runtime) {
366
- [self setupErrorHandler:runtime];
367
-
368
- // Install SharedStore into background runtime
369
- SharedStore::install(runtime);
370
-
371
- // Install SharedRPC with executor for cross-runtime notifications.
372
- // Capture the bg RCTInstance __weak so executor lambdas still in flight
373
- // when the bg host is torn down (BackgroundThread.restart mode=all,
374
- // OTA bundle install) no-op cleanly instead of dispatching onto a
375
- // freed instance and crashing.
376
- __weak RCTInstance *weakBgInstance = _rctInstance;
377
- RPCRuntimeExecutor bgExecutor = [weakBgInstance](std::function<void(jsi::Runtime &)> work) {
378
- RCTInstance *strongBgInstance = weakBgInstance;
379
- if (!strongBgInstance) {
380
- return;
477
+ __weak RCTInstance *weakBgInstance = instance;
478
+
479
+ [instance callFunctionOnBufferedRuntimeExecutor:[=](jsi::Runtime &runtime) {
480
+ try {
481
+ NSData *entryData = bundleData;
482
+ if (downloadState) {
483
+ // Queue this executor before the background surface starts. Once the
484
+ // local common HBC completes, wait for Metro without blocking main.
485
+ dispatch_time_t deadline = dispatch_time(
486
+ DISPATCH_TIME_NOW, kDevVendorLoadTimeoutSeconds * NSEC_PER_SEC);
487
+ if (dispatch_semaphore_wait(downloadState.ready, deadline) != 0) {
488
+ NSError *timeoutError = [NSError errorWithDomain:@"BackgroundThread"
489
+ code:21
490
+ userInfo:@{
491
+ NSLocalizedDescriptionKey:
492
+ @"Timed out loading dev-vendor background delta"
493
+ }];
494
+ [BTLogger error:@"Dev-vendor background delta timed out after 60 seconds"];
495
+ dispatch_async(dispatch_get_main_queue(), ^{ RCTFatal(timeoutError); });
496
+ return;
381
497
  }
382
- [strongBgInstance callFunctionOnBufferedRuntimeExecutor:[work](jsi::Runtime &rt) {
383
- work(rt);
384
- }];
385
- };
386
- SharedRPC::install(runtime, std::move(bgExecutor), "background");
387
- [BTLogger info:@"SharedStore and SharedRPC installed in background runtime"];
388
-
389
- // In split-bundle mode, evaluate the background entry bundle now.
390
- // This must happen BEFORE invokeOptionalGlobalFunction since the entry
391
- // bundle defines __setupBackgroundRPCHandler.
392
- if (isSplitBundle && bgBundleData && bgBundleData.length > 0) {
393
- CFAbsoluteTime bgEvalStart = CFAbsoluteTimeGetCurrent();
394
- auto buffer = std::make_shared<jsi::StringBuffer>(
395
- std::string(static_cast<const char *>(bgBundleData.bytes), bgBundleData.length));
396
- runtime.evaluateJavaScript(std::move(buffer), [bgBundleSourceURL UTF8String]);
397
- double bgEvalMs = (CFAbsoluteTimeGetCurrent() - bgEvalStart) * 1000.0;
398
- [BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg entry evaluated in %.1fms", bgEvalMs]];
498
+ if (downloadState.error || downloadState.data.length == 0) {
499
+ NSError *loadError = downloadState.error ?: [NSError errorWithDomain:@"BackgroundThread"
500
+ code:20
501
+ userInfo:@{
502
+ NSLocalizedDescriptionKey:
503
+ @"Dev-vendor background delta is empty"
504
+ }];
505
+ [BTLogger error:[NSString stringWithFormat:
506
+ @"Dev-vendor background delta failed: %@", loadError.localizedDescription]];
507
+ dispatch_async(dispatch_get_main_queue(), ^{ RCTFatal(loadError); });
508
+ return;
509
+ }
510
+ entryData = downloadState.data;
511
+ }
512
+
513
+ if (fingerprint.length > 0) {
514
+ jsi::Value marker = runtime.global().getProperty(
515
+ runtime, "__ONEKEY_DEV_VENDOR_FINGERPRINT__");
516
+ if (!marker.isString() ||
517
+ marker.asString(runtime).utf8(runtime) != std::string(fingerprint.UTF8String)) {
518
+ throw std::runtime_error("Dev-vendor common fingerprint mismatch");
519
+ }
520
+ runtime.global().setProperty(
521
+ runtime,
522
+ "__ONEKEY_DEV_VENDOR_FULL_BUNDLE_URL__",
523
+ jsi::String::createFromUtf8(runtime, sourceURL.UTF8String));
524
+ runtime.global().setProperty(
525
+ runtime,
526
+ "__ONEKEY_RUNTIME_TARGET__",
527
+ jsi::String::createFromUtf8(runtime, "background"));
528
+ runtime.global().setProperty(
529
+ runtime,
530
+ "__ONEKEY_BACKGROUND_RUNTIME_GENERATION__",
531
+ static_cast<double>(_backgroundRuntimeGeneration));
532
+ }
533
+
534
+ [self setupErrorHandler:runtime];
535
+ SharedStore::install(runtime);
536
+
537
+ RPCRuntimeExecutor bgExecutor = [weakBgInstance](std::function<void(jsi::Runtime &)> work) {
538
+ RCTInstance *strongBgInstance = weakBgInstance;
539
+ if (!strongBgInstance) {
540
+ return;
541
+ }
542
+ [strongBgInstance callFunctionOnBufferedRuntimeExecutor:[work](jsi::Runtime &rt) {
543
+ work(rt);
544
+ }];
545
+ };
546
+ SharedRPC::install(runtime, std::move(bgExecutor), "background");
547
+ [BTLogger info:@"SharedStore and SharedRPC installed in background runtime"];
548
+
549
+ // The entry defines __setupBackgroundRPCHandler, so it must execute
550
+ // before the runtime is reported ready.
551
+ if (evaluateEntry && entryData.length > 0 && sourceURL.length > 0) {
552
+ CFAbsoluteTime bgEvalStart = CFAbsoluteTimeGetCurrent();
553
+ auto buffer = std::make_shared<BackgroundNSDataJSIBuffer>(entryData);
554
+ runtime.evaluateJavaScript(buffer, sourceURL.UTF8String);
555
+ double bgEvalMs = (CFAbsoluteTimeGetCurrent() - bgEvalStart) * 1000.0;
556
+ [BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg entry evaluated in %.1fms", bgEvalMs]];
557
+ }
558
+
559
+ double bgTotalMs = (CFAbsoluteTimeGetCurrent() - bgStartTime) * 1000.0;
560
+ [BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg hostDidStart total setup in %.1fms", bgTotalMs]];
561
+ invokeOptionalGlobalFunction(runtime, "__setupBackgroundRPCHandler");
562
+ } catch (const std::exception &exception) {
563
+ NSString *message = [NSString stringWithUTF8String:exception.what()];
564
+ NSError *evaluationError = [NSError errorWithDomain:@"BackgroundThread"
565
+ code:21
566
+ userInfo:@{
567
+ NSLocalizedDescriptionKey:
568
+ message ?: @"Dev-vendor background evaluation failed"
569
+ }];
570
+ dispatch_async(dispatch_get_main_queue(), ^{ RCTFatal(evaluationError); });
399
571
  }
400
-
401
- double bgTotalMs = (CFAbsoluteTimeGetCurrent() - bgStartTime) * 1000.0;
402
- [BTLogger info:[NSString stringWithFormat:@"[SplitBundle] bg hostDidStart total setup in %.1fms", bgTotalMs]];
403
-
404
- invokeOptionalGlobalFunction(runtime, "__setupBackgroundRPCHandler");
405
572
  }];
406
573
  }
407
574
 
@@ -76,7 +76,10 @@ typedef NS_ENUM(NSInteger, EBgMgrSegmentEvalError) {
76
76
  /// @param entryURL The custom entry URL for the background runner
77
77
  + (void)installSharedBridgeInMainRuntime:(RCTHost *)host
78
78
  thenStartBackgroundRunnerWithEntryURL:(NSString *)entryURL;
79
-
79
+ /// Install the main-runtime bridge and start a background runtime from a
80
+ /// local common HBC followed by a remote modules-only entry bundle.
81
+ + (void)installSharedBridgeInMainRuntime:(RCTHost *)host
82
+ thenStartBackgroundRunnerWithDevVendorConfig:(NSDictionary<NSString *, NSString *> *)config;
80
83
  /// Start background runner with default entry URL
81
84
  - (void)startBackgroundRunner;
82
85
 
@@ -142,7 +142,11 @@ static const NSTimeInterval kBgSegmentEvalWatchdogSeconds = 30.0;
142
142
  // same reason as isStarted: written from the caller's thread (public API
143
143
  // doesn't pin start... to main) and read on main by the health-check.
144
144
  @property (atomic, copy) NSString *lastEntryURL;
145
-
145
+ // Debug-only dev-vendor bootstrap contract. Retained across mode='all'
146
+ // reloads so the self-heal path cannot fall back to a monolithic bundle.
147
+ @property (atomic, copy, nullable) NSDictionary<NSString *, NSString *> *lastDevVendorConfig;
148
+ @property (nonatomic, assign) NSUInteger backgroundRuntimeGeneration;
149
+ @property (nonatomic, assign) BOOL backgroundHMRRestartInFlight;
146
150
  // Forward declaration so restartWithMode: can call it; definition lives
147
151
  // after restartWithMode: for readability (the call site is the natural
148
152
  // place to start reading the restart flow).
@@ -150,6 +154,11 @@ static const NSTimeInterval kBgSegmentEvalWatchdogSeconds = 30.0;
150
154
  isAll:(BOOL)isAll
151
155
  generation:(NSUInteger)myGen
152
156
  retried:(BOOL)retried;
157
+ - (void)startBackgroundRunnerWithDevVendorConfig:(NSDictionary<NSString *, NSString *> *)config;
158
+ - (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL
159
+ devVendorConfig:(NSDictionary<NSString *, NSString *> * _Nullable)devVendorConfig;
160
+ - (void)restartBackgroundForHMRReason:(NSString *)reason
161
+ completion:(void (^)(NSError * _Nullable error))completion;
153
162
  @end
154
163
 
155
164
  // First-stage delay for the post-reload health-check. Picked so the host's
@@ -164,7 +173,11 @@ static BackgroundThreadManager *_sharedInstance = nil;
164
173
  static dispatch_once_t onceToken;
165
174
  static NSString *const MODULE_NAME = @"background";
166
175
  static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/background.bundle?platform=ios&dev=true&lazy=false&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&excludeSource=true&sourcePaths=url-server&app=so.onekey.wallet&transform.routerRoot=app&transform.engine=hermes&transform.bytecode=1&unstable_transformProfile=hermes-stable";
167
-
176
+ static NSString *const DEV_VENDOR_COMMON_PATH_KEY = @"commonBundlePath";
177
+ static NSString *const DEV_VENDOR_ENTRY_URL_KEY = @"entryURL";
178
+ static NSString *const DEV_VENDOR_FINGERPRINT_KEY = @"fingerprint";
179
+ static NSString *const DEV_VENDOR_BACKGROUND_HMR_KEY = @"backgroundHMREnabled";
180
+ static NSString *const BACKGROUND_HMR_REASON_PREFIX = @"onekey-bg-hmr:";
168
181
  #pragma mark - Singleton
169
182
 
170
183
  + (instancetype)sharedInstance {
@@ -210,15 +223,30 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
210
223
  // see a nil strong reference and bail out — instead of dispatching
211
224
  // callFunctionOnBufferedRuntimeExecutor on a freed instance and
212
225
  // crashing (EXC_BAD_ACCESS / use-after-free).
226
+ __weak RCTHost *weakHost = host;
213
227
  __weak id weakInstance = instance;
214
- RPCRuntimeExecutor mainExecutor = [weakInstance](std::function<void(jsi::Runtime &)> work) {
215
- id strongInstance = weakInstance;
216
- if (!strongInstance) {
217
- return;
218
- }
219
- [strongInstance callFunctionOnBufferedRuntimeExecutor:[work](jsi::Runtime &rt) {
220
- work(rt);
221
- }];
228
+ RPCRuntimeExecutor mainExecutor = [weakHost, weakInstance](std::function<void(jsi::Runtime &)> work) {
229
+ // RCTHost invalidates and swaps its RCTInstance on the main queue.
230
+ // Serialize the identity check with that swap: retaining the old
231
+ // Objective-C wrapper is not sufficient because its C++
232
+ // ReactInstance may already have been reset during reload.
233
+ dispatch_async(dispatch_get_main_queue(), ^{
234
+ RCTHost *strongHost = weakHost;
235
+ id strongInstance = weakInstance;
236
+ if (!strongHost || !strongInstance) {
237
+ return;
238
+ }
239
+
240
+ Ivar currentInstanceIvar = class_getInstanceVariable([strongHost class], "_instance");
241
+ id currentInstance = object_getIvar(strongHost, currentInstanceIvar);
242
+ if (currentInstance != strongInstance) {
243
+ return;
244
+ }
245
+
246
+ [strongInstance callFunctionOnBufferedRuntimeExecutor:[work](jsi::Runtime &rt) {
247
+ work(rt);
248
+ }];
249
+ });
222
250
  };
223
251
  SharedRPC::install(runtime, std::move(mainExecutor), "main");
224
252
  // Set the integration-health flag from inside the JS-thread lambda so
@@ -243,7 +271,13 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
243
271
  startBackgroundRunnerWithEntryURL:entryURL];
244
272
  }];
245
273
  }
246
-
274
+ + (void)installSharedBridgeInMainRuntime:(RCTHost *)host
275
+ thenStartBackgroundRunnerWithDevVendorConfig:(NSDictionary<NSString *, NSString *> *)config {
276
+ [self installSharedBridgeInMainRuntime:host completion:^{
277
+ [[BackgroundThreadManager sharedInstance]
278
+ startBackgroundRunnerWithDevVendorConfig:config];
279
+ }];
280
+ }
247
281
  #pragma mark - Public Methods
248
282
 
249
283
  - (void)startBackgroundRunner {
@@ -256,6 +290,35 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
256
290
  }
257
291
 
258
292
  - (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL {
293
+ [self startBackgroundRunnerWithEntryURL:entryURL devVendorConfig:nil];
294
+ }
295
+
296
+ - (void)startBackgroundRunnerWithDevVendorConfig:(NSDictionary<NSString *, NSString *> *)config {
297
+ NSString *commonBundlePath = config[DEV_VENDOR_COMMON_PATH_KEY];
298
+ NSString *entryURL = config[DEV_VENDOR_ENTRY_URL_KEY];
299
+ NSString *fingerprint = config[DEV_VENDOR_FINGERPRINT_KEY];
300
+ NSString *backgroundHMRValue = config[DEV_VENDOR_BACKGROUND_HMR_KEY];
301
+ NSURL *parsedEntryURL = [NSURL URLWithString:entryURL];
302
+ NSRegularExpression *fingerprintPattern =
303
+ [NSRegularExpression regularExpressionWithPattern:@"^[0-9a-f]{64}$"
304
+ options:0
305
+ error:nil];
306
+ if (commonBundlePath.length == 0 ||
307
+ ![[NSFileManager defaultManager] fileExistsAtPath:commonBundlePath] ||
308
+ parsedEntryURL.host.length == 0 ||
309
+ !([backgroundHMRValue isEqualToString:@"true"] ||
310
+ [backgroundHMRValue isEqualToString:@"false"]) ||
311
+ [fingerprintPattern numberOfMatchesInString:fingerprint
312
+ options:0
313
+ range:NSMakeRange(0, fingerprint.length)] != 1) {
314
+ [BTLogger error:@"Invalid dev-vendor background bootstrap config"];
315
+ return;
316
+ }
317
+ [self startBackgroundRunnerWithEntryURL:entryURL devVendorConfig:config];
318
+ }
319
+
320
+ - (void)startBackgroundRunnerWithEntryURL:(NSString *)entryURL
321
+ devVendorConfig:(NSDictionary<NSString *, NSString *> * _Nullable)devVendorConfig {
259
322
  if (self.isStarted) {
260
323
  // Surface the URL mismatch when a second start... call would
261
324
  // otherwise silently drop the host's intent — most common in the
@@ -278,18 +341,27 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
278
341
  // Updated unconditionally — first-time and re-start with a new URL
279
342
  // both reflect into lastEntryURL.
280
343
  self.lastEntryURL = entryURL;
344
+ self.lastDevVendorConfig = devVendorConfig;
281
345
  [BTLogger info:[NSString stringWithFormat:@"Starting background runner with entryURL: %@", entryURL]];
282
346
 
283
347
  dispatch_async(dispatch_get_main_queue(), ^{
348
+ NSUInteger runtimeGeneration = ++self.backgroundRuntimeGeneration;
284
349
  NSDictionary *initialProperties = @{};
285
350
  NSDictionary *launchOptions = @{};
286
351
  self.reactNativeFactoryDelegate = [[BackgroundReactNativeDelegate alloc] init];
287
352
  self.reactNativeFactory = CreateBackgroundReactNativeFactory(self.reactNativeFactoryDelegate);
288
-
353
+ if (devVendorConfig) {
354
+ [self.reactNativeFactoryDelegate
355
+ configureDevVendorCommonBundlePath:devVendorConfig[DEV_VENDOR_COMMON_PATH_KEY]
356
+ entryURL:[NSURL URLWithString:devVendorConfig[DEV_VENDOR_ENTRY_URL_KEY]]
357
+ fingerprint:devVendorConfig[DEV_VENDOR_FINGERPRINT_KEY]
358
+ backgroundHMREnabled:[devVendorConfig[DEV_VENDOR_BACKGROUND_HMR_KEY] isEqualToString:@"true"]
359
+ runtimeGeneration:runtimeGeneration];
360
+ }
289
361
  // Only set jsBundleSource for debug HTTP URLs or explicit OTA overrides.
290
362
  // Leaving the default release name ("background.bundle") unset lets the
291
363
  // delegate fall back to split-bundle mode (common.bundle + entry).
292
- if (![entryURL isEqualToString:@"background.bundle"]) {
364
+ if (!devVendorConfig && ![entryURL isEqualToString:@"background.bundle"]) {
293
365
  [self.reactNativeFactoryDelegate setJsBundleSource:std::string([entryURL UTF8String])];
294
366
  }
295
367
 
@@ -476,6 +548,20 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
476
548
  {
477
549
  BOOL isUI = [mode isEqualToString:@"ui"];
478
550
  BOOL isAll = [mode isEqualToString:@"all"];
551
+ BOOL isBackgroundHMR = [mode isEqualToString:@"background"];
552
+
553
+ if (isBackgroundHMR) {
554
+ #if DEBUG
555
+ [self restartBackgroundForHMRReason:reason completion:completion];
556
+ #else
557
+ NSError *error = [NSError errorWithDomain:@"BackgroundThread"
558
+ code:12
559
+ userInfo:@{NSLocalizedDescriptionKey:
560
+ @"Background HMR restart is unavailable in release builds"}];
561
+ if (completion) completion(error);
562
+ #endif
563
+ return;
564
+ }
479
565
 
480
566
  if (!isUI && !isAll) {
481
567
  NSError *error = [NSError errorWithDomain:@"BackgroundThread"
@@ -552,6 +638,59 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
552
638
  }
553
639
  }
554
640
 
641
+ - (void)restartBackgroundForHMRReason:(NSString *)reason
642
+ completion:(void (^)(NSError * _Nullable error))completion
643
+ {
644
+ dispatch_async(dispatch_get_main_queue(), ^{
645
+ if (![reason hasPrefix:BACKGROUND_HMR_REASON_PREFIX]) {
646
+ NSError *error = [NSError errorWithDomain:@"BackgroundThread"
647
+ code:13
648
+ userInfo:@{NSLocalizedDescriptionKey:
649
+ @"Background HMR restart is missing its runtime generation"}];
650
+ if (completion) completion(error);
651
+ return;
652
+ }
653
+ NSString *generationAndReason = [reason substringFromIndex:BACKGROUND_HMR_REASON_PREFIX.length];
654
+ NSRange separator = [generationAndReason rangeOfString:@":"];
655
+ NSString *generationText = separator.location == NSNotFound
656
+ ? generationAndReason
657
+ : [generationAndReason substringToIndex:separator.location];
658
+ NSScanner *scanner = [NSScanner scannerWithString:generationText];
659
+ unsigned long long generation = 0;
660
+ if (![scanner scanUnsignedLongLong:&generation] || !scanner.isAtEnd ||
661
+ generation != self.backgroundRuntimeGeneration) {
662
+ [BTLogger info:[NSString stringWithFormat:
663
+ @"[BackgroundHMR] ignored stale restart requested=%@ active=%lu",
664
+ generationText,
665
+ (unsigned long)self.backgroundRuntimeGeneration]];
666
+ if (completion) completion(nil);
667
+ return;
668
+ }
669
+ NSDictionary<NSString *, NSString *> *config = self.lastDevVendorConfig;
670
+ if (![config[DEV_VENDOR_BACKGROUND_HMR_KEY] isEqualToString:@"true"] ||
671
+ self.backgroundHMRRestartInFlight) {
672
+ [BTLogger info:@"[BackgroundHMR] restart coalesced or disabled"];
673
+ if (completion) completion(nil);
674
+ return;
675
+ }
676
+
677
+ self.backgroundHMRRestartInFlight = YES;
678
+ ++self.backgroundRuntimeGeneration;
679
+ SharedRPC::invalidate("background");
680
+ self.reactNativeFactory = nil;
681
+ self.reactNativeFactoryDelegate = nil;
682
+ self.isStarted = NO;
683
+ if (completion) completion(nil);
684
+
685
+ // Leave the outgoing TurboModule callback before creating its replacement.
686
+ dispatch_async(dispatch_get_main_queue(), ^{
687
+ [BTLogger info:@"[BackgroundHMR] starting replacement background runtime"];
688
+ [self startBackgroundRunnerWithDevVendorConfig:config];
689
+ self.backgroundHMRRestartInFlight = NO;
690
+ });
691
+ });
692
+ }
693
+
555
694
  #pragma mark - Restart Health Check
556
695
 
557
696
  // Two-stage post-reload health-check.
@@ -634,6 +773,7 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
634
773
 
635
774
  if (isAll && !strongSelf.isStarted) {
636
775
  NSString *cachedURL = strongSelf.lastEntryURL;
776
+ NSDictionary<NSString *, NSString *> *devVendorConfig = strongSelf.lastDevVendorConfig;
637
777
  // Treat the bundled default name as "no real cache" so the
638
778
  // OTA warn still fires for hosts that initially bootstrapped
639
779
  // with the default URL and haven't yet swapped to an OTA-
@@ -641,7 +781,12 @@ static NSString *const MODULE_DEBUG_URL = @"http://localhost:8082/apps/mobile/ba
641
781
  // lastEntryURL that distinguishes them.
642
782
  BOOL hasCustomCachedURL = cachedURL.length > 0
643
783
  && ![cachedURL isEqualToString:@"background.bundle"];
644
- if (hasCustomCachedURL) {
784
+ if (devVendorConfig) {
785
+ [BTLogger info:[NSString stringWithFormat:
786
+ @"restart(%@): bg not respawned by host AppDelegate; replaying dev-vendor common + delta",
787
+ mode]];
788
+ [strongSelf startBackgroundRunnerWithDevVendorConfig:devVendorConfig];
789
+ } else if (hasCustomCachedURL) {
645
790
  // Preferred path: replay the host's last entry URL. On OTA
646
791
  // devices this is the OTA-resolved bundle, which keeps the
647
792
  // bg moduleId table aligned with the new main bundle and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-background-thread",
3
- "version": "3.0.90",
3
+ "version": "3.0.92",
4
4
  "description": "react-native-background-thread",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",