11#if __has_include(<React/RCTAssert.h>)
22#import < React/RCTAssert.h>
33#import < React/RCTBridgeModule.h>
4+ #import < React/RCTConstants.h>
45#import < React/RCTConvert.h>
56#import < React/RCTEventDispatcher.h>
67#import < React/RCTRootView.h>
@@ -36,6 +37,11 @@ @implementation CodePush {
3637 BOOL _allowed;
3738 BOOL _restartInProgress;
3839 NSMutableArray *_restartQueue;
40+
41+ // Reload readiness tracking. See -registerSettleObservers for details.
42+ BOOL _instanceSettled;
43+ // YES exactly while a reload is parked waiting for the instance to settle.
44+ BOOL _reloadPending;
3945}
4046
4147RCT_EXPORT_MODULE ()
@@ -45,6 +51,11 @@ @implementation CodePush {
4551// These constants represent emitted events
4652static NSString *const DownloadProgressEvent = @" CodePushDownloadProgress" ;
4753
54+ // How long a parked reload waits for a settle signal before reloading anyway.
55+ // See -registerSettleObservers for why this exists and why the exact value is
56+ // not critical.
57+ static const NSTimeInterval SettleTimeout = 5.0 ;
58+
4859// These constants represent valid deployment statuses
4960static NSString *const DeploymentFailed = @" DeploymentFailed" ;
5061static NSString *const DeploymentSucceeded = @" DeploymentSucceeded" ;
@@ -395,15 +406,88 @@ - (instancetype)init
395406 _allowed = YES ;
396407 _restartInProgress = NO ;
397408 _restartQueue = [NSMutableArray arrayWithCapacity: 1 ];
398-
409+
399410 self = [super init ];
400411 if (self) {
412+ [self registerSettleObservers ];
401413 [self initializeUpdateAfterRestart ];
402414 }
403415
404416 return self;
405417}
406418
419+ #pragma mark - Immediate update + reload readiness tracking
420+
421+ /*
422+ * In case of an immediate bundle update, we need to tear down the current
423+ * RCTInstance at the right time.
424+ *
425+ * Right after bundle evaluation, RN enqueues -[RCTFabricSurface start] on a
426+ * background queue for that same instance. Tearing the instance down while
427+ * that block is in flight crashes inside RN's mounting layer. This module is
428+ * initialized *during* bundle evaluation, so in case of immediate update
429+ * mode, -loadBundle needs to park the reload until we see the surface get
430+ * past its startup.
431+ *
432+ * The two signals below are the possible outcomes of an instance's startup:
433+ * - RCTContentDidAppearNotification: Fabric posts this from
434+ * RCTRootComponentView on the first child mount. That needs a JS render
435+ * and commit, so by that time we are good to tear things down.
436+ * - RCTJavaScriptDidFailToLoadNotification: the bundle never evaluated, so
437+ * no surface startup will follow.
438+ *
439+ * Note: neither signal is guaranteed, so we also need a fallback timeout. If
440+ * the first render returns nil and mounts no child, RCTContentDidAppear
441+ * never arrives. Think of a splash gate or a fonts/auth loader. That kind of
442+ * app could run codepush.sync() with an IMMEDIATE install before it shows
443+ * any UI.
444+ *
445+ * Running the teardown after the timeout fires is safe because the delay is
446+ * large enough that the teardown already happened by that time.
447+ */
448+ - (void )registerSettleObservers
449+ {
450+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter ];
451+ [center addObserver: self
452+ selector: @selector (instanceDidSettle )
453+ name: RCTContentDidAppearNotification
454+ object: nil ];
455+ [center addObserver: self
456+ selector: @selector (instanceDidSettle )
457+ name: RCTJavaScriptDidFailToLoadNotification
458+ object: nil ];
459+ }
460+
461+ - (void )instanceDidSettle
462+ {
463+ // Note: A reload tears the instance down synchronously.
464+ // Calling -releasePendingReload directly would destroy the surface partway through
465+ // the mount that just notified us.
466+ dispatch_async (dispatch_get_main_queue (), ^{
467+ self->_instanceSettled = YES ;
468+
469+ if (self->_reloadPending ) {
470+ CPLog (@" Instance settled. Restarting app." );
471+ [self releasePendingReload ];
472+ }
473+ });
474+ }
475+
476+ /*
477+ * Fires a parked reload, if one is still parked. Called both by the settle
478+ * signal and by the timeout in -loadBundle, so whichever arrives first wins and
479+ * the other becomes a no-op. Main queue only.
480+ */
481+ - (void )releasePendingReload
482+ {
483+ if (!_reloadPending) {
484+ return ;
485+ }
486+
487+ _reloadPending = NO ;
488+ [self performBundleReload ];
489+ }
490+
407491/*
408492 * This method is used when the app is started to either
409493 * initialize a pending update or rollback a faulty update
@@ -547,18 +631,45 @@ - (void)loadBundle
547631 // This needs to be async dispatched because the bridge is not set on init
548632 // when the app first starts, therefore rollbacks will not take effect.
549633 dispatch_async (dispatch_get_main_queue (), ^{
550- // If the current bundle URL is using http(s), then assume the dev
551- // is debugging and therefore, shouldn't be redirected to a local
552- // file (since Chrome wouldn't support it). Otherwise, update
553- // the current bundle URL to point at the latest update
554- if ([CodePush isUsingTestConfiguration ] || ![super .bridge.bundleURL.scheme hasPrefix: @" http" ]) {
555- [super .bridge setValue: [CodePush bundleURL ] forKey: @" bundleURL" ];
634+ if (!self->_instanceSettled ) {
635+ CPLog (@" Restart deferred until the current instance has settled." );
636+ self->_reloadPending = YES ;
637+
638+ // Weak, so that a module outliving the teardown of its own instance (because of the timer)
639+ // cannot reload against a bridge that no longer belongs to it.
640+ __weak __typeof (self) weakSelf = self;
641+ dispatch_after (dispatch_time (DISPATCH_TIME_NOW , (int64_t )(SettleTimeout * NSEC_PER_SEC )),
642+ dispatch_get_main_queue (), ^{
643+ __typeof (self) strongSelf = weakSelf;
644+ if (strongSelf && strongSelf->_reloadPending ) {
645+ CPLog (@" Timed out waiting for the current instance to settle. Restarting app anyway." );
646+ [strongSelf releasePendingReload ];
647+ }
648+ });
649+ return ;
556650 }
557651
558- RCTTriggerReloadCommandListeners ( @" react-native-code-push: Restart " ) ;
652+ [ self performBundleReload ] ;
559653 });
560654}
561655
656+ /*
657+ * Performs the actual reload. Must be called on the main queue, and only once the
658+ * current instance has settled or the wait for it timed out. See -loadBundle.
659+ */
660+ - (void )performBundleReload
661+ {
662+ // If the current bundle URL is using http(s), then assume the dev
663+ // is debugging and therefore, shouldn't be redirected to a local
664+ // file (since Chrome wouldn't support it). Otherwise, update
665+ // the current bundle URL to point at the latest update
666+ if ([CodePush isUsingTestConfiguration ] || ![super .bridge.bundleURL.scheme hasPrefix: @" http" ]) {
667+ [super .bridge setValue: [CodePush bundleURL ] forKey: @" bundleURL" ];
668+ }
669+
670+ RCTTriggerReloadCommandListeners (@" react-native-code-push: Restart" );
671+ }
672+
562673/*
563674 * This method is used when a pending update never finished loading (i.e. it
564675 * crashed before calling notifyApplicationReady) and needs to be rolled back
@@ -804,7 +915,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
804915
805916 _restartInProgress = NO ;
806917 if ([_restartQueue count ] > 0 ) {
807- BOOL buf = [_restartQueue valueForKey: @" @ firstObject" ];
918+ BOOL buf = [[ _restartQueue firstObject ] boolValue ];
808919 [_restartQueue removeObjectAtIndex: 0 ];
809920 [self restartAppInternal: buf];
810921 }
@@ -1010,7 +1121,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
10101121
10111122 if ([_restartQueue count ] > 0 ) {
10121123 CPLog (@" Executing pending restart." );
1013- BOOL buf = [_restartQueue valueForKey: @" @ firstObject" ];
1124+ BOOL buf = [[ _restartQueue firstObject ] boolValue ];
10141125 [_restartQueue removeObjectAtIndex: 0 ];
10151126 [self restartAppInternal: buf];
10161127 }
0 commit comments