Repository navigation
fix(ios): release the Expo Updates startup gate when the launch already finished - #529
Open
dustinsoftware wants to merge 1 commit into
Open
dustinsoftware wants to merge 1 commit into
dustinsoftware wants to merge 1 commit into
Conversation
…dy finished The gate treated a started controller as ready only when launchAssetUrl() was non-nil, and otherwise waited for the AppControllerDelegate callback. expo-updates sends that callback once per start, and only when a delegate is already set at that moment. A launch that failed before the gate ran (nil launch asset), or one that finished between the gate's first check and `controller.delegate = self`, therefore never released the waiters. A started controller now counts as ready when it has a launch asset or reports a launchDuration, which expo-updates sets in startupProcedureDidLaunch for both normal and emergency launches. The gate checks again after assigning its delegate, and both paths release through one releaseCallbacks().
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues block approval of the iOS Expo Updates startup-race fix.
No open issues 🎉
What changed in this PR
Fixes an iOS Expo Updates startup race that can leave brownfield launches waiting indefinitely when the host-started controller finishes before the startup gate receives its callback. Preserves the Expo 57 safeguard against reading the launch asset before the controller starts.
Changes:
- Detects completed launches even without a launch asset and rechecks after delegate assignment.
- Centralizes waiter release and adds regression cases for failed launches and delegate-registration races.
- Adds a patch changeset for
@callstack/react-native-brownfield.
| File | Description |
|---|---|
packages/react-native-brownfield/tests/ios/run-expo-startup.sh |
Extends the controller stub and tests missed-callback scenarios. |
packages/react-native-brownfield/ios/Expo/ExpoUpdatesStartup.swift |
Detects finished launches and releases pending startup callbacks. |
.changeset/ios-updates-startup-finished-launch.md |
Documents the startup-gate fix for a patch release. |
Review effort: Balanced
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is an upstream port of a patch in our shipping app. The patch works around a race that impacts Expo 57 and 58 using react-native-brownfield, which can sometimes result in a blank screen on launch. As far as I can tell, brownfield 3.6.1 did not have this issue (which is what we were using before). First seen on 5.1.1.
What
ExpoUpdatesStartupnow releases its waiters when a started expo-updates controller has already finished launching, even when there is no launch asset, and re-checks after it assigns its delegate.Why
Once the controller is started, the gate released only on
launchAssetUrl() != nilor on theAppControllerDelegatecallback. expo-updates sends that callback once per start, and only when a delegate is already set (EnabledAppController.startupProcedureDidLaunch, expo-updates 58.0.21EnabledAppController.swift:121-131). Two cases therefore waited forever:main.jsbundle, solaunchAssetUrl()isniland the callback had already been skipped.controller.delegate = self.How
hasFinishedLaunching(_:):launchAssetUrl() != nil, orgetConstantsForModule().toModuleConstantsMap()["launchDuration"]is non-nil. expo-updates setsstartupEndTime, and with itlaunchDuration, at the start ofstartupProcedureDidLaunch, which runs for both normal and emergency launches.isEmergencyLaunchcovers failures only, andisStartupProcedureRunningstays true until the background update check ends, so neither works as the signal.controller.isStarted && hasFinishedLaunching(controller).controller.delegate = self, an already-started controller is checked again and the waiters are released at once if it has finished. If the delegate callback also arrives, it finds no waiters left and does nothing.releaseCallbacks().launchAssetUrl()is still never read beforestart(), so the expo-updates 57 trap fixed in fix(ios): don't read the expo-updates launch asset before the controller starts #508 stays fixed.Test plan
packages/react-native-brownfield/tests/ios/run-expo-startup.sh: the stub gainsgetConstantsForModule()and a hook that runs just before the delegate is assigned. Added cases: launch already failed before the first request; launch finishes (success and failure) before the delegate is assigned. Passes at-Ononeand-O; the same script with themainversion ofExpoUpdatesStartup.swiftfails on the first added case.EnabledAppController(launch time, delegate-only-if-set, emergency launch with and without an embedded bundle):main8/11 (the three cases above hang), this branch 11/11, at-Ononeand-O.swiftc -typecheckof the changed file against a built expo-updates 58.0.21EXUpdatesmodule (iOS simulator, arm64): no errors or warnings.Changeset
.changeset/ios-updates-startup-finished-launch.md: patch for@callstack/react-native-brownfield.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gf7Xx7K4DTaYw2pLT2TJF7