@@ -15,6 +15,13 @@ type BundleRequestObservation = {
1515 sawPrewarmRequest : boolean ;
1616} ;
1717
18+ class ReadyTimeoutError extends Error {
19+ constructor ( ) {
20+ super ( 'Timed out waiting for the app to become ready after Metro bundling.' ) ;
21+ this . name = 'ReadyTimeoutError' ;
22+ }
23+ }
24+
1825export type WaitForMetroBackedAppReadyOptions = {
1926 metro : MetroInstance ;
2027 platformId : string ;
@@ -98,6 +105,106 @@ const waitForBundleRequest = async ({
98105 } ) ;
99106} ;
100107
108+ const waitForReadyAfterBundleRequest = async ( options : {
109+ events : MetroInstance [ 'events' ] ;
110+ readyTimeout : number ;
111+ signal : AbortSignal ;
112+ waitForReady : ( signal : AbortSignal ) => Promise < void > ;
113+ } ) : Promise < void > => {
114+ const { events, readyTimeout, signal, waitForReady } = options ;
115+
116+ return await new Promise < void > ( ( resolve , reject ) => {
117+ let bundlingInProgress = false ;
118+ let settled = false ;
119+ let timeoutId : ReturnType < typeof setTimeout > | null = null ;
120+ const readyController = new AbortController ( ) ;
121+ const readySignal = raceAbortSignals ( [ signal , readyController . signal ] ) ;
122+
123+ const clearReadyTimer = ( ) => {
124+ if ( timeoutId ) {
125+ clearTimeout ( timeoutId ) ;
126+ timeoutId = null ;
127+ }
128+ } ;
129+
130+ const cleanup = ( ) => {
131+ clearReadyTimer ( ) ;
132+ events . removeListener ( onMetroEvent ) ;
133+ signal . removeEventListener ( 'abort' , onAbort ) ;
134+ } ;
135+
136+ const resolveOnce = ( ) => {
137+ if ( settled ) {
138+ return ;
139+ }
140+
141+ settled = true ;
142+ cleanup ( ) ;
143+ resolve ( ) ;
144+ } ;
145+
146+ const rejectOnce = ( error : unknown ) => {
147+ if ( settled ) {
148+ return ;
149+ }
150+
151+ settled = true ;
152+ cleanup ( ) ;
153+ reject ( error ) ;
154+ } ;
155+
156+ const startReadyTimer = ( ) => {
157+ clearReadyTimer ( ) ;
158+ timeoutId = setTimeout ( ( ) => {
159+ readyController . abort ( new DOMException ( 'The operation was aborted' , 'AbortError' ) ) ;
160+ rejectOnce ( new ReadyTimeoutError ( ) ) ;
161+ } , readyTimeout ) ;
162+ } ;
163+
164+ const onAbort = ( ) => {
165+ rejectOnce ( signal . reason ?? new DOMException ( 'The operation was aborted' , 'AbortError' ) ) ;
166+ } ;
167+
168+ const onMetroEvent = ( event : ReportableEvent ) => {
169+ if ( event . type === 'bundle_build_started' ) {
170+ bundlingInProgress = true ;
171+ clearReadyTimer ( ) ;
172+ return ;
173+ }
174+
175+ if (
176+ bundlingInProgress &&
177+ ( event . type === 'bundle_build_done' ||
178+ event . type === 'bundle_build_failed' )
179+ ) {
180+ bundlingInProgress = false ;
181+ startReadyTimer ( ) ;
182+ }
183+ } ;
184+
185+ startReadyTimer ( ) ;
186+ events . addListener ( onMetroEvent ) ;
187+ signal . addEventListener ( 'abort' , onAbort , { once : true } ) ;
188+
189+ void waitForReady ( readySignal )
190+ . then ( ( ) => {
191+ resolveOnce ( ) ;
192+ } )
193+ . catch ( ( error ) => {
194+ if (
195+ readyController . signal . aborted &&
196+ ! signal . aborted &&
197+ error instanceof DOMException &&
198+ error . name === 'AbortError'
199+ ) {
200+ return ;
201+ }
202+
203+ rejectOnce ( error ) ;
204+ } ) ;
205+ } ) ;
206+ } ;
207+
101208export const waitForMetroBackedAppReady = async ( {
102209 metro,
103210 platformId,
@@ -157,9 +264,12 @@ export const waitForMetroBackedAppReady = async ({
157264 ] ) ;
158265 sawPrewarmRequest = bundleRequestResult . sawPrewarmRequest ;
159266
160- const readyPromise = waitForReady (
161- withAbortTimeout ( attemptSignal , readyTimeout )
162- ) ;
267+ const readyPromise = waitForReadyAfterBundleRequest ( {
268+ events : metro . events ,
269+ readyTimeout,
270+ signal : attemptSignal ,
271+ waitForReady,
272+ } ) ;
163273 await Promise . race ( [ readyPromise , crashPromise ] ) ;
164274 attemptController . abort ( ) ;
165275 onAttemptReset ?.( ) ;
@@ -186,14 +296,18 @@ export const waitForMetroBackedAppReady = async ({
186296 continue ;
187297 }
188298
189- if ( isAbortError ( error ) ) {
299+ if ( error instanceof ReadyTimeoutError ) {
190300 throw new StartupStallError ( readyTimeout , attempt , {
191301 code : 'ready_not_reported' ,
192302 lastMetroStatus,
193303 sawPrewarmRequest,
194304 } ) ;
195305 }
196306
307+ if ( isAbortError ( error ) ) {
308+ throw error ;
309+ }
310+
197311 throw error ;
198312 }
199313 }
0 commit comments