@@ -36,6 +36,11 @@ @implementation CodePush {
3636 BOOL _allowed;
3737 BOOL _restartInProgress;
3838 NSMutableArray *_restartQueue;
39+
40+ // Reload readiness tracking. See -registerSettleObserver for details.
41+ BOOL _instanceSettled;
42+ // YES exactly while a reload is parked waiting for the instance to settle.
43+ BOOL _reloadPending;
3944}
4045
4146RCT_EXPORT_MODULE ()
@@ -45,6 +50,11 @@ @implementation CodePush {
4550// These constants represent emitted events
4651static NSString *const DownloadProgressEvent = @" CodePushDownloadProgress" ;
4752
53+ // How long a parked reload waits for a settle signal before reloading anyway.
54+ // See -registerSettleObserver for why this exists and why the exact value is
55+ // not critical.
56+ static const NSTimeInterval SettleTimeout = 5.0 ;
57+
4858// These constants represent valid deployment statuses
4959static NSString *const DeploymentFailed = @" DeploymentFailed" ;
5060static NSString *const DeploymentSucceeded = @" DeploymentSucceeded" ;
@@ -395,15 +405,104 @@ - (instancetype)init
395405 _allowed = YES ;
396406 _restartInProgress = NO ;
397407 _restartQueue = [NSMutableArray arrayWithCapacity: 1 ];
398-
408+
399409 self = [super init ];
400410 if (self) {
411+ [self registerSettleObserver ];
401412 [self initializeUpdateAfterRestart ];
402413 }
403414
404415 return self;
405416}
406417
418+ #pragma mark - Immediate update + reload readiness tracking
419+
420+ /*
421+ * In case of an immediate bundle update, we need to tear down the current
422+ * RCTInstance at the right time.
423+ *
424+ * Right after bundle evaluation, RN enqueues -[RCTFabricSurface start] on a
425+ * background queue for that same instance. Tearing the instance down while
426+ * that block is in flight crashes inside RN's mounting layer. This module is
427+ * initialized *during* bundle evaluation, so in case of immediate update
428+ * mode, -loadBundle needs to park the reload until we see the surface get
429+ * past its startup.
430+ *
431+ * The signal we wait for is RCTContentDidAppearNotification. Fabric posts it
432+ * from RCTRootComponentView on the first child mount. That needs a JS render
433+ * and commit, so by that time the surface startup is done.
434+ *
435+ * Note: the signal is not guaranteed, so we also need a fallback timeout. If
436+ * the first render returns nil and mounts no child, RCTContentDidAppear
437+ * never arrives. Think of a splash gate or a fonts/auth loader. That kind of
438+ * app could run codepush.sync() with an IMMEDIATE install before it shows
439+ * any UI.
440+ *
441+ * Running the teardown after the timeout fires is safe because the delay is
442+ * large enough that the surface startup already completed by that time.
443+ */
444+ - (void )registerSettleObserver
445+ {
446+ [[NSNotificationCenter defaultCenter ] addObserver: self
447+ selector: @selector (instanceDidSettle )
448+ name: RCTContentDidAppearNotification
449+ object: nil ];
450+ }
451+
452+ - (void )instanceDidSettle
453+ {
454+ // Note: A reload tears the instance down synchronously.
455+ // Calling -releasePendingReload directly would destroy the surface partway through
456+ // the mount that just notified us.
457+ dispatch_async (dispatch_get_main_queue (), ^{
458+ self->_instanceSettled = YES ;
459+
460+ if (self->_reloadPending ) {
461+ CPLog (@" Instance settled." );
462+ [self releasePendingReload ];
463+ }
464+ });
465+ }
466+
467+ /*
468+ * Fires a parked reload, if one is still parked. Called by the settle signal, by
469+ * the timeout in -loadBundle and by -allow, so whichever arrives first wins. Main queue only.
470+ */
471+ - (void )releasePendingReload
472+ {
473+ if (!_reloadPending) {
474+ return ;
475+ }
476+
477+ if (!_allowed) {
478+ CPLog (@" Restart stays parked because restarts are disallowed." );
479+ return ;
480+ }
481+
482+ CPLog (@" Restarting app." );
483+ _reloadPending = NO ;
484+ [self performBundleReload ];
485+ }
486+
487+ /*
488+ * Cancels a parked reload, if one is parked.
489+ * Main queue only.
490+ */
491+ - (void )cancelPendingReload
492+ {
493+ if (!_reloadPending) {
494+ return ;
495+ }
496+
497+ CPLog (@" Cleared a parked restart." );
498+ _reloadPending = NO ;
499+
500+ // The restart that parked it still holds _restartInProgress, so we clear that too.
501+ // Otherwise every later restart request would queue behind a restart that is
502+ // never going to happen.
503+ _restartInProgress = NO ;
504+ }
505+
407506/*
408507 * This method is used when the app is started to either
409508 * initialize a pending update or rollback a faulty update
@@ -547,18 +646,55 @@ - (void)loadBundle
547646 // This needs to be async dispatched because the bridge is not set on init
548647 // when the app first starts, therefore rollbacks will not take effect.
549648 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" ];
649+ if (!self->_instanceSettled ) {
650+ CPLog (@" Restart deferred until the current instance has settled." );
651+ self->_reloadPending = YES ;
652+
653+ // Weak, so that a module outliving the teardown of its own instance (because of the timer)
654+ // cannot reload against a bridge that no longer belongs to it.
655+ __weak __typeof (self) weakSelf = self;
656+ dispatch_after (dispatch_time (DISPATCH_TIME_NOW , (int64_t )(SettleTimeout * NSEC_PER_SEC )),
657+ dispatch_get_main_queue (), ^{
658+ __typeof (self) strongSelf = weakSelf;
659+ if (!strongSelf) {
660+ return ;
661+ }
662+
663+ // The timeout is long enough that the surface startup has completed by now,
664+ // whether or not the signal ever arrived. Treating the instance as settled
665+ // from here on means a later restart doesn't park and wait all over again,
666+ // which matters for an app that never posts the signal at all.
667+ strongSelf->_instanceSettled = YES ;
668+
669+ if (strongSelf->_reloadPending ) {
670+ CPLog (@" Timed out waiting for the current instance to settle." );
671+ [strongSelf releasePendingReload ];
672+ }
673+ });
674+ return ;
556675 }
557676
558- RCTTriggerReloadCommandListeners ( @" react-native-code-push: Restart " ) ;
677+ [ self performBundleReload ] ;
559678 });
560679}
561680
681+ /*
682+ * Performs the actual reload. Must be called on the main queue, and only once the
683+ * current instance has settled or the wait for it timed out. See -loadBundle.
684+ */
685+ - (void )performBundleReload
686+ {
687+ // If the current bundle URL is using http(s), then assume the dev
688+ // is debugging and therefore, shouldn't be redirected to a local
689+ // file (since Chrome wouldn't support it). Otherwise, update
690+ // the current bundle URL to point at the latest update
691+ if ([CodePush isUsingTestConfiguration ] || ![super .bridge.bundleURL.scheme hasPrefix: @" http" ]) {
692+ [super .bridge setValue: [CodePush bundleURL ] forKey: @" bundleURL" ];
693+ }
694+
695+ RCTTriggerReloadCommandListeners (@" react-native-code-push: Restart" );
696+ }
697+
562698/*
563699 * This method is used when a pending update never finished loading (i.e. it
564700 * crashed before calling notifyApplicationReady) and needs to be rolled back
@@ -804,7 +940,7 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
804940
805941 _restartInProgress = NO ;
806942 if ([_restartQueue count ] > 0 ) {
807- BOOL buf = [_restartQueue valueForKey: @" @ firstObject" ];
943+ BOOL buf = [[ _restartQueue firstObject ] boolValue ];
808944 [_restartQueue removeObjectAtIndex: 0 ];
809945 [self restartAppInternal: buf];
810946 }
@@ -1008,9 +1144,15 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
10081144 CPLog (@" Re-allowing restarts." );
10091145 _allowed = YES ;
10101146
1147+ // A reload parked by -loadBundle never reached _restartQueue, so it needs
1148+ // its own release. See -releasePendingReload.
1149+ dispatch_async (dispatch_get_main_queue (), ^{
1150+ [self releasePendingReload ];
1151+ });
1152+
10111153 if ([_restartQueue count ] > 0 ) {
10121154 CPLog (@" Executing pending restart." );
1013- BOOL buf = [_restartQueue valueForKey: @" @ firstObject" ];
1155+ BOOL buf = [[ _restartQueue firstObject ] boolValue ];
10141156 [_restartQueue removeObjectAtIndex: 0 ];
10151157 [self restartAppInternal: buf];
10161158 }
@@ -1022,6 +1164,11 @@ - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
10221164 rejecter:(RCTPromiseRejectBlock)reject)
10231165{
10241166 [_restartQueue removeAllObjects ];
1167+
1168+ dispatch_async (dispatch_get_main_queue (), ^{
1169+ [self cancelPendingReload ];
1170+ });
1171+
10251172 resolve (nil );
10261173}
10271174
0 commit comments